yerba 0.4.2 → 0.5.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.
- checksums.yaml +4 -4
- data/README.md +241 -53
- data/ext/yerba/include/yerba.h +13 -1
- data/ext/yerba/yerba.c +239 -113
- 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.toml +2 -1
- 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/set.rs +1 -1
- data/rust/src/commands/unique.rs +80 -0
- data/rust/src/document/condition.rs +17 -1
- data/rust/src/document/get.rs +254 -23
- data/rust/src/document/mod.rs +90 -12
- data/rust/src/document/schema.rs +73 -0
- data/rust/src/document/set.rs +1 -1
- data/rust/src/document/sort.rs +19 -13
- 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/lib.rs +5 -10
- data/rust/src/main.rs +2 -0
- data/rust/src/schema.rs +93 -0
- data/rust/src/syntax.rs +91 -31
- data/rust/src/yerbafile.rs +107 -18
- metadata +8 -1
data/rust/src/commands/get.rs
CHANGED
|
@@ -70,20 +70,22 @@ impl Args {
|
|
|
70
70
|
for resolved_file in &files {
|
|
71
71
|
let document = parse_file(resolved_file);
|
|
72
72
|
|
|
73
|
-
if
|
|
73
|
+
if !document.exists(&self.selector) {
|
|
74
74
|
if is_glob {
|
|
75
75
|
continue;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
use super::color::*;
|
|
79
79
|
|
|
80
|
-
eprintln!("{RED}Error:{RESET} selector \"{}\" not found in {}", self.selector,
|
|
80
|
+
eprintln!("{RED}Error:{RESET} selector \"{}\" not found in {}", self.selector, self.file);
|
|
81
81
|
|
|
82
|
-
show_similar_selectors(
|
|
82
|
+
show_similar_selectors(&self.file, &document, &self.selector);
|
|
83
83
|
process::exit(1);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
if let Some(fields) = &select_fields {
|
|
87
|
+
let mut missing_field = false;
|
|
88
|
+
|
|
87
89
|
for field in fields {
|
|
88
90
|
let field_trimmed = field.trim().trim_start_matches('.');
|
|
89
91
|
let full_selector = if search_path_string.is_empty() {
|
|
@@ -93,24 +95,49 @@ impl Args {
|
|
|
93
95
|
};
|
|
94
96
|
|
|
95
97
|
if !document.exists(&full_selector) {
|
|
98
|
+
if is_glob {
|
|
99
|
+
missing_field = true;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
|
|
96
103
|
use super::color::*;
|
|
97
|
-
eprintln!("{RED}Error:{RESET} select field \"{}\" not found in {}", field.trim(),
|
|
98
|
-
show_similar_selectors(
|
|
104
|
+
eprintln!("{RED}Error:{RESET} select field \"{}\" not found in {}", field.trim(), self.file);
|
|
105
|
+
show_similar_selectors(&self.file, &document, &full_selector);
|
|
99
106
|
process::exit(1);
|
|
100
107
|
}
|
|
101
108
|
}
|
|
109
|
+
|
|
110
|
+
if missing_field {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
102
113
|
}
|
|
103
114
|
|
|
104
|
-
let values: Vec<serde_yaml::Value> = if
|
|
105
|
-
|
|
115
|
+
let (values, selectors, lines): (Vec<serde_yaml::Value>, Vec<String>, Vec<usize>) = if select_fields.is_some() {
|
|
116
|
+
if let Some(condition) = &normalized_condition {
|
|
117
|
+
let triples = document.filter_with_selectors(&search_path_string, condition);
|
|
118
|
+
let (values, rest): (Vec<_>, Vec<_>) = triples.into_iter().map(|(v, s, l)| (v, (s, l))).unzip();
|
|
119
|
+
let (selectors, lines): (Vec<_>, Vec<_>) = rest.into_iter().unzip();
|
|
120
|
+
|
|
121
|
+
(values, selectors, lines)
|
|
122
|
+
} else {
|
|
123
|
+
let located = document.get_all_located(&search_path_string);
|
|
124
|
+
let selectors = located.iter().map(|n| n.selector.clone()).collect();
|
|
125
|
+
let lines = located.iter().map(|n| n.line).collect();
|
|
126
|
+
let values = document.get_values(&search_path_string);
|
|
127
|
+
let values: Vec<_> = values.into_iter().filter(|v| !v.is_null()).collect();
|
|
128
|
+
|
|
129
|
+
(values, selectors, lines)
|
|
130
|
+
}
|
|
131
|
+
} else if let Some(condition) = &normalized_condition {
|
|
132
|
+
(document.filter(&search_path_string, condition), Vec::new(), Vec::new())
|
|
106
133
|
} else {
|
|
107
|
-
document.get_values(&search_path_string)
|
|
134
|
+
(document.get_values(&search_path_string), Vec::new(), Vec::new())
|
|
108
135
|
};
|
|
109
136
|
|
|
110
|
-
for value in values {
|
|
137
|
+
for (index, value) in values.iter().enumerate() {
|
|
111
138
|
if let Some(field) = &extract_field {
|
|
112
139
|
let field_string = field.to_selector_string();
|
|
113
|
-
let json_value = yerba::json::resolve_select_field(
|
|
140
|
+
let json_value = yerba::json::resolve_select_field(value, &field_string);
|
|
114
141
|
|
|
115
142
|
if field.ends_with_bracket() {
|
|
116
143
|
if let serde_json::Value::Array(items) = json_value {
|
|
@@ -128,8 +155,16 @@ impl Args {
|
|
|
128
155
|
|
|
129
156
|
result.insert("__file".to_string(), serde_json::Value::String(resolved_file.clone()));
|
|
130
157
|
|
|
158
|
+
if let Some(selector) = selectors.get(index) {
|
|
159
|
+
result.insert("__selector".to_string(), serde_json::Value::String(selector.clone()));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if let Some(&line) = lines.get(index) {
|
|
163
|
+
result.insert("__line".to_string(), serde_json::Value::Number(line.into()));
|
|
164
|
+
}
|
|
165
|
+
|
|
131
166
|
for field in fields {
|
|
132
|
-
let json_value = yerba::json::resolve_select_field(
|
|
167
|
+
let json_value = yerba::json::resolve_select_field(value, field);
|
|
133
168
|
let json_key = yerba::json::select_field_key(field);
|
|
134
169
|
|
|
135
170
|
result.insert(json_key, json_value);
|
|
@@ -140,7 +175,7 @@ impl Args {
|
|
|
140
175
|
continue;
|
|
141
176
|
}
|
|
142
177
|
|
|
143
|
-
all_results.push(yerba::json::yaml_to_json(
|
|
178
|
+
all_results.push(yerba::json::yaml_to_json(value));
|
|
144
179
|
}
|
|
145
180
|
}
|
|
146
181
|
|
data/rust/src/commands/insert.rs
CHANGED
|
@@ -103,7 +103,7 @@ impl Args {
|
|
|
103
103
|
let mut document = parse_file(&resolved_file);
|
|
104
104
|
let result = document.insert_into(&self.selector, &resolved_value, position.clone());
|
|
105
105
|
|
|
106
|
-
run_op(&
|
|
106
|
+
run_op(&self.file, &document, result);
|
|
107
107
|
output(&resolved_file, &document, self.dry_run);
|
|
108
108
|
}
|
|
109
109
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
use std::process;
|
|
2
|
+
use std::sync::LazyLock;
|
|
3
|
+
|
|
4
|
+
use indoc::indoc;
|
|
5
|
+
|
|
6
|
+
use super::colorize_examples;
|
|
7
|
+
use super::parse_file;
|
|
8
|
+
|
|
9
|
+
static EXAMPLES: LazyLock<String> = LazyLock::new(|| {
|
|
10
|
+
colorize_examples(indoc! {r#"
|
|
11
|
+
yerba location config.yml "database.host"
|
|
12
|
+
yerba location videos.yml "[0]"
|
|
13
|
+
yerba location videos.yml "[0].title"
|
|
14
|
+
yerba location videos.yml "[0].speakers"
|
|
15
|
+
"#})
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
#[derive(clap::Args)]
|
|
19
|
+
#[command(
|
|
20
|
+
about = "Show the location (line, column, offset) of a selector",
|
|
21
|
+
arg_required_else_help = true,
|
|
22
|
+
after_help = EXAMPLES.as_str()
|
|
23
|
+
)]
|
|
24
|
+
pub struct Args {
|
|
25
|
+
file: String,
|
|
26
|
+
selector: String,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl Args {
|
|
30
|
+
pub fn run(self) {
|
|
31
|
+
use super::color::*;
|
|
32
|
+
|
|
33
|
+
let document = parse_file(&self.file);
|
|
34
|
+
let info = document.get_node_info(&self.selector);
|
|
35
|
+
|
|
36
|
+
if info.location.start_line == 0 && info.location.end_line == 0 {
|
|
37
|
+
eprintln!("{RED}Error:{RESET} selector \"{}\" not found in {}", self.selector, self.file);
|
|
38
|
+
|
|
39
|
+
super::show_similar_selectors(&self.file, &document, &self.selector);
|
|
40
|
+
process::exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let json = serde_json::json!({
|
|
44
|
+
"selector": self.selector,
|
|
45
|
+
"file": self.file,
|
|
46
|
+
"start_line": info.location.start_line,
|
|
47
|
+
"start_column": info.location.start_column,
|
|
48
|
+
"end_line": info.location.end_line,
|
|
49
|
+
"end_column": info.location.end_column,
|
|
50
|
+
"start_offset": info.location.start_offset,
|
|
51
|
+
"end_offset": info.location.end_offset,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
println!("{}", serde_json::to_string_pretty(&json).unwrap_or_default());
|
|
55
|
+
}
|
|
56
|
+
}
|
data/rust/src/commands/mod.rs
CHANGED
|
@@ -6,16 +6,19 @@ pub mod directives;
|
|
|
6
6
|
pub mod get;
|
|
7
7
|
pub mod init;
|
|
8
8
|
pub mod insert;
|
|
9
|
+
pub mod location;
|
|
9
10
|
pub mod mate;
|
|
10
11
|
pub mod move_item;
|
|
11
12
|
pub mod move_key;
|
|
12
13
|
pub mod quote_style;
|
|
13
14
|
pub mod remove;
|
|
14
15
|
pub mod rename;
|
|
16
|
+
pub mod schema;
|
|
15
17
|
pub mod selectors;
|
|
16
18
|
pub mod set;
|
|
17
19
|
pub mod sort;
|
|
18
20
|
pub mod sort_keys;
|
|
21
|
+
pub mod unique;
|
|
19
22
|
pub mod version;
|
|
20
23
|
|
|
21
24
|
use std::fs;
|
|
@@ -23,6 +26,10 @@ use std::process;
|
|
|
23
26
|
|
|
24
27
|
use clap::Subcommand;
|
|
25
28
|
|
|
29
|
+
pub(crate) fn is_github_actions() -> bool {
|
|
30
|
+
std::env::var("GITHUB_ACTIONS").is_ok()
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
pub(crate) mod color {
|
|
27
34
|
pub const GREEN: &str = "\x1b[32m";
|
|
28
35
|
pub const RED: &str = "\x1b[31m";
|
|
@@ -153,6 +160,9 @@ pub enum Command {
|
|
|
153
160
|
QuoteStyle(quote_style::Args),
|
|
154
161
|
BlankLines(blank_lines::Args),
|
|
155
162
|
Directives(directives::Args),
|
|
163
|
+
Unique(unique::Args),
|
|
164
|
+
Location(location::Args),
|
|
165
|
+
Schema(schema::Args),
|
|
156
166
|
Selectors(selectors::Args),
|
|
157
167
|
#[command(about = "Create a new Yerbafile in the current directory")]
|
|
158
168
|
Init,
|
|
@@ -180,6 +190,9 @@ impl Command {
|
|
|
180
190
|
Command::QuoteStyle(args) => args.run(),
|
|
181
191
|
Command::BlankLines(args) => args.run(),
|
|
182
192
|
Command::Directives(args) => args.run(),
|
|
193
|
+
Command::Unique(args) => args.run(),
|
|
194
|
+
Command::Location(args) => args.run(),
|
|
195
|
+
Command::Schema(args) => args.run(),
|
|
183
196
|
Command::Selectors(args) => args.run(),
|
|
184
197
|
Command::Init => init::run(),
|
|
185
198
|
Command::Apply(args) => args.run(),
|
|
@@ -214,15 +227,30 @@ pub(crate) fn run_yerbafile(write: bool, files: Vec<String>) {
|
|
|
214
227
|
let mut has_changes = false;
|
|
215
228
|
let mut has_errors = false;
|
|
216
229
|
|
|
230
|
+
let github = is_github_actions();
|
|
231
|
+
|
|
217
232
|
for result in &results {
|
|
218
233
|
if let Some(error) = &result.error {
|
|
219
234
|
eprintln!(" {RED}error:{RESET} {} {DIM}—{RESET} {}", result.file, error);
|
|
235
|
+
|
|
236
|
+
if github {
|
|
237
|
+
use yerba::error::GitHubAnnotations;
|
|
238
|
+
|
|
239
|
+
for annotation in error.github_annotations(&result.file) {
|
|
240
|
+
eprintln!("{}", annotation);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
220
244
|
has_errors = true;
|
|
221
245
|
} else if result.changed {
|
|
222
246
|
if write {
|
|
223
247
|
eprintln!(" {GREEN}updated:{RESET} {}", result.file);
|
|
224
248
|
} else {
|
|
225
249
|
eprintln!(" {YELLOW}would change:{RESET} {}", result.file);
|
|
250
|
+
|
|
251
|
+
if github {
|
|
252
|
+
eprintln!("::error file={}::File does not match Yerbafile rules", result.file);
|
|
253
|
+
}
|
|
226
254
|
}
|
|
227
255
|
|
|
228
256
|
has_changes = true;
|
|
@@ -333,18 +361,18 @@ pub(crate) fn parse_file(file: &str) -> yerba::Document {
|
|
|
333
361
|
})
|
|
334
362
|
}
|
|
335
363
|
|
|
336
|
-
pub(crate) fn run_op(
|
|
337
|
-
run_op_with_hint(
|
|
364
|
+
pub(crate) fn run_op(display_file: &str, document: &yerba::Document, result: Result<(), yerba::YerbaError>) {
|
|
365
|
+
run_op_with_hint(display_file, document, result, None);
|
|
338
366
|
}
|
|
339
367
|
|
|
340
|
-
pub(crate) fn run_op_with_hint(
|
|
368
|
+
pub(crate) fn run_op_with_hint(display_file: &str, document: &yerba::Document, result: Result<(), yerba::YerbaError>, hint: Option<&str>) {
|
|
341
369
|
use color::*;
|
|
342
370
|
|
|
343
371
|
if let Err(error) = result {
|
|
344
372
|
if let yerba::YerbaError::SelectorNotFound(selector) = &error {
|
|
345
|
-
eprintln!("{RED}Error:{RESET} selector \"{selector}\" not found in {
|
|
373
|
+
eprintln!("{RED}Error:{RESET} selector \"{selector}\" not found in {display_file}");
|
|
346
374
|
|
|
347
|
-
show_similar_selectors(
|
|
375
|
+
show_similar_selectors(display_file, document, selector);
|
|
348
376
|
} else {
|
|
349
377
|
eprintln!("{RED}Error:{RESET} {}", error);
|
|
350
378
|
}
|
data/rust/src/commands/remove.rs
CHANGED
|
@@ -32,7 +32,7 @@ impl Args {
|
|
|
32
32
|
let mut document = parse_file(&resolved_file);
|
|
33
33
|
let result = document.remove(&self.selector, &self.value);
|
|
34
34
|
|
|
35
|
-
run_op(&
|
|
35
|
+
run_op(&self.file, &document, result);
|
|
36
36
|
output(&resolved_file, &document, self.dry_run);
|
|
37
37
|
}
|
|
38
38
|
}
|
data/rust/src/commands/rename.rs
CHANGED
|
@@ -34,7 +34,7 @@ impl Args {
|
|
|
34
34
|
let mut document = parse_file(&resolved_file);
|
|
35
35
|
let result = document.rename(&self.source, &self.destination);
|
|
36
36
|
|
|
37
|
-
run_op(&
|
|
37
|
+
run_op(&self.file, &document, result);
|
|
38
38
|
output(&resolved_file, &document, self.dry_run);
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
use std::process;
|
|
2
|
+
|
|
3
|
+
use super::color::*;
|
|
4
|
+
use super::resolve_files;
|
|
5
|
+
|
|
6
|
+
#[derive(clap::Args)]
|
|
7
|
+
#[command(
|
|
8
|
+
about = "Validate YAML files against a JSON schema",
|
|
9
|
+
after_help = colorize_examples(indoc::indoc! {r#"
|
|
10
|
+
yerba schema data/speakers.yml --schema lib/schemas/speaker_schema.json
|
|
11
|
+
yerba schema "data/**/videos.yml" --schema lib/schemas/video_schema.json
|
|
12
|
+
yerba schema data/config.yml --schema schema.json --selector "database"
|
|
13
|
+
"#})
|
|
14
|
+
)]
|
|
15
|
+
pub struct Args {
|
|
16
|
+
/// YAML file(s) to validate (supports globs)
|
|
17
|
+
file: String,
|
|
18
|
+
/// Path to the JSON schema file
|
|
19
|
+
#[arg(long)]
|
|
20
|
+
schema: String,
|
|
21
|
+
/// Selector to scope validation (e.g. "[]", "tiers[]")
|
|
22
|
+
#[arg(long)]
|
|
23
|
+
selector: Option<String>,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fn colorize_examples(text: &str) -> String {
|
|
27
|
+
super::colorize_examples(text)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
impl Args {
|
|
31
|
+
pub fn run(self) {
|
|
32
|
+
let schema = match yerba::schema::load_schema(&self.schema) {
|
|
33
|
+
Ok(schema) => schema,
|
|
34
|
+
Err(error) => {
|
|
35
|
+
eprintln!("{RED}Error:{RESET} {}", error);
|
|
36
|
+
process::exit(1);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
let files = resolve_files(&self.file);
|
|
41
|
+
|
|
42
|
+
if files.is_empty() {
|
|
43
|
+
eprintln!("{RED}Error:{RESET} no files matching: {}", self.file);
|
|
44
|
+
process::exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let mut has_errors = false;
|
|
48
|
+
let selector = self.selector.as_deref();
|
|
49
|
+
|
|
50
|
+
for file in &files {
|
|
51
|
+
let document = match yerba::Document::parse_file(file) {
|
|
52
|
+
Ok(document) => document,
|
|
53
|
+
Err(error) => {
|
|
54
|
+
eprintln!(" {RED}error:{RESET} {} {DIM}—{RESET} {}", file, error);
|
|
55
|
+
has_errors = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
let errors = document.validate_schema(&schema, false, selector);
|
|
61
|
+
|
|
62
|
+
if errors.is_empty() {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
has_errors = true;
|
|
67
|
+
|
|
68
|
+
let relative = file.strip_prefix("./").unwrap_or(file);
|
|
69
|
+
eprintln!(" {RED}error:{RESET} {relative}");
|
|
70
|
+
|
|
71
|
+
for error in &errors {
|
|
72
|
+
eprintln!(" {error}");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
eprintln!();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if has_errors {
|
|
79
|
+
process::exit(1);
|
|
80
|
+
} else {
|
|
81
|
+
eprintln!("{GREEN}All {count} files valid.{RESET}", count = files.len());
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
data/rust/src/commands/set.rs
CHANGED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
use std::process;
|
|
2
|
+
use std::sync::LazyLock;
|
|
3
|
+
|
|
4
|
+
use indoc::indoc;
|
|
5
|
+
|
|
6
|
+
use super::colorize_examples;
|
|
7
|
+
use super::{output, parse_file, resolve_files};
|
|
8
|
+
|
|
9
|
+
static EXAMPLES: LazyLock<String> = LazyLock::new(|| {
|
|
10
|
+
colorize_examples(indoc! {r#"
|
|
11
|
+
yerba unique config.yml "tags"
|
|
12
|
+
yerba unique videos.yml --by ".id"
|
|
13
|
+
yerba unique speakers.yml --by ".name"
|
|
14
|
+
yerba unique speakers.yml --by ".name" --remove
|
|
15
|
+
"#})
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
#[derive(clap::Args)]
|
|
19
|
+
#[command(
|
|
20
|
+
about = "Find or remove duplicate items in a sequence",
|
|
21
|
+
arg_required_else_help = true,
|
|
22
|
+
after_help = EXAMPLES.as_str()
|
|
23
|
+
)]
|
|
24
|
+
pub struct Args {
|
|
25
|
+
file: String,
|
|
26
|
+
/// Selector (optional — omit for root-level sequence)
|
|
27
|
+
selector: Option<String>,
|
|
28
|
+
/// Field to check for uniqueness (omit for scalar arrays)
|
|
29
|
+
#[arg(long)]
|
|
30
|
+
by: Option<String>,
|
|
31
|
+
/// Remove duplicate items (default: report only)
|
|
32
|
+
#[arg(long)]
|
|
33
|
+
remove: bool,
|
|
34
|
+
#[arg(long)]
|
|
35
|
+
dry_run: bool,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
impl Args {
|
|
39
|
+
pub fn run(self) {
|
|
40
|
+
use super::color::*;
|
|
41
|
+
|
|
42
|
+
let selector = self.selector.as_deref().unwrap_or("");
|
|
43
|
+
let by = self.by.as_deref().unwrap_or(".");
|
|
44
|
+
|
|
45
|
+
for resolved_file in resolve_files(&self.file) {
|
|
46
|
+
let mut document = parse_file(&resolved_file);
|
|
47
|
+
|
|
48
|
+
match document.unique(selector, by, self.remove) {
|
|
49
|
+
Ok(duplicates) => {
|
|
50
|
+
let noun = if duplicates.len() == 1 { "duplicate" } else { "duplicates" };
|
|
51
|
+
|
|
52
|
+
if duplicates.is_empty() {
|
|
53
|
+
eprintln!("{GREEN}No duplicates found{RESET} in {}", resolved_file);
|
|
54
|
+
} else if self.remove {
|
|
55
|
+
eprintln!("{YELLOW}Removed {} {noun}{RESET} from {}", duplicates.len(), resolved_file);
|
|
56
|
+
|
|
57
|
+
for duplicate in &duplicates {
|
|
58
|
+
eprintln!(" {DIM}line {}: {} == {}{RESET}", duplicate.line, by, duplicate.value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
output(&resolved_file, &document, self.dry_run);
|
|
62
|
+
} else {
|
|
63
|
+
eprintln!("{RED}Found {} {noun}{RESET} in {}", duplicates.len(), resolved_file);
|
|
64
|
+
|
|
65
|
+
for duplicate in &duplicates {
|
|
66
|
+
eprintln!(" {DIM}line {}: {} == {}{RESET}", duplicate.line, by, duplicate.value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
process::exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
Err(error) => {
|
|
74
|
+
eprintln!("{RED}Error:{RESET} {}", error);
|
|
75
|
+
process::exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -3,13 +3,29 @@ use super::*;
|
|
|
3
3
|
impl Document {
|
|
4
4
|
pub fn filter(&self, dot_path: &str, condition: &str) -> Vec<serde_yaml::Value> {
|
|
5
5
|
self
|
|
6
|
-
.
|
|
6
|
+
.navigate_all_compact(dot_path)
|
|
7
7
|
.iter()
|
|
8
8
|
.filter(|node| self.evaluate_condition_on_node(node, condition))
|
|
9
9
|
.map(node_to_yaml_value)
|
|
10
10
|
.collect()
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
pub fn filter_with_selectors(&self, dot_path: &str, condition: &str) -> Vec<(serde_yaml::Value, String, usize)> {
|
|
14
|
+
let source = self.root.text().to_string();
|
|
15
|
+
|
|
16
|
+
self
|
|
17
|
+
.navigate_all_compact(dot_path)
|
|
18
|
+
.iter()
|
|
19
|
+
.filter(|node| self.evaluate_condition_on_node(node, condition))
|
|
20
|
+
.map(|node| {
|
|
21
|
+
let offset: usize = node.text_range().start().into();
|
|
22
|
+
let line = source[..offset].matches('\n').count() + 1;
|
|
23
|
+
|
|
24
|
+
(node_to_yaml_value(node), super::get::node_selector(node), line)
|
|
25
|
+
})
|
|
26
|
+
.collect()
|
|
27
|
+
}
|
|
28
|
+
|
|
13
29
|
pub(super) fn evaluate_condition_on_node(&self, node: &SyntaxNode, condition: &str) -> bool {
|
|
14
30
|
let condition = condition.trim();
|
|
15
31
|
|