yerba 0.9.0-arm-linux-gnu → 0.9.2-arm-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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 24b7bd0ed1bbddf7baeaff2439c5d7523a4f0ff9f1dba874f50688def682e8e1
4
- data.tar.gz: c6190d6d5b0d6808fc5f3a6350a3dbb3ec0a97efb3f66752ccfec047f2bf4a4a
3
+ metadata.gz: 8b49bb4f346300a0924bc402f297040f7e95d185eec8e58d447c006c841ccdf8
4
+ data.tar.gz: a66cdd879c698a2de753646009639a7a1175d738c281dea1e5ff7a291d2d40d5
5
5
  SHA512:
6
- metadata.gz: 754f91df951d8d46fcf12e0b939d4b10c21dc53fb837d16f77f88f1de3fbfa8b99635ec049df2c049be890583743f78c616196a76ca375d41af938a36e9a2e12
7
- data.tar.gz: 3d57c9f9e5ff42f1b294c347939f8abdd82a98a04fcad712169e58c50104ec1adbbc81af4930a7a08e332ce33329e54f8d2f1894f0f2cf1ceb78d55005ce60d5
6
+ metadata.gz: c3ce02a7e1f57ee29ab86d3880ba0088510d3026277df6677bb1dd173e9e6bb6b2ed5052a6d32c9a860afbab4a17f508a1c989c894ed50cf64e80b2f02fbb8c1
7
+ data.tar.gz: c60175d27fd5e6d87d54ef16d7a6e6c4aa81f85c57edfb4948e9c4bef8894c4e1d740fa0047f6459dd7d8ba8cfd856c3ca7b50cfef9a6a67202ecbae3d8c1c9e
data/README.md CHANGED
@@ -1165,6 +1165,33 @@ scalars.first.value = "New Title"
1165
1165
  scalars.first.document.save!
1166
1166
  ```
1167
1167
 
1168
+ ### Parsing Selectors
1169
+
1170
+ `Yerba::Selector` splits a selector into its segments, using the same parser as the rest of Yerba. Keys are Strings, indexes Integers, `[]` is `:all_items` and `*` is `:all_keys`:
1171
+
1172
+ ```ruby
1173
+ selector = Yerba::Selector.parse("[3].talks[1]")
1174
+
1175
+ selector.segments # => [3, "talks", 1]
1176
+ selector.parent # => "[3].talks"
1177
+ selector.join("speakers", 0).to_s # => "[3].talks[1].speakers[0]"
1178
+
1179
+ Yerba::Selector.parse("[].speakers[]").segments # => [:all_items, "speakers", :all_items]
1180
+ Yerba::Selector.parse(".title").relative? # => true
1181
+
1182
+ Yerba::Selector.build([3, "talks", 1]).to_s # => "[3].talks[1]"
1183
+ ```
1184
+
1185
+ `dig` looks a concrete selector up in plain Ruby values, such as the cached `Document#values`, without calling into the native extension. Use it when reading many values from the same document:
1186
+
1187
+ ```ruby
1188
+ values = document.values
1189
+
1190
+ Yerba::Selector.parse("[3].talks[1].title").dig(values) # => same as document.value_at("[3].talks[1].title")
1191
+ ```
1192
+
1193
+ Parsed selectors are cached per string, so parsing the same selector again is free.
1194
+
1168
1195
  ### Saving
1169
1196
 
1170
1197
  Write changes back to the original file:
Binary file
@@ -68,6 +68,8 @@ struct YerbaParseResult yerba_document_parse_file(const char *path);
68
68
 
69
69
  struct YerbaParseResult yerba_document_parse(const char *content);
70
70
 
71
+ uint64_t yerba_document_generation(const struct Document *document);
72
+
71
73
  void yerba_document_free(struct Document *document);
72
74
 
73
75
  struct YerbaGetResult yerba_document_get(const struct Document *document, const char *path);
