yerba 0.1.1 → 0.2.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +492 -15
  3. data/ext/yerba/extconf.rb +87 -30
  4. data/ext/yerba/include/yerba.h +168 -0
  5. data/ext/yerba/yerba.c +752 -0
  6. data/lib/yerba/collection.rb +31 -0
  7. data/lib/yerba/document.rb +59 -0
  8. data/lib/yerba/formatting.rb +18 -0
  9. data/lib/yerba/location.rb +5 -0
  10. data/lib/yerba/map.rb +166 -0
  11. data/lib/yerba/scalar.rb +85 -0
  12. data/lib/yerba/sequence.rb +182 -0
  13. data/lib/yerba/version.rb +1 -1
  14. data/lib/yerba.rb +30 -4
  15. data/rust/Cargo.lock +378 -2
  16. data/rust/Cargo.toml +5 -1
  17. data/rust/build.rs +11 -0
  18. data/rust/cbindgen.toml +27 -0
  19. data/rust/src/commands/apply.rs +5 -0
  20. data/rust/src/commands/blank_lines.rs +58 -0
  21. data/rust/src/commands/check.rs +5 -0
  22. data/rust/src/commands/delete.rs +35 -0
  23. data/rust/src/commands/get.rs +194 -0
  24. data/rust/src/commands/init.rs +89 -0
  25. data/rust/src/commands/insert.rs +106 -0
  26. data/rust/src/commands/mate.rs +55 -0
  27. data/rust/src/commands/mod.rs +349 -0
  28. data/rust/src/commands/move_item.rs +54 -0
  29. data/rust/src/commands/move_key.rs +87 -0
  30. data/rust/src/commands/quote_style.rs +62 -0
  31. data/rust/src/commands/remove.rs +35 -0
  32. data/rust/src/commands/rename.rs +37 -0
  33. data/rust/src/commands/set.rs +59 -0
  34. data/rust/src/commands/sort.rs +52 -0
  35. data/rust/src/commands/sort_keys.rs +62 -0
  36. data/rust/src/commands/version.rs +8 -0
  37. data/rust/src/document.rs +798 -340
  38. data/rust/src/error.rs +0 -5
  39. data/rust/src/ffi.rs +991 -0
  40. data/rust/src/json.rs +49 -90
  41. data/rust/src/lib.rs +9 -2
  42. data/rust/src/main.rs +55 -839
  43. data/rust/src/selector.rs +241 -0
  44. data/rust/src/syntax.rs +97 -21
  45. data/rust/src/yaml_writer.rs +89 -0
  46. data/rust/src/yerbafile.rs +34 -122
  47. data/yerba.gemspec +4 -0
  48. metadata +33 -1
