strong_json 0.9.0 → 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.
@@ -1,7 +1,5 @@
1
1
  class StrongJSON
2
2
  module Type
3
- NONE = ::Object.new
4
-
5
3
  module Match
6
4
  def =~(value)
7
5
  coerce(value)
@@ -15,8 +13,23 @@ class StrongJSON
15
13
  end
16
14
  end
17
15
 
16
+ module WithAlias
17
+ def alias
18
+ @alias
19
+ end
20
+
21
+ def with_alias(name)
22
+ _ = dup.tap do |copy|
23
+ copy.instance_eval do
24
+ @alias = name
25
+ end
26
+ end
27
+ end
28
+ end
29
+
18
30
  class Base
19
31
  include Match
32
+ include WithAlias
20
33
 
21
34
  # @dynamic type
22
35
  attr_reader :type
@@ -27,8 +40,6 @@ class StrongJSON
27
40
 
28
41
  def test(value)
29
42
  case @type
30
- when :ignored
31
- true
32
43
  when :any
33
44
  true
34
45
  when :number
@@ -46,13 +57,10 @@ class StrongJSON
46
57
  end
47
58
  end
48
59
 
49
- def coerce(value, path: [])
50
- raise Error.new(value: value, type: self, path: path) unless test(value)
51
- raise IllegalTypeError.new(type: self) if path == [] && @type == :ignored
60
+ def coerce(value, path: ErrorPath.root(self))
61
+ raise TypeError.new(value: value, path: path) unless test(value)
52
62
 
53
63
  case type
54
- when :ignored
55
- NONE
56
64
  when :symbol
57
65
  value.to_sym
58
66
  else
@@ -61,32 +69,59 @@ class StrongJSON
61
69
  end
62
70
 
63
71
  def to_s
64
- @type.to_s
72
+ self.alias&.to_s || @type.to_s
73
+ end
74
+
75
+ def ==(other)
76
+ if other.is_a?(Base)
77
+ # @type var other: Base<any>
78
+ other.type == type
79
+ end
80
+ end
81
+
82
+ __skip__ = begin
83
+ alias eql? ==
65
84
  end
66
85
  end
67
86
 
68
87
  class Optional
69
88
  include Match
89
+ include WithAlias
90
+
91
+ # @dynamic type
92
+ attr_reader :type
70
93
 
71
94
  def initialize(type)
72
95
  @type = type
73
96
  end
74
97
 
75
- def coerce(value, path: [])
76
- unless value == nil || NONE.equal?(value)
77
- @type.coerce(value, path: path)
98
+ def coerce(value, path: ErrorPath.root(self))
99
+ unless value == nil
100
+ @type.coerce(value, path: path.expand(type: @type))
78
101
  else
79
102
  nil
80
103
  end
81
104
  end
82
105
 
83
106
  def to_s
84
- "optional(#{@type})"
107
+ self.alias&.to_s || "optional(#{@type})"
108
+ end
109
+
110
+ def ==(other)
111
+ if other.is_a?(Optional)
112
+ # @type var other: Optional<any>
113
+ other.type == type
114
+ end
115
+ end
116
+
117
+ __skip__ = begin
118
+ alias eql? ==
85
119
  end
86
120
  end
87
121
 
88
122
  class Literal
89
123
  include Match
124
+ include WithAlias
90
125
 
91
126
  # @dynamic value
92
127
  attr_reader :value
@@ -96,176 +131,287 @@ class StrongJSON
96
131
  end
97
132
 
98
133
  def to_s
99
- "literal(#{@value})"
134
+ self.alias&.to_s || (_ = @value).inspect
100
135
  end
101
136
 
102
- def coerce(value, path: [])
103
- raise Error.new(path: path, type: self, value: value) unless (_ = self.value) == value
137
+ def coerce(value, path: ErrorPath.root(self))
138
+ raise TypeError.new(path: path, value: value) unless (_ = self.value) == value
104
139
  value
105
140
  end
141
+
142
+ def ==(other)
143
+ if other.is_a?(Literal)
144
+ # @type var other: Literal<any>
145
+ other.value == value
146
+ end
147
+ end
148
+
149
+ __skip__ = begin
150
+ alias eql? ==
151
+ end
106
152
  end
107
153
 
108
154
  class Array
