use alloc::string::ToString;
use alloc::vec;
use core::fmt;
use reliakit_primitives::PrimitiveError;
use crate::error::{JsonPath, JsonPathSegment};
use crate::{JsonObject, JsonValue};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum JsonExtractErrorKind {
Missing,
WrongType {
expected: &'static str,
},
Invalid(PrimitiveError),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JsonExtractError {
path: JsonPath,
kind: JsonExtractErrorKind,
}
impl JsonExtractError {
pub fn path(&self) -> &JsonPath {
&self.path
}
pub fn kind(&self) -> &JsonExtractErrorKind {
&self.kind
}
}
impl fmt::Display for JsonExtractError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.kind {
JsonExtractErrorKind::Missing => write!(f, "{}: required value is missing", self.path),
JsonExtractErrorKind::WrongType { expected } => {
write!(f, "{}: expected a JSON {expected}", self.path)
}
JsonExtractErrorKind::Invalid(err) => write!(f, "{}: {err}", self.path),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for JsonExtractError {}
fn key_path(key: &str) -> JsonPath {
JsonPath::from_segments(vec![JsonPathSegment::Key(key.to_string())])
}
impl JsonObject {
pub fn get_str_as<'a, T>(&'a self, key: &str) -> Result<T, JsonExtractError>
where
T: TryFrom<&'a str, Error = PrimitiveError>,
{
let value = self.get(key).ok_or_else(|| JsonExtractError {
path: key_path(key),
kind: JsonExtractErrorKind::Missing,
})?;
match value.as_str() {
None => Err(JsonExtractError {
path: key_path(key),
kind: JsonExtractErrorKind::WrongType { expected: "string" },
}),
Some(text) => T::try_from(text).map_err(|err| JsonExtractError {
path: key_path(key),
kind: JsonExtractErrorKind::Invalid(err),
}),
}
}
}
impl JsonValue {
pub fn str_as<'a, T>(&'a self) -> Result<T, JsonExtractError>
where
T: TryFrom<&'a str, Error = PrimitiveError>,
{
match self.as_str() {
None => Err(JsonExtractError {
path: JsonPath::default(),
kind: JsonExtractErrorKind::WrongType { expected: "string" },
}),
Some(text) => T::try_from(text).map_err(|err| JsonExtractError {
path: JsonPath::default(),
kind: JsonExtractErrorKind::Invalid(err),
}),
}
}
}
#[cfg(all(test, feature = "primitives"))]
mod tests {
use super::{JsonExtractErrorKind, PrimitiveError};
use crate::parse_str;
use reliakit_primitives::{Email, Hostname};
fn obj(input: &str) -> crate::JsonObject {
parse_str(input).unwrap().as_object().unwrap().clone()
}
#[test]
fn extracts_valid_string_primitive() {
let o = obj(r#"{ "email": "ops@example.com", "host": "api.example.com" }"#);
let email: Email = o.get_str_as("email").unwrap();
assert_eq!(email.as_str(), "ops@example.com");
let host: Hostname = o.get_str_as("host").unwrap();
assert_eq!(host.as_str(), "api.example.com");
}
#[test]
fn missing_key_reports_missing_with_path() {
let o = obj(r#"{ "host": "api.example.com" }"#);
let err = o.get_str_as::<Email>("email").unwrap_err();
assert_eq!(err.kind(), &JsonExtractErrorKind::Missing);
assert_eq!(err.path().to_string(), "$.email");
assert_eq!(err.to_string(), "$.email: required value is missing");
}
#[test]
fn wrong_json_type_reports_wrong_type() {
let o = obj(r#"{ "email": 42 }"#);
let err = o.get_str_as::<Email>("email").unwrap_err();
assert_eq!(
err.kind(),
&JsonExtractErrorKind::WrongType { expected: "string" }
);
assert_eq!(err.to_string(), "$.email: expected a JSON string");
}
#[test]
fn invalid_value_wraps_primitive_error_with_path() {
let o = obj(r#"{ "email": "not-an-email" }"#);
let err = o.get_str_as::<Email>("email").unwrap_err();
assert!(matches!(
err.kind(),
JsonExtractErrorKind::Invalid(PrimitiveError::Invalid { .. })
));
assert!(err.to_string().starts_with("$.email: "));
}
#[test]
fn value_str_as_uses_root_path() {
let doc = parse_str(r#""ops@example.com""#).unwrap();
let email: Email = doc.str_as().unwrap();
assert_eq!(email.as_str(), "ops@example.com");
let num = parse_str("42").unwrap();
let err = num.str_as::<Email>().unwrap_err();
assert_eq!(err.path().to_string(), "$");
assert_eq!(
err.kind(),
&JsonExtractErrorKind::WrongType { expected: "string" }
);
}
}