odin-foundation 1.0.4 → 1.2.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/lib/odin/export.rb +1 -1
  3. data/lib/odin/forms/accessibility.rb +95 -0
  4. data/lib/odin/forms/css.rb +42 -0
  5. data/lib/odin/forms/parser.rb +719 -0
  6. data/lib/odin/forms/renderer.rb +534 -0
  7. data/lib/odin/forms/types.rb +102 -0
  8. data/lib/odin/forms/units.rb +41 -0
  9. data/lib/odin/forms.rb +55 -0
  10. data/lib/odin/parsing/parser.rb +25 -1
  11. data/lib/odin/parsing/tokenizer.rb +38 -20
  12. data/lib/odin/parsing/value_parser.rb +65 -7
  13. data/lib/odin/resolver/import_resolver.rb +40 -12
  14. data/lib/odin/resolver/type_registry.rb +54 -0
  15. data/lib/odin/transform/format_exporters.rb +88 -48
  16. data/lib/odin/transform/source_parsers.rb +2 -2
  17. data/lib/odin/transform/transform_engine.rb +1388 -246
  18. data/lib/odin/transform/transform_expr.rb +222 -0
  19. data/lib/odin/transform/transform_parser.rb +377 -19
  20. data/lib/odin/transform/transform_types.rb +23 -7
  21. data/lib/odin/transform/verb_context.rb +19 -1
  22. data/lib/odin/transform/verbs/aggregation_verbs.rb +2 -1
  23. data/lib/odin/transform/verbs/collection_verbs.rb +164 -89
  24. data/lib/odin/transform/verbs/datetime_verbs.rb +86 -15
  25. data/lib/odin/transform/verbs/extra_verbs.rb +613 -0
  26. data/lib/odin/transform/verbs/financial_verbs.rb +116 -27
  27. data/lib/odin/transform/verbs/geo_verbs.rb +7 -0
  28. data/lib/odin/transform/verbs/numeric_verbs.rb +85 -64
  29. data/lib/odin/transform/verbs/object_verbs.rb +31 -26
  30. data/lib/odin/types/errors.rb +9 -1
  31. data/lib/odin/types/schema.rb +20 -3
  32. data/lib/odin/utils/format_utils.rb +31 -15
  33. data/lib/odin/validation/format_validators.rb +7 -9
  34. data/lib/odin/validation/invariant_evaluator.rb +410 -0
  35. data/lib/odin/validation/schema_definition_validator.rb +357 -0
  36. data/lib/odin/validation/schema_parser.rb +234 -21
  37. data/lib/odin/validation/validator.rb +281 -123
  38. data/lib/odin/version.rb +1 -1
  39. data/lib/odin.rb +100 -4
  40. metadata +14 -2
@@ -21,6 +21,19 @@ module Odin
21
21
  end
22
22
  end
23
23
 
24
+ # Strict yyyy-MM-dd parse; returns nil for any other shape or invalid date.
25
+ def parse_iso_date(s)
26
+ s = s.to_string if s.is_a?(Types::DynValue)
27
+ return nil unless s.is_a?(String)
28
+ m = s.match(/\A(\d{4})-(\d{2})-(\d{2})\z/)
29
+ return nil unless m
30
+ begin
31
+ Date.new(m[1].to_i, m[2].to_i, m[3].to_i)
32
+ rescue ArgumentError
33
+ nil
34
+ end
35
+ end
36
+
24
37
  def parse_timestamp(s)
25
38
  return nil if s.nil?
26
39
  s = s.to_string if s.is_a?(Types::DynValue)
@@ -69,6 +82,34 @@ module Odin
69
82
  end
70
83
  end
71
84
 
85
+ # Extract date and time components positionally from a pattern, returning
86
+ # a UTC Time. Recognizes YYYY/YY/MM/DD/HH/mm/ss tokens.
87
+ def parse_timestamp_with_pattern(value, pattern)
88
+ pos = ->(pat) { pattern.index(pat) }
89
+ slice = ->(p, len) { p ? value[p, len]&.to_i : nil }
90
+
91
+ yyyy = pos.call("YYYY")
92
+ yy = pos.call("YY")
93
+ year = if yyyy
94
+ slice.call(yyyy, 4)
95
+ elsif yy
96
+ 2000 + (slice.call(yy, 2) || 0)
97
+ end
98
+ return nil unless year
99
+
100
+ month = slice.call(pos.call("MM"), 2) || 1
101
+ day = slice.call(pos.call("DD"), 2) || 1
102
+ hour = slice.call(pos.call("HH"), 2) || 0
103
+ minute = slice.call(pos.call("mm"), 2) || 0
104
+ second = slice.call(pos.call("ss"), 2) || 0
105
+
106
+ begin
107
+ Time.utc(year, month, day, hour, minute, second)
108
+ rescue ArgumentError
109
+ nil
110
+ end
111
+ end
112
+
72
113
  def apply_date_pattern(dt, pattern)
73
114
  result = pattern.dup
74
115
  if dt.is_a?(Date) && !dt.is_a?(Time)
@@ -168,11 +209,16 @@ module Odin
168
209
  }
169
210
 
170
211
  registry["parseTimestamp"] = ->(args, _ctx) {
171
- s = args[0]&.to_string
172
- return dv.of_null if s.nil? || s.empty?
173
- t = DateTimeVerbs.parse_timestamp(s)
212
+ return dv.of_null if args.length < 2
213
+
214
+ value = args[0]&.to_string
215
+ pattern = args[1]&.to_string
216
+ return dv.of_null if value.nil? || value.empty? || pattern.nil?
217
+
218
+ t = DateTimeVerbs.parse_timestamp_with_pattern(value, pattern)
174
219
  return dv.of_null unless t
175
- dv.of_timestamp(DateTimeVerbs.format_timestamp_str(t))
220
+ dv.of_string(format("%04d-%02d-%02dT%02d:%02d:%02d",
221
+ t.year, t.month, t.day, t.hour, t.min, t.sec))
176
222
  }