109
155
  include Match
156
+ include WithAlias
157
+
158
+ # @dynamic type
159
+ attr_reader :type
110
160
 
111
161
  def initialize(type)
112
162
  @type = type
113
163
  end
114
164
 
115
- def coerce(value, path: [])
165
+ def coerce(value, path: ErrorPath.root(self))
116
166
  if value.is_a?(::Array)
117
167
  value.map.with_index do |v, i|
118
- @type.coerce(v, path: path+[i])
168
+ @type.coerce(v, path: path.dig(key: i, type: @type))
119
169
  end
120
170
  else
121
- raise Error.new(path: path, type: self, value: value)
171
+ raise TypeError.new(path: path, value: value)
122
172
  end
123
173
  end
124
174
 
125
175
  def to_s
126
- "array(#{@type})"
176
+ self.alias&.to_s || "array(#{@type})"
177
+ end
178
+
179
+ def ==(other)
180
+ if other.is_a?(Array)
181
+ # @type var other: Array<any>
182
+ other.type == type
183
+ end
184
+ end
185
+
186
+ __skip__ = begin
187
+ alias eql? ==
127
188
  end
128
189
  end
129
190
 
130
191
  class Object
131
192
  include Match
193
+ include WithAlias
132
194
 
133
- def initialize(fields)
195
+ # @dynamic fields, ignored_attributes, prohibited_attributes
196
+ attr_reader :fields, :ignored_attributes, :prohibited_attributes
197
+
198
+ def initialize(fields, ignored_attributes:, prohibited_attributes:)
134
199
  @fields = fields
200
+ @ignored_attributes = ignored_attributes
201
+ @prohibited_attributes = prohibited_attributes
135
202
  end
136
203
 
137
- def coerce(object, path: [])
204
+ def coerce(object, path: ErrorPath.root(self))
138
205
  unless object.is_a?(Hash)
139
- raise Error.new(path: path, type: self, value: object)
206
+ raise TypeError.new(path: path, value: object)
140
207
  end
141
208
 
142
- # @type var result: ::Hash<Symbol, any>
143
- result = {}
209
+ unless (intersection = Set.new(object.keys).intersection(prohibited_attributes)).empty?
210
+ raise UnexpectedAttributeError.new(path: path, attribute: intersection.to_a.first)
211
+ end
144
212
 
145
- object.each do |key, value|
146
- unless @fields.key?(key)
147
- raise UnexpectedFieldError.new(path: path + [key], value: value)
213
+ case attrs = ignored_attributes
214
+ when :any
215
+ object = object.dup
216
+ extra_keys = Set.new(object.keys) - Set.new(fields.keys)
217
+ extra_keys.each do |key|
218
+ object.delete(key)
219
+ end
220
+ when Set
221
+ object = object.dup
222
+ attrs.each do |key|
223
+ object.delete(key)
148
224
  end
149
225
  end
150
226
 
151
- @fields.each do |key, type|
152
- value = object.key?(key) ? object[key] : NONE
227
+ # @type var result: ::Hash<Symbol, any>
228
+ result = {}
153
229
 
154
- test_value_type(path + [key], type, value) do |v|
155
- result[key] = v
230
+ object.each do |key, _|
231
+ unless fields.key?(key)
232
+ raise UnexpectedAttributeError.new(path: path, attribute: key)
156
233
  end
157
234
  end
158
235
 
236
+ fields.each do |key, type|
237
+ result[key] = type.coerce(object[key], path: path.dig(key: key, type: type))
238
+ end
239
+
159
240
  _ = result
160
241
  end
161
242
 
162
- def test_value_type(path, type, value)
163
- v = type.coerce(value, path: path)
164
-
165
- return if NONE.equal?(v) || NONE.equal?(type)
166
- return if type.is_a?(Optional) && NONE.equal?(value)
167
-
168
- yield(v)
243
+ def ignore(attrs)
244
+ Object.new(fields, ignored_attributes: attrs, prohibited_attributes: prohibited_attributes)
169
245
  end
170
246
 
171
- def merge(fields)
172
- # @type var fs: Hash<Symbol, _Schema<any>>
247
+ def ignore!(attrs)
248
+ @ignored_attributes = attrs
249
+ self
250
+ end
173
251
 
