yerba 0.7.6-arm-linux-gnu → 0.8.1-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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +66 -2
  3. data/exe/arm-linux-gnu/yerba +0 -0
  4. data/ext/yerba/include/yerba.h +8 -0
  5. data/ext/yerba/yerba.c +70 -8
  6. data/lib/yerba/3.2/yerba.so +0 -0
  7. data/lib/yerba/3.3/yerba.so +0 -0
  8. data/lib/yerba/3.4/yerba.so +0 -0
  9. data/lib/yerba/4.0/yerba.so +0 -0
  10. data/lib/yerba/map.rb +15 -10
  11. data/lib/yerba/node.rb +2 -2
  12. data/lib/yerba/sequence.rb +13 -1
  13. data/lib/yerba/version.rb +1 -1
  14. data/rust/Cargo.lock +2 -342
  15. data/rust/Cargo.toml +15 -6
  16. data/rust/src/commands/blank_lines.rs +2 -2
  17. data/rust/src/commands/directives.rs +4 -4
  18. data/rust/src/commands/get.rs +17 -10
  19. data/rust/src/commands/init.rs +4 -4
  20. data/rust/src/commands/insert.rs +6 -6
  21. data/rust/src/commands/location.rs +2 -2
  22. data/rust/src/commands/mate.rs +7 -7
  23. data/rust/src/commands/mod.rs +119 -90
  24. data/rust/src/commands/move_key.rs +9 -6
  25. data/rust/src/commands/quote_style.rs +4 -5
  26. data/rust/src/commands/rename.rs +3 -3
  27. data/rust/src/commands/schema.rs +6 -6
  28. data/rust/src/commands/selectors.rs +2 -2
  29. data/rust/src/commands/set.rs +12 -1
  30. data/rust/src/commands/sort.rs +36 -42
  31. data/rust/src/commands/sort_keys.rs +2 -2
  32. data/rust/src/commands/ui.rs +157 -0
  33. data/rust/src/commands/unique.rs +7 -7
  34. data/rust/src/commands/version.rs +13 -3
  35. data/rust/src/document/condition.rs +89 -32
  36. data/rust/src/document/delete.rs +6 -1
  37. data/rust/src/document/get.rs +94 -26
  38. data/rust/src/document/insert.rs +26 -0
  39. data/rust/src/document/mod.rs +73 -20
  40. data/rust/src/document/set.rs +91 -2
  41. data/rust/src/error.rs +17 -0
  42. data/rust/src/ffi.rs +76 -9
  43. data/rust/src/json.rs +10 -1
  44. data/rust/src/lib.rs +23 -7
  45. data/rust/src/main.rs +1 -0
  46. data/rust/src/quote_style.rs +12 -11
  47. data/rust/src/selector.rs +19 -3
  48. data/rust/src/syntax.rs +178 -26
  49. data/rust/src/yaml_writer.rs +4 -4
  50. data/rust/src/yerbafile.rs +30 -6
  51. metadata +3 -2
@@ -1,3 +1,4 @@
1
+ use super::ui;
1
2
  use std::process;
2
3
  use std::sync::LazyLock;
3
4
 
