odin-foundation 1.1.0 → 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.
- checksums.yaml +4 -4
- data/lib/odin/transform/transform_engine.rb +241 -101
- data/lib/odin/transform/transform_expr.rb +222 -0
- data/lib/odin/transform/transform_parser.rb +44 -3
- data/lib/odin/transform/verbs/collection_verbs.rb +54 -34
- data/lib/odin/transform/verbs/datetime_verbs.rb +20 -3
- data/lib/odin/transform/verbs/extra_verbs.rb +613 -0
- data/lib/odin/transform/verbs/financial_verbs.rb +69 -5
- data/lib/odin/transform/verbs/geo_verbs.rb +7 -0
- data/lib/odin/transform/verbs/numeric_verbs.rb +80 -57
- data/lib/odin/transform/verbs/object_verbs.rb +17 -10
- data/lib/odin/version.rb +1 -1
- data/lib/odin.rb +2 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a42b39245e4ab2f8821e0e12100d16418316b22758071d7acf28db0b7a754f20
|
|
4
|
+
data.tar.gz: 99f7bc6624af35e90864f3ccc1239eb2c3c8ab1cd1a50bcd5daffdca36655e98
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 00027a7160bb7e6605bdbb3e77e2ea54232147902027cdc98d3dccc6b6ce06635563d4ca84698a12a3325d29b1310e88355911dcd0c75099ad13ba35eef84a64
|
|
7
|
+
data.tar.gz: 4ad5366860defb9e19affa8f666eccd4d3d6bd3a9cba539131bb189987c9b878a6217cb60260bb58ff03660e71d4bb3865eb77fed4bb4ff47a3e01124f4621fb
|
|
@@ -1105,9 +1105,10 @@ module Odin
|
|
|
1105
1105
|
def process_mapping(mapping, source, context, output, modifier_prefix: "")
|
|
1106
1106
|
target = mapping.target_field
|
|
1107
1107
|
|
|
1108
|
-
#
|
|
1109
|
-
#
|
|
1110
|
-
|
|
1108
|
+
# An unrecognized `_`-prefixed target is a computation-only sink: it runs
|
|
1109
|
+
# for side effects (accumulators, counters) but is never emitted. This
|
|
1110
|
+
# lets several %accumulate/%set advance in one loop pass.
|
|
1111
|
+
if target.start_with?("_")
|
|
1111
1112
|
begin
|
|
1112
1113
|
evaluate(mapping.expression, context)
|
|
1113
1114
|
rescue StandardError => e
|
|
@@ -1115,7 +1116,6 @@ module Odin
|
|
|
1115
1116
|
end
|
|
1116
1117
|
return
|
|
1117
1118
|
end
|
|
1118
|
-
return if target.start_with?("_")
|
|
1119
1119
|
|
|
1120
1120
|
begin
|
|
1121
1121
|
mods = mapping_mods(mapping)
|
|
@@ -1532,17 +1532,10 @@ module Odin
|
|
|
1532
1532
|
verb_name = expr.verb_name
|
|
1533
1533
|
args = expr.arguments
|
|
1534
1534
|
|
|
1535
|
-
# Lazy evaluation for
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
return apply_extraction_directives(val, extraction_directives)
|
|
1540
|
-
when "cond"
|
|
1541
|
-
val = evaluate_cond(args, context)
|
|
1542
|
-
return apply_extraction_directives(val, extraction_directives)
|
|
1543
|
-
when "switch"
|
|
1544
|
-
val = evaluate_switch(args, context)
|
|
1545
|
-
return apply_extraction_directives(val, extraction_directives)
|
|
1535
|
+
# Lazy evaluation for control-flow verbs — apply extraction to result.
|
|
1536
|
+
if !context.strict_types && LAZY_VERBS.include?(verb_name) && !(expr.respond_to?(:custom) && expr.custom)
|
|
1537
|
+
handled, val = evaluate_lazy_verb(verb_name, args, context)
|
|
1538
|
+
return apply_extraction_directives(val, extraction_directives) if handled
|
|
1546
1539
|
end
|
|
1547
1540
|
|
|
1548
1541
|
# Eager evaluation: apply extraction directives to CopyExpr arguments
|
|
@@ -1579,18 +1572,19 @@ module Odin
|
|
|
1579
1572
|
invoke_verb(verb_name, evaluated_args, context)
|
|
1580
1573
|
end
|
|
1581
1574
|
|
|
1575
|
+
# Control-flow verbs that evaluate the condition first and run only the
|
|
1576
|
+
# selected branch; and/or/coalesce short-circuit. Strict-types mode
|
|
1577
|
+
# evaluates eagerly so every argument is validated.
|
|
1578
|
+
LAZY_VERBS = %w[ifElse ifNull ifEmpty coalesce and or cond switch].freeze
|
|
1579
|
+
|
|
1582
1580
|
def evaluate_verb(expr, context)
|
|
1583
1581
|
verb_name = expr.verb_name
|
|
1584
1582
|
args = expr.arguments
|
|
1585
1583
|
|
|
1586
|
-
# Lazy evaluation for
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
return
|
|
1590
|
-
when "cond"
|
|
1591
|
-
return evaluate_cond(args, context)
|
|
1592
|
-
when "switch"
|
|
1593
|
-
return evaluate_switch(args, context)
|
|
1584
|
+
# Lazy evaluation for control-flow verbs (skipped under strict types).
|
|
1585
|
+
if !context.strict_types && LAZY_VERBS.include?(verb_name) && !(expr.respond_to?(:custom) && expr.custom)
|
|
1586
|
+
handled, value = evaluate_lazy_verb(verb_name, args, context)
|
|
1587
|
+
return value if handled
|
|
1594
1588
|
end
|
|
1595
1589
|
|
|
1596
1590
|
# Eager evaluation for all other verbs
|
|
@@ -1618,6 +1612,49 @@ module Odin
|
|
|
1618
1612
|
invoke_verb(verb_name, evaluated_args, context)
|
|
1619
1613
|
end
|
|
1620
1614
|
|
|
1615
|
+
# Dispatch a lazy control-flow verb. Returns [handled, value]; handled is
|
|
1616
|
+
# false when arity is too low, so the caller falls back to eager paths.
|
|
1617
|
+
def evaluate_lazy_verb(verb_name, args, context)
|
|
1618
|
+
ev = ->(i) { evaluate(args[i], context) }
|
|
1619
|
+
case verb_name
|
|
1620
|
+
when "ifElse"
|
|
1621
|
+
return [false, Types::DynValue.of_null] if args.length < 3
|
|
1622
|
+
[true, ev.call(0).truthy? ? ev.call(1) : ev.call(2)]
|
|
1623
|
+
when "ifNull"
|
|
1624
|
+
return [false, Types::DynValue.of_null] if args.length < 2
|
|
1625
|
+
v0 = ev.call(0)
|
|
1626
|
+
[true, v0.null? ? ev.call(1) : v0]
|
|
1627
|
+
when "ifEmpty"
|
|
1628
|
+
return [false, Types::DynValue.of_null] if args.length < 2
|
|
1629
|
+
v0 = ev.call(0)
|
|
1630
|
+
[true, lazy_empty?(v0) ? ev.call(1) : v0]
|
|
1631
|
+
when "coalesce"
|
|
1632
|
+
args.each_index do |i|
|
|
1633
|
+
v = ev.call(i)
|
|
1634
|
+
return [true, v] unless v.null?
|
|
1635
|
+
end
|
|
1636
|
+
[true, Types::DynValue.of_null]
|
|
1637
|
+
when "and"
|
|
1638
|
+
return [false, Types::DynValue.of_null] if args.length < 2
|
|
1639
|
+
return [true, Types::DynValue.of_bool(false)] unless ev.call(0).truthy?
|
|
1640
|
+
[true, Types::DynValue.of_bool(ev.call(1).truthy?)]
|
|
1641
|
+
when "or"
|
|
1642
|
+
return [false, Types::DynValue.of_null] if args.length < 2
|
|
1643
|
+
return [true, Types::DynValue.of_bool(true)] if ev.call(0).truthy?
|
|
1644
|
+
[true, Types::DynValue.of_bool(ev.call(1).truthy?)]
|
|
1645
|
+
when "cond"
|
|
1646
|
+
[true, evaluate_cond(args, context)]
|
|
1647
|
+
when "switch"
|
|
1648
|
+
[true, evaluate_switch(args, context)]
|
|
1649
|
+
else
|
|
1650
|
+
[false, Types::DynValue.of_null]
|
|
1651
|
+
end
|
|
1652
|
+
end
|
|
1653
|
+
|
|
1654
|
+
def lazy_empty?(v)
|
|
1655
|
+
v.nil? || v.null? || (v.string? && v.value.empty?)
|
|
1656
|
+
end
|
|
1657
|
+
|
|
1621
1658
|
def evaluate_if_else(args, context)
|
|
1622
1659
|
return Types::DynValue.of_null if args.length < 3
|
|
1623
1660
|
|
|
@@ -2735,6 +2772,31 @@ module Odin
|
|
|
2735
2772
|
.gsub("'", "'")
|
|
2736
2773
|
end
|
|
2737
2774
|
|
|
2775
|
+
# True when every backslash escape in a JSON string body is well-formed.
|
|
2776
|
+
def valid_json_escapes?(s)
|
|
2777
|
+
i = 0
|
|
2778
|
+
len = s.length
|
|
2779
|
+
while i < len
|
|
2780
|
+
if s[i] == "\\"
|
|
2781
|
+
i += 1
|
|
2782
|
+
return false if i >= len
|
|
2783
|
+
case s[i]
|
|
2784
|
+
when "\"", "\\", "/", "b", "f", "n", "r", "t"
|
|
2785
|
+
i += 1
|
|
2786
|
+
when "u"
|
|
2787
|
+
hex = s[(i + 1), 4]
|
|
2788
|
+
return false if hex.nil? || hex.length < 4 || hex !~ /\A[0-9a-fA-F]{4}\z/
|
|
2789
|
+
i += 5
|
|
2790
|
+
else
|
|
2791
|
+
return false
|
|
2792
|
+
end
|
|
2793
|
+
else
|
|
2794
|
+
i += 1
|
|
2795
|
+
end
|
|
2796
|
+
end
|
|
2797
|
+
true
|
|
2798
|
+
end
|
|
2799
|
+
|
|
2738
2800
|
# ── Verb Registry ──
|
|
2739
2801
|
|
|
2740
2802
|
def build_verb_registry
|
|
@@ -2751,6 +2813,7 @@ module Odin
|
|
|
2751
2813
|
Verbs::AggregationVerbs.register(registry)
|
|
2752
2814
|
Verbs::ObjectVerbs.register(registry)
|
|
2753
2815
|
Verbs::GeoVerbs.register(registry)
|
|
2816
|
+
Verbs::ExtraVerbs.register(registry)
|
|
2754
2817
|
|
|
2755
2818
|
registry
|
|
2756
2819
|
end
|
|
@@ -2824,7 +2887,19 @@ module Odin
|
|
|
2824
2887
|
# Type checks
|
|
2825
2888
|
registry["typeOf"] = ->(args, _ctx) {
|
|
2826
2889
|
v = args[0]
|
|
2827
|
-
type_str =
|
|
2890
|
+
type_str =
|
|
2891
|
+
if v.nil?
|
|
2892
|
+
"null"
|
|
2893
|
+
else
|
|
2894
|
+
{
|
|
2895
|
+
null: "null", bool: "boolean", integer: "integer",
|
|
2896
|
+
float: "number", float_raw: "number", string: "string",
|
|
2897
|
+
array: "array", object: "object", date: "date",
|
|
2898
|
+
timestamp: "timestamp", time: "time", duration: "duration",
|
|
2899
|
+
currency: "currency", currency_raw: "currency", percent: "percent",
|
|
2900
|
+
reference: "reference", binary: "binary"
|
|
2901
|
+
}.fetch(v.type, "unknown")
|
|
2902
|
+
end
|
|
2828
2903
|
Types::DynValue.of_string(type_str)
|
|
2829
2904
|
}
|
|
2830
2905
|
registry["isString"] = ->(args, _ctx) { Types::DynValue.of_bool(args[0]&.string? || false) }
|
|
@@ -2836,9 +2911,9 @@ module Odin
|
|
|
2836
2911
|
|
|
2837
2912
|
# Coercion
|
|
2838
2913
|
registry["coerceString"] = ->(args, _ctx) { Types::DynValue.of_string(args[0]&.to_string || "") }
|
|
2839
|
-
registry["coerceNumber"] = ->(args, _ctx) {
|
|
2840
|
-
registry["coerceInteger"] = ->(args, _ctx) { Types::DynValue.of_integer(args[0]
|
|
2841
|
-
registry["coerceBoolean"] = ->(args, _ctx) { Types::DynValue.of_bool(args[0]
|
|
2914
|
+
registry["coerceNumber"] = ->(args, _ctx) { Verbs::NumericVerbs.numeric_result(Verbs::NumericVerbs.to_number(args[0])) }
|
|
2915
|
+
registry["coerceInteger"] = ->(args, _ctx) { Types::DynValue.of_integer(Verbs::NumericVerbs.to_number(args[0]).floor) }
|
|
2916
|
+
registry["coerceBoolean"] = ->(args, _ctx) { Types::DynValue.of_bool(Verbs::NumericVerbs.coerce_boolean(args[0])) }
|
|
2842
2917
|
|
|
2843
2918
|
# Arithmetic
|
|
2844
2919
|
registry["add"] = ->(args, _ctx) {
|
|
@@ -3126,19 +3201,20 @@ module Odin
|
|
|
3126
3201
|
Types::DynValue.of_bool(args[0]&.truthy? || args[1]&.truthy?)
|
|
3127
3202
|
}
|
|
3128
3203
|
|
|
3129
|
-
# Comparison
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
registry["
|
|
3140
|
-
|
|
3141
|
-
}
|
|
3204
|
+
# Comparison: numeric when both coerce to numbers, else lexical.
|
|
3205
|
+
compare = lambda do |a, b|
|
|
3206
|
+
an = Verbs::NumericVerbs.to_double(a)
|
|
3207
|
+
bn = Verbs::NumericVerbs.to_double(b)
|
|
3208
|
+
if !an.nil? && !bn.nil?
|
|
3209
|
+
an <=> bn
|
|
3210
|
+
else
|
|
3211
|
+
(a&.to_string || "") <=> (b&.to_string || "")
|
|
3212
|
+
end
|
|
3213
|
+
end
|
|
3214
|
+
registry["lt"] = ->(args, _ctx) { Types::DynValue.of_bool(compare.call(args[0], args[1]) < 0) }
|
|
3215
|
+
registry["lte"] = ->(args, _ctx) { Types::DynValue.of_bool(compare.call(args[0], args[1]) <= 0) }
|
|
3216
|
+
registry["gt"] = ->(args, _ctx) { Types::DynValue.of_bool(compare.call(args[0], args[1]) > 0) }
|
|
3217
|
+
registry["gte"] = ->(args, _ctx) { Types::DynValue.of_bool(compare.call(args[0], args[1]) >= 0) }
|
|
3142
3218
|
|
|
3143
3219
|
# String operations
|
|
3144
3220
|
registry["contains"] = ->(args, _ctx) {
|
|
@@ -3212,13 +3288,10 @@ module Odin
|
|
|
3212
3288
|
end
|
|
3213
3289
|
}
|
|
3214
3290
|
|
|
3215
|
-
# Assertions
|
|
3291
|
+
# Assertions: pass the value through when truthy, else null.
|
|
3216
3292
|
registry["assert"] = ->(args, _ctx) {
|
|
3217
3293
|
condition = args[0]
|
|
3218
|
-
|
|
3219
|
-
raise TransformError.new(msg) unless condition&.truthy?
|
|
3220
|
-
|
|
3221
|
-
Types::DynValue.of_bool(true)
|
|
3294
|
+
condition&.truthy? ? condition : Types::DynValue.of_null
|
|
3222
3295
|
}
|
|
3223
3296
|
|
|
3224
3297
|
# Switch/cond handled via lazy evaluation in evaluate_verb
|
|
@@ -3266,16 +3339,24 @@ module Odin
|
|
|
3266
3339
|
}
|
|
3267
3340
|
|
|
3268
3341
|
registry["titleCase"] = ->(args, _ctx) {
|
|
3269
|
-
|
|
3270
|
-
Types::DynValue.
|
|
3342
|
+
v = args[0]
|
|
3343
|
+
next Types::DynValue.of_null if v.nil? || v.null?
|
|
3344
|
+
s = v.to_string
|
|
3345
|
+
next Types::DynValue.of_string("") if s.empty?
|
|
3346
|
+
result = s.split(/\s+/).map { |w| w.empty? ? "" : w[0].upcase + w[1..].downcase }.join(" ")
|
|
3347
|
+
Types::DynValue.of_string(result)
|
|
3271
3348
|
}
|
|
3272
3349
|
|
|
3273
3350
|
registry["slugify"] = ->(args, _ctx) {
|
|
3274
|
-
|
|
3351
|
+
v = args[0]
|
|
3352
|
+
next Types::DynValue.of_null if v.nil? || v.null?
|
|
3353
|
+
s = v.to_string
|
|
3354
|
+
next Types::DynValue.of_string("") if s.empty?
|
|
3275
3355
|
result = s.downcase
|
|
3276
|
-
.gsub(/[^a-z0-
|
|
3277
|
-
.
|
|
3278
|
-
.gsub(
|
|
3356
|
+
.gsub(/[^a-z0-9_\s-]/, "")
|
|
3357
|
+
.gsub(/[\s_]+/, "-")
|
|
3358
|
+
.gsub(/-+/, "-")
|
|
3359
|
+
.gsub(/\A-+|-+\z/, "")
|
|
3279
3360
|
Types::DynValue.of_string(result)
|
|
3280
3361
|
}
|
|
3281
3362
|
|
|
@@ -3352,12 +3433,15 @@ module Odin
|
|
|
3352
3433
|
}
|
|
3353
3434
|
|
|
3354
3435
|
registry["split"] = ->(args, _ctx) {
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3436
|
+
v = args[0]
|
|
3437
|
+
next Types::DynValue.of_null if v.nil? || v.null?
|
|
3438
|
+
s = v.to_string
|
|
3439
|
+
delimiter = args[1]&.to_string || ""
|
|
3440
|
+
parts = delimiter.empty? ? [s] : s.split(delimiter, -1)
|
|
3441
|
+
# If a third argument (index) is provided, return that element.
|
|
3359
3442
|
if args[2] && !args[2].null?
|
|
3360
|
-
idx = args[2]
|
|
3443
|
+
idx = Verbs::NumericVerbs.to_double(args[2])&.to_i || 0
|
|
3444
|
+
idx += parts.length if idx < 0
|
|
3361
3445
|
if idx >= 0 && idx < parts.length
|
|
3362
3446
|
Types::DynValue.of_string(parts[idx])
|
|
3363
3447
|
else
|
|
@@ -3388,12 +3472,14 @@ module Odin
|
|
|
3388
3472
|
}
|
|
3389
3473
|
|
|
3390
3474
|
registry["match"] = ->(args, _ctx) {
|
|
3475
|
+
next Types::DynValue.of_null if args.length < 2
|
|
3391
3476
|
s = args[0]&.to_string || ""
|
|
3392
3477
|
pattern = args[1]&.to_string || ""
|
|
3393
3478
|
begin
|
|
3479
|
+
next Types::DynValue.of_null if pattern.length > 256 || s.length > 100_000
|
|
3394
3480
|
Types::DynValue.of_bool(!!(s =~ Regexp.new(pattern)))
|
|
3395
3481
|
rescue RegexpError
|
|
3396
|
-
Types::DynValue.
|
|
3482
|
+
Types::DynValue.of_null
|
|
3397
3483
|
end
|
|
3398
3484
|
}
|
|
3399
3485
|
registry["matches"] = registry["match"]
|
|
@@ -3437,20 +3523,24 @@ module Odin
|
|
|
3437
3523
|
}
|
|
3438
3524
|
|
|
3439
3525
|
registry["repeat"] = ->(args, _ctx) {
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
count =
|
|
3443
|
-
Types::DynValue.
|
|
3526
|
+
v = args[0]
|
|
3527
|
+
next Types::DynValue.of_null if v.nil? || v.null?
|
|
3528
|
+
count = Verbs::NumericVerbs.to_double(args[1])&.to_i
|
|
3529
|
+
next Types::DynValue.of_null if count.nil? || count < 0
|
|
3530
|
+
count = 100_000 if count > 100_000
|
|
3531
|
+
Types::DynValue.of_string(v.to_string * count)
|
|
3444
3532
|
}
|
|
3445
3533
|
|
|
3446
3534
|
registry["replaceRegex"] = ->(args, _ctx) {
|
|
3447
|
-
|
|
3535
|
+
next Types::DynValue.of_null if args.length < 3 || args[0].nil? || args[0].null?
|
|
3536
|
+
s = args[0].to_string
|
|
3448
3537
|
pattern = args[1]&.to_string || ""
|
|
3449
3538
|
replacement = args[2]&.to_string || ""
|
|
3450
3539
|
begin
|
|
3540
|
+
next Types::DynValue.of_null if pattern.length > 256 || s.length > 100_000
|
|
3451
3541
|
Types::DynValue.of_string(s.gsub(Regexp.new(pattern), replacement))
|
|
3452
3542
|
rescue RegexpError
|
|
3453
|
-
Types::DynValue.
|
|
3543
|
+
Types::DynValue.of_null
|
|
3454
3544
|
end
|
|
3455
3545
|
}
|
|
3456
3546
|
|
|
@@ -3531,10 +3621,17 @@ module Odin
|
|
|
3531
3621
|
}
|
|
3532
3622
|
|
|
3533
3623
|
registry["base64Decode"] = ->(args, _ctx) {
|
|
3534
|
-
|
|
3624
|
+
v = args[0]
|
|
3625
|
+
next Types::DynValue.of_null if v.nil? || v.null?
|
|
3626
|
+
s = v.to_string.tr("-_", "+/")
|
|
3627
|
+
pad = (4 - s.length % 4) % 4
|
|
3628
|
+
s += "=" * pad
|
|
3535
3629
|
require "base64"
|
|
3536
3630
|
begin
|
|
3537
|
-
|
|
3631
|
+
decoded = Base64.strict_decode64(s)
|
|
3632
|
+
decoded.force_encoding("UTF-8")
|
|
3633
|
+
next Types::DynValue.of_null unless decoded.valid_encoding?
|
|
3634
|
+
Types::DynValue.of_string(decoded)
|
|
3538
3635
|
rescue ArgumentError
|
|
3539
3636
|
Types::DynValue.of_null
|
|
3540
3637
|
end
|
|
@@ -3546,42 +3643,67 @@ module Odin
|
|
|
3546
3643
|
}
|
|
3547
3644
|
|
|
3548
3645
|
registry["hexDecode"] = ->(args, _ctx) {
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3646
|
+
v = args[0]
|
|
3647
|
+
next Types::DynValue.of_null if v.nil? || v.null?
|
|
3648
|
+
s = v.to_string
|
|
3649
|
+
next Types::DynValue.of_null if s.length.odd? || s.match?(/[^0-9a-fA-F]/)
|
|
3650
|
+
decoded = [s].pack("H*")
|
|
3651
|
+
decoded.force_encoding("UTF-8")
|
|
3652
|
+
next Types::DynValue.of_null unless decoded.valid_encoding?
|
|
3653
|
+
Types::DynValue.of_string(decoded)
|
|
3555
3654
|
}
|
|
3556
3655
|
|
|
3557
3656
|
registry["urlEncode"] = ->(args, _ctx) {
|
|
3558
|
-
|
|
3657
|
+
v = args[0]
|
|
3658
|
+
next Types::DynValue.of_null if v.nil? || v.null?
|
|
3559
3659
|
require "uri"
|
|
3560
|
-
Types::DynValue.of_string(URI.encode_www_form_component(
|
|
3660
|
+
Types::DynValue.of_string(URI.encode_www_form_component(v.to_string, "UTF-8").gsub("+", "%20").gsub("%7E", "~"))
|
|
3561
3661
|
}
|
|
3562
3662
|
|
|
3563
3663
|
registry["urlDecode"] = ->(args, _ctx) {
|
|
3564
|
-
|
|
3664
|
+
v = args[0]
|
|
3665
|
+
next Types::DynValue.of_null if v.nil? || v.null?
|
|
3666
|
+
s = v.to_string
|
|
3667
|
+
# Reject malformed percent-encoding (% not followed by two hex digits).
|
|
3668
|
+
next Types::DynValue.of_null if s.scan(/%(..?|.?)/).any? { |seq| seq[0].length != 2 || seq[0].match?(/[^0-9a-fA-F]/) }
|
|
3565
3669
|
require "uri"
|
|
3566
3670
|
begin
|
|
3567
|
-
|
|
3671
|
+
decoded = URI.decode_www_form_component(s, "UTF-8")
|
|
3672
|
+
next Types::DynValue.of_null unless decoded.valid_encoding?
|
|
3673
|
+
Types::DynValue.of_string(decoded)
|
|
3568
3674
|
rescue ArgumentError
|
|
3569
|
-
Types::DynValue.
|
|
3675
|
+
Types::DynValue.of_null
|
|
3570
3676
|
end
|
|
3571
3677
|
}
|
|
3572
3678
|
|
|
3573
3679
|
registry["jsonEncode"] = ->(args, _ctx) {
|
|
3574
3680
|
v = args[0]
|
|
3681
|
+
next Types::DynValue.of_null if v.nil? || v.null?
|
|
3575
3682
|
require "json"
|
|
3576
|
-
|
|
3683
|
+
if v.object? || v.array?
|
|
3684
|
+
next Types::DynValue.of_string(JSON.generate(v.to_ruby))
|
|
3685
|
+
end
|
|
3686
|
+
# Scalars: JSON-escape the string and drop the surrounding quotes.
|
|
3687
|
+
encoded = JSON.generate(v.to_string)
|
|
3688
|
+
Types::DynValue.of_string(encoded[1...-1])
|
|
3577
3689
|
}
|
|
3578
3690
|
|
|
3579
3691
|
registry["jsonDecode"] = ->(args, _ctx) {
|
|
3580
|
-
|
|
3692
|
+
v = args[0]
|
|
3693
|
+
next Types::DynValue.of_null if v.nil? || v.null?
|
|
3694
|
+
s = v.to_string
|
|
3581
3695
|
require "json"
|
|
3696
|
+
if s.start_with?("{", "[")
|
|
3697
|
+
begin
|
|
3698
|
+
parsed = JSON.parse(s)
|
|
3699
|
+
next Types::DynValue.from_ruby(parsed) if parsed.is_a?(Hash) || parsed.is_a?(Array)
|
|
3700
|
+
rescue JSON::ParserError
|
|
3701
|
+
end
|
|
3702
|
+
end
|
|
3703
|
+
# Unescape as a JSON string; an invalid escape yields null.
|
|
3704
|
+
next Types::DynValue.of_null unless valid_json_escapes?(s)
|
|
3582
3705
|
begin
|
|
3583
|
-
|
|
3584
|
-
Types::DynValue.from_ruby(parsed)
|
|
3706
|
+
Types::DynValue.of_string(JSON.parse("\"#{s}\""))
|
|
3585
3707
|
rescue JSON::ParserError
|
|
3586
3708
|
Types::DynValue.of_null
|
|
3587
3709
|
end
|
|
@@ -3678,13 +3800,38 @@ module Odin
|
|
|
3678
3800
|
registry["coerceDate"] = ->(args, _ctx) {
|
|
3679
3801
|
v = args[0]
|
|
3680
3802
|
return Types::DynValue.of_null if v.nil? || v.null?
|
|
3803
|
+
return v if v.type == :date
|
|
3804
|
+
if v.type == :timestamp
|
|
3805
|
+
s = v.to_string
|
|
3806
|
+
s = s[0...s.index("T")] if s.include?("T")
|
|
3807
|
+
next Types::DynValue.of_date(s)
|
|
3808
|
+
end
|
|
3809
|
+
|
|
3681
3810
|
s = v.to_string.strip
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3811
|
+
next Types::DynValue.of_null if s.empty?
|
|
3812
|
+
|
|
3813
|
+
valid_ymd = ->(y, mo, d) { mo.between?(1, 12) && d >= 1 && Date.valid_date?(y, mo, d) }
|
|
3814
|
+
|
|
3815
|
+
# yyyy-MM-dd prefix
|
|
3816
|
+
if s.length >= 10 && s[4] == "-" && s[7] == "-" &&
|
|
3817
|
+
s[0, 4] =~ /\A\d{4}\z/ && s[5, 2] =~ /\A\d{2}\z/ && s[8, 2] =~ /\A\d{2}\z/
|
|
3818
|
+
next Types::DynValue.of_date(s[0, 10])
|
|
3687
3819
|
end
|
|
3820
|
+
|
|
3821
|
+
# Compact YYYYMMDD
|
|
3822
|
+
if (m = s.match(/\A(\d{4})(\d{2})(\d{2})\z/))
|
|
3823
|
+
y, mo, d = m[1].to_i, m[2].to_i, m[3].to_i
|
|
3824
|
+
next valid_ymd.call(y, mo, d) ? Types::DynValue.of_date(format("%04d-%02d-%02d", y, mo, d)) : Types::DynValue.of_null
|
|
3825
|
+
end
|
|
3826
|
+
|
|
3827
|
+
# Slash MM/DD/YYYY (US), or DD/MM/YYYY when first > 12
|
|
3828
|
+
if (m = s.match(%r{\A(\d{1,2})/(\d{1,2})/(\d{4})\z}))
|
|
3829
|
+
first, second, y = m[1].to_i, m[2].to_i, m[3].to_i
|
|
3830
|
+
mo, d = first > 12 ? [second, first] : [first, second]
|
|
3831
|
+
next valid_ymd.call(y, mo, d) ? Types::DynValue.of_date(format("%04d-%02d-%02d", y, mo, d)) : Types::DynValue.of_null
|
|
3832
|
+
end
|
|
3833
|
+
|
|
3834
|
+
Types::DynValue.of_null
|
|
3688
3835
|
}
|
|
3689
3836
|
|
|
3690
3837
|
registry["coerceTimestamp"] = ->(args, _ctx) {
|
|
@@ -3715,23 +3862,19 @@ module Odin
|
|
|
3715
3862
|
registry["toObject"] = ->(args, _ctx) {
|
|
3716
3863
|
v = args[0]
|
|
3717
3864
|
if v.nil? || v.null?
|
|
3718
|
-
Types::DynValue.
|
|
3865
|
+
Types::DynValue.of_null
|
|
3719
3866
|
elsif v.object?
|
|
3720
3867
|
v
|
|
3721
3868
|
elsif v.array?
|
|
3722
3869
|
items = v.value || []
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
Types::DynValue.of_object(obj)
|
|
3728
|
-
else
|
|
3729
|
-
obj = {}
|
|
3730
|
-
items.each_with_index { |item, i| obj[i.to_s] = item }
|
|
3731
|
-
Types::DynValue.of_object(obj)
|
|
3870
|
+
obj = {}
|
|
3871
|
+
items.each do |item|
|
|
3872
|
+
pair = to_object_pair(item)
|
|
3873
|
+
obj[pair[0]] = pair[1] if pair
|
|
3732
3874
|
end
|
|
3875
|
+
obj.empty? ? Types::DynValue.of_null : Types::DynValue.of_object(obj)
|
|
3733
3876
|
else
|
|
3734
|
-
Types::DynValue.
|
|
3877
|
+
Types::DynValue.of_null
|
|
3735
3878
|
end
|
|
3736
3879
|
}
|
|
3737
3880
|
|
|
@@ -3795,11 +3938,8 @@ module Odin
|
|
|
3795
3938
|
digits = raw.gsub(/\D/, "")
|
|
3796
3939
|
formatted = case country
|
|
3797
3940
|
when "US", "CA"
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
elsif digits.length == 11 && digits.start_with?("1")
|
|
3801
|
-
"+1 (#{digits[1..3]}) #{digits[4..6]}-#{digits[7..10]}"
|
|
3802
|
-
end
|
|
3941
|
+
d = (digits.length == 11 && digits.start_with?("1")) ? digits[1..] : digits
|
|
3942
|
+
"(#{d[0..2]}) #{d[3..5]}-#{d[6..9]}" if d.length == 10
|
|
3803
3943
|
when "GB"
|
|
3804
3944
|
if digits.length == 11 && digits.start_with?("0")
|
|
3805
3945
|
"+44 #{digits[1..4]} #{digits[5..10]}"
|