strong_json 0.9.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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