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.
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
+ }
@@ -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,8 +190,7 @@ 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
 
@@ -291,10 +304,7 @@ impl Yerbafile {
291
304
  continue;
292
305
  }
293
306
 
294
- let file_results: Vec<RuleResult> = file_strings
295
- .par_iter()
296
- .map(|file| self.apply_pipeline_to_file(rule, file, write))
297
- .collect();
307
+ let file_results: Vec<RuleResult> = file_strings.par_iter().map(|file| self.apply_pipeline_to_file(rule, file, write)).collect();
298
308
 
299
309
  results.extend(file_results);
300
310
  }
@@ -353,15 +363,16 @@ fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<
353
363
  PipelineStep::QuoteStyle(config) => {
354
364
  let dot_path = config.path.as_deref();
355
365
 
356
- let key_style = config.key_style.parse::<QuoteStyle>().map_err(YerbaError::ParseError)?;
366
+ let key_style = config.key_style.parse::<crate::KeyStyle>().map_err(YerbaError::ParseError)?;
357
367
 
358
- let value_style = config
359
- .value_style
360
- .parse::<QuoteStyle>()
361
- .map_err(YerbaError::ParseError)?;
368
+ let value_style = config.value_style.parse::<QuoteStyle>().map_err(YerbaError::ParseError)?;
362
369
 
363
370
  document.enforce_key_style(&key_style, dot_path)?;
364
- document.enforce_quotes_at(&value_style, dot_path)?;
371
+ let warnings = document.enforce_quotes_at(&value_style, dot_path)?;
372
+
373
+ for warning in &warnings {
374
+ eprintln!(" warning: {}", warning);
375
+ }
365
376
 
366
377
  Ok(())
367
378
  }
@@ -451,14 +462,24 @@ fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<
451
462
 
452
463
  PipelineStep::Sort(config) => {
453
464
  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();
465
+ let sort_fields = config.by.as_deref().map(crate::SortField::parse_list).unwrap_or_default();
459
466
 
460
467
  document.sort_items(&full_path, &sort_fields, config.case_sensitive)
461
468
  }
469
+
470
+ PipelineStep::Directives(config) => {
471
+ if config.ensure && config.remove {
472
+ return Err(YerbaError::ParseError("directives: ensure and remove are mutually exclusive".to_string()));
473
+ }
474
+
475
+ if config.ensure {
476
+ document.ensure_directives()
477
+ } else if config.remove {
478
+ document.remove_directives()
479
+ } else {
480
+ Ok(())
481
+ }
482
+ }
462
483
  }
463
484
  }
464
485
 
metadata CHANGED
@@ -1,7 +1,7 @@
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.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marco Roth
@@ -43,6 +43,7 @@ files:
43
43
  - rust/src/commands/blank_lines.rs
44
44
  - rust/src/commands/check.rs
45
45
  - rust/src/commands/delete.rs
46
+ - rust/src/commands/directives.rs
46
47
  - rust/src/commands/get.rs
47
48
  - rust/src/commands/init.rs
48
49
  - rust/src/commands/insert.rs
@@ -59,7 +60,14 @@ files:
59
60
  - rust/src/commands/sort_keys.rs
60
61
  - rust/src/commands/version.rs
61
62
  - rust/src/didyoumean.rs
62
- - rust/src/document.rs
63
+ - rust/src/document/condition.rs
64
+ - rust/src/document/delete.rs
65
+ - rust/src/document/get.rs
66
+ - rust/src/document/insert.rs
67
+ - rust/src/document/mod.rs
68
+ - rust/src/document/set.rs
69
+ - rust/src/document/sort.rs
70
+ - rust/src/document/style.rs
63
71
  - rust/src/error.rs
64
72
  - rust/src/ffi.rs
65
73
  - rust/src/json.rs