yerba 0.2.2 → 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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +167 -12
  3. data/ext/yerba/extconf.rb +39 -12
  4. data/ext/yerba/include/yerba.h +21 -10
  5. data/ext/yerba/yerba.c +91 -25
  6. data/lib/yerba/collection.rb +35 -0
  7. data/lib/yerba/document.rb +16 -0
  8. data/lib/yerba/sequence.rb +169 -1
  9. data/lib/yerba/version.rb +1 -1
  10. data/lib/yerba.rb +7 -2
  11. data/rust/Cargo.lock +1 -0
  12. data/rust/Cargo.toml +2 -2
  13. data/rust/cbindgen.toml +1 -0
  14. data/rust/rustfmt.toml +1 -1
  15. data/rust/src/commands/blank_lines.rs +1 -4
  16. data/rust/src/commands/delete.rs +9 -4
  17. data/rust/src/commands/directives.rs +61 -0
  18. data/rust/src/commands/get.rs +52 -26
  19. data/rust/src/commands/insert.rs +8 -4
  20. data/rust/src/commands/mod.rs +71 -9
  21. data/rust/src/commands/move_item.rs +2 -1
  22. data/rust/src/commands/move_key.rs +2 -1
  23. data/rust/src/commands/quote_style.rs +12 -6
  24. data/rust/src/commands/remove.rs +8 -4
  25. data/rust/src/commands/rename.rs +8 -4
  26. data/rust/src/commands/selectors.rs +158 -0
  27. data/rust/src/commands/set.rs +33 -16
  28. data/rust/src/commands/sort.rs +342 -10
  29. data/rust/src/didyoumean.rs +53 -0
  30. data/rust/src/document/condition.rs +139 -0
  31. data/rust/src/document/delete.rs +91 -0
  32. data/rust/src/document/get.rs +262 -0
  33. data/rust/src/document/insert.rs +314 -0
  34. data/rust/src/document/mod.rs +784 -0
  35. data/rust/src/document/set.rs +90 -0
  36. data/rust/src/document/sort.rs +607 -0
  37. data/rust/src/document/style.rs +473 -0
  38. data/rust/src/error.rs +35 -7
  39. data/rust/src/ffi.rs +213 -520
  40. data/rust/src/json.rs +1 -7
  41. data/rust/src/lib.rs +89 -2
  42. data/rust/src/main.rs +2 -0
  43. data/rust/src/quote_style.rs +83 -7
  44. data/rust/src/selector.rs +2 -7
  45. data/rust/src/syntax.rs +41 -21
  46. data/rust/src/yerbafile.rs +39 -18
  47. metadata +12 -2
  48. data/rust/Cargo.lock +0 -805
  49. data/rust/src/document.rs +0 -2237
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29db5a562148d3de23fa0a071d7e6c43481bc376e57a01fb68c90537119e3785
4
- data.tar.gz: 6e7e32d7779acc555896c77a27c64b6231770e58951877ca36c1efb189539037
3
+ metadata.gz: 8e77d3cce1f16b9adf15e44b0dec51f2182ab12eeeacea0e0d6d1223d7a1f23b
4
+ data.tar.gz: 7abd1edd2196ce73d22dab4b05dbb2f2af47cb04e1cff756bd8df5a678dcbe9e
5
5
  SHA512:
6
- metadata.gz: fcc7982270f2875ae541830e4a5343d92834a09bb8a425e3a11fff231a42555f55771359365d07bee692533ba2f1899792924fc5f9c1fab48d46a661d839b842
7
- data.tar.gz: f67c13246c8ba1d462e015e117344d9d3d4e00163c8810bd46e36015672e5697e31fd2a457360cda924d38f6ac66c900907ec0bb8bd7a7782ff086cb8f16d8e0
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.2"
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
@@ -162,6 +164,12 @@ Use `--condition` to only apply the change when a sibling field matches:
162
164
  yerba set config.yml "database.host" "0.0.0.0" --condition ".port == 5432"
