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.
- checksums.yaml +4 -4
- data/lib/odin/export.rb +1 -1
- data/lib/odin/forms/accessibility.rb +95 -0
- data/lib/odin/forms/css.rb +42 -0
- data/lib/odin/forms/parser.rb +719 -0
- data/lib/odin/forms/renderer.rb +534 -0
- data/lib/odin/forms/types.rb +102 -0
- data/lib/odin/forms/units.rb +41 -0
- data/lib/odin/forms.rb +55 -0
- data/lib/odin/parsing/parser.rb +25 -1
- data/lib/odin/parsing/tokenizer.rb +38 -20
- data/lib/odin/parsing/value_parser.rb +65 -7
- data/lib/odin/resolver/import_resolver.rb +40 -12
- data/lib/odin/resolver/type_registry.rb +54 -0
- data/lib/odin/transform/format_exporters.rb +88 -48
- data/lib/odin/transform/source_parsers.rb +2 -2
- data/lib/odin/transform/transform_engine.rb +1388 -246
- data/lib/odin/transform/transform_expr.rb +222 -0
- data/lib/odin/transform/transform_parser.rb +377 -19
- data/lib/odin/transform/transform_types.rb +23 -7
- data/lib/odin/transform/verb_context.rb +19 -1
- data/lib/odin/transform/verbs/aggregation_verbs.rb +2 -1
- data/lib/odin/transform/verbs/collection_verbs.rb +164 -89
- data/lib/odin/transform/verbs/datetime_verbs.rb +86 -15
- data/lib/odin/transform/verbs/extra_verbs.rb +613 -0
- data/lib/odin/transform/verbs/financial_verbs.rb +116 -27
- data/lib/odin/transform/verbs/geo_verbs.rb +7 -0
- data/lib/odin/transform/verbs/numeric_verbs.rb +85 -64
- data/lib/odin/transform/verbs/object_verbs.rb +31 -26
- data/lib/odin/types/errors.rb +9 -1
- data/lib/odin/types/schema.rb +20 -3
- data/lib/odin/utils/format_utils.rb +31 -15
- data/lib/odin/validation/format_validators.rb +7 -9
- data/lib/odin/validation/invariant_evaluator.rb +410 -0
- data/lib/odin/validation/schema_definition_validator.rb +357 -0
- data/lib/odin/validation/schema_parser.rb +234 -21
- data/lib/odin/validation/validator.rb +281 -123
- data/lib/odin/version.rb +1 -1
- data/lib/odin.rb +100 -4
- 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
|