yerba 0.3.0 → 0.4.0

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.
@@ -58,7 +58,100 @@ module Yerba
58
58
  def each
59
59
  return enum_for(:each) unless block_given?
60
60
 
61
- length.times { |i| yield self[i] }
61
+ length.times { |index| yield self[index] }
62
+ end
63
+
64
+ def find_by(selector = nil, value = nil, **criteria)
65
+ index = index_of(selector, value, **criteria)
66
+
67
+ self[index] if index
68
+ end
69
+
70
+ def where(selector = nil, value = nil, **criteria)
71
+ indices_of(selector, value, **criteria).map { |index| self[index] }
72
+ end
73
+
74
+ def pluck(*fields)
75
+ return [] unless @document
76
+
77
+ all_values = @document.get_value(@selector)
78
+ return [] unless all_values.is_a?(Array)
79
+
80
+ if fields.length == 1
81
+
82
+ all_values.map { |item| item.is_a?(Hash) ? item[fields.first.to_s] : item }
83
+ else
84
+
85
+ all_values.map { |item| fields.map(&:to_s).map { |field| item.is_a?(Hash) ? item[field] : nil } }
86
+ end
87
+ end
88
+
89
+ def index_of(selector = nil, value = nil, **criteria)
90
+ if selector && value.nil? && criteria.empty?
91
+ values = @document&.get("#{@selector}[]")
92
+
93
+ return values.index(selector) if values.is_a?(Array)
94
+
95
+ return nil
96
+ end
97
+
98
+ criteria[selector] = value if selector && value
99
+ pairs = expand_nested_criteria(criteria)
100
+
101
+ indices = nil
102
+
103
+ pairs.each do |field, expected|
104
+ field_string = field.to_s
105
+
106
+ if field_string.include?("[]")
107
+ matching = nested_indices_for(field_string, expected)
108
+ else
109
+ values = @document&.get("#{@selector}[].#{field_string}")
110
+
111
+ next unless values.is_a?(Array)
112
+
113
+ matching = values.each_with_index.filter_map { |actual, index| index if actual == expected }
114
+ end
115
+
116
+ indices = indices ? indices & matching : matching
117
+ end
118
+
119
+ indices&.first
120
+ end
121
+
122
+ def indices_of(selector = nil, value = nil, **criteria)
123
+ if selector && value.nil? && criteria.empty?
124
+ values = @document&.get("#{@selector}[]")
125
+
126
+ if values.is_a?(Array)
127
+ return values.each_with_index.filter_map { |actual, index| index if actual == selector }
128
+ end
129
+
130
+ return []
131
+ end
132
+
133
+ criteria[selector] = value if selector && value
134
+ pairs = expand_nested_criteria(criteria)
135
+
136
+ indices = nil
137
+
138
+ pairs.each do |field, expected|
139
+ field_string = field.to_s
140
+
141
+ if field_string.include?("[]")
142
+ matching = nested_indices_for(field_string, expected)
143
+ else
144
+ values = @document&.get("#{@selector}[].#{field_string}")
145
+
146
+ next unless values.is_a?(Array)
147
+
148
+ matching = values.each_with_index.filter_map { |actual, index| index if actual == expected }
149
+ end
150
+
151
+ indices = indices ? indices & matching : matching
152
+ end
153
+
154
+ indices || []
62
155
  end
63
156
 
64
157
  def length
@@ -69,6 +162,7 @@ module Yerba
69
162
  scalar_items.length
70
163
  else
71
164
  data = @document.get_value(@selector)
165
+
72
166
  data.is_a?(Array) ? data.length : 0
73
167
  end
74
168
  else
@@ -156,6 +250,79 @@ module Yerba
156
250
 
157
251
  private
158
252
 