163
165
  ```
164
166
 
167
+ Use `--all` to update all nodes matching a wildcard selector:
168
+
169
+ ```bash
170
+ yerba set videos.yml "[].description" "" --all
171
+ ```
172
+
165
173
  ### `insert`
166
174
 
167
175
  Insert a new key into a map or a new item into a sequence. By default, new items are appended at the end.
@@ -249,15 +257,25 @@ yerba move-key config.yml "database.pool" --after "database.name"
249
257
 
250
258
  ### `sort`
251
259
 
252
- Sort items in a sequence. For simple scalar sequences, no options are needed. For sequences of maps, use `--by` to specify sort fields. Append `:desc` for descending order:
260
+ Sort items in a sequence. For simple scalar sequences, no options are needed. For sequences of maps, use `--by` to specify the sort field. Use `--order desc` for descending. Repeat `--by` and `--order` for tie-breakers:
253
261
 
254
262
  ```bash
255
263
  yerba sort config.yml "tags"
256
- yerba sort videos.yml --by "title"
257
- yerba sort videos.yml --by "date:desc,title"
258
- yerba sort videos.yml "[].speakers" --by "name"
264
+ yerba sort videos.yml --by ".title"
265
+ yerba sort videos.yml --by ".date" --order desc --by ".title"
266
+ yerba sort videos.yml "[].speakers" --by ".name"
267
+ ```
268
+
269
+ Use `--order` with a comma-separated list to specify an explicit custom order. All items must be listed:
270
+
271
+ ```bash
272
+ yerba sort videos.yml "[]" --by ".id" --order "talk-c,talk-a,talk-b"
273
+ yerba sort speakers.yml "[]" --by ".name" --order "Charlie,Alice,Bob"
274
+ yerba sort config.yml "tags" --by "." --order "yaml,ruby,rust"
259
275
  ```
260
276
 
277
+ This is useful for reordering items in a specific sequence (e.g., conference schedule order, priority lists) or when an LLM agent needs to rearrange items programmatically.
278
+
261
279
  ### `sort-keys`
262
280
 
263
281
  Reorder the keys in a map to match a predefined order. If any key in the document is not present in the order list, the command aborts with an error, this ensures you account for every field:
@@ -270,7 +288,7 @@ yerba sort-keys "data/**/videos.yml" "[]" "id,title,speakers"
270
288
 
271
289
  ### `quote-style`
272
290
 
273
- 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:
274
292
 
275
293
  ```bash
276
294
  yerba quote-style config.yml --values double
@@ -285,6 +303,36 @@ yerba quote-style config.yml "[].speakers" --values plain
285
303
  yerba quote-style "data/**/*.yml" --keys plain --values double
286
304
  ```
287
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
+
288
336
  ### `blank-lines`
289
337
 
290
338
  Enforce a consistent number of blank lines between sequence entries:
@@ -295,6 +343,67 @@ yerba blank-lines videos.yml "[]" 1
295
343
  yerba blank-lines config.yml "tags" 0
296
344
  ```
297
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
+
356
+ ### `selectors`
357
+
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:
359
+
360
+ ```bash
361
+ yerba selectors config.yml
362
+ ```
363
+
364
+ Output:
365
+ ```
366
+ database
367
+ database.host
368
+ database.port
369
+ tags
370
+ tags[]
371
+ ```
372
+
373
+ For sequences of objects:
374
+
375
+ ```bash
376
+ yerba selectors videos.yml
377
+ ```
378
+
379
+ Output:
380
+ ```
381
+ []
382
+ [].id
383
+ [].title
384
+ [].speakers
385
+ [].speakers[]
386
+ [].speakers[].name
387
+ [].speakers[].slug
388
+ [].video_id
389
+ [].video_provider
390
+ ```
391
+
392
+ Pass a selector to scope the output to a specific subtree:
393
+
394
+ ```bash
395
+ yerba selectors config.yml "database"
396
+ yerba selectors videos.yml "[]"
397
+ yerba selectors videos.yml "[].speakers"
398
+ ```
399
+
400
+ Works with glob patterns to show the union of selectors across multiple files:
401
+
402
+ ```bash
403
+ yerba selectors "data/**/videos.yml"
404
+ yerba selectors "data/**/videos.yml" "[]"
405
+ ```
406
+
298
407
  ## `Yerbafile`