174
- fs = case fields
175
- when Object
176
- fields.instance_variable_get(:"@fields")
177
- when Hash
178
- fields
179
- end
252
+ def prohibit(attrs)
253
+ Object.new(fields, ignored_attributes: ignored_attributes, prohibited_attributes: attrs)
254
+ end
180
255
 
181
- Object.new(@fields.merge(fs))
256
+ def prohibit!(attrs)
257
+ @prohibited_attributes = attrs
258
+ self
182
259
  end
183
260
 
184
- def except(*keys)
185
- Object.new(keys.each.with_object(@fields.dup) do |key, hash|
186
- hash.delete key
187
- end)
261
+ def update_fields
262
+ fields.dup.yield_self do |fields|
263
+ yield fields
264
+
265
+ Object.new(fields, ignored_attributes: ignored_attributes, prohibited_attributes: prohibited_attributes)
266
+ end
188
267
  end
189
268
 
190
269
  def to_s
191
- # @type var fields: ::Array<String>
192
- fields = []
270
+ fields = @fields.map do |name, type|
271
+ "#{name}: #{type}"
272
+ end
193
273
 
194
- @fields.each do |name, type|
195
- fields << "#{name}: #{type}"
274
+ self.alias&.to_s || "object(#{fields.join(', ')})"
275
+ end
276
+
277
+ def ==(other)
278
+ if other.is_a?(Object)
279
+ # @type var other: Object<any>
280
+ other.fields == fields &&
281
+ other.ignored_attributes == ignored_attributes &&
282
+ other.prohibited_attributes == prohibited_attributes
196
283
  end
284
+ end
197
285
 
198
- "object(#{fields.join(', ')})"
286
+ __skip__ = begin
287
+ alias eql? ==
199
288
  end
200
289
  end
201
290
 
202
291
  class Enum
203
292
  include Match
293
+ include WithAlias
204
294
 
205
- # @dynamic types
295
+ # @dynamic types, detector
206
296
  attr_reader :types
297
+ attr_reader :detector
207
298
 
208
- def initialize(types)
299
+ def initialize(types, detector = nil)
209
300
  @types = types
301
+ @detector = detector
210
302
  end
211
303
 
212
304
  def to_s
213
- "enum(#{types.map(&:to_s).join(", ")})"
305
+ self.alias&.to_s || "enum(#{types.map(&:to_s).join(", ")})"
214
306
  end
215
307
 
216
- def coerce(value, path: [])
308
+ def coerce(value, path: ErrorPath.root(self))
309
+ if d = detector
310
+ type = d[value]
311
+ if type && types.include?(type)
312
+ return type.coerce(value, path: path.expand(type: type))
313
+ end
314
+ end
315
+
217
316
  types.each do |ty|
218
317
  begin
219
- return ty.coerce(value, path: path)
220
- rescue UnexpectedFieldError, IllegalTypeError, Error # rubocop:disable Lint/HandleExceptions
318
+ return ty.coerce(value, path: path.expand(type: ty))
319
+ rescue UnexpectedAttributeError, TypeError # rubocop:disable Lint/HandleExceptions
221
320
  end
222
321
  end
223
322
 
224
- raise Error.new(path: path, type: self, value: value)
323
+ raise TypeError.new(path: path, value: value)
324
+ end
325
+
326
+ def ==(other)
327
+ if other.is_a?(Enum)
328
+ # @type var other: Enum<any>
329
+ other.types == types &&
330
+ other.detector == detector
331
+ end
332
+ end
333
+
334
+ __skip__ = begin
335
+ alias eql? ==
225
336
  end
226
337
  end
227
338
 
228
- class UnexpectedFieldError < StandardError
339
+ class UnexpectedAttributeError < StandardError
340
+ # @dynamic path, attribute
341
+ attr_reader :path, :attribute
342
+
343
+ def initialize(path:, attribute:)
344
+ @path = path
345
+ @attribute = attribute
346
+ super "UnexpectedAttributeError at #{path.to_s}: attribute=#{attribute}"
347
+ end
348
+
349
+ def type
350
+ path.type
351
+ end
352
+ end
353
+
354
+ class TypeError < StandardError
229
355
  # @dynamic path, value
230
356
  attr_reader :path, :value
231
357
 
