yerba 0.4.2-arm-linux-gnu → 0.5.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 +242 -54
  3. data/exe/arm-linux-gnu/yerba +0 -0
  4. data/ext/yerba/include/yerba.h +13 -1
  5. data/ext/yerba/yerba.c +239 -113
  6. data/lib/yerba/3.2/yerba.so +0 -0
  7. data/lib/yerba/3.3/yerba.so +0 -0
  8. data/lib/yerba/3.4/yerba.so +0 -0
  9. data/lib/yerba/4.0/yerba.so +0 -0
  10. data/lib/yerba/document.rb +54 -18
  11. data/lib/yerba/map.rb +55 -43
  12. data/lib/yerba/node.rb +58 -0
  13. data/lib/yerba/scalar.rb +20 -23
  14. data/lib/yerba/sequence.rb +88 -55
  15. data/lib/yerba/version.rb +1 -1
  16. data/lib/yerba.rb +2 -0
  17. data/rust/Cargo.lock +1120 -35
  18. data/rust/Cargo.toml +3 -2
  19. data/rust/src/commands/delete.rs +1 -1
  20. data/rust/src/commands/get.rs +47 -12
  21. data/rust/src/commands/insert.rs +1 -1
  22. data/rust/src/commands/location.rs +56 -0
  23. data/rust/src/commands/mod.rs +33 -5
  24. data/rust/src/commands/remove.rs +1 -1
  25. data/rust/src/commands/rename.rs +1 -1
  26. data/rust/src/commands/schema.rs +84 -0
  27. data/rust/src/commands/selectors.rs +4 -4
  28. data/rust/src/commands/set.rs +1 -1
  29. data/rust/src/commands/sort.rs +1 -1
  30. data/rust/src/commands/unique.rs +80 -0
  31. data/rust/src/document/condition.rs +18 -2
  32. data/rust/src/document/delete.rs +52 -8
  33. data/rust/src/document/get.rs +256 -25
  34. data/rust/src/document/insert.rs +3 -3
  35. data/rust/src/document/mod.rs +112 -34
  36. data/rust/src/document/schema.rs +73 -0
  37. data/rust/src/document/set.rs +1 -1
  38. data/rust/src/document/sort.rs +21 -15
  39. data/rust/src/document/style.rs +3 -3
  40. data/rust/src/document/unique.rs +86 -0
  41. data/rust/src/error.rs +78 -0
  42. data/rust/src/ffi.rs +89 -9
  43. data/rust/src/json.rs +16 -16
  44. data/rust/src/lib.rs +7 -12
  45. data/rust/src/main.rs +2 -0
  46. data/rust/src/schema.rs +93 -0
  47. data/rust/src/selector.rs +16 -0
  48. data/rust/src/syntax.rs +91 -31
  49. data/rust/src/yerbafile.rs +127 -81
  50. metadata +9 -2
