Skip to main content

apollo_federation/connectors/json_selection/methods/public/
slice.rs

1use std::iter::empty;
2
3use serde_json_bytes::Value as JSON;
4use shape::Shape;
5use shape::ShapeCase;
6
7use crate::connectors::json_selection::ApplyToError;
8use crate::connectors::json_selection::ApplyToInternal;
9use crate::connectors::json_selection::MethodArgs;
10use crate::connectors::json_selection::ShapeContext;
11use crate::connectors::json_selection::VarsWithPathsMap;
12use crate::connectors::json_selection::immutable::InputPath;
13use crate::connectors::json_selection::location::Ranged;
14use crate::connectors::json_selection::location::WithRange;
15use crate::connectors::spec::ConnectSpec;
16use crate::impl_arrow_method;
17
18impl_arrow_method!(SliceMethod, slice_method, slice_shape);
19/// Extracts part of an array given a set of indices and returns a new array.
20/// Can also be used on a string to get chars at the specified indices.
21/// The simplest possible example:
22///
23/// $->echo([0,1,2,3,4,5])->slice(1, 3)     would result in [1,2]
24/// $->echo("hello")->slice(1,3)            would result in "el"
25fn slice_method(
26    method_name: &WithRange<String>,
27    method_args: Option<&MethodArgs>,
28    data: &JSON,
29    vars: &VarsWithPathsMap,
30    input_path: &InputPath<JSON>,
31    spec: ConnectSpec,
32) -> (Option<JSON>, Vec<ApplyToError>) {
33    let length = if let JSON::Array(array) = data {
34        array.len() as i64
35    } else if let JSON::String(s) = data {
36        s.as_str().len() as i64
37    } else {
38        return (
39            None,
40            vec![ApplyToError::new(
41                format!(
42                    "Method ->{} requires an array or string input",
43                    method_name.as_ref()
44                ),
45                input_path.to_vec(),
46                method_name.range(),
47                spec,
48            )],
49        );
50    };
51
52    if let Some(MethodArgs { args, .. }) = method_args {
53        let mut errors = Vec::new();
54
55        let start = args
56            .first()
57            .and_then(|arg| {
58                let (value_opt, apply_errors) = arg.apply_to_path(data, vars, input_path, spec);
59                errors.extend(apply_errors);
60                value_opt
61            })
62            .and_then(|n| n.as_i64())
63            .unwrap_or(0)
64            .max(0)
65            .min(length) as usize;
66
67        let end = args
68            .get(1)
69            .and_then(|arg| {
70                let (value_opt, apply_errors) = arg.apply_to_path(data, vars, input_path, spec);
71                errors.extend(apply_errors);
72                value_opt
73            })
74            .and_then(|n| n.as_i64())
75            .unwrap_or(length)
76            .max(0)
77            .min(length) as usize;
78
79        let array = match data {
80            JSON::Array(array) => {
81                if end - start > 0 {
82                    JSON::Array(
83                        array
84                            .iter()
85                            .skip(start)
86                            .take(end - start)
87                            .cloned()
88                            .collect(),
89                    )
90                } else {
91                    JSON::Array(Vec::new())
92                }
93            }
94
95            JSON::String(s) => {
96                if end - start > 0 {
97                    JSON::String(s.as_str()[start..end].to_string().into())
98                } else {
99                    JSON::String("".to_string().into())
100                }
101            }
102
103            _ => unreachable!(),
104        };
105
106        (Some(array), errors)
107    } else {
108        // TODO Should calling ->slice or ->slice() without arguments be an
109        // error? In JavaScript, array->slice() copies the array, but that's not
110        // so useful in an immutable value-typed language like JSONSelection.
111        (Some(data.clone()), Vec::new())
112    }
113}
114#[allow(dead_code)] // method type-checking disabled until we add name resolution
115fn slice_shape(
116    context: &ShapeContext,
117    method_name: &WithRange<String>,
118    _method_args: Option<&MethodArgs>,
119    input_shape: Shape,
120    _dollar_shape: Shape,
121) -> Shape {
122    // There are more clever shapes we could compute here (when start and end
123    // are statically known integers and input_shape is an array or string with
124    // statically known prefix elements, for example) but for now we play it
125    // safe (and honest) by returning a new variable-length array whose element
126    // shape is a union of the original element (prefix and tail) shapes.
127    match input_shape.case() {
128        ShapeCase::Array { prefix, tail } => {
129            let mut one_shapes = prefix.clone();
130            if !tail.is_none() {
131                one_shapes.push(tail.clone());
132            }
133            Shape::array(
134                [],
135                Shape::one(one_shapes, empty()),
136                input_shape.locations().cloned(),
137            )
138        }
139        ShapeCase::String(_) => Shape::string(input_shape.locations().cloned()),
140        ShapeCase::Name(_, _) => input_shape, // TODO: add a way to validate inputs after name resolution
141        ShapeCase::Unknown => Shape::unknown(input_shape.locations().cloned()),
142        _ => Shape::error(
143            format!(
144                "Method ->{} requires an array or string input",
145                method_name.as_ref()
146            ),
147            input_shape
148                .locations()
149                .cloned()
150                .chain(method_name.shape_location(context.source_id())),
151        ),
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use serde_json_bytes::json;
158
159    use crate::selection;
160
161    #[test]
162    fn slice_should_grab_parts_of_array_by_specified_indices() {
163        assert_eq!(
164            selection!("$->slice(1, 3)").apply_to(&json!([1, 2, 3, 4, 5])),
165            (Some(json!([2, 3])), vec![]),
166        );
167    }
168
169    #[test]
170    fn slice_should_stop_at_end_when_array_is_shorter_than_specified_end_index() {
171        assert_eq!(
172            selection!("$->slice(1, 3)").apply_to(&json!([1, 2])),
173            (Some(json!([2])), vec![]),
174        );
175    }
176
177    #[test]
178    fn slice_should_return_empty_array_when_array_is_shorter_than_specified_indices() {
179        assert_eq!(
180            selection!("$->slice(1, 3)").apply_to(&json!([1])),
181            (Some(json!([])), vec![]),
182        );
183    }
184
185    #[test]
186    fn slice_should_return_empty_array_when_provided_empty_array() {
187        assert_eq!(
188            selection!("$->slice(1, 3)").apply_to(&json!([])),
189            (Some(json!([])), vec![]),
190        );
191    }
192
193    #[test]
194    fn slice_should_return_blank_when_string_is_empty() {
195        assert_eq!(
196            selection!("$->slice(1, 3)").apply_to(&json!("")),
197            (Some(json!("")), vec![]),
198        );
199    }
200
201    #[test]
202    fn slice_should_return_part_of_string() {
203        assert_eq!(
204            selection!("$->slice(1, 3)").apply_to(&json!("hello")),
205            (Some(json!("el")), vec![]),
206        );
207    }
208
209    #[test]
210    fn slice_should_return_part_of_string_when_slice_indices_are_larger_than_string() {
211        assert_eq!(
212            selection!("$->slice(1, 3)").apply_to(&json!("he")),
213            (Some(json!("e")), vec![]),
214        );
215    }
216
217    #[test]
218    fn slice_should_return_empty_string_when_indices_are_completely_out_of_string_bounds() {
219        assert_eq!(
220            selection!("$->slice(1, 3)").apply_to(&json!("h")),
221            (Some(json!("")), vec![]),
222        );
223    }
224}