Skip to main content

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