@@ -0,0 +1,93 @@
1
+ use std::fs;
2
+ use std::path::Path;
3
+
4
+ use crate::error::YerbaError;
5
+
6
+ #[derive(Debug)]
7
+ pub struct ValidationError {
8
+ pub path: String,
9
+ pub message: String,
10
+ pub item_label: Option<String>,
11
+ pub line: Option<usize>,
12
+ }
13
+
14
+ impl std::fmt::Display for ValidationError {
15
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16
+ let location = match self.line {
17
+ Some(line) => format!(" (line {})", line),
18
+ None => String::new(),
19
+ };
20
+
21
+ match &self.item_label {
22
+ Some(label) => write!(f, "{} at {} ({}){}", self.message, self.path, label, location),
23
+ None => write!(f, "{} at {}{}", self.message, self.path, location),
24
+ }
25
+ }
26
+ }
27
+
28
+ pub fn validate_value(value: &serde_json::Value, schema: &serde_json::Value) -> Vec<ValidationError> {
29
+ let compiled = match jsonschema::draft7::new(schema) {
30
+ Ok(compiled) => compiled,
31
+ Err(error) => {
32
+ return vec![ValidationError {
33
+ path: String::new(),
34
+ message: format!("invalid schema: {}", error),
35
+ item_label: None,
36
+ line: None,
37
+ }];
38
+ }
39
+ };
40
+
41
+ compiled
42
+ .iter_errors(value)
43
+ .map(|error| ValidationError {
44
+ path: error.instance_path.to_string(),
45
+ message: error.to_string(),
46
+ item_label: None,
47
+ line: None,
48
+ })
49
+ .collect()
50
+ }
51
+
52
+ pub fn validate_array(items: &[serde_json::Value], schema: &serde_json::Value) -> Vec<ValidationError> {
53
+ let compiled = match jsonschema::draft7::new(schema) {
54
+ Ok(compiled) => compiled,
55
+ Err(error) => {
56
+ return vec![ValidationError {
57
+ path: String::new(),
58
+ message: format!("invalid schema: {}", error),
59
+ item_label: None,
60
+ line: None,
61
+ }];
62
+ }
63
+ };
64
+
65
+ let mut errors = Vec::new();
66
+
67
+ for (index, item) in items.iter().enumerate() {
68
+ let item_label = item
69
+ .get("name")
70
+ .or_else(|| item.get("title"))
71
+ .or_else(|| item.get("id"))
72
+ .and_then(|value| value.as_str())
73
+ .map(|string| string.to_string())
74
+ .unwrap_or_else(|| format!("index {}", index));
75
+
76
+ for error in compiled.iter_errors(item) {
77
+ errors.push(ValidationError {
78
+ path: format!("/{}{}", index, error.instance_path),
79
+ message: error.to_string(),
80
+ item_label: Some(item_label.clone()),
81
+ line: None,
82
+ });
83
+ }
84
+ }
85
+
86
+ errors
87
+ }
88
+
89
+ pub fn load_schema(path: impl AsRef<Path>) -> Result<serde_json::Value, YerbaError> {
90
+ let content = fs::read_to_string(path.as_ref())?;
91
+
92
+ serde_json::from_str(&content).map_err(|error| YerbaError::ParseError(format!("invalid JSON schema: {}", error)))
93
+ }
data/rust/src/selector.rs CHANGED
@@ -171,6 +171,22 @@ impl Selector {
171
171
 
172
172
  result
173
173
  }
174
+
175
+ pub fn parent_path(&self) -> String {
176
+ let segments = self.segments();
177
+
178
+ if segments.len() <= 1 {
179
+ return String::new();
180
+ }
181
+
182
+ let parent_segments = &segments[..segments.len() - 1];
183
+ let parent = match self {
184
+ Selector::Relative(_) => Selector::Relative(parent_segments.to_vec()),
185
+ Selector::Absolute(_) => Selector::Absolute(parent_segments.to_vec()),
186
+ };
187
+
188
+ parent.to_selector_string()
189
+ }
174
190
  }
175
191
 
