yerba 0.4.2-x86_64-linux-gnu → 0.5.0-x86_64-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 +241 -53
- data/exe/x86_64-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 +1110 -25
- 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 +9 -2
|
@@ -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/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
|
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
|
|
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/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
|
|