@@ -53,14 +54,14 @@ fn is_explicit_reorder(orders: &[String]) -> bool {
53
54
 
54
55
  impl Args {
55
56
  pub fn run(self) {
56
- use super::color::*;
57
+ use super::ui;
57
58
 
58
59
  if self.by.is_empty() && self.order.is_empty() && self.selector.is_none() {
59
60
  let document = parse_file(&self.file);
60
61
 
61
- eprintln!("{RED}Error:{RESET} specify a selector, --by, or --order");
62
+ eprintln!("{} specify a selector, --by, or --order", ui::failure("Error:"));
62
63
  eprintln!();
63
- eprintln!(" {BOLD}Examples:{RESET}");
64
+ eprintln!(" {}", ui::strong("Examples:"));
64
65
  eprintln!(" yerba sort \"{}\" \"tags\"", self.file);
65
66
  eprintln!(" yerba sort \"{}\" --by \".title\" --order asc", self.file);
66
67
  eprintln!();
@@ -80,10 +81,8 @@ impl Args {
80
81
  }
81
82
 
82
83
  fn show_values(self) {
83
- use super::color::*;
84
-
85
84
  if self.by.len() != 1 {
86
- eprintln!("{RED}Error:{RESET} --order is required when using --by");
85
+ eprintln!("{} --order is required when using --by", ui::failure("Error:"));
87
86
  process::exit(1);
88
87
  }
89
88
 
@@ -94,14 +93,14 @@ impl Args {
94
93
  let (labels, context_values, selector_display) = self.resolve_labels(&document, by, selector, &items_selector);
95
94
 
96
95
  let context_hint = if self.context.is_empty() {
97
- format!("\n\n {DIM}Add --context \".field\" to show additional fields alongside values{RESET}")
96
+ format!("\n\n {}", ui::subtle("Add --context \".field\" to show additional fields alongside values"))
98
97
  } else {
99
98
  String::new()
100
99
  };
101
100
 
102
- eprintln!("{RED}Error:{RESET} --order is required when using --by");
101
+ eprintln!("{} --order is required when using --by", ui::failure("Error:"));
103
102
  eprintln!();
104
- eprintln!(" {BOLD}Current values (by {by}):{RESET}");
103
+ eprintln!(" {}", ui::strong(format!("Current values (by {}):", by)));
105
104
 
106
105
  for (index, label) in labels.iter().enumerate() {
107
106
  let context = context_values.get(index).map(|c| c.as_slice()).unwrap_or(&[]);
@@ -113,22 +112,20 @@ impl Args {
113
112
 
114
113
  eprintln!("{context_hint}");
115
114
  eprintln!();
116
- eprintln!(" {BOLD}To sort alphabetically:{RESET}");
115
+ eprintln!(" {}", ui::strong("To sort alphabetically:"));
117
116
  eprintln!(" yerba sort \"{}\"{selector_display} --by \"{by}\" --order asc", self.file);
118
117
  eprintln!(" yerba sort \"{}\"{selector_display} --by \"{by}\" --order desc", self.file);
119
118
  eprintln!();
120
- eprintln!(" {BOLD}To reorder explicitly:{RESET}");
119
+ eprintln!(" {}", ui::strong("To reorder explicitly:"));
121
120
  eprintln!(" yerba sort \"{}\"{selector_display} --by \"{by}\" --order \"{csv}\"", self.file);
122
121
  eprintln!();
123
- eprintln!(" {BOLD}To move individual items:{RESET}");
122
+ eprintln!(" {}", ui::strong("To move individual items:"));
124
123
  eprintln!(" yerba move \"{}\"{selector_display} <item> --before/--after <target>", self.file);
125
124
 
126
125
  process::exit(1);
127
126
  }
128
127
 
129
128
  fn run_sort(self) {
130
- use super::color::*;
131
-
132
129
  let selector = self.selector.as_deref().unwrap_or("");
133
130
 
134
131
  let sort_fields: Vec<yerba::SortField> = if self.by.is_empty() {
@@ -145,7 +142,7 @@ impl Args {
145
142
  Some("desc" | "descending") => yerba::SortField::desc(field),
146
143
  Some("asc" | "ascending") | None => yerba::SortField::asc(field),
147
144
  Some(other) => {
148
- eprintln!("{RED}Error:{RESET} invalid sort direction \"{other}\". Use \"asc\" or \"desc\"");
145
+ eprintln!("{} invalid sort direction \"{other}\". Use \"asc\" or \"desc\"", ui::failure("Error:"));
149
146
  process::exit(1);
150
147
  }
151
148
  }
@@ -161,7 +158,7 @@ impl Args {
161
158
 
162
159
  match document.get_value(&first_item_selector) {
163
160
  Some(first) if first.is_mapping() => {
164
- eprintln!("{RED}Error:{RESET} --by is required to sort a sequence of maps");
161
+ eprintln!("{} --by is required to sort a sequence of maps", ui::failure("Error:"));
165
162
  eprintln!();
166
163
 
167
164
  let selectors = document.selectors();
@@ -175,7 +172,7 @@ impl Args {
175
172
  .collect();
176
173
 
177
174
  if !fields.is_empty() {
178
- eprintln!(" {BOLD}Available fields:{RESET}");
175
+ eprintln!(" {}", ui::strong("Available fields:"));
179
176
 
180
177
  for field in &fields {
181
178
  let short = field.rsplit_once('.').map(|(_, f)| f).unwrap_or(field);
@@ -187,9 +184,9 @@ impl Args {
187
184
  }
188
185
 
189
186
  None if selector.is_empty() => {
190
- eprintln!("{RED}Error:{RESET} no sequence found at root level in {}", resolved_file);
187
+ eprintln!("{} no sequence found at root level in {}", ui::failure("Error:"), resolved_file);
191
188
  eprintln!();
192
- eprintln!(" {DIM}Specify a selector for the sequence to sort:{RESET}");
189
+ eprintln!(" {}", ui::subtle("Specify a selector for the sequence to sort:"));
193
190
  eprintln!(" yerba sort \"{}\" \"<selector>\"", self.file);
194
191
  eprintln!();
195
192
 
@@ -209,16 +206,14 @@ impl Args {
209
206
  }
210
207
 
211
208
  fn run_reorder(self) {
212
- use super::color::*;
213
-
214
209
  if self.by.len() != 1 {
215
- eprintln!("{RED}Error:{RESET} explicit --order requires exactly one --by field");
210
+ eprintln!("{} explicit --order requires exactly one --by field", ui::failure("Error:"));
216
211
 
217
212
  process::exit(1);
218
213
  }
219
214
 
220
215
  if self.order.len() != 1 {
221
- eprintln!("{RED}Error:{RESET} explicit --order must be a single comma-separated list");
216
+ eprintln!("{} explicit --order must be a single comma-separated list", ui::failure("Error:"));
222
217
 
223
218
  process::exit(1);
224
219
  }
@@ -236,17 +231,17 @@ impl Args {
236
231
  let duplicates: Vec<&String> = labels.iter().filter(|label| !seen.insert(label.as_str())).collect();
237
232
 
238
233
  if !duplicates.is_empty() {
239
- eprintln!("{RED}Error:{RESET} --order requires unique values for {by}, but found duplicates");
234
+ eprintln!("{} --order requires unique values for {by}, but found duplicates", ui::failure("Error:"));
240
235
  eprintln!();
241
- eprintln!(" {BOLD}Duplicate values:{RESET}");
236
+ eprintln!(" {}", ui::strong("Duplicate values:"));
242
237
 
243
238
  for label in &duplicates {
244
239
  eprintln!(" {label}");
245
240
  }
246
241
 
247
242
  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}");
243
+ eprintln!(" {}", ui::subtle("Use \"yerba sort\" with --by instead to sort by field, or"));
244
+ eprintln!(" {}", ui::subtle("choose a --by field with unique values (e.g. \".id\")"));
250
245
 
251
246
  process::exit(1);
252
247
  }
@@ -256,16 +251,19 @@ impl Args {
256
251
  if !values_with_commas.is_empty() {
257
252
  let selector_display = if selector.is_empty() { String::new() } else { format!(" \"{selector}\"") };
258
253
 
259
- eprintln!("{RED}Error:{RESET} some values for {by} contain commas, which conflicts with --order parsing");
254
+ eprintln!(
255
+ "{} some values for {by} contain commas, which conflicts with --order parsing",
256
+ ui::failure("Error:")
257
+ );
260
258
  eprintln!();
261
- eprintln!(" {BOLD}Values with commas:{RESET}");
259
+ eprintln!(" {}", ui::strong("Values with commas:"));
262
260
 
263
261
  for label in &values_with_commas {
264
262
  eprintln!(" {label}");
265
263
  }
266
264
 
267
265
  eprintln!();
268
- eprintln!(" {BOLD}Use yerba move to reorder individual items instead:{RESET}");
266
+ eprintln!(" {}", ui::strong("Use yerba move to reorder individual items instead:"));
269
267
  eprintln!(" yerba move \"{}\"{selector_display} <item> --before/--after <target>", self.file);
270
268
 
271
269
  process::exit(1);
@@ -277,28 +275,26 @@ impl Args {
277
275
  match document.reorder_items(container, by, &desired_order) {
278
276
  Ok(()) => output(&self.file, &document, self.dry_run),
279
277
  Err(error) => {
280
- eprintln!("{RED}Error:{RESET} {}", error);
278
+ eprintln!("{} {}", ui::failure("Error:"), error);
281
279
  process::exit(1);
282
280
  }
283
281
  }
284
282
  }
285
283
 
286
284
  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
285
  let items = document.get_values(items_selector);
290
286
 
291
287
  if items.is_empty() {
292
288
  if selector.is_empty() {
293
- eprintln!("{RED}Error:{RESET} no sequence found at root level");
289
+ eprintln!("{} no sequence found at root level", ui::failure("Error:"));
294
290
  eprintln!();
295
- eprintln!(" {DIM}If the file is a map, specify which sequence to sort:{RESET}");
291
+ eprintln!(" {}", ui::subtle("If the file is a map, specify which sequence to sort:"));
296
292
  eprintln!(" yerba sort \"{}\" \"<selector>\" --by \"{by}\" --order asc", self.file);
297
293
  eprintln!();
298
294
 
299
295
  super::show_similar_selectors(&self.file, document, "[]");
300
296
  } else {
301
- eprintln!("{RED}Error:{RESET} no sequence found at selector: {selector}");
297
+ eprintln!("{} no sequence found at selector: {selector}", ui::failure("Error:"));
302
298
 
303
299
  super::show_similar_selectors(&self.file, document, selector);
304
300
  }
@@ -316,7 +312,7 @@ impl Args {
316
312
  };
317
313
 
318
314
  if !document.exists(&by_selector) {
319
- eprintln!("{RED}Error:{RESET} field \"{by}\" not found in items");
315
+ eprintln!("{} field \"{by}\" not found in items", ui::failure("Error:"));
320
316
 
321
317
  super::show_similar_selectors(&self.file, document, &by_selector);
322
318
 
@@ -367,17 +363,15 @@ impl Args {
367
363
  }
368
364
 
369
365
  fn format_label_line(&self, index: usize, label: &str, context: &[String]) -> String {
370
- use super::color::*;
371
-
372
366
  if context.is_empty() {
373
- format!("{DIM}[{index}]{RESET} {label}")
367
+ format!("{} {}", ui::subtle(format!("[{}]", index)), label)
374
368
  } else {
375
369
  let context = context.iter().filter(|c| !c.is_empty()).cloned().collect::<Vec<_>>().join(", ");
376
370
 
377
371
  if context.is_empty() {
378
- format!("{DIM}[{index}]{RESET} {label}")
372
+ format!("{} {}", ui::subtle(format!("[{}]", index)), label)
379
373
  } else {
380
- format!("{DIM}[{index}]{RESET} {label} {DIM}{context}{RESET}")
374
+ format!("{} {} {}", ui::subtle(format!("[{}]", index)), label, ui::subtle(context))
381
375
  }
382
376
  }
383
377
  }
@@ -1,3 +1,4 @@
1
+ use super::ui;
1
2
  use std::process;
2
3
  use std::sync::LazyLock;
3
4
 
@@ -41,8 +42,7 @@ impl Args {
41
42
  let document = parse_file(resolved_file);
42
43
 
43
44
  if let Err(error) = document.validate_sort_keys(&self.selector, &key_order) {
44
- use super::color::*;
45
- eprintln!("{RED}Error in {}{RESET}: {}", resolved_file, error);
45
+ eprintln!("{}: {}", ui::failure(format!("Error in {}", resolved_file)), error);
46
46
  has_errors = true;
47
47
  }
48
48
  }
@@ -0,0 +1,157 @@
1
+ use std::io::IsTerminal;
2
+ use std::sync::OnceLock;
3
+
4
+ fn colored() -> bool {
5
+ static COLORED: OnceLock<bool> = OnceLock::new();
6
+
7
+ *COLORED.get_or_init(|| {
8
+ if std::env::var_os("NO_COLOR").is_some() {
9
+ return false;
10
+ }
11
+
12
+ if std::env::var_os("CLICOLOR_FORCE").is_some_and(|value| value != "0") {
13
+ return true;
14
+ }
15
+
16
+ std::io::stderr().is_terminal() || super::is_github_actions()
17
+ })
18
+ }
19
+
20
+ #[derive(Clone, Copy)]
21
+ pub struct Color(&'static str);
22
+
23
+ pub const GREEN: Color = Color("32");
24
+ pub const YELLOW: Color = Color("33");
25
+ pub const RED: Color = Color("31");
26
+ pub const CYAN: Color = Color("36");
27
+
28
+ pub struct Style {
29
+ color: Option<Color>,
30
+ bold: bool,
31
+ dim: bool,
32
+ }
33
+
34
+ impl Style {
35
+ pub const fn plain() -> Self {
36
+ Style {
37
+ color: None,
38
+ bold: false,
39
+ dim: false,
40
+ }
41
+ }
42
+
43
+ pub const fn fg(color: Color) -> Self {
44
+ Style {
45
+ color: Some(color),
46
+ bold: false,
47
+ dim: false,
48
+ }
49
+ }
50
+
51
+ pub const fn bold(mut self) -> Self {
52
+ self.bold = true;
53
+ self
54
+ }
55
+
56
+ pub const fn dim(mut self) -> Self {
57
+ self.dim = true;
58
+ self
59
+ }
60
+
61
+ pub fn paint(&self, text: impl std::fmt::Display) -> String {
62
+ if !colored() {
63
+ return text.to_string();
64
+ }
65
+
66
+ let mut codes: Vec<&str> = Vec::new();
67
+
68
+ if self.bold {
69
+ codes.push("1");
70
+ }
71
+
72
+ if self.dim {
73
+ codes.push("2");
74
+ }
75
+
76
+ if let Some(color) = &self.color {
77
+ codes.push(color.0);
78
+ }
79
+
80
+ if codes.is_empty() {
81
+ return text.to_string();
82
+ }
83
+
84
+ format!("\x1b[{}m{}\x1b[0m", codes.join(";"), text)
85
+ }
86
+ }
87
+
88
+ pub fn success(text: impl std::fmt::Display) -> String {
89
+ Style::fg(GREEN).bold().paint(text)
90
+ }
91
+
92
+ pub fn pending(text: impl std::fmt::Display) -> String {
93
+ Style::fg(YELLOW).bold().paint(text)
94
+ }
95
+
96
+ pub fn failure(text: impl std::fmt::Display) -> String {
97
+ Style::fg(RED).bold().paint(text)
98
+ }
99
+
100
+ pub fn muted(text: impl std::fmt::Display) -> String {
101
+ Style::fg(CYAN).paint(text)
102
+ }
103
+
104
+ pub fn subtle(text: impl std::fmt::Display) -> String {
105
+ Style::plain().dim().paint(text)
106
+ }
107
+
108
+ pub fn strong(text: impl std::fmt::Display) -> String {
109
+ Style::plain().bold().paint(text)
110
+ }
111
+
112
+ pub fn banner() -> String {
113
+ format!("🧉 {} {}", Style::fg(GREEN).bold().paint("yerba"), subtle(format!("v{}", yerba::version())))
114
+ }
115
+
116
+ pub const INDENT: &str = " ";
117
+ pub const STATUS_INDENT: &str = " ";
118
+
119
+ pub mod glyph {
120
+ pub const OK: &str = "✓";
121
+ pub const PENDING: &str = "~";
122
+ pub const FAIL: &str = "✗";
123
+ }
124
+
125
+ pub mod raw {
126
+ fn sequence(code: &'static str) -> &'static str {
127
+ if super::colored() {
128
+ code
129
+ } else {
130
+ ""
131
+ }
132
+ }
133
+
134
+ pub fn green() -> &'static str {
135
+ sequence("\x1b[32m")
136
+ }
137
+
138
+ pub fn yellow() -> &'static str {
139
+ sequence("\x1b[33m")
140
+ }
141
+
142
+ pub fn bold() -> &'static str {
143
+ sequence("\x1b[1m")
144
+ }
145
+
146
+ pub fn dim() -> &'static str {
147
+ sequence("\x1b[2m")
148
+ }
149
+
150
+ pub fn italic() -> &'static str {
151
+ sequence("\x1b[3m")
152
+ }
153
+
154
+ pub fn reset() -> &'static str {
155
+ sequence("\x1b[0m")
156
+ }
157
+ }
@@ -37,7 +37,7 @@ pub struct Args {
37
37
 
38
38
  impl Args {
39
39
  pub fn run(self) {
40
- use super::color::*;
40
+ use super::ui;
41
41
 
42
42
  let selector = self.selector.as_deref().unwrap_or("");
43
43
  let by = self.by.as_deref().unwrap_or(".");
@@ -50,20 +50,20 @@ impl Args {
50
50
  let noun = if duplicates.len() == 1 { "duplicate" } else { "duplicates" };
51
51
 
52
52
  if duplicates.is_empty() {
53
- eprintln!("{GREEN}No duplicates found{RESET} in {}", resolved_file);
53
+ eprintln!("{} in {}", ui::success("No duplicates found"), resolved_file);
54
54
  } else if self.remove {
55
- eprintln!("{YELLOW}Removed {} {noun}{RESET} from {}", duplicates.len(), resolved_file);
55
+ eprintln!("{} from {}", ui::pending(format!("Removed {} {noun}", duplicates.len())), resolved_file);
56
56
 
57
57
  for duplicate in &duplicates {
58
- eprintln!(" {DIM}line {}: {} == {}{RESET}", duplicate.line, by, duplicate.value);
58
+ eprintln!(" {}", ui::subtle(format!("line {}: {} == {}", duplicate.line, by, duplicate.value)));
59
59
  }
60
60
 
61
61
  output(&resolved_file, &document, self.dry_run);
62
62
  } else {
63
- eprintln!("{RED}Found {} {noun}{RESET} in {}", duplicates.len(), resolved_file);
63
+ eprintln!("{} in {}", ui::failure(format!("Found {} {noun}", duplicates.len())), resolved_file);
64
64
 
65
65
  for duplicate in &duplicates {
66
- eprintln!(" {DIM}line {}: {} == {}{RESET}", duplicate.line, by, duplicate.value);
66
+ eprintln!(" {}", ui::subtle(format!("line {}: {} == {}", duplicate.line, by, duplicate.value)));
67
67
  }
68
68
 
69
69
  process::exit(1);
@@ -71,7 +71,7 @@ impl Args {
71
71
  }
72
72
 
73
73
  Err(error) => {
74
- eprintln!("{RED}Error:{RESET} {}", error);
74
+ eprintln!("{} {}", ui::failure("Error:"), error);
75
75
  process::exit(1);
76
76
  }
77
77
  }
@@ -1,8 +1,18 @@
1
- use super::color::*;
1
+ use super::ui;
2
2
 
3
3
  pub fn run() {
4
- println!("🧉 {BOLD}yerba{RESET} {DIM}v{}{RESET}", yerba::version());
4
+ println!("{}", ui::banner());
5
5
  println!(
6
- " {BOLD}Y{RESET}{DIM}AML{RESET} {BOLD}E{RESET}{DIM}diting and{RESET} {BOLD}R{RESET}{DIM}efactoring with{RESET} {BOLD}B{RESET}{DIM}etter{RESET} {BOLD}A{RESET}{DIM}ccuracy{RESET}"
6
+ " {}{} {}{} {}{} {}{} {}{}",
7
+ ui::strong("Y"),
8
+ ui::subtle("AML"),
9
+ ui::strong("E"),
10
+ ui::subtle("diting and"),
11
+ ui::strong("R"),
12
+ ui::subtle("efactoring with"),
13
+ ui::strong("B"),
14
+ ui::subtle("etter"),
15
+ ui::strong("A"),
16
+ ui::subtle("ccuracy")
7
17
  );
8
18
  }
@@ -1,29 +1,83 @@
1
1
  use super::*;
2
2
 
3
+ const CONDITION_OPERATORS: &str = "expected `<selector> <operator> <value>`, where operator is one of ==, !=, contains, not_contains";
4
+
5
+ pub fn validate_condition(condition: &str) -> Result<(), YerbaError> {
6
+ let trimmed = condition.trim();
7
+
8
+ if trimmed.is_empty() {
9
+ return Err(YerbaError::InvalidCondition(condition.to_string(), "condition is empty".to_string()));
10
+ }
11
+
12
+ let Some((left, _operator, _right)) = parse_condition(trimmed) else {
13
+ return Err(YerbaError::InvalidCondition(condition.to_string(), CONDITION_OPERATORS.to_string()));
14
+ };
15
+
16
+ if crate::selector::Selector::parse(&left).is_empty() {
17
+ return Err(YerbaError::InvalidCondition(
18
+ condition.to_string(),
19
+ "condition is missing a selector on the left of the operator".to_string(),
20
+ ));
21
+ }
22
+
23
+ Ok(())
24
+ }
25
+
26
+ pub fn validate_item_condition(condition: &str) -> Result<(), YerbaError> {
27
+ validate_condition(condition)?;
28
+
29
+ let trimmed = condition.trim();
30
+
31
+ let Some((left, _operator, _right)) = parse_condition(trimmed) else {
32
+ return Ok(());
33
+ };
34
+
35
+ if !crate::selector::Selector::parse(&left).is_relative() {
36
+ return Err(YerbaError::InvalidCondition(
37
+ condition.to_string(),
38
+ format!(
39
+ "selector \"{}\" is absolute, but this condition is tested against each item, so it must be relative and start with `.` — did you mean \".{}\"?",
40
+ left.trim(),
41
+ left.trim()
42
+ ),
43
+ ));
44
+ }
45
+
46
+ Ok(())
47
+ }
48
+
3
49
  impl Document {
4
- pub fn filter(&self, dot_path: &str, condition: &str) -> Vec<yaml_serde::Value> {
5
- self
6
- .navigate_all_compact(dot_path)
7
- .iter()
8
- .filter(|node| self.evaluate_condition_on_node(node, condition))
9
- .map(node_to_yaml_value)
10
- .collect()
50
+ pub fn filter(&self, dot_path: &str, condition: &str) -> Result<Vec<yaml_serde::Value>, YerbaError> {
51
+ validate_item_condition(condition)?;
52
+
53
+ Ok(
54
+ self
55
+ .navigate_all_compact(dot_path)
56
+ .iter()
57
+ .filter(|node| self.evaluate_condition_on_node(node, condition))
58
+ .map(node_to_yaml_value)
59
+ .collect(),
60
+ )
11
61
  }
12
62
 
13
- pub fn filter_with_selectors(&self, dot_path: &str, condition: &str) -> Vec<(yaml_serde::Value, String, usize)> {
63
+ pub fn filter_with_selectors(&self, dot_path: &str, condition: &str) -> Result<Vec<(yaml_serde::Value, String, usize)>, YerbaError> {
64
+ validate_item_condition(condition)?;
65
+
14
66
  let source = self.source_text();
15
67
 
16
- self
17
- .navigate_all_compact(dot_path)
18
- .iter()
19
- .filter(|node| self.evaluate_condition_on_node(node, condition))
20
- .map(|node| {
21
- let offset: usize = node.text_range().start().into();
22
- let line = line_at(&source, offset);
23
-
24
- (node_to_yaml_value(node), super::get::node_selector(node), line)
25
- })
26
- .collect()
68
+ Ok(
69
+ self
70
+ .navigate_all_compact(dot_path)
71
+ .iter()
72
+ .filter(|node| self.evaluate_condition_on_node(node, condition))
73
+ .map(|node| {
74
+ let offset: usize = node.text_range().start().into();
75
+ let line = line_at(&source, offset);
76
+
77
+ (node_to_yaml_value(node), super::get::node_selector(node), line)
78
+ })
79
+ .collect(),
80
+ )
27
81
  }
28
82
 
29
83
  pub(super) fn evaluate_condition_on_node(&self, node: &SyntaxNode, condition: &str) -> bool {
@@ -84,31 +138,32 @@ impl Document {
84
138
  }
85
139
  }
86
140
 
87
- pub fn evaluate_condition(&self, parent_path: &str, condition: &str) -> bool {
141
+ pub fn evaluate_condition(&self, parent_path: &str, condition: &str) -> Result<bool, YerbaError> {
142
+ if parent_path.is_empty() {
143
+ validate_condition(condition)?;
144
+ } else {
145
+ validate_item_condition(condition)?;
146
+ }
147
+
88
148
  let condition = condition.trim();
89
149
 
90
150
  let (left, operator, right) = match parse_condition(condition) {
91
151
  Some(parts) => parts,
92
- None => return false,
152
+ None => return Ok(false),
93
153
  };
94
154
 
95
155
  let path = crate::selector::Selector::parse(&left);
156
+ let path_string = path.to_selector_string();
96
157
 
97
- let full_path = if path.is_relative() {
98
- let path_string = path.to_selector_string();
99
-
100
- if parent_path.is_empty() {
101
- path_string
102
- } else {
103
- format!("{}.{}", parent_path, path_string)
104
- }
158
+ let full_path = if parent_path.is_empty() {
159
+ path_string
105
160
  } else {
106
- path.to_selector_string()
161
+ format!("{}.{}", parent_path, path_string)
107
162
  };
108
163
 
109
164
  let has_brackets = crate::selector::Selector::parse(&full_path).has_brackets();
110
165
 
111
- match operator {
166
+ let result = match operator {
112
167
  "==" => {
113
168
  if has_brackets {
114
169
  self.get_all(&full_path).iter().any(|value| value == &right)
@@ -150,6 +205,8 @@ impl Document {
150
205
  }
151
206
  }
152
207
  _ => false,
153
- }
208
+ };
209
+
210
+ Ok(result)
154
211
  }
155
212
  }
@@ -31,6 +31,7 @@ impl Document {
31
31
 
32
32
  pub fn delete(&mut self, dot_path: &str) -> Result<(), YerbaError> {
33
33
  Self::validate_path(dot_path)?;
34
+ self.refuse_flow_target(dot_path)?;
34
35
 
35
36
  let selector = crate::selector::Selector::parse(dot_path);
36
37
  let segments = selector.segments();
@@ -88,13 +89,17 @@ impl Document {
88
89
  }
89
90
 
90
91
  crate::selector::SelectorSegment::Index(index) => self.remove_at(&parent_path, *index),
91
- crate::selector::SelectorSegment::AllItems => Err(YerbaError::SelectorNotFound(dot_path.to_string())),
92
+ crate::selector::SelectorSegment::AllItems | crate::selector::SelectorSegment::AllKeys => Err(YerbaError::SelectorNotFound(dot_path.to_string())),
92
93
  }
93
94
  }
94
95
 
95
96
  pub fn remove(&mut self, dot_path: &str, value: &str) -> Result<(), YerbaError> {
96
97
  let current_node = self.navigate(dot_path)?;
97
98
 
99
+ if find_flow_sequence(&current_node).is_some() {
100
+ return Err(YerbaError::FlowCollectionNotWritable(dot_path.to_string()));
101
+ }
102
+
98
103
  let sequence = find_block_sequence(&current_node).ok_or_else(|| YerbaError::NotASequence(dot_path.to_string()))?;
99
104
 
100
105
  let target_entry = sequence