299
408
 
300
409
  A `Yerbafile` is a YAML configuration file that defines formatting and editing rules as pipelines of operations that are applied to your files across your project.
@@ -358,6 +467,7 @@ Available pipeline steps:
358
467
  - `delete` Remove a key (supports conditions)
359
468
  - `rename` Rename a key
360
469
  - `remove` Remove an item from a sequence
470
+ - `directives` Add or remove the document start marker (`---`)
361
471
  - `get` Read a value and store it as a variable for subsequent steps
362
472
 
363
473
  This makes it easy to enforce project-wide YAML conventions in CI:
@@ -428,6 +538,12 @@ document["database"]["host"].value = "0.0.0.0"
428
538
  document.set("database.port", 3306)
429
539
  ```
430
540
 
541
+ Set all matching nodes at once with `all: true`:
542
+
543
+ ```ruby
544
+ document.set("[].description", "", all: true)
545
+ ```
546
+
431
547
  Insert new keys with positional control:
432
548
 
433
549
  ```ruby
@@ -441,7 +557,44 @@ tags = document["tags"]
441
557
  tags << "yaml"
442
558
  tags << { name: "Rust", version: "1.80" }
443
559
  tags.remove("obsolete")
444
- 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)
445
598
  ```
446
599
 
447
600
  ### Quote Style Control
@@ -450,7 +603,7 @@ Read and set the quote style on individual scalars:
450
603
 
451
604
  ```ruby
452
605
  scalar = document["database.host"]
453
- scalar.quote_style # => :double
606
+ scalar.quote_style # => :double
454
607
  scalar.quote_style = :single
455
608
  ```
456
609
 
@@ -474,6 +627,10 @@ collection.each do |document|
474
627
  puts document.get("[0].title")
475
628
  end
476
629
 
630
+ collection.find_by(name: "Alice")
631
+ collection.where(kind: "talk")
632
+ collection.pluck(:name)
633
+
477
634
  collection.apply! do |document|
478
635
  document.set("status", "published")
479
636
  end
@@ -499,10 +656,9 @@ After checking out the repo, run `bundle install` to install Ruby dependencies,
499
656
 
500
657
  ### Building from source
501
658
 
502
- 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:
503
660
 
504
661
  ```bash
505
- cd rust
506
662
  cargo build
507
663
  cargo test
508
664
  ```
@@ -518,7 +674,6 @@ cargo run -- get config.yml "database.host"
518
674
  Or build a release binary:
519
675
 
520
676
  ```bash
521
- cd rust
522
677
  cargo build --release
523
678
  ./target/release/yerba --help
524
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
@@ -93,19 +112,27 @@ if target_platform
93
112
  end
94
113
  end
95
114
 
96
- lib_path = File.join(lib_dir, lib_name)
115
+ static_lib = File.join(lib_dir, "libyerba.a")
97
116
 
98
- unless File.exist?(lib_path)
99
- abort "ERROR: Shared library not found at #{lib_path}"
100
- end
117
+ if File.exist?(static_lib)
118
+ puts "yerba: Static library found at #{static_lib}"
119
+ $LDFLAGS << " #{static_lib}"
120
+ else
121
+ lib_path = File.join(lib_dir, lib_name)
101
122
 
102
- puts "yerba: Shared library found at #{lib_path}"
123
+ unless File.exist?(lib_path)
124
+ abort "ERROR: Shared library not found at #{lib_path}"
125
+ end
103
126
 
104
- $LDFLAGS << " -L#{lib_dir} -lyerba"
105
- $CFLAGS << " -I#{File.join(__dir__, "include")}"
127
+ puts "yerba: Shared library found at #{lib_path} (dynamic)"
128
+
129
+ $LDFLAGS << " -L#{lib_dir} -lyerba"
106
130
 
107
- if RbConfig::CONFIG["host_os"].match?(/darwin|linux/)
108
- $LDFLAGS << " -Wl,-rpath,#{lib_dir}"
131
+ if RbConfig::CONFIG["host_os"].match?(/darwin|linux/)
132
+ $LDFLAGS << " -Wl,-rpath,#{lib_dir}"
133
+ end
109
134
  end
