yerba 0.1.2 → 0.2.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +492 -15
  3. data/exe/x86_64-linux/yerba +0 -0
  4. data/ext/yerba/extconf.rb +87 -30
  5. data/ext/yerba/include/yerba.h +168 -0
  6. data/ext/yerba/yerba.c +752 -0
  7. data/lib/yerba/collection.rb +31 -0
  8. data/lib/yerba/document.rb +59 -0
  9. data/lib/yerba/formatting.rb +18 -0
  10. data/lib/yerba/location.rb +5 -0
  11. data/lib/yerba/map.rb +166 -0
  12. data/lib/yerba/scalar.rb +85 -0
  13. data/lib/yerba/sequence.rb +182 -0
  14. data/lib/yerba/version.rb +1 -1
  15. data/lib/yerba.rb +32 -4
  16. data/rust/Cargo.lock +378 -2
  17. data/rust/Cargo.toml +5 -1
  18. data/rust/build.rs +11 -0
  19. data/rust/cbindgen.toml +27 -0
  20. data/rust/src/commands/apply.rs +5 -0
  21. data/rust/src/commands/blank_lines.rs +58 -0
  22. data/rust/src/commands/check.rs +5 -0
  23. data/rust/src/commands/delete.rs +35 -0
  24. data/rust/src/commands/get.rs +194 -0
  25. data/rust/src/commands/init.rs +89 -0
  26. data/rust/src/commands/insert.rs +106 -0
  27. data/rust/src/commands/mate.rs +55 -0
  28. data/rust/src/commands/mod.rs +349 -0
  29. data/rust/src/commands/move_item.rs +54 -0
  30. data/rust/src/commands/move_key.rs +87 -0
  31. data/rust/src/commands/quote_style.rs +62 -0
  32. data/rust/src/commands/remove.rs +35 -0
  33. data/rust/src/commands/rename.rs +37 -0
  34. data/rust/src/commands/set.rs +59 -0
  35. data/rust/src/commands/sort.rs +52 -0
  36. data/rust/src/commands/sort_keys.rs +62 -0
  37. data/rust/src/commands/version.rs +8 -0
  38. data/rust/src/document.rs +764 -333
  39. data/rust/src/error.rs +0 -5
  40. data/rust/src/ffi.rs +991 -0
  41. data/rust/src/json.rs +49 -90
  42. data/rust/src/lib.rs +9 -2
  43. data/rust/src/main.rs +55 -843
  44. data/rust/src/selector.rs +241 -0
  45. data/rust/src/syntax.rs +97 -21
  46. data/rust/src/yaml_writer.rs +89 -0
  47. data/rust/src/yerbafile.rs +11 -126
  48. data/yerba.gemspec +5 -0
  49. metadata +34 -1
