yerba 0.7.5-x86_64-linux-gnu → 0.8.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.
@@ -656,6 +656,18 @@ impl Document {
656
656
  self.reparse(&new_source)
657
657
  }
658
658
 
659
+ pub fn enforce_final_newline(&mut self, count: usize) -> Result<(), YerbaError> {
660
+ let source = self.to_string();
661
+ let trimmed = source.trim_end_matches('\n');
662
+ let expected = format!("{}{}", trimmed, "\n".repeat(count));
663
+
664
+ if source != expected {
665
+ self.reparse(&expected)
666
+ } else {
667
+ Ok(())
668
+ }
669
+ }
670
+
659
671
  pub fn enforce_key_style(&mut self, style: &crate::KeyStyle, dot_path: Option<&str>) -> Result<(), YerbaError> {
660
672
  let scope_ranges: Vec<TextRange> = match dot_path {
661
673
  Some(path) if !path.is_empty() => self.navigate_all_compact(path).iter().map(|node| node.text_range()).collect(),
data/rust/src/error.rs CHANGED
@@ -4,7 +4,9 @@ pub enum YerbaError {
4
4
  IoError(std::io::Error),
5
5
  SelectorNotFound(String),
6
6
  AmbiguousSelector(String, usize),
7
+ InvalidCondition(String, String),
7
8
  NotASequence(String),
9
+ FlowCollectionNotWritable(String),
8
10
  IndexOutOfBounds(usize, usize),
9
11
  UnknownKeys(Vec<String>),
10
12
  DuplicateValues(Vec<crate::DuplicateInfo>),
@@ -25,6 +27,14 @@ impl std::fmt::Display for YerbaError {
25
27
  YerbaError::SelectorNotFound(selector) => write!(f, "selector not found: {}", selector),
26
28
  YerbaError::NotASequence(path) => write!(f, "not a sequence: {}", path),
27
29
 
30
+ YerbaError::FlowCollectionNotWritable(path) => {
31
+ write!(
32
+ f,
33
+ "cannot write to \"{}\": flow collections are not writable yet. Convert it to block style first, with `collection_style` or `yerba collection-style`",
34
+ path
35
+ )
36
+ }
37
+
28
38
  YerbaError::AmbiguousSelector(selector, count) => {
29
39
  write!(
30
40
  f,
@@ -33,6 +43,10 @@ impl std::fmt::Display for YerbaError {
33
43
  )
34
44
  }
35
45
 
46
+ YerbaError::InvalidCondition(condition, reason) => {
47
+ write!(f, "invalid condition \"{}\": {}", condition, reason)
48
+ }
49
+
36
50
  YerbaError::DuplicateKey {
37
51
  key,
38
52
  first_line,
data/rust/src/ffi.rs CHANGED
@@ -330,6 +330,24 @@ pub unsafe extern "C" fn yerba_document_resolve_selectors(document: *const Docum
330
330
  CString::new(json_string).unwrap_or_default().into_raw()
331
331
  }
332
332
 
333
+ #[no_mangle]
334
+ pub unsafe extern "C" fn yerba_selector_has_wildcard(path: *const c_char) -> bool {
335
+ let selector_string = CStr::from_ptr(path).to_str().unwrap_or("");
336
+
337
+ crate::selector::Selector::parse(selector_string).has_wildcard()
338
+ }
339
+
340
+ #[no_mangle]
341
+ pub unsafe extern "C" fn yerba_document_keys(document: *const Document, path: *const c_char) -> *mut c_char {
342
+ let document = &*document;
343
+ let selector_string = CStr::from_ptr(path).to_str().unwrap_or("");
344
+
345
+ let keys = document.keys_at(selector_string);
346
+ let json_string = serde_json::to_string(&keys).unwrap_or_else(|_| "[]".to_string());
347
+
348
+ CString::new(json_string).unwrap_or_default().into_raw()
349
+ }
350
+
333
351
  #[no_mangle]
334
352
  pub unsafe extern "C" fn yerba_document_get_quote_style(document: *const Document, path: *const c_char) -> *mut c_char {
335
353
  let document = &*document;
@@ -409,12 +427,33 @@ pub unsafe extern "C" fn yerba_document_set_sequence_indent(document: *mut Docum
409
427
  }
410
428
  }
411
429
 
430
+ #[no_mangle]
431
+ pub unsafe extern "C" fn yerba_validate_condition(condition: *const c_char, item_context: bool) -> *mut c_char {
432
+ if condition.is_null() {
433
+ return ptr::null_mut();
434
+ }
435
+
436
+ let cond = CStr::from_ptr(condition).to_str().unwrap_or("");
437
+
438
+ let result = if item_context {
439
+ crate::validate_item_condition(cond)
440
+ } else {
441
+ crate::validate_condition(cond)
442
+ };
443
+
444
+ match result {
445
+ Ok(()) => ptr::null_mut(),
446
+ Err(error) => CString::new(error.to_string()).unwrap_or_default().into_raw(),
447
+ }
448
+ }
449
+
412
450
  #[no_mangle]
413
451
  pub unsafe extern "C" fn yerba_document_evaluate_condition(document: *const Document, parent_path: *const c_char, condition: *const c_char) -> bool {
414
452
  let document = &*document;
415
453
  let parent = CStr::from_ptr(parent_path).to_str().unwrap_or("");
416
454
  let cond = CStr::from_ptr(condition).to_str().unwrap_or("");
417
- document.evaluate_condition(parent, cond)
455
+
456
+ document.evaluate_condition(parent, cond).unwrap_or(false)
418
457
  }
419
458
 
420
459
  #[no_mangle]
@@ -440,7 +479,7 @@ pub unsafe extern "C" fn yerba_document_find(document: *const Document, path: *c
440
479
 
441
480
  let select_string = if select.is_null() { None } else { CStr::from_ptr(select).to_str().ok() };
442
481
 
443
- let results = document.find_items(selector_string, condition_string, select_string);
482
+ let results = document.find_items(selector_string, condition_string, select_string).unwrap_or_default();
444
483
  let json = serde_json::to_string_pretty(&results).unwrap_or_else(|_| "[]".to_string());
445
484
 
446
485
  CString::new(json).unwrap_or_default().into_raw()
@@ -830,12 +869,7 @@ pub unsafe extern "C" fn yerba_get_result_free(result: YerbaGetResult) {
830
869
  }
831
870
  }
832
871
 
833
- #[no_mangle]
834
- pub unsafe extern "C" fn yerba_glob_get(glob_pattern: *const c_char, path: *const c_char) -> YerbaTypedList {
835
- let pattern = CStr::from_ptr(glob_pattern).to_str().unwrap_or("");
836
- let selector_string = CStr::from_ptr(path).to_str().unwrap_or("");
837
- let nodes = crate::glob_get(pattern, selector_string);
838
-
872
+ fn located_nodes_to_list(nodes: &[crate::LocatedNode]) -> YerbaTypedList {
839
873
  let results: Vec<serde_json::Value> = nodes
840
874
  .iter()
841
875
  .map(|node| {
@@ -865,6 +899,18 @@ pub unsafe extern "C" fn yerba_glob_get(glob_pattern: *const c_char, path: *cons
865
899
  value["type"] = serde_json::json!(vt as u8);
866
900
  }
867
901
 
902
+ if let Some(key_name) = &node.key_name {
903
+ value["key_name"] = serde_json::json!(key_name);
904
+ value["key_location"] = serde_json::json!({
905
+ "start_line": node.key_location.start_line,
906
+ "start_column": node.key_location.start_column,
907
+ "end_line": node.key_location.end_line,
908
+ "end_column": node.key_location.end_column,
909
+ "start_offset": node.key_location.start_offset,
910
+ "end_offset": node.key_location.end_offset,
911
+ });
912
+ }
913
+
868
914
  value
869
915
  })
870
916
  .collect();
@@ -878,6 +924,22 @@ pub unsafe extern "C" fn yerba_glob_get(glob_pattern: *const c_char, path: *cons
878
924
  }
879
925
  }
880
926
 
927
+ #[no_mangle]
928
+ pub unsafe extern "C" fn yerba_glob_get(glob_pattern: *const c_char, path: *const c_char) -> YerbaTypedList {
929
+ let pattern = CStr::from_ptr(glob_pattern).to_str().unwrap_or("");
930
+ let selector_string = CStr::from_ptr(path).to_str().unwrap_or("");
931
+
932
+ located_nodes_to_list(&crate::glob_get(pattern, selector_string))
933
+ }
934
+
935
+ #[no_mangle]
936
+ pub unsafe extern "C" fn yerba_document_get_all(document: *const Document, path: *const c_char) -> YerbaTypedList {
937
+ let document = &*document;
938
+ let selector_string = CStr::from_ptr(path).to_str().unwrap_or("");
939
+
940
+ located_nodes_to_list(&document.get_all_located(selector_string))
941
+ }
942
+
881
943
  #[no_mangle]
882
944
  pub unsafe extern "C" fn yerba_glob_find(glob_pattern: *const c_char, path: *const c_char, condition: *const c_char, select: *const c_char) -> YerbaTypedList {
883
945
  let pattern = CStr::from_ptr(glob_pattern).to_str().unwrap_or("");
@@ -885,7 +947,7 @@ pub unsafe extern "C" fn yerba_glob_find(glob_pattern: *const c_char, path: *con
885
947
  let condition_string = if condition.is_null() { None } else { CStr::from_ptr(condition).to_str().ok() };
886
948
  let select_string = if select.is_null() { None } else { CStr::from_ptr(select).to_str().ok() };
887
949
 
888
- let all_results = crate::glob_find(pattern, selector_string, condition_string, select_string);
950
+ let all_results = crate::glob_find(pattern, selector_string, condition_string, select_string).unwrap_or_default();
889
951
  let length = all_results.len();
890
952
  let json = serde_json::to_string_pretty(&all_results).unwrap_or_else(|_| "[]".to_string());
891
953
 
data/rust/src/json.rs CHANGED
@@ -71,6 +71,12 @@ pub fn resolve_select_field(value: &yaml_serde::Value, field: &str) -> serde_jso
71
71
  }
72
72
  }
73
73
 
74
+ SelectorSegment::AllKeys => {
75
+ if let yaml_serde::Value::Mapping(mapping) = current {
76
+ next_values.extend(mapping.iter().map(|(_, entry)| entry.clone()));
77
+ }
78
+ }
79
+
74
80
  SelectorSegment::Index(index) => {
75
81
  if let yaml_serde::Value::Sequence(sequence) = current {
76
82
  if let Some(item) = sequence.get(*index) {
@@ -96,7 +102,10 @@ pub fn resolve_select_field(value: &yaml_serde::Value, field: &str) -> serde_jso
96
102
  current_values = next_values;
97
103
  }
98
104
 
99
- let used_all_items = parsed.segments().iter().any(|s| matches!(s, SelectorSegment::AllItems));
105
+ let used_all_items = parsed
106
+ .segments()
107
+ .iter()
108
+ .any(|s| matches!(s, SelectorSegment::AllItems | SelectorSegment::AllKeys));
100
109
 
101
110
  if current_values.is_empty() {
102
111
  if used_all_items {
data/rust/src/lib.rs CHANGED
@@ -11,7 +11,9 @@ mod yaml_writer;
11
11
  pub mod yerbafile;
12
12
 
13
13
  pub use document::style::StyleEnforcement;
14
- pub use document::{collect_selectors, Document, DuplicateInfo, InsertPosition, LocatedNode, Location, NodeInfo, NodeType, SortField};
14
+ pub use document::{
15
+ collect_selectors, validate_condition, validate_item_condition, Document, DuplicateInfo, InsertPosition, LocatedNode, Location, NodeInfo, NodeType, SortField,
16
+ };
15
17
  pub use error::YerbaError;
16
18
  pub use quote_style::{KeyStyle, QuoteStyle};
17
19
  pub use selector::Selector;
@@ -53,24 +55,28 @@ pub fn glob_get(pattern: &str, selector: &str) -> Vec<document::LocatedNode> {
53
55
  .collect()
54
56
  }
55
57
 
56
- pub fn glob_find(pattern: &str, selector: &str, condition: Option<&str>, select: Option<&str>) -> Vec<serde_json::Value> {
58
+ pub fn glob_find(pattern: &str, selector: &str, condition: Option<&str>, select: Option<&str>) -> Result<Vec<serde_json::Value>, YerbaError> {
57
59
  use rayon::prelude::*;
58
60
 
61
+ if let Some(cond) = condition {
62
+ crate::document::validate_condition(cond)?;
63
+ }
64
+
59
65
  let files = match glob::glob(pattern) {
60
66
  Ok(paths) => paths.filter_map(|p| p.ok()).collect::<Vec<_>>(),
61
- Err(_) => return vec![],
67
+ Err(_) => return Ok(vec![]),
62
68
  };
63
69
 
64
70
  let select_fields: Option<Vec<&str>> = select.map(|s| s.split(',').collect());
65
71
 
66
- files
72
+ let results: Vec<serde_json::Value> = files
67
73
  .par_iter()
68
74
  .flat_map(|file| {
69
75
  let mut file_results = Vec::new();
70
76
 
71
77
  if let Ok(document) = Document::parse_file(file) {
72
78
  let values = match condition {
73
- Some(cond) => document.filter(selector, cond),
79
+ Some(cond) => document.filter(selector, cond).unwrap_or_default(),
74
80
  None => document.get_values(selector),
75
81
  };
76
82
 
@@ -108,5 +114,7 @@ pub fn glob_find(pattern: &str, selector: &str, condition: Option<&str>, select:
108
114
 
109
115
  file_results
110
116
  })
111
- .collect()
117
+ .collect();
118
+
119
+ Ok(results)
112
120
  }
data/rust/src/main.rs CHANGED
@@ -20,6 +20,7 @@ static HELP: LazyLock<String> = LazyLock::new(|| {
20
20
  key.nested Nested key path "database.settings.pool"
21
21
  [] All items in array "[].title"
22
22
  [N] Item at index "[0].title"
23
+ * All values in a map "database.*"
23
24
  [].key[].nested Nested array access "[].speakers[].name"
24
25
 
25
26
  Conditions:
data/rust/src/selector.rs CHANGED
@@ -21,6 +21,7 @@ pub enum Selector {
21
21
  pub enum SelectorSegment {
22
22
  Key(String),
23
23
  AllItems,
24
+ AllKeys,
24
25
  Index(usize),
25
26
  }
26
27
 
@@ -69,7 +70,10 @@ impl Selector {
69
70
  }
70
71
 
71
72
  pub fn has_wildcard(&self) -> bool {
72
- self.segments().iter().any(|s| matches!(s, SelectorSegment::AllItems))
73
+ self
74
+ .segments()
75
+ .iter()
76
+ .any(|s| matches!(s, SelectorSegment::AllItems | SelectorSegment::AllKeys))
73
77
  }
74
78
 
75
79
  /// Split into the container selector (up to and including the last []) and the remaining field selector.
@@ -163,6 +167,14 @@ impl Selector {
163
167
  result.push_str("[]");
164
168
  }
165
169
 
170
+ SelectorSegment::AllKeys => {
171
+ if index > 0 && !result.ends_with('.') {
172
+ result.push('.');
173
+ }
174
+
175
+ result.push('*');
176
+ }
177
+
166
178
  SelectorSegment::Index(i) => {
167
179
  result.push_str(&format!("[{}]", i));
168
180
  }
@@ -227,7 +239,9 @@ fn parse_segments(input: &str) -> Vec<SelectorSegment> {
227
239
  Some(index) => {
228
240
  let key = &rest[..index];
229
241
 
230
- if !key.is_empty() {
242
+ if key == "*" {
243
+ segments.push(SelectorSegment::AllKeys);
244
+ } else if !key.is_empty() {
231
245
  segments.push(SelectorSegment::Key(key.to_string()));
232
246
  }
233
247
 
@@ -238,7 +252,9 @@ fn parse_segments(input: &str) -> Vec<SelectorSegment> {
238
252
  }
239
253
  }
240
254
  None => {
241
- if !rest.is_empty() {
255
+ if rest == "*" {
256
+ segments.push(SelectorSegment::AllKeys);
257
+ } else if !rest.is_empty() {
242
258
  segments.push(SelectorSegment::Key(rest.to_string()));
243
259
  }
244
260
 
data/rust/src/syntax.rs CHANGED
@@ -1,7 +1,7 @@
1
1
  use rowan::ast::AstNode;
2
2
  use rowan::{TextRange, TextSize};
3
3
 
4
- use yaml_parser::ast::{BlockMap, BlockMapEntry, BlockSeq};
4
+ use yaml_parser::ast::{BlockMap, BlockMapEntry, BlockSeq, FlowMap, FlowMapEntry, FlowSeq};
5
5
  use yaml_parser::{SyntaxKind, SyntaxNode, SyntaxToken};
6
6
 
7
7
  #[derive(Debug, Clone, PartialEq)]
@@ -82,6 +82,46 @@ pub fn find_block_sequence(node: &SyntaxNode) -> Option<BlockSeq> {
82
82
  node.descendants().find_map(BlockSeq::cast)
83
83
  }
84
84
 
85
+ fn first_collection_node(node: &SyntaxNode) -> Option<SyntaxNode> {
86
+ node.descendants().find(|descendant| {
87
+ matches!(
88
+ descendant.kind(),
89
+ SyntaxKind::BLOCK_MAP | SyntaxKind::BLOCK_SEQ | SyntaxKind::FLOW_MAP | SyntaxKind::FLOW_SEQ
90
+ )
91
+ })
92
+ }
93
+
94
+ pub fn find_flow_sequence(node: &SyntaxNode) -> Option<FlowSeq> {
95
+ first_collection_node(node).and_then(FlowSeq::cast)
96
+ }
97
+
98
+ pub fn find_flow_map(node: &SyntaxNode) -> Option<FlowMap> {
99
+ first_collection_node(node).and_then(FlowMap::cast)
100
+ }
101
+
102
+ pub fn flow_sequence_entries(sequence: &FlowSeq) -> Vec<SyntaxNode> {
103
+ sequence
104
+ .entries()
105
+ .map(|entries| entries.entries().map(|entry| entry.syntax().clone()).collect())
106
+ .unwrap_or_default()
107
+ }
108
+
109
+ pub fn flow_map_entries(map: &FlowMap) -> Vec<FlowMapEntry> {
110
+ map.entries().map(|entries| entries.entries().collect()).unwrap_or_default()
111
+ }
112
+
113
+ pub fn find_flow_entry_by_key(map: &FlowMap, key: &str) -> Option<FlowMapEntry> {
114
+ flow_map_entries(map)
115
+ .into_iter()
116
+ .find(|entry| entry.key().and_then(|found| extract_scalar_text(found.syntax())).as_deref() == Some(key))
117
+ }
118
+
119
+ pub fn in_flow_collection(node: &SyntaxNode) -> bool {
120
+ node
121
+ .ancestors()
122
+ .any(|ancestor| matches!(ancestor.kind(), SyntaxKind::FLOW_MAP | SyntaxKind::FLOW_SEQ))
123
+ }
124
+
85
125
  pub enum FirstCollection {
86
126
  Map(BlockMap),
87
127
  Sequence(BlockSeq),
@@ -288,7 +328,7 @@ pub fn is_yaml_non_string(value: &str) -> bool {
288
328
  }
289
329
 
290
330
  pub fn is_yaml_truthy(value: &str) -> bool {
291
- matches!(value, "true" | "True" | "TRUE" | "yes" | "Yes" | "YES" | "on" | "On" | "ON" | "y" | "Y")
331
+ matches!(value, "true" | "True" | "TRUE" | "yes" | "Yes" | "YES" | "on" | "On" | "ON")
292
332
  }
293
333
 
294
334
  pub fn detect_yaml_type_from_plain(value: &str) -> YerbaValueType {
@@ -297,31 +337,12 @@ pub fn detect_yaml_type_from_plain(value: &str) -> YerbaValueType {
297
337
  return YerbaValueType::Null;
298
338
  }
299
339
 
300
- // Boolean (YAML 1.2 + 1.1)
340
+ // Boolean, resolved the way libyaml/Psych resolve it: the YAML 1.1 bool set
341
+ // without the single-letter y/Y/n/N forms (which libyaml treats as strings),
342
+ // and broader than YAML 1.2 core schema (which only accepts true/false).
301
343
  if matches!(
302
344
  value,
303
- "true"
304
- | "True"
305
- | "TRUE"
306
- | "false"
307
- | "False"
308
- | "FALSE"
309
- | "yes"
310
- | "Yes"
311
- | "YES"
312
- | "no"
313
- | "No"
314
- | "NO"
315
- | "on"
316
- | "On"
317
- | "ON"
318
- | "off"
319
- | "Off"
320
- | "OFF"
321
- | "y"
322
- | "Y"
323
- | "n"
324
- | "N"
345
+ "true" | "True" | "TRUE" | "false" | "False" | "FALSE" | "yes" | "Yes" | "YES" | "no" | "No" | "NO" | "on" | "On" | "ON" | "off" | "Off" | "OFF"
325
346
  ) {
326
347
  return YerbaValueType::Boolean;
327
348
  }
@@ -41,6 +41,7 @@ pub enum PipelineStep {
41
41
  Directives(DirectivesConfig),
42
42
  Unique(UniqueConfig),
43
43
  Schema(SchemaConfig),
44
+ FinalNewline(FinalNewlineConfig),
44
45
  }
45
46
 
46
47
  #[derive(Debug, Clone, Deserialize)]
@@ -95,6 +96,16 @@ pub struct SchemaConfig {
95
96
  pub items: bool,
96
97
  }
97
98
 
99
+ #[derive(Debug, Clone, Deserialize)]
100
+ pub struct FinalNewlineConfig {
101
+ #[serde(default = "default_one")]
102
+ pub count: usize,
103
+ }
104
+
105
+ fn default_one() -> usize {
106
+ 1
107
+ }
108
+
98
109
  #[derive(Debug, Clone, Deserialize)]
99
110
  pub struct RenameConfig {
100
111
  pub from: String,
@@ -188,8 +199,13 @@ impl<'de> Deserialize<'de> for PipelineStep {
188
199
  return Ok(PipelineStep::Schema(config));
189
200
  }
190
201
 
202
+ if let Some(value) = mapping.get(yaml_serde::Value::String("final_newline".to_string())) {
203
+ let config: FinalNewlineConfig = yaml_serde::from_value(value.clone()).map_err(serde::de::Error::custom)?;
204
+ return Ok(PipelineStep::FinalNewline(config));
205
+ }
206
+
191
207
  Err(serde::de::Error::custom(
192
- "unknown pipeline step: expected sort_keys, quote_style, collection_style, sequence_indent, set, insert, delete, rename, remove, blank_lines, sort, directives, unique, or schema",
208
+ "unknown pipeline step: expected sort_keys, quote_style, collection_style, sequence_indent, set, insert, delete, rename, remove, blank_lines, sort, directives, unique, schema, or final_newline",
193
209
  ))
194
210
  }
195
211
  }
@@ -263,9 +279,33 @@ impl Yerbafile {
263
279
  let content = fs::read_to_string(path.as_ref())?;
264
280
  let mut yerbafile: Yerbafile = yaml_serde::from_str(&content).map_err(|error| YerbaError::ParseError(format!("{}", error)))?;
265
281
  yerbafile.directory = path.as_ref().parent().map(|p| p.to_path_buf());
282
+ yerbafile.validate_conditions()?;
266
283
  Ok(yerbafile)
267
284
  }
268
285
 
286
+ fn validate_conditions(&self) -> Result<(), YerbaError> {
287
+ let pipelines = std::iter::once(&self.pipeline).chain(self.rules.iter().map(|rule| &rule.pipeline));
288
+
289
+ for pipeline in pipelines {
290
+ for step in pipeline {
291
+ let condition = match step {
292
+ PipelineStep::Set(config) => config.condition.as_deref(),
293
+ PipelineStep::Insert(config) => config.condition.as_deref(),
294
+ PipelineStep::Delete(config) => config.condition.as_deref(),
295
+ PipelineStep::Rename(config) => config.condition.as_deref(),
296
+ PipelineStep::Remove(config) => config.condition.as_deref(),
297
+ _ => None,
298
+ };
299
+
300
+ if let Some(condition) = condition {
301
+ crate::validate_condition(condition)?;
302
+ }
303
+ }
304
+ }
305
+
306
+ Ok(())
307
+ }
308
+
269
309
  pub fn resolve_path(&self, relative: &str) -> PathBuf {
270
310
  match &self.directory {
271
311
  Some(directory) => directory.join(relative),
@@ -359,6 +399,22 @@ impl Yerbafile {
359
399
  results.extend(file_results);
360
400
  }
361
401
 
402
+ if !self.pipeline.is_empty() {
403
+ if let Some(ref global_glob) = self.files {
404
+ if let Ok(paths) = glob::glob(global_glob) {
405
+ let unprocessed: Vec<String> = paths
406
+ .filter_map(|entry| entry.ok())
407
+ .map(|path| path.to_string_lossy().to_string())
408
+ .filter(|file| !globally_processed.contains(file.as_str()))
409
+ .collect();
410
+
411
+ let global_results: Vec<RuleResult> = unprocessed.par_iter().map(|file| self.apply_global_pipeline_to_file(file, write)).collect();
412
+
413
+ results.extend(global_results);
414
+ }
415
+ }
416
+ }
417
+
362
418
  results
363
419
  }
364
420
 
@@ -436,9 +492,55 @@ impl Yerbafile {
436
492
  ran_global = true;
437
493
  }
438
494
 
495
+ if !ran_global && self.should_run_global_pipeline(file) {
496
+ results.push(self.apply_global_pipeline_to_file(file, write));
497
+ }
498
+
439
499
  results
440
500
  }
441
501
 
502
+ fn apply_global_pipeline_to_file(&self, file: &str, write: bool) -> RuleResult {
503
+ let mut document = match Document::parse_file(file) {
504
+ Ok(document) => document,
505
+ Err(error) => {
506
+ return RuleResult {
507
+ file: file.to_string(),
508
+ changed: false,
509
+ error: Some(error),
510
+ }
511
+ }
512
+ };
513
+
514
+ let original = document.to_string();
515
+
516
+ if let Err(error) = execute_pipeline(&mut document, &self.pipeline, None, file, self) {
517
+ return RuleResult {
518
+ file: file.to_string(),
519
+ changed: false,
520
+ error: Some(error),
521
+ };
522
+ }
523
+
524
+ let new_content = document.to_string();
525
+ let changed = new_content != original;
526
+
527
+ if changed && write {
528
+ if let Err(error) = fs::write(file, &new_content) {
529
+ return RuleResult {
530
+ file: file.to_string(),
531
+ changed,
532
+ error: Some(YerbaError::IoError(error)),
533
+ };
534
+ }
535
+ }
536
+
537
+ RuleResult {
538
+ file: file.to_string(),
539
+ changed,
540
+ error: None,
541
+ }
542
+ }
543
+
442
544
  fn relativize_path(&self, file_path: &str) -> Option<String> {
443
545
  let path = Path::new(file_path);
444
546
 
@@ -597,7 +699,7 @@ fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<
597
699
  if let Some(condition) = &config.condition {
598
700
  let parent_path = full_path.rsplit_once('.').map(|(parent, _)| parent).unwrap_or("");
599
701
 
600
- if !document.evaluate_condition(parent_path, condition) {
702
+ if !document.evaluate_condition(parent_path, condition)? {
601
703
  return Ok(());
602
704
  }
603
705
  }
@@ -611,7 +713,7 @@ fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<
611
713
  if let Some(condition) = &config.condition {
612
714
  let parent_path = full_path.rsplit_once('.').map(|(parent, _)| parent).unwrap_or("");
613
715
 
614
- if !document.evaluate_condition(parent_path, condition) {
716
+ if !document.evaluate_condition(parent_path, condition)? {
615
717
  return Ok(());
616
718
  }
617
719
  }
@@ -629,7 +731,7 @@ fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<
629
731
  if let Some(condition) = &config.condition {
630
732
  let parent_path = selector.rsplit_once('.').map(|(parent, _)| parent).unwrap_or("");
631
733
 
632
- if !document.evaluate_condition(parent_path, condition) {
734
+ if !document.evaluate_condition(parent_path, condition)? {
633
735
  continue;
634
736
  }
635
737
  }
@@ -642,7 +744,7 @@ fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<
642
744
  if let Some(condition) = &config.condition {
643
745
  let parent_path = full_path.rsplit_once('.').map(|(parent, _)| parent).unwrap_or("");
644
746
 
645
- if !document.evaluate_condition(parent_path, condition) {
747
+ if !document.evaluate_condition(parent_path, condition)? {
646
748
  return Ok(());
647
749
  }
648
750
  }
@@ -657,7 +759,7 @@ fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<
657
759
  if let Some(condition) = &config.condition {
658
760
  let parent_path = full_path.rsplit_once('.').map(|(parent, _)| parent).unwrap_or("");
659
761
 
660
- if !document.evaluate_condition(parent_path, condition) {
762
+ if !document.evaluate_condition(parent_path, condition)? {
661
763
  return Ok(());
662
764
  }
663
765
  }
@@ -671,7 +773,7 @@ fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<
671
773
  if let Some(condition) = &config.condition {
672
774
  let parent_path = full_path.rsplit_once('.').map(|(parent, _)| parent).unwrap_or("");
673
775
 
674
- if !document.evaluate_condition(parent_path, condition) {
776
+ if !document.evaluate_condition(parent_path, condition)? {
675
777
  return Ok(());
676
778
  }
677
779
  }
@@ -744,6 +846,8 @@ fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<
744
846
 
745
847
  Ok(())
746
848
  }
849
+
850
+ PipelineStep::FinalNewline(config) => document.enforce_final_newline(config.count),
747
851
  }
748
852
  }
749
853
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yerba
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.5
4
+ version: 0.8.0
5
5
  platform: x86_64-linux-gnu
6
6
  authors:
7
7
  - Marco Roth
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-07 00:00:00.000000000 Z
11
+ date: 2026-07-26 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A CLI tool for editing YAML while preserving structure, comments, and
14
14
  format.