yerba 0.3.0-arm-linux-gnu → 0.4.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +93 -8
  3. data/exe/arm-linux-gnu/yerba +0 -0
  4. data/ext/yerba/extconf.rb +22 -3
  5. data/ext/yerba/include/yerba.h +32 -9
  6. data/ext/yerba/yerba.c +133 -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 +45 -3
  13. data/lib/yerba/query_result.rb +50 -0
  14. data/lib/yerba/sequence.rb +187 -1
  15. data/lib/yerba/version.rb +1 -1
  16. data/lib/yerba/yerbafile.rb +45 -0
  17. data/lib/yerba.rb +2 -0
  18. data/rust/Cargo.lock +3 -1
  19. data/rust/Cargo.toml +3 -2
  20. data/rust/cbindgen.toml +1 -0
  21. data/rust/rustfmt.toml +1 -1
  22. data/rust/src/commands/apply.rs +11 -2
  23. data/rust/src/commands/blank_lines.rs +1 -4
  24. data/rust/src/commands/check.rs +11 -2
  25. data/rust/src/commands/directives.rs +61 -0
  26. data/rust/src/commands/get.rs +5 -22
  27. data/rust/src/commands/mod.rs +16 -18
  28. data/rust/src/commands/quote_style.rs +12 -6
  29. data/rust/src/commands/selectors.rs +3 -19
  30. data/rust/src/commands/sort.rs +22 -157
  31. data/rust/src/didyoumean.rs +2 -4
  32. data/rust/src/document/condition.rs +139 -0
  33. data/rust/src/document/delete.rs +91 -0
  34. data/rust/src/document/get.rs +262 -0
  35. data/rust/src/document/insert.rs +384 -0
  36. data/rust/src/document/mod.rs +784 -0
  37. data/rust/src/document/set.rs +100 -0
  38. data/rust/src/document/sort.rs +639 -0
  39. data/rust/src/document/style.rs +473 -0
  40. data/rust/src/error.rs +24 -6
  41. data/rust/src/ffi.rs +272 -518
  42. data/rust/src/json.rs +1 -7
  43. data/rust/src/lib.rs +88 -2
  44. data/rust/src/main.rs +2 -0
  45. data/rust/src/quote_style.rs +83 -7
  46. data/rust/src/selector.rs +2 -7
  47. data/rust/src/syntax.rs +41 -21
  48. data/rust/src/yerbafile.rs +86 -19
  49. metadata +13 -3
  50. data/rust/src/document.rs +0 -2304
data/rust/src/json.rs CHANGED
@@ -117,12 +117,6 @@ pub fn select_field_key(field: &str) -> String {
117
117
  parsed
118
118
  .segments()
119
119
  .iter()
120
- .find_map(|segment| {
121
- if let SelectorSegment::Key(key) = segment {
122
- Some(key.clone())
123
- } else {
124
- None
125
- }
126
- })
120
+ .find_map(|segment| if let SelectorSegment::Key(key) = segment { Some(key.clone()) } else { None })
127
121
  .unwrap_or_else(|| field.to_string())
128
122
  }
data/rust/src/lib.rs CHANGED
@@ -9,9 +9,9 @@ mod syntax;
9
9
  mod yaml_writer;
10
10
  pub mod yerbafile;
11
11
 
12
- pub use document::{collect_selectors, Document, InsertPosition, SortField};
12
+ pub use document::{collect_selectors, Document, InsertPosition, Location, NodeInfo, NodeType, SortField};
13
13
  pub use error::YerbaError;
14
- pub use quote_style::QuoteStyle;
14
+ pub use quote_style::{KeyStyle, QuoteStyle};
15
15
  pub use selector::Selector;
16
16
  pub use syntax::{detect_yaml_type, ScalarValue, YerbaValueType};
17
17
  pub use yaml_writer::json_to_yaml_text;
