yerba 0.4.2 → 0.5.1
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/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 +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 +8 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
impl Document {
|
|
4
|
+
pub fn validate_schema(&self, schema: &serde_json::Value, items: bool, selector: Option<&str>) -> Vec<crate::schema::ValidationError> {
|
|
5
|
+
let path = selector.unwrap_or("");
|
|
6
|
+
let has_wildcard = crate::selector::Selector::parse(path).has_wildcard();
|
|
7
|
+
|
|
8
|
+
let value = match self.get_value(path) {
|
|
9
|
+
Some(value) => crate::json::yaml_to_json(&value),
|
|
10
|
+
None => return vec![],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let validate_items = items || has_wildcard;
|
|
14
|
+
|
|
15
|
+
let mut errors = if validate_items {
|
|
16
|
+
match value.as_array() {
|
|
17
|
+
Some(array) => crate::schema::validate_array(array, schema),
|
|
18
|
+
None if value.is_null() => vec![crate::schema::ValidationError {
|
|
19
|
+
path: String::new(),
|
|
20
|
+
message: "expected an array but document is empty or not a sequence".to_string(),
|
|
21
|
+
item_label: None,
|
|
22
|
+
line: None,
|
|
23
|
+
}],
|
|
24
|
+
None => crate::schema::validate_array(std::slice::from_ref(&value), schema),
|
|
25
|
+
}
|
|
26
|
+
} else {
|
|
27
|
+
if value.is_null() {
|
|
28
|
+
return vec![];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
crate::schema::validate_value(&value, schema)
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
let source = self.to_string();
|
|
35
|
+
|
|
36
|
+
for error in &mut errors {
|
|
37
|
+
if !error.path.is_empty() {
|
|
38
|
+
let selector = json_pointer_to_selector(&error.path);
|
|
39
|
+
|
|
40
|
+
if let Ok(node) = self.navigate(&selector) {
|
|
41
|
+
let offset: usize = node.text_range().start().into();
|
|
42
|
+
let line = source[..offset].chars().filter(|c| *c == '\n').count() + 1;
|
|
43
|
+
|
|
44
|
+
error.line = Some(line);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
errors
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn json_pointer_to_selector(pointer: &str) -> String {
|
|
54
|
+
pointer
|
|
55
|
+
.strip_prefix('/')
|
|
56
|
+
.unwrap_or(pointer)
|
|
57
|
+
.split('/')
|
|
58
|
+
.map(|segment| {
|
|
59
|
+
if let Ok(index) = segment.parse::<usize>() {
|
|
60
|
+
format!("[{}]", index)
|
|
61
|
+
} else {
|
|
62
|
+
segment.to_string()
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
.fold(String::new(), |mut path, segment| {
|
|
66
|
+
if !path.is_empty() && !segment.starts_with('[') {
|
|
67
|
+
path.push('.');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
path.push_str(&segment);
|
|
71
|
+
path
|
|
72
|
+
})
|
|
73
|
+
}
|
data/rust/src/document/set.rs
CHANGED
|
@@ -26,7 +26,7 @@ impl Document {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
pub fn set_all(&mut self, dot_path: &str, value: &str) -> Result<(), YerbaError> {
|
|
29
|
-
let nodes = self.
|
|
29
|
+
let nodes = self.navigate_all_compact(dot_path);
|
|
30
30
|
|
|
31
31
|
if nodes.is_empty() {
|
|
32
32
|
return Err(YerbaError::SelectorNotFound(dot_path.to_string()));
|
data/rust/src/document/sort.rs
CHANGED
|
@@ -113,12 +113,15 @@ impl Document {
|
|
|
113
113
|
return self.validate_each_sort_keys(sequence_path, key_order);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
let current_node = self.navigate(dot_path)
|
|
116
|
+
let current_node = match self.navigate(dot_path) {
|
|
117
|
+
Ok(node) => node,
|
|
118
|
+
Err(_) => return Ok(()),
|
|
119
|
+
};
|
|
117
120
|
|
|
118
|
-
let map = current_node
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
let map = match current_node.descendants().find_map(BlockMap::cast) {
|
|
122
|
+
Some(map) => map,
|
|
123
|
+
None => return Ok(()),
|
|
124
|
+
};
|
|
122
125
|
|
|
123
126
|
let unknown_keys: Vec<String> = map
|
|
124
127
|
.entries()
|
|
@@ -138,12 +141,15 @@ impl Document {
|
|
|
138
141
|
return self.sort_each_keys(sequence_path, key_order);
|
|
139
142
|
}
|
|
140
143
|
|
|
141
|
-
let current_node = self.navigate(dot_path)
|
|
144
|
+
let current_node = match self.navigate(dot_path) {
|
|
145
|
+
Ok(node) => node,
|
|
146
|
+
Err(_) => return Ok(()),
|
|
147
|
+
};
|
|
142
148
|
|
|
143
|
-
let map = current_node
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
149
|
+
let map = match current_node.descendants().find_map(BlockMap::cast) {
|
|
150
|
+
Some(map) => map,
|
|
151
|
+
None => return Ok(()),
|
|
152
|
+
};
|
|
147
153
|
|
|
148
154
|
let entries: Vec<_> = map.entries().collect();
|
|
149
155
|
|
|
@@ -202,7 +208,7 @@ impl Document {
|
|
|
202
208
|
Err(_) => return Ok(()),
|
|
203
209
|
}
|
|
204
210
|
} else {
|
|
205
|
-
let found = self.
|
|
211
|
+
let found = self.navigate_all_compact(dot_path);
|
|
206
212
|
if found.is_empty() {
|
|
207
213
|
return Ok(());
|
|
208
214
|
}
|
|
@@ -307,7 +313,7 @@ impl Document {
|
|
|
307
313
|
Err(_) => return Ok(()),
|
|
308
314
|
}
|
|
309
315
|
} else {
|
|
310
|
-
let found = self.
|
|
316
|
+
let found = self.navigate_all_compact(dot_path);
|
|
311
317
|
if found.is_empty() {
|
|
312
318
|
return Ok(());
|
|
313
319
|
}
|
|
@@ -438,7 +444,7 @@ impl Document {
|
|
|
438
444
|
(dot_path, "")
|
|
439
445
|
};
|
|
440
446
|
|
|
441
|
-
let parent_nodes = self.
|
|
447
|
+
let parent_nodes = self.navigate_all_compact(parent_path);
|
|
442
448
|
let source = self.root.text().to_string();
|
|
443
449
|
let mut edits: Vec<(TextRange, String)> = Vec::new();
|
|
444
450
|
|
|
@@ -571,10 +577,10 @@ impl Document {
|
|
|
571
577
|
};
|
|
572
578
|
|
|
573
579
|
let labels: Vec<String> = match self.get_value(&items_selector) {
|
|
574
|
-
Some(
|
|
580
|
+
Some(yaml_serde::Value::Sequence(sequence)) => sequence
|
|
575
581
|
.iter()
|
|
576
582
|
.map(|value| match value {
|
|
577
|
-
|
|
583
|
+
yaml_serde::Value::String(string) => string.clone(),
|
|
578
584
|
_ => String::new(),
|
|
579
585
|
})
|
|
580
586
|
.collect(),
|
data/rust/src/document/style.rs
CHANGED
|
@@ -3,7 +3,7 @@ use super::*;
|
|
|
3
3
|
impl Document {
|
|
4
4
|
pub fn enforce_blank_lines(&mut self, dot_path: &str, blank_lines: usize) -> Result<(), YerbaError> {
|
|
5
5
|
let nodes = if dot_path.contains('[') {
|
|
6
|
-
self.
|
|
6
|
+
self.navigate_all_compact(dot_path)
|
|
7
7
|
} else {
|
|
8
8
|
vec![self.navigate(dot_path)?]
|
|
9
9
|
};
|
|
@@ -105,7 +105,7 @@ impl Document {
|
|
|
105
105
|
let source = self.root.text().to_string();
|
|
106
106
|
|
|
107
107
|
let scope_ranges: Vec<TextRange> = match dot_path {
|
|
108
|
-
Some(path) if !path.is_empty() => self.
|
|
108
|
+
Some(path) if !path.is_empty() => self.navigate_all_compact(path).iter().map(|node| node.text_range()).collect(),
|
|
109
109
|
_ => vec![self.root.text_range()],
|
|
110
110
|
};
|
|
111
111
|
|
|
@@ -219,7 +219,7 @@ impl Document {
|
|
|
219
219
|
let source = self.root.text().to_string();
|
|
220
220
|
|
|
221
221
|
let scope_ranges: Vec<TextRange> = match dot_path {
|
|
222
|
-
Some(path) if !path.is_empty() => self.
|
|
222
|
+
Some(path) if !path.is_empty() => self.navigate_all_compact(path).iter().map(|node| node.text_range()).collect(),
|
|
223
223
|
_ => vec![self.root.text_range()],
|
|
224
224
|
};
|
|
225
225
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
use yaml_parser::ast::BlockSeq;
|
|
2
|
+
|
|
3
|
+
use rowan::ast::AstNode;
|
|
4
|
+
|
|
5
|
+
use crate::document::{extract_scalar_text, navigate_from_node, Document};
|
|
6
|
+
use crate::error::YerbaError;
|
|
7
|
+
|
|
8
|
+
#[derive(Debug, Clone)]
|
|
9
|
+
pub struct DuplicateInfo {
|
|
10
|
+
pub value: String,
|
|
11
|
+
pub line: usize,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
impl Document {
|
|
15
|
+
pub fn unique(&mut self, dot_path: &str, by: &str, remove: bool) -> Result<Vec<DuplicateInfo>, YerbaError> {
|
|
16
|
+
self.unique_with_options(dot_path, by, remove, false)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
pub fn unique_with_options(&mut self, dot_path: &str, by: &str, remove: bool, allow_blank_duplicates: bool) -> Result<Vec<DuplicateInfo>, YerbaError> {
|
|
20
|
+
let current_node = self.navigate(dot_path)?;
|
|
21
|
+
let source = self.root.text().to_string();
|
|
22
|
+
|
|
23
|
+
let sequence = match current_node.descendants().find_map(BlockSeq::cast) {
|
|
24
|
+
Some(sequence) => sequence,
|
|
25
|
+
None => return Ok(Vec::new()),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let entries: Vec<_> = sequence.entries().collect();
|
|
29
|
+
|
|
30
|
+
if entries.len() <= 1 {
|
|
31
|
+
return Ok(Vec::new());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let by_is_scalar = by == ".";
|
|
35
|
+
|
|
36
|
+
let labels: Vec<(Option<String>, usize)> = entries
|
|
37
|
+
.iter()
|
|
38
|
+
.map(|entry| {
|
|
39
|
+
let offset: usize = entry.syntax().text_range().start().into();
|
|
40
|
+
let line = source[..offset].matches('\n').count() + 1;
|
|
41
|
+
|
|
42
|
+
let value = if by_is_scalar {
|
|
43
|
+
Some(entry.flow().and_then(|flow| extract_scalar_text(flow.syntax())).unwrap_or_default())
|
|
44
|
+
} else {
|
|
45
|
+
let field = by.strip_prefix('.').unwrap_or(by);
|
|
46
|
+
let nodes = navigate_from_node(entry.syntax(), field);
|
|
47
|
+
|
|
48
|
+
nodes.first().and_then(extract_scalar_text)
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
(value, line)
|
|
52
|
+
})
|
|
53
|
+
.collect();
|
|
54
|
+
|
|
55
|
+
let mut seen = std::collections::HashSet::new();
|
|
56
|
+
let mut duplicate_indices: Vec<usize> = Vec::new();
|
|
57
|
+
let mut duplicates: Vec<DuplicateInfo> = Vec::new();
|
|
58
|
+
|
|
59
|
+
for (index, (label, line)) in labels.iter().enumerate() {
|
|
60
|
+
let label = match label {
|
|
61
|
+
None => continue,
|
|
62
|
+
Some(value) => value,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if allow_blank_duplicates && label.is_empty() {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if !seen.insert(label.clone()) {
|
|
70
|
+
duplicate_indices.push(index);
|
|
71
|
+
duplicates.push(DuplicateInfo {
|
|
72
|
+
value: label.clone(),
|
|
73
|
+
line: *line,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if remove && !duplicate_indices.is_empty() {
|
|
79
|
+
for &index in duplicate_indices.iter().rev() {
|
|
80
|
+
self.remove_at(dot_path, index)?;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
Ok(duplicates)
|
|
85
|
+
}
|
|
86
|
+
}
|
data/rust/src/error.rs
CHANGED
|
@@ -7,6 +7,8 @@ pub enum YerbaError {
|
|
|
7
7
|
NotASequence(String),
|
|
8
8
|
IndexOutOfBounds(usize, usize),
|
|
9
9
|
UnknownKeys(Vec<String>),
|
|
10
|
+
DuplicateValues(Vec<crate::DuplicateInfo>),
|
|
11
|
+
SchemaValidation(Vec<crate::schema::ValidationError>),
|
|
10
12
|
DuplicateKey {
|
|
11
13
|
key: String,
|
|
12
14
|
first_line: usize,
|
|
@@ -48,10 +50,26 @@ impl std::fmt::Display for YerbaError {
|
|
|
48
50
|
)
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
YerbaError::DuplicateValues(duplicates) => {
|
|
54
|
+
let noun = if duplicates.len() == 1 { "duplicate" } else { "duplicates" };
|
|
55
|
+
let details: Vec<String> = duplicates
|
|
56
|
+
.iter()
|
|
57
|
+
.map(|duplicate| format!("\"{}\" (line {})", duplicate.value, duplicate.line))
|
|
58
|
+
.collect();
|
|
59
|
+
|
|
60
|
+
write!(f, "found {} {}: {}", duplicates.len(), noun, details.join(", "))
|
|
61
|
+
}
|
|
62
|
+
|
|
51
63
|
YerbaError::IndexOutOfBounds(index, length) => {
|
|
52
64
|
write!(f, "index {} out of bounds (length {})", index, length)
|
|
53
65
|
}
|
|
54
66
|
|
|
67
|
+
YerbaError::SchemaValidation(errors) => {
|
|
68
|
+
let details: Vec<String> = errors.iter().map(|error| error.to_string()).collect();
|
|
69
|
+
|
|
70
|
+
write!(f, "schema validation failed:\n{}", details.join("\n"))
|
|
71
|
+
}
|
|
72
|
+
|
|
55
73
|
YerbaError::UnknownKeys(keys) => {
|
|
56
74
|
let suggestion = keys.iter().map(|key| format!("\"{}\"", key)).collect::<Vec<_>>().join(", ");
|
|
57
75
|
|
|
@@ -71,3 +89,63 @@ impl From<std::io::Error> for YerbaError {
|
|
|
71
89
|
YerbaError::IoError(err)
|
|
72
90
|
}
|
|
73
91
|
}
|
|
92
|
+
|
|
93
|
+
pub struct GitHubAnnotation {
|
|
94
|
+
pub level: &'static str,
|
|
95
|
+
pub file: String,
|
|
96
|
+
pub line: Option<usize>,
|
|
97
|
+
pub message: String,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
impl std::fmt::Display for GitHubAnnotation {
|
|
101
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
102
|
+
match self.line {
|
|
103
|
+
Some(line) => write!(f, "::{}file={},line={}::{}", self.level, self.file, line, self.message),
|
|
104
|
+
None => write!(f, "::{}file={}::{}", self.level, self.file, self.message),
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
pub trait GitHubAnnotations {
|
|
110
|
+
fn github_annotations(&self, file: &str) -> Vec<GitHubAnnotation>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
impl GitHubAnnotations for YerbaError {
|
|
114
|
+
fn github_annotations(&self, file: &str) -> Vec<GitHubAnnotation> {
|
|
115
|
+
match self {
|
|
116
|
+
YerbaError::DuplicateValues(duplicates) => duplicates
|
|
117
|
+
.iter()
|
|
118
|
+
.map(|duplicate| GitHubAnnotation {
|
|
119
|
+
level: "error ",
|
|
120
|
+
file: file.to_string(),
|
|
121
|
+
line: Some(duplicate.line),
|
|
122
|
+
message: format!("duplicate: \"{}\"", duplicate.value),
|
|
123
|
+
})
|
|
124
|
+
.collect(),
|
|
125
|
+
|
|
126
|
+
YerbaError::SchemaValidation(errors) => errors
|
|
127
|
+
.iter()
|
|
128
|
+
.map(|error| GitHubAnnotation {
|
|
129
|
+
level: "error ",
|
|
130
|
+
file: file.to_string(),
|
|
131
|
+
line: error.line,
|
|
132
|
+
message: error.to_string(),
|
|
133
|
+
})
|
|
134
|
+
.collect(),
|
|
135
|
+
|
|
136
|
+
YerbaError::DuplicateKey { key, duplicate_line, .. } => vec![GitHubAnnotation {
|
|
137
|
+
level: "error ",
|
|
138
|
+
file: file.to_string(),
|
|
139
|
+
line: Some(*duplicate_line),
|
|
140
|
+
message: format!("duplicate key: \"{}\"", key),
|
|
141
|
+
}],
|
|
142
|
+
|
|
143
|
+
_ => vec![GitHubAnnotation {
|
|
144
|
+
level: "error ",
|
|
145
|
+
file: file.to_string(),
|
|
146
|
+
line: None,
|
|
147
|
+
message: self.to_string(),
|
|
148
|
+
}],
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
data/rust/src/ffi.rs
CHANGED
|
@@ -298,13 +298,22 @@ pub unsafe extern "C" fn yerba_document_get_value(document: *const Document, pat
|
|
|
298
298
|
|
|
299
299
|
/// Caller must free with yerba_string_free.
|
|
300
300
|
#[no_mangle]
|
|
301
|
-
pub unsafe extern "C" fn
|
|
301
|
+
pub unsafe extern "C" fn yerba_document_selectors(document: *const Document) -> *mut c_char {
|
|
302
|
+
let document = &*document;
|
|
303
|
+
|
|
304
|
+
let selectors = document.selectors();
|
|
305
|
+
let json_string = serde_json::to_string(&selectors).unwrap_or_else(|_| "[]".to_string());
|
|
306
|
+
|
|
307
|
+
CString::new(json_string).unwrap_or_default().into_raw()
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
#[no_mangle]
|
|
311
|
+
pub unsafe extern "C" fn yerba_document_resolve_selectors(document: *const Document, path: *const c_char) -> *mut c_char {
|
|
302
312
|
let document = &*document;
|
|
303
313
|
let selector_string = CStr::from_ptr(path).to_str().unwrap_or("");
|
|
304
314
|
|
|
305
|
-
let
|
|
306
|
-
let
|
|
307
|
-
let json_string = serde_json::to_string(&json_values).unwrap_or_else(|_| "[]".to_string());
|
|
315
|
+
let selectors = document.resolve_selectors(selector_string);
|
|
316
|
+
let json_string = serde_json::to_string(&selectors).unwrap_or_else(|_| "[]".to_string());
|
|
308
317
|
|
|
309
318
|
CString::new(json_string).unwrap_or_default().into_raw()
|
|
310
319
|
}
|
|
@@ -357,6 +366,13 @@ pub unsafe extern "C" fn yerba_document_exists(document: *const Document, path:
|
|
|
357
366
|
document.exists(selector_string)
|
|
358
367
|
}
|
|
359
368
|
|
|
369
|
+
#[no_mangle]
|
|
370
|
+
pub unsafe extern "C" fn yerba_document_valid_selector(document: *const Document, path: *const c_char) -> bool {
|
|
371
|
+
let document = &*document;
|
|
372
|
+
let selector_string = CStr::from_ptr(path).to_str().unwrap_or("");
|
|
373
|
+
document.is_valid_selector(selector_string)
|
|
374
|
+
}
|
|
375
|
+
|
|
360
376
|
#[no_mangle]
|
|
361
377
|
pub unsafe extern "C" fn yerba_document_find(document: *const Document, path: *const c_char, condition: *const c_char, select: *const c_char) -> *mut c_char {
|
|
362
378
|
let document = &*document;
|
|
@@ -404,13 +420,19 @@ pub unsafe extern "C" fn yerba_document_insert(
|
|
|
404
420
|
document: *mut Document,
|
|
405
421
|
path: *const c_char,
|
|
406
422
|
value: *const c_char,
|
|
423
|
+
value_type: YerbaValueType,
|
|
407
424
|
before: *const c_char,
|
|
408
425
|
after: *const c_char,
|
|
409
426
|
at: i64,
|
|
410
427
|
) -> YerbaResult {
|
|
411
428
|
let document = &mut *document;
|
|
412
429
|
let selector_string = CStr::from_ptr(path).to_str().unwrap_or("");
|
|
413
|
-
let
|
|
430
|
+
let raw_value = CStr::from_ptr(value).to_str().unwrap_or("");
|
|
431
|
+
|
|
432
|
+
let value_string = match value_type {
|
|
433
|
+
YerbaValueType::String => crate::syntax::quote_if_needed(raw_value),
|
|
434
|
+
_ => raw_value.to_string(),
|
|
435
|
+
};
|
|
414
436
|
|
|
415
437
|
let position = if at >= 0 {
|
|
416
438
|
InsertPosition::At(at as usize)
|
|
@@ -424,7 +446,7 @@ pub unsafe extern "C" fn yerba_document_insert(
|
|
|
424
446
|
InsertPosition::Last
|
|
425
447
|
};
|
|
426
448
|
|
|
427
|
-
match document.insert_into(selector_string, value_string, position) {
|
|
449
|
+
match document.insert_into(selector_string, &value_string, position) {
|
|
428
450
|
Ok(()) => YerbaResult::ok(),
|
|
429
451
|
Err(e) => YerbaResult::err(&e.to_string()),
|
|
430
452
|
}
|
|
@@ -626,6 +648,44 @@ pub unsafe extern "C" fn yerba_document_blank_lines(document: *mut Document, pat
|
|
|
626
648
|
}
|
|
627
649
|
}
|
|
628
650
|
|
|
651
|
+
/// Caller must free with yerba_string_free.
|
|
652
|
+
#[no_mangle]
|
|
653
|
+
pub unsafe extern "C" fn yerba_document_validate_schema(document: *const Document, schema_json: *const c_char, selector: *const c_char) -> *mut c_char {
|
|
654
|
+
let document = &*document;
|
|
655
|
+
let schema_string = CStr::from_ptr(schema_json).to_str().unwrap_or("");
|
|
656
|
+
let selector_string = if selector.is_null() { None } else { CStr::from_ptr(selector).to_str().ok() };
|
|
657
|
+
|
|
658
|
+
let schema: serde_json::Value = match serde_json::from_str(schema_string) {
|
|
659
|
+
Ok(schema) => schema,
|
|
660
|
+
Err(e) => {
|
|
661
|
+
let error = serde_json::json!([{"message": format!("invalid schema: {}", e), "path": "", "line": null}]);
|
|
662
|
+
return CString::new(error.to_string()).unwrap_or_default().into_raw();
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
let errors = document.validate_schema(&schema, false, selector_string);
|
|
667
|
+
|
|
668
|
+
if errors.is_empty() {
|
|
669
|
+
return ptr::null_mut();
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
let json_errors: Vec<serde_json::Value> = errors
|
|
673
|
+
.iter()
|
|
674
|
+
.map(|error| {
|
|
675
|
+
serde_json::json!({
|
|
676
|
+
"message": error.message,
|
|
677
|
+
"path": error.path,
|
|
678
|
+
"line": error.line,
|
|
679
|
+
"item_label": error.item_label,
|
|
680
|
+
})
|
|
681
|
+
})
|
|
682
|
+
.collect();
|
|
683
|
+
|
|
684
|
+
CString::new(serde_json::to_string(&json_errors).unwrap_or_else(|_| "[]".to_string()))
|
|
685
|
+
.unwrap_or_default()
|
|
686
|
+
.into_raw()
|
|
687
|
+
}
|
|
688
|
+
|
|
629
689
|
/// Caller must free with yerba_string_free.
|
|
630
690
|
#[no_mangle]
|
|
631
691
|
pub unsafe extern "C" fn yerba_yerbafile_find(directory: *const c_char) -> *mut c_char {
|
|
@@ -716,11 +776,31 @@ pub unsafe extern "C" fn yerba_get_result_free(result: YerbaGetResult) {
|
|
|
716
776
|
pub unsafe extern "C" fn yerba_glob_get(glob_pattern: *const c_char, path: *const c_char) -> YerbaTypedList {
|
|
717
777
|
let pattern = CStr::from_ptr(glob_pattern).to_str().unwrap_or("");
|
|
718
778
|
let selector_string = CStr::from_ptr(path).to_str().unwrap_or("");
|
|
719
|
-
let
|
|
779
|
+
let nodes = crate::glob_get(pattern, selector_string);
|
|
720
780
|
|
|
721
|
-
let results: Vec<serde_json::Value> =
|
|
781
|
+
let results: Vec<serde_json::Value> = nodes
|
|
722
782
|
.iter()
|
|
723
|
-
.map(|
|
|
783
|
+
.map(|node| {
|
|
784
|
+
let mut value = serde_json::json!({
|
|
785
|
+
"node_type": node.node_type,
|
|
786
|
+
"selector": node.selector,
|
|
787
|
+
"line": node.line,
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
if let Some(file_path) = &node.file_path {
|
|
791
|
+
value["file_path"] = serde_json::json!(file_path);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if let Some(text) = &node.text {
|
|
795
|
+
value["text"] = serde_json::json!(text);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if let Some(vt) = node.value_type {
|
|
799
|
+
value["type"] = serde_json::json!(vt as u8);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
value
|
|
803
|
+
})
|
|
724
804
|
.collect();
|
|
725
805
|
|
|
726
806
|
let length = results.len();
|
data/rust/src/json.rs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
use crate::selector::{Selector, SelectorSegment};
|
|
2
2
|
|
|
3
|
-
pub fn yaml_to_json(value: &
|
|
3
|
+
pub fn yaml_to_json(value: &yaml_serde::Value) -> serde_json::Value {
|
|
4
4
|
match value {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
yaml_serde::Value::Null => serde_json::Value::Null,
|
|
6
|
+
yaml_serde::Value::Bool(boolean) => serde_json::Value::Bool(*boolean),
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
yaml_serde::Value::Number(number) => {
|
|
9
9
|
if let Some(integer) = number.as_i64() {
|
|
10
10
|
serde_json::Value::Number(integer.into())
|
|
11
11
|
} else if let Some(float) = number.as_f64() {
|
|
@@ -15,16 +15,16 @@ pub fn yaml_to_json(value: &serde_yaml::Value) -> serde_json::Value {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
yaml_serde::Value::String(string) => serde_json::Value::String(string.clone()),
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
yaml_serde::Value::Sequence(sequence) => serde_json::Value::Array(sequence.iter().map(yaml_to_json).collect()),
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
yaml_serde::Value::Mapping(mapping) => {
|
|
23
23
|
let mut map = serde_json::Map::new();
|
|
24
24
|
|
|
25
25
|
for (key, yaml_value) in mapping {
|
|
26
26
|
let json_key = match key {
|
|
27
|
-
|
|
27
|
+
yaml_serde::Value::String(string) => string.clone(),
|
|
28
28
|
_ => format!("{:?}", key),
|
|
29
29
|
};
|
|
30
30
|
|
|
@@ -34,19 +34,19 @@ pub fn yaml_to_json(value: &serde_yaml::Value) -> serde_json::Value {
|
|
|
34
34
|
serde_json::Value::Object(map)
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
yaml_serde::Value::Tagged(tagged) => yaml_to_json(&tagged.value),
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
pub fn resolve_select_field(value: &
|
|
41
|
+
pub fn resolve_select_field(value: &yaml_serde::Value, field: &str) -> serde_json::Value {
|
|
42
42
|
let parsed = Selector::parse(field);
|
|
43
43
|
let segments = parsed.segments();
|
|
44
44
|
|
|
45
45
|
if segments.len() == 1 {
|
|
46
46
|
if let SelectorSegment::Key(key) = &segments[0] {
|
|
47
|
-
if let
|
|
47
|
+
if let yaml_serde::Value::Mapping(map) = value {
|
|
48
48
|
for (map_key, yaml_value) in map {
|
|
49
|
-
if let
|
|
49
|
+
if let yaml_serde::Value::String(key_string) = map_key {
|
|
50
50
|
if key_string == key {
|
|
51
51
|
return yaml_to_json(yaml_value);
|
|
52
52
|
}
|
|
@@ -66,13 +66,13 @@ pub fn resolve_select_field(value: &serde_yaml::Value, field: &str) -> serde_jso
|
|
|
66
66
|
for current in ¤t_values {
|
|
67
67
|
match segment {
|
|
68
68
|
SelectorSegment::AllItems => {
|
|
69
|
-
if let
|
|
69
|
+
if let yaml_serde::Value::Sequence(sequence) = current {
|
|
70
70
|
next_values.extend(sequence.iter().cloned());
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
SelectorSegment::Index(index) => {
|
|
75
|
-
if let
|
|
75
|
+
if let yaml_serde::Value::Sequence(sequence) = current {
|
|
76
76
|
if let Some(item) = sequence.get(*index) {
|
|
77
77
|
next_values.push(item.clone());
|
|
78
78
|
}
|
|
@@ -80,9 +80,9 @@ pub fn resolve_select_field(value: &serde_yaml::Value, field: &str) -> serde_jso
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
SelectorSegment::Key(key) => {
|
|
83
|
-
if let
|
|
83
|
+
if let yaml_serde::Value::Mapping(map) = current {
|
|
84
84
|
for (map_key, yaml_value) in map {
|
|
85
|
-
if let
|
|
85
|
+
if let yaml_serde::Value::String(key_string) = map_key {
|
|
86
86
|
if key_string == key {
|
|
87
87
|
next_values.push(yaml_value.clone());
|
|
88
88
|
}
|
data/rust/src/lib.rs
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
pub mod didyoumean;
|
|
2
2
|
mod document;
|
|
3
|
-
mod error;
|
|
3
|
+
pub mod error;
|
|
4
4
|
pub mod ffi;
|
|
5
5
|
pub mod json;
|
|
6
6
|
mod quote_style;
|
|
7
|
+
pub mod schema;
|
|
7
8
|
pub mod selector;
|
|
8
9
|
mod syntax;
|
|
9
10
|
mod yaml_writer;
|
|
10
11
|
pub mod yerbafile;
|
|
11
12
|
|
|
12
|
-
pub use document::{collect_selectors, Document, InsertPosition, Location, NodeInfo, NodeType, SortField};
|
|
13
|
+
pub use document::{collect_selectors, Document, DuplicateInfo, InsertPosition, LocatedNode, Location, NodeInfo, NodeType, SortField};
|
|
13
14
|
pub use error::YerbaError;
|
|
14
15
|
pub use quote_style::{KeyStyle, QuoteStyle};
|
|
15
16
|
pub use selector::Selector;
|
|
@@ -29,11 +30,9 @@ pub fn parse_file(path: impl AsRef<std::path::Path>) -> Result<Document, YerbaEr
|
|
|
29
30
|
Document::parse_file(path)
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
pub fn glob_get(pattern: &str, selector: &str) -> Vec<
|
|
33
|
+
pub fn glob_get(pattern: &str, selector: &str) -> Vec<document::LocatedNode> {
|
|
33
34
|
use rayon::prelude::*;
|
|
34
35
|
|
|
35
|
-
let parsed_selector = Selector::parse(selector);
|
|
36
|
-
|
|
37
36
|
let files = match glob::glob(pattern) {
|
|
38
37
|
Ok(paths) => paths.filter_map(|p| p.ok()).collect::<Vec<_>>(),
|
|
39
38
|
Err(_) => return vec![],
|
|
@@ -45,11 +44,7 @@ pub fn glob_get(pattern: &str, selector: &str) -> Vec<ScalarValue> {
|
|
|
45
44
|
let mut results = Vec::new();
|
|
46
45
|
|
|
47
46
|
if let Ok(document) = Document::parse_file(file) {
|
|
48
|
-
|
|
49
|
-
results.extend(document.get_all_typed(selector));
|
|
50
|
-
} else if let Some(scalar) = document.get_typed(selector) {
|
|
51
|
-
results.push(scalar);
|
|
52
|
-
}
|
|
47
|
+
results.extend(document.get_all_located(selector));
|
|
53
48
|
}
|
|
54
49
|
|
|
55
50
|
results
|
|
@@ -93,10 +88,10 @@ pub fn glob_find(pattern: &str, selector: &str, condition: Option<&str>, select:
|
|
|
93
88
|
}
|
|
94
89
|
}
|
|
95
90
|
None => {
|
|
96
|
-
if let
|
|
91
|
+
if let yaml_serde::Value::Mapping(map) = value {
|
|
97
92
|
for (key, yaml_value) in map {
|
|
98
93
|
let json_key = match key {
|
|
99
|
-
|
|
94
|
+
yaml_serde::Value::String(string) => string.clone(),
|
|
100
95
|
_ => format!("{:?}", key),
|
|
101
96
|
};
|
|
102
97
|
|
data/rust/src/main.rs
CHANGED
|
@@ -48,7 +48,9 @@ static HELP: LazyLock<String> = LazyLock::new(|| {
|
|
|
48
48
|
yerba sort-keys config.yml "database" "id,host,port,name"
|
|
49
49
|
yerba quote-style "data/**/*.yml" --values double
|
|
50
50
|
yerba sort videos.yml "[]" --by ".id" --order "talk-3,talk-1,talk-2"
|
|
51
|
+
yerba schema "data/**/*.yml" --schema schema.json
|
|
51
52
|
yerba selectors videos.yml
|
|
53
|
+
yerba location videos.yml "[0].title"
|
|
52
54
|
"#})
|
|
53
55
|
});
|
|
54
56
|
|