@@ -91,6 +93,19 @@ char *yerba_document_resolve_selectors(const struct Document *document, const ch
91
93
 
92
94
  bool yerba_selector_has_wildcard(const char *path);
93
95
 
96
+ /**
97
+ * Parses a selector into JSON: `{"relative": bool, "segments": [...]}`.
98
+ *
99
+ * Keys are encoded as strings, indexes as integers, `[]` as `{"all": "items"}` and `*` as `{"all": "keys"}`.
100
+ */
101
+ char *yerba_selector_parse(const char *path);
102
+
103
+ /**
104
+ * Builds a selector string from a JSON array of segments, encoded like `yerba_selector_parse` returns them.
105
+ */
106
+ char *yerba_selector_build(const char *segments_json,
107
+ bool relative);
108
+
94
109
  char *yerba_document_keys(const struct Document *document, const char *path);
95
110
 
96
111
  char *yerba_document_get_quote_style(const struct Document *document, const char *path);
data/ext/yerba/yerba.c CHANGED
@@ -339,6 +339,23 @@ static VALUE document_get_value(VALUE self, VALUE path) {
339
339
  return rb_funcall(rb_path2class("JSON"), rb_intern("parse"), 1, json_string);
340
340
  }
341
341
 
342
+ /* document.values → the whole document as Ruby objects, cached until the document changes */
343
+ static VALUE document_values(VALUE self) {
344
+ struct Document *document = get_document(self);
345
+ VALUE generation = ULL2NUM(yerba_document_generation(document));
346
+
347
+ if (rb_equal(rb_ivar_get(self, rb_intern("values_generation")), generation)) {
348
+ return rb_ivar_get(self, rb_intern("values"));
349
+ }
350
+
351
+ VALUE values = document_get_value(self, make_utf8_string(""));
352
+
353
+ rb_ivar_set(self, rb_intern("values"), values);
354
+ rb_ivar_set(self, rb_intern("values_generation"), generation);
355
+
356
+ return values;
357
+ }
358
+
342
359
  /* document.selectors → ["database", "database.host", "tags", "tags[]", ...] */
343
360
  static VALUE document_selectors(VALUE self) {
344
361
  struct Document *document = get_document(self);
@@ -1150,6 +1167,30 @@ static VALUE collection_s_find(int argc, VALUE *argv, VALUE self) {
1150
1167
  return rb_funcall(rb_path2class("JSON"), rb_intern("parse"), 1, json_string);
1151
1168
  }
1152
1169
 
1170
+ /* Selector.native_parse(selector) → {"relative" => bool, "segments" => [...]} */
1171
+ static VALUE selector_s_native_parse(VALUE klass, VALUE selector) {
1172
+ char *json = yerba_selector_parse(StringValueCStr(selector));
1173
+
1174
+ if (!json) return Qnil;
1175
+
1176
+ VALUE json_string = make_utf8_string(json);
1177
+ yerba_string_free(json);
1178
+
1179
+ return rb_funcall(rb_path2class("JSON"), rb_intern("parse"), 1, json_string);
1180
+ }
1181
+
1182
+ /* Selector.native_build(segments_json, relative) → selector string */
1183
+ static VALUE selector_s_native_build(VALUE klass, VALUE segments_json, VALUE relative) {
1184
+ char *result = yerba_selector_build(StringValueCStr(segments_json), RTEST(relative));
1185
+
1186
+ if (!result) return rb_str_new_cstr("");
1187
+
1188
+ VALUE selector = make_utf8_string(result);
1189
+ yerba_string_free(result);
1190
+
1191
+ return selector;
1192
+ }
1193
+
1153
1194
  /* Yerbafile.locate(directory = nil) → path string or nil */
1154
1195
  static VALUE yerbafile_s_locate(int argc, VALUE *argv, VALUE klass) {
1155
1196
  VALUE directory;
@@ -1176,6 +1217,10 @@ void Init_yerba(void) {
1176
1217
  rb_eParseError = rb_define_class_under(rb_mYerba, "ParseError", rb_eError);
1177
1218
  rb_ePathValidationError = rb_define_class_under(rb_mYerba, "PathValidationError", rb_eError);
1178
1219
 
1220
+ VALUE rb_cSelector = rb_define_class_under(rb_mYerba, "Selector", rb_cObject);
1221
+ rb_define_private_method(rb_singleton_class(rb_cSelector), "native_parse", selector_s_native_parse, 1);
1222
+ rb_define_private_method(rb_singleton_class(rb_cSelector), "native_build", selector_s_native_build, 2);
1223
+
1179
1224
  VALUE rb_cCollection = rb_define_class_under(rb_mYerba, "Collection", rb_cObject);
1180
1225
  rb_define_singleton_method(rb_cCollection, "get", collection_s_get, 2);
1181
1226
  rb_define_singleton_method(rb_cCollection, "find", collection_s_find, -1);
@@ -1189,6 +1234,7 @@ void Init_yerba(void) {
1189
1234
  rb_define_method(rb_cDocument, "[]", document_bracket, 1);
1190
1235
  rb_define_method(rb_cDocument, "node_at", document_bracket, 1);
1191
1236
  rb_define_method(rb_cDocument, "value_at", document_get_value, 1);
1237
+ rb_define_method(rb_cDocument, "values", document_values, 0);
1192
1238
  rb_define_method(rb_cDocument, "get_all", document_get_all, 1);
1193
1239
  rb_define_method(rb_cDocument, "location", document_location, -1);
1194
1240
  rb_define_method(rb_cDocument, "locations", document_locations, 1);
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Yerba
6
+ # A parsed selector, split into its segments.
7
+ #
8
+ # Yerba::Selector.parse("[3].talks[1]").segments # => [3, "talks", 1]
9
+ # Yerba::Selector.parse("[].speakers[]").segments # => [:all_items, "speakers", :all_items]
10
+ # Yerba::Selector.parse("database.*").segments # => ["database", :all_keys]
11
+ # Yerba::Selector.parse(".title").relative? # => true
12
+ #
13
+ # Keys are Strings, indexes Integers, `[]` is `:all_items` and `*` is `:all_keys`.
14
+ #
15
+ class Selector
16
+ ALL_KEYS = :all_keys
17
+ ALL_ITEMS = :all_items
18
+
19
+ attr_reader :segments
20
+
21
+ def self.parse(selector)
22
+ return selector if selector.is_a?(Selector)
23
+
24
+ string = selector.to_s
25
+
26
+ cache[string] ||= begin
27
+ parsed = native_parse(string)
28
+
29
+ new(parsed["segments"].map { |segment| decode(segment) }, relative: parsed["relative"], source: string)
30
+ end
31
+ end
32
+
33
+ # Builds a selector from segments: Strings (keys), Integers (indexes), `:all_items` or `:all_keys`.
34
+ #
35
+ # Yerba::Selector.build([3, "talks", 1]).to_s # => "[3].talks[1]"
36
+ #
37
+ def self.build(segments, relative: false)
38
+ new(segments, relative: relative)
39
+ end
40
+
41
+ def self.clear_cache!
42
+ @cache = nil
43
+ end
44
+
45
+ def self.cache
46
+ @cache ||= {}
47
+ end
48
+ private_class_method :cache
49
+
50
+ def self.decode(segment)
51
+ case segment
52
+ when Hash
53
+ segment["all"] == "keys" ? ALL_KEYS : ALL_ITEMS
54
+ else
55
+ segment
56
+ end
57
+ end
58
+ private_class_method :decode
59
+
60
+ def initialize(segments, relative: false, source: nil)
61
+ @segments = segments.dup.freeze
62
+ @relative = relative
63
+ @source = source
64
+
65
+ freeze
66
+ end
67
+
68
+ def relative?
69
+ @relative
70
+ end
71
+
72
+ def absolute?
73
+ !@relative
74
+ end
75
+
76
+ def empty?
77
+ segments.empty?
78
+ end
79
+
80
+ def wildcard?
81
+ segments.any? { |segment| [ALL_ITEMS, ALL_KEYS].include?(segment) }
82
+ end
83
+
84
+ # "[3].talks[1]" => "[3].talks"
85
+ def parent
86
+ self.class.build(segments[0...-1], relative: relative?)
87
+ end
88
+
89
+ # Selector.parse("[3]").join("talks", 1) => "[3].talks[1]"
90
+ def join(*more)
91
+ self.class.build(segments + more, relative: relative?)
92
+ end
93
+
94
+ def dig(values)
95
+ raise ArgumentError, "Cannot dig a wildcard selector (#{self}), use Document#find or Collection.find instead" if wildcard?
96
+
97
+ segments.reduce(values) do |current, segment|
98
+ case current
99
+ when Array
100
+ segment.is_a?(Integer) ? current[segment] : nil
101
+ when Hash
102
+ segment.is_a?(Integer) ? nil : current[segment]
103
+ end
104
+ end
105
+ end
106
+
107
+ def to_s
108
+ @source || self.class.send(:native_build, JSON.generate(encoded_segments), relative?)
109
+ end
110
+ alias to_str to_s
111
+
112
+ def ==(other)
113
+ other.is_a?(Selector) && other.segments == segments && other.relative? == relative?
114
+ end
115
+ alias eql? ==
116
+
117
+ def hash
118
+ [self.class, segments, relative?].hash
119
+ end
120
+
121
+ def inspect
122
+ "#<#{self.class.name} #{to_s.inspect} #{segments.inspect}>"
123
+ end
124
+
125
+ private
126
+
127
+ def encoded_segments
128
+ segments.map do |segment|
129
+ case segment
130
+ when ALL_ITEMS
131
+ { all: "items" }
132
+ when ALL_KEYS
133
+ { all: "keys" }
134
+ else
135
+ segment
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
data/lib/yerba/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Yerba
4
- VERSION = "0.9.0"
4
+ VERSION = "0.9.2"
5
5
  end
data/lib/yerba.rb CHANGED
@@ -4,6 +4,7 @@ require "fileutils"
4
4
 
5
5
  require_relative "yerba/version"
6
6
  require_relative "yerba/location"
7
+ require_relative "yerba/selector"
7
8
  require_relative "yerba/formatting"
8
9
  require_relative "yerba/node"
9
10
  require_relative "yerba/scalar"
data/rust/Cargo.lock CHANGED
@@ -1431,7 +1431,7 @@ dependencies = [
1431
1431
 
1432
1432
  [[package]]
1433
1433
  name = "yerba"
1434
- version = "0.9.0"
1434
+ version = "0.9.2"
1435
1435
  dependencies = [
1436
1436
  "anstyle-query",
1437
1437
  "cbindgen",
data/rust/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "yerba"
3
- version = "0.9.0"
3
+ version = "0.9.2"
4
4
  edition = "2021"
5
5
  authors = ["Marco Roth <marco.roth@intergga.ch>"]
6
6
  description = "YAML Editing and Refactoring with Better Accuracy"
@@ -34,6 +34,7 @@ pub struct Args {
34
34
  /// Comma-separated map keys to separate from what follows them
35
35
  #[arg(long)]
36
36
  after: Option<String>,
37
+ /// Leave entries alone when their value is null, an empty collection, or an empty string
37
38
  #[arg(long)]
38
39
  skip_empty: bool,
39
40
  #[arg(long)]
@@ -31,6 +31,7 @@ pub struct LocatedNode {
31
31
 
32
32
  use std::fs;
33
33
  use std::path::{Path, PathBuf};
34
+ use std::sync::atomic::{AtomicU64, Ordering};
34
35
 
35
36
  use rowan::ast::AstNode;
36
37
  use rowan::{TextRange, TextSize};
@@ -126,10 +127,13 @@ pub enum InsertPosition {
126
127
  FromSortOrder(Vec<String>),
127
128
  }
128
129
 
130
+ static NEXT_GENERATION: AtomicU64 = AtomicU64::new(1);
131
+
129
132
  #[derive(Debug)]
130
133
  pub struct Document {
131
134
  root: SyntaxNode,
132
135
  path: Option<PathBuf>,
136
+ generation: u64,
133
137
  }
134
138
 
135
139
  impl Document {
@@ -138,7 +142,15 @@ impl Document {
138
142
 
139
143
  check_duplicate_keys(&tree)?;
140
144
 
141
- Ok(Document { root: tree, path: None })
145
+ Ok(Document {
146
+ root: tree,
147
+ path: None,
148
+ generation: NEXT_GENERATION.fetch_add(1, Ordering::Relaxed),
149
+ })
150
+ }
151
+
152
+ pub fn generation(&self) -> u64 {
153
+ self.generation
142
154
  }
143
155
 
144
156
  pub fn parse_file(path: impl AsRef<Path>) -> Result<Self, YerbaError> {
data/rust/src/ffi.rs CHANGED
@@ -217,6 +217,11 @@ const EMPTY_LOCATION: YerbaLocation = YerbaLocation {
217
217
  end_column: 0,
218
218
  };
219
219
 
220
+ #[no_mangle]
221
+ pub unsafe extern "C" fn yerba_document_generation(document: *const Document) -> u64 {
222
+ (*document).generation()
223
+ }
224
+
220
225
  #[no_mangle]
221
226
  pub unsafe extern "C" fn yerba_document_free(document: *mut Document) {
222
227
  if !document.is_null() {
@@ -337,6 +342,64 @@ pub unsafe extern "C" fn yerba_selector_has_wildcard(path: *const c_char) -> boo
337
342
  crate::selector::Selector::parse(selector_string).has_wildcard()
338
343
  }
339
344
 
345
+ /// Parses a selector into JSON: `{"relative": bool, "segments": [...]}`.
346
+ ///
347
+ /// Keys are encoded as strings, indexes as integers, `[]` as `{"all": "items"}` and `*` as `{"all": "keys"}`.
348
+ #[no_mangle]
349
+ pub unsafe extern "C" fn yerba_selector_parse(path: *const c_char) -> *mut c_char {
350
+ use crate::selector::{Selector, SelectorSegment};
351
+
352
+ let selector_string = CStr::from_ptr(path).to_str().unwrap_or("");
353
+ let selector = Selector::parse(selector_string);
354
+
355
+ let segments: Vec<serde_json::Value> = selector
356
+ .segments()
357
+ .iter()
358
+ .map(|segment| match segment {
359
+ SelectorSegment::Key(key) => serde_json::json!(key),
360
+ SelectorSegment::Index(index) => serde_json::json!(index),
361
+ SelectorSegment::AllItems => serde_json::json!({ "all": "items" }),
362
+ SelectorSegment::AllKeys => serde_json::json!({ "all": "keys" }),
363
+ })
364
+ .collect();
365
+
366
+ let json = serde_json::json!({ "relative": selector.is_relative(), "segments": segments });
367
+
368
+ CString::new(json.to_string()).unwrap_or_default().into_raw()
369
+ }
370
+
371
+ /// Builds a selector string from a JSON array of segments, encoded like `yerba_selector_parse` returns them.
372
+ #[no_mangle]
373
+ pub unsafe extern "C" fn yerba_selector_build(segments_json: *const c_char, relative: bool) -> *mut c_char {
374
+ use crate::selector::{Selector, SelectorSegment};
375
+
376
+ let json_string = CStr::from_ptr(segments_json).to_str().unwrap_or("[]");
377
+ let values: Vec<serde_json::Value> = serde_json::from_str(json_string).unwrap_or_default();
378
+
379
+ let segments: Vec<SelectorSegment> = values
380
+ .iter()
381
+ .filter_map(|value| match value {
382
+ serde_json::Value::String(key) => Some(SelectorSegment::Key(key.clone())),
383
+ serde_json::Value::Number(number) => number.as_u64().map(|index| SelectorSegment::Index(index as usize)),
384
+ serde_json::Value::Object(object) => match object.get("all").and_then(|all| all.as_str()) {
385
+ Some("items") => Some(SelectorSegment::AllItems),
386
+ Some("keys") => Some(SelectorSegment::AllKeys),
387
+ _ => None,
388
+ },
389
+ _ => None,
390
+ })
391
+ .collect();
392
+
393
+ let selector_string = Selector::Absolute(segments).to_selector_string();
394
+ let selector_string = if relative && !selector_string.is_empty() {
395
+ format!(".{}", selector_string)
396
+ } else {
397
+ selector_string
398
+ };
399
+
400
+ CString::new(selector_string).unwrap_or_default().into_raw()
401
+ }
402
+
340
403
  #[no_mangle]
341
404
  pub unsafe extern "C" fn yerba_document_keys(document: *const Document, path: *const c_char) -> *mut c_char {
342
405
  let document = &*document;
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yerba
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.9.2
5
5
  platform: arm-linux-gnu
6
6
  authors:
7
7
  - Marco Roth
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-04 00:00:00.000000000 Z
11
+ date: 2026-10-13 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A CLI tool for editing YAML while preserving structure, comments, and
14
14
  format.
@@ -39,6 +39,7 @@ files:
39
39
  - lib/yerba/node.rb
40
40
  - lib/yerba/query_result.rb
41
41
  - lib/yerba/scalar.rb
42
+ - lib/yerba/selector.rb
42
43
  - lib/yerba/sequence.rb
43
44
  - lib/yerba/version.rb
44
45
  - lib/yerba/yerbafile.rb