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    mut 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([], Shape::one(one_shapes, empty()), input_shape.locations)
134        }
135        ShapeCase::String(_) => Shape::string(input_shape.locations),
136        ShapeCase::Name(_, _) => input_shape, // TODO: add a way to validate inputs after name resolution
137        ShapeCase::Unknown => Shape::unknown(input_shape.locations),
138        _ => Shape::error(
139            format!(
140                "Method ->{} requires an array or string input",
141                method_name.as_ref()
142            ),
143            {
144                input_shape
145                    .locations
146                    .extend(method_name.shape_location(context.source_id()));
147                input_shape.locations
148            },
149        ),
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use serde_json_bytes::json;
156
157    use crate::selection;
158
159    #[test]
160    fn slice_should_grab_parts_of_array_by_specified_indices() {
161        assert_eq!(
162            selection!("$->slice(1, 3)").apply_to(&json!([1, 2, 3, 4, 5])),
163            (Some(json!([2, 3])), vec![]),
164        );
165    }
166
167    #[test]
168    fn slice_should_stop_at_end_when_array_is_shorter_than_specified_end_index() {
169        assert_eq!(
170            selection!("$->slice(1, 3)").apply_to(&json!([1, 2])),
171            (Some(json!([2])), vec![]),
172        );
173    }
174
175    #[test]
176    fn slice_should_return_empty_array_when_array_is_shorter_than_specified_indices() {
177        assert_eq!(
178            selection!("$->slice(1, 3)").apply_to(&json!([1])),
179            (Some(json!([])), vec![]),
180        );
181    }
182
183    #[test]
184    fn slice_should_return_empty_array_when_provided_empty_array() {
185        assert_eq!(
186            selection!("$->slice(1, 3)").apply_to(&json!([])),
187            (Some(json!([])), vec![]),
188        );
189    }
190
191    #[test]
192    fn slice_should_return_blank_when_string_is_empty() {
193        assert_eq!(
194            selection!("$->slice(1, 3)").apply_to(&json!("")),
195            (Some(json!("")), vec![]),
196        );
197    }
198
199    #[test]
200    fn slice_should_return_part_of_string() {
201        assert_eq!(
202            selection!("$->slice(1, 3)").apply_to(&json!("hello")),
203            (Some(json!("el")), vec![]),
204        );
205    }
206
207    #[test]
208    fn slice_should_return_part_of_string_when_slice_indices_are_larger_than_string() {
209        assert_eq!(
210            selection!("$->slice(1, 3)").apply_to(&json!("he")),
211            (Some(json!("e")), vec![]),
212        );
213    }
214
215    #[test]
216    fn slice_should_return_empty_string_when_indices_are_completely_out_of_string_bounds() {
217        assert_eq!(
218            selection!("$->slice(1, 3)").apply_to(&json!("h")),
219            (Some(json!("")), vec![]),
220        );
221    }
222}