yerba 0.4.2 → 0.5.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.
data/ext/yerba/yerba.c CHANGED
@@ -146,53 +146,6 @@ static VALUE document_s_parse(VALUE klass, VALUE content) {
146
146
  return instance;
147
147
  }
148
148
 
149
- /* document.get(path) */
150
- static VALUE document_get(VALUE self, VALUE path) {
151
- struct Document *document = get_document(self);
152
- YerbaGetResult result = yerba_document_get(document, StringValueCStr(path));
153
-
154
- if (result.error) {
155
- VALUE message = make_utf8_string(result.error);
156
- yerba_get_result_free(result);
157
-
158
- rb_raise(rb_ePathValidationError, "%s", StringValueCStr(message));
159
- }
160
-
161
- if (!result.is_list) {
162
- VALUE ruby_value = typed_value_to_ruby(result.single);
163
- yerba_get_result_free(result);
164
-
165
- return ruby_value;
166
- } else {
167
- VALUE json_string = make_utf8_string(result.list.json);
168
- yerba_get_result_free(result);
169
-
170
- VALUE items = rb_funcall(rb_path2class("JSON"), rb_intern("parse"), 1, json_string);
171
- long length = RARRAY_LEN(items);
172
- VALUE array = rb_ary_new_capa(length);
173
-
174
- for (long i = 0; i < length; i++) {
175
- VALUE item = rb_ary_entry(items, i);
176
- VALUE text = rb_hash_aref(item, rb_str_new_cstr("text"));
177
- int type_value = NUM2INT(rb_hash_aref(item, rb_str_new_cstr("type")));
178
-
179
- if (NIL_P(text)) {
180
- rb_ary_push(array, Qnil);
181
-
182
- continue;
183
- }
184
-
185
- YerbaTypedValue typed_value;
186
- typed_value.text = (char *)StringValueCStr(text);
187
- typed_value.value_type = (YerbaValueType)type_value;
188
-
189
- rb_ary_push(array, typed_value_to_ruby(typed_value));
190
- }
191
-
192
- return array;
193
- }
194
- }
195
-
196
149
  static VALUE location_to_ruby(YerbaLocation location) {
197
150
  VALUE klass = rb_path2class("Yerba::Location");
198
151
 
@@ -206,11 +159,9 @@ static VALUE location_to_ruby(YerbaLocation location) {
206
159
  );
207
160
  }
208
161
 
