reliakit_json/
primitives.rs1use alloc::string::ToString;
30use alloc::vec;
31use core::fmt;
32
33use reliakit_primitives::PrimitiveError;
34
35use crate::error::{JsonPath, JsonPathSegment};
36use crate::{JsonObject, JsonValue};
37
38#[derive(Debug, Clone, PartialEq, Eq)]
41#[non_exhaustive]
42pub enum JsonExtractErrorKind {
43 Missing,
45 WrongType {
47 expected: &'static str,
49 },
50 Invalid(PrimitiveError),
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct JsonExtractError {
58 path: JsonPath,
59 kind: JsonExtractErrorKind,
60}
61
62impl JsonExtractError {
63 pub fn path(&self) -> &JsonPath {
65 &self.path
66 }
67
68 pub fn kind(&self) -> &JsonExtractErrorKind {
70 &self.kind
71 }
72}
73
74impl fmt::Display for JsonExtractError {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 match &self.kind {
77 JsonExtractErrorKind::Missing => write!(f, "{}: required value is missing", self.path),
78 JsonExtractErrorKind::WrongType { expected } => {
79 write!(f, "{}: expected a JSON {expected}", self.path)
80 }
81 JsonExtractErrorKind::Invalid(err) => write!(f, "{}: {err}", self.path),
82 }
83 }
84}
85
86#[cfg(feature = "std")]
87impl std::error::Error for JsonExtractError {}
88
89fn key_path(key: &str) -> JsonPath {
91 JsonPath::from_segments(vec![JsonPathSegment::Key(key.to_string())])
92}
93
94impl JsonObject {
95 pub fn get_str_as<'a, T>(&'a self, key: &str) -> Result<T, JsonExtractError>
103 where
104 T: TryFrom<&'a str, Error = PrimitiveError>,
105 {
106 let value = self.get(key).ok_or_else(|| JsonExtractError {
107 path: key_path(key),
108 kind: JsonExtractErrorKind::Missing,
109 })?;
110 match value.as_str() {
111 None => Err(JsonExtractError {
112 path: key_path(key),
113 kind: JsonExtractErrorKind::WrongType { expected: "string" },
114 }),
115 Some(text) => T::try_from(text).map_err(|err| JsonExtractError {
116 path: key_path(key),
117 kind: JsonExtractErrorKind::Invalid(err),
118 }),
119 }
120 }
121}
122
123impl JsonValue {
124 pub fn str_as<'a, T>(&'a self) -> Result<T, JsonExtractError>
133 where
134 T: TryFrom<&'a str, Error = PrimitiveError>,
135 {
136 match self.as_str() {
137 None => Err(JsonExtractError {
138 path: JsonPath::default(),
139 kind: JsonExtractErrorKind::WrongType { expected: "string" },
140 }),
141 Some(text) => T::try_from(text).map_err(|err| JsonExtractError {
142 path: JsonPath::default(),
143 kind: JsonExtractErrorKind::Invalid(err),
144 }),
145 }
146 }
147}
148
149#[cfg(all(test, feature = "primitives"))]
150mod tests {
151 use super::{JsonExtractErrorKind, PrimitiveError};
152 use crate::parse_str;
153 use reliakit_primitives::{Email, Hostname};
154
155 fn obj(input: &str) -> crate::JsonObject {
156 parse_str(input).unwrap().as_object().unwrap().clone()
157 }
158
159 #[test]
160 fn extracts_valid_string_primitive() {
161 let o = obj(r#"{ "email": "ops@example.com", "host": "api.example.com" }"#);
162 let email: Email = o.get_str_as("email").unwrap();
163 assert_eq!(email.as_str(), "ops@example.com");
164 let host: Hostname = o.get_str_as("host").unwrap();
165 assert_eq!(host.as_str(), "api.example.com");
166 }
167
168 #[test]
169 fn missing_key_reports_missing_with_path() {
170 let o = obj(r#"{ "host": "api.example.com" }"#);
171 let err = o.get_str_as::<Email>("email").unwrap_err();
172 assert_eq!(err.kind(), &JsonExtractErrorKind::Missing);
173 assert_eq!(err.path().to_string(), "$.email");
174 assert_eq!(err.to_string(), "$.email: required value is missing");
175 }
176
177 #[test]
178 fn wrong_json_type_reports_wrong_type() {
179 let o = obj(r#"{ "email": 42 }"#);
180 let err = o.get_str_as::<Email>("email").unwrap_err();
181 assert_eq!(
182 err.kind(),
183 &JsonExtractErrorKind::WrongType { expected: "string" }
184 );
185 assert_eq!(err.to_string(), "$.email: expected a JSON string");
186 }
187
188 #[test]
189 fn invalid_value_wraps_primitive_error_with_path() {
190 let o = obj(r#"{ "email": "not-an-email" }"#);
191 let err = o.get_str_as::<Email>("email").unwrap_err();
192 assert!(matches!(
193 err.kind(),
194 JsonExtractErrorKind::Invalid(PrimitiveError::Invalid { .. })
195 ));
196 assert!(err.to_string().starts_with("$.email: "));
197 }
198
199 #[test]
200 fn value_str_as_uses_root_path() {
201 let doc = parse_str(r#""ops@example.com""#).unwrap();
202 let email: Email = doc.str_as().unwrap();
203 assert_eq!(email.as_str(), "ops@example.com");
204
205 let num = parse_str("42").unwrap();
206 let err = num.str_as::<Email>().unwrap_err();
207 assert_eq!(err.path().to_string(), "$");
208 assert_eq!(
209 err.kind(),
210 &JsonExtractErrorKind::WrongType { expected: "string" }
211 );
212 }
213}