yerba 0.3.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 477663fa314d01cbe3c98e8a5a8bdc08c0e26d287bff36d23ae1eea519e81609
4
- data.tar.gz: 3a9ce13df82bbebd5cbbfc7590d0d334e841c4314bb059f31047ba707bd9bb1e
3
+ metadata.gz: 8e77d3cce1f16b9adf15e44b0dec51f2182ab12eeeacea0e0d6d1223d7a1f23b
4
+ data.tar.gz: 7abd1edd2196ce73d22dab4b05dbb2f2af47cb04e1cff756bd8df5a678dcbe9e
5
5
  SHA512:
6
- metadata.gz: be2932fad743bdf806070d5898f73533e8225994dc8d6581b0f47c5ee79519a8cfc83fd4f88cae4607fe5e36ee85f2cc165f288bbeca0184a9756fe18e7b3b27
7
- data.tar.gz: 6251a8c8d261c458781e48a2adc8364172bd999c4ecbcc8e38ba625cf9a2434285efabd96dbfb64eabf78791f5a6a187cb76114bfc37facce78d48aa782696cf
6
+ metadata.gz: 3d22690ae793eb147848050793e22eeeeb8614d942d258e685b4bbea3094d0c118ac72d93c4beb56044bad8d7a3ce9a37947fac8870c302755d4777075a191ca
7
+ data.tar.gz: 3c4d9c7159d665b55355c965f5f2a41b5962b17dde6f29aa5a57006e33fc9b64c987fe833872e102d5bf4882eb11673f67c1001d857011213b2012ce5648344a
data/README.md CHANGED
@@ -47,13 +47,15 @@ Use `yerba` as a library in your Rust project:
47
47
 
48
48
  ```toml
49
49
  [dependencies]
50
- yerba = "0.3"
50
+ yerba = "0.4"
51
51
  ```
52
52
 
53
53
  ```rust
54
54
  let mut document = yerba::parse_file("config.yml")?;
55
55
  document.set("database.host", "0.0.0.0")?;
56
- document.save()?;
56
+
57
+ document.save()?; // saves to original path
58
+ document.save_to("output.yml")?; // saves to new path
57
59
  ```
58
60
 
59
61
  ### Ruby Gem
@@ -286,7 +288,7 @@ yerba sort-keys "data/**/videos.yml" "[]" "id,title,speakers"
286
288
 
287
289
  ### `quote-style`
288
290
 
289
- Enforce a consistent quote style across keys and/or values. Available styles are `plain`, `single`, and `double`:
291
+ Enforce a consistent quote style across keys and/or values:
290
292
 
291
293
  ```bash
292
294
  yerba quote-style config.yml --values double
@@ -301,6 +303,36 @@ yerba quote-style config.yml "[].speakers" --values plain
301
303
  yerba quote-style "data/**/*.yml" --keys plain --values double
302
304
  ```
303
305
 
306
+ Use block scalar styles to enforce multiline formatting on specific fields:
307
+
308
+ ```bash
309
+ yerba quote-style videos.yml "[].description" --values literal
310
+ ```
311
+
312
+ **Key styles** (`--keys`):
313
+
314
+ | Style | Symbol | Example |
315
+ |-------|--------|---------|
316
+ | `plain` | — | `host: value` |
317
+ | `single` | `'` | `'host': value` |
318
+ | `double` | `"` | `"host": value` |
319
+
320
+ **Value styles** (`--values`):
321
+
322
+ | Style | Symbol | Example | Behavior |
323
+ |-------|--------|---------|----------|
324
+ | `plain` | — | `host: localhost` | Unquoted |
325
+ | `single` | `'` | `host: 'localhost'` | Single-quoted |
326
+ | `double` | `"` | `host: "localhost"` | Double-quoted, supports `\n` escapes |
327
+ | `literal` | `\|-` | Preserves newlines | Strip trailing newline |
328
+ | `literal-clip` | `\|` | Preserves newlines | Keep one trailing newline |
329
+ | `literal-keep` | `\|+` | Preserves newlines | Keep all trailing newlines |
330
+ | `folded` | `>-` | Folds newlines to spaces | Strip trailing newline |
331
+ | `folded-clip` | `>` | Folds newlines to spaces | Keep one trailing newline |
332
+ | `folded-keep` | `>+` | Folds newlines to spaces | Keep all trailing newlines |
333
+
334
+ Block scalars are only converted when scoped to a specific selector. An unscoped `--values double` will not touch existing block scalars.
335
+
304
336
  ### `blank-lines`
