routee_compass/plugin/input/default/inject/
inject_plugin.rs

1use super::{CoordinateOrientation, WriteMode};
2use crate::{
3    app::search::SearchApp,
4    plugin::input::{input_plugin::InputPlugin, InputJsonExtensions, InputPluginError},
5};
6use routee_compass_core::util::geo::PolygonalRTree;
7use serde_json::Value;
8use std::sync::Arc;
9
10pub enum InjectInputPlugin {
11    Basic {
12        key: String,
13        value: serde_json::Value,
14        write_mode: WriteMode,
15    },
16    Spatial {
17        key: String,
18        values: PolygonalRTree<f32, Value>,
19        write_mode: WriteMode,
20        orientation: CoordinateOrientation,
21        default: Option<Value>,
22    },
23}
24
25impl InputPlugin for InjectInputPlugin {
26    fn process(
27        &self,
28        input: &mut serde_json::Value,
29        _search_app: Arc<SearchApp>,
30    ) -> Result<(), InputPluginError> {
31        process_inject(self, input)
32    }
33}
34
35pub fn process_inject(
36    plugin: &InjectInputPlugin,
37    input: &mut serde_json::Value,
38) -> Result<(), InputPluginError> {
39    match plugin {
40        InjectInputPlugin::Basic {
41            key,
42            value,
43            write_mode,
44        } => write_mode.write_to_query(input, key, value),
45        InjectInputPlugin::Spatial {
46            values,
47            key,
48            write_mode,
49            orientation,
50            default,
51        } => {
52            let coord = match orientation {
53                CoordinateOrientation::Origin => input.get_origin_coordinate(),
54                CoordinateOrientation::Destination => match input.get_destination_coordinate() {
55                    Ok(Some(coord)) => Ok(coord),
56                    Ok(None) => Err(InputPluginError::InputPluginFailed(String::from(
57                        "destination-oriented spatial inject plugin but query has no destination",
58                    ))),
59                    Err(e) => Err(e),
60                },
61            }?;
62            let point = geo::Geometry::Point(geo::Point(coord));
63            let mut intersect_iter = values.intersection(&point).map_err(|e| {
64                InputPluginError::InputPluginFailed(format!(
65                    "failure while intersecting spatial inject data: {e}"
66                ))
67            })?;
68            match (intersect_iter.next(), default) {
69                (None, None) => {
70                    // nothing intersects + no default -> NOOP
71                    Ok(())
72                }
73                (None, Some(default_value)) => {
74                    // nothing intersects but we have a default
75                    write_mode.write_to_query(input, key, default_value)
76                }
77                (Some(found), _) => {
78                    // found an intersecting geometry with a value to assign
79                    write_mode.write_to_query(input, key, &found.data)
80                }
81            }
82        }
83    }
84}
85
86#[cfg(test)]
87mod test {
88    use super::{process_inject, InjectInputPlugin};
89    use crate::plugin::input::default::inject::{
90        inject_plugin_config::SpatialInjectPlugin, CoordinateOrientation, InjectPluginConfig,
91        WriteMode,
92    };
93    use config::Config;
94    use itertools::Itertools;
95    use serde_json::{json, Value};
96    use std::path::Path;
97
98    #[test]
99    fn test_kv() {
100        let mut query = json!({});
101        let key = String::from("key_on_query");
102        let value = json![{"k": "v"}];
103        let plugin = InjectInputPlugin::Basic {
104            key: key.clone(),
105            value: value.clone(),
106            write_mode: WriteMode::Overwrite,
107        };
108        process_inject(&plugin, &mut query).expect("test failed");
109        let result_value = query.get(&key).expect("test failed: key was not set");
110        assert_eq!(
111            result_value, &value,
112            "test failed: value stored in GeoJSON with matching location does not match"
113        )
114    }
115
116    #[test]
117    fn test_kv_from_file() {
118        let plugins = test_kv_conf();
119        let result = plugins.iter().fold(json![{}], |mut input, plugin| {
120            process_inject(plugin, &mut input).unwrap();
121            input
122        });
123        let result_string = serde_json::to_string(&result).unwrap();
124        let expected = String::from(
125            r#"{"test_a":{"foo":"bar","baz":"bees"},"test_b":["test",5,3.14159],"test_c":[0,0,0,0]}"#,
126        );
127        assert_eq!(result_string, expected);
128    }
129    #[test]
130    fn test_spatial_contains() {
131        let mut query = json!({
132            "origin_x": -105.11011135094863,
133            "origin_y": 39.83906153425838
134        });
135        let source_key = Some(String::from("key_on_geojson"));
136        let key = String::from("key_on_query");
137        let plugin = setup_spatial(&source_key, &key, &None);
138        process_inject(&plugin, &mut query).expect("test failed");
139        let value = query.get(&key).expect("test failed: key was not set");
140        let value_number = value.as_i64().expect("test failed: value was not a number");
141        assert_eq!(
142            value_number, 5000,
143            "test failed: value stored in GeoJSON with matching location was not injected"
144        )
145    }
146
147    #[test]
148    fn test_spatial_not_contains() {
149        let mut query = json!({
150            "origin_x": -105.07021837975549,
151            "origin_y": 39.93602243844981
152        });
153        let source_key = Some(String::from("key_on_geojson"));
154        let key = String::from("key_on_query");
155        let default = String::from("found default");
156        let plugin = setup_spatial(&source_key, &key, &Some(json![default.clone()]));
157        process_inject(&plugin, &mut query).expect("test failed");
158        let value = query.get(&key).expect("test failed: key was not set");
159        let value_str = value.as_str().expect("test failed: value was not a str");
160        assert_eq!(
161            value_str, &default,
162            "test failed: value stored in GeoJSON with location that does not match did not return default fill value"
163        )
164    }
165
166    #[test]
167    fn test_spatial_not_contains_no_default() {
168        let mut query = json!({
169            "origin_x": -105.07021837975549,
170            "origin_y": 39.93602243844981
171        });
172        let expected = query.clone();
173        let source_key = Some(String::from("key_on_geojson"));
174        let key = String::from("key_on_query");
175        let plugin = setup_spatial(&source_key, &key, &None);
176        process_inject(&plugin, &mut query).expect("test failed");
177        assert_eq!(
178            query, expected,
179            "test failed: process should be idempotent when the query is not contained and there is no default value"
180        )
181    }
182
183    #[test]
184    fn test_spatial_from_json() {
185        let source_key = String::from("key_on_geojson");
186        let key = String::from("key_on_query");
187        let filepath = if cfg!(target_os = "windows") {
188            // Escape backslashes for Windows before adding to JSON
189            test_geojson_filepath().replace("\\", "\\\\")
190        } else {
191            // Use the path as-is for non-Windows systems
192            test_geojson_filepath()
193        };
194        let conf_str = format!(
195            r#"
196        {{
197            "type": "inject",
198            "format": "spatial_key_value",
199            "spatial_input_file": "{}",
200            "source_key": "{}",
201            "key": "{}",
202            "write_mode": "overwrite",
203            "orientation": "origin"
204        }}
205        "#,
206            filepath, &source_key, &key
207        );
208        let conf: InjectPluginConfig =
209            serde_json::from_str(&conf_str).expect("failed to decode configuration");
210        let plugin = conf.build().expect("failed to build plugin");
211        let mut query = json!({
212            "origin_x": -105.11011135094863,
213            "origin_y": 39.83906153425838
214        });
215
216        process_inject(&plugin, &mut query).expect("failed to run plugin");
217        let value = query.get(&key).expect("test failed: key was not set");
218        let value_number = value.as_i64().expect("test failed: value was not a number");
219        assert_eq!(
220            value_number, 5000,
221            "test failed: value stored in GeoJSON with matching location was not injected"
222        )
223    }
224
225    fn test_geojson_filepath() -> String {
226        let spatial_input_filepath = Path::new(env!("CARGO_MANIFEST_DIR"))
227            .join("src")
228            .join("plugin")
229            .join("input")
230            .join("default")
231            .join("inject")
232            .join("test")
233            .join("test.geojson");
234        let path_str = spatial_input_filepath
235            .to_str()
236            .expect("test invariant failed: unable to convert filepath to string");
237        path_str.to_string()
238    }
239
240    fn test_kv_conf() -> Vec<InjectInputPlugin> {
241        let kv_conf_filepath = Path::new(env!("CARGO_MANIFEST_DIR"))
242            .join("src")
243            .join("plugin")
244            .join("input")
245            .join("default")
246            .join("inject")
247            .join("test")
248            .join("test_inject.toml");
249        let conf_source = config::File::from(kv_conf_filepath);
250
251        let config_toml = Config::builder()
252            .add_source(conf_source)
253            .build()
254            .expect("test invariant failed");
255        let config_json = config_toml
256            .clone()
257            .try_deserialize::<serde_json::Value>()
258            .expect("test invariant failed");
259        let input_plugin_array = config_json
260            .get("input_plugin")
261            .expect("TOML file should have an 'input_plugin' key")
262            .clone();
263        let array = input_plugin_array
264            .as_array()
265            .expect("key input_plugin should be an array");
266        let plugins = array
267            .iter()
268            .map(|conf| {
269                let ipc = serde_json::from_value::<InjectPluginConfig>(conf.clone())
270                    .unwrap_or_else(|_| {
271                        panic!(
272                            "'input_plugin' entry should be valid: {}",
273                            serde_json::to_string(&conf).unwrap_or_default()
274                        )
275                    });
276                ipc.build().unwrap_or_else(|_| {
277                    panic!(
278                        "InjectPluginConfig.build failed: {}",
279                        serde_json::to_string(&conf).unwrap_or_default()
280                    )
281                })
282            })
283            .collect_vec();
284        plugins
285    }
286
287    fn setup_spatial(
288        source_key: &Option<String>,
289        key: &str,
290        default: &Option<Value>,
291    ) -> InjectInputPlugin {
292        let spatial_input_file = test_geojson_filepath();
293        let conf = InjectPluginConfig::SpatialKeyValue(SpatialInjectPlugin {
294            spatial_input_file,
295            source_key: source_key.clone(),
296            key: key.to_owned(),
297            write_mode: WriteMode::Overwrite,
298            orientation: CoordinateOrientation::Origin,
299            default: default.clone(),
300        });
301
302        conf.build().expect("test invariant failed")
303    }
304}