110
135
 
136
+ $CFLAGS << " -I#{File.join(__dir__, "include")}"
137
+
111
138
  create_makefile("yerba/yerba")
@@ -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,
@@ -102,7 +102,8 @@ char *yerba_document_find(const struct Document *document,
102
102
  struct YerbaResult yerba_document_set(struct Document *document,
103
103
  const char *path,
104
104
  const char *value,
105
- enum YerbaValueType value_type);
105
+ enum YerbaValueType value_type,
106
+ bool all);
106
107
 
107
108
  struct YerbaResult yerba_document_insert(struct Document *document,
108
109
  const char *path,
@@ -128,6 +129,11 @@ struct YerbaResult yerba_document_remove_at(struct Document *document,
128
129
  const char *path,
129
130
  uintptr_t index);
130
131
 
132
+ struct YerbaResult yerba_document_move_item(struct Document *document,
133
+ const char *path,
134
+ uintptr_t from,
135
+ uintptr_t to);
136
+
131
137
  struct YerbaResult yerba_document_rename(struct Document *document,
132
138
  const char *source,
133
139
  const char *dest);
@@ -137,6 +143,11 @@ struct YerbaResult yerba_document_sort(struct Document *document,
137
143
  const char *by,
138
144
  bool case_sensitive);
139
145
 
146
+ struct YerbaResult yerba_document_reorder(struct Document *document,
147
+ const char *path,
148
+ const char *by,
149
+ const char *order_csv);
150
+
140
151
  struct YerbaResult yerba_document_sort_keys(struct Document *document,
141
152
  const char *path,
142
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
 
@@ -399,6 +399,7 @@ static VALUE document_set(int argc, VALUE *argv, VALUE self) {
399
399
  const char *c_value;
400
400
  YerbaValueType value_type;
401
401
  char number_buffer[64];
402
+ bool all = false;
402
403
 
403
404
  if (value == Qnil) {
404
405
  c_value = "null";
@@ -422,7 +423,13 @@ static VALUE document_set(int argc, VALUE *argv, VALUE self) {
422
423
  value_type = YERBA_VALUE_TYPE_STRING;
423
424
  }
424
425
 
425
- YerbaResult result = yerba_document_set(document, StringValueCStr(path), c_value, value_type);
426
+ if (!NIL_P(opts)) {
427
+ VALUE v_all = rb_hash_aref(opts, ID2SYM(rb_intern("all")));
428
+
429
+ if (RTEST(v_all)) all = true;
430
+ }
431
+
432
+ YerbaResult result = yerba_document_set(document, StringValueCStr(path), c_value, value_type, all);
426
433
  check_result(result);
427
434
 
428
435
  return self;
@@ -526,24 +533,83 @@ static VALUE document_rename(VALUE self, VALUE source, VALUE destination) {
526
533
  return self;
527
534
  }
528
535
 
529
- /* document.sort(path, by: nil, case_sensitive: false) */
536
+ /* document.sort(path = "", by: nil, order: nil, case_sensitive: false) */
530
537
  static VALUE document_sort(int argc, VALUE *argv, VALUE self) {
531
538
  VALUE path, opts;
532
- 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("");
533
542
 
534
543
  const char *by = NULL;
535
544
  bool case_sensitive = false;
545
+ VALUE v_order = Qnil;
536
546
 
537
547
  if (!NIL_P(opts)) {
538
548
  VALUE v_by = rb_hash_aref(opts, ID2SYM(rb_intern("by")));
549
+ v_order = rb_hash_aref(opts, ID2SYM(rb_intern("order")));
539
550
  VALUE v_case_sensitive = rb_hash_aref(opts, ID2SYM(rb_intern("case_sensitive")));
540
551
 
541
- if (!NIL_P(v_by)) by = StringValueCStr(v_by);
542
- 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
+ }
543
562
  }
544
563
 
545
564
  struct Document *document = get_document(self);
546
- 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);
547
613
 
548
614
  check_result(result);
549
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