305
337
 
306
338
  Enforce a consistent number of blank lines between sequence entries:
@@ -311,6 +343,16 @@ yerba blank-lines videos.yml "[]" 1
311
343
  yerba blank-lines config.yml "tags" 0
312
344
  ```
313
345
 
346
+ ### `directives`
347
+
348
+ Add or remove the document start marker (`---`):
349
+
350
+ ```bash
351
+ yerba directives config.yml --ensure
352
+ yerba directives config.yml --remove
353
+ yerba directives "data/**/*.yml" --ensure
354
+ ```
355
+
314
356
  ### `selectors`
315
357
 
316
358
  Show all valid selectors for a YAML file. Useful for discovering the structure of a file and knowing which selectors you can use with other commands:
@@ -425,6 +467,7 @@ Available pipeline steps:
425
467
  - `delete` Remove a key (supports conditions)
426
468
  - `rename` Rename a key
427
469
  - `remove` Remove an item from a sequence
470
+ - `directives` Add or remove the document start marker (`---`)
428
471
  - `get` Read a value and store it as a variable for subsequent steps
429
472
 
430
473
  This makes it easy to enforce project-wide YAML conventions in CI:
@@ -514,7 +557,44 @@ tags = document["tags"]
514
557
  tags << "yaml"
515
558
  tags << { name: "Rust", version: "1.80" }
516
559
  tags.remove("obsolete")
517
- tags.sort(by: "name")
560
+ ```
561
+
562
+ ### Sorting
563
+
564
+ Sort sequences in place. Works on both the document and sequence level:
565
+
566
+ ```ruby
567
+ document.sort(by: :name)
568
+ document.sort(by: :name, order: :desc)
569
+ document.sort(by: :name, order: ["Charlie", "Bob", "Alice"])
570
+ document.sort("tags")
571
+ document.sort("tags", order: :desc)
572
+ document.sort("tags", order: ["rust", "ruby", "go"])
573
+ ```
574
+
575
+ The `by:` option accepts symbols, strings, or dot-prefixed strings (`:name`, `"name"`, `".name"`).
576
+
577
+ ### Querying
578
+
579
+ Find and filter items in sequences with `find_by`, `where`, and `pluck`:
580
+
581
+ ```ruby
582
+ document.find_by(name: "Alice")
583
+ document.where(role: "admin")
584
+ document.pluck(:name)
585
+
586
+ document.find_by(speakers: { name: "Alice" })
587
+ document.where(tags: ["ruby"])
588
+ document.find_by("database.host": "localhost")
589
+ ```
590
+
591
+ These methods work on `Document` (delegates to root), `Sequence`, and `Collection` (searches across files):
592
+
593
+ ```ruby
594
+ collection = Yerba.files("data/**/*.yml")
595
+ collection.find_by(name: "Alice")
596
+ collection.where(kind: "talk")
597
+ collection.pluck(:name)
518
598
  ```
519
599
 
520
600
  ### Quote Style Control
@@ -523,7 +603,7 @@ Read and set the quote style on individual scalars:
523
603
 
524
604
  ```ruby
525
605
  scalar = document["database.host"]
526
- scalar.quote_style # => :double
606
+ scalar.quote_style # => :double
527
607
  scalar.quote_style = :single
528
608
  ```
529
609
 
@@ -547,6 +627,10 @@ collection.each do |document|
547
627
  puts document.get("[0].title")