176
192
  fn parse_segments(input: &str) -> Vec<SelectorSegment> {
data/rust/src/syntax.rs CHANGED
@@ -8,6 +8,9 @@ use yaml_parser::{SyntaxKind, SyntaxNode, SyntaxToken};
8
8
  pub struct ScalarValue {
9
9
  pub text: String,
10
10
  pub kind: SyntaxKind,
11
+ pub file_path: Option<String>,
12
+ pub selector: Option<String>,
13
+ pub line: Option<usize>,
11
14
  }
12
15
 
13
16
  #[repr(C)]
@@ -29,25 +32,44 @@ pub fn detect_yaml_type(scalar: &ScalarValue) -> YerbaValueType {
29
32
  }
30
33
 
31
34
  pub fn extract_scalar(node: &SyntaxNode) -> Option<ScalarValue> {
32
- let token = find_scalar_token(node)?;
35
+ if let Some(token) = find_scalar_token(node) {
36
+ let text = match token.kind() {
37
+ SyntaxKind::PLAIN_SCALAR => token.text().to_string(),
33
38
 
34
- let text = match token.kind() {
35
- SyntaxKind::PLAIN_SCALAR => token.text().to_string(),
39
+ SyntaxKind::DOUBLE_QUOTED_SCALAR => {
40
+ let raw = token.text();
41
+ unescape_double_quoted(&raw[1..raw.len() - 1])
42
+ }
36
43
 
37
- SyntaxKind::DOUBLE_QUOTED_SCALAR => {
38
- let raw = token.text();
39
- unescape_double_quoted(&raw[1..raw.len() - 1])
40
- }
44
+ SyntaxKind::SINGLE_QUOTED_SCALAR => {
45
+ let raw = token.text();
46
+ unescape_single_quoted(&raw[1..raw.len() - 1])
47
+ }
41
48
 
42
- SyntaxKind::SINGLE_QUOTED_SCALAR => {
43
- let raw = token.text();
44
- unescape_single_quoted(&raw[1..raw.len() - 1])
45
- }
49
+ _ => return None,
50
+ };
46
51
 
47
- _ => return None,
48
- };
52
+ return Some(ScalarValue {
53
+ text,
54
+ kind: token.kind(),
55
+ file_path: None,
56
+ selector: None,
57
+ line: None,
58
+ });
59
+ }
49
60
 
50
- Some(ScalarValue { text, kind: token.kind() })
61
+ let block_token = node
62
+ .descendants_with_tokens()
63
+ .filter_map(|element| element.into_token())
64
+ .find(|token| token.kind() == SyntaxKind::BLOCK_SCALAR_TEXT)?;
65
+
66
+ Some(ScalarValue {
67
+ text: dedent_block_scalar(block_token.text()),
68
+ kind: SyntaxKind::BLOCK_SCALAR_TEXT,
69
+ file_path: None,
70
+ selector: None,
71
+ line: None,
72
+ })
51
73
  }
52
74
 
53
75
  pub fn is_map_key(token: &SyntaxToken) -> bool {
@@ -89,28 +111,61 @@ pub fn format_scalar_value(value: &str, kind: SyntaxKind) -> String {
89
111
  }
90
112
  }
91
113
 
92
- pub fn extract_scalar_text(node: &SyntaxNode) -> Option<String> {
93
- let token = find_scalar_token(node)?;
114
+ pub fn quote_if_needed(value: &str) -> String {
115
+ if is_yaml_non_string(value) {
116
+ format_scalar_value(value, SyntaxKind::DOUBLE_QUOTED_SCALAR)
117
+ } else {
118
+ value.to_string()
119
+ }
120
+ }
94
121
 
95
- match token.kind() {
96
- SyntaxKind::PLAIN_SCALAR => Some(token.text().to_string()),
122
+ pub fn extract_scalar_text(node: &SyntaxNode) -> Option<String> {
123
+ if let Some(token) = find_scalar_token(node) {
124
+ return match token.kind() {
125
+ SyntaxKind::PLAIN_SCALAR => Some(token.text().to_string()),
97
126
 
98
- SyntaxKind::DOUBLE_QUOTED_SCALAR => {
99
- let text = token.text();
100
- let inner = &text[1..text.len() - 1];
127
+ SyntaxKind::DOUBLE_QUOTED_SCALAR => {
128
+ let text = token.text();
129
+ let inner = &text[1..text.len() - 1];
101
130
 
102
- Some(unescape_double_quoted(inner))
103
- }
131
+ Some(unescape_double_quoted(inner))
132
+ }
104
133
 
105
- SyntaxKind::SINGLE_QUOTED_SCALAR => {
106
- let text = token.text();
107
- let inner = &text[1..text.len() - 1];
134
+ SyntaxKind::SINGLE_QUOTED_SCALAR => {
135
+ let text = token.text();
136
+ let inner = &text[1..text.len() - 1];
108
137
 
109
- Some(unescape_single_quoted(inner))
110
- }
138
+ Some(unescape_single_quoted(inner))
139
+ }
111
140
 
112
- _ => None,
141
+ _ => None,
142
+ };
113
143
  }
144
+
145
+ let block_scalar_text = node
146
+ .descendants_with_tokens()
147
+ .filter_map(|element| element.into_token())
148
+ .find(|token| token.kind() == SyntaxKind::BLOCK_SCALAR_TEXT)?;
149
+
150
+ Some(dedent_block_scalar(block_scalar_text.text()))
151
+ }
152
+
153
+ pub fn dedent_block_scalar(text: &str) -> String {
154
+ let lines: Vec<&str> = text.lines().collect();
155
+ let min_indent = lines
156
+ .iter()
157
+ .filter(|line| !line.trim().is_empty())
158
+ .map(|line| line.len() - line.trim_start().len())
159
+ .min()
160
+ .unwrap_or(0);
161
+
162
+ let dedented: String = lines
163
+ .iter()
164
+ .map(|line| if line.len() >= min_indent { &line[min_indent..] } else { line.trim() })
165
+ .collect::<Vec<_>>()
166
+ .join("\n");
167
+
168
+ dedented.trim().to_string()
114
169
  }
115
170
 
116
171
  pub fn unescape_double_quoted(text: &str) -> String {
@@ -260,8 +315,13 @@ pub fn detect_yaml_type_from_plain(value: &str) -> YerbaValueType {
260
315
  return YerbaValueType::Integer;
261
316
  }
262
317
 
263
- // Octal (0o...) and hex (0x...)
264
- if value.starts_with("0x") || value.starts_with("0X") || value.starts_with("0o") || value.starts_with("0O") {
318
+ // Hex (0x...) only valid hex digits after prefix
319
+ if (value.starts_with("0x") || value.starts_with("0X")) && value.len() > 2 && value[2..].chars().all(|c| c.is_ascii_hexdigit()) {
320
+ return YerbaValueType::Integer;
321
+ }
322
+
323
+ // Octal (0o...) — only valid octal digits after prefix
324
+ if (value.starts_with("0o") || value.starts_with("0O")) && value.len() > 2 && value[2..].chars().all(|c| matches!(c, '0'..='7')) {
265
325
  return YerbaValueType::Integer;
266
326
  }
267
327
 
@@ -9,6 +9,8 @@ use crate::{Document, QuoteStyle, YerbaError};
9
9
  pub struct Yerbafile {
10
10
  #[serde(default)]
11
11
  pub rules: Vec<Rule>,
12
+ #[serde(skip)]
13
+ pub directory: Option<PathBuf>,
12
14
  }
13
15
 
14
16
  #[derive(Debug, Clone, Deserialize)]
@@ -31,6 +33,8 @@ pub enum PipelineStep {
31
33
  BlankLines(BlankLinesConfig),
32
34
  Sort(SortConfig),
33
35
  Directives(DirectivesConfig),
36
+ Unique(UniqueConfig),
37
+ Schema(SchemaConfig),
34
38
  }
35
39
 
36
40
  #[derive(Debug, Clone, Deserialize)]
@@ -58,6 +62,31 @@ pub struct DirectivesConfig {
58
62
  pub remove: bool,
59
63
  }
60
64
 
65
+ #[derive(Debug, Clone, Deserialize)]
66
+ pub struct UniqueConfig {
67
+ #[serde(default)]
68
+ pub path: Option<String>,
69
+ #[serde(default = "default_dot")]
70
+ pub by: String,
71
+ #[serde(default)]
72
+ pub remove: bool,
73
+ #[serde(default)]
74
+ pub allow_blank_duplicates: bool,
75
+ }
76
+
77
+ fn default_dot() -> String {
78
+ ".".to_string()
79
+ }
80
+
81
+ #[derive(Debug, Clone, Deserialize)]
82
+ pub struct SchemaConfig {
83
+ pub file: String,
84
+ #[serde(default)]
85
+ pub path: Option<String>,
86
+ #[serde(default)]
87
+ pub items: bool,
88
+ }
89
+
61
90
  #[derive(Debug, Clone, Deserialize)]
62
91
  pub struct RenameConfig {
63
92
  pub from: String,
@@ -79,60 +108,70 @@ impl<'de> Deserialize<'de> for PipelineStep {
79
108
  where
80
109
  D: serde::Deserializer<'de>,
81
110
  {
82
- let mapping = serde_yaml::Mapping::deserialize(deserializer)?;
111
+ let mapping = yaml_serde::Mapping::deserialize(deserializer)?;
83
112
 
84
- if let Some(value) = mapping.get(serde_yaml::Value::String("sort_keys".to_string())) {
85
- let config: SortKeysConfig = serde_yaml::from_value(value.clone()).map_err(serde::de::Error::custom)?;
113
+ if let Some(value) = mapping.get(yaml_serde::Value::String("sort_keys".to_string())) {
114
+ let config: SortKeysConfig = yaml_serde::from_value(value.clone()).map_err(serde::de::Error::custom)?;
86
115
  return Ok(PipelineStep::SortKeys(config));
87
116
  }
88
117
 
89
- if let Some(value) = mapping.get(serde_yaml::Value::String("quote_style".to_string())) {
90
- let config: QuoteStyleConfig = serde_yaml::from_value(value.clone()).map_err(serde::de::Error::custom)?;
118
+ if let Some(value) = mapping.get(yaml_serde::Value::String("quote_style".to_string())) {
119
+ let config: QuoteStyleConfig = yaml_serde::from_value(value.clone()).map_err(serde::de::Error::custom)?;
91
120
  return Ok(PipelineStep::QuoteStyle(config));
92
121
  }
93
122
 
94
- if let Some(value) = mapping.get(serde_yaml::Value::String("set".to_string())) {
95
- let config: SetConfig = serde_yaml::from_value(value.clone()).map_err(serde::de::Error::custom)?;
123
+ if let Some(value) = mapping.get(yaml_serde::Value::String("set".to_string())) {
124
+ let config: SetConfig = yaml_serde::from_value(value.clone()).map_err(serde::de::Error::custom)?;
96
125
  return Ok(PipelineStep::Set(config));
97
126
  }
98
127
 
99
- if let Some(value) = mapping.get(serde_yaml::Value::String("insert".to_string())) {
100
- let config: InsertConfig = serde_yaml::from_value(value.clone()).map_err(serde::de::Error::custom)?;
128
+ if let Some(value) = mapping.get(yaml_serde::Value::String("insert".to_string())) {
129
+ let config: InsertConfig = yaml_serde::from_value(value.clone()).map_err(serde::de::Error::custom)?;
101
130
  return Ok(PipelineStep::Insert(config));
102
131
  }
103
132
 
104
- if let Some(value) = mapping.get(serde_yaml::Value::String("delete".to_string())) {
105
- let config: DeleteConfig = serde_yaml::from_value(value.clone()).map_err(serde::de::Error::custom)?;
133
+ if let Some(value) = mapping.get(yaml_serde::Value::String("delete".to_string())) {
134
+ let config: DeleteConfig = yaml_serde::from_value(value.clone()).map_err(serde::de::Error::custom)?;
106
135
  return Ok(PipelineStep::Delete(config));
107
136
  }
108
137
 
109
- if let Some(value) = mapping.get(serde_yaml::Value::String("rename".to_string())) {
110
- let config: RenameConfig = serde_yaml::from_value(value.clone()).map_err(serde::de::Error::custom)?;
138
+ if let Some(value) = mapping.get(yaml_serde::Value::String("rename".to_string())) {
139
+ let config: RenameConfig = yaml_serde::from_value(value.clone()).map_err(serde::de::Error::custom)?;
111
140
  return Ok(PipelineStep::Rename(config));
112
141
  }
113
142
 
114
- if let Some(value) = mapping.get(serde_yaml::Value::String("remove".to_string())) {
115
- let config: RemoveConfig = serde_yaml::from_value(value.clone()).map_err(serde::de::Error::custom)?;
143
+ if let Some(value) = mapping.get(yaml_serde::Value::String("remove".to_string())) {
144
+ let config: RemoveConfig = yaml_serde::from_value(value.clone()).map_err(serde::de::Error::custom)?;
116
145
  return Ok(PipelineStep::Remove(config));
117
146
  }
118
147
 
119
- if let Some(value) = mapping.get(serde_yaml::Value::String("blank_lines".to_string())) {
120
- let config: BlankLinesConfig = serde_yaml::from_value(value.clone()).map_err(serde::de::Error::custom)?;
148
+ if let Some(value) = mapping.get(yaml_serde::Value::String("blank_lines".to_string())) {
149
+ let config: BlankLinesConfig = yaml_serde::from_value(value.clone()).map_err(serde::de::Error::custom)?;
121
150
  return Ok(PipelineStep::BlankLines(config));
122
151
  }
123
152
 
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)?;
153
+ if let Some(value) = mapping.get(yaml_serde::Value::String("directives".to_string())) {
154
+ let config: DirectivesConfig = yaml_serde::from_value(value.clone()).map_err(serde::de::Error::custom)?;
126
155
  return Ok(PipelineStep::Directives(config));
127
156
  }
128
157
 
129
- if let Some(value) = mapping.get(serde_yaml::Value::String("sort".to_string())) {
130
- let config: SortConfig = serde_yaml::from_value(value.clone()).map_err(serde::de::Error::custom)?;
158
+ if let Some(value) = mapping.get(yaml_serde::Value::String("sort".to_string())) {
159
+ let config: SortConfig = yaml_serde::from_value(value.clone()).map_err(serde::de::Error::custom)?;
131
160
  return Ok(PipelineStep::Sort(config));
132
161
  }
133
162
 
163
+ if let Some(value) = mapping.get(yaml_serde::Value::String("unique".to_string())) {
164
+ let config: UniqueConfig = yaml_serde::from_value(value.clone()).map_err(serde::de::Error::custom)?;
165
+ return Ok(PipelineStep::Unique(config));
166
+ }
167
+
168
+ if let Some(value) = mapping.get(yaml_serde::Value::String("schema".to_string())) {
169
+ let config: SchemaConfig = yaml_serde::from_value(value.clone()).map_err(serde::de::Error::custom)?;
170
+ return Ok(PipelineStep::Schema(config));
171
+ }
172
+
134
173
  Err(serde::de::Error::custom(
135
- "unknown pipeline step: expected sort_keys, quote_style, set, insert, delete, rename, remove, blank_lines, sort, or directives",
174
+ "unknown pipeline step: expected sort_keys, quote_style, set, insert, delete, rename, remove, blank_lines, sort, directives, unique, or schema",
136
175
  ))
137
176
  }
138
177
  }
@@ -184,16 +223,24 @@ fn default_key_style() -> String {
184
223
  pub struct RuleResult {
185
224
  pub file: String,
186
225
  pub changed: bool,
187
- pub error: Option<String>,
226
+ pub error: Option<YerbaError>,
188
227
  }
189
228
 
190
229
  impl Yerbafile {
191
230
  pub fn load(path: impl AsRef<Path>) -> Result<Self, YerbaError> {
192
231
  let content = fs::read_to_string(path.as_ref())?;
193
- let yerbafile: Yerbafile = serde_yaml::from_str(&content).map_err(|error| YerbaError::ParseError(format!("{}", error)))?;
232
+ let mut yerbafile: Yerbafile = yaml_serde::from_str(&content).map_err(|error| YerbaError::ParseError(format!("{}", error)))?;
233
+ yerbafile.directory = path.as_ref().parent().map(|p| p.to_path_buf());
194
234
  Ok(yerbafile)
195
235
  }
196
236
 
237
+ pub fn resolve_path(&self, relative: &str) -> PathBuf {
238
+ match &self.directory {
239
+ Some(directory) => directory.join(relative),
240
+ None => PathBuf::from(relative),
241
+ }
242
+ }
243
+
197
244
  pub fn find() -> Option<PathBuf> {
198
245
  Self::find_from(std::env::current_dir().ok()?)
199
246
  }
@@ -255,7 +302,7 @@ impl Yerbafile {
255
302
  results.push(RuleResult {
256
303
  file: rule.files.clone(),
257
304
  changed: false,
258
- error: Some(format!("invalid glob: {}", error)),
305
+ error: Some(YerbaError::ParseError(format!("invalid glob: {}", error))),
259
306
  });
260
307
 
261
308
  continue;
@@ -264,50 +311,6 @@ impl Yerbafile {
264
311
 
265
312
  let file_strings: Vec<String> = files.iter().map(|path| path.to_string_lossy().to_string()).collect();
266
313
 
267
- let mut has_validation_error = false;
268
-
269
- for step in &rule.pipeline {
270
- if let PipelineStep::SortKeys(config) = step {
271
- let full_path = resolve_step_path(rule.path.as_deref(), config.path.as_deref());
272
- let key_order: Vec<&str> = config.order.iter().map(|key| key.as_str()).collect();
273
-
274
- let validation_results: Vec<RuleResult> = file_strings
275
- .par_iter()
276
- .filter_map(|file| {
277
- let document = match Document::parse_file(file) {
278
- Ok(document) => document,
279
- Err(error) => {
280
- return Some(RuleResult {
281
- file: file.clone(),
282
- changed: false,
283
- error: Some(format!("{}", error)),
284
- });
285
- }
286
- };
287
-
288
- if let Err(error) = document.validate_sort_keys(&full_path, &key_order) {
289
- Some(RuleResult {
290
- file: file.clone(),
291
- changed: false,
292
- error: Some(format!("{}", error)),
293
- })
294
- } else {
295
- None
296
- }
297
- })
298
- .collect();
299
-
300
- if !validation_results.is_empty() {
301
- has_validation_error = true;
302
- results.extend(validation_results);
303
- }
304
- }
305
- }
306
-
307
- if has_validation_error {
308
- continue;
309
- }
310
-
311
314
  let file_results: Vec<RuleResult> = file_strings.par_iter().map(|file| self.apply_pipeline_to_file(rule, file, write)).collect();
312
315
 
313
316
  results.extend(file_results);
@@ -323,7 +326,7 @@ impl Yerbafile {
323
326
  return RuleResult {
324
327
  file: file.to_string(),
325
328
  changed: false,
326
- error: Some(format!("{}", error)),
329
+ error: Some(error),
327
330
  }
328
331
  }
329
332
  };
@@ -332,11 +335,11 @@ impl Yerbafile {
332
335
  let base_path = rule.path.as_deref();
333
336
 
334
337
  for step in &rule.pipeline {
335
- if let Err(error) = execute_step(&mut document, step, base_path) {
338
+ if let Err(error) = execute_step(&mut document, step, base_path, file, self) {
336
339
  return RuleResult {
337
340
  file: file.to_string(),
338
341
  changed: false,
339
- error: Some(format!("{}", error)),
342
+ error: Some(error),
340
343
  };
341
344
  }
342
345
  }
@@ -349,7 +352,7 @@ impl Yerbafile {
349
352
  return RuleResult {
350
353
  file: file.to_string(),
351
354
  changed,
352
- error: Some(format!("{}", error)),
355
+ error: Some(YerbaError::IoError(error)),
353
356
  };
354
357
  }
355
358
  }
@@ -396,7 +399,7 @@ impl Yerbafile {
396
399
  let base_path = rule.path.as_deref();
397
400
 
398
401
  for step in &rule.pipeline {
399
- execute_step(document, step, base_path)?;
402
+ execute_step(document, step, base_path, file_path, self)?;
400
403
  }
401
404
  }
402
405
 
@@ -404,7 +407,7 @@ impl Yerbafile {
404
407
  }
405
408
  }
406
409
 
407
- fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<&str>) -> Result<(), YerbaError> {
410
+ fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<&str>, _file: &str, yerbafile: &Yerbafile) -> Result<(), YerbaError> {
408
411
  match step {
409
412
  PipelineStep::QuoteStyle(config) => {
410
413
  let dot_path = config.path.as_deref();
@@ -427,6 +430,7 @@ fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<
427
430
  let full_path = resolve_step_path(base_path, config.path.as_deref());
428
431
  let key_order: Vec<&str> = config.order.iter().map(|key| key.as_str()).collect();
429
432
 
433
+ document.validate_sort_keys(&full_path, &key_order)?;
430
434
  document.sort_keys(&full_path, &key_order)
431
435
  }
432
436
 
@@ -461,15 +465,33 @@ fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<
461
465
  PipelineStep::Delete(config) => {
462
466
  let full_path = resolve_step_path(base_path, Some(&config.path));
463
467
 
464
- if let Some(condition) = &config.condition {
465
- let parent_path = full_path.rsplit_once('.').map(|(parent, _)| parent).unwrap_or("");
468
+ if full_path.contains("[]") {
469
+ let concrete_selectors = document.resolve_selectors(&full_path);
466
470
 
467
- if !document.evaluate_condition(parent_path, condition) {
468
- return Ok(());
471
+ for selector in concrete_selectors.into_iter().rev() {
472
+ if let Some(condition) = &config.condition {
473
+ let parent_path = selector.rsplit_once('.').map(|(parent, _)| parent).unwrap_or("");
474
+
475
+ if !document.evaluate_condition(parent_path, condition) {
476
+ continue;
477
+ }
478
+ }
479
+
480
+ document.delete(&selector)?;
481
+ }
482
+
483
+ Ok(())
484
+ } else {
485
+ if let Some(condition) = &config.condition {
486
+ let parent_path = full_path.rsplit_once('.').map(|(parent, _)| parent).unwrap_or("");
487
+
488
+ if !document.evaluate_condition(parent_path, condition) {
489
+ return Ok(());
490
+ }
469
491
  }
470
- }
471
492
 
472
- document.delete(&full_path)
493
+ document.delete(&full_path)
494
+ }
473
495
  }
474
496
 
475
497
  PipelineStep::Rename(config) => {
@@ -526,6 +548,30 @@ fn execute_step(document: &mut Document, step: &PipelineStep, base_path: Option<
526
548
  Ok(())
527
549
  }
528
550
  }
551
+
552
+ PipelineStep::Schema(config) => {
553
+ let schema_path = yerbafile.resolve_path(&config.file);
554
+ let schema = crate::schema::load_schema(&schema_path)?;
555
+ let selector = resolve_step_path(base_path, config.path.as_deref());
556
+ let errors = document.validate_schema(&schema, config.items, if selector.is_empty() { None } else { Some(&selector) });
557
+
558
+ if errors.is_empty() {
559
+ Ok(())
560
+ } else {
561
+ Err(YerbaError::SchemaValidation(errors))
562
+ }
563
+ }
564
+
565
+ PipelineStep::Unique(config) => {
566
+ let full_path = resolve_step_path(base_path, config.path.as_deref());
567
+ let duplicates = document.unique_with_options(&full_path, &config.by, config.remove, config.allow_blank_duplicates)?;
568
+
569
+ if !duplicates.is_empty() && !config.remove {
570
+ return Err(YerbaError::DuplicateValues(duplicates));
571
+ }
572
+
573
+ Ok(())
574
+ }
529
575
  }
530
576
  }
531
577