232
- def initialize(path: , value:)
358
+ def initialize(path:, value:)
233
359
  @path = path
234
360
  @value = value
361
+ type = path.type
362
+ s = type.alias || type
363
+ super "TypeError at #{path.to_s}: expected=#{s}, value=#{value.inspect}"
235
364
  end
236
365
 
237
- def to_s
238
- position = "#{path.join('.')}"
239
- "Unexpected field of #{position} (#{value})"
366
+ def type
367
+ path.type
240
368
  end
241
369
  end
242
370
 
243
- class IllegalTypeError < StandardError
244
- # @dynamic type
245
- attr_reader :type
371
+ class ErrorPath
372
+ # @dynamic type, parent
373
+ attr_reader :type, :parent
246
374
 
247
- def initialize(type:)
375
+ def initialize(type:, parent:)
248
376
  @type = type
377
+ @parent = parent
249
378
  end
250
379
 
251
- def to_s
252
- "#{type} can not be put on toplevel"
380
+ def dig(key:, type:)
381
+ # @type var parent: [Integer | Symbol | nil, ErrorPath]
382
+ parent = [key, self]
383
+ self.class.new(type: type, parent: parent)
253
384
  end
254
- end
255
385
 
256
- class Error < StandardError
257
- # @dynamic path, type, value
258
- attr_reader :path, :type, :value
386
+ def expand(type:)
387
+ # @type var parent: [Integer | Symbol | nil, ErrorPath]
388
+ parent = [nil, self]
389
+ self.class.new(type: type, parent: parent)
390
+ end
259
391
 
260
- def initialize(path:, type:, value:)
261
- @path = path
262
- @type = type
263
- @value = value
392
+ def self.root(type)
393
+ self.new(type: type, parent: nil)
394
+ end
395
+
396
+ def root?
397
+ !parent
264
398
  end
265
399
 
266
400
  def to_s
267
- position = path.empty? ? "" : " at .#{path.join('.')}"
268
- "Expected type of value #{value}#{position} is #{type}"
401
+ if pa = parent
402
+ if key = pa[0]
403
+ pa[1].to_s + case key
404
+ when Integer
405
+ "[#{key}]"
406
+ when Symbol
407
+ ".#{key}"
408
+ end
409
+ else
410
+ pa[1].to_s
411
+ end
412
+ else
413
+ "$"
414
+ end
269
415
  end
270
416
  end
271
417
  end
@@ -2,7 +2,11 @@ class StrongJSON
2
2
  module Types
3
3
  # @type method object: (?Hash<Symbol, ty>) -> Type::Object<any>
4
4
  def object(fields = {})
5
- Type::Object.new(fields)
5
+ if fields.empty?
6
+ Type::Object.new(fields, ignored_attributes: nil, prohibited_attributes: Set.new)
7
+ else
8
+ Type::Object.new(fields, ignored_attributes: :any, prohibited_attributes: Set.new)
9
+ end
6
10
  end
7
11
 
8
12
  # @type method array: (?ty) -> Type::Array<any>
@@ -39,10 +43,6 @@ class StrongJSON
39
43
  optional(any)
40
44
  end
41
45
 
42
- def prohibited
43
- StrongJSON::Type::Base.new(:prohibited)
44
- end
45
-
46
46
  def symbol
47
47
  StrongJSON::Type::Base.new(:symbol)
48
48
  end
@@ -51,8 +51,8 @@ class StrongJSON
51
51
  StrongJSON::Type::Literal.new(value)
52
52
  end
53
53
 
54
- def enum(*types)
55
- StrongJSON::Type::Enum.new(types)
54
+ def enum(*types, detector: nil)
55
+ StrongJSON::Type::Enum.new(types, detector)
56
56
  end
57
57
 
58
58
  def string?
@@ -75,15 +75,12 @@ class StrongJSON
75
75
  optional(symbol)
76
76
  end
77
77
 
78
- def ignored
79
- StrongJSON::Type::Base.new(:ignored)
80
- end
81
-
82
78
  def array?(ty)
83
79
  optional(array(ty))
84
80
  end
85
81
 
86
- def object?(fields)
82
+ # @type method object?: (?Hash<Symbol, ty>) -> Type::Optional<any>
83
+ def object?(fields={})
87
84
  optional(object(fields))
88
85
  end
89
86
 
