yerba 0.5.1-x86_64-linux-gnu → 0.6.1-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.
data/rust/src/lib.rs CHANGED
@@ -10,6 +10,7 @@ mod syntax;
10
10
  mod yaml_writer;
11
11
  pub mod yerbafile;
12
12
 
13
+ pub use document::style::StyleEnforcement;
13
14
  pub use document::{collect_selectors, Document, DuplicateInfo, InsertPosition, LocatedNode, Location, NodeInfo, NodeType, SortField};
14
15
  pub use error::YerbaError;
15
16
  pub use quote_style::{KeyStyle, QuoteStyle};
@@ -37,6 +37,23 @@ pub fn json_to_yaml_text(value: &Value, quote_style: &QuoteStyle, indent: usize)
37
37
  .join("\n")
38
38
  }
39
39
 
40
+ Value::Array(array) => {
41
+ let prefix = " ".repeat(indent);
42
+
43
+ array
44
+ .iter()
45
+ .map(|item| match item {
46
+ Value::Object(_) => {
47
+ let inner = json_to_yaml_text(item, quote_style, indent + 2);
48
+ format!("{}- {}", prefix, inner.trim_start())
49
+ }
50
+
51
+ _ => format!("{}- {}", prefix, format_yaml_scalar(item, quote_style)),
52
+ })
53
+ .collect::<Vec<_>>()
54
+ .join("\n")
55
+ }
56
+
40
57
  _ => {
41
58
  let prefix = " ".repeat(indent);
42
59
 
@@ -45,6 +62,87 @@ pub fn json_to_yaml_text(value: &Value, quote_style: &QuoteStyle, indent: usize)
45
62
  }
46
63
  }
47
64
 
