1use crate::compiler::function::EnumVariant;
2use crate::compiler::prelude::*;
3use crate::value;
4
5static VARIANT_ENUM: &[EnumVariant] = &[
6 EnumVariant {
7 value: "object",
8 description: "JSON object - {}",
9 },
10 EnumVariant {
11 value: "array",
12 description: "JSON array - []",
13 },
14 EnumVariant {
15 value: "string",
16 description: "JSON-formatted string values wrapped with quote marks",
17 },
18 EnumVariant {
19 value: "number",
20 description: "Integer or float numbers",
21 },
22 EnumVariant {
23 value: "bool",
24 description: "True or false",
25 },
26 EnumVariant {
27 value: "null",
28 description: "Exact null value",
29 },
30];
31
32static PARAMETERS: &[Parameter] = &[
33 Parameter::required(
34 "value",
35 kind::BYTES,
36 "The value to check if it is a valid JSON document.",
37 ),
38 Parameter::optional(
39 "variant",
40 kind::BYTES,
41 "The variant of the JSON type to explicitly check for.",
42 )
43 .enum_variants(VARIANT_ENUM),
44];
45
46fn is_json(value: Value) -> Resolved {
47 let bytes = value.try_bytes()?;
48
49 match serde_json::from_slice::<'_, serde::de::IgnoredAny>(&bytes) {
50 Ok(_) => Ok(value!(true)),
51 Err(_) => Ok(value!(false)),
52 }
53}
54
55fn is_json_with_variant(value: Value, variant: &Bytes) -> Resolved {
56 let bytes = value.try_bytes()?;
57
58 if serde_json::from_slice::<'_, serde::de::IgnoredAny>(&bytes).is_ok() {
59 for c in bytes {
60 return match c {
61 b' ' | b'\n' | b'\t' | b'\r' => continue,
63 b'{' => Ok(value!(variant.as_ref() == b"object")),
64 b'[' => Ok(value!(variant.as_ref() == b"array")),
65 b't' | b'f' => Ok(value!(variant.as_ref() == b"bool")),
66 b'-' | b'0'..=b'9' => Ok(value!(variant.as_ref() == b"number")),
67 b'"' => Ok(value!(variant.as_ref() == b"string")),
68 b'n' => Ok(value!(variant.as_ref() == b"null")),
69 _ => break,
70 };
71 }
72 }
73
74 Ok(value!(false))
75}
76
77fn variants() -> Vec<Value> {
78 vec![
79 value!("object"),
80 value!("array"),
81 value!("bool"),
82 value!("number"),
83 value!("string"),
84 value!("null"),
85 ]
86}
87
88#[derive(Clone, Copy, Debug)]
89pub struct IsJson;
90
91impl Function for IsJson {
92 fn identifier(&self) -> &'static str {
93 "is_json"
94 }
95
96 fn usage(&self) -> &'static str {
97 "Check if the string is a valid JSON document."
98 }
99
100 fn category(&self) -> &'static str {
101 Category::Type.as_ref()
102 }
103
104 fn return_kind(&self) -> u16 {
105 kind::BOOLEAN
106 }
107
108 fn return_rules(&self) -> &'static [&'static str] {
109 &[
110 "Returns `true` if `value` is a valid JSON document.",
111 "Returns `false` if `value` is not JSON-formatted.",
112 ]
113 }
114
115 fn parameters(&self) -> &'static [Parameter] {
116 PARAMETERS
117 }
118
119 fn examples(&self) -> &'static [Example] {
120 &[
121 example! {
122 title: "Valid JSON object",
123 source: r#"is_json("{}")"#,
124 result: Ok("true"),
125 },
126 example! {
127 title: "Non-valid value",
128 source: r#"is_json("{")"#,
129 result: Ok("false"),
130 },
131 example! {
132 title: "Exact variant",
133 source: r#"is_json("{}", variant: "object")"#,
134 result: Ok("true"),
135 },
136 example! {
137 title: "Non-valid exact variant",
138 source: r#"is_json("{}", variant: "array")"#,
139 result: Ok("false"),
140 },
141 example! {
142 title: "Valid JSON string",
143 source: r#"is_json(s'"test"')"#,
144 result: Ok("true"),
145 },
146 ]
147 }
148
149 fn compile(
150 &self,
151 state: &state::TypeState,
152 _ctx: &mut FunctionCompileContext,
153 arguments: ArgumentList,
154 ) -> Compiled {
155 let value = arguments.required("value");
156 let variant = arguments.optional_enum("variant", &variants(), state)?;
157
158 match variant {
159 Some(raw_variant) => {
160 let variant = raw_variant
161 .try_bytes()
162 .map_err(|e| Box::new(e) as Box<dyn DiagnosticMessage>)?;
163 Ok(IsJsonVariantsFn { value, variant }.as_expr())
164 }
165 None => Ok(IsJsonFn { value }.as_expr()),
166 }
167 }
168}
169
170#[derive(Clone, Debug)]
171struct IsJsonFn {
172 value: Box<dyn Expression>,
173}
174
175impl FunctionExpression for IsJsonFn {
176 fn resolve(&self, ctx: &mut Context) -> Resolved {
177 let value = self.value.resolve(ctx)?;
178 is_json(value)
179 }
180
181 fn type_def(&self, _: &state::TypeState) -> TypeDef {
182 TypeDef::boolean().infallible()
183 }
184}
185
186#[derive(Clone, Debug)]
187struct IsJsonVariantsFn {
188 value: Box<dyn Expression>,
189 variant: Bytes,
190}
191
192impl FunctionExpression for IsJsonVariantsFn {
193 fn resolve(&self, ctx: &mut Context) -> Resolved {
194 let value = self.value.resolve(ctx)?;
195 let variant = &self.variant;
196
197 is_json_with_variant(value, variant)
198 }
199
200 fn type_def(&self, _: &state::TypeState) -> TypeDef {
201 TypeDef::boolean().infallible()
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 test_function![
210 is_json => IsJson;
211
212 object {
213 args: func_args![value: "{}"],
214 want: Ok(value!(true)),
215 tdef: TypeDef::boolean().infallible(),
216 }
217
218 string {
219 args: func_args![value: r#""test""#],
220 want: Ok(value!(true)),
221 tdef: TypeDef::boolean().infallible(),
222 }
223
224 invalid {
225 args: func_args![value: "}{"],
226 want: Ok(value!(false)),
227 tdef: TypeDef::boolean().infallible(),
228 }
229
230 exact_variant {
231 args: func_args![value: "{}", variant: "object"],
232 want: Ok(value!(true)),
233 tdef: TypeDef::boolean().infallible(),
234 }
235
236 exact_variant_invalid {
237 args: func_args![value: "123", variant: "null"],
238 want: Ok(value!(false)),
239 tdef: TypeDef::boolean().infallible(),
240 }
241
242 variant_with_spaces {
243 args: func_args![value: " []", variant: "array"],
244 want: Ok(value!(true)),
245 tdef: TypeDef::boolean().infallible(),
246 }
247
248 invalid_variant {
249 args: func_args![value: "[]", variant: "invalid-variant"],
250 want: Err(r#"invalid enum variant""#),
251 tdef: TypeDef::boolean().infallible(),
252 }
253
254 invalid_variant_type {
255 args: func_args![value: "[]", variant: 100],
256 want: Err(r#"invalid enum variant""#),
257 tdef: TypeDef::boolean().infallible(),
258 }
259 ];
260}