yerba 0.2.2-arm-linux-gnu → 0.3.0-arm-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 +78 -5
- data/exe/arm-linux-gnu/yerba +0 -0
- data/ext/yerba/extconf.rb +17 -9
- data/ext/yerba/include/yerba.h +2 -1
- data/ext/yerba/yerba.c +8 -1
- 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/version.rb +1 -1
- data/lib/yerba.rb +7 -2
- data/rust/Cargo.lock +1 -1
- data/rust/Cargo.toml +2 -2
- data/rust/src/commands/delete.rs +9 -4
- data/rust/src/commands/get.rs +56 -13
- data/rust/src/commands/insert.rs +8 -4
- data/rust/src/commands/mod.rs +72 -5
- data/rust/src/commands/move_item.rs +2 -1
- data/rust/src/commands/move_key.rs +2 -1
- data/rust/src/commands/remove.rs +8 -4
- data/rust/src/commands/rename.rs +8 -4
- data/rust/src/commands/selectors.rs +174 -0
- data/rust/src/commands/set.rs +33 -16
- data/rust/src/commands/sort.rs +477 -10
- data/rust/src/didyoumean.rs +55 -0
- data/rust/src/document.rs +118 -51
- data/rust/src/error.rs +12 -2
- data/rust/src/ffi.rs +8 -3
- data/rust/src/lib.rs +2 -1
- data/rust/src/main.rs +2 -0
- metadata +4 -2
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,477 @@ 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];
|
|
91
|
+
let selector = self.selector.as_deref().unwrap_or("");
|
|
92
|
+
|
|
93
|
+
let items_selector = if selector.is_empty() {
|
|
94
|
+
"[]".to_string()
|
|
95
|
+
} else {
|
|
96
|
+
format!("{}[]", selector)
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
let document = parse_file(&self.file);
|
|
100
|
+
let (labels, context_values, selector_display) = self.resolve_labels(&document, by, selector, &items_selector);
|
|
101
|
+
|
|
102
|
+
let context_hint = if self.context.is_empty() {
|
|
103
|
+
format!("\n\n {DIM}Add --context \".field\" to show additional fields alongside values{RESET}")
|
|
104
|
+
} else {
|
|
105
|
+
String::new()
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
eprintln!("{RED}Error:{RESET} --order is required when using --by");
|
|
109
|
+
eprintln!();
|
|
110
|
+
eprintln!(" {BOLD}Current values (by {by}):{RESET}");
|
|
111
|
+
|
|
112
|
+
for (index, label) in labels.iter().enumerate() {
|
|
113
|
+
let context = context_values.get(index).map(|c| c.as_slice()).unwrap_or(&[]);
|
|
114
|
+
eprintln!(" {}", self.format_label_line(index, label, context));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let csv: String = labels.join(",");
|
|
118
|
+
|
|
119
|
+
eprintln!("{context_hint}");
|
|
120
|
+
eprintln!();
|
|
121
|
+
eprintln!(" {BOLD}To sort alphabetically:{RESET}");
|
|
122
|
+
eprintln!(
|
|
123
|
+
" yerba sort \"{}\"{selector_display} --by \"{by}\" --order asc",
|
|
124
|
+
self.file
|
|
125
|
+
);
|
|
126
|
+
eprintln!(
|
|
127
|
+
" yerba sort \"{}\"{selector_display} --by \"{by}\" --order desc",
|
|
128
|
+
self.file
|
|
129
|
+
);
|
|
130
|
+
eprintln!();
|
|
131
|
+
eprintln!(" {BOLD}To reorder explicitly:{RESET}");
|
|
132
|
+
eprintln!(
|
|
133
|
+
" yerba sort \"{}\"{selector_display} --by \"{by}\" --order \"{csv}\"",
|
|
134
|
+
self.file
|
|
135
|
+
);
|
|
136
|
+
eprintln!();
|
|
137
|
+
eprintln!(" {BOLD}To move individual items:{RESET}");
|
|
138
|
+
eprintln!(
|
|
139
|
+
" yerba move \"{}\"{selector_display} <item> --before/--after <target>",
|
|
140
|
+
self.file
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
process::exit(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fn run_sort(self) {
|
|
147
|
+
use super::color::*;
|
|
148
|
+
|
|
41
149
|
let selector = self.selector.as_deref().unwrap_or("");
|
|
42
|
-
|
|
150
|
+
|
|
151
|
+
let sort_fields: Vec<yerba::SortField> = if self.by.is_empty() {
|
|
152
|
+
Vec::new()
|
|
153
|
+
} else {
|
|
154
|
+
self
|
|
155
|
+
.by
|
|
156
|
+
.iter()
|
|
157
|
+
.enumerate()
|
|
158
|
+
.map(|(index, field)| {
|
|
159
|
+
let direction = self.order.get(index).map(|s| s.as_str());
|
|
160
|
+
|
|
161
|
+
match direction {
|
|
162
|
+
Some("desc" | "descending") => yerba::SortField::desc(field),
|
|
163
|
+
Some("asc" | "ascending") | None => yerba::SortField::asc(field),
|
|
164
|
+
Some(other) => {
|
|
165
|
+
eprintln!("{RED}Error:{RESET} invalid sort direction \"{other}\". Use \"asc\" or \"desc\"");
|
|
166
|
+
process::exit(1);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
.collect()
|
|
171
|
+
};
|
|
43
172
|
|
|
44
173
|
for resolved_file in resolve_files(&self.file) {
|
|
45
174
|
let mut document = parse_file(&resolved_file);
|
|
46
175
|
|
|
176
|
+
if sort_fields.is_empty() {
|
|
177
|
+
let first_item_selector = if selector.is_empty() {
|
|
178
|
+
"[0]".to_string()
|
|
179
|
+
} else {
|
|
180
|
+
format!("{}[0]", selector)
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
match document.get_value(&first_item_selector) {
|
|
184
|
+
Some(first) if first.is_mapping() => {
|
|
185
|
+
eprintln!("{RED}Error:{RESET} --by is required to sort a sequence of maps");
|
|
186
|
+
eprintln!();
|
|
187
|
+
|
|
188
|
+
let selectors = document.selectors();
|
|
189
|
+
let prefix = if selector.is_empty() { "[]." } else { "" };
|
|
190
|
+
let fields: Vec<&String> = selectors
|
|
191
|
+
.iter()
|
|
192
|
+
.filter(|s| {
|
|
193
|
+
let check = if prefix.is_empty() {
|
|
194
|
+
format!("{}[].", selector)
|
|
195
|
+
} else {
|
|
196
|
+
prefix.to_string()
|
|
197
|
+
};
|
|
198
|
+
s.starts_with(&check) && !s[check.len()..].contains('.') && !s[check.len()..].contains('[')
|
|
199
|
+
})
|
|
200
|
+
.collect();
|
|
201
|
+
|
|
202
|
+
if !fields.is_empty() {
|
|
203
|
+
eprintln!(" {BOLD}Available fields:{RESET}");
|
|
204
|
+
|
|
205
|
+
for field in &fields {
|
|
206
|
+
let short = field.rsplit_once('.').map(|(_, f)| f).unwrap_or(field);
|
|
207
|
+
eprintln!(" .{short}");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
process::exit(1);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
None if selector.is_empty() => {
|
|
215
|
+
eprintln!(
|
|
216
|
+
"{RED}Error:{RESET} no sequence found at root level in {}",
|
|
217
|
+
resolved_file
|
|
218
|
+
);
|
|
219
|
+
eprintln!();
|
|
220
|
+
eprintln!(" {DIM}Specify a selector for the sequence to sort:{RESET}");
|
|
221
|
+
eprintln!(" yerba sort \"{}\" \"<selector>\"", self.file);
|
|
222
|
+
eprintln!();
|
|
223
|
+
|
|
224
|
+
super::show_similar_selectors(&resolved_file, &document, "[]");
|
|
225
|
+
|
|
226
|
+
process::exit(1);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
_ => {}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
47
233
|
if document.sort_items(selector, &sort_fields, self.case_sensitive).is_ok() {
|
|
48
234
|
output(&resolved_file, &document, self.dry_run);
|
|
49
235
|
}
|
|
50
236
|
}
|
|
51
237
|
}
|
|
238
|
+
|
|
239
|
+
fn run_reorder(self) {
|
|
240
|
+
use super::color::*;
|
|
241
|
+
|
|
242
|
+
if self.by.len() != 1 {
|
|
243
|
+
eprintln!("{RED}Error:{RESET} explicit --order requires exactly one --by field");
|
|
244
|
+
|
|
245
|
+
process::exit(1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if self.order.len() != 1 {
|
|
249
|
+
eprintln!("{RED}Error:{RESET} explicit --order must be a single comma-separated list");
|
|
250
|
+
|
|
251
|
+
process::exit(1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let by = &self.by[0];
|
|
255
|
+
let order = &self.order[0];
|
|
256
|
+
let selector = self.selector.as_deref().unwrap_or("");
|
|
257
|
+
|
|
258
|
+
let items_selector = if selector.is_empty() {
|
|
259
|
+
"[]".to_string()
|
|
260
|
+
} else {
|
|
261
|
+
format!("{}[]", selector)
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
let mut document = parse_file(&self.file);
|
|
265
|
+
let mut seen = std::collections::HashSet::new();
|
|
266
|
+
|
|
267
|
+
let (labels, _, _) = self.resolve_labels(&document, by, selector, &items_selector);
|
|
268
|
+
let duplicates: Vec<&String> = labels.iter().filter(|label| !seen.insert(label.as_str())).collect();
|
|
269
|
+
|
|
270
|
+
if !duplicates.is_empty() {
|
|
271
|
+
eprintln!("{RED}Error:{RESET} --order requires unique values for {by}, but found duplicates");
|
|
272
|
+
eprintln!();
|
|
273
|
+
eprintln!(" {BOLD}Duplicate values:{RESET}");
|
|
274
|
+
|
|
275
|
+
for label in &duplicates {
|
|
276
|
+
eprintln!(" {label}");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
eprintln!();
|
|
280
|
+
eprintln!(" {DIM}Use \"yerba sort\" with --by instead to sort by field, or");
|
|
281
|
+
eprintln!(" choose a --by field with unique values (e.g. \".id\"){RESET}");
|
|
282
|
+
|
|
283
|
+
process::exit(1);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let values_with_commas: Vec<&String> = labels.iter().filter(|l| l.contains(',')).collect();
|
|
287
|
+
|
|
288
|
+
if !values_with_commas.is_empty() {
|
|
289
|
+
let selector_display = if selector.is_empty() {
|
|
290
|
+
String::new()
|
|
291
|
+
} else {
|
|
292
|
+
format!(" \"{selector}\"")
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
eprintln!("{RED}Error:{RESET} some values for {by} contain commas, which conflicts with --order parsing");
|
|
296
|
+
eprintln!();
|
|
297
|
+
eprintln!(" {BOLD}Values with commas:{RESET}");
|
|
298
|
+
|
|
299
|
+
for label in &values_with_commas {
|
|
300
|
+
eprintln!(" {label}");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
eprintln!();
|
|
304
|
+
eprintln!(" {BOLD}Use yerba move to reorder individual items instead:{RESET}");
|
|
305
|
+
eprintln!(
|
|
306
|
+
" yerba move \"{}\"{selector_display} <item> --before/--after <target>",
|
|
307
|
+
self.file
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
process::exit(1);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
let desired_order: Vec<&str> = order.split(',').map(|s| s.trim()).collect();
|
|
314
|
+
|
|
315
|
+
let mut used = vec![false; labels.len()];
|
|
316
|
+
let mut moves: Vec<usize> = Vec::new();
|
|
317
|
+
|
|
318
|
+
for desired in &desired_order {
|
|
319
|
+
let found = labels
|
|
320
|
+
.iter()
|
|
321
|
+
.enumerate()
|
|
322
|
+
.find(|(index, label)| label.as_str() == *desired && !used[*index]);
|
|
323
|
+
|
|
324
|
+
if let Some((index, _)) = found {
|
|
325
|
+
moves.push(index);
|
|
326
|
+
used[index] = true;
|
|
327
|
+
} else {
|
|
328
|
+
eprintln!("{RED}Error:{RESET} no item found with {by} == \"{desired}\"");
|
|
329
|
+
eprintln!();
|
|
330
|
+
eprintln!(" {BOLD}Available values:{RESET}");
|
|
331
|
+
|
|
332
|
+
for (index, label) in labels.iter().enumerate() {
|
|
333
|
+
eprintln!(" {DIM}{index}:{RESET} {label}");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
process::exit(1);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
let missing: Vec<&String> = labels
|
|
341
|
+
.iter()
|
|
342
|
+
.enumerate()
|
|
343
|
+
.filter(|(index, _)| !used[*index])
|
|
344
|
+
.map(|(_, label)| label)
|
|
345
|
+
.collect();
|
|
346
|
+
|
|
347
|
+
if !missing.is_empty() {
|
|
348
|
+
eprintln!(
|
|
349
|
+
"{RED}Error:{RESET} --order must specify all {} items, but {} are missing",
|
|
350
|
+
labels.len(),
|
|
351
|
+
missing.len()
|
|
352
|
+
);
|
|
353
|
+
eprintln!();
|
|
354
|
+
eprintln!(" {BOLD}Missing values:{RESET}");
|
|
355
|
+
|
|
356
|
+
for label in &missing {
|
|
357
|
+
eprintln!(" {label}");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
eprintln!();
|
|
361
|
+
eprintln!(" {BOLD}All values (by {by}):{RESET}");
|
|
362
|
+
|
|
363
|
+
for label in &labels {
|
|
364
|
+
eprintln!(" {label}");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
eprintln!();
|
|
368
|
+
eprintln!(" {BOLD}To move individual items, use:{RESET}");
|
|
369
|
+
eprintln!(" yerba move <file> <selector> <item> --before/--after <target>");
|
|
370
|
+
|
|
371
|
+
process::exit(1);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
let container = if selector.is_empty() { "" } else { selector };
|
|
375
|
+
|
|
376
|
+
for target in 0..moves.len() {
|
|
377
|
+
let source = moves[target];
|
|
378
|
+
|
|
379
|
+
if source != target {
|
|
380
|
+
let result = document.move_item(container, source, target);
|
|
381
|
+
|
|
382
|
+
if let Err(error) = result {
|
|
383
|
+
eprintln!("{RED}Error:{RESET} {}", error);
|
|
384
|
+
process::exit(1);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
for item in moves.iter_mut().skip(target + 1) {
|
|
388
|
+
if *item >= target && *item < source {
|
|
389
|
+
*item += 1;
|
|
390
|
+
} else if *item == source {
|
|
391
|
+
*item = target;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
output(&self.file, &document, self.dry_run);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
fn resolve_labels(
|
|
401
|
+
&self,
|
|
402
|
+
document: &yerba::Document,
|
|
403
|
+
by: &str,
|
|
404
|
+
selector: &str,
|
|
405
|
+
items_selector: &str,
|
|
406
|
+
) -> (Vec<String>, Vec<Vec<String>>, String) {
|
|
407
|
+
use super::color::*;
|
|
408
|
+
|
|
409
|
+
let items = document.get_values(items_selector);
|
|
410
|
+
|
|
411
|
+
if items.is_empty() {
|
|
412
|
+
if selector.is_empty() {
|
|
413
|
+
eprintln!("{RED}Error:{RESET} no sequence found at root level");
|
|
414
|
+
eprintln!();
|
|
415
|
+
eprintln!(" {DIM}If the file is a map, specify which sequence to sort:{RESET}");
|
|
416
|
+
eprintln!(
|
|
417
|
+
" yerba sort \"{}\" \"<selector>\" --by \"{by}\" --order asc",
|
|
418
|
+
self.file
|
|
419
|
+
);
|
|
420
|
+
eprintln!();
|
|
421
|
+
|
|
422
|
+
super::show_similar_selectors(&self.file, document, "[]");
|
|
423
|
+
} else {
|
|
424
|
+
eprintln!("{RED}Error:{RESET} no sequence found at selector: {selector}");
|
|
425
|
+
|
|
426
|
+
super::show_similar_selectors(&self.file, document, selector);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
process::exit(1);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
let by_is_scalar = by == ".";
|
|
433
|
+
|
|
434
|
+
if !by_is_scalar {
|
|
435
|
+
let by_selector = if selector.is_empty() {
|
|
436
|
+
format!("[].{}", by.strip_prefix('.').unwrap_or(by))
|
|
437
|
+
} else {
|
|
438
|
+
format!("{}[].{}", selector, by.strip_prefix('.').unwrap_or(by))
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
if !document.exists(&by_selector) {
|
|
442
|
+
eprintln!("{RED}Error:{RESET} field \"{by}\" not found in items");
|
|
443
|
+
|
|
444
|
+
super::show_similar_selectors(&self.file, document, &by_selector);
|
|
445
|
+
|
|
446
|
+
process::exit(1);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
let labels: Vec<String> = items
|
|
451
|
+
.iter()
|
|
452
|
+
.map(|item| {
|
|
453
|
+
if by_is_scalar {
|
|
454
|
+
match item {
|
|
455
|
+
serde_yaml::Value::String(string) => string.clone(),
|
|
456
|
+
_ => serde_json::to_string(&yerba::json::yaml_to_json(item)).unwrap_or_default(),
|
|
457
|
+
}
|
|
458
|
+
} else {
|
|
459
|
+
let field = by.strip_prefix('.').unwrap_or(by);
|
|
460
|
+
|
|
461
|
+
yerba::json::resolve_select_field(item, field)
|
|
462
|
+
.as_str()
|
|
463
|
+
.unwrap_or("")
|
|
464
|
+
.to_string()
|
|
465
|
+
}
|
|
466
|
+
})
|
|
467
|
+
.collect();
|
|
468
|
+
|
|
469
|
+
let context_values: Vec<Vec<String>> = items
|
|
470
|
+
.iter()
|
|
471
|
+
.map(|item| {
|
|
472
|
+
self
|
|
473
|
+
.context
|
|
474
|
+
.iter()
|
|
475
|
+
.map(|context| {
|
|
476
|
+
let field = context.strip_prefix('.').unwrap_or(context);
|
|
477
|
+
|
|
478
|
+
let value = yerba::json::resolve_select_field(item, field);
|
|
479
|
+
|
|
480
|
+
match &value {
|
|
481
|
+
serde_json::Value::String(string) => string.clone(),
|
|
482
|
+
serde_json::Value::Null => String::new(),
|
|
483
|
+
_ => serde_json::to_string(&value).unwrap_or_default(),
|
|
484
|
+
}
|
|
485
|
+
})
|
|
486
|
+
.collect()
|
|
487
|
+
})
|
|
488
|
+
.collect();
|
|
489
|
+
|
|
490
|
+
let selector_display = if selector.is_empty() {
|
|
491
|
+
String::new()
|
|
492
|
+
} else {
|
|
493
|
+
format!(" \"{selector}\"")
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
(labels, context_values, selector_display)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
fn format_label_line(&self, index: usize, label: &str, context: &[String]) -> String {
|
|
500
|
+
use super::color::*;
|
|
501
|
+
|
|
502
|
+
if context.is_empty() {
|
|
503
|
+
format!("{DIM}[{index}]{RESET} {label}")
|
|
504
|
+
} else {
|
|
505
|
+
let context = context
|
|
506
|
+
.iter()
|
|
507
|
+
.filter(|c| !c.is_empty())
|
|
508
|
+
.cloned()
|
|
509
|
+
.collect::<Vec<_>>()
|
|
510
|
+
.join(", ");
|
|
511
|
+
|
|
512
|
+
if context.is_empty() {
|
|
513
|
+
format!("{DIM}[{index}]{RESET} {label}")
|
|
514
|
+
} else {
|
|
515
|
+
format!("{DIM}[{index}]{RESET} {label} {DIM}{context}{RESET}")
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
52
519
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
|
14
|
+
.sort_by(|(a_item, a_distance), (b_item, b_distance)| a_distance.cmp(b_distance).then_with(|| a_item.cmp(b_item)));
|
|
15
|
+
|
|
16
|
+
scored.into_iter().next().map(|(item, _)| item)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
pub fn didyoumean_ranked(input: &str, list: &[String], threshold: usize) -> Vec<String> {
|
|
20
|
+
if list.is_empty() {
|
|
21
|
+
return Vec::new();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let input_lower = input.to_lowercase();
|
|
25
|
+
|
|
26
|
+
let mut scored: Vec<(String, usize)> = list
|
|
27
|
+
.iter()
|
|
28
|
+
.map(|item| (item.clone(), levenshtein(&input_lower, &item.to_lowercase())))
|
|
29
|
+
.filter(|(_, distance)| *distance <= threshold)
|
|
30
|
+
.collect();
|
|
31
|
+
|
|
32
|
+
scored
|
|
33
|
+
.sort_by(|(a_item, a_distance), (b_item, b_distance)| a_distance.cmp(b_distance).then_with(|| a_item.cmp(b_item)));
|
|
34
|
+
|
|
35
|
+
scored.into_iter().map(|(item, _)| item).collect()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn levenshtein(a: &str, b: &str) -> usize {
|
|
39
|
+
let b_length = b.len();
|
|
40
|
+
let mut previous: Vec<usize> = (0..=b_length).collect();
|
|
41
|
+
let mut current = vec![0; b_length + 1];
|
|
42
|
+
|
|
43
|
+
for (i, a_character) in a.chars().enumerate() {
|
|
44
|
+
current[0] = i + 1;
|
|
45
|
+
|
|
46
|
+
for (j, b_character) in b.chars().enumerate() {
|
|
47
|
+
let cost = if a_character == b_character { 0 } else { 1 };
|
|
48
|
+
current[j + 1] = (previous[j] + cost).min(previous[j + 1] + 1).min(current[j] + 1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
std::mem::swap(&mut previous, &mut current);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
previous[b_length]
|
|
55
|
+
}
|