@@ -0,0 +1,194 @@
1
+ use std::process;
2
+ use std::sync::LazyLock;
3
+
4
+ use indoc::indoc;
5
+
6
+ use super::{colorize_examples, parse_file, resolve_files};
7
+
8
+ static EXAMPLES: LazyLock<String> = LazyLock::new(|| {
9
+ colorize_examples(indoc! {r#"
10
+ yerba get config.yml "database.host"
11
+ yerba get videos.yml "[].title"
12
+ yerba get videos.yml "[0].title"
13
+ yerba get "data/**/videos.yml" "[].speakers[].name"
14
+ yerba get videos.yml "[]" --select ".title,.speakers"
15
+ yerba get videos.yml "[]" --condition ".kind == keynote"
16
+ yerba get videos.yml "[]" --select ".title" --condition ".kind == keynote"
17
+ yerba get videos.yml "[].speakers[]" --condition "[].video_provider == youtube"
18
+ yerba get videos.yml "[]" --condition ".speakers contains Matz" --raw
19
+ "#})
20
+ });
21
+
22
+ #[derive(clap::Args)]
23
+ #[command(
24
+ about = "Get values, filter items, and select fields from YAML files",
25
+ arg_required_else_help = true,
26
+ after_help = EXAMPLES.as_str()
27
+ )]
28
+ pub struct Args {
29
+ file: String,
30
+ selector: String,
31
+ /// Filter items by condition (e.g. '.kind == keynote', '[].video_provider == youtube')
32
+ #[arg(long)]
33
+ condition: Option<String>,
34
+ /// Comma-separated fields to include (e.g. '.title,.speakers')
35
+ #[arg(long)]
36
+ select: Option<String>,
37
+ /// Output raw values (one per line) instead of JSON
38
+ #[arg(long)]
39
+ raw: bool,
40
+ #[arg(long, hide = true)]
41
+ json: bool,
42
+ }
43
+
44
+ impl Args {
45
+ pub fn run(self) {
46
+ let selector = yerba::Selector::parse(&self.selector);
47
+ let condition_path = self.condition.as_deref().map(yerba::Selector::parse);
48
+ let select_fields: Option<Vec<&str>> = self.select.as_deref().map(|fields| fields.split(',').collect());
49
+ let (search_path, extract_field) = self.resolve_search_scope(&selector, condition_path.as_ref());
50
+
51
+ let normalized_condition = self.condition.as_ref().map(|condition| {
52
+ if !condition.starts_with('.') {
53
+ if let Some(bracket_end) = condition.find(']') {
54
+ let after_bracket = &condition[bracket_end + 1..];
55
+
56
+ if after_bracket.starts_with('.') {
57
+ return after_bracket.to_string();
58
+ }
59
+ }
60
+ }
61
+
62
+ condition.clone()
63
+ });
64
+
65
+ let search_path_string = search_path.to_selector_string();
66
+ let mut all_results: Vec<serde_json::Value> = Vec::new();
67
+
68
+ for resolved_file in resolve_files(&self.file) {
69
+ let document = parse_file(&resolved_file);
70
+
71
+ let values: Vec<serde_yaml::Value> = if let Some(condition) = &normalized_condition {
72
+ document.filter(&search_path_string, condition)
73
+ } else {
74
+ document.get_values(&search_path_string)
75
+ };
76
+
77
+ if values.is_empty()
78
+ && !selector.has_brackets()
79
+ && normalized_condition.is_none()
80
+ && !document.exists(&self.selector)
81
+ {
82
+ use super::color::*;
83
+ eprintln!("{RED}Error:{RESET} path not found: {}", self.selector);
84
+ process::exit(1);
85
+ }
86
+
87
+ for value in values {
88
+ if let Some(field) = &extract_field {
89
+ let field_string = field.to_selector_string();
90
+ let json_value = yerba::json::resolve_select_field(&value, &field_string);
91
+
92
+ if field.ends_with_bracket() {
93
+ if let serde_json::Value::Array(items) = json_value {
94
+ all_results.extend(items);
95
+ }
96
+ } else {
97
+ all_results.push(json_value);
98
+ }
99
+
100
+ continue;
101
+ }
102
+
103
+ if let Some(fields) = &select_fields {
104
+ let mut result = serde_json::Map::new();
105
+
106
+ result.insert("__file".to_string(), serde_json::Value::String(resolved_file.clone()));
107
+
108
+ for field in fields {
109
+ let json_value = yerba::json::resolve_select_field(&value, field);
110
+ let json_key = yerba::json::select_field_key(field);
111
+
112
+ result.insert(json_key, json_value);
113
+ }
114
+
115
+ all_results.push(serde_json::Value::Object(result));
116
+
117
+ continue;
118
+ }
119
+
120
+ all_results.push(yerba::json::yaml_to_json(&value));
121
+ }
122
+ }
123
+
124
+ if self.raw {
125
+ for value in &all_results {
126
+ match value {
127
+ serde_json::Value::String(string) => println!("{}", string),
128
+ serde_json::Value::Null => println!("null"),
129
+ serde_json::Value::Bool(boolean) => println!("{}", boolean),
130
+ serde_json::Value::Number(number) => println!("{}", number),
131
+ _ => println!("{}", serde_json::to_string(value).unwrap_or_default()),
132
+ }
133
+ }
134
+ } else if all_results.len() == 1 {
135
+ println!(
136
+ "{}",
137
+ serde_json::to_string_pretty(&all_results[0]).unwrap_or_else(|_| "null".to_string())
138
+ );
139
+ } else {
140
+ println!(
141
+ "{}",
142
+ serde_json::to_string_pretty(&all_results).unwrap_or_else(|_| "[]".to_string())
143
+ );
144
+ }
145
+ }
146
+
147
+ fn resolve_search_scope(
148
+ &self,
149
+ selector: &yerba::Selector,
150
+ condition_path: Option<&yerba::Selector>,
151
+ ) -> (yerba::Selector, Option<yerba::Selector>) {
152
+ if let Some(condition) = condition_path {
153
+ if condition.is_relative() {
154
+ let (container, field) = selector.split_at_last_bracket();
155
+
156
+ if container.is_empty() {
157
+ return (selector.clone(), None);
158
+ }
159
+
160
+ let extract = if field.is_empty() { None } else { Some(field) };
161
+
162
+ return (container, extract);
163
+ } else {
164
+ let (container, _) = condition.split_at_first_bracket();
165
+ let container_string = container.to_selector_string();
166
+ let selector_string = selector.to_selector_string();
167
+
168
+ if selector_string.starts_with(&container_string) {
169
+ let rest = selector_string[container_string.len()..].trim_start_matches('.');
170
+
171
+ if rest.is_empty() {
172
+ return (container, None);
173
+ }
174
+
175
+ return (container, Some(yerba::Selector::parse(&format!(".{}", rest))));
176
+ }
177
+
178
+ return (container, None);
179
+ }
180
+ }
181
+
182
+ if selector.has_brackets() && !selector.ends_with_bracket() {
183
+ let (container, field) = selector.split_at_last_bracket();
184
+
185
+ if !container.is_empty() {
186
+ let extract = if field.is_empty() { None } else { Some(field) };
187
+
188
+ return (container, extract);
189
+ }
190
+ }
191
+
192
+ (selector.clone(), None)
193
+ }
194
+ }
@@ -0,0 +1,89 @@
1
+ use std::path::Path;
2
+ use std::process;
3
+
4
+ use indoc::indoc;
5
+
6
+ use super::color::*;
7
+
8
+ pub fn run() {
9
+ let filename = "Yerbafile";
10
+
11
+ if Path::new(filename).exists() {
12
+ eprintln!("🧉 {YELLOW}{BOLD}Yerbafile already exists.{RESET}");
13
+ process::exit(1);
14
+ }
15
+
16
+ let content = indoc! {r#"
17
+ # rules:
18
+ # - files: "**/*.yml"
19
+ # pipeline:
20
+ #
21
+ # # Enforce quote style on values and keys
22
+ # - quote_style:
23
+ # key_style: plain # plain, single, double
24
+ # value_style: double # plain, single, double
25
+ #
26
+ # # Override quote style for a specific selector
27
+ # - quote_style:
28
+ # path: "[].speakers"
29
+ # value_style: plain
30
+ #
31
+ # # Sort keys in a predefined order (aborts on unknown keys)
32
+ # - sort_keys:
33
+ # path: "[]"
34
+ # order:
35
+ # - id
36
+ # - title
37
+ # - description
38
+ #
39
+ # # Sort items in a sequence by field(s)
40
+ # - sort:
41
+ # path: ""
42
+ # by: name
43
+ # # case_sensitive: false
44
+ #
45
+ # # Enforce blank lines between sequence entries
46
+ # - blank_lines:
47
+ # count: 1
48
+ #
49
+ # # Set a value (with optional condition)
50
+ # - set:
51
+ # path: "status"
52
+ # value: "published"
53
+ # # condition: ".draft == true"
54
+ #
55
+ # # Insert a new key or sequence item
56
+ # - insert:
57
+ # path: "tags"
58
+ # value: "yaml"
59
+ # # condition: ".tags not_contains yaml"
60
+ #
61
+ # # Delete a key (with optional condition)
62
+ # - delete:
63
+ # path: "deprecated_field"
64
+ # # condition: ".deprecated_field == true"
65
+ #
66
+ # # Rename a key
67
+ # - rename:
68
+ # from: "old_name"
69
+ # to: "new_name"
70
+ #
71
+ # # Remove an item from a sequence
72
+ # - remove:
73
+ # path: "tags"
74
+ # value: "obsolete"
75
+ #
76
+ # # Get a value and store it as a variable
77
+ # - get:
78
+ # path: "[].name"
79
+ # as: "names"
80
+ # # file: "other_file.yml"
81
+ "#};
82
+
83
+ std::fs::write(filename, content).unwrap_or_else(|error| {
84
+ eprintln!("{RED}Error:{RESET} writing Yerbafile: {}", error);
85
+ process::exit(1);
86
+ });
87
+
88
+ eprintln!("🧉 {GREEN}{BOLD}Created Yerbafile{RESET}");
89
+ }
@@ -0,0 +1,106 @@
1
+ use std::sync::LazyLock;
2
+
3
+ use indoc::indoc;
4
+
5
+ use super::colorize_examples;
6
+ use super::{output, parse_file, run_op};
7
+
8
+ static EXAMPLES: LazyLock<String> = LazyLock::new(|| {
9
+ colorize_examples(indoc! {r#"
10
+ yerba insert config.yml "database.ssl" true
11
+ yerba insert config.yml "database.ssl" true --after "host"
12
+ yerba insert config.yml "database.ssl" true --before "port"
13
+ yerba insert config.yml "tags" "yaml"
14
+ yerba insert config.yml "tags" "yaml" --at 0
15
+ yerba insert config.yml "tags" "yaml" --after "ruby"
16
+ yerba insert speakers.yml "" "name: Bob" --after ".name == Alice"
17
+ yerba insert videos.yml "[0].speakers" "Diana" --before ".name == Charlie"
18
+ yerba insert videos.yml "" --from "new_talk.yml" --after ".id == first-talk"
19
+ "#})
20
+ });
21
+
22
+ #[derive(clap::Args)]
23
+ #[command(
24
+ about = "Insert a new key into a map or item into a sequence",
25
+ arg_required_else_help = true,
26
+ after_help = EXAMPLES.as_str()
27
+ )]
28
+ pub struct Args {
29
+ file: String,
30
+ selector: String,
31
+ value: Option<String>,
32
+ #[arg(long, help = "Read value from a file (use - for stdin)")]
33
+ from: Option<String>,
34
+ #[arg(long, help = "Insert before this value or condition (e.g. \".name == Alice\")")]
35
+ before: Option<String>,
36
+ #[arg(long, help = "Insert after this value or condition (e.g. \".name == Alice\")")]
37
+ after: Option<String>,
38
+ #[arg(long)]
39
+ at: Option<usize>,
40
+ #[arg(long)]
41
+ dry_run: bool,
42
+ }
43
+
44
+ impl Args {
45
+ pub fn run(self) {
46
+ let resolved_value = if let Some(from_path) = self.from {
47
+ if from_path == "-" {
48
+ use std::io::Read;
49
+ let mut buffer = String::new();
50
+
51
+ std::io::stdin().read_to_string(&mut buffer).unwrap_or_else(|error| {
52
+ use super::color::*;
53
+ eprintln!("{RED}Error:{RESET} reading stdin: {}", error);
54
+
55
+ std::process::exit(1);
56
+ });
57
+
58
+ buffer.trim().to_string()
59
+ } else {
60
+ std::fs::read_to_string(&from_path)
61
+ .unwrap_or_else(|error| {
62
+ use super::color::*;
63
+ eprintln!("{RED}Error:{RESET} reading {}: {}", from_path, error);
64
+ std::process::exit(1);
65
+ })
66
+ .trim()
67
+ .to_string()
68
+ }
69
+ } else if let Some(val) = self.value {
70
+ val
71
+ } else {
72
+ use super::color::*;
73
+ eprintln!("{RED}Error:{RESET} either a value argument or --from is required");
74
+
75
+ std::process::exit(1);
76
+ };
77
+
78
+ let parent_path = self.selector.rsplit_once('.').map(|(parent, _)| parent).unwrap_or("");
79
+
80
+ let position = if let Some(index) = self.at {
81
+ yerba::InsertPosition::At(index)
82
+ } else if let Some(target) = self.before {
83
+ if target.starts_with('.') {
84
+ yerba::InsertPosition::BeforeCondition(target)
85
+ } else {
86
+ yerba::InsertPosition::Before(target)
87
+ }
88
+ } else if let Some(target) = self.after {
89
+ if target.starts_with('.') {
90
+ yerba::InsertPosition::AfterCondition(target)
91
+ } else {
92
+ yerba::InsertPosition::After(target)
93
+ }
94
+ } else {
95
+ yerba::Yerbafile::find()
96
+ .and_then(|yerbafile_path| yerba::Yerbafile::load(&yerbafile_path).ok())
97
+ .and_then(|yerbafile| yerbafile.sort_order_for(&self.file, parent_path))
98
+ .map(yerba::InsertPosition::FromSortOrder)
99
+ .unwrap_or(yerba::InsertPosition::Last)
100
+ };
101
+
102
+ let mut document = parse_file(&self.file);
103
+ run_op(|| document.insert_into(&self.selector, &resolved_value, position));
104
+ output(&self.file, &document, self.dry_run);
105
+ }
106
+ }
@@ -0,0 +1,55 @@
1
+ use super::color::*;
2
+
3
+ pub fn run() {
4
+ let g = GREEN;
5
+ let b = BOLD;
6
+ let d = DIM;
7
+ let i = "\x1b[3m"; // italic
8
+ let y = YELLOW;
9
+ let r = RESET;
10
+ let hr = format!(" {d}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}{r}");
11
+
12
+ println!();
13
+ println!(" {b}{g}\u{1f9c9} YERBA MATE{r}");
14
+ println!();
15
+ println!(" {d}From the Guarani people of South America to the world.{r}");
16
+ println!(" {d}Brewed from the leaves of{r} {i}Ilex paraguariensis{r}{d}, shared{r}");
17
+ println!(" {d}in a hollowed calabaza, sipped through a metal bombilla.{r}");
18
+ println!(" {d}One cup, many rounds.{r}");
19
+ println!();
20
+ println!("{hr}");
21
+ println!();
22
+ println!(" {b}{g}yerba{r} {d}/\u{02c8}\u{0292}\u{025b}\u{027e}.ba/{r} {y}noun{r}");
23
+ println!(" The dried leaves of {i}Ilex paraguariensis{r}, used to brew");
24
+ println!(" mate. From Guarani {i}ka'a{r}, meaning \"herb\".");
25
+ println!();
26
+ println!(" {b}{g}mate{r} {d}/\u{02c8}ma.te/{r} {y}noun{r}");
27
+ println!(" A traditional South American caffeine-rich infusion.");
28
+ println!(" Also the hollowed calabaza (gourd) from which it is drunk.");
29
+ println!();
30
+ println!(" {b}{g}bombilla{r} {d}/bom\u{02c8}bi.\u{0292}a/{r} {y}noun{r}");
31
+ println!(" A metal straw with a filtered tip at the bottom, used");
32
+ println!(" to sip mate without swallowing the leaves.");
33
+ println!();
34
+ println!(" {b}{g}cebador{r} {d}/se.ba\u{02c8}\u{00f0}o\u{027e}/{r} {y}noun{r}");
35
+ println!(" The person who prepares and serves mate to the group.");
36
+ println!(" A role of care, not hierarchy.");
37
+ println!();
38
+ println!(" {b}{g}ronda{r} {d}/\u{02c8}ron.da/{r} {y}noun{r}");
39
+ println!(" The circle of people sharing mate. The cup passes");
40
+ println!(" from hand to hand until it returns to the cebador.");
41
+ println!();
42
+ println!(" {b}{g}cebar{r} {d}/se\u{02c8}ba\u{027e}/{r} {y}verb{r}");
43
+ println!(" To pour hot water over the yerba and serve a round.");
44
+ println!(" The act of preparing each individual serving.");
45
+ println!();
46
+ println!(" {b}{g}aprontar{r} {d}/ap\u{027e}on\u{02c8}ta\u{027e}/{r} {y}verb{r}");
47
+ println!(" To set up mate before the first pour: arranging the");
48
+ println!(" yerba, heating the water, positioning the bombilla.");
49
+ println!();
50
+ println!(" {b}{g}ensillar{r} {d}/ensi\u{02c8}\u{0292}a\u{027e}/{r} {y}verb{r}");
51
+ println!(" To replace spent yerba with fresh leaves mid-session,");
52
+ println!(" extending the life of the mate.");
53
+ println!();
54
+ println!("{hr}");
55
+ }