yerba 0.7.6 → 0.8.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 +66 -2
- data/ext/yerba/include/yerba.h +8 -0
- data/ext/yerba/yerba.c +70 -8
- data/lib/yerba/map.rb +15 -10
- data/lib/yerba/node.rb +2 -2
- data/lib/yerba/sequence.rb +13 -1
- data/lib/yerba/version.rb +1 -1
- data/rust/Cargo.toml +15 -6
- data/rust/src/commands/blank_lines.rs +2 -2
- data/rust/src/commands/directives.rs +4 -4
- data/rust/src/commands/get.rs +17 -10
- data/rust/src/commands/init.rs +4 -4
- data/rust/src/commands/insert.rs +6 -6
- data/rust/src/commands/location.rs +2 -2
- data/rust/src/commands/mate.rs +7 -7
- data/rust/src/commands/mod.rs +119 -90
- data/rust/src/commands/move_key.rs +9 -6
- data/rust/src/commands/quote_style.rs +4 -5
- data/rust/src/commands/rename.rs +3 -3
- data/rust/src/commands/schema.rs +6 -6
- data/rust/src/commands/selectors.rs +2 -2
- data/rust/src/commands/set.rs +12 -1
- data/rust/src/commands/sort.rs +36 -42
- data/rust/src/commands/sort_keys.rs +2 -2
- data/rust/src/commands/ui.rs +157 -0
- data/rust/src/commands/unique.rs +7 -7
- data/rust/src/commands/version.rs +13 -3
- data/rust/src/document/condition.rs +89 -32
- data/rust/src/document/delete.rs +6 -1
- data/rust/src/document/get.rs +94 -26
- data/rust/src/document/insert.rs +26 -0
- data/rust/src/document/mod.rs +73 -20
- data/rust/src/document/set.rs +91 -2
- data/rust/src/error.rs +17 -0
- data/rust/src/ffi.rs +76 -9
- data/rust/src/json.rs +10 -1
- data/rust/src/lib.rs +23 -7
- data/rust/src/main.rs +1 -0
- data/rust/src/quote_style.rs +12 -11
- data/rust/src/selector.rs +19 -3
- data/rust/src/syntax.rs +178 -26
- data/rust/src/yaml_writer.rs +4 -4
- data/rust/src/yerbafile.rs +30 -6
- metadata +2 -1
data/rust/src/ffi.rs
CHANGED
|
@@ -330,6 +330,24 @@ pub unsafe extern "C" fn yerba_document_resolve_selectors(document: *const Docum
|
|
|
330
330
|
CString::new(json_string).unwrap_or_default().into_raw()
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
+
#[no_mangle]
|
|
334
|
+
pub unsafe extern "C" fn yerba_selector_has_wildcard(path: *const c_char) -> bool {
|
|
335
|
+
let selector_string = CStr::from_ptr(path).to_str().unwrap_or("");
|
|
336
|
+
|
|
337
|
+
crate::selector::Selector::parse(selector_string).has_wildcard()
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
#[no_mangle]
|
|
341
|
+
pub unsafe extern "C" fn yerba_document_keys(document: *const Document, path: *const c_char) -> *mut c_char {
|
|
342
|
+
let document = &*document;
|
|
343
|
+
let selector_string = CStr::from_ptr(path).to_str().unwrap_or("");
|
|
344
|
+
|
|
345
|
+
let keys = document.keys_at(selector_string);
|
|
346
|
+
let json_string = serde_json::to_string(&keys).unwrap_or_else(|_| "[]".to_string());
|
|
347
|
+
|
|
348
|
+
CString::new(json_string).unwrap_or_default().into_raw()
|
|
349
|
+
}
|
|
350
|
+
|
|
333
351
|
#[no_mangle]
|
|
334
352
|
pub unsafe extern "C" fn yerba_document_get_quote_style(document: *const Document, path: *const c_char) -> *mut c_char {
|
|
335
353
|
let document = &*document;
|
|
@@ -409,12 +427,33 @@ pub unsafe extern "C" fn yerba_document_set_sequence_indent(document: *mut Docum
|
|
|
409
427
|
}
|
|
410
428
|
}
|
|
411
429
|
|
|
430
|
+
#[no_mangle]
|
|
431
|
+
pub unsafe extern "C" fn yerba_validate_condition(condition: *const c_char, item_context: bool) -> *mut c_char {
|
|
432
|
+
if condition.is_null() {
|
|
433
|
+
return ptr::null_mut();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
let cond = CStr::from_ptr(condition).to_str().unwrap_or("");
|
|
437
|
+
|
|
438
|
+
let result = if item_context {
|
|
439
|
+
crate::validate_item_condition(cond)
|
|
440
|
+
} else {
|
|
441
|
+
crate::validate_condition(cond)
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
match result {
|
|
445
|
+
Ok(()) => ptr::null_mut(),
|
|
446
|
+
Err(error) => CString::new(error.to_string()).unwrap_or_default().into_raw(),
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
412
450
|
#[no_mangle]
|
|
413
451
|
pub unsafe extern "C" fn yerba_document_evaluate_condition(document: *const Document, parent_path: *const c_char, condition: *const c_char) -> bool {
|
|
414
452
|
let document = &*document;
|
|
415
453
|
let parent = CStr::from_ptr(parent_path).to_str().unwrap_or("");
|
|
416
454
|
let cond = CStr::from_ptr(condition).to_str().unwrap_or("");
|
|
417
|
-
|
|
455
|
+
|
|
456
|
+
document.evaluate_condition(parent, cond).unwrap_or(false)
|
|
418
457
|
}
|
|
419
458
|
|
|
420
459
|
#[no_mangle]
|
|
@@ -440,7 +479,7 @@ pub unsafe extern "C" fn yerba_document_find(document: *const Document, path: *c
|
|
|
440
479
|
|
|
441
480
|
let select_string = if select.is_null() { None } else { CStr::from_ptr(select).to_str().ok() };
|
|
442
481
|
|
|
443
|
-
let results = document.find_items(selector_string, condition_string, select_string);
|
|
482
|
+
let results = document.find_items(selector_string, condition_string, select_string).unwrap_or_default();
|
|
444
483
|
let json = serde_json::to_string_pretty(&results).unwrap_or_else(|_| "[]".to_string());
|
|
445
484
|
|
|
446
485
|
CString::new(json).unwrap_or_default().into_raw()
|
|
@@ -707,6 +746,7 @@ pub unsafe extern "C" fn yerba_document_blank_lines(document: *mut Document, pat
|
|
|
707
746
|
}
|
|
708
747
|
|
|
709
748
|
/// Caller must free with yerba_string_free.
|
|
749
|
+
#[cfg(feature = "schema")]
|
|
710
750
|
#[no_mangle]
|
|
711
751
|
pub unsafe extern "C" fn yerba_document_validate_schema(document: *const Document, schema_json: *const c_char, selector: *const c_char) -> *mut c_char {
|
|
712
752
|
let document = &*document;
|
|
@@ -745,6 +785,7 @@ pub unsafe extern "C" fn yerba_document_validate_schema(document: *const Documen
|
|
|
745
785
|
}
|
|
746
786
|
|
|
747
787
|
/// Caller must free with yerba_string_free.
|
|
788
|
+
#[cfg(feature = "cli")]
|
|
748
789
|
#[no_mangle]
|
|
749
790
|
pub unsafe extern "C" fn yerba_yerbafile_find(directory: *const c_char) -> *mut c_char {
|
|
750
791
|
let start = if directory.is_null() {
|
|
@@ -764,6 +805,7 @@ pub unsafe extern "C" fn yerba_yerbafile_find(directory: *const c_char) -> *mut
|
|
|
764
805
|
}
|
|
765
806
|
}
|
|
766
807
|
|
|
808
|
+
#[cfg(feature = "cli")]
|
|
767
809
|
#[no_mangle]
|
|
768
810
|
pub unsafe extern "C" fn yerba_document_apply_yerbafile(document: *mut Document, file_path: *const c_char, yerbafile_path: *const c_char) -> YerbaResult {
|
|
769
811
|
let document = &mut *document;
|
|
@@ -830,12 +872,7 @@ pub unsafe extern "C" fn yerba_get_result_free(result: YerbaGetResult) {
|
|
|
830
872
|
}
|
|
831
873
|
}
|
|
832
874
|
|
|
833
|
-
|
|
834
|
-
pub unsafe extern "C" fn yerba_glob_get(glob_pattern: *const c_char, path: *const c_char) -> YerbaTypedList {
|
|
835
|
-
let pattern = CStr::from_ptr(glob_pattern).to_str().unwrap_or("");
|
|
836
|
-
let selector_string = CStr::from_ptr(path).to_str().unwrap_or("");
|
|
837
|
-
let nodes = crate::glob_get(pattern, selector_string);
|
|
838
|
-
|
|
875
|
+
fn located_nodes_to_list(nodes: &[crate::LocatedNode]) -> YerbaTypedList {
|
|
839
876
|
let results: Vec<serde_json::Value> = nodes
|
|
840
877
|
.iter()
|
|
841
878
|
.map(|node| {
|
|
@@ -865,6 +902,18 @@ pub unsafe extern "C" fn yerba_glob_get(glob_pattern: *const c_char, path: *cons
|
|
|
865
902
|
value["type"] = serde_json::json!(vt as u8);
|
|
866
903
|
}
|
|
867
904
|
|
|
905
|
+
if let Some(key_name) = &node.key_name {
|
|
906
|
+
value["key_name"] = serde_json::json!(key_name);
|
|
907
|
+
value["key_location"] = serde_json::json!({
|
|
908
|
+
"start_line": node.key_location.start_line,
|
|
909
|
+
"start_column": node.key_location.start_column,
|
|
910
|
+
"end_line": node.key_location.end_line,
|
|
911
|
+
"end_column": node.key_location.end_column,
|
|
912
|
+
"start_offset": node.key_location.start_offset,
|
|
913
|
+
"end_offset": node.key_location.end_offset,
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
|
|
868
917
|
value
|
|
869
918
|
})
|
|
870
919
|
.collect();
|
|
@@ -878,6 +927,24 @@ pub unsafe extern "C" fn yerba_glob_get(glob_pattern: *const c_char, path: *cons
|
|
|
878
927
|
}
|
|
879
928
|
}
|
|
880
929
|
|
|
930
|
+
#[cfg(feature = "glob")]
|
|
931
|
+
#[no_mangle]
|
|
932
|
+
pub unsafe extern "C" fn yerba_glob_get(glob_pattern: *const c_char, path: *const c_char) -> YerbaTypedList {
|
|
933
|
+
let pattern = CStr::from_ptr(glob_pattern).to_str().unwrap_or("");
|
|
934
|
+
let selector_string = CStr::from_ptr(path).to_str().unwrap_or("");
|
|
935
|
+
|
|
936
|
+
located_nodes_to_list(&crate::glob_get(pattern, selector_string))
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
#[no_mangle]
|
|
940
|
+
pub unsafe extern "C" fn yerba_document_get_all(document: *const Document, path: *const c_char) -> YerbaTypedList {
|
|
941
|
+
let document = &*document;
|
|
942
|
+
let selector_string = CStr::from_ptr(path).to_str().unwrap_or("");
|
|
943
|
+
|
|
944
|
+
located_nodes_to_list(&document.get_all_located(selector_string))
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
#[cfg(feature = "glob")]
|
|
881
948
|
#[no_mangle]
|
|
882
949
|
pub unsafe extern "C" fn yerba_glob_find(glob_pattern: *const c_char, path: *const c_char, condition: *const c_char, select: *const c_char) -> YerbaTypedList {
|
|
883
950
|
let pattern = CStr::from_ptr(glob_pattern).to_str().unwrap_or("");
|
|
@@ -885,7 +952,7 @@ pub unsafe extern "C" fn yerba_glob_find(glob_pattern: *const c_char, path: *con
|
|
|
885
952
|
let condition_string = if condition.is_null() { None } else { CStr::from_ptr(condition).to_str().ok() };
|
|
886
953
|
let select_string = if select.is_null() { None } else { CStr::from_ptr(select).to_str().ok() };
|
|
887
954
|
|
|
888
|
-
let all_results = crate::glob_find(pattern, selector_string, condition_string, select_string);
|
|
955
|
+
let all_results = crate::glob_find(pattern, selector_string, condition_string, select_string).unwrap_or_default();
|
|
889
956
|
let length = all_results.len();
|
|
890
957
|
let json = serde_json::to_string_pretty(&all_results).unwrap_or_else(|_| "[]".to_string());
|
|
891
958
|
|
data/rust/src/json.rs
CHANGED
|
@@ -71,6 +71,12 @@ pub fn resolve_select_field(value: &yaml_serde::Value, field: &str) -> serde_jso
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
SelectorSegment::AllKeys => {
|
|
75
|
+
if let yaml_serde::Value::Mapping(mapping) = current {
|
|
76
|
+
next_values.extend(mapping.iter().map(|(_, entry)| entry.clone()));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
74
80
|
SelectorSegment::Index(index) => {
|
|
75
81
|
if let yaml_serde::Value::Sequence(sequence) = current {
|
|
76
82
|
if let Some(item) = sequence.get(*index) {
|
|
@@ -96,7 +102,10 @@ pub fn resolve_select_field(value: &yaml_serde::Value, field: &str) -> serde_jso
|
|
|
96
102
|
current_values = next_values;
|
|
97
103
|
}
|
|
98
104
|
|
|
99
|
-
let used_all_items = parsed
|
|
105
|
+
let used_all_items = parsed
|
|
106
|
+
.segments()
|
|
107
|
+
.iter()
|
|
108
|
+
.any(|s| matches!(s, SelectorSegment::AllItems | SelectorSegment::AllKeys));
|
|
100
109
|
|
|
101
110
|
if current_values.is_empty() {
|
|
102
111
|
if used_all_items {
|
data/rust/src/lib.rs
CHANGED
|
@@ -4,19 +4,27 @@ pub mod error;
|
|
|
4
4
|
pub mod ffi;
|
|
5
5
|
pub mod json;
|
|
6
6
|
mod quote_style;
|
|
7
|
+
#[cfg(feature = "schema")]
|
|
7
8
|
pub mod schema;
|
|
8
9
|
pub mod selector;
|
|
9
10
|
mod syntax;
|
|
10
11
|
mod yaml_writer;
|
|
12
|
+
#[cfg(feature = "cli")]
|
|
11
13
|
pub mod yerbafile;
|
|
12
14
|
|
|
13
15
|
pub use document::style::StyleEnforcement;
|
|
14
|
-
pub use document::{
|
|
16
|
+
pub use document::{
|
|
17
|
+
collect_selectors, validate_condition, validate_item_condition, Document, DuplicateInfo, InsertPosition, LocatedNode, Location, NodeInfo, NodeType, SortField,
|
|
18
|
+
};
|
|
15
19
|
pub use error::YerbaError;
|
|
16
20
|
pub use quote_style::{KeyStyle, QuoteStyle};
|
|
17
21
|
pub use selector::Selector;
|
|
18
|
-
pub use syntax::{
|
|
22
|
+
pub use syntax::{
|
|
23
|
+
detect_yaml_type, is_flow_collection, is_inline_scalar_safe, is_plain_safe, is_plain_safe_in_flow, is_quoted_scalar, is_raw_yaml_text, is_valid_inline_value,
|
|
24
|
+
needs_quoting, needs_quoting_in_flow, quote_if_needed, ScalarValue, YerbaValueType,
|
|
25
|
+
};
|
|
19
26
|
pub use yaml_writer::json_to_yaml_text;
|
|
27
|
+
#[cfg(feature = "cli")]
|
|
20
28
|
pub use yerbafile::Yerbafile;
|
|
21
29
|
|
|
22
30
|
pub fn version() -> &'static str {
|
|
@@ -31,6 +39,7 @@ pub fn parse_file(path: impl AsRef<std::path::Path>) -> Result<Document, YerbaEr
|
|
|
31
39
|
Document::parse_file(path)
|
|
32
40
|
}
|
|
33
41
|
|
|
42
|
+
#[cfg(feature = "glob")]
|
|
34
43
|
pub fn glob_get(pattern: &str, selector: &str) -> Vec<document::LocatedNode> {
|
|
35
44
|
use rayon::prelude::*;
|
|
36
45
|
|
|
@@ -53,24 +62,29 @@ pub fn glob_get(pattern: &str, selector: &str) -> Vec<document::LocatedNode> {
|
|
|
53
62
|
.collect()
|
|
54
63
|
}
|
|
55
64
|
|
|
56
|
-
|
|
65
|
+
#[cfg(feature = "glob")]
|
|
66
|
+
pub fn glob_find(pattern: &str, selector: &str, condition: Option<&str>, select: Option<&str>) -> Result<Vec<serde_json::Value>, YerbaError> {
|
|
57
67
|
use rayon::prelude::*;
|
|
58
68
|
|
|
69
|
+
if let Some(cond) = condition {
|
|
70
|
+
crate::document::validate_condition(cond)?;
|
|
71
|
+
}
|
|
72
|
+
|
|
59
73
|
let files = match glob::glob(pattern) {
|
|
60
74
|
Ok(paths) => paths.filter_map(|p| p.ok()).collect::<Vec<_>>(),
|
|
61
|
-
Err(_) => return vec![],
|
|
75
|
+
Err(_) => return Ok(vec![]),
|
|
62
76
|
};
|
|
63
77
|
|
|
64
78
|
let select_fields: Option<Vec<&str>> = select.map(|s| s.split(',').collect());
|
|
65
79
|
|
|
66
|
-
files
|
|
80
|
+
let results: Vec<serde_json::Value> = files
|
|
67
81
|
.par_iter()
|
|
68
82
|
.flat_map(|file| {
|
|
69
83
|
let mut file_results = Vec::new();
|
|
70
84
|
|
|
71
85
|
if let Ok(document) = Document::parse_file(file) {
|
|
72
86
|
let values = match condition {
|
|
73
|
-
Some(cond) => document.filter(selector, cond),
|
|
87
|
+
Some(cond) => document.filter(selector, cond).unwrap_or_default(),
|
|
74
88
|
None => document.get_values(selector),
|
|
75
89
|
};
|
|
76
90
|
|
|
@@ -108,5 +122,7 @@ pub fn glob_find(pattern: &str, selector: &str, condition: Option<&str>, select:
|
|
|
108
122
|
|
|
109
123
|
file_results
|
|
110
124
|
})
|
|
111
|
-
.collect()
|
|
125
|
+
.collect();
|
|
126
|
+
|
|
127
|
+
Ok(results)
|
|
112
128
|
}
|
data/rust/src/main.rs
CHANGED
|
@@ -20,6 +20,7 @@ static HELP: LazyLock<String> = LazyLock::new(|| {
|
|
|
20
20
|
key.nested Nested key path "database.settings.pool"
|
|
21
21
|
[] All items in array "[].title"
|
|
22
22
|
[N] Item at index "[0].title"
|
|
23
|
+
* All values in a map "database.*"
|
|
23
24
|
[].key[].nested Nested array access "[].speakers[].name"
|
|
24
25
|
|
|
25
26
|
Conditions:
|
data/rust/src/quote_style.rs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
use clap::ValueEnum;
|
|
2
1
|
use yaml_parser::SyntaxKind;
|
|
3
2
|
|
|
4
|
-
#[derive(Debug, Clone, PartialEq
|
|
3
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
4
|
+
#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
|
|
5
5
|
pub enum KeyStyle {
|
|
6
6
|
/// Unquoted key (host:)
|
|
7
7
|
Plain,
|
|
@@ -34,33 +34,34 @@ impl KeyStyle {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
#[derive(Debug, Clone, PartialEq
|
|
37
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
38
|
+
#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
|
|
38
39
|
pub enum QuoteStyle {
|
|
39
40
|
/// Unquoted value (host: localhost)
|
|
40
41
|
Plain,
|
|
41
42
|
/// Single-quoted value (host: 'localhost')
|
|
42
|
-
#[value(alias = "single-quoted")]
|
|
43
|
+
#[cfg_attr(feature = "cli", value(alias = "single-quoted"))]
|
|
43
44
|
Single,
|
|
44
45
|
/// Double-quoted value (host: "localhost")
|
|
45
|
-
#[value(alias = "double-quoted")]
|
|
46
|
+
#[cfg_attr(feature = "cli", value(alias = "double-quoted"))]
|
|
46
47
|
Double,
|
|
47
48
|
/// Literal block scalar, strip trailing newline (|-)
|
|
48
|
-
#[value(alias = "block-literal", alias = "|-")]
|
|
49
|
+
#[cfg_attr(feature = "cli", value(alias = "block-literal", alias = "|-"))]
|
|
49
50
|
Literal,
|
|
50
51
|
/// Literal block scalar, keep one trailing newline (|)
|
|
51
|
-
#[value(alias = "|")]
|
|
52
|
+
#[cfg_attr(feature = "cli", value(alias = "|"))]
|
|
52
53
|
LiteralClip,
|
|
53
54
|
/// Literal block scalar, keep all trailing newlines (|+)
|
|
54
|
-
#[value(alias = "|+")]
|
|
55
|
+
#[cfg_attr(feature = "cli", value(alias = "|+"))]
|
|
55
56
|
LiteralKeep,
|
|
56
57
|
/// Folded block scalar, strip trailing newline (>-)
|
|
57
|
-
#[value(alias = "block-folded", alias = ">-")]
|
|
58
|
+
#[cfg_attr(feature = "cli", value(alias = "block-folded", alias = ">-"))]
|
|
58
59
|
Folded,
|
|
59
60
|
/// Folded block scalar, keep one trailing newline (>)
|
|
60
|
-
#[value(alias = ">")]
|
|
61
|
+
#[cfg_attr(feature = "cli", value(alias = ">"))]
|
|
61
62
|
FoldedClip,
|
|
62
63
|
/// Folded block scalar, keep all trailing newlines (>+)
|
|
63
|
-
#[value(alias = ">+")]
|
|
64
|
+
#[cfg_attr(feature = "cli", value(alias = ">+"))]
|
|
64
65
|
FoldedKeep,
|
|
65
66
|
}
|
|
66
67
|
|
data/rust/src/selector.rs
CHANGED
|
@@ -21,6 +21,7 @@ pub enum Selector {
|
|
|
21
21
|
pub enum SelectorSegment {
|
|
22
22
|
Key(String),
|
|
23
23
|
AllItems,
|
|
24
|
+
AllKeys,
|
|
24
25
|
Index(usize),
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -69,7 +70,10 @@ impl Selector {
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
pub fn has_wildcard(&self) -> bool {
|
|
72
|
-
self
|
|
73
|
+
self
|
|
74
|
+
.segments()
|
|
75
|
+
.iter()
|
|
76
|
+
.any(|s| matches!(s, SelectorSegment::AllItems | SelectorSegment::AllKeys))
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
/// Split into the container selector (up to and including the last []) and the remaining field selector.
|
|
@@ -163,6 +167,14 @@ impl Selector {
|
|
|
163
167
|
result.push_str("[]");
|
|
164
168
|
}
|
|
165
169
|
|
|
170
|
+
SelectorSegment::AllKeys => {
|
|
171
|
+
if index > 0 && !result.ends_with('.') {
|
|
172
|
+
result.push('.');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
result.push('*');
|
|
176
|
+
}
|
|
177
|
+
|
|
166
178
|
SelectorSegment::Index(i) => {
|
|
167
179
|
result.push_str(&format!("[{}]", i));
|
|
168
180
|
}
|
|
@@ -227,7 +239,9 @@ fn parse_segments(input: &str) -> Vec<SelectorSegment> {
|
|
|
227
239
|
Some(index) => {
|
|
228
240
|
let key = &rest[..index];
|
|
229
241
|
|
|
230
|
-
if
|
|
242
|
+
if key == "*" {
|
|
243
|
+
segments.push(SelectorSegment::AllKeys);
|
|
244
|
+
} else if !key.is_empty() {
|
|
231
245
|
segments.push(SelectorSegment::Key(key.to_string()));
|
|
232
246
|
}
|
|
233
247
|
|
|
@@ -238,7 +252,9 @@ fn parse_segments(input: &str) -> Vec<SelectorSegment> {
|
|
|
238
252
|
}
|
|
239
253
|
}
|
|
240
254
|
None => {
|
|
241
|
-
if
|
|
255
|
+
if rest == "*" {
|
|
256
|
+
segments.push(SelectorSegment::AllKeys);
|
|
257
|
+
} else if !rest.is_empty() {
|
|
242
258
|
segments.push(SelectorSegment::Key(rest.to_string()));
|
|
243
259
|
}
|
|
244
260
|
|
data/rust/src/syntax.rs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
use rowan::ast::AstNode;
|
|
2
2
|
use rowan::{TextRange, TextSize};
|
|
3
3
|
|
|
4
|
-
use yaml_parser::ast::{BlockMap, BlockMapEntry, BlockSeq};
|
|
4
|
+
use yaml_parser::ast::{BlockMap, BlockMapEntry, BlockSeq, FlowMap, FlowMapEntry, FlowSeq};
|
|
5
5
|
use yaml_parser::{SyntaxKind, SyntaxNode, SyntaxToken};
|
|
6
6
|
|
|
7
7
|
#[derive(Debug, Clone, PartialEq)]
|
|
@@ -82,6 +82,46 @@ pub fn find_block_sequence(node: &SyntaxNode) -> Option<BlockSeq> {
|
|
|
82
82
|
node.descendants().find_map(BlockSeq::cast)
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
fn first_collection_node(node: &SyntaxNode) -> Option<SyntaxNode> {
|
|
86
|
+
node.descendants().find(|descendant| {
|
|
87
|
+
matches!(
|
|
88
|
+
descendant.kind(),
|
|
89
|
+
SyntaxKind::BLOCK_MAP | SyntaxKind::BLOCK_SEQ | SyntaxKind::FLOW_MAP | SyntaxKind::FLOW_SEQ
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
pub fn find_flow_sequence(node: &SyntaxNode) -> Option<FlowSeq> {
|
|
95
|
+
first_collection_node(node).and_then(FlowSeq::cast)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
pub fn find_flow_map(node: &SyntaxNode) -> Option<FlowMap> {
|
|
99
|
+
first_collection_node(node).and_then(FlowMap::cast)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
pub fn flow_sequence_entries(sequence: &FlowSeq) -> Vec<SyntaxNode> {
|
|
103
|
+
sequence
|
|
104
|
+
.entries()
|
|
105
|
+
.map(|entries| entries.entries().map(|entry| entry.syntax().clone()).collect())
|
|
106
|
+
.unwrap_or_default()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
pub fn flow_map_entries(map: &FlowMap) -> Vec<FlowMapEntry> {
|
|
110
|
+
map.entries().map(|entries| entries.entries().collect()).unwrap_or_default()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
pub fn find_flow_entry_by_key(map: &FlowMap, key: &str) -> Option<FlowMapEntry> {
|
|
114
|
+
flow_map_entries(map)
|
|
115
|
+
.into_iter()
|
|
116
|
+
.find(|entry| entry.key().and_then(|found| extract_scalar_text(found.syntax())).as_deref() == Some(key))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
pub fn in_flow_collection(node: &SyntaxNode) -> bool {
|
|
120
|
+
node
|
|
121
|
+
.ancestors()
|
|
122
|
+
.any(|ancestor| matches!(ancestor.kind(), SyntaxKind::FLOW_MAP | SyntaxKind::FLOW_SEQ))
|
|
123
|
+
}
|
|
124
|
+
|
|
85
125
|
pub enum FirstCollection {
|
|
86
126
|
Map(BlockMap),
|
|
87
127
|
Sequence(BlockSeq),
|
|
@@ -142,8 +182,139 @@ pub fn format_scalar_value(value: &str, kind: SyntaxKind) -> String {
|
|
|
142
182
|
}
|
|
143
183
|
}
|
|
144
184
|
|
|
185
|
+
const LEADING_INDICATORS: [char; 16] = ['#', '&', '*', '!', '|', '>', '\'', '"', '%', '@', '`', ',', '[', ']', '{', '}'];
|
|
186
|
+
const FLOW_INDICATORS: [char; 5] = [',', '[', ']', '{', '}'];
|
|
187
|
+
|
|
188
|
+
pub fn is_plain_safe(value: &str) -> bool {
|
|
189
|
+
if value.is_empty() || value != value.trim() {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if value.contains('\n') || value.contains('\t') {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if value.contains(": ") || value.ends_with(':') {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if value.contains(" #") {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if value.starts_with("---") || value.starts_with("...") {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let mut characters = value.chars();
|
|
210
|
+
let first = characters.next().expect("value is non-empty");
|
|
211
|
+
|
|
212
|
+
if LEADING_INDICATORS.contains(&first) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if matches!(first, '-' | '?' | ':') {
|
|
217
|
+
return matches!(characters.next(), Some(next) if next != ' ');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
true
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
pub fn is_plain_safe_in_flow(value: &str) -> bool {
|
|
224
|
+
is_plain_safe(value) && !value.contains(FLOW_INDICATORS)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
pub fn is_quoted_scalar(value: &str) -> bool {
|
|
228
|
+
let bytes = value.as_bytes();
|
|
229
|
+
|
|
230
|
+
if bytes.len() < 2 {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
match (bytes[0], bytes[bytes.len() - 1]) {
|
|
235
|
+
(b'"', b'"') => {
|
|
236
|
+
let interior = &value[1..value.len() - 1];
|
|
237
|
+
let mut escaped = false;
|
|
238
|
+
|
|
239
|
+
for character in interior.chars() {
|
|
240
|
+
if escaped {
|
|
241
|
+
escaped = false;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
match character {
|
|
246
|
+
'\\' => escaped = true,
|
|
247
|
+
'"' => return false,
|
|
248
|
+
_ => {}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
!escaped
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
(b'\'', b'\'') => {
|
|
256
|
+
let interior = &value[1..value.len() - 1];
|
|
257
|
+
let mut characters = interior.chars().peekable();
|
|
258
|
+
|
|
259
|
+
while let Some(character) = characters.next() {
|
|
260
|
+
if character == '\'' {
|
|
261
|
+
if characters.peek() == Some(&'\'') {
|
|
262
|
+
characters.next();
|
|
263
|
+
} else {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
true
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
_ => false,
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
pub fn is_flow_collection(value: &str) -> bool {
|
|
277
|
+
(value.starts_with('[') && value.ends_with(']')) || (value.starts_with('{') && value.ends_with('}'))
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
pub fn is_raw_yaml_text(value: &str) -> bool {
|
|
281
|
+
value.contains('\n') || value.starts_with("- ") || is_quoted_scalar(value) || is_flow_collection(value)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
pub fn is_valid_inline_value(value: &str) -> bool {
|
|
285
|
+
if value.is_empty() {
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
is_raw_yaml_text(value) || is_plain_safe(value)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
pub fn is_inline_scalar_safe(value: &str) -> bool {
|
|
293
|
+
if value.is_empty() {
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if value.contains('\n') || value.starts_with("- ") {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
is_quoted_scalar(value) || is_flow_collection(value) || is_plain_safe(value)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
pub fn needs_quoting(value: &str) -> bool {
|
|
305
|
+
is_yaml_non_string(value) || !is_plain_safe(value)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
pub fn needs_quoting_in_flow(value: &str) -> bool {
|
|
309
|
+
is_yaml_non_string(value) || !is_plain_safe_in_flow(value)
|
|
310
|
+
}
|
|
311
|
+
|
|
145
312
|
pub fn quote_if_needed(value: &str) -> String {
|
|
146
|
-
if
|
|
313
|
+
if is_raw_yaml_text(value) {
|
|
314
|
+
return value.to_string();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if needs_quoting(value) {
|
|
147
318
|
format_scalar_value(value, SyntaxKind::DOUBLE_QUOTED_SCALAR)
|
|
148
319
|
} else {
|
|
149
320
|
value.to_string()
|
|
@@ -288,7 +459,7 @@ pub fn is_yaml_non_string(value: &str) -> bool {
|
|
|
288
459
|
}
|
|
289
460
|
|
|
290
461
|
pub fn is_yaml_truthy(value: &str) -> bool {
|
|
291
|
-
matches!(value, "true" | "True" | "TRUE" | "yes" | "Yes" | "YES" | "on" | "On" | "ON"
|
|
462
|
+
matches!(value, "true" | "True" | "TRUE" | "yes" | "Yes" | "YES" | "on" | "On" | "ON")
|
|
292
463
|
}
|
|
293
464
|
|
|
294
465
|
pub fn detect_yaml_type_from_plain(value: &str) -> YerbaValueType {
|
|
@@ -297,31 +468,12 @@ pub fn detect_yaml_type_from_plain(value: &str) -> YerbaValueType {
|
|
|
297
468
|
return YerbaValueType::Null;
|
|
298
469
|
}
|
|
299
470
|
|
|
300
|
-
// Boolean
|
|
471
|
+
// Boolean, resolved the way libyaml/Psych resolve it: the YAML 1.1 bool set
|
|
472
|
+
// without the single-letter y/Y/n/N forms (which libyaml treats as strings),
|
|
473
|
+
// and broader than YAML 1.2 core schema (which only accepts true/false).
|
|
301
474
|
if matches!(
|
|
302
475
|
value,
|
|
303
|
-
"true"
|
|
304
|
-
| "True"
|
|
305
|
-
| "TRUE"
|
|
306
|
-
| "false"
|
|
307
|
-
| "False"
|
|
308
|
-
| "FALSE"
|
|
309
|
-
| "yes"
|
|
310
|
-
| "Yes"
|
|
311
|
-
| "YES"
|
|
312
|
-
| "no"
|
|
313
|
-
| "No"
|
|
314
|
-
| "NO"
|
|
315
|
-
| "on"
|
|
316
|
-
| "On"
|
|
317
|
-
| "ON"
|
|
318
|
-
| "off"
|
|
319
|
-
| "Off"
|
|
320
|
-
| "OFF"
|
|
321
|
-
| "y"
|
|
322
|
-
| "Y"
|
|
323
|
-
| "n"
|
|
324
|
-
| "N"
|
|
476
|
+
"true" | "True" | "TRUE" | "false" | "False" | "FALSE" | "yes" | "Yes" | "YES" | "no" | "No" | "NO" | "on" | "On" | "ON" | "off" | "Off" | "OFF"
|
|
325
477
|
) {
|
|
326
478
|
return YerbaValueType::Boolean;
|
|
327
479
|
}
|
data/rust/src/yaml_writer.rs
CHANGED
|
@@ -69,8 +69,8 @@ pub fn yaml_value_to_flow_text(value: &yaml_serde::Value) -> String {
|
|
|
69
69
|
yaml_serde::Value::Number(number) => number.to_string(),
|
|
70
70
|
|
|
71
71
|
yaml_serde::Value::String(string) => {
|
|
72
|
-
if crate::syntax::
|
|
73
|
-
|
|
72
|
+
if crate::syntax::needs_quoting_in_flow(string) {
|
|
73
|
+
crate::syntax::format_scalar_value(string, yaml_parser::SyntaxKind::DOUBLE_QUOTED_SCALAR)
|
|
74
74
|
} else {
|
|
75
75
|
string.clone()
|
|
76
76
|
}
|
|
@@ -162,8 +162,8 @@ fn format_yaml_scalar(value: &Value, quote_style: &QuoteStyle) -> String {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
QuoteStyle::Plain => {
|
|
165
|
-
if crate::syntax::
|
|
166
|
-
|
|
165
|
+
if crate::syntax::needs_quoting(string) {
|
|
166
|
+
crate::syntax::format_scalar_value(string, yaml_parser::SyntaxKind::DOUBLE_QUOTED_SCALAR)
|
|
167
167
|
} else {
|
|
168
168
|
string.clone()
|
|
169
169
|
}
|