yerba 0.2.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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +528 -0
  4. data/exe/yerba +6 -0
  5. data/ext/yerba/extconf.rb +111 -0
  6. data/ext/yerba/yerba.c +752 -0
  7. data/lib/yerba/3.2/yerba.so +0 -0
  8. data/lib/yerba/3.3/yerba.so +0 -0
  9. data/lib/yerba/3.4/yerba.so +0 -0
  10. data/lib/yerba/4.0/yerba.so +0 -0
  11. data/lib/yerba/collection.rb +31 -0
  12. data/lib/yerba/document.rb +59 -0
  13. data/lib/yerba/formatting.rb +18 -0
  14. data/lib/yerba/location.rb +5 -0
  15. data/lib/yerba/map.rb +166 -0
  16. data/lib/yerba/scalar.rb +85 -0
  17. data/lib/yerba/sequence.rb +182 -0
  18. data/lib/yerba/version.rb +5 -0
  19. data/lib/yerba.rb +131 -0
  20. data/rust/Cargo.lock +805 -0
  21. data/rust/Cargo.toml +36 -0
  22. data/rust/build.rs +11 -0
  23. data/rust/cbindgen.toml +27 -0
  24. data/rust/rustfmt.toml +2 -0
  25. data/rust/src/commands/apply.rs +5 -0
  26. data/rust/src/commands/blank_lines.rs +58 -0
  27. data/rust/src/commands/check.rs +5 -0
  28. data/rust/src/commands/delete.rs +35 -0
  29. data/rust/src/commands/get.rs +194 -0
  30. data/rust/src/commands/init.rs +89 -0
  31. data/rust/src/commands/insert.rs +106 -0
  32. data/rust/src/commands/mate.rs +55 -0
  33. data/rust/src/commands/mod.rs +349 -0
  34. data/rust/src/commands/move_item.rs +54 -0
  35. data/rust/src/commands/move_key.rs +87 -0
  36. data/rust/src/commands/quote_style.rs +62 -0
  37. data/rust/src/commands/remove.rs +35 -0
  38. data/rust/src/commands/rename.rs +37 -0
  39. data/rust/src/commands/set.rs +59 -0
  40. data/rust/src/commands/sort.rs +52 -0
  41. data/rust/src/commands/sort_keys.rs +62 -0
  42. data/rust/src/commands/version.rs +8 -0
  43. data/rust/src/document.rs +2237 -0
  44. data/rust/src/error.rs +45 -0
  45. data/rust/src/ffi.rs +991 -0
  46. data/rust/src/json.rs +128 -0
  47. data/rust/src/lib.rs +29 -0
  48. data/rust/src/main.rs +72 -0
  49. data/rust/src/quote_style.rs +42 -0
  50. data/rust/src/selector.rs +241 -0
  51. data/rust/src/syntax.rs +262 -0
  52. data/rust/src/yaml_writer.rs +89 -0
  53. data/rust/src/yerbafile.rs +475 -0
  54. data/sig/yerba.rbs +3 -0
  55. data/yerba.gemspec +52 -0
  56. metadata +108 -0
