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,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Odin
|
|
4
|
+
module Types
|
|
5
|
+
class OdinDirective
|
|
6
|
+
attr_reader :name, :value
|
|
7
|
+
|
|
8
|
+
def initialize(name, value = nil)
|
|
9
|
+
@name = -name.to_s
|
|
10
|
+
@value = value&.freeze
|
|
11
|
+
freeze
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def ==(other)
|
|
15
|
+
other.is_a?(OdinDirective) && name == other.name && value == other.value
|
|
16
|
+
end
|
|
17
|
+
alias eql? ==
|
|
18
|
+
|
|
19
|
+
def hash
|
|
20
|
+
[name, value].hash
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_s
|
|
24
|
+
value ? "#{name}(#{value})" : name
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Odin
|
|
4
|
+
module Types
|
|
5
|
+
class OdinDocument
|
|
6
|
+
def initialize(assignments:, metadata:, modifiers:, comments:)
|
|
7
|
+
@assignments = assignments.freeze
|
|
8
|
+
@metadata = metadata.freeze
|
|
9
|
+
@modifiers = modifiers.freeze
|
|
10
|
+
@comments = comments.freeze
|
|
11
|
+
freeze
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def get(path)
|
|
15
|
+
@assignments[path]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def [](path)
|
|
19
|
+
get(path)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def paths
|
|
23
|
+
@assignments.keys
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def include?(path)
|
|
27
|
+
@assignments.key?(path)
|
|
28
|
+
end
|
|
29
|
+
alias has_path? include?
|
|
30
|
+
|
|
31
|
+
def size
|
|
32
|
+
@assignments.size
|
|
33
|
+
end
|
|
34
|
+
alias length size
|
|
35
|
+
|
|
36
|
+
def assignments
|
|
37
|
+
@assignments
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def metadata
|
|
41
|
+
@metadata
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def metadata_value(key)
|
|
45
|
+
@metadata[key]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def modifiers_for(path)
|
|
49
|
+
@modifiers[path]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def all_modifiers
|
|
53
|
+
@modifiers
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def comment_for(path)
|
|
57
|
+
@comments[path]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def all_comments
|
|
61
|
+
@comments
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def empty?
|
|
65
|
+
@assignments.empty? && @metadata.empty?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def each_assignment(&block)
|
|
69
|
+
@assignments.each(&block)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def each_metadata(&block)
|
|
73
|
+
@metadata.each(&block)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def ==(other)
|
|
77
|
+
other.is_a?(OdinDocument) &&
|
|
78
|
+
assignments == other.assignments &&
|
|
79
|
+
metadata == other.metadata
|
|
80
|
+
end
|
|
81
|
+
alias eql? ==
|
|
82
|
+
|
|
83
|
+
def hash
|
|
84
|
+
[assignments, metadata].hash
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.empty
|
|
88
|
+
new(assignments: {}, metadata: {}, modifiers: {}, comments: {})
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Odin
|
|
4
|
+
module Types
|
|
5
|
+
class OdinDocumentBuilder
|
|
6
|
+
def initialize
|
|
7
|
+
@assignments = {}
|
|
8
|
+
@metadata = {}
|
|
9
|
+
@modifiers = {}
|
|
10
|
+
@comments = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def set(path, value, modifiers: nil, comment: nil)
|
|
14
|
+
@assignments[path] = value
|
|
15
|
+
@modifiers[path] = modifiers if modifiers
|
|
16
|
+
@comments[path] = comment if comment
|
|
17
|
+
self
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def set_metadata(key, value)
|
|
21
|
+
@metadata[key] = value
|
|
22
|
+
self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def set_string(path, value, modifiers: nil)
|
|
26
|
+
set(path, OdinString.new(value), modifiers: modifiers)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def set_integer(path, value, modifiers: nil)
|
|
30
|
+
set(path, OdinInteger.new(value), modifiers: modifiers)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def set_number(path, value, modifiers: nil)
|
|
34
|
+
set(path, OdinNumber.new(value), modifiers: modifiers)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def set_boolean(path, value, modifiers: nil)
|
|
38
|
+
set(path, value ? TRUE_VAL : FALSE_VAL, modifiers: modifiers)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def set_null(path, modifiers: nil)
|
|
42
|
+
set(path, NULL, modifiers: modifiers)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def set_currency(path, value, currency_code: nil, decimal_places: 2, modifiers: nil)
|
|
46
|
+
set(path, OdinCurrency.new(value, currency_code: currency_code,
|
|
47
|
+
decimal_places: decimal_places), modifiers: modifiers)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def remove(path)
|
|
51
|
+
@assignments.delete(path)
|
|
52
|
+
@modifiers.delete(path)
|
|
53
|
+
@comments.delete(path)
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def build
|
|
58
|
+
OdinDocument.new(
|
|
59
|
+
assignments: @assignments.dup,
|
|
60
|
+
metadata: @metadata.dup,
|
|
61
|
+
modifiers: @modifiers.dup,
|
|
62
|
+
comments: @comments.dup
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "bigdecimal"
|
|
5
|
+
|
|
6
|
+
module Odin
|
|
7
|
+
module Types
|
|
8
|
+
class DynValue
|
|
9
|
+
TYPES = %i[
|
|
10
|
+
null bool integer float float_raw
|
|
11
|
+
currency currency_raw percent reference binary
|
|
12
|
+
date timestamp time duration
|
|
13
|
+
string array object
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
attr_reader :type, :value, :decimal_places, :currency_code
|
|
17
|
+
|
|
18
|
+
def initialize(type:, value: nil, decimal_places: 0, currency_code: nil)
|
|
19
|
+
@type = type
|
|
20
|
+
@value = value
|
|
21
|
+
@decimal_places = decimal_places
|
|
22
|
+
@currency_code = currency_code&.freeze
|
|
23
|
+
freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Factory methods
|
|
27
|
+
def self.of_null
|
|
28
|
+
new(type: :null)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.of_bool(v)
|
|
32
|
+
new(type: :bool, value: v)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.of_integer(v)
|
|
36
|
+
new(type: :integer, value: v.to_i)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.of_float(v)
|
|
40
|
+
new(type: :float, value: v.to_f)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.of_float_raw(raw)
|
|
44
|
+
new(type: :float_raw, value: raw.to_s)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.of_string(v)
|
|
48
|
+
new(type: :string, value: v.to_s)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.of_array(items)
|
|
52
|
+
new(type: :array, value: items)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.of_object(entries)
|
|
56
|
+
new(type: :object, value: entries)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.of_currency(v, dp = 2, code = nil)
|
|
60
|
+
new(type: :currency, value: v.to_f, decimal_places: dp, currency_code: code)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.of_currency_raw(raw, dp = 2, code = nil)
|
|
64
|
+
new(type: :currency_raw, value: raw.to_s, decimal_places: dp, currency_code: code)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.of_percent(v)
|
|
68
|
+
new(type: :percent, value: v.to_f)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.of_reference(p)
|
|
72
|
+
new(type: :reference, value: p.to_s)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.of_binary(data)
|
|
76
|
+
new(type: :binary, value: data)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.of_date(v)
|
|
80
|
+
new(type: :date, value: v)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.of_timestamp(v)
|
|
84
|
+
new(type: :timestamp, value: v)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.of_time(v)
|
|
88
|
+
new(type: :time, value: v.to_s)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.of_duration(v)
|
|
92
|
+
new(type: :duration, value: v.to_s)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Helpers: parse JSON strings into DynValue arrays/objects
|
|
96
|
+
def self.extract_array(json_string)
|
|
97
|
+
parsed = JSON.parse(json_string)
|
|
98
|
+
raise ArgumentError, "Not a JSON array" unless parsed.is_a?(::Array)
|
|
99
|
+
|
|
100
|
+
of_array(parsed.map { |item| from_json_value(item) })
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.extract_object(json_string)
|
|
104
|
+
parsed = JSON.parse(json_string)
|
|
105
|
+
raise ArgumentError, "Not a JSON object" unless parsed.is_a?(::Hash)
|
|
106
|
+
|
|
107
|
+
of_object(parsed.transform_values { |v| from_json_value(v) })
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def self.from_json_value(val)
|
|
111
|
+
case val
|
|
112
|
+
when nil then of_null
|
|
113
|
+
when true, false then of_bool(val)
|
|
114
|
+
when Integer then of_integer(val)
|
|
115
|
+
when Float then of_float(val)
|
|
116
|
+
when String then of_string(val)
|
|
117
|
+
when Array then of_array(val.map { |v| from_json_value(v) })
|
|
118
|
+
when Hash then of_object(val.transform_values { |v| from_json_value(v) })
|
|
119
|
+
else of_string(val.to_s)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Type predicates
|
|
124
|
+
def null?; type == :null; end
|
|
125
|
+
def bool?; type == :bool; end
|
|
126
|
+
def integer?; type == :integer; end
|
|
127
|
+
def float?; type == :float || type == :float_raw; end
|
|
128
|
+
def currency?; type == :currency || type == :currency_raw; end
|
|
129
|
+
def percent?; type == :percent; end
|
|
130
|
+
def string?; type == :string; end
|
|
131
|
+
def array?; type == :array; end
|
|
132
|
+
def object?; type == :object; end
|
|
133
|
+
def reference?; type == :reference; end
|
|
134
|
+
def binary?; type == :binary; end
|
|
135
|
+
def date?; type == :date; end
|
|
136
|
+
def timestamp?; type == :timestamp; end
|
|
137
|
+
def time?; type == :time; end
|
|
138
|
+
def duration?; type == :duration; end
|
|
139
|
+
def numeric?; integer? || float? || currency? || percent?; end
|
|
140
|
+
|
|
141
|
+
def temporal?; date? || timestamp? || time? || duration?; end
|
|
142
|
+
|
|
143
|
+
# Coercion accessors
|
|
144
|
+
def as_bool; value; end
|
|
145
|
+
def as_int; value.to_i; end
|
|
146
|
+
def as_float; value.to_f; end
|
|
147
|
+
def as_string; value.to_s; end
|
|
148
|
+
def as_array; value; end
|
|
149
|
+
def as_object; value; end
|
|
150
|
+
|
|
151
|
+
# Coerce to numeric value
|
|
152
|
+
def to_number
|
|
153
|
+
case type
|
|
154
|
+
when :integer then value
|
|
155
|
+
when :float then value
|
|
156
|
+
when :float_raw then value.to_f
|
|
157
|
+
when :currency then value.to_f
|
|
158
|
+
when :currency_raw then value.to_f
|
|
159
|
+
when :percent then value
|
|
160
|
+
when :string
|
|
161
|
+
return value.to_i if value.match?(/\A-?\d+\z/)
|
|
162
|
+
return value.to_f if value.match?(/\A-?\d+(\.\d+)?([eE][+-]?\d+)?\z/)
|
|
163
|
+
|
|
164
|
+
0
|
|
165
|
+
when :bool then value ? 1 : 0
|
|
166
|
+
else 0
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Coerce to string representation
|
|
171
|
+
def to_string
|
|
172
|
+
case type
|
|
173
|
+
when :null then ""
|
|
174
|
+
when :bool then value.to_s
|
|
175
|
+
when :integer, :float, :percent then value.to_s
|
|
176
|
+
when :float_raw, :currency_raw then value.to_s
|
|
177
|
+
when :currency then value.is_a?(BigDecimal) ? value.to_s("F") : value.to_s
|
|
178
|
+
when :string then value
|
|
179
|
+
when :array then JSON.generate(to_ruby)
|
|
180
|
+
when :object then JSON.generate(to_ruby)
|
|
181
|
+
else value.to_s
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Truthiness: null/false/0/"" are falsy
|
|
186
|
+
def truthy?
|
|
187
|
+
case type
|
|
188
|
+
when :null then false
|
|
189
|
+
when :bool then value
|
|
190
|
+
when :integer then value != 0
|
|
191
|
+
when :float, :float_raw then to_number != 0.0
|
|
192
|
+
when :string then !value.empty?
|
|
193
|
+
when :currency, :currency_raw then to_number != 0.0
|
|
194
|
+
when :percent then value != 0.0
|
|
195
|
+
when :array then true
|
|
196
|
+
when :object then true
|
|
197
|
+
else true
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Object/array access helpers
|
|
202
|
+
def get(key)
|
|
203
|
+
return nil unless object?
|
|
204
|
+
|
|
205
|
+
value[key]
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def get_index(index)
|
|
209
|
+
return nil unless array?
|
|
210
|
+
|
|
211
|
+
value[index]
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def ==(other)
|
|
215
|
+
other.is_a?(DynValue) && type == other.type && value == other.value &&
|
|
216
|
+
decimal_places == other.decimal_places && currency_code == other.currency_code
|
|
217
|
+
end
|
|
218
|
+
alias eql? ==
|
|
219
|
+
|
|
220
|
+
def hash
|
|
221
|
+
[type, value, decimal_places, currency_code].hash
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Convert Ruby native object to DynValue
|
|
225
|
+
def self.from_ruby(obj)
|
|
226
|
+
case obj
|
|
227
|
+
when DynValue then obj
|
|
228
|
+
when nil then of_null
|
|
229
|
+
when true, false then of_bool(obj)
|
|
230
|
+
when Integer then of_integer(obj)
|
|
231
|
+
when Float then of_float(obj)
|
|
232
|
+
when BigDecimal then of_float(obj.to_f)
|
|
233
|
+
when String then of_string(obj)
|
|
234
|
+
when Array then of_array(obj.map { |v| from_ruby(v) })
|
|
235
|
+
when Hash then of_object(obj.transform_keys(&:to_s).transform_values { |v| from_ruby(v) })
|
|
236
|
+
else of_string(obj.to_s)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Convert DynValue to Ruby native object
|
|
241
|
+
def to_ruby
|
|
242
|
+
case type
|
|
243
|
+
when :null then nil
|
|
244
|
+
when :bool then value
|
|
245
|
+
when :integer then value
|
|
246
|
+
when :float then value
|
|
247
|
+
when :float_raw then value.to_f
|
|
248
|
+
when :string then value
|
|
249
|
+
when :currency then value.is_a?(BigDecimal) ? value.to_f : value.to_f
|
|
250
|
+
when :currency_raw then value.to_f
|
|
251
|
+
when :percent then value
|
|
252
|
+
when :date, :timestamp, :time, :duration then value.to_s
|
|
253
|
+
when :reference then value
|
|
254
|
+
when :binary then value
|
|
255
|
+
when :array then value.map(&:to_ruby)
|
|
256
|
+
when :object then value.transform_values(&:to_ruby)
|
|
257
|
+
else value
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def to_s
|
|
262
|
+
case type
|
|
263
|
+
when :null then "null"
|
|
264
|
+
when :bool then value.to_s
|
|
265
|
+
else value.to_s
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Odin
|
|
4
|
+
module Errors
|
|
5
|
+
module ParseErrorCode
|
|
6
|
+
UNEXPECTED_CHARACTER = -"P001"
|
|
7
|
+
BARE_STRING_NOT_ALLOWED = -"P002"
|
|
8
|
+
INVALID_ARRAY_INDEX = -"P003"
|
|
9
|
+
UNTERMINATED_STRING = -"P004"
|
|
10
|
+
INVALID_ESCAPE_SEQUENCE = -"P005"
|
|
11
|
+
INVALID_TYPE_PREFIX = -"P006"
|
|
12
|
+
DUPLICATE_PATH_ASSIGNMENT = -"P007"
|
|
13
|
+
INVALID_HEADER_SYNTAX = -"P008"
|
|
14
|
+
INVALID_DIRECTIVE = -"P009"
|
|
15
|
+
MAXIMUM_DEPTH_EXCEEDED = -"P010"
|
|
16
|
+
MAXIMUM_DOCUMENT_SIZE_EXCEEDED = -"P011"
|
|
17
|
+
INVALID_UTF8_SEQUENCE = -"P012"
|
|
18
|
+
NON_CONTIGUOUS_ARRAY_INDICES = -"P013"
|
|
19
|
+
EMPTY_DOCUMENT = -"P014"
|
|
20
|
+
ARRAY_INDEX_OUT_OF_RANGE = -"P015"
|
|
21
|
+
|
|
22
|
+
ALL = {
|
|
23
|
+
"P001" => "Unexpected character",
|
|
24
|
+
"P002" => "Strings must be quoted",
|
|
25
|
+
"P003" => "Invalid array index",
|
|
26
|
+
"P004" => "Unterminated string",
|
|
27
|
+
"P005" => "Invalid escape sequence",
|
|
28
|
+
"P006" => "Invalid type prefix",
|
|
29
|
+
"P007" => "Duplicate path assignment",
|
|
30
|
+
"P008" => "Invalid header syntax",
|
|
31
|
+
"P009" => "Invalid directive",
|
|
32
|
+
"P010" => "Maximum depth exceeded",
|
|
33
|
+
"P011" => "Maximum document size exceeded",
|
|
34
|
+
"P012" => "Invalid UTF-8 sequence",
|
|
35
|
+
"P013" => "Non-contiguous array indices",
|
|
36
|
+
"P014" => "Empty document",
|
|
37
|
+
"P015" => "Array index out of range"
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
def self.message(code)
|
|
41
|
+
ALL[code] || "Unknown error"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
module ValidationErrorCode
|
|
46
|
+
REQUIRED_FIELD_MISSING = -"V001"
|
|
47
|
+
TYPE_MISMATCH = -"V002"
|
|
48
|
+
VALUE_OUT_OF_BOUNDS = -"V003"
|
|
49
|
+
PATTERN_MISMATCH = -"V004"
|
|
50
|
+
INVALID_ENUM_VALUE = -"V005"
|
|
51
|
+
ARRAY_LENGTH_VIOLATION = -"V006"
|
|
52
|
+
UNIQUE_CONSTRAINT_VIOLATION = -"V007"
|
|
53
|
+
INVARIANT_VIOLATION = -"V008"
|
|
54
|
+
CARDINALITY_CONSTRAINT_VIOLATION = -"V009"
|
|
55
|
+
CONDITIONAL_REQUIREMENT_NOT_MET = -"V010"
|
|
56
|
+
UNKNOWN_FIELD = -"V011"
|
|
57
|
+
CIRCULAR_REFERENCE = -"V012"
|
|
58
|
+
UNRESOLVED_REFERENCE = -"V013"
|
|
59
|
+
|
|
60
|
+
ALL = {
|
|
61
|
+
"V001" => "Required field missing",
|
|
62
|
+
"V002" => "Type mismatch",
|
|
63
|
+
"V003" => "Value out of bounds",
|
|
64
|
+
"V004" => "Pattern mismatch",
|
|
65
|
+
"V005" => "Invalid enum value",
|
|
66
|
+
"V006" => "Array length violation",
|
|
67
|
+
"V007" => "Unique constraint violation",
|
|
68
|
+
"V008" => "Invariant violation",
|
|
69
|
+
"V009" => "Cardinality constraint violation",
|
|
70
|
+
"V010" => "Conditional requirement not met",
|
|
71
|
+
"V011" => "Unknown field",
|
|
72
|
+
"V012" => "Circular reference",
|
|
73
|
+
"V013" => "Unresolved reference"
|
|
74
|
+
}.freeze
|
|
75
|
+
|
|
76
|
+
def self.message(code)
|
|
77
|
+
ALL[code] || "Unknown error"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
class OdinError < StandardError
|
|
82
|
+
attr_reader :code
|
|
83
|
+
|
|
84
|
+
def initialize(code, message)
|
|
85
|
+
@code = code
|
|
86
|
+
super("[#{code}] #{message}")
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
class ParseError < OdinError
|
|
91
|
+
attr_reader :line, :column
|
|
92
|
+
|
|
93
|
+
def initialize(code, line, column, detail = nil)
|
|
94
|
+
@line = line
|
|
95
|
+
@column = column
|
|
96
|
+
msg = ParseErrorCode.message(code)
|
|
97
|
+
msg = "#{msg}: #{detail}" if detail
|
|
98
|
+
msg = "#{msg} at line #{line}, column #{column}"
|
|
99
|
+
super(code, msg)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
class ValidationError
|
|
104
|
+
attr_reader :path, :code, :message, :expected, :actual, :schema_path
|
|
105
|
+
|
|
106
|
+
def initialize(code:, path:, message:, expected: nil, actual: nil, schema_path: nil)
|
|
107
|
+
@code = code
|
|
108
|
+
@path = path
|
|
109
|
+
@message = message
|
|
110
|
+
@expected = expected
|
|
111
|
+
@actual = actual
|
|
112
|
+
@schema_path = schema_path
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def to_s
|
|
116
|
+
"[#{code}] #{message} at '#{path}'"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
class ValidationResult
|
|
121
|
+
attr_reader :errors
|
|
122
|
+
|
|
123
|
+
def initialize(errors = [])
|
|
124
|
+
@errors = errors.freeze
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def valid?
|
|
128
|
+
errors.empty?
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def self.valid
|
|
132
|
+
new([])
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def self.with_errors(errors)
|
|
136
|
+
new(errors)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
class PatchError < OdinError
|
|
141
|
+
attr_reader :path
|
|
142
|
+
|
|
143
|
+
def initialize(message, path)
|
|
144
|
+
@path = path
|
|
145
|
+
super("PATCH", "#{message} at '#{path}'")
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Odin
|
|
4
|
+
module Types
|
|
5
|
+
class OdinModifiers
|
|
6
|
+
attr_reader :required, :confidential, :deprecated, :attr
|
|
7
|
+
|
|
8
|
+
def initialize(required: false, confidential: false, deprecated: false, attr: nil)
|
|
9
|
+
@required = required
|
|
10
|
+
@confidential = confidential
|
|
11
|
+
@deprecated = deprecated
|
|
12
|
+
@attr = attr
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
NONE = new
|
|
17
|
+
|
|
18
|
+
def any?
|
|
19
|
+
required || confidential || deprecated
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def ==(other)
|
|
23
|
+
other.is_a?(OdinModifiers) &&
|
|
24
|
+
required == other.required &&
|
|
25
|
+
confidential == other.confidential &&
|
|
26
|
+
deprecated == other.deprecated &&
|
|
27
|
+
self.attr == other.attr
|
|
28
|
+
end
|
|
29
|
+
alias eql? ==
|
|
30
|
+
|
|
31
|
+
def hash
|
|
32
|
+
[required, confidential, deprecated, self.attr].hash
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_s
|
|
36
|
+
parts = []
|
|
37
|
+
parts << "required" if required
|
|
38
|
+
parts << "confidential" if confidential
|
|
39
|
+
parts << "deprecated" if deprecated
|
|
40
|
+
parts << "attr=#{self.attr}" if self.attr
|
|
41
|
+
parts.empty? ? "OdinModifiers(none)" : "OdinModifiers(#{parts.join(', ')})"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|