253
+ def expand_nested_criteria(criteria)
254
+ expanded = []
255
+
256
+ criteria.each do |field, value|
257
+ if value.is_a?(Hash)
258
+ flatten_hash("#{field}[]", value).each do |path, leaf_value|
259
+ expanded << [path, leaf_value]
260
+ end
261
+ elsif value.is_a?(Array)
262
+ value.each do |item|
263
+ expanded << ["#{field}[]", item]
264
+ end
265
+ else
266
+ expanded << [field, value]
267
+ end
268
+ end
269
+
270
+ expanded
271
+ end
272
+
273
+ def flatten_hash(prefix, hash)
274
+ result = {}
275
+
276
+ hash.each do |key, value|
277
+ path = "#{prefix}.#{key}"
278
+
279
+ if value.is_a?(Hash)
280
+ flatten_hash("#{path}[]", value).each { |nested_path, leaf| result[nested_path] = leaf }
281
+ else
282
+ result[path] = value
283
+ end
284
+ end
285
+
286
+ result
287
+ end
288
+
289
+ def nested_indices_for(field, expected)
290
+ all_values = @document&.get_value(@selector)
291
+ return [] unless all_values.is_a?(Array)
292
+
293
+ all_values.each_with_index.filter_map do |item, index|
294
+ next unless item.is_a?(Hash)
295
+
296
+ nested_values = dig_values(item, field)
297
+ index if nested_values.include?(expected)
298
+ end
299
+ end
300
+
301
+ def dig_values(hash, path)
302
+ parts = path.split(".")
303
+ current = [hash]
304
+
305
+ parts.each do |part|
306
+ next_values = []
307
+
308
+ current.each do |value|
309
+ if part == "[]" && value.is_a?(Array)
310
+ next_values.concat(value)
311
+ elsif part.end_with?("[]")
312
+ key = part.chomp("[]")
313
+ child = value.is_a?(Hash) ? value[key] : nil
314
+ next_values.concat(child) if child.is_a?(Array)
315
+ elsif value.is_a?(Hash)
316
+ next_values << value[part] if value.key?(part)
317
+ end
318
+ end
319
+
320
+ current = next_values
321
+ end
322
+
323
+ current
324
+ end
325
+
159
326
  def format_for_insert(value)
160
327
  Formatting.quote(value, detect_quote_style)
161
328
  end
@@ -172,6 +339,7 @@ module Yerba
172
339
  result
173
340
  else
174
341
  data = @document.get_value(@selector)
342
+
175
343
  data.is_a?(Array) ? data : []
176
344
  end
177
345
  else
data/lib/yerba/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Yerba
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/rust/Cargo.lock ADDED
@@ -0,0 +1 @@
1
+ ../Cargo.lock
data/rust/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "yerba"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  edition = "2021"
5
5
  authors = ["Marco Roth <marco.roth@intergga.ch>"]
6
6
  description = "YAML Editing and Refactoring with Better Accuracy"
data/rust/cbindgen.toml CHANGED
@@ -9,6 +9,7 @@ no_includes = true
9
9
 
10
10
  [export]