@@ -0,0 +1,241 @@
1
+ /// A parsed selector used across selectors, conditions, and select fields.
2
+ ///
3
+ /// Selectors starting with `.` are relative to the current item context.
4
+ /// All other selectors are absolute from the document root.
5
+ ///
6
+ /// Examples:
7
+ /// ".title" → Relative([Key("title")])
8
+ /// ".speakers[].name" → Relative([Key("speakers"), AllItems, Key("name")])
9
+ /// "database.host" → Absolute([Key("database"), Key("host")])
10
+ /// "[].title" → Absolute([AllItems, Key("title")])
11
+ /// "[0].speakers[]" → Absolute([Index(0), Key("speakers"), AllItems])
12
+ /// "[]" → Absolute([AllItems])
13
+ ///
14
+ #[derive(Debug, Clone, PartialEq)]
15
+ pub enum Selector {
16
+ Relative(Vec<SelectorSegment>),
17
+ Absolute(Vec<SelectorSegment>),
18
+ }
19
+
20
+ #[derive(Debug, Clone, PartialEq)]
21
+ pub enum SelectorSegment {
22
+ Key(String),
23
+ AllItems,
24
+ Index(usize),
25
+ }
26
+
27
+ impl Selector {
28
+ pub fn parse(input: &str) -> Self {
29
+ let input = input.trim();
30
+
31
+ if input.is_empty() {
32
+ return Selector::Absolute(Vec::new());
33
+ }
34
+
35
+ if let Some(rest) = input.strip_prefix('.') {
36
+ Selector::Relative(parse_segments(rest))
37
+ } else {
38
+ Selector::Absolute(parse_segments(input))
39
+ }
40
+ }
41
+
42
+ pub fn is_relative(&self) -> bool {
43
+ matches!(self, Selector::Relative(_))
44
+ }
45
+
46
+ pub fn is_absolute(&self) -> bool {
47
+ matches!(self, Selector::Absolute(_))
48
+ }
49
+
50
+ pub fn segments(&self) -> &[SelectorSegment] {
51
+ match self {
52
+ Selector::Relative(segments) | Selector::Absolute(segments) => segments,
53
+ }
54
+ }
55
+
56
+ pub fn is_empty(&self) -> bool {
57
+ self.segments().is_empty()
58
+ }
59
+
60
+ pub fn ends_with_bracket(&self) -> bool {
61
+ matches!(
62
+ self.segments().last(),
63
+ Some(SelectorSegment::AllItems | SelectorSegment::Index(_))
64
+ )
65
+ }
66
+
67
+ pub fn has_brackets(&self) -> bool {
68
+ self
69
+ .segments()
70
+ .iter()
71
+ .any(|s| matches!(s, SelectorSegment::AllItems | SelectorSegment::Index(_)))
72
+ }
73
+
74
+ pub fn has_wildcard(&self) -> bool {
75
+ self.segments().iter().any(|s| matches!(s, SelectorSegment::AllItems))
76
+ }
77
+
78
+ /// Split into the container selector (up to and including the last []) and the remaining field selector.
79
+ /// Used to separate "where to search" from "what to extract".
80
+ ///
81
+ /// "[].speakers[].name" → ("[].speakers[]", ".name")
82
+ /// "[].title" → ("[]", ".title")
83
+ /// "[]" → ("[]", "")
84
+ /// "database.host" → ("", "database.host")
85
+ pub fn split_at_last_bracket(&self) -> (Selector, Selector) {
86
+ let segments = self.segments();
87
+
88
+ let last_bracket = segments
89
+ .iter()
90
+ .rposition(|s| matches!(s, SelectorSegment::AllItems | SelectorSegment::Index(_)));
91
+
92
+ match last_bracket {
93
+ Some(pos) => {
94
+ let container = segments[..=pos].to_vec();
95
+ let field = segments[pos + 1..].to_vec();
96
+
97
+ (Selector::Absolute(container), Selector::Relative(field))
98
+ }
99
+ None => (Selector::Absolute(Vec::new()), Selector::Absolute(segments.to_vec())),
100
+ }
101
+ }
102
+
103
+ /// Split at the first [] bracket — used for condition evaluation.
104
+ /// The condition is evaluated on items at the first container level.
105
+ ///
106
+ /// "[].speakers[]" → ("[],", ".speakers[]")
107
+ /// "[].title" → ("[]", ".title")
108
+ /// "[]" → ("[]", "")
109
+ pub fn split_at_first_bracket(&self) -> (Selector, Selector) {
110
+ let segments = self.segments();
111
+
112
+ let first_bracket = segments
113
+ .iter()
114
+ .position(|s| matches!(s, SelectorSegment::AllItems | SelectorSegment::Index(_)));
115
+
116
+ match first_bracket {
117
+ Some(position) => {
118
+ let container = segments[..=position].to_vec();
119
+ let rest = segments[position + 1..].to_vec();
120
+
121
+ (Selector::Absolute(container), Selector::Relative(rest))
122
+ }
123
+
124
+ None => (Selector::Absolute(Vec::new()), Selector::Absolute(segments.to_vec())),
125
+ }
126
+ }
127
+
128
+ /// Resolve a relative selector against a base absolute selector.
129
+ ///
130
+ /// ".title" + "[]" → "[].title"
131
+ /// ".name" + "[].speakers[]" → "[].speakers[].name"
132
+ /// If self is absolute, returns self unchanged.
133
+ pub fn resolve_relative(&self, base: &Selector) -> Selector {
134
+ if self.is_absolute() {
135
+ return self.clone();
136
+ }
137
+
138
+ let mut segments = base.segments().to_vec();
139
+ segments.extend_from_slice(self.segments());
140
+
141
+ Selector::Absolute(segments)
142
+ }
143
+
144
+ pub fn to_selector_string(&self) -> String {
145
+ let segments = self.segments();
146
+
147
+ if segments.is_empty() {
148
+ return String::new();
149
+ }
150
+
151
+ let mut result = String::new();
152
+
153
+ for (index, segment) in segments.iter().enumerate() {
154
+ match segment {
155
+ SelectorSegment::Key(key) => {
156
+ if index > 0 && !matches!(segments.get(index - 1), Some(SelectorSegment::Key(_))) {
157
+ if !result.ends_with('.') {
158
+ result.push('.');
159
+ }
160
+ } else if index > 0 {
161
+ result.push('.');
162
+ }
163
+
164
+ result.push_str(key);
165
+ }
166
+
167
+ SelectorSegment::AllItems => {
168
+ result.push_str("[]");
169
+ }
170
+
171
+ SelectorSegment::Index(i) => {
172
+ result.push_str(&format!("[{}]", i));
173
+ }
174
+ }
175
+ }
176
+
177
+ result
178
+ }
179
+ }
180
+
181
+ fn parse_segments(input: &str) -> Vec<SelectorSegment> {
182
+ let mut segments = Vec::new();
183
+ let mut rest = input;
184
+
185
+ while !rest.is_empty() {
186
+ if rest.starts_with('[') {
187
+ if let Some(close) = rest.find(']') {
188
+ let inner = &rest[1..close];
189
+
190
+ if inner.is_empty() {
191
+ segments.push(SelectorSegment::AllItems);
192
+ } else if let Ok(index) = inner.parse::<usize>() {
193
+ segments.push(SelectorSegment::Index(index));
194
+ }
195
+
196
+ rest = &rest[close + 1..];
197
+
198
+ if rest.starts_with('.') {
199
+ rest = &rest[1..];
200
+ }
201
+ } else {
202
+ break;
203
+ }
204
+ } else {
205
+ let dot_index = rest.find('.');
206
+ let bracket_index = rest.find('[');
207
+
208
+ let split_at = match (dot_index, bracket_index) {
209
+ (Some(dot), Some(bracket)) => Some(dot.min(bracket)),
210
+ (Some(dot), None) => Some(dot),
211
+ (None, Some(bracket)) => Some(bracket),
212
+ (None, None) => None,
213
+ };
214
+
215
+ match split_at {
216
+ Some(index) => {
217
+ let key = &rest[..index];
218
+
219
+ if !key.is_empty() {
220
+ segments.push(SelectorSegment::Key(key.to_string()));
221
+ }
222
+
223
+ rest = &rest[index..];
224
+
225
+ if rest.starts_with('.') {
226
+ rest = &rest[1..];
227
+ }
228
+ }
229
+ None => {
230
+ if !rest.is_empty() {
231
+ segments.push(SelectorSegment::Key(rest.to_string()));
232
+ }
233
+
234
+ break;
235
+ }
236
+ }
237
+ }
238
+ }
239
+
240
+ segments
241
+ }
data/rust/src/syntax.rs CHANGED
@@ -4,6 +4,55 @@ use rowan::{TextRange, TextSize};
4
4
  use yaml_parser::ast::{BlockMap, BlockMapEntry};