209
- /* document[](path) Yerba::Scalar, Yerba::Map, Yerba::Sequence, or nil */
210
- static VALUE document_bracket(VALUE self, VALUE path) {
211
- struct Document *document = get_document(self);
212
- YerbaGetResult result = yerba_document_get(document, StringValueCStr(path));
213
-
162
+ /* Build a Yerba::Scalar, Map, Sequence, or nil from a YerbaGetResult.
163
+ Frees the result internally. */
164
+ static VALUE node_from_get_result(YerbaGetResult result, VALUE self, VALUE path) {
214
165
  if (result.error) {
215
166
  VALUE message = make_utf8_string(result.error);
216
167
  yerba_get_result_free(result);
@@ -218,7 +169,6 @@ static VALUE document_bracket(VALUE self, VALUE path) {
218
169
  rb_raise(rb_ePathValidationError, "%s", StringValueCStr(message));
219
170
  }
220
171
 
221
- VALUE instance;
222
172
  VALUE location = location_to_ruby(result.location);
223
173
  VALUE key = Qnil;
224
174
 
@@ -226,37 +176,31 @@ static VALUE document_bracket(VALUE self, VALUE path) {
226
176
  VALUE key_location = location_to_ruby(result.key_location);
227
177
  VALUE key_value = make_utf8_string(result.key_name);
228
178
 
229
- key = rb_funcall(rb_path2class("Yerba::Scalar"), rb_intern("new"), 4, Qnil, Qnil, key_value, key_location);
179
+ VALUE key_kwargs = rb_hash_new();
180
+ rb_hash_aset(key_kwargs, ID2SYM(rb_intern("value")), key_value);
181
+ key = rb_funcallv_kw(rb_path2class("Yerba::Scalar"), rb_intern("from_document"), 5, (VALUE[]){ Qnil, Qnil, key_location, Qnil, key_kwargs }, RB_PASS_KEYWORDS);
230
182
  }
231
183
 
232
184
  switch (result.node_type) {
233
185
  case NODE_TYPE_SCALAR: {
234
- VALUE klass = rb_path2class("Yerba::Scalar");
235
186
  VALUE value = typed_value_to_ruby(result.single);
236
187
  yerba_get_result_free(result);
237
188
 
238
- instance = rb_funcall(klass, rb_intern("new"), 5, self, path, value, location, key);
189
+ VALUE kwargs = rb_hash_new();
190
+ rb_hash_aset(kwargs, ID2SYM(rb_intern("value")), value);
239
191
 
240
- return instance;
192
+ return rb_funcallv_kw(rb_path2class("Yerba::Scalar"), rb_intern("from_document"), 5, (VALUE[]){ self, path, location, key, kwargs }, RB_PASS_KEYWORDS);
241
193
  }
242
194
 
243
- case NODE_TYPE_MAP: {
195
+ case NODE_TYPE_MAP:
244
196
  yerba_get_result_free(result);
245
- VALUE klass = rb_path2class("Yerba::Map");
246
-
247
- instance = rb_funcall(klass, rb_intern("new"), 4, self, path, location, key);
248
197
 
249
- return instance;
250
- }
198
+ return rb_funcall(rb_path2class("Yerba::Map"), rb_intern("from_document"), 4, self, path, location, key);
251
199
 
252
- case NODE_TYPE_SEQUENCE: {
200
+ case NODE_TYPE_SEQUENCE:
253
201
  yerba_get_result_free(result);
254
- VALUE klass = rb_path2class("Yerba::Sequence");
255
-
256
- instance = rb_funcall(klass, rb_intern("new"), 4, self, path, location, key);
257
202
 
258
- return instance;
259
- }
203
+ return rb_funcall(rb_path2class("Yerba::Sequence"), rb_intern("from_document"), 4, self, path, location, key);
260
204
 
261
205
  default:
262
206
  yerba_get_result_free(result);
@@ -265,6 +209,47 @@ static VALUE document_bracket(VALUE self, VALUE path) {
265
209
  }
266
210
  }
267
211
 
212
+ /* document[](path) → Yerba::Scalar, Yerba::Map, Yerba::Sequence, Array, or nil */
213
+ static VALUE document_bracket(VALUE self, VALUE path) {
214
+ struct Document *document = get_document(self);
215
+ char index_buffer[32];
216
+
217
+ if (RB_TYPE_P(path, T_FIXNUM)) {
218
+ snprintf(index_buffer, sizeof(index_buffer), "[%ld]", FIX2LONG(path));
219
+ path = rb_str_new_cstr(index_buffer);
220
+ }
221
+
222
+ const char *selector = StringValueCStr(path);
223
+
224
+ /* Wildcard: resolve to concrete selectors and return array of nodes */
225
+ if (strstr(selector, "[]") != NULL) {
226
+ char *json = yerba_document_resolve_selectors(document, selector);
227
+
228
+ if (!json) return rb_ary_new();
229
+
230
+ VALUE json_string = make_utf8_string(json);
231
+ yerba_string_free(json);
232
+
233
+ VALUE resolved = rb_funcall(rb_path2class("JSON"), rb_intern("parse"), 1, json_string);
234
+ long length = RARRAY_LEN(resolved);
235
+ VALUE array = rb_ary_new_capa(length);
236
+
237
+ for (long i = 0; i < length; i++) {
238
+ VALUE concrete_path = rb_ary_entry(resolved, i);
239
+ YerbaGetResult result = yerba_document_get(document, StringValueCStr(concrete_path));
240
+ VALUE node = node_from_get_result(result, self, concrete_path);
241
+
242
+ if (!NIL_P(node)) rb_ary_push(array, node);
243
+ }
244
+
245
+ return array;
246
+ }
247
+
248
+ YerbaGetResult result = yerba_document_get(document, selector);
249
+
250
+ return node_from_get_result(result, self, path);
251
+ }
252
+
268
253
  /* document.exists?(path) */
269
254
  static VALUE document_exists_p(VALUE self, VALUE path) {
270
255
  struct Document *document = get_document(self);
@@ -272,6 +257,13 @@ static VALUE document_exists_p(VALUE self, VALUE path) {
272
257
  return yerba_document_exists(document, StringValueCStr(path)) ? Qtrue : Qfalse;
273
258
  }
274
259
 
260
+ /* document.valid_selector?(selector) → true/false */
261
+ static VALUE document_valid_selector_p(VALUE self, VALUE path) {
262
+ struct Document *document = get_document(self);
263
+
264
+ return yerba_document_valid_selector(document, StringValueCStr(path)) ? Qtrue : Qfalse;
265
+ }
266
+
275
267
  /* document.get_value(path) → parsed Ruby object (Hash/Array/String/Integer/etc) */
276
268
  static VALUE document_get_value(VALUE self, VALUE path) {
277
269
  struct Document *document = get_document(self);
@@ -285,10 +277,59 @@ static VALUE document_get_value(VALUE self, VALUE path) {
285
277
  return rb_funcall(rb_path2class("JSON"), rb_intern("parse"), 1, json_string);
286
278
  }
287
279
 
288
- /* document.get_values(path)Array of parsed Ruby objects */
289
- static VALUE document_get_values(VALUE self, VALUE path) {
280
+ /* document.selectors["database", "database.host", "tags", "tags[]", ...] */
281
+ static VALUE document_selectors(VALUE self) {
290
282
  struct Document *document = get_document(self);
291
- char *json = yerba_document_get_values(document, StringValueCStr(path));
283
+ char *json = yerba_document_selectors(document);
284
+
285
+ if (!json) return rb_ary_new();
286
+
287
+ VALUE json_string = make_utf8_string(json);
288
+ yerba_string_free(json);
289
+
290
+ return rb_funcall(rb_path2class("JSON"), rb_intern("parse"), 1, json_string);
291
+ }
292
+
293
+ /* document.locations(selector) → [Yerba::Location, ...] */
294
+ static VALUE document_locations(VALUE self, VALUE path) {
295
+ struct Document *document = get_document(self);
296
+ char *json = yerba_document_resolve_selectors(document, StringValueCStr(path));
297
+
298
+ if (!json) return rb_ary_new();
299
+
300
+ VALUE json_string = make_utf8_string(json);
301
+ yerba_string_free(json);
302
+
303
+ VALUE resolved = rb_funcall(rb_path2class("JSON"), rb_intern("parse"), 1, json_string);
304
+ long length = RARRAY_LEN(resolved);
305
+ VALUE array = rb_ary_new_capa(length);
306
+
307
+ for (long i = 0; i < length; i++) {
308
+ VALUE concrete_path = rb_ary_entry(resolved, i);
309
+ YerbaGetResult result = yerba_document_get(document, StringValueCStr(concrete_path));
310
+
311
+ if (result.error) {
312
+ yerba_get_result_free(result);
313
+ continue;
314
+ }
315
+
316
+ if (result.location.start_line == 0 && result.location.end_line == 0) {
317
+ yerba_get_result_free(result);
318
+ continue;
319
+ }
320
+
321
+ VALUE location = location_to_ruby(result.location);
322
+ yerba_get_result_free(result);
323
+ rb_ary_push(array, location);
324
+ }
325
+
326
+ return array;
327
+ }
328
+
329
+ /* document.resolve_selectors(path) → ["[0].speakers[0]", "[0].speakers[1]", ...] */
330
+ static VALUE document_resolve_selectors(VALUE self, VALUE path) {
331
+ struct Document *document = get_document(self);
332
+ char *json = yerba_document_resolve_selectors(document, StringValueCStr(path));
292
333
 
293
334
  if (!json) return rb_ary_new();
294
335
 
@@ -379,6 +420,41 @@ static VALUE document_find(int argc, VALUE *argv, VALUE self) {
379
420
  return rb_funcall(rb_path2class("JSON"), rb_intern("parse"), 1, json_string);
380
421
  }
381
422
 
423
+ /* Convert a Ruby VALUE to a C string + YerbaValueType pair.
424
+ The caller must provide a number_buffer of at least 64 bytes. */
425
+ struct TypedValue {
426
+ const char *text;
427
+ YerbaValueType type;
428
+ };
429
+
430
+ static struct TypedValue ruby_to_typed_value(VALUE value, char *number_buffer) {
431
+ struct TypedValue result;
432
+
433
+ if (value == Qnil) {
434
+ result.text = "null";
435
+ result.type = YERBA_VALUE_TYPE_NULL;
436
+ } else if (value == Qtrue) {
437
+ result.text = "true";
438
+ result.type = YERBA_VALUE_TYPE_BOOLEAN;
439
+ } else if (value == Qfalse) {
440
+ result.text = "false";
441
+ result.type = YERBA_VALUE_TYPE_BOOLEAN;
442
+ } else if (RB_INTEGER_TYPE_P(value)) {
443
+ snprintf(number_buffer, 64, "%ld", NUM2LONG(value));
444
+ result.text = number_buffer;
445
+ result.type = YERBA_VALUE_TYPE_INTEGER;
446
+ } else if (RB_FLOAT_TYPE_P(value)) {
447
+ snprintf(number_buffer, 64, "%g", NUM2DBL(value));
448
+ result.text = number_buffer;
449
+ result.type = YERBA_VALUE_TYPE_FLOAT;
450
+ } else {
451
+ result.text = StringValueCStr(value);
452
+ result.type = YERBA_VALUE_TYPE_STRING;
453
+ }
454
+
455
+ return result;
456
+ }
457
+
382
458
  /* document.set(path, value, condition: nil, if_exists: false, if_missing: false) */
383
459
  static VALUE document_set(int argc, VALUE *argv, VALUE self) {
384
460
  VALUE path, value, opts;
@@ -396,40 +472,17 @@ static VALUE document_set(int argc, VALUE *argv, VALUE self) {
396
472
  if (RTEST(v_if_missing) && yerba_document_exists(document, StringValueCStr(path))) return self;
397
473
  }
398
474
 
399
- const char *c_value;
400
- YerbaValueType value_type;
401
475
  char number_buffer[64];
476
+ struct TypedValue typed_value = ruby_to_typed_value(value, number_buffer);
402
477
  bool all = false;
403
478
 
404
- if (value == Qnil) {
405
- c_value = "null";
406
- value_type = YERBA_VALUE_TYPE_NULL;
407
- } else if (value == Qtrue) {
408
- c_value = "true";
409
- value_type = YERBA_VALUE_TYPE_BOOLEAN;
410
- } else if (value == Qfalse) {
411
- c_value = "false";
412
- value_type = YERBA_VALUE_TYPE_BOOLEAN;
413
- } else if (RB_INTEGER_TYPE_P(value)) {
414
- snprintf(number_buffer, sizeof(number_buffer), "%ld", NUM2LONG(value));
415
- c_value = number_buffer;
416
- value_type = YERBA_VALUE_TYPE_INTEGER;
417
- } else if (RB_FLOAT_TYPE_P(value)) {
418
- snprintf(number_buffer, sizeof(number_buffer), "%g", NUM2DBL(value));
419
- c_value = number_buffer;
420
- value_type = YERBA_VALUE_TYPE_FLOAT;
421
- } else {
422
- c_value = StringValueCStr(value);
423
- value_type = YERBA_VALUE_TYPE_STRING;
424
- }
425
-
426
479
  if (!NIL_P(opts)) {
427
480
  VALUE v_all = rb_hash_aref(opts, ID2SYM(rb_intern("all")));
428
481
 
429
482
  if (RTEST(v_all)) all = true;
430
483
  }
431
484
 
432
- YerbaResult result = yerba_document_set(document, StringValueCStr(path), c_value, value_type, all);
485
+ YerbaResult result = yerba_document_set(document, StringValueCStr(path), typed_value.text, typed_value.type, all);
433
486
  check_result(result);
434
487
 
435
488
  return self;
@@ -454,8 +507,11 @@ static VALUE document_insert(int argc, VALUE *argv, VALUE self) {
454
507
  if (!NIL_P(v_at)) at = NUM2LL(v_at);
455
508
  }
456
509
 
510
+ char number_buffer[64];
511
+ struct TypedValue typed_value = ruby_to_typed_value(value, number_buffer);
512
+
457
513
  struct Document *document = get_document(self);
458
- YerbaResult result = yerba_document_insert(document, StringValueCStr(path), StringValueCStr(value), before, after, at);
514
+ YerbaResult result = yerba_document_insert(document, StringValueCStr(path), typed_value.text, typed_value.type, before, after, at);
459
515
  check_result(result);
460
516
 
461
517
  return self;
@@ -699,6 +755,24 @@ static VALUE document_apply_yerbafile(int argc, VALUE *argv, VALUE self) {
699
755
  return self;
700
756
  }
701
757
 
758
+ /* document.validate_schema(schema_json, selector = nil) */
759
+ static VALUE document_validate_schema(int argc, VALUE *argv, VALUE self) {
760
+ VALUE schema_json, selector;
761
+ rb_scan_args(argc, argv, "11", &schema_json, &selector);
762
+
763
+ struct Document *document = get_document(self);
764
+ const char *selector_str = NIL_P(selector) ? NULL : StringValueCStr(selector);
765
+
766
+ char *result = yerba_document_validate_schema(document, StringValueCStr(schema_json), selector_str);
767
+
768
+ if (!result) return rb_ary_new();
769
+
770
+ VALUE json_string = make_utf8_string(result);
771
+ yerba_string_free(result);
772
+
773
+ return rb_funcall(rb_path2class("JSON"), rb_intern("parse"), 1, json_string);
774
+ }
775
+
702
776
  /* document.to_s */
703
777
  static VALUE document_to_s(VALUE self) {
704
778
  struct Document *document = get_document(self);
@@ -736,14 +810,41 @@ static VALUE document_changed_p(VALUE self) {
736
810
  return rb_str_equal(current, original) ? Qfalse : Qtrue;
737
811
  }
738
812
 
813
+ /* document.location(selector = nil) → Yerba::Location */
814
+ static VALUE document_location(int argc, VALUE *argv, VALUE self) {
815
+ VALUE path;
816
+ rb_scan_args(argc, argv, "01", &path);
817
+
818
+ struct Document *document = get_document(self);
819
+ const char *selector = NIL_P(path) ? "" : StringValueCStr(path);
820
+
821
+ YerbaGetResult result = yerba_document_get(document, selector);
822
+
823
+ if (result.error) {
824
+ yerba_get_result_free(result);
825
+ return Qnil;
826
+ }
827
+
828
+ if (result.location.start_line == 0 && result.location.end_line == 0 && strlen(selector) > 0) {
829
+ yerba_get_result_free(result);
830
+ return Qnil;
831
+ }
832
+
833
+ VALUE location = location_to_ruby(result.location);
834
+ yerba_get_result_free(result);
835
+
836
+ return location;
837
+ }
838
+
739
839
  /* document.path */
740
840
  static VALUE document_path(VALUE self) {
741
841
  return rb_iv_get(self, "@path");
742
842
  }
743
843
 
744
- /* Collection.get(glob, path) get values across files */
844
+ /* Collection.get(glob, selector) [Yerba::Scalar|Map|Sequence, ...] */
745
845
  static VALUE collection_s_get(VALUE self, VALUE pattern, VALUE path) {
746
- (void)self;
846
+ (void) self;
847
+
747
848
  YerbaTypedList result = yerba_glob_get(StringValueCStr(pattern), StringValueCStr(path));
748
849
 
749
850
  if (!result.json) return rb_ary_new();
@@ -755,20 +856,40 @@ static VALUE collection_s_get(VALUE self, VALUE pattern, VALUE path) {
755
856
  long length = RARRAY_LEN(items);
756
857
  VALUE array = rb_ary_new_capa(length);
757
858
 
859
+
758
860
  for (long i = 0; i < length; i++) {
759
861
  VALUE item = rb_ary_entry(items, i);
760
- VALUE text = rb_hash_aref(item, rb_str_new_cstr("text"));
761
- int type_value = NUM2INT(rb_hash_aref(item, rb_str_new_cstr("type")));
862
+ VALUE node_type = rb_hash_aref(item, rb_str_new_cstr("node_type"));
863
+ VALUE file_path = rb_hash_aref(item, rb_str_new_cstr("file_path"));
864
+ VALUE selector = rb_hash_aref(item, rb_str_new_cstr("selector"));
865
+ const char *type_str = NIL_P(node_type) ? "" : StringValueCStr(node_type);
762
866
 
763
- if (NIL_P(text)) {
764
- rb_ary_push(array, Qnil);
765
- continue;
766
- }
867
+ if (NIL_P(selector)) continue;
868
+
869
+ VALUE line = rb_hash_aref(item, rb_str_new_cstr("line"));
870
+ VALUE kwargs = rb_hash_new();
767
871
 
768
- YerbaTypedValue typed_value;
769
- typed_value.text = (char *)StringValueCStr(text);
770
- typed_value.value_type = (YerbaValueType)type_value;
771
- rb_ary_push(array, typed_value_to_ruby(typed_value));
872
+ rb_hash_aset(kwargs, ID2SYM(rb_intern("selector")), selector);
873
+ if (!NIL_P(file_path)) rb_hash_aset(kwargs, ID2SYM(rb_intern("file_path")), file_path);
874
+ if (!NIL_P(line)) rb_hash_aset(kwargs, ID2SYM(rb_intern("line")), line);
875
+
876
+ if (strcmp(type_str, "scalar") == 0) {
877
+ VALUE text = rb_hash_aref(item, rb_str_new_cstr("text"));
878
+ if (NIL_P(text)) continue;
879
+
880
+ int type_value = NUM2INT(rb_hash_aref(item, rb_str_new_cstr("type")));
881
+ YerbaTypedValue typed_value;
882
+ typed_value.text = (char *)StringValueCStr(text);
883
+ typed_value.value_type = (YerbaValueType)type_value;
884
+
885
+ rb_hash_aset(kwargs, ID2SYM(rb_intern("value")), typed_value_to_ruby(typed_value));
886
+ VALUE scalar_args[1] = { kwargs };
887
+ rb_ary_push(array, rb_funcallv_kw(rb_path2class("Yerba::Scalar"), rb_intern("from"), 1, scalar_args, RB_PASS_KEYWORDS));
888
+ } else {
889
+ const char *klass_name = strcmp(type_str, "sequence") == 0 ? "Yerba::Sequence" : "Yerba::Map";
890
+ VALUE node_args[1] = { kwargs };
891
+ rb_ary_push(array, rb_funcallv_kw(rb_path2class(klass_name), rb_intern("from"), 1, node_args, RB_PASS_KEYWORDS));
892
+ }
772
893
  }
773
894
 
774
895
  return array;
@@ -835,13 +956,17 @@ void Init_yerba(void) {
835
956
  rb_define_alloc_func(rb_cDocument, document_alloc);
836
957
  rb_define_method(rb_cDocument, "initialize", document_initialize, 1);
837
958
  rb_define_singleton_method(rb_cDocument, "parse", document_s_parse, 1);
838
- rb_define_method(rb_cDocument, "get", document_get, 1);
839
959
  rb_define_method(rb_cDocument, "[]", document_bracket, 1);
840
- rb_define_method(rb_cDocument, "get_value", document_get_value, 1);
841
- rb_define_method(rb_cDocument, "get_values", document_get_values, 1);
960
+ rb_define_method(rb_cDocument, "node_at", document_bracket, 1);
961
+ rb_define_method(rb_cDocument, "value_at", document_get_value, 1);
962
+ rb_define_method(rb_cDocument, "location", document_location, -1);
963
+ rb_define_method(rb_cDocument, "locations", document_locations, 1);
964
+ rb_define_method(rb_cDocument, "selectors", document_selectors, 0);
965
+ rb_define_method(rb_cDocument, "resolve_selectors", document_resolve_selectors, 1);
842
966
  rb_define_method(rb_cDocument, "get_quote_style", document_get_quote_style, 1);
843
967
  rb_define_method(rb_cDocument, "set_quote_style", document_set_quote_style, 2);
844
968
  rb_define_method(rb_cDocument, "exists?", document_exists_p, 1);
969
+ rb_define_method(rb_cDocument, "valid_selector?", document_valid_selector_p, 1);
845
970
  rb_define_method(rb_cDocument, "condition?", document_condition_p, -1);
846
971
  rb_define_method(rb_cDocument, "find", document_find, -1);
847
972
  rb_define_method(rb_cDocument, "set", document_set, -1);
@@ -857,6 +982,7 @@ void Init_yerba(void) {
857
982
  rb_define_method(rb_cDocument, "quote_style", document_quote_style, -1);
858
983
  rb_define_method(rb_cDocument, "blank_lines", document_blank_lines, 2);
859
984
  rb_define_method(rb_cDocument, "apply_yerbafile", document_apply_yerbafile, -1);
985
+ rb_define_method(rb_cDocument, "validate_schema", document_validate_schema, -1);
860
986
  rb_define_method(rb_cDocument, "to_s", document_to_s, 0);
861
987
  rb_define_method(rb_cDocument, "write!", document_save, 0);
862
988
  rb_define_method(rb_cDocument, "changed?", document_changed_p, 0);
@@ -4,10 +4,26 @@ module Yerba
4
4
  class Document
5
5
  ROOT_SELECTOR = ""
6
6
 
7
+ def self.cache
8
+ @cache ||= Hash.new { |hash, path| hash[path] = new(path) }
9
+ end
10
+
11
+ def self.clear_cache!
12
+ @cache = nil
13
+ end
14
+
15
+ def selector
16
+ ROOT_SELECTOR
17
+ end
18
+
7
19
  def root
8
20
  self[ROOT_SELECTOR]
9
21
  end
10
22
 
23
+ def []=(key, value)
24
+ root[key] = value
25
+ end
26
+
11
27
  def map?
12
28
  root.is_a?(Map)
13
29
  end
@@ -17,11 +33,11 @@ module Yerba
17
33
  end
18
34
 
19
35
  def to_h
20
- get_value(ROOT_SELECTOR)
36
+ value_at(ROOT_SELECTOR)
21
37
  end
22
38
 
23
39
  def to_a
24
- get_value(ROOT_SELECTOR)
40
+ value_at(ROOT_SELECTOR)
25
41
  end
26
42
 
27
43
  def to_yaml
@@ -29,25 +45,13 @@ module Yerba
29
45
  end
30
46
 
31
47
  def dig(*keys)
32
- result = keys.reduce(self) { |node, key| node.nil? ? nil : node[key] }
33
-
34
- result&.value
48
+ keys.reduce(self) { |node, key| node.nil? ? nil : node[key] }
35
49
  end
36
50
 
37
- def at_path(path)
38
- if path.include?("[]")
39
- values = get(path)
40
- return [] unless values.is_a?(Array)
51
+ def fetch(selector)
52
+ validate_selector!(selector)
41
53
 
42
- path.sub("[]", "")
43
-
44
- values.each_with_index.map do |_value, index|
45
- resolved_path = path.sub("[]", "[#{index}]")
46
- self[resolved_path]
47
- end
48
- else
49
- self[path]
50
- end
54
+ self[selector]
51
55
  end
52
56
 
53
57
  def find_by(...)
@@ -90,6 +94,38 @@ module Yerba
90
94
  self
91
95
  end
92
96
 
97
+ def valid?(schema, selector: nil)
98
+ errors = validate(schema, selector: selector)
99
+
100
+ errors.empty?
101
+ end
102
+
103
+ def validate(schema, selector: nil)
104
+ schema_json = schema.is_a?(String) ? schema : JSON.generate(schema)
105
+
106
+ validate_schema(schema_json, selector)
107
+ end
108
+
109
+ def validate_selector!(selector)
110
+ return if valid_selector?(selector)
111
+
112
+ available = selectors
113
+ message = "selector \"#{selector}\" is not valid for this document"
114
+
115
+ if available.any?
116
+ suggestions = DidYouMean::SpellChecker.new(dictionary: available).correct(selector)
117
+
118
+ if suggestions.any?
119
+ message += ". Did you mean: #{suggestions.first(3).join(", ")}?"
120
+ else
121
+ message += ". Available selectors: #{available.first(5).join(", ")}"
122
+ message += ", ..." if available.length > 5
123
+ end
124
+ end
125
+
126
+ raise Yerba::SelectorNotFoundError, message
127
+ end
128
+
93
129
  def inspect
94
130
  if path
95
131
  "#<Yerba::Document path=#{path.inspect}>"