yerba 0.9.1-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: c707c7ffe315229191bf5abdcb11a4cec4c15fee923526a3a421d25153cb4dbb
4
- data.tar.gz: 88005dc082d0dd365c9afdab234524500e4c9cb09f4744b405f4dc34c95857fd
3
+ metadata.gz: 8b49bb4f346300a0924bc402f297040f7e95d185eec8e58d447c006c841ccdf8
4
+ data.tar.gz: a66cdd879c698a2de753646009639a7a1175d738c281dea1e5ff7a291d2d40d5
5
5
  SHA512:
6
- metadata.gz: da39f67f8e2b890eff454dcc40fc982b60ad4625a4e9b8eb474251de69fa8143af74443ce71156ecde9ba98a50091ed67b0fa7ecf008bda2bc0fa0f761811f65
7
- data.tar.gz: 4cc8faf84db4bb2dfa266f29697f2177c5c63f4d4ea0cc8125c0514fef526d59b9c95a1d564b0c1099a093aee3b1082493584fb54466a7170789b006880b633f
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
@@ -93,6 +93,19 @@ char *yerba_document_resolve_selectors(const struct Document *document, const ch
93
93
 
94
94
  bool yerba_selector_has_wildcard(const char *path);
95
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
+
96
109
  char *yerba_document_keys(const struct Document *document, const char *path);
97
110
 
98
111
  char *yerba_document_get_quote_style(const struct Document *document, const char *path);
data/ext/yerba/yerba.c CHANGED
@@ -1167,6 +1167,30 @@ static VALUE collection_s_find(int argc, VALUE *argv, VALUE self) {
1167
1167
  return rb_funcall(rb_path2class("JSON"), rb_intern("parse"), 1, json_string);
1168
1168
  }
1169
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
+
1170
1194
  /* Yerbafile.locate(directory = nil) → path string or nil */
1171
1195
  static VALUE yerbafile_s_locate(int argc, VALUE *argv, VALUE klass) {
1172
1196
  VALUE directory;
@@ -1193,6 +1217,10 @@ void Init_yerba(void) {
1193
1217
  rb_eParseError = rb_define_class_under(rb_mYerba, "ParseError", rb_eError);
1194
1218
  rb_ePathValidationError = rb_define_class_under(rb_mYerba, "PathValidationError", rb_eError);
1195
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
+
1196
1224
  VALUE rb_cCollection = rb_define_class_under(rb_mYerba, "Collection", rb_cObject);
1197
1225
  rb_define_singleton_method(rb_cCollection, "get", collection_s_get, 2);
1198
1226
  rb_define_singleton_method(rb_cCollection, "find", collection_s_find, -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.1"
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.1"
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.1"
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"
data/rust/src/ffi.rs CHANGED
@@ -342,6 +342,64 @@ pub unsafe extern "C" fn yerba_selector_has_wildcard(path: *const c_char) -> boo
342
342
  crate::selector::Selector::parse(selector_string).has_wildcard()
343
343
  }
344
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
+
345
403
  #[no_mangle]
346
404
  pub unsafe extern "C" fn yerba_document_keys(document: *const Document, path: *const c_char) -> *mut c_char {
347
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.1
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-10-12 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