yerba 0.2.2-x86_64-linux-gnu → 0.4.0-x86_64-linux-gnu
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +167 -12
- data/exe/x86_64-linux-gnu/yerba +0 -0
- data/ext/yerba/extconf.rb +39 -12
- data/ext/yerba/include/yerba.h +21 -10
- data/ext/yerba/yerba.c +91 -25
- data/lib/yerba/3.2/yerba.so +0 -0
- data/lib/yerba/3.3/yerba.so +0 -0
- data/lib/yerba/3.4/yerba.so +0 -0
- data/lib/yerba/4.0/yerba.so +0 -0
- data/lib/yerba/collection.rb +35 -0
- data/lib/yerba/document.rb +16 -0
- data/lib/yerba/sequence.rb +169 -1
- data/lib/yerba/version.rb +1 -1
- data/lib/yerba.rb +7 -2
- data/rust/Cargo.lock +1 -1
- data/rust/Cargo.toml +2 -2
- data/rust/cbindgen.toml +1 -0
- data/rust/rustfmt.toml +1 -1
- data/rust/src/commands/blank_lines.rs +1 -4
- data/rust/src/commands/delete.rs +9 -4
- data/rust/src/commands/directives.rs +61 -0
- data/rust/src/commands/get.rs +52 -26
- data/rust/src/commands/insert.rs +8 -4
- data/rust/src/commands/mod.rs +71 -9
- data/rust/src/commands/move_item.rs +2 -1
- data/rust/src/commands/move_key.rs +2 -1
- data/rust/src/commands/quote_style.rs +12 -6
- data/rust/src/commands/remove.rs +8 -4
- data/rust/src/commands/rename.rs +8 -4
- data/rust/src/commands/selectors.rs +158 -0
- data/rust/src/commands/set.rs +33 -16
- data/rust/src/commands/sort.rs +342 -10
- data/rust/src/didyoumean.rs +53 -0
- data/rust/src/document/condition.rs +139 -0
- data/rust/src/document/delete.rs +91 -0
- data/rust/src/document/get.rs +262 -0
- data/rust/src/document/insert.rs +314 -0
- data/rust/src/document/mod.rs +784 -0
- data/rust/src/document/set.rs +90 -0
- data/rust/src/document/sort.rs +607 -0
- data/rust/src/document/style.rs +473 -0
- data/rust/src/error.rs +35 -7
- data/rust/src/ffi.rs +213 -520
- data/rust/src/json.rs +1 -7
- data/rust/src/lib.rs +89 -2
- data/rust/src/main.rs +2 -0
- data/rust/src/quote_style.rs +83 -7
- data/rust/src/selector.rs +2 -7
- data/rust/src/syntax.rs +41 -21
- data/rust/src/yerbafile.rs +39 -18
- metadata +13 -3
- data/rust/src/document.rs +0 -2237
data/rust/src/commands/sort.rs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
use std::process;
|
|
1
2
|
use std::sync::LazyLock;
|
|
2
3
|
|
|
3
4
|
use indoc::indoc;
|
|
@@ -8,17 +9,17 @@ use super::{output, parse_file, resolve_files};
|
|
|
8
9
|
static EXAMPLES: LazyLock<String> = LazyLock::new(|| {
|
|
9
10
|
colorize_examples(indoc! {r#"
|
|
10
11
|
yerba sort config.yml "tags"
|
|
11
|
-
yerba sort videos.yml --by "title"
|
|
12
|
-
yerba sort videos.yml --by "date
|
|
13
|
-
yerba sort videos.yml "[].speakers"
|
|
14
|
-
yerba sort videos.yml "[]
|
|
15
|
-
yerba sort
|
|
12
|
+
yerba sort videos.yml --by ".title"
|
|
13
|
+
yerba sort videos.yml --by ".date" --order desc --by ".title"
|
|
14
|
+
yerba sort videos.yml "[].speakers" --by ".name"
|
|
15
|
+
yerba sort videos.yml "[]" --by ".id" --order "talk-3,talk-1,talk-2"
|
|
16
|
+
yerba sort speakers.yml "[]" --by ".name" --order "Charlie,Alice,Bob"
|
|
16
17
|
"#})
|
|
17
18
|
});
|
|
18
19
|
|
|
19
20
|
#[derive(clap::Args)]
|
|
20
21
|
#[command(
|
|
21
|
-
about = "Sort items in a sequence
|
|
22
|
+
about = "Sort or reorder items in a sequence",
|
|
22
23
|
arg_required_else_help = true,
|
|
23
24
|
after_help = EXAMPLES.as_str()
|
|
24
25
|
)]
|
|
@@ -26,9 +27,15 @@ pub struct Args {
|
|
|
26
27
|
file: String,
|
|
27
28
|
/// Selector (optional — omit for root-level sequence)
|
|
28
29
|
selector: Option<String>,
|
|
29
|
-
///
|
|
30
|
-
#[arg(long)]
|
|
31
|
-
by:
|
|
30
|
+
/// Field to sort or match by (can be repeated for tie-breakers)
|
|
31
|
+
#[arg(long, action = clap::ArgAction::Append)]
|
|
32
|
+
by: Vec<String>,
|
|
33
|
+
/// Sort direction (asc/desc) or explicit order (comma-separated values, must list all items)
|
|
34
|
+
#[arg(long, action = clap::ArgAction::Append)]
|
|
35
|
+
order: Vec<String>,
|
|
36
|
+
/// Additional fields to display alongside values (e.g. --context ".title")
|
|
37
|
+
#[arg(long, action = clap::ArgAction::Append)]
|
|
38
|
+
context: Vec<String>,
|
|
32
39
|
/// Case-sensitive sort (default: case-insensitive)
|
|
33
40
|
#[arg(long)]
|
|
34
41
|
case_sensitive: bool,
|
|
@@ -36,17 +43,342 @@ pub struct Args {
|
|
|
36
43
|
dry_run: bool,
|
|
37
44
|
}
|
|
38
45
|
|
|
46
|
+
fn is_direction(value: &str) -> bool {
|
|
47
|
+
matches!(value, "asc" | "desc" | "ascending" | "descending")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn is_explicit_reorder(orders: &[String]) -> bool {
|
|
51
|
+
orders.iter().any(|o| !is_direction(o))
|
|
52
|
+
}
|
|
53
|
+
|
|
39
54
|
impl Args {
|
|
40
55
|
pub fn run(self) {
|
|
56
|
+
use super::color::*;
|
|
57
|
+
|
|
58
|
+
if self.by.is_empty() && self.order.is_empty() && self.selector.is_none() {
|
|
59
|
+
let document = parse_file(&self.file);
|
|
60
|
+
|
|
61
|
+
eprintln!("{RED}Error:{RESET} specify a selector, --by, or --order");
|
|
62
|
+
eprintln!();
|
|
63
|
+
eprintln!(" {BOLD}Examples:{RESET}");
|
|
64
|
+
eprintln!(" yerba sort \"{}\" \"tags\"", self.file);
|
|
65
|
+
eprintln!(" yerba sort \"{}\" --by \".title\" --order asc", self.file);
|
|
66
|
+
eprintln!();
|
|
67
|
+
|
|
68
|
+
super::show_similar_selectors(&self.file, &document, "[]");
|
|
69
|
+
|
|
70
|
+
process::exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if !self.by.is_empty() && self.order.is_empty() {
|
|
74
|
+
self.show_values();
|
|
75
|
+
} else if is_explicit_reorder(&self.order) {
|
|
76
|
+
self.run_reorder();
|
|
77
|
+
} else {
|
|
78
|
+
self.run_sort();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fn show_values(self) {
|
|
83
|
+
use super::color::*;
|
|
84
|
+
|
|
85
|
+
if self.by.len() != 1 {
|
|
86
|
+
eprintln!("{RED}Error:{RESET} --order is required when using --by");
|
|
87
|
+
process::exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let by = &self.by[0];
|
|
41
91
|
let selector = self.selector.as_deref().unwrap_or("");
|
|
42
|
-
let
|
|
92
|
+
let items_selector = if selector.is_empty() { "[]".to_string() } else { format!("{}[]", selector) };
|
|
93
|
+
let document = parse_file(&self.file);
|
|
94
|
+
let (labels, context_values, selector_display) = self.resolve_labels(&document, by, selector, &items_selector);
|
|
95
|
+
|
|
96
|
+
let context_hint = if self.context.is_empty() {
|
|
97
|
+
format!("\n\n {DIM}Add --context \".field\" to show additional fields alongside values{RESET}")
|
|
98
|
+
} else {
|
|
99
|
+
String::new()
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
eprintln!("{RED}Error:{RESET} --order is required when using --by");
|
|
103
|
+
eprintln!();
|
|
104
|
+
eprintln!(" {BOLD}Current values (by {by}):{RESET}");
|
|
105
|
+
|
|
106
|
+
for (index, label) in labels.iter().enumerate() {
|
|
107
|
+
let context = context_values.get(index).map(|c| c.as_slice()).unwrap_or(&[]);
|
|
108
|
+
|
|
109
|
+
eprintln!(" {}", self.format_label_line(index, label, context));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let csv: String = labels.join(",");
|
|
113
|
+
|
|
114
|
+
eprintln!("{context_hint}");
|
|
115
|
+
eprintln!();
|
|
116
|
+
eprintln!(" {BOLD}To sort alphabetically:{RESET}");
|
|
117
|
+
eprintln!(" yerba sort \"{}\"{selector_display} --by \"{by}\" --order asc", self.file);
|
|
118
|
+
eprintln!(" yerba sort \"{}\"{selector_display} --by \"{by}\" --order desc", self.file);
|
|
119
|
+
eprintln!();
|
|
120
|
+
eprintln!(" {BOLD}To reorder explicitly:{RESET}");
|
|
121
|
+
eprintln!(" yerba sort \"{}\"{selector_display} --by \"{by}\" --order \"{csv}\"", self.file);
|
|
122
|
+
eprintln!();
|
|
123
|
+
eprintln!(" {BOLD}To move individual items:{RESET}");
|
|
124
|
+
eprintln!(" yerba move \"{}\"{selector_display} <item> --before/--after <target>", self.file);
|
|
125
|
+
|
|
126
|
+
process::exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fn run_sort(self) {
|
|
130
|
+
use super::color::*;
|
|
131
|
+
|
|
132
|
+
let selector = self.selector.as_deref().unwrap_or("");
|
|
133
|
+
|
|
134
|
+
let sort_fields: Vec<yerba::SortField> = if self.by.is_empty() {
|
|
135
|
+
Vec::new()
|
|
136
|
+
} else {
|
|
137
|
+
self
|
|
138
|
+
.by
|
|
139
|
+
.iter()
|
|
140
|
+
.enumerate()
|
|
141
|
+
.map(|(index, field)| {
|
|
142
|
+
let direction = self.order.get(index).map(|s| s.as_str());
|
|
143
|
+
|
|
144
|
+
match direction {
|
|
145
|
+
Some("desc" | "descending") => yerba::SortField::desc(field),
|
|
146
|
+
Some("asc" | "ascending") | None => yerba::SortField::asc(field),
|
|
147
|
+
Some(other) => {
|
|
148
|
+
eprintln!("{RED}Error:{RESET} invalid sort direction \"{other}\". Use \"asc\" or \"desc\"");
|
|
149
|
+
process::exit(1);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
.collect()
|
|
154
|
+
};
|
|
43
155
|
|
|
44
156
|
for resolved_file in resolve_files(&self.file) {
|
|
45
157
|
let mut document = parse_file(&resolved_file);
|
|
46
158
|
|
|
159
|
+
if sort_fields.is_empty() {
|
|
160
|
+
let first_item_selector = if selector.is_empty() { "[0]".to_string() } else { format!("{}[0]", selector) };
|
|
161
|
+
|
|
162
|
+
match document.get_value(&first_item_selector) {
|
|
163
|
+
Some(first) if first.is_mapping() => {
|
|
164
|
+
eprintln!("{RED}Error:{RESET} --by is required to sort a sequence of maps");
|
|
165
|
+
eprintln!();
|
|
166
|
+
|
|
167
|
+
let selectors = document.selectors();
|
|
168
|
+
let prefix = if selector.is_empty() { "[]." } else { "" };
|
|
169
|
+
let fields: Vec<&String> = selectors
|
|
170
|
+
.iter()
|
|
171
|
+
.filter(|s| {
|
|
172
|
+
let check = if prefix.is_empty() { format!("{}[].", selector) } else { prefix.to_string() };
|
|
173
|
+
s.starts_with(&check) && !s[check.len()..].contains('.') && !s[check.len()..].contains('[')
|
|
174
|
+
})
|
|
175
|
+
.collect();
|
|
176
|
+
|
|
177
|
+
if !fields.is_empty() {
|
|
178
|
+
eprintln!(" {BOLD}Available fields:{RESET}");
|
|
179
|
+
|
|
180
|
+
for field in &fields {
|
|
181
|
+
let short = field.rsplit_once('.').map(|(_, f)| f).unwrap_or(field);
|
|
182
|
+
eprintln!(" .{short}");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
process::exit(1);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
None if selector.is_empty() => {
|
|
190
|
+
eprintln!("{RED}Error:{RESET} no sequence found at root level in {}", resolved_file);
|
|
191
|
+
eprintln!();
|
|
192
|
+
eprintln!(" {DIM}Specify a selector for the sequence to sort:{RESET}");
|
|
193
|
+
eprintln!(" yerba sort \"{}\" \"<selector>\"", self.file);
|
|
194
|
+
eprintln!();
|
|
195
|
+
|
|
196
|
+
super::show_similar_selectors(&resolved_file, &document, "[]");
|
|
197
|
+
|
|
198
|
+
process::exit(1);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
_ => {}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
47
205
|
if document.sort_items(selector, &sort_fields, self.case_sensitive).is_ok() {
|
|
48
206
|
output(&resolved_file, &document, self.dry_run);
|
|
49
207
|
}
|
|
50
208
|
}
|
|
51
209
|
}
|
|
210
|
+
|
|
211
|
+
fn run_reorder(self) {
|
|
212
|
+
use super::color::*;
|
|
213
|
+
|
|
214
|
+
if self.by.len() != 1 {
|
|
215
|
+
eprintln!("{RED}Error:{RESET} explicit --order requires exactly one --by field");
|
|
216
|
+
|
|
217
|
+
process::exit(1);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if self.order.len() != 1 {
|
|
221
|
+
eprintln!("{RED}Error:{RESET} explicit --order must be a single comma-separated list");
|
|
222
|
+
|
|
223
|
+
process::exit(1);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let by = &self.by[0];
|
|
227
|
+
let order = &self.order[0];
|
|
228
|
+
let selector = self.selector.as_deref().unwrap_or("");
|
|
229
|
+
|
|
230
|
+
let items_selector = if selector.is_empty() { "[]".to_string() } else { format!("{}[]", selector) };
|
|
231
|
+
|
|
232
|
+
let mut document = parse_file(&self.file);
|
|
233
|
+
let mut seen = std::collections::HashSet::new();
|
|
234
|
+
|
|
235
|
+
let (labels, _, _) = self.resolve_labels(&document, by, selector, &items_selector);
|
|
236
|
+
let duplicates: Vec<&String> = labels.iter().filter(|label| !seen.insert(label.as_str())).collect();
|
|
237
|
+
|
|
238
|
+
if !duplicates.is_empty() {
|
|
239
|
+
eprintln!("{RED}Error:{RESET} --order requires unique values for {by}, but found duplicates");
|
|
240
|
+
eprintln!();
|
|
241
|
+
eprintln!(" {BOLD}Duplicate values:{RESET}");
|
|
242
|
+
|
|
243
|
+
for label in &duplicates {
|
|
244
|
+
eprintln!(" {label}");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
eprintln!();
|
|
248
|
+
eprintln!(" {DIM}Use \"yerba sort\" with --by instead to sort by field, or");
|
|
249
|
+
eprintln!(" choose a --by field with unique values (e.g. \".id\"){RESET}");
|
|
250
|
+
|
|
251
|
+
process::exit(1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let values_with_commas: Vec<&String> = labels.iter().filter(|l| l.contains(',')).collect();
|
|
255
|
+
|
|
256
|
+
if !values_with_commas.is_empty() {
|
|
257
|
+
let selector_display = if selector.is_empty() { String::new() } else { format!(" \"{selector}\"") };
|
|
258
|
+
|
|
259
|
+
eprintln!("{RED}Error:{RESET} some values for {by} contain commas, which conflicts with --order parsing");
|
|
260
|
+
eprintln!();
|
|
261
|
+
eprintln!(" {BOLD}Values with commas:{RESET}");
|
|
262
|
+
|
|
263
|
+
for label in &values_with_commas {
|
|
264
|
+
eprintln!(" {label}");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
eprintln!();
|
|
268
|
+
eprintln!(" {BOLD}Use yerba move to reorder individual items instead:{RESET}");
|
|
269
|
+
eprintln!(" yerba move \"{}\"{selector_display} <item> --before/--after <target>", self.file);
|
|
270
|
+
|
|
271
|
+
process::exit(1);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let desired_order: Vec<&str> = order.split(',').map(|s| s.trim()).collect();
|
|
275
|
+
let container = if selector.is_empty() { "" } else { selector };
|
|
276
|
+
|
|
277
|
+
match document.reorder_items(container, by, &desired_order) {
|
|
278
|
+
Ok(()) => output(&self.file, &document, self.dry_run),
|
|
279
|
+
Err(error) => {
|
|
280
|
+
eprintln!("{RED}Error:{RESET} {}", error);
|
|
281
|
+
process::exit(1);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
fn resolve_labels(&self, document: &yerba::Document, by: &str, selector: &str, items_selector: &str) -> (Vec<String>, Vec<Vec<String>>, String) {
|
|
287
|
+
use super::color::*;
|
|
288
|
+
|
|
289
|
+
let items = document.get_values(items_selector);
|
|
290
|
+
|
|
291
|
+
if items.is_empty() {
|
|
292
|
+
if selector.is_empty() {
|
|
293
|
+
eprintln!("{RED}Error:{RESET} no sequence found at root level");
|
|
294
|
+
eprintln!();
|
|
295
|
+
eprintln!(" {DIM}If the file is a map, specify which sequence to sort:{RESET}");
|
|
296
|
+
eprintln!(" yerba sort \"{}\" \"<selector>\" --by \"{by}\" --order asc", self.file);
|
|
297
|
+
eprintln!();
|
|
298
|
+
|
|
299
|
+
super::show_similar_selectors(&self.file, document, "[]");
|
|
300
|
+
} else {
|
|
301
|
+
eprintln!("{RED}Error:{RESET} no sequence found at selector: {selector}");
|
|
302
|
+
|
|
303
|
+
super::show_similar_selectors(&self.file, document, selector);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
process::exit(1);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
let by_is_scalar = by == ".";
|
|
310
|
+
|
|
311
|
+
if !by_is_scalar {
|
|
312
|
+
let by_selector = if selector.is_empty() {
|
|
313
|
+
format!("[].{}", by.strip_prefix('.').unwrap_or(by))
|
|
314
|
+
} else {
|
|
315
|
+
format!("{}[].{}", selector, by.strip_prefix('.').unwrap_or(by))
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
if !document.exists(&by_selector) {
|
|
319
|
+
eprintln!("{RED}Error:{RESET} field \"{by}\" not found in items");
|
|
320
|
+
|
|
321
|
+
super::show_similar_selectors(&self.file, document, &by_selector);
|
|
322
|
+
|
|
323
|
+
process::exit(1);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let labels: Vec<String> = items
|
|
328
|
+
.iter()
|
|
329
|
+
.map(|item| {
|
|
330
|
+
if by_is_scalar {
|
|
331
|
+
match item {
|
|
332
|
+
serde_yaml::Value::String(string) => string.clone(),
|
|
333
|
+
_ => serde_json::to_string(&yerba::json::yaml_to_json(item)).unwrap_or_default(),
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
let field = by.strip_prefix('.').unwrap_or(by);
|
|
337
|
+
|
|
338
|
+
yerba::json::resolve_select_field(item, field).as_str().unwrap_or("").to_string()
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
.collect();
|
|
342
|
+
|
|
343
|
+
let context_values: Vec<Vec<String>> = items
|
|
344
|
+
.iter()
|
|
345
|
+
.map(|item| {
|
|
346
|
+
self
|
|
347
|
+
.context
|
|
348
|
+
.iter()
|
|
349
|
+
.map(|context| {
|
|
350
|
+
let field = context.strip_prefix('.').unwrap_or(context);
|
|
351
|
+
|
|
352
|
+
let value = yerba::json::resolve_select_field(item, field);
|
|
353
|
+
|
|
354
|
+
match &value {
|
|
355
|
+
serde_json::Value::String(string) => string.clone(),
|
|
356
|
+
serde_json::Value::Null => String::new(),
|
|
357
|
+
_ => serde_json::to_string(&value).unwrap_or_default(),
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
.collect()
|
|
361
|
+
})
|
|
362
|
+
.collect();
|
|
363
|
+
|
|
364
|
+
let selector_display = if selector.is_empty() { String::new() } else { format!(" \"{selector}\"") };
|
|
365
|
+
|
|
366
|
+
(labels, context_values, selector_display)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
fn format_label_line(&self, index: usize, label: &str, context: &[String]) -> String {
|
|
370
|
+
use super::color::*;
|
|
371
|
+
|
|
372
|
+
if context.is_empty() {
|
|
373
|
+
format!("{DIM}[{index}]{RESET} {label}")
|
|
374
|
+
} else {
|
|
375
|
+
let context = context.iter().filter(|c| !c.is_empty()).cloned().collect::<Vec<_>>().join(", ");
|
|
376
|
+
|
|
377
|
+
if context.is_empty() {
|
|
378
|
+
format!("{DIM}[{index}]{RESET} {label}")
|
|
379
|
+
} else {
|
|
380
|
+
format!("{DIM}[{index}]{RESET} {label} {DIM}{context}{RESET}")
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
52
384
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
pub fn didyoumean(input: &str, list: &[String]) -> Option<String> {
|
|
2
|
+
if list.is_empty() {
|
|
3
|
+
return None;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
let input_lower = input.to_lowercase();
|
|
7
|
+
|
|
8
|
+
let mut scored: Vec<(String, usize)> = list
|
|
9
|
+
.iter()
|
|
10
|
+
.map(|item| (item.clone(), levenshtein(&input_lower, &item.to_lowercase())))
|
|
11
|
+
.collect();
|
|
12
|
+
|
|
13
|
+
scored.sort_by(|(a_item, a_distance), (b_item, b_distance)| a_distance.cmp(b_distance).then_with(|| a_item.cmp(b_item)));
|
|
14
|
+
|
|
15
|
+
scored.into_iter().next().map(|(item, _)| item)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
pub fn didyoumean_ranked(input: &str, list: &[String], threshold: usize) -> Vec<String> {
|
|
19
|
+
if list.is_empty() {
|
|
20
|
+
return Vec::new();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let input_lower = input.to_lowercase();
|
|
24
|
+
|
|
25
|
+
let mut scored: Vec<(String, usize)> = list
|
|
26
|
+
.iter()
|
|
27
|
+
.map(|item| (item.clone(), levenshtein(&input_lower, &item.to_lowercase())))
|
|
28
|
+
.filter(|(_, distance)| *distance <= threshold)
|
|
29
|
+
.collect();
|
|
30
|
+
|
|
31
|
+
scored.sort_by(|(a_item, a_distance), (b_item, b_distance)| a_distance.cmp(b_distance).then_with(|| a_item.cmp(b_item)));
|
|
32
|
+
|
|
33
|
+
scored.into_iter().map(|(item, _)| item).collect()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fn levenshtein(a: &str, b: &str) -> usize {
|
|
37
|
+
let b_length = b.len();
|
|
38
|
+
let mut previous: Vec<usize> = (0..=b_length).collect();
|
|
39
|
+
let mut current = vec![0; b_length + 1];
|
|
40
|
+
|
|
41
|
+
for (i, a_character) in a.chars().enumerate() {
|
|
42
|
+
current[0] = i + 1;
|
|
43
|
+
|
|
44
|
+
for (j, b_character) in b.chars().enumerate() {
|
|
45
|
+
let cost = if a_character == b_character { 0 } else { 1 };
|
|
46
|
+
current[j + 1] = (previous[j] + cost).min(previous[j + 1] + 1).min(current[j] + 1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
std::mem::swap(&mut previous, &mut current);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
previous[b_length]
|
|
53
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
impl Document {
|
|
4
|
+
pub fn filter(&self, dot_path: &str, condition: &str) -> Vec<serde_yaml::Value> {
|
|
5
|
+
self
|
|
6
|
+
.navigate_all(dot_path)
|
|
7
|
+
.iter()
|
|
8
|
+
.filter(|node| self.evaluate_condition_on_node(node, condition))
|
|
9
|
+
.map(node_to_yaml_value)
|
|
10
|
+
.collect()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
pub(super) fn evaluate_condition_on_node(&self, node: &SyntaxNode, condition: &str) -> bool {
|
|
14
|
+
let condition = condition.trim();
|
|
15
|
+
|
|
16
|
+
let (left, operator, right) = match parse_condition(condition) {
|
|
17
|
+
Some(parts) => parts,
|
|
18
|
+
None => return false,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let path = crate::selector::Selector::parse(&left);
|
|
22
|
+
|
|
23
|
+
if !path.is_relative() {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let target_nodes = navigate_from_node(node, &path.to_selector_string());
|
|
28
|
+
let values: Vec<String> = target_nodes.iter().filter_map(extract_scalar_text).collect();
|
|
29
|
+
|
|
30
|
+
match operator {
|
|
31
|
+
"==" => values.iter().any(|value| value == &right),
|
|
32
|
+
"!=" => values.iter().all(|value| value != &right),
|
|
33
|
+
"contains" => {
|
|
34
|
+
if values.iter().any(|value| value == &right || value.contains(&right)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for node in &target_nodes {
|
|
39
|
+
if let Some(sequence) = node.descendants().find_map(BlockSeq::cast) {
|
|
40
|
+
for entry in sequence.entries() {
|
|
41
|
+
if let Some(text) = entry.flow().and_then(|flow| extract_scalar_text(flow.syntax())) {
|
|
42
|
+
if text == right {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
false
|
|
51
|
+
}
|
|
52
|
+
"not_contains" => {
|
|
53
|
+
for node in &target_nodes {
|
|
54
|
+
if let Some(sequence) = node.descendants().find_map(BlockSeq::cast) {
|
|
55
|
+
for entry in sequence.entries() {
|
|
56
|
+
if let Some(text) = entry.flow().and_then(|flow| extract_scalar_text(flow.syntax())) {
|
|
57
|
+
if text == right {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
!values.iter().any(|value| value == &right || value.contains(&right))
|
|
66
|
+
}
|
|
67
|
+
_ => false,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
pub fn evaluate_condition(&self, parent_path: &str, condition: &str) -> bool {
|
|
72
|
+
let condition = condition.trim();
|
|
73
|
+
|
|
74
|
+
let (left, operator, right) = match parse_condition(condition) {
|
|
75
|
+
Some(parts) => parts,
|
|
76
|
+
None => return false,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
let path = crate::selector::Selector::parse(&left);
|
|
80
|
+
|
|
81
|
+
let full_path = if path.is_relative() {
|
|
82
|
+
let path_string = path.to_selector_string();
|
|
83
|
+
|
|
84
|
+
if parent_path.is_empty() {
|
|
85
|
+
path_string
|
|
86
|
+
} else {
|
|
87
|
+
format!("{}.{}", parent_path, path_string)
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
path.to_selector_string()
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
let has_brackets = crate::selector::Selector::parse(&full_path).has_brackets();
|
|
94
|
+
|
|
95
|
+
match operator {
|
|
96
|
+
"==" => {
|
|
97
|
+
if has_brackets {
|
|
98
|
+
self.get_all(&full_path).iter().any(|value| value == &right)
|
|
99
|
+
} else {
|
|
100
|
+
self.get(&full_path).unwrap_or_default() == right
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
"!=" => {
|
|
104
|
+
if has_brackets {
|
|
105
|
+
self.get_all(&full_path).iter().all(|value| value != &right)
|
|
106
|
+
} else {
|
|
107
|
+
self.get(&full_path).unwrap_or_default() != right
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
"contains" => {
|
|
111
|
+
if has_brackets {
|
|
112
|
+
self.get_all(&full_path).iter().any(|value| value == &right || value.contains(&right))
|
|
113
|
+
} else {
|
|
114
|
+
let items = self.get_sequence_values(&full_path);
|
|
115
|
+
|
|
116
|
+
if !items.is_empty() {
|
|
117
|
+
items.iter().any(|item| item == &right)
|
|
118
|
+
} else {
|
|
119
|
+
self.get(&full_path).map(|value| value.contains(&right)).unwrap_or(false)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
"not_contains" => {
|
|
124
|
+
if has_brackets {
|
|
125
|
+
self.get_all(&full_path).iter().all(|value| value != &right && !value.contains(&right))
|
|
126
|
+
} else {
|
|
127
|
+
let items = self.get_sequence_values(&full_path);
|
|
128
|
+
|
|
129
|
+
if !items.is_empty() {
|
|
130
|
+
!items.iter().any(|item| item == &right)
|
|
131
|
+
} else {
|
|
132
|
+
self.get(&full_path).map(|value| !value.contains(&right)).unwrap_or(true)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
_ => false,
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
impl Document {
|
|
4
|
+
pub fn rename(&mut self, source_path: &str, destination_path: &str) -> Result<(), YerbaError> {
|
|
5
|
+
Self::validate_path(source_path)?;
|
|
6
|
+
Self::validate_path(destination_path)?;
|
|
7
|
+
|
|
8
|
+
let source_parent = source_path.rsplit_once('.').map(|(parent, _)| parent).unwrap_or("");
|
|
9
|
+
let destination_parent = destination_path.rsplit_once('.').map(|(parent, _)| parent).unwrap_or("");
|
|
10
|
+
let destination_key = destination_path.rsplit_once('.').map(|(_, key)| key).unwrap_or(destination_path);
|
|
11
|
+
|
|
12
|
+
if source_parent == destination_parent {
|
|
13
|
+
let (parent_path, source_key) = source_path.rsplit_once('.').unwrap_or(("", source_path));
|
|
14
|
+
let parent_node = self.navigate(parent_path)?;
|
|
15
|
+
|
|
16
|
+
let map = parent_node
|
|
17
|
+
.descendants()
|
|
18
|
+
.find_map(BlockMap::cast)
|
|
19
|
+
.ok_or_else(|| YerbaError::SelectorNotFound(source_path.to_string()))?;
|
|
20
|
+
|
|
21
|
+
let entry = find_entry_by_key(&map, source_key).ok_or_else(|| YerbaError::SelectorNotFound(source_path.to_string()))?;
|
|
22
|
+
let key_node = entry.key().ok_or_else(|| YerbaError::SelectorNotFound(source_path.to_string()))?;
|
|
23
|
+
let key_token = find_scalar_token(key_node.syntax()).ok_or_else(|| YerbaError::SelectorNotFound(source_path.to_string()))?;
|
|
24
|
+
let new_text = format_scalar_value(destination_key, key_token.kind());
|
|
25
|
+
|
|
26
|
+
self.replace_token(&key_token, &new_text)
|
|
27
|
+
} else {
|
|
28
|
+
let value = self.get(source_path).ok_or_else(|| YerbaError::SelectorNotFound(source_path.to_string()))?;
|
|
29
|
+
|
|
30
|
+
self.delete(source_path)?;
|
|
31
|
+
self.insert_into(destination_path, &value, InsertPosition::Last)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
pub fn delete(&mut self, dot_path: &str) -> Result<(), YerbaError> {
|
|
36
|
+
Self::validate_path(dot_path)?;
|
|
37
|
+
|
|
38
|
+
let (parent_path, last_key) = dot_path.rsplit_once('.').unwrap_or(("", dot_path));
|
|
39
|
+
let parent_node = self.navigate(parent_path)?;
|
|
40
|
+
|
|
41
|
+
let map = parent_node
|
|
42
|
+
.descendants()
|
|
43
|
+
.find_map(BlockMap::cast)
|
|
44
|
+
.ok_or_else(|| YerbaError::SelectorNotFound(dot_path.to_string()))?;
|
|
45
|
+
|
|
46
|
+
let entry = find_entry_by_key(&map, last_key).ok_or_else(|| YerbaError::SelectorNotFound(dot_path.to_string()))?;
|
|
47
|
+
|
|
48
|
+
self.remove_node(entry.syntax())
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
pub fn remove(&mut self, dot_path: &str, value: &str) -> Result<(), YerbaError> {
|
|
52
|
+
let current_node = self.navigate(dot_path)?;
|
|
53
|
+
|
|
54
|
+
let sequence = current_node
|
|
55
|
+
.descendants()
|
|
56
|
+
.find_map(BlockSeq::cast)
|
|
57
|
+
.ok_or_else(|| YerbaError::NotASequence(dot_path.to_string()))?;
|
|
58
|
+
|
|
59
|
+
let target_entry = sequence
|
|
60
|
+
.entries()
|
|
61
|
+
.find(|entry| {
|
|
62
|
+
entry
|
|
63
|
+
.flow()
|
|
64
|
+
.and_then(|flow| extract_scalar_text(flow.syntax()))
|
|
65
|
+
.map(|text| text == value)
|
|
66
|
+
.unwrap_or(false)
|
|
67
|
+
})
|
|
68
|
+
.ok_or_else(|| YerbaError::SelectorNotFound(format!("{} item '{}'", dot_path, value)))?;
|
|
69
|
+
|
|
70
|
+
self.remove_node(target_entry.syntax())
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
pub fn remove_at(&mut self, dot_path: &str, index: usize) -> Result<(), YerbaError> {
|
|
74
|
+
Self::validate_path(dot_path)?;
|
|
75
|
+
|
|
76
|
+
let current_node = self.navigate(dot_path)?;
|
|
77
|
+
|
|
78
|
+
let sequence = current_node
|
|
79
|
+
.descendants()
|
|
80
|
+
.find_map(BlockSeq::cast)
|
|
81
|
+
.ok_or_else(|| YerbaError::NotASequence(dot_path.to_string()))?;
|
|
82
|
+
|
|
83
|
+
let entries: Vec<_> = sequence.entries().collect();
|
|
84
|
+
|
|
85
|
+
if index >= entries.len() {
|
|
86
|
+
return Err(YerbaError::IndexOutOfBounds(index, entries.len()));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
self.remove_node(entries[index].syntax())
|
|
90
|
+
}
|
|
91
|
+
}
|