11
11
  include = [
12
+ "NodeType",
12
13
  "YerbaValueType",
13
14
  "YerbaResult",
14
15
  "YerbaTypedValue",
data/rust/rustfmt.toml CHANGED
@@ -1,2 +1,2 @@
1
1
  tab_spaces = 2
2
- max_width = 120
2
+ max_width = 160
@@ -39,10 +39,7 @@ impl Args {
39
39
  } else {
40
40
  use super::color::*;
41
41
 
42
- eprintln!(
43
- "{RED}Error:{RESET} expected a number for blank line count, got '{}'",
44
- self.first
45
- );
42
+ eprintln!("{RED}Error:{RESET} expected a number for blank line count, got '{}'", self.first);
46
43
 
47
44
  std::process::exit(1);
48
45
  };
@@ -0,0 +1,61 @@
1
+ use std::sync::LazyLock;
2
+
3
+ use indoc::indoc;
4
+
5
+ use super::colorize_examples;
6
+ use super::{output, parse_file, resolve_files};
7
+
8
+ static EXAMPLES: LazyLock<String> = LazyLock::new(|| {
9
+ colorize_examples(indoc! {r#"
10
+ yerba directives config.yml --ensure
11
+ yerba directives config.yml --remove
12
+ yerba directives "data/**/*.yml" --ensure
13
+ yerba directives config.yml --ensure --dry-run
14
+ "#})
15
+ });
16
+
17
+ #[derive(clap::Args)]
18
+ #[command(
19
+ about = "Add or remove the document start marker (---)",
20
+ arg_required_else_help = true,
21
+ after_help = EXAMPLES.as_str()
22
+ )]
23
+ pub struct Args {
24
+ file: String,
25
+ /// Add --- if missing
26
+ #[arg(long)]
27
+ ensure: bool,
28
+ /// Remove --- if present
29
+ #[arg(long)]
30
+ remove: bool,
31
+ #[arg(long)]
32
+ dry_run: bool,
33
+ }
34
+
35
+ impl Args {
36
+ pub fn run(self) {
37
+ if !self.ensure && !self.remove {
38
+ use super::color::*;
39
+ eprintln!("{RED}Error:{RESET} specify --ensure or --remove");
40
+ std::process::exit(1);
41
+ }
42
+
43
+ if self.ensure && self.remove {
44
+ use super::color::*;
45
+ eprintln!("{RED}Error:{RESET} --ensure and --remove are mutually exclusive");
46
+ std::process::exit(1);
47
+ }
48
+
49
+ for resolved_file in resolve_files(&self.file) {
50
+ let mut document = parse_file(&resolved_file);
51
+
52
+ if self.ensure {
53
+ let _ = document.ensure_directives();
54
+ } else if self.remove {
55
+ let _ = document.remove_directives();
56
+ }
57
+
58
+ output(&resolved_file, &document, self.dry_run);
59
+ }
60
+ }
61
+ }
@@ -77,10 +77,7 @@ impl Args {
77
77
 
78
78
  use super::color::*;
79
79
 
80
- eprintln!(
81
- "{RED}Error:{RESET} selector \"{}\" not found in {}",
82
- self.selector, resolved_file
83
- );
80
+ eprintln!("{RED}Error:{RESET} selector \"{}\" not found in {}", self.selector, resolved_file);
84
81
 
85
82
  show_similar_selectors(resolved_file, &document, &self.selector);
86
83
  process::exit(1);
@@ -97,11 +94,7 @@ impl Args {
97
94
 
98
95
  if !document.exists(&full_selector) {
99
96
  use super::color::*;
100
- eprintln!(
101
- "{RED}Error:{RESET} select field \"{}\" not found in {}",
102
- field.trim(),
103
- resolved_file
104
- );
97
+ eprintln!("{RED}Error:{RESET} select field \"{}\" not found in {}", field.trim(), resolved_file);
105
98
  show_similar_selectors(resolved_file, &document, &full_selector);
106
99
  process::exit(1);
107
100
  }
@@ -175,23 +168,13 @@ impl Args {
175
168
  }
176
169
  }
177
170
  } else if all_results.len() == 1 {
178
- println!(
179
- "{}",
180
- serde_json::to_string_pretty(&all_results[0]).unwrap_or_else(|_| "null".to_string())
181
- );
171
+ println!("{}", serde_json::to_string_pretty(&all_results[0]).unwrap_or_else(|_| "null".to_string()));
182
172
  } else {
183
- println!(
184
- "{}",
185
- serde_json::to_string_pretty(&all_results).unwrap_or_else(|_| "[]".to_string())
186
- );
173
+ println!("{}", serde_json::to_string_pretty(&all_results).unwrap_or_else(|_| "[]".to_string()));
187
174
  }
188
175
  }
189
176
 