5
5
  use yaml_parser::{SyntaxKind, SyntaxNode, SyntaxToken};
6
6
 
7
+ #[derive(Debug, Clone, PartialEq)]
8
+ pub struct ScalarValue {
9
+ pub text: String,
10
+ pub kind: SyntaxKind,
11
+ }
12
+
13
+ #[repr(C)]
14
+ #[derive(Debug, Clone, Copy, PartialEq)]
15
+ pub enum YerbaValueType {
16
+ Null = 0,
17
+ Boolean = 1,
18
+ Integer = 2,
19
+ Float = 3,
20
+ String = 4,
21
+ }
22
+
23
+ pub fn detect_yaml_type(scalar: &ScalarValue) -> YerbaValueType {
24
+ if scalar.kind != SyntaxKind::PLAIN_SCALAR {
25
+ return YerbaValueType::String;
26
+ }
27
+
28
+ detect_yaml_type_from_plain(&scalar.text)
29
+ }
30
+
31
+ pub fn extract_scalar(node: &SyntaxNode) -> Option<ScalarValue> {
32
+ let token = find_scalar_token(node)?;
33
+
34
+ let text = match token.kind() {
35
+ SyntaxKind::PLAIN_SCALAR => token.text().to_string(),
36
+
37
+ SyntaxKind::DOUBLE_QUOTED_SCALAR => {
38
+ let raw = token.text();
39
+ unescape_double_quoted(&raw[1..raw.len() - 1])
40
+ }
41
+
42
+ SyntaxKind::SINGLE_QUOTED_SCALAR => {
43
+ let raw = token.text();
44
+ unescape_single_quoted(&raw[1..raw.len() - 1])
45
+ }
46
+
47
+ _ => return None,
48
+ };
49
+
50
+ Some(ScalarValue {
51
+ text,
52
+ kind: token.kind(),
53
+ })
54
+ }
55
+
7
56
  pub fn is_map_key(token: &SyntaxToken) -> bool {
8
57
  token
9
58
  .parent_ancestors()
@@ -141,46 +190,73 @@ pub fn removal_range(node: &SyntaxNode) -> TextRange {
141
190
  }
142
191
 
143
192
  pub fn is_yaml_non_string(value: &str) -> bool {
144
- // Null (YAML 1.1 + 1.2)
145
- if matches!(value, "null" | "Null" | "NULL" | "~" | "") {
146
- return true;
147
- }
148
-
149
- // Boolean (YAML 1.2)
150
- if matches!(value, "true" | "True" | "TRUE" | "false" | "False" | "FALSE") {
151
- return true;
152
- }
193
+ detect_yaml_type_from_plain(value) != YerbaValueType::String
194
+ }
153
195
 
154
- // Boolean (YAML 1.1 extras)
155
- if matches!(
196
+ pub fn is_yaml_truthy(value: &str) -> bool {
197
+ matches!(
156
198
  value,
157
- "yes" | "Yes" | "YES" | "no" | "No" | "NO" | "on" | "On" | "ON" | "off" | "Off" | "OFF" | "y" | "Y" | "n" | "N"
158
- ) {
159
- return true;
199
+ "true" | "True" | "TRUE" | "yes" | "Yes" | "YES" | "on" | "On" | "ON" | "y" | "Y"
200
+ )
201
+ }
202
+
203
+ pub fn detect_yaml_type_from_plain(value: &str) -> YerbaValueType {
204
+ // Null (YAML 1.1 + 1.2)
205
+ if matches!(value, "null" | "Null" | "NULL" | "~" | "") {
206
+ return YerbaValueType::Null;
160
207
  }
161
208
 
162
- // Special floats (YAML 1.1 + 1.2)
209
+ // Boolean (YAML 1.2 + 1.1)
163
210
  if matches!(
164
211
  value,
165
- ".inf" | ".Inf" | ".INF" | "-.inf" | "-.Inf" | "-.INF" | "+.inf" | "+.Inf" | "+.INF" | ".nan" | ".NaN" | ".NAN"
212
+ "true"
213
+ | "True"
214
+ | "TRUE"
215
+ | "false"
216
+ | "False"
217
+ | "FALSE"
218
+ | "yes"
219
+ | "Yes"
220
+ | "YES"
221
+ | "no"
222
+ | "No"
223
+ | "NO"
224
+ | "on"
225
+ | "On"
226
+ | "ON"
227
+ | "off"
228
+ | "Off"
229
+ | "OFF"
230
+ | "y"
231
+ | "Y"
232
+ | "n"
233
+ | "N"
166
234
  ) {
167
- return true;
235
+ return YerbaValueType::Boolean;
168
236
  }
169
237
 
170
238
  // Integer
171
239
  if value.parse::<i64>().is_ok() {
172
- return true;
240
+ return YerbaValueType::Integer;
173
241
  }
174
242
 
175
243
  // Octal (0o...) and hex (0x...)
176
244
  if value.starts_with("0x") || value.starts_with("0X") || value.starts_with("0o") || value.starts_with("0O") {
177
- return true;
245
+ return YerbaValueType::Integer;
246
+ }
247
+
248
+ // Special floats (YAML 1.1 + 1.2)
249
+ if matches!(
250
+ value,
251
+ ".inf" | ".Inf" | ".INF" | "-.inf" | "-.Inf" | "-.INF" | "+.inf" | "+.Inf" | "+.INF" | ".nan" | ".NaN" | ".NAN"
252
+ ) {
253
+ return YerbaValueType::Float;
178
254
  }
179
255
 
180
256
  // Float
181
257
  if value.parse::<f64>().is_ok() {
182
- return true;
258
+ return YerbaValueType::Float;
183
259
  }
184
260
 
185
- false
261
+ YerbaValueType::String
186
262
  }
@@ -0,0 +1,89 @@
1
+ use serde_json::Value;
2
+
3
+ use crate::QuoteStyle;
4
+
5
+ pub fn json_to_yaml_text(value: &Value, quote_style: &QuoteStyle, indent: usize) -> String {
6
+ match value {
7
+ Value::Object(map) => {
8
+ let prefix = " ".repeat(indent);
9
+
10
+ map
11
+ .iter()
12
+ .map(|(k, v)| match v {
13
+ Value::Array(arr) => {
14
+ let items: Vec<String> = arr
15
+ .iter()
16
+ .map(|item| match item {
17
+ Value::Object(_) => {
18
+ let inner = json_to_yaml_text(item, quote_style, indent + 4);
19
+ format!("{} - {}", prefix, inner.trim_start())
20
+ }
21
+
22
+ _ => format!("{} - {}", prefix, format_yaml_scalar(item, quote_style)),
23
+ })
24
+ .collect();
25
+
26
+ format!("{}{}:\n{}", prefix, k, items.join("\n"))
27
+ }
28
+
29
+ Value::Object(_) => {
30
+ let inner = json_to_yaml_text(v, quote_style, indent + 2);
31
+ format!("{}{}:\n{}", prefix, k, inner)
32
+ }
33
+
34
+ _ => format!("{}{}: {}", prefix, k, format_yaml_scalar(v, quote_style)),
35
+ })
36
+ .collect::<Vec<_>>()
37
+ .join("\n")
38
+ }
39
+
40
+ _ => {
41
+ let prefix = " ".repeat(indent);
42
+
43
+ format!("{}{}", prefix, format_yaml_scalar(value, quote_style))
44
+ }
45
+ }
46
+ }
47
+
48
+ fn format_yaml_scalar(value: &Value, quote_style: &QuoteStyle) -> String {
49
+ match value {
50
+ Value::Null => "null".to_string(),
51
+ Value::Bool(boolean) => boolean.to_string(),
52
+ Value::Number(number) => number.to_string(),
53
+ Value::String(string) => match quote_style {
54
+ QuoteStyle::Double => {
55
+ let escaped = string.replace('\\', "\\\\").replace('"', "\\\"");
56
+
57
+ format!("\"{}\"", escaped)
58
+ }
59
+
60
+ QuoteStyle::Single => {
61
+ let escaped = string.replace('\'', "''");
62
+
63
+ format!("'{}'", escaped)
64
+ }
65
+
66
+ QuoteStyle::Plain => {
67
+ if crate::syntax::is_yaml_non_string(string) {
68
+ format!("\"{}\"", string.replace('"', "\\\""))
69
+ } else {
70
+ string.clone()
71
+ }
72
+ }
73
+
74
+ _ => {
75
+ let escaped = string.replace('\\', "\\\\").replace('"', "\\\"");
76
+
77
+ format!("\"{}\"", escaped)
78
+ }
79
+ },
80
+
81
+ Value::Array(array) => {
82
+ let items: Vec<String> = array.iter().map(|item| format_yaml_scalar(item, quote_style)).collect();
83
+
84
+ format!("[{}]", items.join(", "))
85
+ }
86
+
87
+ Value::Object(_) => format!("{:?}", value),
88
+ }
89
+ }