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
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Odin
4
+ module Transform
5
+ # Compiles an infix arithmetic formula string into a tree of existing verbs
6
+ # at parse time. No runtime evaluator: the result is an ordinary verb
7
+ # expression, so arithmetic runs through the deterministic verbs (add,
8
+ # subtract, multiply, divide, mod, negate, pow, and a whitelist of numeric
9
+ # functions). Variables resolve under an explicit bindings object passed as
10
+ # the second argument: in %expr "a + b" @.vars, the name a reads @.vars.a.
11
+ #
12
+ # Precedence, high to low:
13
+ # 1. parentheses, function call
14
+ # 2. ^ power (right-associative)
15
+ # 3. unary - / + (looser than ^, so -2^2 = -(2^2) = -4; (-2)^2 = 4)
16
+ # 4. * / % (left-associative)
17
+ # 5. + - (left-associative)
18
+ module Expr
19
+ class ExprSyntaxError < StandardError
20
+ attr_reader :code
21
+
22
+ def initialize(message)
23
+ @code = "T015"
24
+ super("Invalid %expr formula: #{message}")
25
+ end
26
+ end
27
+
28
+ BINARY_OP = { "+" => "add", "-" => "subtract", "*" => "multiply", "/" => "divide", "%" => "mod" }.freeze
29
+
30
+ FUNCTIONS = {
31
+ "abs" => { verb: "abs", min: 1, max: 1 },
32
+ "floor" => { verb: "floor", min: 1, max: 1 },
33
+ "ceil" => { verb: "ceil", min: 1, max: 1 },
34
+ "trunc" => { verb: "trunc", min: 1, max: 1 },
35
+ "sqrt" => { verb: "sqrt", min: 1, max: 1 },
36
+ "round" => { verb: "round", min: 1, max: 2 },
37
+ "pow" => { verb: "pow", min: 2, max: 2 },
38
+ "min" => { verb: "minOf", min: 1, max: Float::INFINITY },
39
+ "max" => { verb: "maxOf", min: 1, max: Float::INFINITY }
40
+ }.freeze
41
+
42
+ Token = Struct.new(:kind, :value, :is_float)
43
+
44
+ module_function
45
+
46
+ def compile(formula, binding_path = nil)
47
+ Parser.new(tokenize(formula), binding_path).parse
48
+ end
49
+
50
+ def tokenize(src)
51
+ tokens = []
52
+ i = 0
53
+ len = src.length
54
+ while i < len
55
+ c = src[i]
56
+ if c =~ /\s/
57
+ i += 1
58
+ next
59
+ end
60
+ if c =~ /[0-9]/
61
+ j = i
62
+ is_float = false
63
+ j += 1 while j < len && src[j] =~ /[0-9]/
64
+ if src[j] == "."
65
+ is_float = true
66
+ j += 1
67
+ j += 1 while j < len && src[j] =~ /[0-9]/
68
+ end
69
+ if src[j] == "e" || src[j] == "E"
70
+ is_float = true
71
+ j += 1
72
+ j += 1 if src[j] == "+" || src[j] == "-"
73
+ j += 1 while j < len && src[j] =~ /[0-9]/
74
+ end
75
+ tokens << Token.new(:num, src[i...j], is_float)
76
+ i = j
77
+ next
78
+ end
79
+ if c =~ /[A-Za-z_]/
80
+ j = i
81
+ j += 1 while j < len && src[j] =~ /[A-Za-z0-9_.]/
82
+ tokens << Token.new(:ident, src[i...j], false)
83
+ i = j
84
+ next
85
+ end
86
+ case c
87
+ when "(" then tokens << Token.new(:lparen, c, false)
88
+ when ")" then tokens << Token.new(:rparen, c, false)
89
+ when "," then tokens << Token.new(:comma, c, false)
90
+ when "+", "-", "*", "/", "%", "^" then tokens << Token.new(:op, c, false)
91
+ else raise ExprSyntaxError, "unexpected character '#{c}'"
92
+ end
93
+ i += 1
94
+ end
95
+ tokens
96
+ end
97
+
98
+ def literal(text, is_float)
99
+ value = is_float ? Types::DynValue.of_float(text.to_f) : Types::DynValue.of_integer(text.to_i)
100
+ LiteralExpr.new(value)
101
+ end
102
+
103
+ def verb_node(verb, args)
104
+ VerbExpr.new(verb, args, custom: false)
105
+ end
106
+
107
+ class Parser
108
+ def initialize(tokens, binding_path)
109
+ @tokens = tokens
110
+ @binding_path = binding_path
111
+ @pos = 0
112
+ end
113
+
114
+ def parse
115
+ raise ExprSyntaxError, "empty formula" if @tokens.empty?
116
+ expr = parse_additive
117
+ raise ExprSyntaxError, "unexpected token '#{peek.value}'" if @pos < @tokens.length
118
+ expr
119
+ end
120
+
121
+ private
122
+
123
+ def peek
124
+ @tokens[@pos]
125
+ end
126
+
127
+ def advance
128
+ t = @tokens[@pos]
129
+ @pos += 1
130
+ t
131
+ end
132
+
133
+ def parse_additive
134
+ left = parse_multiplicative
135
+ while peek&.kind == :op && (peek.value == "+" || peek.value == "-")
136
+ op = advance.value
137
+ right = parse_multiplicative
138
+ left = Expr.verb_node(BINARY_OP[op], [left, right])
139
+ end
140
+ left
141
+ end
142
+
143
+ def parse_multiplicative
144
+ left = parse_unary
145
+ while peek&.kind == :op && %w[* / %].include?(peek.value)
146
+ op = advance.value
147
+ right = parse_unary
148
+ left = Expr.verb_node(BINARY_OP[op], [left, right])
149
+ end
150
+ left
151
+ end
152
+
153
+ def parse_unary
154
+ t = peek
155
+ if t&.kind == :op && (t.value == "-" || t.value == "+")
156
+ advance
157
+ operand = parse_unary
158
+ return t.value == "-" ? Expr.verb_node("negate", [operand]) : operand
159
+ end
160
+ parse_power
161
+ end
162
+
163
+ def parse_power
164
+ base = parse_primary
165
+ if peek&.kind == :op && peek.value == "^"
166
+ advance
167
+ exponent = parse_unary
168
+ return Expr.verb_node("pow", [base, exponent])
169
+ end
170
+ base
171
+ end
172
+
173
+ def parse_primary
174
+ t = advance
175
+ raise ExprSyntaxError, "unexpected end of formula" if t.nil?
176
+
177
+ case t.kind
178
+ when :num
179
+ Expr.literal(t.value, t.is_float)
180
+ when :lparen
181
+ inner = parse_additive
182
+ close = advance
183
+ raise ExprSyntaxError, "missing closing parenthesis" if close.nil? || close.kind != :rparen
184
+ inner
185
+ when :ident
186
+ return parse_call(t.value) if peek&.kind == :lparen
187
+ if @binding_path.nil?
188
+ raise ExprSyntaxError, "variable '#{t.value}' requires a bindings object, e.g. %expr \"...\" @.vars"
189
+ end
190
+ CopyExpr.new("#{@binding_path}.#{t.value}")
191
+ else
192
+ raise ExprSyntaxError, "unexpected token '#{t.value}'"
193
+ end
194
+ end
195
+
196
+ def parse_call(name)
197
+ fn = FUNCTIONS[name]
198
+ raise ExprSyntaxError, "unknown function '#{name}'" if fn.nil?
199
+
200
+ advance # consume '('
201
+ args = []
202
+ if peek&.kind != :rparen
203
+ args << parse_additive
204
+ while peek&.kind == :comma
205
+ advance
206
+ args << parse_additive
207
+ end
208
+ end
209
+ close = advance
210
+ raise ExprSyntaxError, "missing ) after #{name}(" if close.nil? || close.kind != :rparen
211
+
212
+ if args.length < fn[:min] || args.length > fn[:max]
213
+ bound = fn[:min] == fn[:max] ? fn[:min].to_s : "#{fn[:min]}-#{fn[:max]}"
214
+ raise ExprSyntaxError, "#{name}() takes #{bound} arguments, got #{args.length}"
215
+ end
216
+ args << LiteralExpr.new(Types::DynValue.of_integer(0)) if name == "round" && args.length == 1
217
+ Expr.verb_node(fn[:verb], args)
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end