yerba 0.2.2-arm-linux-gnu → 0.4.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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +167 -12
  3. data/exe/arm-linux-gnu/yerba +0 -0
  4. data/ext/yerba/extconf.rb +39 -12
  5. data/ext/yerba/include/yerba.h +21 -10
  6. data/ext/yerba/yerba.c +91 -25
  7. data/lib/yerba/3.2/yerba.so +0 -0
  8. data/lib/yerba/3.3/yerba.so +0 -0
  9. data/lib/yerba/3.4/yerba.so +0 -0
  10. data/lib/yerba/4.0/yerba.so +0 -0
  11. data/lib/yerba/collection.rb +35 -0
  12. data/lib/yerba/document.rb +16 -0
  13. data/lib/yerba/sequence.rb +169 -1
  14. data/lib/yerba/version.rb +1 -1
  15. data/lib/yerba.rb +7 -2
  16. data/rust/Cargo.lock +1 -1
  17. data/rust/Cargo.toml +2 -2
  18. data/rust/cbindgen.toml +1 -0
  19. data/rust/rustfmt.toml +1 -1
  20. data/rust/src/commands/blank_lines.rs +1 -4
  21. data/rust/src/commands/delete.rs +9 -4
  22. data/rust/src/commands/directives.rs +61 -0
  23. data/rust/src/commands/get.rs +52 -26
  24. data/rust/src/commands/insert.rs +8 -4
  25. data/rust/src/commands/mod.rs +71 -9
  26. data/rust/src/commands/move_item.rs +2 -1
  27. data/rust/src/commands/move_key.rs +2 -1
  28. data/rust/src/commands/quote_style.rs +12 -6
  29. data/rust/src/commands/remove.rs +8 -4
  30. data/rust/src/commands/rename.rs +8 -4
  31. data/rust/src/commands/selectors.rs +158 -0
  32. data/rust/src/commands/set.rs +33 -16
  33. data/rust/src/commands/sort.rs +342 -10
  34. data/rust/src/didyoumean.rs +53 -0
  35. data/rust/src/document/condition.rs +139 -0
  36. data/rust/src/document/delete.rs +91 -0
  37. data/rust/src/document/get.rs +262 -0
  38. data/rust/src/document/insert.rs +314 -0
  39. data/rust/src/document/mod.rs +784 -0
  40. data/rust/src/document/set.rs +90 -0
  41. data/rust/src/document/sort.rs +607 -0
  42. data/rust/src/document/style.rs +473 -0
  43. data/rust/src/error.rs +35 -7
  44. data/rust/src/ffi.rs +213 -520
  45. data/rust/src/json.rs +1 -7
  46. data/rust/src/lib.rs +89 -2
  47. data/rust/src/main.rs +2 -0
  48. data/rust/src/quote_style.rs +83 -7
  49. data/rust/src/selector.rs +2 -7
  50. data/rust/src/syntax.rs +41 -21
  51. data/rust/src/yerbafile.rs +39 -18
  52. metadata +13 -3
  53. data/rust/src/document.rs +0 -2237
@@ -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:desc,title"
13
- yerba sort videos.yml "[].speakers"
14
- yerba sort videos.yml "[].speakers" --by "name"
15
- yerba sort videos.yml --by "kind,date:desc,title" --dry-run
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 by field(s)",
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
- /// Comma-separated sort fields, optionally with :desc (e.g. "date:desc,title")
30
- #[arg(long)]
31
- by: Option<String>,
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 sort_fields = self.by.as_deref().map(yerba::SortField::parse_list).unwrap_or_default();
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
+ }