diff --git a/Cargo.lock b/Cargo.lock index c15dcd2..29dcf99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,9 +61,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" [[package]] name = "autocfg" @@ -73,9 +73,9 @@ checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "autotools" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8da1805e028a172334c3b680f93e71126f2327622faef2ec3d893c0a4ad77" +checksum = "ef941527c41b0fc0dd48511a8154cd5fc7e29200a0ff8b7203c5d777dbc795cf" dependencies = [ "cc", ] @@ -117,9 +117,9 @@ checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "cc" -version = "1.0.90" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" [[package]] name = "cexpr" @@ -221,9 +221,9 @@ dependencies = [ [[package]] name = "either" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" [[package]] name = "endian-type" @@ -350,7 +350,7 @@ dependencies = [ [[package]] name = "jnv" -version = "0.2.1" +version = "0.2.2" dependencies = [ "anyhow", "clap", @@ -385,7 +385,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -412,9 +412,9 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "minimal-lexical" @@ -484,9 +484,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.17" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3928fb5db768cb86f891ff014f0144589297e3c6a1aba6ed7cecfdace270c7" +checksum = "5ac2cf0f2e4f42b49f5ffd07dae8d746508ef7526c13940e5f524012ae6c6550" dependencies = [ "proc-macro2", "syn", @@ -494,18 +494,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] [[package]] name = "promkit" -version = "0.3.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d06099a0a47b6bd7414d6692596b754a12ec4537fc46b72c7363a88fee66d9" +checksum = "25c21f295a5492afa96e0635c1c8d9f0bcb1d485c5494f40a150a3e11cd026a4" dependencies = [ "anyhow", "crossterm", @@ -513,15 +513,14 @@ dependencies = [ "radix_trie", "serde", "serde_json", - "thiserror", "unicode-width", ] [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -616,18 +615,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" dependencies = [ "proc-macro2", "quote", @@ -636,9 +635,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.115" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ "indexmap", "itoa", @@ -675,9 +674,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -690,15 +689,15 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "strsim" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.55" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", @@ -719,18 +718,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", @@ -829,7 +828,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -849,17 +848,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] @@ -870,9 +870,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -882,9 +882,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -894,9 +894,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" @@ -906,9 +912,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -918,9 +924,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -930,9 +936,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -942,6 +948,6 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" diff --git a/Cargo.toml b/Cargo.toml index 2b39a57..b2a2756 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jnv" -version = "0.2.1" +version = "0.2.2" authors = ["ynqa "] edition = "2021" description = "JSON navigator and interactive filter leveraging jq" @@ -9,11 +9,11 @@ license = "MIT" readme = "README.md" [dependencies] -anyhow = "1.0.80" +anyhow = "1.0.82" clap = { version = "4.5.4", features = ["derive"] } gag = "1.0.0" j9 = "0.1.3" -promkit = "0.3.3" +promkit = "0.4.0" radix_trie = "0.2.1" # The profile that 'cargo dist' will build with diff --git a/src/jnv.rs b/src/jnv.rs index 4ab8ebf..dcf5825 100644 --- a/src/jnv.rs +++ b/src/jnv.rs @@ -1,4 +1,4 @@ -use std::{cell::RefCell, collections::HashSet, rc::Rc}; +use std::cell::RefCell; use anyhow::Result; use gag::Gag; @@ -9,48 +9,114 @@ use promkit::{ style::{Attribute, Attributes, Color, ContentStyle}, }, json::{self, JsonNode, JsonPathSegment, JsonStream}, - keymap::KeymapManager, listbox, + pane::Pane, serde_json::{self, Deserializer}, snapshot::Snapshot, style::StyleBuilder, suggest::Suggest, - text, text_editor, Prompt, PromptSignal, Renderer, + switch::ActiveKeySwitcher, + text, text_editor, PaneFactory, Prompt, PromptSignal, }; +use crate::trie::FilterTrie; + mod keymap; -mod render; -mod trie; -use trie::QueryTrie; + +/// Deserializes a JSON string into a vector of `serde_json::Value`. +/// +/// This function takes a JSON string as input and attempts to parse it into a vector +/// of `serde_json::Value`, which represents any valid JSON value (e.g., object, array, string, number). +/// It leverages `serde_json::Deserializer` to parse the string and collect the results. +/// +/// # Arguments +/// * `json_str` - A string slice that holds the JSON data to be deserialized. +/// +/// # Returns +/// An `anyhow::Result` wrapping a vector of `serde_json::Value`. On success, it contains the parsed +/// JSON data. On failure, it contains an error detailing what went wrong during parsing. +fn deserialize_json(json_str: &str) -> anyhow::Result> { + Deserializer::from_str(json_str) + .into_iter::() + .map(|res| res.map_err(anyhow::Error::from)) + .collect::>>() +} + +fn run_jq(query: &str, json_stream: &[serde_json::Value]) -> anyhow::Result> { + // libjq writes to the console when an internal error occurs. + // + // e.g. + // ``` + // let _ = j9::run(". | select(.number == invalid_no_quote)", "{}"); + // jq: error: invalid_no_quote/0 is not defined at , line 1: + // . | select(.number == invalid_no_quote) + // ``` + // + // While errors themselves are not an issue, + // they interfere with the console output handling mechanism + // in promkit and qjq (e.g., causing line numbers to shift). + // Therefore, we'll ignore console output produced inside j9::run. + // + // It's possible that this could be handled + // within github.com/ynqa/j9, but for now, + // we'll proceed with this workaround. + // + // For reference, the functionality of a quiet mode in libjq is + // also being discussed at https://github.com/jqlang/jq/issues/1225. + let ignore_err = Gag::stderr().unwrap(); + let mut jq_ret = Vec::::new(); + for v in json_stream.iter() { + let inner_ret: Vec = match j9::run(query, &v.to_string()) { + Ok(ret) => ret, + Err(e) => { + return Err(anyhow::anyhow!(e)); + } + }; + jq_ret.extend(inner_ret); + } + drop(ignore_err); + Ok(jq_ret) +} pub struct Jnv { - input_json_stream: Vec, - expand_depth: Option, - no_hint: bool, + input_stream: Vec, - query_editor_renderer: text_editor::Renderer, - hint_message_renderer: text::Renderer, + // Keybindings + keymap: RefCell>, + + // For Rendering + filter_editor: Snapshot, + hint_message: Snapshot, + suggestions: listbox::State, + json: json::State, + + // Store the filter history + trie: FilterTrie, + // Store the filter suggestions suggest: Suggest, - suggest_renderer: listbox::Renderer, - json_renderer: json::Renderer, - keymap: KeymapManager, + + json_expand_depth: Option, + no_hint: bool, } impl Jnv { pub fn try_new( - input_json_str: String, - expand_depth: Option, + input: String, + filter_editor: text_editor::State, + hint_message: text::State, + suggestions: listbox::State, + json_theme: json::Theme, + json_expand_depth: Option, no_hint: bool, - edit_mode: text_editor::Mode, - indent: usize, - suggestion_list_length: usize, - ) -> Result { - let stream = deserialize_json(&input_json_str)?; - let all_kinds = JsonStream::new(stream.clone(), None).flatten_kinds(); - let suggestions = all_kinds - .iter() - .filter_map(|kind| kind.path()) - .map(|segments| { + ) -> Result> { + let input_stream = deserialize_json(&input)?; + + let mut trie = FilterTrie::default(); + trie.insert(".", input_stream.clone()); + + let all_kinds = JsonStream::new(input_stream.clone(), None).flatten_kinds(); + let suggest = Suggest::from_iter(all_kinds.iter().filter_map(|kind| kind.path()).map( + |segments| { if segments.is_empty() { ".".to_string() } else { @@ -75,265 +141,185 @@ impl Jnv { }) .collect::() } - }); - - Ok(Self { - input_json_stream: stream.clone(), - expand_depth, - no_hint, - query_editor_renderer: text_editor::Renderer { - texteditor: Default::default(), - history: Default::default(), - prefix: String::from("❯❯ "), - mask: Default::default(), - prefix_style: StyleBuilder::new().fgc(Color::Blue).build(), - active_char_style: StyleBuilder::new().bgc(Color::Magenta).build(), - inactive_char_style: StyleBuilder::new().build(), - edit_mode, - word_break_chars: HashSet::from(['.', '|', '(', ')', '[', ']']), - lines: Default::default(), }, - hint_message_renderer: text::Renderer { - text: Default::default(), - style: StyleBuilder::new() - .fgc(Color::Green) - .attrs(Attributes::from(Attribute::Bold)) - .build(), - }, - suggest: Suggest::from_iter(suggestions), - suggest_renderer: listbox::Renderer { - listbox: listbox::Listbox::from_iter(Vec::::new()), - cursor: String::from("❯ "), - active_item_style: StyleBuilder::new() - .fgc(Color::Grey) - .bgc(Color::Yellow) - .build(), - inactive_item_style: StyleBuilder::new().fgc(Color::Grey).build(), - lines: Some(suggestion_list_length), - }, - keymap: KeymapManager::new("default", self::keymap::default) - .register("on_suggest", self::keymap::on_suggest), - json_renderer: json::Renderer { - stream: JsonStream::new(stream, expand_depth), - theme: json::Theme { - curly_brackets_style: StyleBuilder::new() - .attrs(Attributes::from(Attribute::Bold)) - .build(), - square_brackets_style: StyleBuilder::new() - .attrs(Attributes::from(Attribute::Bold)) - .build(), - key_style: StyleBuilder::new().fgc(Color::Cyan).build(), - string_value_style: StyleBuilder::new().fgc(Color::Green).build(), - number_value_style: StyleBuilder::new().build(), - boolean_value_style: StyleBuilder::new().build(), - null_value_style: StyleBuilder::new().fgc(Color::Grey).build(), - active_item_attribute: Attribute::Bold, - inactive_item_attribute: Attribute::Dim, - lines: Default::default(), - indent, + )); + + Ok(Prompt { + renderer: Self { + keymap: RefCell::new( + ActiveKeySwitcher::new("default", self::keymap::default as keymap::Keymap) + .register("on_suggest", self::keymap::on_suggest), + ), + filter_editor: Snapshot::::new(filter_editor), + hint_message: Snapshot::::new(hint_message), + suggestions, + json: json::State { + stream: JsonStream::new(input_stream.clone(), json_expand_depth), + theme: json_theme, }, + trie, + suggest, + json_expand_depth, + no_hint, + input_stream, }, }) } - fn update_hint_message( - &self, - renderer: &mut self::render::Renderer, - text: String, - style: ContentStyle, - ) { + fn update_hint_message(&mut self, text: String, style: ContentStyle) { if !self.no_hint { - renderer - .hint_message_snapshot + self.hint_message .after_mut() - .replace(text::Renderer { text, style }) + .replace(text::State { text, style }) } } +} + +impl promkit::Finalizer for Jnv { + type Return = String; + + fn finalize(&self) -> anyhow::Result { + Ok(self + .filter_editor + .after() + .texteditor + .text_without_cursor() + .to_string()) + } +} - fn evaluate( - &self, - event: &Event, - renderer: &mut Box, - trie: RefCell, - ) -> promkit::Result { - let renderer = self::render::Renderer::cast_mut(renderer.as_mut())?; - let signal = match renderer.keymap.get() { - Some(f) => f(event, renderer), - None => Ok(PromptSignal::Quit), - }?; - let completed = renderer - .query_editor_snapshot +impl promkit::Renderer for Jnv { + fn create_panes(&self, width: u16, height: u16) -> Vec { + vec![ + self.filter_editor.create_pane(width, height), + self.hint_message.create_pane(width, height), + self.suggestions.create_pane(width, height), + self.json.create_pane(width, height), + ] + } + + fn evaluate(&mut self, event: &Event) -> anyhow::Result { + let keymap = *self.keymap.borrow_mut().get(); + let signal = keymap(event, self); + let filter = self + .filter_editor .after() .texteditor .text_without_cursor() .to_string(); - if completed - != renderer - .query_editor_snapshot + // Check if the query has changed + if filter + != self + .filter_editor .borrow_before() .texteditor .text_without_cursor() .to_string() { - renderer.hint_message_snapshot.reset_after_to_init(); - - // libjq writes to the console when an internal error occurs. - // - // e.g. - // ``` - // let _ = j9::run(". | select(.number == invalid_no_quote)", "{}"); - // jq: error: invalid_no_quote/0 is not defined at , line 1: - // . | select(.number == invalid_no_quote) - // ``` - // - // While errors themselves are not an issue, - // they interfere with the console output handling mechanism - // in promkit and qjq (e.g., causing line numbers to shift). - // Therefore, we'll ignore console output produced inside j9::run. - // - // It's possible that this could be handled - // within github.com/ynqa/j9, but for now, - // we'll proceed with this workaround. - // - // For reference, the functionality of a quiet mode in libjq is - // also being discussed at https://github.com/jqlang/jq/issues/1225. - let ignore_err = Gag::stderr().unwrap(); + self.hint_message.reset_after_to_init(); - let mut flatten_ret = Vec::::new(); - for v in &self.input_json_stream { - let inner_ret: Vec = match j9::run(&completed, &v.to_string()) { - Ok(ret) => ret, - Err(_e) => { - self.update_hint_message( - renderer, - format!("Failed to execute jq query '{}'", &completed), - StyleBuilder::new() - .fgc(Color::Red) - .attrs(Attributes::from(Attribute::Bold)) - .build(), - ); - if let Some(searched) = trie.borrow().prefix_search_value(&completed) { - renderer.json_snapshot.after_mut().stream = - JsonStream::new(searched.clone(), self.expand_depth); - } - return Ok(signal); - } - }; - flatten_ret.extend(inner_ret); - } - drop(ignore_err); - - if flatten_ret.is_empty() { - self.update_hint_message( - renderer, - format!( - "JSON query ('{}') was executed, but no results were returned.", - &completed - ), - StyleBuilder::new() - .fgc(Color::Red) - .attrs(Attributes::from(Attribute::Bold)) - .build(), - ); - if let Some(searched) = trie.borrow().prefix_search_value(&completed) { - renderer.json_snapshot.after_mut().stream = - JsonStream::new(searched.clone(), self.expand_depth); + match self.trie.exact_search(&filter) { + Some(jsonl) => { + self.json.stream = JsonStream::new(jsonl.clone(), self.json_expand_depth); + self.update_hint_message( + format!( + "JSON query ('{}') was already executed. Result was retrieved from cache.", + &filter + ), + StyleBuilder::new() + .fgc(Color::DarkGrey) + .attrs(Attributes::from(Attribute::Bold)) + .build(), + ); } - } else { - match deserialize_json(&flatten_ret.join("\n")) { - Ok(jsonl) => { - let stream = JsonStream::new(jsonl.clone(), self.expand_depth); + None => { + match run_jq(&filter, &self.input_stream) { + Ok(ret) => { + if ret.is_empty() { + self.update_hint_message( + format!( + "JSON query ('{}') was executed, but no results were returned.", + &filter + ), + StyleBuilder::new() + .fgc(Color::Red) + .attrs(Attributes::from(Attribute::Bold)) + .build(), + ); + if let Some(searched) = self.trie.prefix_search(&filter) { + self.json.stream = + JsonStream::new(searched.clone(), self.json_expand_depth); + } + } else { + match deserialize_json(&ret.join("\n")) { + Ok(jsonl) => { + let stream = + JsonStream::new(jsonl.clone(), self.json_expand_depth); - let is_null = stream - .roots() - .iter() - .all(|node| node == &JsonNode::Leaf(serde_json::Value::Null)); - if is_null { + let is_null = stream.roots().iter().all(|node| { + node == &JsonNode::Leaf(serde_json::Value::Null) + }); + if is_null { + self.update_hint_message( + format!("JSON query resulted in 'null', which may indicate a typo or incorrect query: '{}'", &filter), + StyleBuilder::new() + .fgc(Color::Yellow) + .attrs(Attributes::from(Attribute::Bold)) + .build(), + ); + if let Some(searched) = self.trie.prefix_search(&filter) + { + self.json.stream = JsonStream::new( + searched.clone(), + self.json_expand_depth, + ); + } + } else { + // SUCCESS! + self.trie.insert(&filter, jsonl); + self.json.stream = stream; + } + } + Err(e) => { + self.update_hint_message( + format!( + "Failed to parse query result for viewing: {}", + e + ), + StyleBuilder::new() + .fgc(Color::Red) + .attrs(Attributes::from(Attribute::Bold)) + .build(), + ); + if let Some(searched) = self.trie.prefix_search(&filter) { + self.json.stream = JsonStream::new( + searched.clone(), + self.json_expand_depth, + ); + } + } + } + } + } + Err(_) => { self.update_hint_message( - renderer, - format!("JSON query resulted in 'null', which may indicate a typo or incorrect query: '{}'", &completed), + format!("Failed to execute jq query '{}'", &filter), StyleBuilder::new() - .fgc(Color::Yellow) + .fgc(Color::Red) .attrs(Attributes::from(Attribute::Bold)) .build(), ); - if let Some(searched) = trie.borrow().prefix_search_value(&completed) { - renderer.json_snapshot.after_mut().stream = - JsonStream::new(searched.clone(), self.expand_depth); + if let Some(searched) = self.trie.prefix_search(&filter) { + self.json.stream = + JsonStream::new(searched.clone(), self.json_expand_depth); } - } else { - // SUCCESS! - trie.borrow_mut().insert(&completed, jsonl); - renderer.json_snapshot.after_mut().stream = stream; - } - } - Err(e) => { - self.update_hint_message( - renderer, - format!("Failed to parse query result for viewing: {}", e), - StyleBuilder::new() - .fgc(Color::Red) - .attrs(Attributes::from(Attribute::Bold)) - .build(), - ); - if let Some(searched) = trie.borrow().prefix_search_value(&completed) { - renderer.json_snapshot.after_mut().stream = - JsonStream::new(searched.clone(), self.expand_depth); + return signal; } } } - // flatten_ret.is_empty() } - // before != completed } - Ok(signal) + signal } - - pub fn prompt(self) -> Result> { - let rc_self = Rc::new(RefCell::new(self)); - let rc_self_clone = rc_self.clone(); - - let keymap_clone = rc_self_clone.borrow().keymap.clone(); - let query_editor_renderer_clone = rc_self_clone.borrow().query_editor_renderer.clone(); - let hint_message_renderer_clone = rc_self_clone.borrow().hint_message_renderer.clone(); - let suggest_clone = rc_self_clone.borrow().suggest.clone(); - let suggest_renderer_clone = rc_self_clone.borrow().suggest_renderer.clone(); - let json_renderer_clone = rc_self_clone.borrow().json_renderer.clone(); - Ok(Prompt::try_new( - Box::new(self::render::Renderer { - keymap: keymap_clone, - query_editor_snapshot: Snapshot::::new( - query_editor_renderer_clone, - ), - hint_message_snapshot: Snapshot::::new(hint_message_renderer_clone), - suggest: suggest_clone, - suggest_snapshot: Snapshot::::new(suggest_renderer_clone), - json_snapshot: Snapshot::::new(json_renderer_clone), - }), - Box::new( - move |event: &Event, - renderer: &mut Box| - -> promkit::Result { - let trie = RefCell::new(QueryTrie::default()); - rc_self_clone.borrow().evaluate(event, renderer, trie) - }, - ), - |renderer: &(dyn Renderer + '_)| -> promkit::Result { - Ok(self::render::Renderer::cast(renderer)? - .query_editor_snapshot - .after() - .texteditor - .text_without_cursor() - .to_string()) - }, - )?) - } -} - -fn deserialize_json(json_str: &str) -> Result> { - Deserializer::from_str(json_str) - .into_iter::() - .map(|res| res.map_err(anyhow::Error::new)) - .collect::>>() } diff --git a/src/jnv/keymap.rs b/src/jnv/keymap.rs index 34d3f61..b87c170 100644 --- a/src/jnv/keymap.rs +++ b/src/jnv/keymap.rs @@ -1,13 +1,13 @@ use promkit::{ crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers}, listbox::Listbox, - text_editor, PromptSignal, Result, + text_editor, PromptSignal, }; -pub fn default(event: &Event, renderer: &mut crate::jnv::render::Renderer) -> Result { - let query_editor_after_mut = renderer.query_editor_snapshot.after_mut(); - let suggest_after_mut = renderer.suggest_snapshot.after_mut(); - let json_after_mut = renderer.json_snapshot.after_mut(); +pub type Keymap = fn(&Event, &mut crate::jnv::Jnv) -> anyhow::Result; + +pub fn default(event: &Event, jnv: &mut crate::jnv::Jnv) -> anyhow::Result { + let filter_editor = jnv.filter_editor.after_mut(); match event { Event::Key(KeyEvent { @@ -16,19 +16,16 @@ pub fn default(event: &Event, renderer: &mut crate::jnv::render::Renderer) -> Re kind: KeyEventKind::Press, state: KeyEventState::NONE, }) => { - let query = query_editor_after_mut - .texteditor - .text_without_cursor() - .to_string(); - if let Some(mut candidates) = renderer.suggest.prefix_search(query) { + let query = filter_editor.texteditor.text_without_cursor().to_string(); + if let Some(mut candidates) = jnv.suggest.prefix_search(query) { candidates.sort_by(|a, b| a.len().cmp(&b.len()).then_with(|| a.cmp(b))); - suggest_after_mut.listbox = Listbox::from_iter(candidates); - query_editor_after_mut + jnv.suggestions.listbox = Listbox::from_iter(candidates); + filter_editor .texteditor - .replace(&suggest_after_mut.listbox.get()); + .replace(&jnv.suggestions.listbox.get()); - renderer.keymap.switch("on_suggest"); + jnv.keymap.borrow_mut().switch("on_suggest"); } } @@ -46,7 +43,7 @@ pub fn default(event: &Event, renderer: &mut crate::jnv::render::Renderer) -> Re kind: KeyEventKind::Press, state: KeyEventState::NONE, }) => { - query_editor_after_mut.texteditor.backward(); + filter_editor.texteditor.backward(); } Event::Key(KeyEvent { code: KeyCode::Right, @@ -54,38 +51,38 @@ pub fn default(event: &Event, renderer: &mut crate::jnv::render::Renderer) -> Re kind: KeyEventKind::Press, state: KeyEventState::NONE, }) => { - query_editor_after_mut.texteditor.forward(); + filter_editor.texteditor.forward(); } Event::Key(KeyEvent { code: KeyCode::Char('a'), modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, state: KeyEventState::NONE, - }) => query_editor_after_mut.texteditor.move_to_head(), + }) => filter_editor.texteditor.move_to_head(), Event::Key(KeyEvent { code: KeyCode::Char('e'), modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, state: KeyEventState::NONE, - }) => query_editor_after_mut.texteditor.move_to_tail(), + }) => filter_editor.texteditor.move_to_tail(), Event::Key(KeyEvent { code: KeyCode::Char('b'), modifiers: KeyModifiers::ALT, kind: KeyEventKind::Press, state: KeyEventState::NONE, - }) => query_editor_after_mut + }) => filter_editor .texteditor - .move_to_previous_nearest(&query_editor_after_mut.word_break_chars), + .move_to_previous_nearest(&filter_editor.word_break_chars), Event::Key(KeyEvent { code: KeyCode::Char('f'), modifiers: KeyModifiers::ALT, kind: KeyEventKind::Press, state: KeyEventState::NONE, - }) => query_editor_after_mut + }) => filter_editor .texteditor - .move_to_next_nearest(&query_editor_after_mut.word_break_chars), + .move_to_next_nearest(&filter_editor.word_break_chars), // Erase char(s). Event::Key(KeyEvent { @@ -93,13 +90,13 @@ pub fn default(event: &Event, renderer: &mut crate::jnv::render::Renderer) -> Re modifiers: KeyModifiers::NONE, kind: KeyEventKind::Press, state: KeyEventState::NONE, - }) => query_editor_after_mut.texteditor.erase(), + }) => filter_editor.texteditor.erase(), Event::Key(KeyEvent { code: KeyCode::Char('u'), modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, state: KeyEventState::NONE, - }) => query_editor_after_mut.texteditor.erase_all(), + }) => filter_editor.texteditor.erase_all(), // Erase to the nearest character. Event::Key(KeyEvent { @@ -107,18 +104,18 @@ pub fn default(event: &Event, renderer: &mut crate::jnv::render::Renderer) -> Re modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, state: KeyEventState::NONE, - }) => query_editor_after_mut + }) => filter_editor .texteditor - .erase_to_previous_nearest(&query_editor_after_mut.word_break_chars), + .erase_to_previous_nearest(&filter_editor.word_break_chars), Event::Key(KeyEvent { code: KeyCode::Char('d'), modifiers: KeyModifiers::ALT, kind: KeyEventKind::Press, state: KeyEventState::NONE, - }) => query_editor_after_mut + }) => filter_editor .texteditor - .erase_to_next_nearest(&query_editor_after_mut.word_break_chars), + .erase_to_next_nearest(&filter_editor.word_break_chars), // Move up. Event::Key(KeyEvent { @@ -133,7 +130,7 @@ pub fn default(event: &Event, renderer: &mut crate::jnv::render::Renderer) -> Re kind: KeyEventKind::Press, state: KeyEventState::NONE, }) => { - json_after_mut.stream.backward(); + jnv.json.stream.backward(); } // Move down. @@ -149,7 +146,7 @@ pub fn default(event: &Event, renderer: &mut crate::jnv::render::Renderer) -> Re kind: KeyEventKind::Press, state: KeyEventState::NONE, }) => { - json_after_mut.stream.forward(); + jnv.json.stream.forward(); } // Move to tail @@ -159,7 +156,7 @@ pub fn default(event: &Event, renderer: &mut crate::jnv::render::Renderer) -> Re kind: KeyEventKind::Press, state: KeyEventState::NONE, }) => { - json_after_mut.stream.move_to_tail(); + jnv.json.stream.move_to_tail(); } // Move to head @@ -169,7 +166,7 @@ pub fn default(event: &Event, renderer: &mut crate::jnv::render::Renderer) -> Re kind: KeyEventKind::Press, state: KeyEventState::NONE, }) => { - json_after_mut.stream.move_to_head(); + jnv.json.stream.move_to_head(); } // Toggle collapse/expand @@ -179,7 +176,7 @@ pub fn default(event: &Event, renderer: &mut crate::jnv::render::Renderer) -> Re kind: KeyEventKind::Press, state: KeyEventState::NONE, }) => { - json_after_mut.stream.toggle(); + jnv.json.stream.toggle(); } Event::Key(KeyEvent { @@ -188,7 +185,7 @@ pub fn default(event: &Event, renderer: &mut crate::jnv::render::Renderer) -> Re kind: KeyEventKind::Press, state: KeyEventState::NONE, }) => { - json_after_mut.stream.expand_all(); + jnv.json.stream.expand_all(); } Event::Key(KeyEvent { @@ -197,7 +194,7 @@ pub fn default(event: &Event, renderer: &mut crate::jnv::render::Renderer) -> Re kind: KeyEventKind::Press, state: KeyEventState::NONE, }) => { - json_after_mut.stream.collapse_all(); + jnv.json.stream.collapse_all(); } // Input char. @@ -212,9 +209,9 @@ pub fn default(event: &Event, renderer: &mut crate::jnv::render::Renderer) -> Re modifiers: KeyModifiers::SHIFT, kind: KeyEventKind::Press, state: KeyEventState::NONE, - }) => match query_editor_after_mut.edit_mode { - text_editor::Mode::Insert => query_editor_after_mut.texteditor.insert(*ch), - text_editor::Mode::Overwrite => query_editor_after_mut.texteditor.overwrite(*ch), + }) => match filter_editor.edit_mode { + text_editor::Mode::Insert => filter_editor.texteditor.insert(*ch), + text_editor::Mode::Overwrite => filter_editor.texteditor.overwrite(*ch), }, _ => (), @@ -222,12 +219,8 @@ pub fn default(event: &Event, renderer: &mut crate::jnv::render::Renderer) -> Re Ok(PromptSignal::Continue) } -pub fn on_suggest( - event: &Event, - renderer: &mut crate::jnv::render::Renderer, -) -> Result { - let query_editor_after_mut = renderer.query_editor_snapshot.after_mut(); - let suggest_after_mut = renderer.suggest_snapshot.after_mut(); +pub fn on_suggest(event: &Event, jnv: &mut crate::jnv::Jnv) -> anyhow::Result { + let query_editor_after_mut = jnv.filter_editor.after_mut(); match event { Event::Key(KeyEvent { @@ -249,10 +242,10 @@ pub fn on_suggest( kind: KeyEventKind::Press, state: KeyEventState::NONE, }) => { - suggest_after_mut.listbox.forward(); + jnv.suggestions.listbox.forward(); query_editor_after_mut .texteditor - .replace(&suggest_after_mut.listbox.get()); + .replace(&jnv.suggestions.listbox.get()); } Event::Key(KeyEvent { @@ -261,15 +254,15 @@ pub fn on_suggest( kind: KeyEventKind::Press, state: KeyEventState::NONE, }) => { - suggest_after_mut.listbox.backward(); + jnv.suggestions.listbox.backward(); query_editor_after_mut .texteditor - .replace(&suggest_after_mut.listbox.get()); + .replace(&jnv.suggestions.listbox.get()); } _ => { - suggest_after_mut.listbox = Listbox::from_iter(Vec::::new()); - renderer.keymap.switch("default"); + jnv.suggestions.listbox = Listbox::from_iter(Vec::::new()); + jnv.keymap.borrow_mut().switch("default"); // This block is specifically designed to prevent the default action of toggling collapse/expand // from being executed when the Enter key is pressed. This is done from the perspective of user @@ -283,7 +276,7 @@ pub fn on_suggest( }) = event { } else { - return default(event, renderer); + return default(event, jnv); } } } diff --git a/src/jnv/render.rs b/src/jnv/render.rs deleted file mode 100644 index 6a275c4..0000000 --- a/src/jnv/render.rs +++ /dev/null @@ -1,28 +0,0 @@ -use promkit::{ - impl_as_any, impl_cast, json, keymap::KeymapManager, listbox, pane::Pane, snapshot::Snapshot, - suggest::Suggest, text, text_editor, -}; - -#[derive(Clone)] -pub struct Renderer { - pub keymap: KeymapManager, - pub query_editor_snapshot: Snapshot, - pub hint_message_snapshot: Snapshot, - pub suggest: Suggest, - pub suggest_snapshot: Snapshot, - pub json_snapshot: Snapshot, -} - -impl_as_any!(Renderer); -impl_cast!(Renderer); - -impl promkit::Renderer for Renderer { - fn create_panes(&self, width: u16) -> Vec { - let mut panes = Vec::new(); - panes.extend(self.query_editor_snapshot.create_panes(width)); - panes.extend(self.hint_message_snapshot.create_panes(width)); - panes.extend(self.suggest_snapshot.create_panes(width)); - panes.extend(self.json_snapshot.create_panes(width)); - panes - } -} diff --git a/src/jnv/trie.rs b/src/jnv/trie.rs deleted file mode 100644 index e1267c8..0000000 --- a/src/jnv/trie.rs +++ /dev/null @@ -1,22 +0,0 @@ -use radix_trie::{Trie, TrieCommon}; - -use promkit::serde_json; - -#[derive(Default)] -pub struct QueryTrie(Trie>); - -impl QueryTrie { - pub fn insert(&mut self, query: &str, json_nodes: Vec) { - self.0.insert(query.to_string(), json_nodes); - } - - pub fn prefix_search(&self, query: &str) -> Option<(&String, &Vec)> { - self.0 - .get_ancestor(query) - .and_then(|subtrie| Some((subtrie.key()?, subtrie.value()?))) - } - - pub fn prefix_search_value(&self, query: &str) -> Option<&Vec> { - self.prefix_search(query).map(|tup| tup.1) - } -} diff --git a/src/main.rs b/src/main.rs index 5f6a8c9..44d6d3a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use std::{ + collections::HashSet, fs::File, io::{self, Read}, path::PathBuf, @@ -7,10 +8,16 @@ use std::{ use anyhow::{anyhow, Result}; use clap::Parser; -use promkit::text_editor; +use promkit::{ + crossterm::style::{Attribute, Attributes, Color}, + json, listbox, + style::StyleBuilder, + text, text_editor, +}; mod jnv; use jnv::Jnv; +mod trie; /// JSON navigator and interactive filter leveraging jq #[derive(Parser)] @@ -90,7 +97,7 @@ pub struct Args { Note: Increasing this depth can significantly slow down the display for large datasets. " )] - pub expand_depth: Option, + pub json_expand_depth: Option, #[arg( short = 'l', @@ -141,15 +148,68 @@ fn parse_input(args: &Args) -> Result { fn main() -> Result<()> { let args = Args::parse(); + + let input = parse_input(&args)?; + + let filter_editor = text_editor::State { + texteditor: Default::default(), + history: Default::default(), + prefix: String::from("❯❯ "), + mask: Default::default(), + prefix_style: StyleBuilder::new().fgc(Color::Blue).build(), + active_char_style: StyleBuilder::new().bgc(Color::Magenta).build(), + inactive_char_style: StyleBuilder::new().build(), + edit_mode: args.edit_mode, + word_break_chars: HashSet::from(['.', '|', '(', ')', '[', ']']), + lines: Default::default(), + }; + + let hint_message = text::State { + text: Default::default(), + style: StyleBuilder::new() + .fgc(Color::Green) + .attrs(Attributes::from(Attribute::Bold)) + .build(), + }; + + let suggestions = listbox::State { + listbox: listbox::Listbox::from_iter(Vec::::new()), + cursor: String::from("❯ "), + active_item_style: StyleBuilder::new() + .fgc(Color::Grey) + .bgc(Color::Yellow) + .build(), + inactive_item_style: StyleBuilder::new().fgc(Color::Grey).build(), + lines: Some(args.suggestion_list_length), + }; + + let json_theme = json::Theme { + curly_brackets_style: StyleBuilder::new() + .attrs(Attributes::from(Attribute::Bold)) + .build(), + square_brackets_style: StyleBuilder::new() + .attrs(Attributes::from(Attribute::Bold)) + .build(), + key_style: StyleBuilder::new().fgc(Color::Cyan).build(), + string_value_style: StyleBuilder::new().fgc(Color::Green).build(), + number_value_style: StyleBuilder::new().build(), + boolean_value_style: StyleBuilder::new().build(), + null_value_style: StyleBuilder::new().fgc(Color::Grey).build(), + active_item_attribute: Attribute::Bold, + inactive_item_attribute: Attribute::Dim, + lines: Default::default(), + indent: args.indent, + }; + let mut prompt = Jnv::try_new( - parse_input(&args)?, - args.expand_depth, + input, + filter_editor, + hint_message, + suggestions, + json_theme, + args.json_expand_depth, args.no_hint, - args.edit_mode, - args.indent, - args.suggestion_list_length, - )? - .prompt()?; + )?; let _ = prompt.run()?; Ok(()) } diff --git a/src/trie.rs b/src/trie.rs new file mode 100644 index 0000000..560f4d4 --- /dev/null +++ b/src/trie.rs @@ -0,0 +1,109 @@ +use radix_trie::{Trie, TrieCommon}; + +use promkit::serde_json; + +#[derive(Default, Clone)] +pub struct FilterTrie(Trie>); + +impl FilterTrie { + pub fn insert(&mut self, query: &str, json_nodes: Vec) { + self.0.insert(query.to_string(), json_nodes); + } + + pub fn exact_search(&self, query: &str) -> Option<&Vec> { + self.0.get(query) + } + + pub fn prefix_search(&self, query: &str) -> Option<&Vec> { + self.0 + .get_ancestor(query) + .and_then(|subtrie| subtrie.value()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + mod exact_search { + use super::*; + use serde_json::json; + + #[test] + fn test_exact_match() { + let mut trie = FilterTrie::default(); + trie.insert("apple", vec![json!({"type": "fruit"})]); + trie.insert("app", vec![json!({"type": "abbreviation"})]); + + let result = trie.exact_search("app"); + assert!(result.is_some()); + let values = result.unwrap(); + assert_eq!(values.len(), 1); + assert_eq!(values[0], json!({"type": "abbreviation"})); + } + + #[test] + fn test_no_match() { + let mut trie = FilterTrie::default(); + trie.insert("apple", vec![json!({"type": "fruit"})]); + trie.insert("app", vec![json!({"type": "abbreviation"})]); + + let result = trie.exact_search("application"); + assert!(result.is_none()); + } + } + + mod prefix_search { + use super::*; + use serde_json::json; + + #[test] + fn test_with_exact_prefix() { + let mut trie = FilterTrie::default(); + trie.insert("apple", vec![json!({"type": "fruit"})]); + trie.insert("app", vec![json!({"type": "abbreviation"})]); + + let result = trie.prefix_search("app"); + assert!(result.is_some()); + let values = result.unwrap(); + assert_eq!(values.len(), 1); + assert_eq!(values[0], json!({"type": "abbreviation"})); + } + + #[test] + fn test_with_longer_query_than_keys() { + let mut trie = FilterTrie::default(); + trie.insert("apple", vec![json!({"type": "fruit"})]); + trie.insert("app", vec![json!({"type": "abbreviation"})]); + + let result = trie.prefix_search("application"); + assert!(result.is_some()); + let values = result.unwrap(); + assert_eq!(values.len(), 1); + assert_eq!(values[0], json!({"type": "abbreviation"})); + } + + #[test] + fn test_with_full_key_match() { + let mut trie = FilterTrie::default(); + trie.insert("apple", vec![json!({"type": "fruit"})]); + trie.insert("app", vec![json!({"type": "abbreviation"})]); + + let result = trie.prefix_search("apple"); + assert!(result.is_some()); + let values = result.unwrap(); + assert_eq!(values.len(), 1); + assert_eq!(values[0], json!({"type": "fruit"})); + } + + #[test] + fn test_with_shorter_query_than_any_key() { + let mut trie = FilterTrie::default(); + trie.insert("apple", vec![json!({"type": "fruit"})]); + trie.insert("app", vec![json!({"type": "abbreviation"})]); + + let result = trie.prefix_search("ap"); + assert!(result.is_none()); + } + } +}