yerba 0.4.2-aarch64-linux-gnu → 0.5.1-aarch64-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.
- checksums.yaml +4 -4
- data/README.md +242 -54
- data/exe/aarch64-linux-gnu/yerba +0 -0
- data/ext/yerba/include/yerba.h +13 -1
- data/ext/yerba/yerba.c +239 -113
- data/lib/yerba/3.2/yerba.so +0 -0
- data/lib/yerba/3.3/yerba.so +0 -0
- data/lib/yerba/3.4/yerba.so +0 -0
- data/lib/yerba/4.0/yerba.so +0 -0
- data/lib/yerba/document.rb +54 -18
- data/lib/yerba/map.rb +55 -43
- data/lib/yerba/node.rb +58 -0
- data/lib/yerba/scalar.rb +20 -23
- data/lib/yerba/sequence.rb +88 -55
- data/lib/yerba/version.rb +1 -1
- data/lib/yerba.rb +2 -0
- data/rust/Cargo.lock +1120 -35
- data/rust/Cargo.toml +3 -2
- data/rust/src/commands/delete.rs +1 -1
- data/rust/src/commands/get.rs +47 -12
- data/rust/src/commands/insert.rs +1 -1
- data/rust/src/commands/location.rs +56 -0
- data/rust/src/commands/mod.rs +33 -5
- data/rust/src/commands/remove.rs +1 -1
- data/rust/src/commands/rename.rs +1 -1
- data/rust/src/commands/schema.rs +84 -0
- data/rust/src/commands/selectors.rs +4 -4
- data/rust/src/commands/set.rs +1 -1
- data/rust/src/commands/sort.rs +1 -1
- data/rust/src/commands/unique.rs +80 -0
- data/rust/src/document/condition.rs +18 -2
- data/rust/src/document/delete.rs +52 -8
- data/rust/src/document/get.rs +256 -25
- data/rust/src/document/insert.rs +3 -3
- data/rust/src/document/mod.rs +112 -34
- data/rust/src/document/schema.rs +73 -0
- data/rust/src/document/set.rs +1 -1
- data/rust/src/document/sort.rs +21 -15
- data/rust/src/document/style.rs +3 -3
- data/rust/src/document/unique.rs +86 -0
- data/rust/src/error.rs +78 -0
- data/rust/src/ffi.rs +89 -9
- data/rust/src/json.rs +16 -16
- data/rust/src/lib.rs +7 -12
- data/rust/src/main.rs +2 -0
- data/rust/src/schema.rs +93 -0
- data/rust/src/selector.rs +16 -0
- data/rust/src/syntax.rs +91 -31
- data/rust/src/yerbafile.rs +127 -81
- metadata +9 -2
data/rust/src/schema.rs
ADDED
|
@@ -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
|
-
|
|
35
|
-
|
|
39
|
+
SyntaxKind::DOUBLE_QUOTED_SCALAR => {
|
|
40
|
+
let raw = token.text();
|
|
41
|
+
unescape_double_quoted(&raw[1..raw.len() - 1])
|
|
42
|
+
}
|
|
36
43
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
SyntaxKind::SINGLE_QUOTED_SCALAR => {
|
|
45
|
+
let raw = token.text();
|
|
46
|
+
unescape_single_quoted(&raw[1..raw.len() - 1])
|
|
47
|
+
}
|
|
41
48
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
unescape_single_quoted(&raw[1..raw.len() - 1])
|
|
45
|
-
}
|
|
49
|
+
_ => return None,
|
|
50
|
+
};
|
|
46
51
|
|
|
47
|
-
|
|
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
|
-
|
|
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
|
|
93
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
127
|
+
SyntaxKind::DOUBLE_QUOTED_SCALAR => {
|
|
128
|
+
let text = token.text();
|
|
129
|
+
let inner = &text[1..text.len() - 1];
|
|
101
130
|
|
|
102
|
-
|
|
103
|
-
|
|
131
|
+
Some(unescape_double_quoted(inner))
|
|
132
|
+
}
|
|
104
133
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
134
|
+
SyntaxKind::SINGLE_QUOTED_SCALAR => {
|
|
135
|
+
let text = token.text();
|
|
136
|
+
let inner = &text[1..text.len() - 1];
|
|
108
137
|
|
|
109
|
-
|
|
110
|
-
|
|
138
|
+
Some(unescape_single_quoted(inner))
|
|
139
|
+
}
|
|
111
140
|
|
|
112
|
-
|
|
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
|
-
//
|
|
264
|
-
if value.starts_with("0x") || value.starts_with("0X")
|
|
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
|
|
data/rust/src/yerbafile.rs
CHANGED
|
@@ -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 =
|
|
111
|
+
let mapping = yaml_serde::Mapping::deserialize(deserializer)?;
|
|
83
112
|
|
|
84
|
-
if let Some(value) = mapping.get(
|
|
85
|
-
let config: SortKeysConfig =
|
|
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(
|
|
90
|
-
let config: QuoteStyleConfig =
|
|
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(
|
|
95
|
-
let config: SetConfig =
|
|
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(
|
|
100
|
-
let config: InsertConfig =
|
|
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(
|
|
105
|
-
let config: DeleteConfig =
|
|
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(
|
|
110
|
-
let config: RenameConfig =
|
|
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(
|
|
115
|
-
let config: RemoveConfig =
|
|
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(
|
|
120
|
-
let config: BlankLinesConfig =
|
|
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(
|
|
125
|
-
let config: DirectivesConfig =
|
|
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(
|
|
130
|
-
let config: SortConfig =
|
|
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
|
|
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<
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
465
|
-
let
|
|
468
|
+
if full_path.contains("[]") {
|
|
469
|
+
let concrete_selectors = document.resolve_selectors(&full_path);
|
|
466
470
|
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
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
|
|