data/rust/src/json.rs ADDED
@@ -0,0 +1,128 @@
1
+ use crate::selector::{Selector, SelectorSegment};
2
+
3
+ pub fn yaml_to_json(value: &serde_yaml::Value) -> serde_json::Value {
4
+ match value {
5
+ serde_yaml::Value::Null => serde_json::Value::Null,
6
+ serde_yaml::Value::Bool(boolean) => serde_json::Value::Bool(*boolean),
7
+
8
+ serde_yaml::Value::Number(number) => {
9
+ if let Some(integer) = number.as_i64() {
10
+ serde_json::Value::Number(integer.into())
11
+ } else if let Some(float) = number.as_f64() {
12
+ serde_json::json!(float)
13
+ } else {
14
+ serde_json::Value::String(number.to_string())
15
+ }
16
+ }
17
+
18
+ serde_yaml::Value::String(string) => serde_json::Value::String(string.clone()),
19
+
20
+ serde_yaml::Value::Sequence(sequence) => serde_json::Value::Array(sequence.iter().map(yaml_to_json).collect()),
21
+
22
+ serde_yaml::Value::Mapping(mapping) => {
23
+ let mut map = serde_json::Map::new();
24
+
25
+ for (key, yaml_value) in mapping {
26
+ let json_key = match key {
27
+ serde_yaml::Value::String(string) => string.clone(),
28
+ _ => format!("{:?}", key),
29
+ };
30
+
31
+ map.insert(json_key, yaml_to_json(yaml_value));
32
+ }
33
+
34
+ serde_json::Value::Object(map)
35
+ }
36
+
37
+ serde_yaml::Value::Tagged(tagged) => yaml_to_json(&tagged.value),
38
+ }
39
+ }
40
+
41
+ pub fn resolve_select_field(value: &serde_yaml::Value, field: &str) -> serde_json::Value {
42
+ let parsed = Selector::parse(field);
43
+ let segments = parsed.segments();
44
+
45
+ if segments.len() == 1 {
46
+ if let SelectorSegment::Key(key) = &segments[0] {
47
+ if let serde_yaml::Value::Mapping(map) = value {
48
+ for (map_key, yaml_value) in map {
49
+ if let serde_yaml::Value::String(key_string) = map_key {
50
+ if key_string == key {
51
+ return yaml_to_json(yaml_value);
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ return serde_json::Value::Null;
58
+ }
59
+ }
60
+
61
+ let mut current_values = vec![value.clone()];
62
+
63
+ for segment in segments {
64
+ let mut next_values = Vec::new();
65
+
66
+ for current in &current_values {
67
+ match segment {
68
+ SelectorSegment::AllItems => {
69
+ if let serde_yaml::Value::Sequence(sequence) = current {
70
+ next_values.extend(sequence.iter().cloned());
71
+ }
72
+ }
73
+
74
+ SelectorSegment::Index(index) => {
75
+ if let serde_yaml::Value::Sequence(sequence) = current {
76
+ if let Some(item) = sequence.get(*index) {
77
+ next_values.push(item.clone());
78
+ }
79
+ }
80
+ }
81
+
82
+ SelectorSegment::Key(key) => {
83
+ if let serde_yaml::Value::Mapping(map) = current {
84
+ for (map_key, yaml_value) in map {
85
+ if let serde_yaml::Value::String(key_string) = map_key {
86
+ if key_string == key {
87
+ next_values.push(yaml_value.clone());
88
+ }
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+
96
+ current_values = next_values;
97
+ }
98
+
99
+ let used_all_items = parsed.segments().iter().any(|s| matches!(s, SelectorSegment::AllItems));
100
+
101
+ if current_values.is_empty() {
102
+ if used_all_items {
103
+ serde_json::Value::Array(Vec::new())
104
+ } else {
105
+ serde_json::Value::Null
106
+ }
107
+ } else if current_values.len() == 1 && !used_all_items {
108
+ yaml_to_json(&current_values[0])
109
+ } else {
110
+ serde_json::Value::Array(current_values.iter().map(yaml_to_json).collect())
111
+ }
112
+ }
113
+
114
+ pub fn select_field_key(field: &str) -> String {
115
+ let parsed = Selector::parse(field);
116
+
117
+ parsed
118
+ .segments()
119
+ .iter()
120
+ .find_map(|segment| {
121
+ if let SelectorSegment::Key(key) = segment {
122
+ Some(key.clone())
123
+ } else {
124
+ None
125
+ }
126
+ })
127
+ .unwrap_or_else(|| field.to_string())
128
+ }
data/rust/src/lib.rs ADDED
@@ -0,0 +1,29 @@
1
+ mod document;
2
+ mod error;
3
+ pub mod ffi;
4
+ pub mod json;
5
+ mod quote_style;
6
+ pub mod selector;
7
+ mod syntax;
8
+ mod yaml_writer;
9
+ pub mod yerbafile;
10
+
11
+ pub use document::{Document, InsertPosition, SortField};
12
+ pub use error::YerbaError;
13
+ pub use quote_style::QuoteStyle;
14
+ pub use selector::Selector;
15
+ pub use syntax::{detect_yaml_type, ScalarValue, YerbaValueType};
16
+ pub use yaml_writer::json_to_yaml_text;
17
+ pub use yerbafile::Yerbafile;
18
+
19
+ pub fn version() -> &'static str {
20
+ env!("CARGO_PKG_VERSION")
21
+ }
22
+
23
+ pub fn parse(source: &str) -> Result<Document, YerbaError> {
24
+ Document::parse(source)
25
+ }
26
+
27
+ pub fn parse_file(path: impl AsRef<std::path::Path>) -> Result<Document, YerbaError> {
28
+ Document::parse_file(path)
29
+ }
data/rust/src/main.rs ADDED
@@ -0,0 +1,72 @@
1
+ mod commands;
2
+
3
+ use std::sync::LazyLock;
4
+
5
+ use clap::builder::styling::{AnsiColor, Effects, Styles};
6
+ use clap::Parser;
7
+ use indoc::indoc;
8
+
9
+ const STYLES: Styles = Styles::styled()
10
+ .header(AnsiColor::Green.on_default().effects(Effects::BOLD))
11
+ .usage(AnsiColor::Green.on_default().effects(Effects::BOLD))
12
+ .literal(AnsiColor::Cyan.on_default())
13
+ .placeholder(AnsiColor::Yellow.on_default())
14
+ .valid(AnsiColor::Green.on_default());
15
+
16
+ static HELP: LazyLock<String> = LazyLock::new(|| {
17
+ commands::colorize_help(indoc! {r#"
18
+ Selectors:
19
+ key A single key "database.host"
20
+ key.nested Nested key path "database.settings.pool"
21
+ [] All items in array "[].title"
22
+ [N] Item at index "[0].title"
23
+ [].key[].nested Nested array access "[].speakers[].name"
24
+
25
+ Conditions:
26
+ .key == value Equality ".kind == keynote"
27
+ .key != value Inequality ".status != draft"
28
+ .key contains val Substring or member ".title contains Ruby"
29
+ .key not_contains Negated contains ".title not_contains test"
30
+
31
+ Yerbafile:
32
+ yerba init Create a new Yerbafile in the current directory
33
+ yerba check Check if all files match the rules (exits 1 if not)
34
+ yerba apply Apply all rules and write changes
35
+
36
+ Examples:
37
+ yerba get config.yml "database.host"
38
+ yerba get videos.yml "[0].title"
39
+ yerba get videos.yml "[]" --select ".title,.speakers"
40
+ yerba get "data/**/videos.yml" "[]" --condition ".kind == keynote" --select ".id,.title"
41
+ yerba set config.yml "database.host" "0.0.0.0"
42
+ yerba insert config.yml "tags" "yaml" --after "ruby"
43
+ yerba insert speakers.yml "" --from "speaker.yml" --after ".name == Alice"
44
+ yerba delete config.yml "database.pool"
45
+ yerba move videos.yml "" ".id == talk-2" --after ".id == talk-1"
46
+ yerba sort-keys config.yml "database" "id,host,port,name"
47
+ yerba quote-style "data/**/*.yml" --values double
48
+ "#})
49
+ });
50
+
51
+ #[derive(Parser)]
52
+ #[command(
53
+ name = "yerba",
54
+ version = yerba::version(),
55
+ disable_version_flag = true,
56
+ styles = STYLES,
57
+ about = "Yerba 🧉 YAML Editing and Refactoring with Better Accuracy",
58
+ arg_required_else_help = true,
59
+ override_usage = "\x1b[1myerba\x1b[0m <command> <file> <selector> [options]",
60
+ disable_help_subcommand = true,
61
+ after_help = HELP.as_str()
62
+ )]
63
+ #[allow(clippy::upper_case_acronyms)]
64
+ struct CLI {
65
+ #[command(subcommand)]
66
+ command: commands::Command,
67
+ }
68
+
69
+ fn main() {
70
+ let cli = CLI::parse();
71
+ cli.command.run();
72
+ }
@@ -0,0 +1,42 @@
1
+ use clap::ValueEnum;
2
+ use yaml_parser::SyntaxKind;
3
+
4
+ #[derive(Debug, Clone, PartialEq, ValueEnum)]
5
+ pub enum QuoteStyle {
6
+ Plain,
7
+ #[value(alias = "single-quoted")]
8
+ Single,
9
+ #[value(alias = "double-quoted")]
10
+ Double,
11
+ #[value(alias = "block-literal")]
12
+ Literal,
13
+ #[value(alias = "block-folded")]
14
+ Folded,
15
+ }
16
+
17
+ impl std::str::FromStr for QuoteStyle {
18
+ type Err = String;
19
+
20
+ fn from_str(string: &str) -> Result<Self, Self::Err> {
21
+ match string {
22
+ "plain" => Ok(QuoteStyle::Plain),
23
+ "single" | "single-quoted" => Ok(QuoteStyle::Single),
24
+ "double" | "double-quoted" => Ok(QuoteStyle::Double),
25
+ "literal" | "block-literal" => Ok(QuoteStyle::Literal),
26
+ "folded" | "block-folded" => Ok(QuoteStyle::Folded),
27
+ _ => Err(format!("unknown quote style: '{}'", string)),
28
+ }
29
+ }
30
+ }
31
+
32
+ impl QuoteStyle {
33
+ pub(crate) fn to_syntax_kind(&self) -> SyntaxKind {
34
+ match self {
35
+ QuoteStyle::Plain => SyntaxKind::PLAIN_SCALAR,
36
+ QuoteStyle::Single => SyntaxKind::SINGLE_QUOTED_SCALAR,
37
+ QuoteStyle::Double => SyntaxKind::DOUBLE_QUOTED_SCALAR,
38
+ QuoteStyle::Literal => SyntaxKind::BLOCK_SCALAR_TEXT,
39
+ QuoteStyle::Folded => SyntaxKind::BLOCK_SCALAR_TEXT,
40
+ }
41
+ }
42
+ }
@@ -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
+ }