177
223
 
178
224
  registry["addDays"] = ->(args, _ctx) {
@@ -342,7 +388,7 @@ module Odin
342
388
  return dv.of_null if v.nil? || v.null?
343
389
  d = DateTimeVerbs.parse_date(v)
344
390
  return dv.of_null unless d
345
- dv.of_date(DateTimeVerbs.format_date_str(Date.new(d.year, d.month, 1)))
391
+ dv.of_string(DateTimeVerbs.format_date_str(Date.new(d.year, d.month, 1)))
346
392
  }
347
393
 
348
394
  registry["endOfMonth"] = ->(args, _ctx) {
@@ -351,7 +397,7 @@ module Odin
351
397
  d = DateTimeVerbs.parse_date(v)
352
398
  return dv.of_null unless d
353
399
  last = Date.new(d.year, d.month, -1)
354
- dv.of_date(DateTimeVerbs.format_date_str(last))
400
+ dv.of_string(DateTimeVerbs.format_date_str(last))
355
401
  }
356
402
 
357
403
  registry["startOfYear"] = ->(args, _ctx) {
@@ -359,7 +405,7 @@ module Odin
359
405
  return dv.of_null if v.nil? || v.null?
360
406
  d = DateTimeVerbs.parse_date(v)
361
407
  return dv.of_null unless d
362
- dv.of_date(DateTimeVerbs.format_date_str(Date.new(d.year, 1, 1)))
408
+ dv.of_string(DateTimeVerbs.format_date_str(Date.new(d.year, 1, 1)))
363
409
  }
364
410
 
365
411
  registry["endOfYear"] = ->(args, _ctx) {
@@ -367,7 +413,7 @@ module Odin
367
413
  return dv.of_null if v.nil? || v.null?
368
414
  d = DateTimeVerbs.parse_date(v)
369
415
  return dv.of_null unless d
370
- dv.of_date(DateTimeVerbs.format_date_str(Date.new(d.year, 12, 31)))
416
+ dv.of_string(DateTimeVerbs.format_date_str(Date.new(d.year, 12, 31)))
371
417
  }
372
418
 
373
419
  registry["dayOfWeek"] = ->(args, _ctx) {
@@ -478,7 +524,7 @@ module Odin
478
524
  d1 = DateTimeVerbs.parse_date(v1)
479
525
  d2 = DateTimeVerbs.parse_date(v2)
480
526
  return dv.of_null unless d1 && d2
481
- dv.of_integer((d2 - d1).to_i.abs)
527
+ dv.of_integer((d2 - d1).to_i)
482
528
  }
483
529
 
484
530
  registry["ageFromDate"] = ->(args, _ctx) {
@@ -499,8 +545,12 @@ module Odin
499
545
  registry["isValidDate"] = ->(args, _ctx) {
500
546
  s = args[0]&.to_string
501
547
  return dv.of_bool(false) if s.nil? || s.empty?
502
- d = DateTimeVerbs.parse_date(s)
503
- dv.of_bool(!d.nil?)
548
+ return dv.of_bool(true) unless DateTimeVerbs.parse_iso_date(s).nil?
549
+ if args.length >= 2
550
+ pattern = args[1]&.to_string || ""
551
+ return dv.of_bool(true) unless DateTimeVerbs.parse_date_with_pattern(s, pattern).nil?
552
+ end
553
+ dv.of_bool(false)
504
554
  }
505
555
 
506
556
  registry["formatLocaleDate"] = ->(args, _ctx) {
@@ -544,17 +594,38 @@ module Odin
544
594
  return dv.of_null if v.nil? || v.null?
545
595
  d = DateTimeVerbs.parse_date(v)
546
596
  return dv.of_null unless d
547
- if d.saturday?
548
- d += 2
549
- elsif d.sunday?
597
+ loop do
550
598
  d += 1
599
+ break unless d.saturday? || d.sunday?
551
600
  end
552
601
  dv.of_string(DateTimeVerbs.format_date_str(d))
553
602
  }
554
603
 
555
604
  registry["formatDuration"] = ->(args, _ctx) {
556
605
  return dv.of_null if args.empty?
557
- iso = args[0]&.to_string
606
+ arg = args[0]
607
+ return dv.of_null if arg.nil? || arg.null?
608
+
609
+ iso = arg.to_string
610
+
611
+ # Numeric seconds (typed or numeric string): expand into d/h/m/s.
612
+ if arg.numeric? || iso.match?(/\A\d+(?:\.\d+)?\z/)
613
+ total = arg.numeric? ? arg.to_number.to_f : iso.to_f
614
+ return dv.of_null if total < 0
615
+ days = (total / 86400).floor
616
+ total -= days * 86400
617
+ hours = (total / 3600).floor
618
+ total -= hours * 3600
619
+ minutes = (total / 60).floor
620
+ seconds = total - minutes * 60
621
+ parts = []
622
+ parts << DateTimeVerbs.duration_part(days, "day") unless days == 0
623
+ parts << DateTimeVerbs.duration_part(hours, "hour") unless hours == 0
624
+ parts << DateTimeVerbs.duration_part(minutes, "minute") unless minutes == 0
625
+ parts << DateTimeVerbs.duration_part(seconds, "second") unless seconds == 0
626
+ return dv.of_string(parts.empty? ? "0 seconds" : parts.join(", "))
627
+ end
628
+
558
629
  return dv.of_null if iso.nil? || !iso.start_with?("P")
559
630
  parts = []
560
631
  in_time = false