@@ -91,8 +88,8 @@ class StrongJSON
91
88
  optional(literal(value))
92
89
  end
93
90
 
94
- def enum?(*types)
95
- optional(enum(*types))
91
+ def enum?(*types, detector: nil)
92
+ optional(enum(*types, detector: detector))
96
93
  end
97
94
  end
98
95
  end
@@ -1,5 +1,5 @@
1
1
  class StrongJSON
2
2
  # @dynamic initialize, let
3
3
 
4
- VERSION = "0.9.0"
4
+ VERSION = "1.0.0"
5
5
  end
data/lib/strong_json.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require "strong_json/version"
2
2
  require "strong_json/type"
3
3
  require "strong_json/types"
4
+ require "strong_json/error_reporter"
5
+ require "prettyprint"
4
6
 
5
7
  class StrongJSON
6
8
  def initialize(&block)
@@ -8,7 +10,7 @@ class StrongJSON
8
10
  end
9
11
 
10
12
  def let(name, type)
11
- define_singleton_method(name) { type }
13
+ define_singleton_method(name) { type.with_alias(name) }
12
14
  end
13
15
 
14
16
  include StrongJSON::Types
data/sig/strong_json.rbi CHANGED
@@ -6,20 +6,30 @@ end
6
6
 
7
7
  StrongJSON::VERSION: String
8
8
 
9
+ class StandardError
10
+ def initialize: (String) -> any
11
+ end
12
+
9
13
  interface StrongJSON::_Schema<'type>
10
- def coerce: (any, ?path: ::Array<Symbol>) -> 'type
14
+ def coerce: (any, ?path: Type::ErrorPath) -> 'type
11
15
  def =~: (any) -> bool
12
16
  def to_s: -> String
13
17
  def is_a?: (any) -> bool
18
+ def alias: -> Symbol?
19
+ def with_alias: (Symbol) -> self
20
+ def ==: (any) -> bool
21
+ def yield_self: <'a> () { (self) -> 'a } -> 'a
14
22
  end
15
23
 
16
24
  type StrongJSON::ty = _Schema<any>
17
25
 
18
26
  module StrongJSON::Types
19
27
  def object: <'x> (Hash<Symbol, ty>) -> Type::Object<'x>
20
- | () -> Type::Object<Hash<Symbol, any>>
28
+ | () -> Type::Object<{}>
21
29
  def object?: <'x> (Hash<Symbol, ty>) -> Type::Optional<'x>
30
+ | () -> Type::Optional<{}>
22
31
  def any: () -> Type::Base<any>
32
+ def any?: () -> Type::Optional<any>
23
33
  def optional: <'x> (_Schema<'x>) -> Type::Optional<'x>
24
34
  | () -> Type::Optional<any>
25
35
  def string: () -> Type::Base<String>
@@ -37,8 +47,17 @@ module StrongJSON::Types
37
47
  def array?: <'x> (_Schema<'x>) -> Type::Optional<::Array<'x>>
38
48
  def literal: <'x> ('x) -> Type::Literal<'x>
39
49
  def literal?: <'x> ('x) -> Type::Optional<'x>
40
- def enum: <'x> (*_Schema<any>) -> Type::Enum<'x>
41
- def enum?: <'x> (*_Schema<any>) -> Type::Optional<'x>
42
- def ignored: () -> _Schema<nil>
43
- def prohibited: () -> _Schema<nil>
50
+ def enum: <'x> (*_Schema<any>, ?detector: Type::detector?) -> Type::Enum<'x>
51
+ def enum?: <'x> (*_Schema<any>, ?detector: Type::detector?) -> Type::Optional<'x>
52
+ end
53
+
54
+ class StrongJSON::ErrorReporter
55
+ attr_reader path: Type::ErrorPath
56
+ @string: String
57
+ def initialize: (path: Type::ErrorPath) -> any
58
+ def format: -> void
59
+ def (private) format_trace: (path: Type::ErrorPath, ?index: Integer) -> void
60
+ def (private) format_aliases: (path: Type::ErrorPath, where: ::Array<String>) -> ::Array<String>
61
+ def (private) pretty: (ty, any, ?expand_alias: bool) -> void
62
+ def pretty_str: (ty, ?expand_alias: bool) -> ::String
44
63
  end