548
628
  end
549
629
 
630
+ collection.find_by(name: "Alice")
631
+ collection.where(kind: "talk")
632
+ collection.pluck(:name)
633
+
550
634
  collection.apply! do |document|
551
635
  document.set("status", "published")
552
636
  end
@@ -572,10 +656,9 @@ After checking out the repo, run `bundle install` to install Ruby dependencies,
572
656
 
573
657
  ### Building from source
574
658
 
575
- The Rust core is in the `rust/` directory:
659
+ The Rust core is in the `rust/` directory, with a workspace `Cargo.toml` at the root so all cargo commands work from the project root:
576
660
 
577
661
  ```bash
578
- cd rust
579
662
  cargo build
580
663
  cargo test
581
664
  ```
@@ -591,7 +674,6 @@ cargo run -- get config.yml "database.host"
591
674
  Or build a release binary:
592
675
 
593
676
  ```bash
594
- cd rust
595
677
  cargo build --release
596
678
  ./target/release/yerba --help
597
679
  ```
data/ext/yerba/extconf.rb CHANGED
@@ -44,22 +44,41 @@ if cross_compiling && target_platform.nil?
44
44
  end
45
45
  end
46
46
 
47
+ workspace_target_dir = File.join(root_dir, "target")
48
+ crate_target_dir = File.join(rust_dir, "target")
49
+
47
50
  if target_platform
48
51
  puts "yerba: Cross-compiling Rust for target: #{target_platform}"
49
52
  system("rustup target add #{target_platform}") || warn("yerba: Failed to add Rust target #{target_platform}")
50
53
 
51
54
  cargo_args = "--release --target #{target_platform}"
52
- lib_dir = File.join(rust_dir, "target", target_platform, "release")
55
+ lib_dir = [workspace_target_dir, crate_target_dir]
56
+ .map { |dir| File.join(dir, target_platform, "release") }
57
+ .find { |dir| Dir.exist?(dir) } || File.join(crate_target_dir, target_platform, "release")
53
58
  else
54
59
  puts "yerba: Compiling Rust library for native platform..."
60
+
55
61
  cargo_args = "--release"
56
- lib_dir = File.join(rust_dir, "target", "release")
62
+
63
+ lib_dir = [workspace_target_dir, crate_target_dir]
64
+ .map { |dir| File.join(dir, "release") }
65
+ .find { |dir| Dir.exist?(dir) } || File.join(crate_target_dir, "release")
57
66
  end
58
67
 
59
- unless system("cd #{rust_dir} && cargo build #{cargo_args}")
68
+ unless system("cd #{root_dir} && cargo build #{cargo_args}")
60
69
  abort "ERROR: Failed to compile yerba from Rust source."
61
70
  end
62
71
 
72
+ lib_dir = if target_platform
73
+ [workspace_target_dir, crate_target_dir]
74
+ .map { |dir| File.join(dir, target_platform, "release") }
75
+ .find { |dir| Dir.exist?(dir) } || lib_dir
76
+ else
77
+ [workspace_target_dir, crate_target_dir]
78
+ .map { |dir| File.join(dir, "release") }
79
+ .find { |dir| Dir.exist?(dir) } || lib_dir
80
+ end
81
+
63
82
  if target_platform
64
83
  platform_key = ENV.fetch("RCD_PLATFORM", "")
65
84
  else
@@ -7,12 +7,12 @@
7
7
  #ifndef YERBA_H
8
8
  #define YERBA_H
9
9
 
10
- typedef enum YerbaNodeType {
11
- YERBA_NODE_TYPE_SCALAR = 0,
12
- YERBA_NODE_TYPE_MAP = 1,
13
- YERBA_NODE_TYPE_SEQUENCE = 2,
14
- YERBA_NODE_TYPE_NOT_FOUND = 3,
15
- } YerbaNodeType;
10
+ typedef enum NodeType {
11
+ NODE_TYPE_SCALAR = 0,
12
+ NODE_TYPE_MAP = 1,
13
+ NODE_TYPE_SEQUENCE = 2,
14
+ NODE_TYPE_NOT_FOUND = 3,
15
+ } NodeType;
16
16
 
