1use crate::compiler::prelude::*;
2use crate::stdlib::json_utils::bom::StripBomFromUTF8;
3use crate::stdlib::json_utils::json_type_def::json_type_def;
4
5fn parse_yaml(value: Value) -> Resolved {
6 Ok(serde_yaml_ng::from_slice(value.try_bytes()?.strip_bom())
7 .map_err(|e| format!("unable to parse yaml: {e}"))?)
8}
9
10#[derive(Clone, Copy, Debug)]
11pub struct ParseYaml;
12
13impl Function for ParseYaml {
14 fn identifier(&self) -> &'static str {
15 "parse_yaml"
16 }
17
18 fn summary(&self) -> &'static str {
19 "parse a string to a YAML type"
20 }
21
22 fn usage(&self) -> &'static str {
23 indoc! {"
24 Parses the provided `value` as YAML.
25
26 Only YAML types are returned. If you need to convert a `string` into a `timestamp`,
27 consider the `parse_timestamp` function.
28 "}
29 }
30
31 fn category(&self) -> &'static str {
32 Category::Parse.as_ref()
33 }
34
35 fn internal_failure_reasons(&self) -> &'static [&'static str] {
36 &["`value` is not a valid YAML-formatted payload."]
37 }
38
39 fn return_kind(&self) -> u16 {
40 kind::BOOLEAN
41 | kind::INTEGER
42 | kind::FLOAT
43 | kind::BYTES
44 | kind::OBJECT
45 | kind::ARRAY
46 | kind::NULL
47 }
48
49 fn notices(&self) -> &'static [&'static str] {
50 &[indoc! {"
51 Only YAML types are returned. If you need to convert a `string` into a `timestamp`,
52 consider the [`parse_timestamp`](#parse_timestamp) function.
53 "}]
54 }
55
56 fn parameters(&self) -> &'static [Parameter] {
57 const PARAMETERS: &[Parameter] = &[Parameter::required(
58 "value",
59 kind::BYTES,
60 "The string representation of the YAML to parse.",
61 )];
62
63 PARAMETERS
64 }
65
66 fn examples(&self) -> &'static [Example] {
67 &[
68 example! {
69 title: "Parse simple YAML",
70 source: r"parse_yaml!(s'key: val')",
71 result: Ok(r#"{ "key": "val" }"#),
72 },
73 example! {
74 title: "Parse YAML",
75 source: r#"parse_yaml!(s'
76 object:
77 string: value
78 number: 42
79 boolean: false
80 array:
81 - hello
82 - world
83 json_array: ["hello", "world"]
84 ')"#,
85 result: Ok(r#"{
86 "object": {
87 "string": "value",
88 "number": 42,
89 "boolean": false,
90 "array": [
91 "hello",
92 "world"
93 ],
94 "json_array": [
95 "hello",
96 "world"
97 ]
98 }
99 }"#),
100 },
101 example! {
102 title: "Parse YAML string",
103 source: r"parse_yaml!(s'hello')",
104 result: Ok("hello"),
105 },
106 example! {
107 title: "Parse YAML quoted string",
108 source: r#"parse_yaml!(s'"hello"')"#,
109 result: Ok("hello"),
110 },
111 example! {
112 title: "Parse YAML integer",
113 source: r#"parse_yaml!("42")"#,
114 result: Ok("42"),
115 },
116 example! {
117 title: "Parse YAML float",
118 source: r#"parse_yaml!("42.13")"#,
119 result: Ok("42.13"),
120 },
121 example! {
122 title: "Parse YAML boolean",
123 source: r#"parse_yaml!("false")"#,
124 result: Ok("false"),
125 },
126 example! {
127 title: "Parse embedded JSON",
128 source: r#"parse_yaml!(s'{"key": "val"}')"#,
129 result: Ok(r#"{ "key": "val" }"#),
130 },
131 example! {
132 title: "Parse embedded JSON array",
133 source: r#"parse_yaml!("[true, 0]")"#,
134 result: Ok("[true, 0]"),
135 },
136 ]
137 }
138
139 fn compile(
140 &self,
141 _state: &state::TypeState,
142 _ctx: &mut FunctionCompileContext,
143 arguments: ArgumentList,
144 ) -> Compiled {
145 let value = arguments.required("value");
146
147 Ok(ParseYamlFn { value }.as_expr())
148 }
149}
150
151#[derive(Debug, Clone)]
152struct ParseYamlFn {
153 value: Box<dyn Expression>,
154}
155
156impl FunctionExpression for ParseYamlFn {
157 fn resolve(&self, ctx: &mut Context) -> Resolved {
158 let value = self.value.resolve(ctx)?;
159 parse_yaml(value)
160 }
161
162 fn type_def(&self, _: &state::TypeState) -> TypeDef {
163 json_type_def()
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use crate::value;
171
172 test_function![
173 parse_yaml => ParseYaml;
174
175 parses {
176 args: func_args![ value: r"
177 field: value
178 " ],
179 want: Ok(value!({ field: "value" })),
180 tdef: json_type_def(),
181 }
182
183 complex_yaml {
184 args: func_args![ value: r#"
185 object:
186 string: value
187 number: 42
188 boolean: false
189 array:
190 - hello
191 - world
192 json_array: ["hello", "world"]
193 "# ],
194 want: Ok(value!({ object: {string: "value", number: 42, boolean: false, array: ["hello", "world"], json_array: ["hello", "world"]} })),
195 tdef: json_type_def(),
196 }
197
198 parses_json {
199 args: func_args![ value: r#"{"field": "value"}"# ],
200 want: Ok(value!({ field: "value" })),
201 tdef: json_type_def(),
202 }
203
204 complex_json {
205 args: func_args![ value: r#"{"object": {"string":"value","number":42,"array":["hello","world"],"boolean":false}}"# ],
206 want: Ok(value!({ object: {string: "value", number: 42, array: ["hello", "world"], boolean: false} })),
207 tdef: json_type_def(),
208 }
209
210 incomplete_json_errors {
211 args: func_args![ value: r#"{"field": "value"# ],
212 want: Err(
213 r"unable to parse yaml: found unexpected end of stream at line 1 column 17, while scanning a quoted scalar at line 1 column 11"
214 ),
215 tdef: json_type_def(),
216 }
217 ];
218}