@@ -28,3 +28,89 @@ pub fn parse(source: &str) -> Result<Document, YerbaError> {
28
28
  pub fn parse_file(path: impl AsRef<std::path::Path>) -> Result<Document, YerbaError> {
29
29
  Document::parse_file(path)
30
30
  }
31
+
32
+ pub fn glob_get(pattern: &str, selector: &str) -> Vec<ScalarValue> {
33
+ use rayon::prelude::*;
34
+
35
+ let parsed_selector = Selector::parse(selector);
36
+
37
+ let files = match glob::glob(pattern) {
38
+ Ok(paths) => paths.filter_map(|p| p.ok()).collect::<Vec<_>>(),
39
+ Err(_) => return vec![],
40
+ };
41
+
42
+ files
43
+ .par_iter()
44
+ .flat_map(|file| {
45
+ let mut results = Vec::new();
46
+
47
+ if let Ok(document) = Document::parse_file(file) {
48
+ if parsed_selector.has_wildcard() {
49
+ results.extend(document.get_all_typed(selector));
50
+ } else if let Some(scalar) = document.get_typed(selector) {
51
+ results.push(scalar);
52
+ }
53
+ }
54
+
55
+ results
56
+ })
57
+ .collect()
58
+ }
59
+
60
+ pub fn glob_find(pattern: &str, selector: &str, condition: Option<&str>, select: Option<&str>) -> Vec<serde_json::Value> {
61
+ use rayon::prelude::*;
62
+
63
+ let files = match glob::glob(pattern) {
64
+ Ok(paths) => paths.filter_map(|p| p.ok()).collect::<Vec<_>>(),
65
+ Err(_) => return vec![],
66
+ };
67
+
68
+ let select_fields: Option<Vec<&str>> = select.map(|s| s.split(',').collect());
69
+
70
+ files
71
+ .par_iter()
72
+ .flat_map(|file| {
73
+ let mut file_results = Vec::new();
74
+
75
+ if let Ok(document) = Document::parse_file(file) {
76
+ let values = match condition {
77
+ Some(cond) => document.filter(selector, cond),
78
+ None => document.get_values(selector),
79
+ };
80
+
81
+ let file_string = file.to_string_lossy().to_string();
82
+
83
+ for value in &values {
84
+ let mut result = serde_json::Map::new();
85
+ result.insert("__file".to_string(), serde_json::Value::String(file_string.clone()));
86
+
87
+ match &select_fields {
88
+ Some(fields) => {
89
+ for field in fields {
90
+ let json_value = json::resolve_select_field(value, field);
91
+ let json_key = json::select_field_key(field);
92
+ result.insert(json_key, json_value);
93
+ }
94
+ }
95
+ None => {
96
+ if let serde_yaml::Value::Mapping(map) = value {
97
+ for (key, yaml_value) in map {
98
+ let json_key = match key {
99
+ serde_yaml::Value::String(string) => string.clone(),
100
+ _ => format!("{:?}", key),
101
+ };
102
+
103
+ result.insert(json_key, json::yaml_to_json(yaml_value));
104
+ }
105
+ }
106
+ }
107
+ }
108
+
109
+ file_results.push(serde_json::Value::Object(result));
110
+ }
111
+ }
112
+
113
+ file_results
114
+ })
115
+ .collect()
116
+ }
data/rust/src/main.rs CHANGED
@@ -31,7 +31,9 @@ static HELP: LazyLock<String> = LazyLock::new(|| {
31
31
  Yerbafile:
32
32
  yerba init Create a new Yerbafile in the current directory
33
33
  yerba check Check if all files match the rules (exits 1 if not)
34
+ yerba check <file> Check a specific file against matching rules
34
35
  yerba apply Apply all rules and write changes
36
+ yerba apply <file> Apply rules to a specific file
35
37
 
36
38
  Examples:
37
39
  yerba get config.yml "database.host"
@@ -1,29 +1,85 @@
1
1
  use clap::ValueEnum;
2
2
  use yaml_parser::SyntaxKind;
3
3
 
4
+ #[derive(Debug, Clone, PartialEq, ValueEnum)]
5
+ pub enum KeyStyle {
6
+ /// Unquoted key (host:)
7
+ Plain,
8
+ /// Single-quoted key ('host':)
9
+ Single,
10
+ /// Double-quoted key ("host":)
11
+ Double,
12
+ }
13
+
14
+ impl std::str::FromStr for KeyStyle {
15
+ type Err = String;
16
+
17
+ fn from_str(string: &str) -> Result<Self, Self::Err> {
18
+ match string {
19
+ "plain" => Ok(KeyStyle::Plain),
20
+ "single" | "single-quoted" => Ok(KeyStyle::Single),
21
+ "double" | "double-quoted" => Ok(KeyStyle::Double),
22
+ _ => Err(format!("unknown key style: '{}'. Valid options: plain, single, double", string)),
23
+ }
24
+ }
25
+ }
26
+
27
+ impl KeyStyle {
28
+ pub(crate) fn to_syntax_kind(&self) -> SyntaxKind {
29
+ match self {
30
+ KeyStyle::Plain => SyntaxKind::PLAIN_SCALAR,
31
+ KeyStyle::Single => SyntaxKind::SINGLE_QUOTED_SCALAR,
32
+ KeyStyle::Double => SyntaxKind::DOUBLE_QUOTED_SCALAR,
33
+ }
34
+ }
35
+ }
36
+
4
37
  #[derive(Debug, Clone, PartialEq, ValueEnum)]
5
38
  pub enum QuoteStyle {
39
+ /// Unquoted value (host: localhost)
6
40
  Plain,
41
+ /// Single-quoted value (host: 'localhost')
7
42
  #[value(alias = "single-quoted")]
8
43
  Single,
44
+ /// Double-quoted value (host: "localhost")
9
45
  #[value(alias = "double-quoted")]
10
46
  Double,
11
- #[value(alias = "block-literal")]
47
+ /// Literal block scalar, strip trailing newline (|-)
48
+ #[value(alias = "block-literal", alias = "|-")]
12
49
  Literal,
13
- #[value(alias = "block-folded")]
50
+ /// Literal block scalar, keep one trailing newline (|)
51
+ #[value(alias = "|")]
52
+ LiteralClip,
53
+ /// Literal block scalar, keep all trailing newlines (|+)
54
+ #[value(alias = "|+")]
55
+ LiteralKeep,
56
+ /// Folded block scalar, strip trailing newline (>-)
57
+ #[value(alias = "block-folded", alias = ">-")]
14
58
  Folded,
59
+ /// Folded block scalar, keep one trailing newline (>)
60
+ #[value(alias = ">")]
61
+ FoldedClip,
62
+ /// Folded block scalar, keep all trailing newlines (>+)
63
+ #[value(alias = ">+")]
64
+ FoldedKeep,
15
65
  }
16
66
 
17
67
  impl std::str::FromStr for QuoteStyle {
18
68
  type Err = String;
19
69
 
20
70
  fn from_str(string: &str) -> Result<Self, Self::Err> {
21
- match string {
71
+ let normalized = string.replace('_', "-");
72
+
73
+ match normalized.as_str() {
22
74
  "plain" => Ok(QuoteStyle::Plain),
23
75
  "single" | "single-quoted" => Ok(QuoteStyle::Single),
24
76
  "double" | "double-quoted" => Ok(QuoteStyle::Double),
25
- "literal" | "block-literal" => Ok(QuoteStyle::Literal),
26
- "folded" | "block-folded" => Ok(QuoteStyle::Folded),
77
+ "literal" | "block-literal" | "|-" => Ok(QuoteStyle::Literal),
78
+ "literal-clip" | "|" => Ok(QuoteStyle::LiteralClip),
79
+ "literal-keep" | "|+" => Ok(QuoteStyle::LiteralKeep),
80
+ "folded" | "block-folded" | ">-" => Ok(QuoteStyle::Folded),
81
+ "folded-clip" | ">" => Ok(QuoteStyle::FoldedClip),
82
+ "folded-keep" | ">+" => Ok(QuoteStyle::FoldedKeep),
27
83
  _ => Err(format!("unknown quote style: '{}'", string)),
28
84
  }
29
85
  }
@@ -35,8 +91,28 @@ impl QuoteStyle {
35
91
  QuoteStyle::Plain => SyntaxKind::PLAIN_SCALAR,
36
92
  QuoteStyle::Single => SyntaxKind::SINGLE_QUOTED_SCALAR,
37
93
  QuoteStyle::Double => SyntaxKind::DOUBLE_QUOTED_SCALAR,
38
- QuoteStyle::Literal => SyntaxKind::BLOCK_SCALAR_TEXT,
39
- QuoteStyle::Folded => SyntaxKind::BLOCK_SCALAR_TEXT,
94
+ QuoteStyle::Literal | QuoteStyle::LiteralClip | QuoteStyle::LiteralKeep | QuoteStyle::Folded | QuoteStyle::FoldedClip | QuoteStyle::FoldedKeep => {
95
+ SyntaxKind::BLOCK_SCALAR_TEXT
96
+ }
97
+ }
98
+ }
99
+
100
+ pub(crate) fn is_block_scalar(&self) -> bool {
101
+ matches!(
102
+ self,
103
+ QuoteStyle::Literal | QuoteStyle::LiteralClip | QuoteStyle::LiteralKeep | QuoteStyle::Folded | QuoteStyle::FoldedClip | QuoteStyle::FoldedKeep
104
+ )
105
+ }
106
+
107
+ pub(crate) fn block_header(&self) -> &'static str {
108
+ match self {
109
+ QuoteStyle::Literal => "|-",
110
+ QuoteStyle::LiteralClip => "|",
111
+ QuoteStyle::LiteralKeep => "|+",
112
+ QuoteStyle::Folded => ">-",
113
+ QuoteStyle::FoldedClip => ">",
114
+ QuoteStyle::FoldedKeep => ">+",
115
+ _ => "",
40
116
  }
41
117
  }
42
118
  }
data/rust/src/selector.rs CHANGED
@@ -58,10 +58,7 @@ impl Selector {
58
58
  }
59
59
 
60
60
  pub fn ends_with_bracket(&self) -> bool {
61
- matches!(
62
- self.segments().last(),
63
- Some(SelectorSegment::AllItems | SelectorSegment::Index(_))
64
- )
61
+ matches!(self.segments().last(), Some(SelectorSegment::AllItems | SelectorSegment::Index(_)))
65
62
  }
66
63
 
67
64
  pub fn has_brackets(&self) -> bool {
@@ -109,9 +106,7 @@ impl Selector {
109
106
  pub fn split_at_first_bracket(&self) -> (Selector, Selector) {
110
107
  let segments = self.segments();
111
108
 
112
- let first_bracket = segments
113
- .iter()
114
- .position(|s| matches!(s, SelectorSegment::AllItems | SelectorSegment::Index(_)));
109
+ let first_bracket = segments.iter().position(|s| matches!(s, SelectorSegment::AllItems | SelectorSegment::Index(_)));
115
110
 
116
111
  match first_bracket {
117
112
  Some(position) => {
data/rust/src/syntax.rs CHANGED
@@ -47,16 +47,11 @@ pub fn extract_scalar(node: &SyntaxNode) -> Option<ScalarValue> {
47
47
  _ => return None,
48
48
  };
49
49
 
50
- Some(ScalarValue {
51
- text,
52
- kind: token.kind(),
53
- })
50
+ Some(ScalarValue { text, kind: token.kind() })
54
51
  }
55
52
 
56
53
  pub fn is_map_key(token: &SyntaxToken) -> bool {
57
- token
58
- .parent_ancestors()
59
- .any(|ancestor| ancestor.kind() == SyntaxKind::BLOCK_MAP_KEY)
54
+ token.parent_ancestors().any(|ancestor| ancestor.kind() == SyntaxKind::BLOCK_MAP_KEY)
60
55
  }
61
56
 
62
57
  pub fn find_entry_by_key(map: &BlockMap, key: &str) -> Option<BlockMapEntry> {
@@ -70,15 +65,12 @@ pub fn find_entry_by_key(map: &BlockMap, key: &str) -> Option<BlockMapEntry> {
70
65
  }
71
66
 
72
67
  pub fn find_scalar_token(node: &SyntaxNode) -> Option<SyntaxToken> {
73
- node
74
- .descendants_with_tokens()
75
- .filter_map(|element| element.into_token())
76
- .find(|token| {
77
- matches!(
78
- token.kind(),
79
- SyntaxKind::PLAIN_SCALAR | SyntaxKind::DOUBLE_QUOTED_SCALAR | SyntaxKind::SINGLE_QUOTED_SCALAR
80
- )
81
- })
68
+ node.descendants_with_tokens().filter_map(|element| element.into_token()).find(|token| {
69
+ matches!(
70
+ token.kind(),
71
+ SyntaxKind::PLAIN_SCALAR | SyntaxKind::DOUBLE_QUOTED_SCALAR | SyntaxKind::SINGLE_QUOTED_SCALAR
72
+ )
73
+ })
82
74
  }
83
75
 
84
76
  pub fn format_scalar_value(value: &str, kind: SyntaxKind) -> String {
@@ -122,7 +114,38 @@ pub fn extract_scalar_text(node: &SyntaxNode) -> Option<String> {
122
114
  }
123
115
 
124
116
  pub fn unescape_double_quoted(text: &str) -> String {
125
- text.replace("\\\"", "\"").replace("\\\\", "\\")
117
+ let mut result = String::with_capacity(text.len());
118
+ let mut chars = text.chars();
119
+
120
+ while let Some(character) = chars.next() {
121
+ if character == '\\' {
122
+ match chars.next() {
123
+ Some('n') => result.push('\n'),
124
+ Some('t') => result.push('\t'),
125
+ Some('r') => result.push('\r'),
126
+ Some('\\') => result.push('\\'),
127
+ Some('"') => result.push('"'),
128
+ Some('/') => result.push('/'),
129
+ Some('0') => result.push('\0'),
130
+ Some('a') => result.push('\u{07}'),
131
+ Some('b') => result.push('\u{08}'),
132
+ Some('e') => result.push('\u{1b}'),
133
+ Some('v') => result.push('\u{0b}'),
134
+ Some(' ') => result.push(' '),
135
+ Some('_') => result.push('\u{a0}'),
136
+ Some('\n') => {} // line continuation: skip newline and leading whitespace
137
+ Some(other) => {
138
+ result.push('\\');
139
+ result.push(other);
140
+ }
141
+ None => result.push('\\'),
142
+ }
143
+ } else {
144
+ result.push(character);
145
+ }
146
+ }
147
+
148
+ result
126
149
  }
127
150
 
128
151
  pub fn unescape_single_quoted(text: &str) -> String {
@@ -194,10 +217,7 @@ pub fn is_yaml_non_string(value: &str) -> bool {
194
217
  }
195
218
 
196
219
  pub fn is_yaml_truthy(value: &str) -> bool {
197
- matches!(
198
- value,
199
- "true" | "True" | "TRUE" | "yes" | "Yes" | "YES" | "on" | "On" | "ON" | "y" | "Y"
200
- )
220
+ matches!(value, "true" | "True" | "TRUE" | "yes" | "Yes" | "YES" | "on" | "On" | "ON" | "y" | "Y")
201
221
  }
202
222
 
203
223
  pub fn detect_yaml_type_from_plain(value: &str) -> YerbaValueType {
@@ -30,6 +30,7 @@ pub enum PipelineStep {
30
30
  Remove(RemoveConfig),
31
31
  BlankLines(BlankLinesConfig),
32
32
  Sort(SortConfig),
33
+ Directives(DirectivesConfig),
33
34
  }
34
35
 
35
36
  #[derive(Debug, Clone, Deserialize)]
@@ -49,6 +50,14 @@ pub struct BlankLinesConfig {
49
50
  pub count: usize,
50
51
  }
51
52
 
53
+ #[derive(Debug, Clone, Deserialize)]
54
+ pub struct DirectivesConfig {
55
+ #[serde(default)]
56
+ pub ensure: bool,
57
+ #[serde(default)]
58
+ pub remove: bool,
59
+ }
60
+
52
61
  #[derive(Debug, Clone, Deserialize)]
53
62
  pub struct RenameConfig {
54
63
  pub from: String,
@@ -112,13 +121,18 @@ impl<'de> Deserialize<'de> for PipelineStep {
112
121
  return Ok(PipelineStep::BlankLines(config));
113
122
  }
114
123
 
124
+ if let Some(value) = mapping.get(serde_yaml::Value::String("directives".to_string())) {
125
+ let config: DirectivesConfig = serde_yaml::from_value(value.clone()).map_err(serde::de::Error::custom)?;
126
+ return Ok(PipelineStep::Directives(config));
127
+ }
128
+
115
129
  if let Some(value) = mapping.get(serde_yaml::Value::String("sort".to_string())) {
116
130
  let config: SortConfig = serde_yaml::from_value(value.clone()).map_err(serde::de::Error::custom)?;
117
131
  return Ok(PipelineStep::Sort(config));
118
132
  }
119
133
 
120
134
  Err(serde::de::Error::custom(
121
- "unknown pipeline step: expected sort_keys, quote_style, set, insert, delete, rename, remove, blank_lines, or sort",
135
+ "unknown pipeline step: expected sort_keys, quote_style, set, insert, delete, rename, remove, blank_lines, sort, or directives",
122
136
  ))
123
137
  }
124
138
  }
@@ -176,15 +190,18 @@ pub struct RuleResult {
176
190
  impl Yerbafile {
177
191
  pub fn load(path: impl AsRef<Path>) -> Result<Self, YerbaError> {
178
192
  let content = fs::read_to_string(path.as_ref())?;
179
- let yerbafile: Yerbafile =
180
- serde_yaml::from_str(&content).map_err(|error| YerbaError::ParseError(format!("{}", error)))?;
193
+ let yerbafile: Yerbafile = serde_yaml::from_str(&content).map_err(|error| YerbaError::ParseError(format!("{}", error)))?;
181
194
  Ok(yerbafile)
182
195
  }
183
196
 
184
197
  pub fn find() -> Option<PathBuf> {
198
+ Self::find_from(std::env::current_dir().ok()?)
199
+ }
200
+
201
+ pub fn find_from(start: impl AsRef<Path>) -> Option<PathBuf> {
185
202
  let candidates = ["Yerbafile", "Yerbafile.yml", "Yerbafile.yaml", ".yerbafile"];
186
203
 
187
- let mut directory = std::env::current_dir().ok()?;
204
+ let mut directory = start.as_ref().to_path_buf();
188
205
 
189
206
  loop {
190
207
  for candidate in &candidates {
@@ -291,10 +308,7 @@ impl Yerbafile {
291
308
  continue;
292
309
  }
293
310
 
294
- let file_results: Vec<RuleResult> = file_strings
295
- .par_iter()
296
- .map(|file| self.apply_pipeline_to_file(rule, file, write))
297
- .collect();
311
+ let file_results: Vec<RuleResult> = file_strings.par_iter().map(|file| self.apply_pipeline_to_file(rule, file, write)).collect();
298
312
 
299
313
  results.extend(file_results);
300
314
  }
@@ -346,6 +360,48 @@ impl Yerbafile {
346
360
  error: None,
347
361
  }
348
362
  }
363
+
364
+ pub fn apply_file(&self, file: &str, write: bool) -> Vec<RuleResult> {
365
+ let mut results = Vec::new();
366
+
367
+ for rule in &self.rules {
368
+ if let Ok(pattern) = glob::Pattern::new(&rule.files) {
369
+ if !pattern.matches(file) && !pattern.matches_path(Path::new(file)) {
370
+ continue;
371
+ }
372
+ } else {
373
+ continue;
374
+ }
375
+
376
+ results.push(self.apply_pipeline_to_file(rule, file, write));
377
+ }
378
+
379
+ results
380
+ }
381
+
382
+ pub fn apply_to_document(&self, document: &mut Document, file_path: &str) -> Result<bool, YerbaError> {
383
+ let original = document.to_string();
384
+
385
+ for rule in &self.rules {
386
+ if !file_path.is_empty() {
387
+ if let Ok(pattern) = glob::Pattern::new(&rule.files) {
388
+ if !pattern.matches(file_path) && !pattern.matches_path(Path::new(file_path)) {
389
+ continue;
390
+ }
391
+ } else {
392
+ continue;
393
+ }
394
+ }
395
+
396
+ let base_path = rule.path.as_deref();
397
+
398
+ for step in &rule.pipeline {
399
+ execute_step(document, step, base_path)?;
400
+ }
401
+ }
402
+
403
+ Ok(document.to_string() != original)
404
+ }
349
405
  }
350
406
 
351
407
  fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<&str>) -> Result<(), YerbaError> {
@@ -353,15 +409,16 @@ fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<
353
409
  PipelineStep::QuoteStyle(config) => {
354
410
  let dot_path = config.path.as_deref();
355
411
 
356
- let key_style = config.key_style.parse::<QuoteStyle>().map_err(YerbaError::ParseError)?;
412
+ let key_style = config.key_style.parse::<crate::KeyStyle>().map_err(YerbaError::ParseError)?;
357
413
 
358
- let value_style = config
359
- .value_style
360
- .parse::<QuoteStyle>()
361
- .map_err(YerbaError::ParseError)?;
414
+ let value_style = config.value_style.parse::<QuoteStyle>().map_err(YerbaError::ParseError)?;
362
415
 
363
416
  document.enforce_key_style(&key_style, dot_path)?;
364
- document.enforce_quotes_at(&value_style, dot_path)?;
417
+ let warnings = document.enforce_quotes_at(&value_style, dot_path)?;
418
+
419
+ for warning in &warnings {
420
+ eprintln!(" warning: {}", warning);
421
+ }
365
422
 
366
423
  Ok(())
367
424
  }
@@ -451,14 +508,24 @@ fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<
451
508
 
452
509
  PipelineStep::Sort(config) => {
453
510
  let full_path = resolve_step_path(base_path, config.path.as_deref());
454
- let sort_fields = config
455
- .by
456
- .as_deref()
457
- .map(crate::SortField::parse_list)
458
- .unwrap_or_default();
511
+ let sort_fields = config.by.as_deref().map(crate::SortField::parse_list).unwrap_or_default();
459
512
 
460
513
  document.sort_items(&full_path, &sort_fields, config.case_sensitive)
461
514
  }
515
+
516
+ PipelineStep::Directives(config) => {
517
+ if config.ensure && config.remove {
518
+ return Err(YerbaError::ParseError("directives: ensure and remove are mutually exclusive".to_string()));
519
+ }
520
+
521
+ if config.ensure {
522
+ document.ensure_directives()
523
+ } else if config.remove {
524
+ document.remove_directives()
525
+ } else {
526
+ Ok(())
527
+ }
528
+ }
462
529
  }
463
530
  }
464
531
 
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.3.0
4
+ version: 0.4.1
5
5
  platform: arm-linux-gnu
6
6
  authors:
7
7
  - Marco Roth
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-04 00:00:00.000000000 Z
11
+ date: 2026-05-05 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.
@@ -36,9 +36,11 @@ files:
36
36
  - lib/yerba/formatting.rb
37
37
  - lib/yerba/location.rb
38
38
  - lib/yerba/map.rb
39
+ - lib/yerba/query_result.rb
39
40
  - lib/yerba/scalar.rb
40
41
  - lib/yerba/sequence.rb
41
42
  - lib/yerba/version.rb
43
+ - lib/yerba/yerbafile.rb
42
44
  - rust/Cargo.lock
43
45
  - rust/Cargo.toml
44
46
  - rust/build.rs
@@ -48,6 +50,7 @@ files:
48
50
  - rust/src/commands/blank_lines.rs
49
51
  - rust/src/commands/check.rs
50
52
  - rust/src/commands/delete.rs
53
+ - rust/src/commands/directives.rs
51
54
  - rust/src/commands/get.rs
52
55
  - rust/src/commands/init.rs
53
56
  - rust/src/commands/insert.rs
@@ -64,7 +67,14 @@ files:
64
67
  - rust/src/commands/sort_keys.rs
65
68
  - rust/src/commands/version.rs
66
69
  - rust/src/didyoumean.rs
67
- - rust/src/document.rs
70
+ - rust/src/document/condition.rs
71
+ - rust/src/document/delete.rs
72
+ - rust/src/document/get.rs
73
+ - rust/src/document/insert.rs
74
+ - rust/src/document/mod.rs
75
+ - rust/src/document/set.rs
76
+ - rust/src/document/sort.rs
77
+ - rust/src/document/style.rs
68
78
  - rust/src/error.rs
69
79
  - rust/src/ffi.rs
70
80
  - rust/src/json.rs