1use crate::compiler::prelude::*;
2use crate::path::{OwnedSegment, OwnedValuePath};
3
4fn set(path: Value, mut value: Value, data: Value) -> Resolved {
5 let path = match path {
6 Value::Array(segments) => {
7 let mut insert = OwnedValuePath::root();
8
9 for segment in segments {
10 let segment = match segment {
11 Value::Bytes(path) => {
12 OwnedSegment::Field(String::from_utf8_lossy(&path).into())
13 }
14 #[allow(clippy::cast_possible_truncation)] Value::Integer(index) => OwnedSegment::Index(index as isize),
16 value => {
17 return Err(format!(
18 "path segment must be either string or integer, not {}",
19 value.kind()
20 )
21 .into());
22 }
23 };
24
25 insert.push_segment(segment);
26 }
27
28 insert
29 }
30 value => {
31 return Err(ValueError::Expected {
32 got: value.kind(),
33 expected: Kind::array(Collection::any()) | Kind::bytes(),
34 }
35 .into());
36 }
37 };
38 value.insert(&path, data);
39 Ok(value)
40}
41
42#[derive(Clone, Copy, Debug)]
43pub struct Set;
44
45impl Function for Set {
46 fn identifier(&self) -> &'static str {
47 "set"
48 }
49
50 fn usage(&self) -> &'static str {
51 indoc! {"
52 Dynamically insert data into the path of a given object or array.
53
54 If you know the path you want to assign a value to,
55 use static path assignments such as `.foo.bar[1] = true` for
56 improved performance and readability. However, if you do not
57 know the path names, use the dynamic `set` function to
58 insert the data into the object or array.
59 "}
60 }
61
62 fn category(&self) -> &'static str {
63 Category::Path.as_ref()
64 }
65
66 fn internal_failure_reasons(&self) -> &'static [&'static str] {
67 &["The `path` segment must be a string or an integer."]
68 }
69
70 fn return_kind(&self) -> u16 {
71 kind::OBJECT | kind::ARRAY
72 }
73
74 fn parameters(&self) -> &'static [Parameter] {
75 const PARAMETERS: &[Parameter] = &[
76 Parameter::required(
77 "value",
78 kind::OBJECT | kind::ARRAY,
79 "The object or array to insert data into.",
80 ),
81 Parameter::required(
82 "path",
83 kind::ARRAY,
84 "An array of path segments to insert the value into.",
85 ),
86 Parameter::required("data", kind::ANY, "The data to be inserted."),
87 ];
88 PARAMETERS
89 }
90
91 fn examples(&self) -> &'static [Example] {
92 &[
93 example! {
94 title: "Single-segment top-level field",
95 source: r#"set!(value: { "foo": "bar" }, path: ["foo"], data: "baz")"#,
96 result: Ok(r#"{ "foo": "baz" }"#),
97 },
98 example! {
99 title: "Multi-segment nested field",
100 source: r#"set!(value: { "foo": { "bar": "baz" } }, path: ["foo", "bar"], data: "qux")"#,
101 result: Ok(r#"{ "foo": { "bar": "qux" } }"#),
102 },
103 example! {
104 title: "Array",
105 source: r#"set!(value: ["foo", "bar", "baz"], path: [-2], data: 42)"#,
106 result: Ok(r#"["foo", 42, "baz"]"#),
107 },
108 example! {
109 title: "Nested fields",
110 source: r#"set!(value: {}, path: ["foo", "bar"], data: "baz")"#,
111 result: Ok(r#"{ "foo": { "bar" : "baz" } }"#),
112 },
113 example! {
114 title: "Nested indexing",
115 source: r#"set!(value: {"foo": { "bar": [] }}, path: ["foo", "bar", 1], data: "baz")"#,
116 result: Ok(r#"{ "foo": { "bar": [null, "baz"] } }"#),
117 },
118 example! {
119 title: "External target",
120 source: r#"set!(value: ., path: ["bar"], data: "baz")"#,
121 input: r#"{ "foo": true }"#,
122 result: Ok(r#"{ "foo": true, "bar": "baz" }"#),
123 },
124 example! {
125 title: "Variable",
126 source: indoc! {r#"
127 var = { "foo": true }
128 set!(value: var, path: ["bar"], data: "baz")
129 "#},
130 result: Ok(r#"{ "foo": true, "bar": "baz" }"#),
131 },
132 example! {
133 title: "Invalid indexing",
134 source: r#"set!(value: [], path: ["foo"], data: "baz")"#,
135 result: Ok(r#"{ "foo": "baz" }"#),
136 },
137 example! {
138 title: "Invalid segment type",
139 source: r#"set!({"foo": { "bar": [92, 42] }}, ["foo", true], "baz")"#,
140 result: Err(
141 r#"function call error for "set" at (0:56): path segment must be either string or integer, not boolean"#,
142 ),
143 },
144 ]
145 }
146
147 fn compile(
148 &self,
149 _state: &state::TypeState,
150 _ctx: &mut FunctionCompileContext,
151 arguments: ArgumentList,
152 ) -> Compiled {
153 let value = arguments.required("value");
154 let path = arguments.required("path");
155 let data = arguments.required("data");
156
157 Ok(SetFn { value, path, data }.as_expr())
158 }
159}
160
161#[derive(Debug, Clone)]
162pub(crate) struct SetFn {
163 value: Box<dyn Expression>,
164 path: Box<dyn Expression>,
165 data: Box<dyn Expression>,
166}
167
168impl FunctionExpression for SetFn {
169 fn resolve(&self, ctx: &mut Context) -> Resolved {
170 let path = self.path.resolve(ctx)?;
171 let value = self.value.resolve(ctx)?;
172 let data = self.data.resolve(ctx)?;
173
174 set(path, value, data)
175 }
176
177 fn type_def(&self, state: &state::TypeState) -> TypeDef {
178 let value_td = self.value.type_def(state);
179
180 let mut td = TypeDef::from(Kind::never()).fallible();
181
182 if value_td.is_array() {
183 td = td.or_array(Collection::any());
184 }
185
186 if value_td.is_object() {
187 td = td.or_object(Collection::any());
188 }
189
190 td
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use crate::value;
198
199 test_function![
200 set => Set;
201
202 array {
203 args: func_args![value: value!([]), path: vec![0], data: true],
204 want: Ok(vec![true]),
205 tdef: TypeDef::array(Collection::any()).fallible(),
206 }
207
208 object {
209 args: func_args![value: value!({}), path: vec!["foo"], data: true],
210 want: Ok(value!({ "foo": true })),
211 tdef: TypeDef::object(Collection::any()).fallible(),
212 }
213 ];
214}