65
+ pub fn yaml_value_to_flow_text(value: &yaml_serde::Value) -> String {
66
+ match value {
67
+ yaml_serde::Value::Null => "null".to_string(),
68
+ yaml_serde::Value::Bool(boolean) => boolean.to_string(),
69
+ yaml_serde::Value::Number(number) => number.to_string(),
70
+
71
+ yaml_serde::Value::String(string) => {
72
+ if crate::syntax::is_yaml_non_string(string) {
73
+ format!("\"{}\"", string.replace('"', "\\\""))
74
+ } else {
75
+ string.clone()
76
+ }
77
+ }
78
+
79
+ yaml_serde::Value::Sequence(sequence) => {
80
+ let items: Vec<String> = sequence.iter().map(yaml_value_to_flow_text).collect();
81
+
82
+ format!("[{}]", items.join(", "))
83
+ }
84
+
85
+ yaml_serde::Value::Mapping(mapping) => {
86
+ let pairs: Vec<String> = mapping
87
+ .iter()
88
+ .map(|(key, value)| {
89
+ let key_string = match key {
90
+ yaml_serde::Value::String(string) => string.clone(),
91
+ _ => format!("{:?}", key),
92
+ };
93
+
94
+ format!("{}: {}", key_string, yaml_value_to_flow_text(value))
95
+ })
96
+ .collect();
97
+
98
+ format!("{{{}}}", pairs.join(", "))
99
+ }
100
+
101
+ yaml_serde::Value::Tagged(tagged) => yaml_value_to_flow_text(&tagged.value),
102
+ }
103
+ }
104
+
105
+ pub fn yaml_value_to_block_text(value: &yaml_serde::Value, indent: usize) -> String {
106
+ let prefix = " ".repeat(indent);
107
+
108
+ match value {
109
+ yaml_serde::Value::Sequence(sequence) => sequence
110
+ .iter()
111
+ .map(|item| match item {
112
+ yaml_serde::Value::Mapping(_) => {
113
+ let inner = yaml_value_to_block_text(item, indent + 2);
114
+ format!("{}- {}", prefix, inner.trim_start())
115
+ }
116
+
117
+ _ => format!("{}- {}", prefix, yaml_value_to_flow_text(item)),
118
+ })
119
+ .collect::<Vec<_>>()
120
+ .join("\n"),
121
+
122
+ yaml_serde::Value::Mapping(mapping) => mapping
123
+ .iter()
124
+ .map(|(key, value)| {
125
+ let key_string = match key {
126
+ yaml_serde::Value::String(string) => string.clone(),
127
+ _ => format!("{:?}", key),
128
+ };
129
+ match value {
130
+ yaml_serde::Value::Sequence(_) | yaml_serde::Value::Mapping(_) => {
131
+ let inner = yaml_value_to_block_text(value, indent + 2);
132
+
133
+ format!("{}{}:\n{}", prefix, key_string, inner)
134
+ }
135
+
136
+ _ => format!("{}{}: {}", prefix, key_string, yaml_value_to_flow_text(value)),
137
+ }
138
+ })
139
+ .collect::<Vec<_>>()
140
+ .join("\n"),
141
+
142
+ _ => format!("{}{}", prefix, yaml_value_to_flow_text(value)),
143
+ }
144
+ }
145
+
48
146
  fn format_yaml_scalar(value: &Value, quote_style: &QuoteStyle) -> String {
49
147
  match value {
50
148
  Value::Null => "null".to_string(),
@@ -7,6 +7,10 @@ use crate::{Document, QuoteStyle, YerbaError};
7
7
 
8
8
  #[derive(Debug, Deserialize)]
9
9
  pub struct Yerbafile {
10
+ #[serde(default)]
11
+ pub files: Option<String>,
12
+ #[serde(default)]
13
+ pub pipeline: Vec<PipelineStep>,
10
14
  #[serde(default)]
11
15
  pub rules: Vec<Rule>,
12
16
  #[serde(skip)]
@@ -25,6 +29,8 @@ pub struct Rule {
25
29
  pub enum PipelineStep {
26
30
  SortKeys(SortKeysConfig),
27
31
  QuoteStyle(QuoteStyleConfig),
32
+ CollectionStyle(CollectionStyleConfig),
33
+ SequenceIndent(SequenceIndentConfig),
28
34
  Set(SetConfig),
29
35
  Insert(InsertConfig),
30
36
  Delete(DeleteConfig),
@@ -60,6 +66,8 @@ pub struct DirectivesConfig {
60
66
  pub ensure: bool,
61
67
  #[serde(default)]
62
68
  pub remove: bool,
69
+ #[serde(default)]
70
+ pub max: Option<usize>,
63
71
  }
64
72
 
65
73
  #[derive(Debug, Clone, Deserialize)]
@@ -120,6 +128,16 @@ impl<'de> Deserialize<'de> for PipelineStep {
120
128
  return Ok(PipelineStep::QuoteStyle(config));
121
129
  }
122
130
 
131
+ if let Some(value) = mapping.get(yaml_serde::Value::String("collection_style".to_string())) {
132
+ let config: CollectionStyleConfig = yaml_serde::from_value(value.clone()).map_err(serde::de::Error::custom)?;
133
+ return Ok(PipelineStep::CollectionStyle(config));
134
+ }
135
+
136
+ if let Some(value) = mapping.get(yaml_serde::Value::String("sequence_indent".to_string())) {
137
+ let config: SequenceIndentConfig = yaml_serde::from_value(value.clone()).map_err(serde::de::Error::custom)?;
138
+ return Ok(PipelineStep::SequenceIndent(config));
139
+ }
140
+
123
141
  if let Some(value) = mapping.get(yaml_serde::Value::String("set".to_string())) {
124
142
  let config: SetConfig = yaml_serde::from_value(value.clone()).map_err(serde::de::Error::custom)?;
125
143
  return Ok(PipelineStep::Set(config));
@@ -171,7 +189,7 @@ impl<'de> Deserialize<'de> for PipelineStep {
171
189
  }
172
190
 
173
191
  Err(serde::de::Error::custom(
174
- "unknown pipeline step: expected sort_keys, quote_style, set, insert, delete, rename, remove, blank_lines, sort, directives, unique, or schema",
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",
175
193
  ))
176
194
  }
177
195
  }
@@ -192,6 +210,20 @@ pub struct QuoteStyleConfig {
192
210
  pub path: Option<String>,
193
211
  }
194
212
 
213
+ #[derive(Debug, Clone, Deserialize)]
214
+ pub struct CollectionStyleConfig {
215
+ pub style: String,
216
+ #[serde(default)]
217
+ pub path: Option<String>,
218
+ }
219
+
220
+ #[derive(Debug, Clone, Deserialize)]
221
+ pub struct SequenceIndentConfig {
222
+ pub style: String,
223
+ #[serde(default)]
224
+ pub path: Option<String>,
225
+ }
226
+
195
227
  #[derive(Debug, Clone, Deserialize)]
196
228
  pub struct SetConfig {
197
229
  pub path: String,
@@ -293,6 +325,7 @@ impl Yerbafile {
293
325
 
294
326
  pub fn apply(&self, write: bool) -> Vec<RuleResult> {
295
327
  let mut results = Vec::new();
328
+ let mut globally_processed: std::collections::HashSet<String> = std::collections::HashSet::new();
296
329
 
297
330
  for rule in &self.rules {
298
331
  let files = match glob::glob(&rule.files) {
@@ -311,7 +344,17 @@ impl Yerbafile {
311
344
 
312
345
  let file_strings: Vec<String> = files.iter().map(|path| path.to_string_lossy().to_string()).collect();
313
346
 
314
- let file_results: Vec<RuleResult> = file_strings.par_iter().map(|file| self.apply_pipeline_to_file(rule, file, write)).collect();
347
+ let file_results: Vec<RuleResult> = file_strings
348
+ .par_iter()
349
+ .map(|file| {
350
+ let run_global = !globally_processed.contains(file.as_str()) && self.should_run_global_pipeline(file);
351
+ self.apply_pipeline_to_file(rule, file, write, run_global)
352
+ })
353
+ .collect();
354
+
355
+ for result in &file_results {
356
+ globally_processed.insert(result.file.clone());
357
+ }
315
358
 
316
359
  results.extend(file_results);
317
360
  }
@@ -319,7 +362,7 @@ impl Yerbafile {
319
362
  results
320
363
  }
321
364
 
322
- fn apply_pipeline_to_file(&self, rule: &Rule, file: &str, write: bool) -> RuleResult {
365
+ fn apply_pipeline_to_file(&self, rule: &Rule, file: &str, write: bool, run_global: bool) -> RuleResult {
323
366
  let mut document = match Document::parse_file(file) {
324
367
  Ok(document) => document,
325
368
  Err(error) => {
@@ -332,6 +375,17 @@ impl Yerbafile {
332
375
  };
333
376
 
334
377
  let original = document.to_string();
378
+
379
+ if run_global {
380
+ if let Err(error) = execute_pipeline(&mut document, &self.pipeline, None, file, self) {
381
+ return RuleResult {
382
+ file: file.to_string(),
383
+ changed: false,
384
+ error: Some(error),
385
+ };
386
+ }
387
+ }
388
+
335
389
  let base_path = rule.path.as_deref();
336
390
 
337
391
  for step in &rule.pipeline {
@@ -366,6 +420,7 @@ impl Yerbafile {
366
420
 
367
421
  pub fn apply_file(&self, file: &str, write: bool) -> Vec<RuleResult> {
368
422
  let mut results = Vec::new();
423
+ let mut ran_global = false;
369
424
 
370
425
  for rule in &self.rules {
371
426
  if let Ok(pattern) = glob::Pattern::new(&rule.files) {
@@ -376,15 +431,34 @@ impl Yerbafile {
376
431
  continue;
377
432
  }
378
433
 
379
- results.push(self.apply_pipeline_to_file(rule, file, write));
434
+ let run_global = !ran_global && self.should_run_global_pipeline(file);
435
+ results.push(self.apply_pipeline_to_file(rule, file, write, run_global));
436
+ ran_global = true;
380
437
  }
381
438
 
382
439
  results
383
440
  }
384
441
 
442
+ fn should_run_global_pipeline(&self, file_path: &str) -> bool {
443
+ if self.pipeline.is_empty() {
444
+ return false;
445
+ }
446
+
447
+ match &self.files {
448
+ Some(glob_pattern) => glob::Pattern::new(glob_pattern)
449
+ .map(|pattern| pattern.matches(file_path) || pattern.matches_path(Path::new(file_path)))
450
+ .unwrap_or(false),
451
+ None => true,
452
+ }
453
+ }
454
+
385
455
  pub fn apply_to_document(&self, document: &mut Document, file_path: &str) -> Result<bool, YerbaError> {
386
456
  let original = document.to_string();
387
457
 
458
+ if self.should_run_global_pipeline(file_path) {
459
+ execute_pipeline(document, &self.pipeline, None, file_path, self)?;
460
+ }
461
+
388
462
  for rule in &self.rules {
389
463
  if !file_path.is_empty() {
390
464
  if let Ok(pattern) = glob::Pattern::new(&rule.files) {
@@ -407,6 +481,55 @@ impl Yerbafile {
407
481
  }
408
482
  }
409
483
 
484
+ fn execute_pipeline(document: &mut Document, steps: &[PipelineStep], base_path: Option<&str>, file: &str, yerbafile: &Yerbafile) -> Result<(), YerbaError> {
485
+ use crate::document::style::StyleEnforcement;
486
+
487
+ let mut enforcement = StyleEnforcement {
488
+ collection_style: None,
489
+ sequence_indent: None,
490
+ key_style: None,
491
+ value_style: None,
492
+ };
493
+
494
+ let mut has_enforcement = false;
495
+ let mut remaining_steps: Vec<&PipelineStep> = Vec::new();
496
+
497
+ for step in steps {
498
+ match step {
499
+ PipelineStep::CollectionStyle(config) if config.path.is_none() && base_path.is_none() => {
500
+ enforcement.collection_style = Some(config.style.clone());
501
+ has_enforcement = true;
502
+ }
503
+
504
+ PipelineStep::SequenceIndent(config) if config.path.is_none() && base_path.is_none() => {
505
+ enforcement.sequence_indent = Some(config.style.clone());
506
+ has_enforcement = true;
507
+ }
508
+
509
+ PipelineStep::QuoteStyle(config) if config.path.is_none() && base_path.is_none() => {
510
+ let key_style = config.key_style.parse::<crate::KeyStyle>().ok();
511
+ let value_style = config.value_style.parse::<QuoteStyle>().ok();
512
+
513
+ enforcement.key_style = key_style;
514
+ enforcement.value_style = value_style;
515
+ has_enforcement = true;
516
+ }
517
+
518
+ _ => remaining_steps.push(step),
519
+ }
520
+ }
521
+
522
+ if has_enforcement {
523
+ document.enforce_styles(&enforcement)?;
524
+ }
525
+
526
+ for step in remaining_steps {
527
+ execute_step(document, step, base_path, file, yerbafile)?;
528
+ }
529
+
530
+ Ok(())
531
+ }
532
+
410
533
  fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<&str>, _file: &str, yerbafile: &Yerbafile) -> Result<(), YerbaError> {
411
534
  match step {
412
535
  PipelineStep::QuoteStyle(config) => {
@@ -426,6 +549,20 @@ fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<
426
549
  Ok(())
427
550
  }
428
551
 
552
+ PipelineStep::CollectionStyle(config) => {
553
+ let dot_path = resolve_step_path(base_path, config.path.as_deref());
554
+ let path = if dot_path.is_empty() { None } else { Some(dot_path.as_str()) };
555
+
556
+ document.enforce_collection_style(&config.style, path)
557
+ }
558
+
559
+ PipelineStep::SequenceIndent(config) => {
560
+ let dot_path = resolve_step_path(base_path, config.path.as_deref());
561
+ let path = if dot_path.is_empty() { None } else { Some(dot_path.as_str()) };
562
+
563
+ document.enforce_sequence_indent(&config.style, path)
564
+ }
565
+
429
566
  PipelineStep::SortKeys(config) => {
430
567
  let full_path = resolve_step_path(base_path, config.path.as_deref());
431
568
  let key_order: Vec<&str> = config.order.iter().map(|key| key.as_str()).collect();
@@ -540,6 +677,21 @@ fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<
540
677
  return Err(YerbaError::ParseError("directives: ensure and remove are mutually exclusive".to_string()));
541
678
  }
542
679
 
680
+ if let Some(max) = config.max {
681
+ let locations = document.directive_locations();
682
+
683
+ if locations.len() > max {
684
+ let positions: Vec<String> = locations.iter().map(|(line, col)| format!("{}:{}", line, col)).collect();
685
+
686
+ return Err(YerbaError::ParseError(format!(
687
+ "found {} directive markers (---) at {}, expected at most {}",
688
+ locations.len(),
689
+ positions.join(", "),
690
+ max
691
+ )));
692
+ }
693
+ }
694
+
543
695
  if config.ensure {
544
696
  document.ensure_directives()
545
697
  } else if config.remove {
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.5.1
4
+ version: 0.6.1
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-05-19 00:00:00.000000000 Z
11
+ date: 2026-06-19 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.