odin-foundation 1.0.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 +7 -0
- data/lib/odin/diff/differ.rb +115 -0
- data/lib/odin/diff/patcher.rb +64 -0
- data/lib/odin/export.rb +330 -0
- data/lib/odin/parsing/parser.rb +1193 -0
- data/lib/odin/parsing/token.rb +26 -0
- data/lib/odin/parsing/token_type.rb +40 -0
- data/lib/odin/parsing/tokenizer.rb +825 -0
- data/lib/odin/parsing/value_parser.rb +322 -0
- data/lib/odin/resolver/import_resolver.rb +137 -0
- data/lib/odin/serialization/canonicalize.rb +112 -0
- data/lib/odin/serialization/stringify.rb +582 -0
- data/lib/odin/transform/format_exporters.rb +819 -0
- data/lib/odin/transform/source_parsers.rb +385 -0
- data/lib/odin/transform/transform_engine.rb +2837 -0
- data/lib/odin/transform/transform_parser.rb +979 -0
- data/lib/odin/transform/transform_types.rb +278 -0
- data/lib/odin/transform/verb_context.rb +87 -0
- data/lib/odin/transform/verbs/aggregation_verbs.rb +106 -0
- data/lib/odin/transform/verbs/collection_verbs.rb +640 -0
- data/lib/odin/transform/verbs/datetime_verbs.rb +602 -0
- data/lib/odin/transform/verbs/financial_verbs.rb +356 -0
- data/lib/odin/transform/verbs/geo_verbs.rb +125 -0
- data/lib/odin/transform/verbs/numeric_verbs.rb +434 -0
- data/lib/odin/transform/verbs/object_verbs.rb +123 -0
- data/lib/odin/types/array_item.rb +42 -0
- data/lib/odin/types/diff.rb +89 -0
- data/lib/odin/types/directive.rb +28 -0
- data/lib/odin/types/document.rb +92 -0
- data/lib/odin/types/document_builder.rb +67 -0
- data/lib/odin/types/dyn_value.rb +270 -0
- data/lib/odin/types/errors.rb +149 -0
- data/lib/odin/types/modifiers.rb +45 -0
- data/lib/odin/types/ordered_map.rb +79 -0
- data/lib/odin/types/schema.rb +262 -0
- data/lib/odin/types/value_type.rb +28 -0
- data/lib/odin/types/values.rb +618 -0
- data/lib/odin/types.rb +12 -0
- data/lib/odin/utils/format_utils.rb +186 -0
- data/lib/odin/utils/path_utils.rb +25 -0
- data/lib/odin/utils/security_limits.rb +17 -0
- data/lib/odin/validation/format_validators.rb +238 -0
- data/lib/odin/validation/redos_protection.rb +102 -0
- data/lib/odin/validation/schema_parser.rb +813 -0
- data/lib/odin/validation/schema_serializer.rb +262 -0
- data/lib/odin/validation/validator.rb +1061 -0
- data/lib/odin/version.rb +5 -0
- data/lib/odin.rb +90 -0
- metadata +160 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module Odin
|
|
6
|
+
module Validation
|
|
7
|
+
class SchemaSerializer
|
|
8
|
+
# Serialize an OdinSchema back to ODIN text
|
|
9
|
+
def serialize(schema)
|
|
10
|
+
lines = []
|
|
11
|
+
|
|
12
|
+
# 1. Metadata header
|
|
13
|
+
unless schema.metadata.empty?
|
|
14
|
+
lines << "{$}"
|
|
15
|
+
schema.metadata.each do |key, value|
|
|
16
|
+
lines << "#{key} = \"#{escape_string(value.to_s)}\""
|
|
17
|
+
end
|
|
18
|
+
lines << ""
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# 2. Import directives
|
|
22
|
+
schema.imports.each do |imp|
|
|
23
|
+
if imp.alias_name
|
|
24
|
+
lines << "@import \"#{imp.path}\" as #{imp.alias_name}"
|
|
25
|
+
else
|
|
26
|
+
lines << "@import \"#{imp.path}\""
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
lines << "" unless schema.imports.empty?
|
|
30
|
+
|
|
31
|
+
# 3. Type definitions
|
|
32
|
+
schema.types.each do |type_name, schema_type|
|
|
33
|
+
lines << "{@#{type_name}}"
|
|
34
|
+
|
|
35
|
+
# Composition
|
|
36
|
+
if schema_type.composition
|
|
37
|
+
lines << "= #{schema_type.composition}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
schema_type.fields.each do |field_name, field|
|
|
41
|
+
lines << serialize_field(field_name, field)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Object constraints for this type
|
|
45
|
+
if schema.object_constraints[type_name]
|
|
46
|
+
schema.object_constraints[type_name].each do |constraint|
|
|
47
|
+
lines << serialize_object_constraint(constraint)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
lines << ""
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# 4. Root fields
|
|
55
|
+
unless schema.fields.empty?
|
|
56
|
+
# Group by section
|
|
57
|
+
root_fields = {}
|
|
58
|
+
sectioned_fields = Hash.new { |h, k| h[k] = {} }
|
|
59
|
+
|
|
60
|
+
schema.fields.each do |path, field|
|
|
61
|
+
parts = path.split(".", 2)
|
|
62
|
+
if parts.length > 1
|
|
63
|
+
sectioned_fields[parts[0]][parts[1]] = field
|
|
64
|
+
else
|
|
65
|
+
root_fields[path] = field
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
root_fields.each do |name, field|
|
|
70
|
+
lines << serialize_field(name, field)
|
|
71
|
+
end
|
|
72
|
+
lines << "" unless root_fields.empty?
|
|
73
|
+
|
|
74
|
+
sectioned_fields.each do |section, fields|
|
|
75
|
+
lines << "{#{section}}"
|
|
76
|
+
fields.each do |name, field|
|
|
77
|
+
lines << serialize_field(name, field)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
if schema.object_constraints[section]
|
|
81
|
+
schema.object_constraints[section].each do |constraint|
|
|
82
|
+
lines << serialize_object_constraint(constraint)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
lines << ""
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# 5. Array definitions
|
|
91
|
+
schema.arrays.each do |array_path, schema_array|
|
|
92
|
+
header = "{#{array_path}[]"
|
|
93
|
+
if schema_array.columns && !schema_array.columns.empty?
|
|
94
|
+
header += " : #{schema_array.columns.join(', ')}"
|
|
95
|
+
end
|
|
96
|
+
header += "}"
|
|
97
|
+
lines << header
|
|
98
|
+
|
|
99
|
+
# Array-level constraints
|
|
100
|
+
bounds_parts = []
|
|
101
|
+
if schema_array.min_items || schema_array.max_items
|
|
102
|
+
min_str = schema_array.min_items&.to_s || ""
|
|
103
|
+
max_str = schema_array.max_items&.to_s || ""
|
|
104
|
+
bounds_parts << ":(#{min_str}..#{max_str})"
|
|
105
|
+
end
|
|
106
|
+
bounds_parts << ":unique" if schema_array.unique
|
|
107
|
+
lines << bounds_parts.join("") unless bounds_parts.empty?
|
|
108
|
+
|
|
109
|
+
schema_array.item_fields.each do |field_name, field|
|
|
110
|
+
lines << serialize_field(field_name, field)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
if schema.object_constraints[array_path]
|
|
114
|
+
schema.object_constraints[array_path].each do |constraint|
|
|
115
|
+
lines << serialize_object_constraint(constraint)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
lines << ""
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# 6. Orphan object constraints (not already output with types/fields/arrays)
|
|
123
|
+
outputted_scopes = Set.new
|
|
124
|
+
schema.types.each_key { |k| outputted_scopes.add(k) }
|
|
125
|
+
schema.arrays.each_key { |k| outputted_scopes.add(k) }
|
|
126
|
+
# Sectioned fields
|
|
127
|
+
schema.fields.each_key do |path|
|
|
128
|
+
parts = path.split(".", 2)
|
|
129
|
+
outputted_scopes.add(parts[0]) if parts.length > 1
|
|
130
|
+
end
|
|
131
|
+
outputted_scopes.add("") # root constraints handled inline
|
|
132
|
+
|
|
133
|
+
schema.object_constraints.each do |scope, constraints|
|
|
134
|
+
next if outputted_scopes.include?(scope)
|
|
135
|
+
lines << "{#{scope}}" unless scope.empty?
|
|
136
|
+
constraints.each do |constraint|
|
|
137
|
+
lines << serialize_object_constraint(constraint)
|
|
138
|
+
end
|
|
139
|
+
lines << ""
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
lines.join("\n").rstrip + "\n"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
def serialize_field(name, field)
|
|
148
|
+
parts = [name, "="]
|
|
149
|
+
spec = []
|
|
150
|
+
|
|
151
|
+
# Modifiers
|
|
152
|
+
spec << "!" if field.required
|
|
153
|
+
spec << "~" if field.nullable
|
|
154
|
+
spec << "*" if field.redacted
|
|
155
|
+
spec << "-" if field.deprecated
|
|
156
|
+
|
|
157
|
+
# Type
|
|
158
|
+
type_str = serialize_type(field.field_type, field.type_ref)
|
|
159
|
+
spec << type_str unless type_str.empty?
|
|
160
|
+
|
|
161
|
+
# Constraints
|
|
162
|
+
field.constraints.each do |constraint|
|
|
163
|
+
spec << serialize_constraint(constraint)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Directives
|
|
167
|
+
spec << ":computed" if field.computed
|
|
168
|
+
spec << ":immutable" if field.immutable
|
|
169
|
+
|
|
170
|
+
# Conditionals
|
|
171
|
+
field.conditionals.each do |cond|
|
|
172
|
+
prefix = cond.unless ? ":unless" : ":if"
|
|
173
|
+
if cond.value == "true" && cond.operator == "="
|
|
174
|
+
spec << "#{prefix} #{cond.field}"
|
|
175
|
+
else
|
|
176
|
+
spec << "#{prefix} #{cond.field} #{cond.operator} #{cond.value}"
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Default value
|
|
181
|
+
spec << field.default_value.to_s if field.default_value
|
|
182
|
+
|
|
183
|
+
spec_str = spec.join("")
|
|
184
|
+
# Schema field values must be quoted strings for ODIN parsing
|
|
185
|
+
"#{name} = \"#{escape_string(spec_str)}\"".rstrip
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def serialize_type(field_type, type_ref)
|
|
189
|
+
case field_type
|
|
190
|
+
when Types::SchemaFieldType::STRING then ""
|
|
191
|
+
when Types::SchemaFieldType::INTEGER then "##"
|
|
192
|
+
when Types::SchemaFieldType::NUMBER then "#"
|
|
193
|
+
when Types::SchemaFieldType::BOOLEAN then "?"
|
|
194
|
+
when Types::SchemaFieldType::CURRENCY then "#$"
|
|
195
|
+
when Types::SchemaFieldType::PERCENT then "#%"
|
|
196
|
+
when Types::SchemaFieldType::DATE then "date"
|
|
197
|
+
when Types::SchemaFieldType::TIMESTAMP then "timestamp"
|
|
198
|
+
when Types::SchemaFieldType::TIME then "time"
|
|
199
|
+
when Types::SchemaFieldType::DURATION then "duration"
|
|
200
|
+
when Types::SchemaFieldType::REFERENCE then type_ref ? "@#{type_ref}" : "@"
|
|
201
|
+
when Types::SchemaFieldType::BINARY then "^"
|
|
202
|
+
when Types::SchemaFieldType::NULL then "~"
|
|
203
|
+
when Types::SchemaFieldType::ANY then ""
|
|
204
|
+
else ""
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def serialize_constraint(constraint)
|
|
209
|
+
case constraint
|
|
210
|
+
when Types::BoundsConstraint
|
|
211
|
+
min_str = constraint.min&.to_s || ""
|
|
212
|
+
max_str = constraint.max&.to_s || ""
|
|
213
|
+
if constraint.min == constraint.max && constraint.min
|
|
214
|
+
":(#{min_str})"
|
|
215
|
+
else
|
|
216
|
+
":(#{min_str}..#{max_str})"
|
|
217
|
+
end
|
|
218
|
+
when Types::PatternConstraint
|
|
219
|
+
":/#{constraint.pattern}/"
|
|
220
|
+
when Types::EnumConstraint
|
|
221
|
+
"(#{constraint.values.join(', ')})"
|
|
222
|
+
when Types::SizeConstraint
|
|
223
|
+
min_str = constraint.min_length&.to_s || ""
|
|
224
|
+
max_str = constraint.max_length&.to_s || ""
|
|
225
|
+
":(#{min_str}..#{max_str})"
|
|
226
|
+
when Types::FormatConstraint
|
|
227
|
+
":format #{constraint.format_name}"
|
|
228
|
+
when Types::UniqueConstraint
|
|
229
|
+
":unique"
|
|
230
|
+
else
|
|
231
|
+
""
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def serialize_object_constraint(constraint)
|
|
236
|
+
case constraint
|
|
237
|
+
when Types::SchemaInvariant
|
|
238
|
+
":invariant #{constraint.expression}"
|
|
239
|
+
when Types::SchemaCardinality
|
|
240
|
+
case constraint.cardinality_type
|
|
241
|
+
when "of"
|
|
242
|
+
min_str = constraint.min&.to_s || ""
|
|
243
|
+
max_str = constraint.max&.to_s || ""
|
|
244
|
+
":of (#{min_str}..#{max_str}) #{constraint.fields.join(', ')}"
|
|
245
|
+
when "one_of"
|
|
246
|
+
":one_of #{constraint.fields.join(', ')}"
|
|
247
|
+
when "exactly_one"
|
|
248
|
+
":exactly_one #{constraint.fields.join(', ')}"
|
|
249
|
+
when "at_most_one"
|
|
250
|
+
":at_most_one #{constraint.fields.join(', ')}"
|
|
251
|
+
end
|
|
252
|
+
else
|
|
253
|
+
""
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def escape_string(str)
|
|
258
|
+
str.gsub("\\", "\\\\").gsub('"', '\\"').gsub("\n", "\\n").gsub("\t", "\\t")
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|