17
17
  typedef enum YerbaValueType {
18
18
  YERBA_VALUE_TYPE_NULL = 0,
@@ -50,7 +50,7 @@ typedef struct YerbaLocation {
50
50
 
51
51
  typedef struct YerbaGetResult {
52
52
  bool is_list;
53
- enum YerbaNodeType node_type;
53
+ enum NodeType node_type;
54
54
  struct YerbaTypedValue single;
55
55
  struct YerbaTypedList list;
56
56
  struct YerbaLocation location;
@@ -82,11 +82,11 @@ char *yerba_document_get_value(const struct Document *document, const char *path
82
82
  */
83
83
  char *yerba_document_get_values(const struct Document *document, const char *path);
84
84
 
85
- int32_t yerba_document_get_quote_style(const struct Document *document, const char *path);
85
+ char *yerba_document_get_quote_style(const struct Document *document, const char *path);
86
86
 
87
87
  struct YerbaResult yerba_document_set_quote_style(struct Document *document,
88
88
  const char *path,
89
- int32_t style);
89
+ const char *style);
90
90
 
91
91
  bool yerba_document_evaluate_condition(const struct Document *document,
92
92
  const char *parent_path,
@@ -129,6 +129,11 @@ struct YerbaResult yerba_document_remove_at(struct Document *document,
129
129
  const char *path,
130
130
  uintptr_t index);
131
131
 
132
+ struct YerbaResult yerba_document_move_item(struct Document *document,
133
+ const char *path,
134
+ uintptr_t from,
135
+ uintptr_t to);
136
+
132
137
  struct YerbaResult yerba_document_rename(struct Document *document,
133
138
  const char *source,
134
139
  const char *dest);
@@ -138,6 +143,11 @@ struct YerbaResult yerba_document_sort(struct Document *document,
138
143
  const char *by,
139
144
  bool case_sensitive);
140
145
 
146
+ struct YerbaResult yerba_document_reorder(struct Document *document,
147
+ const char *path,
148
+ const char *by,
149
+ const char *order_csv);
150
+
141
151
  struct YerbaResult yerba_document_sort_keys(struct Document *document,
142
152
  const char *path,
143
153
  const char *order);
data/ext/yerba/yerba.c CHANGED
@@ -230,7 +230,7 @@ static VALUE document_bracket(VALUE self, VALUE path) {
230
230
  }
231
231
 
232
232
  switch (result.node_type) {
233
- case YERBA_NODE_TYPE_SCALAR: {
233
+ case NODE_TYPE_SCALAR: {
234
234
  VALUE klass = rb_path2class("Yerba::Scalar");
235
235
  VALUE value = typed_value_to_ruby(result.single);
236
236
  yerba_get_result_free(result);
@@ -240,7 +240,7 @@ static VALUE document_bracket(VALUE self, VALUE path) {
240
240
  return instance;
241
241
  }
242
242
 
243
- case YERBA_NODE_TYPE_MAP: {
243
+ case NODE_TYPE_MAP: {
244
244
  yerba_get_result_free(result);
245
245
  VALUE klass = rb_path2class("Yerba::Map");
246
246
 
@@ -249,7 +249,7 @@ static VALUE document_bracket(VALUE self, VALUE path) {
249
249
  return instance;
250
250
  }
251
251
 
252
- case YERBA_NODE_TYPE_SEQUENCE: {
252
+ case NODE_TYPE_SEQUENCE: {
253
253
  yerba_get_result_free(result);
254
254
  VALUE klass = rb_path2class("Yerba::Sequence");
255
255
 
@@ -298,35 +298,35 @@ static VALUE document_get_values(VALUE self, VALUE path) {
298
298
  return rb_funcall(rb_path2class("JSON"), rb_intern("parse"), 1, json_string);
299
299
  }
300
300
 
301
- /* document.get_quote_style(path) → :plain, :single, :double, or nil */
301
+ /* document.get_quote_style(path) → :plain, :single, :double, :literal, etc. or nil */
302
302
  static VALUE document_get_quote_style(VALUE self, VALUE path) {
303
303
  struct Document *document = get_document(self);
304
- int style = yerba_document_get_quote_style(document, StringValueCStr(path));
304
+ char *style = yerba_document_get_quote_style(document, StringValueCStr(path));
305
305
 
306
- switch (style) {
307
- case 0: return ID2SYM(rb_intern("plain"));
308
- case 1: return ID2SYM(rb_intern("single"));
309
- case 2: return ID2SYM(rb_intern("double"));
310
- default: return Qnil;
311
- }
306
+ if (style == NULL) return Qnil;
307
+
308
+ VALUE symbol = ID2SYM(rb_intern(style));
309
+
310
+ yerba_string_free(style);
311
+
312
+ return symbol;
312
313
  }
313
314
 
314
315
  /* document.set_quote_style(path, style) */
315
316
  static VALUE document_set_quote_style(VALUE self, VALUE path, VALUE style) {
316
317
  struct Document *document = get_document(self);
317
318
 
318
- int style_int;
319
+ const char *style_string;
320
+
319
321
  if (RB_TYPE_P(style, T_SYMBOL)) {
320
- ID style_id = SYM2ID(style);
321
- if (style_id == rb_intern("plain")) style_int = 0;
322
- else if (style_id == rb_intern("single")) style_int = 1;
323
- else if (style_id == rb_intern("double")) style_int = 2;
324
- else rb_raise(rb_eError, "Invalid quote style (use :plain, :single, or :double)");
322
+ style_string = rb_id2name(SYM2ID(style));
323
+ } else if (RB_TYPE_P(style, T_STRING)) {
324
+ style_string = StringValueCStr(style);
325
325
  } else {
326
- style_int = NUM2INT(style);
326
+ rb_raise(rb_eError, "Invalid quote style (expected Symbol or String)");
327
327
  }
328
328
 
329
- YerbaResult result = yerba_document_set_quote_style(document, StringValueCStr(path), style_int);
329
+ YerbaResult result = yerba_document_set_quote_style(document, StringValueCStr(path), style_string);
330
330
 
331
331
  check_result(result);
332
332
 
@@ -533,24 +533,83 @@ static VALUE document_rename(VALUE self, VALUE source, VALUE destination) {
533
533
  return self;
534
534
  }
535
535
 
536
- /* document.sort(path, by: nil, case_sensitive: false) */
536
+ /* document.sort(path = "", by: nil, order: nil, case_sensitive: false) */
537
537
  static VALUE document_sort(int argc, VALUE *argv, VALUE self) {
538
538
  VALUE path, opts;
539
- rb_scan_args(argc, argv, "1:", &path, &opts);
539
+ rb_scan_args(argc, argv, "01:", &path, &opts);
540
+
541
+ if (NIL_P(path)) path = rb_str_new_cstr("");
540
542
 
541
543
  const char *by = NULL;
542
544
  bool case_sensitive = false;
545
+ VALUE v_order = Qnil;
543
546
 
544
547
  if (!NIL_P(opts)) {
545
548
  VALUE v_by = rb_hash_aref(opts, ID2SYM(rb_intern("by")));
549
+ v_order = rb_hash_aref(opts, ID2SYM(rb_intern("order")));
546
550
  VALUE v_case_sensitive = rb_hash_aref(opts, ID2SYM(rb_intern("case_sensitive")));
547
551
 
548
- if (!NIL_P(v_by)) by = StringValueCStr(v_by);
549
- if (RTEST(v_case_sensitive)) case_sensitive = true;
552
+ if (SYMBOL_P(v_by)) {
553
+ VALUE by_string = rb_sym2str(v_by);
554
+ by = StringValueCStr(by_string);
555
+ } else if (!NIL_P(v_by)) {
556
+ by = StringValueCStr(v_by);
557
+ }
558
+
559
+ if (RTEST(v_case_sensitive)) {
560
+ case_sensitive = true;
561
+ }
550
562
  }
551
563
 
552
564
  struct Document *document = get_document(self);
553
- YerbaResult result = yerba_document_sort(document, StringValueCStr(path), by, case_sensitive);
565
+ const char *path_string = StringValueCStr(path);
566
+
567
+ if (RB_TYPE_P(v_order, T_ARRAY)) {
568
+ VALUE order_csv = rb_ary_join(v_order, rb_str_new_cstr(","));
569
+ const char *order_string = StringValueCStr(order_csv);
570
+ const char *reorder_path = StringValueCStr(path);
571
+ const char *reorder_by;
572
+
573
+ if (by) {
574
+ VALUE reorder_by_value = rb_hash_aref(opts, ID2SYM(rb_intern("by")));
575
+
576
+ if (SYMBOL_P(reorder_by_value)) {
577
+ reorder_by_value = rb_sym2str(reorder_by_value);
578
+ }
579
+
580
+ reorder_by = StringValueCStr(reorder_by_value);
581
+ } else {
582
+ reorder_by = ".";
583
+ }
584
+
585
+ YerbaResult result = yerba_document_reorder(document, reorder_path, reorder_by, order_string);
586
+ check_result(result);
587
+
588
+ return self;
589
+ }
590
+
591
+ const char *order = NULL;
592
+
593
+ if (SYMBOL_P(v_order)) {
594
+ VALUE order_string = rb_sym2str(v_order);
595
+ order = StringValueCStr(order_string);
596
+ } else if (!NIL_P(v_order)) {
597
+ order = StringValueCStr(v_order);
598
+ }
599
+
600
+ VALUE by_with_order = Qnil;
601
+
602
+ if (order && strcmp(order, "desc") == 0) {
603
+ if (by) {
604
+ by_with_order = rb_sprintf("%s:desc", by);
605
+ } else {
606
+ by_with_order = rb_str_new_cstr(":desc");
607
+ }
608
+
609
+ by = StringValueCStr(by_with_order);
610
+ }
611
+
612
+ YerbaResult result = yerba_document_sort(document, path_string, by, case_sensitive);
554
613
 
555
614
  check_result(result);
556
615
 
@@ -20,6 +20,41 @@ module Yerba
20
20
  self.class.find(@glob, path, condition: condition, select: select)
21
21
  end
22
22
 
23
+ def find_by(...)
24
+ each do |document|
25
+ next unless document.sequence?
26
+
27
+ result = document.root.find_by(...)
28
+ return result if result
29
+ end
30
+
31
+ nil
32
+ end
33
+
34
+ def where(...)
35
+ results = []
36
+
37
+ each do |document|
38
+ next unless document.sequence?
39
+
40
+ results.concat(document.root.where(...))
41
+ end
42
+
43
+ results
44
+ end
45
+
46
+ def pluck(...)
47
+ results = []
48
+
49
+ each do |document|
50
+ next unless document.sequence?
51
+
52
+ results.concat(document.root.pluck(...))
53
+ end
54
+
55
+ results
56
+ end
57
+
23
58
  def apply!
24
59
  each do |document|
25
60
  yield document
@@ -48,6 +48,22 @@ module Yerba
48
48
  end
49
49
  end
50
50
 
51
+ def find_by(...)
52
+ root.find_by(...)
53
+ end
54
+
55
+ def where(...)
56
+ root.where(...)
57
+ end
58
+
59
+ def pluck(...)
60
+ root.pluck(...)
61
+ end
62
+
63
+ def <<(item)
64
+ root << item
65
+ end
66
+
51
67
  def inspect
52
68
  if path
53
69
  "#<Yerba::Document path=#{path.inspect}>"