190
- fn resolve_search_scope(
191
- &self,
192
- selector: &yerba::Selector,
193
- condition_path: Option<&yerba::Selector>,
194
- ) -> (yerba::Selector, Option<yerba::Selector>) {
177
+ fn resolve_search_scope(&self, selector: &yerba::Selector, condition_path: Option<&yerba::Selector>) -> (yerba::Selector, Option<yerba::Selector>) {
195
178
  if let Some(condition) = condition_path {
196
179
  if condition.is_relative() {
197
180
  let (container, field) = selector.split_at_last_bracket();
@@ -2,6 +2,7 @@ pub mod apply;
2
2
  pub mod blank_lines;
3
3
  pub mod check;
4
4
  pub mod delete;
5
+ pub mod directives;
5
6
  pub mod get;
6
7
  pub mod init;
7
8
  pub mod insert;
@@ -126,10 +127,7 @@ pub(crate) fn colorize_help(input: &str) -> String {
126
127
  }
127
128
 
128
129
  if columns.len() == 3 {
129
- output.push_str(&format!(
130
- " \x1b[36m{:<20}{RESET} {:<21} {DIM}{}{RESET}\n",
131
- columns[0], columns[1], columns[2]
132
- ));
130
+ output.push_str(&format!(" \x1b[36m{:<20}{RESET} {:<21} {DIM}{}{RESET}\n", columns[0], columns[1], columns[2]));
133
131
  } else if columns.len() == 2 {
134
132
  output.push_str(&format!(" \x1b[36m{:<20}{RESET} {}\n", columns[0], columns[1]));
135
133
  } else {
@@ -154,6 +152,7 @@ pub enum Command {
154
152
  Sort(sort::Args),
155
153
  QuoteStyle(quote_style::Args),
156
154
  BlankLines(blank_lines::Args),
155
+ Directives(directives::Args),
157
156
  Selectors(selectors::Args),
158
157
  #[command(about = "Create a new Yerbafile in the current directory")]
159
158
  Init,
@@ -182,6 +181,7 @@ impl Command {
182
181
  Command::Sort(args) => args.run(),
183
182
  Command::QuoteStyle(args) => args.run(),
184
183
  Command::BlankLines(args) => args.run(),
184
+ Command::Directives(args) => args.run(),
185
185
  Command::Selectors(args) => args.run(),
186
186
  Command::Init => init::run(),
187
187
  Command::Apply => apply::run(),
@@ -334,12 +334,7 @@ pub(crate) fn run_op(file: &str, document: &yerba::Document, result: Result<(),
334
334
  run_op_with_hint(file, document, result, None);
335
335
  }
336
336
 
337
- pub(crate) fn run_op_with_hint(
338
- file: &str,
339
- document: &yerba::Document,
340
- result: Result<(), yerba::YerbaError>,
341
- hint: Option<&str>,
342
- ) {
337
+ pub(crate) fn run_op_with_hint(file: &str, document: &yerba::Document, result: Result<(), yerba::YerbaError>, hint: Option<&str>) {
343
338
  use color::*;
344
339
 
345
340
  if let Err(error) = result {
@@ -11,7 +11,7 @@ static EXAMPLES: LazyLock<String> = LazyLock::new(|| {
11
11
  yerba quote-style config.yml --keys plain
12
12
  yerba quote-style config.yml --keys plain --values double
13
13
  yerba quote-style config.yml "[].speakers" --values plain
14
- yerba quote-style "data/**/*.yml" --keys plain --values double
14
+ yerba quote-style "data/**/*.yml" --keys plain --values double
15
15
  "#})
16
16
  });
17
17
 
@@ -25,12 +25,12 @@ pub struct Args {
25
25
  file: String,
26
26
  /// Selector to scope the operation (optional — omit for whole file)
27
27
  selector: Option<String>,
28
- /// Quote style for values (plain, single, double, literal, folded)
28
+ /// Quote style for keys
29
29
  #[arg(long)]
30
- values: Option<yerba::QuoteStyle>,
31
- /// Quote style for keys (plain, single, double)
30
+ keys: Option<yerba::KeyStyle>,
31
+ /// Quote style for values
32
32
  #[arg(long)]
33
- keys: Option<yerba::QuoteStyle>,
33
+ values: Option<yerba::QuoteStyle>,
34
34
  #[arg(long)]
35
35
  dry_run: bool,
36
36
  }
@@ -53,7 +53,13 @@ impl Args {
53
53
  }
54
54
 
55
55
  if let Some(style) = &self.values {
56
- let _ = document.enforce_quotes_at(style, selector);
56
+ if let Ok(warnings) = document.enforce_quotes_at(style, selector) {
57
+ for warning in warnings {
58
+ use super::color::*;
59
+
60
+ eprintln!("{YELLOW}Warning:{RESET} {} — {}", resolved_file, warning);
61
+ }
62
+ }
57
63
  }
58
64
 
59
65
  output(&resolved_file, &document, self.dry_run);
@@ -81,11 +81,7 @@ impl Args {
81
81
 
82
82
  for resolved_file in resolve_files(&self.file) {
83
83
  let document = parse_file(&resolved_file);
84
- let prefix = if selector.is_empty() {
85
- String::new()
86
- } else {
87
- selector.to_string()
88
- };
84
+ let prefix = if selector.is_empty() { String::new() } else { selector.to_string() };
89
85
 
90
86
  let values = if selector.is_empty() {
91
87
  document.get_value("").into_iter().collect::<Vec<_>>()
@@ -112,14 +108,7 @@ impl Args {
112
108
  if let Some(label) = info.count_label() {
113
109
  let padding = max_selector_len - selector.len() + 2;
114
110
 
115
- println!(
116
- "{}{}{}{}{}",
117
- selector,
118
- " ".repeat(padding),
119
- color::DIM,
120
- label,
121
- color::RESET
122
- );
111
+ println!("{}{}{}{}{}", selector, " ".repeat(padding), color::DIM, label, color::RESET);
123
112
  } else {
124
113
  println!("{}", selector);
125
114
  }
@@ -127,12 +116,7 @@ impl Args {
127
116
  }
128
117
  }
129
118
 
130
- fn collect_selectors(
131
- value: &serde_yaml::Value,
132
- prefix: &str,
133
- selectors: &mut BTreeMap<String, SelectorInfo>,
134
- counter: &mut usize,
135
- ) {
119
+ fn collect_selectors(value: &serde_yaml::Value, prefix: &str, selectors: &mut BTreeMap<String, SelectorInfo>, counter: &mut usize) {
136
120
  match value {
137
121
  serde_yaml::Value::Mapping(map) => {
138
122
  for (key, child) in map {