strong_json 1.0.1 → 2.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0035c9edc36454dc79ce83f91e948c91c7a10935cc4c7dc1ab3c50113c209100
4
- data.tar.gz: b77289a1d16f20e2f29875e5156917f984587ad70f6d9bddd4aa4d7a62f40cb9
3
+ metadata.gz: 3024051238c907ceb915ad211936b82d0c4422f6980f2abeff76a667017f6a0a
4
+ data.tar.gz: 9f9d1efa03cb0d35155837928577062ae696066841d50a6495c33f40f7fee6ec
5
5
  SHA512:
6
- metadata.gz: d230d14df235f9831ea73287abd9aa6018053ead823a7816bb73516adfc3599e29d727295055ab2439c6342592ebe1331e91a74df775adff8f550524b5be2d41
7
- data.tar.gz: ac439273a3b6ef148f2f135b3f3c9820caf25831f8bfaae554971830b1c009d42242a6a614b7786eb2669c551e356eb66823375f2bbecf9f2a372bb9b3154228
6
+ metadata.gz: 99a431a8d8f686604b8b0a44a1827e0440c61144d9d1b5f1253fbf79b6707dfb10f60a54de337b4dc87b7d27f31b6bd5f3c20f0a6a8a2dff2f48c2a144020827
7
+ data.tar.gz: 38953830ba84e56e13ee9ab8ff91b97f305bdea2cc81ca12655f2d474932f1ea36bc4c9b1971dd17b123af18ff1036df317ea69bdc93c60002d4fe73cc1654f3
@@ -0,0 +1,29 @@
1
+ name: Ruby
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ pull_request: {}
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: "ubuntu-latest"
12
+ strategy:
13
+ matrix:
14
+ container_tag:
15
+ - master-nightly-bionic
16
+ - 2.6.5-bionic
17
+ - 2.7.0-bionic
18
+ container:
19
+ image: rubylang/ruby:${{ matrix.container_tag }}
20
+ steps:
21
+ - uses: actions/checkout@v1
22
+ - name: Install
23
+ run: |
24
+ ruby -v
25
+ gem install bundler
26
+ bundle install
27
+ - name: Run test
28
+ run: |
29
+ bundle exec rake
data/.gitignore CHANGED
@@ -1,4 +1,5 @@
1
1
  /.bundle/
2
+ /vendor/bundle/
2
3
  /.yardoc
3
4
  /Gemfile.lock
4
5
  /_yardoc/
@@ -1 +1 @@
1
- 2.5.1
1
+ 2.6.6
@@ -1,3 +1,5 @@
1
1
  language: ruby
2
2
  rvm:
3
- - "2.5.3"
3
+ - "2.5.8"
4
+ - "2.6.6"
5
+ - "2.7.1"
@@ -2,6 +2,26 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 2.1.2
6
+
7
+ * Update Steep
8
+
9
+ ## 2.1.1 (2020-05-16)
10
+
11
+ * [24](https://github.com/soutaro/strong_json/pull/24): Ship with RBS
12
+
13
+ ## 2.1.0 (2019-07-19)
14
+
15
+ * [20](https://github.com/soutaro/strong_json/pull/20): Add `integer` type
16
+
17
+ ## 2.0.0 (2019-06-28)
18
+
19
+ * [17](https://github.com/soutaro/strong_json/pull/17): Revise `Object` type _ignore_/_reject_ API
20
+
21
+ ## 1.1.0 (2019-06-09)
22
+
23
+ * [#15](https://github.com/soutaro/strong_json/pull/15): Add `hash` type
24
+
5
25
  ## 1.0.1 (2019-05-29)
6
26
 
7
27
  * Improve enum error reporting
data/Gemfile CHANGED
@@ -2,3 +2,7 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in strong_json.gemspec
4
4
  gemspec
5
+
6
+ gem "rake"
7
+ gem "rspec"
8
+ gem "steep"
data/README.md CHANGED
@@ -47,7 +47,7 @@ If the input JSON data conforms to `order`'s structure, the `json` will be that
47
47
 
48
48
  If the input JSON contains attributes which is not white-listed in the definition, it will raise an exception.
49
49
 
50
- If an attribute has a value which does not match with given type, the `coerce` method call will raise an exception `StrongJSON::Type::Error`.
50
+ If an attribute has a value which does not match with given type, the `coerce` method call will raise an exception `StrongJSON::Type::TypeError`.
51
51
 
52
52
  ## Catalogue of Types
53
53
 
@@ -59,26 +59,20 @@ If an attribute has a value which does not match with given type, the `coerce` m
59
59
 
60
60
  #### Ignoring unknown attributes
61
61
 
62
- `Object` type ignores unknown attributes by default.
63
- You can reject the unknown attributes.
62
+ You can use `ignore` method to ignore unknown attributes.
64
63
 
65
64
  ```
66
- object(attrs).ignore(Set.new) # Ignores nothing (== raise an error)
67
- object(attrs).ignore!(Set.new) # Destructive version
65
+ object(attrs).ignore() # Ignores all unknown attributes.
66
+ object(attrs).ignore(:x, :y) # Ignores :x and :y, but rejects other unknown attributes.
67
+ object(attrs).ignore(except: Set[:x, :y]) # Rejects :x and :y, but ignores other unknown attributes.
68
68
  ```
69
69
 
70
- You can selectively ignore attributes:
70
+ `Object` also provides `reject` method to do the opposite.
71
71
 
72
72
  ```
73
- object(attrs).ignore(Set.new([:a, :b, :c])) # Ignores only :a, :b, and :c
74
- object(attrs).ignore(:any) # Ignores everything (default)
75
- ```
76
-
77
- `Object` also has `prohibit` method to specify attributes to make the type check failed.
78
-
79
- ```
80
- object(attrs).prohibit(Set.new([:created_at, :updated_at])) # Make type check failed if :created_at or :updated_at included
81
- object(attrs).prohibit!(Set.new([:created_at, :updated_at])) # Destructive version
73
+ object(attrs).reject() # Rejects all unknown attributes. (default)
74
+ object(attrs).reject(:x, :y) # Rejects :x and :y, but ignores other unknown attributes.
75
+ object(attrs).reject(except: Set[:x, :y]) # Ignores :x and :y, but rejects other unknown attributes.
82
76
  ```
83
77
 
84
78
  ### array(type)
@@ -86,6 +80,11 @@ object(attrs).prohibit!(Set.new([:created_at, :updated_at])) # Destructive vers
86
80
  * The value must be an array
87
81
  * All elements in the array must be value of given `type`
88
82
 
83
+ ### hash(type)
84
+
85
+ * The value must be an object
86
+ * All values in the object must be value of given `type`
87
+
89
88
  ### optional(type)
90
89
 
91
90
  * The value can be `nil` (or not contained in an object)
@@ -118,6 +117,7 @@ enum(person,
118
117
  ### Base types
119
118
 
120
119
  * `number` The value must be an instance of `Numeric`
120
+ * `integer` The value must be an instance of `Integer`
121
121
  * `string` The value must be an instance of `String`
122
122
  * `boolean` The value must be `true` or `false`
123
123
  * `numeric` The value must be an instance of `Numeric` or a string which represents a number
@@ -133,6 +133,7 @@ enum(person,
133
133
  There are some alias for `optional(base)`, where base is base types, as the following:
134
134
 
135
135
  * `number?`
136
+ * `integer?`
136
137
  * `string?`
137
138
  * `boolean?`
138
139
  * `numeric?`
data/Rakefile CHANGED
@@ -6,11 +6,11 @@ RSpec::Core::RakeTask.new(:spec)
6
6
  task :default => [:spec, :typecheck, :"example:typecheck"]
7
7
 
8
8
  task :typecheck do
9
- sh "bundle exec steep check --strict lib"
9
+ sh "bundle exec steep check"
10
10
  end
11
11
 
12
12
  namespace :example do
13
13
  task :typecheck do
14
- sh "bundle exec steep check --strict -I sig -I example example"
14
+ sh "bundle exec steep check --steepfile=example/Steepfile"
15
15
  end
16
16
  end
@@ -0,0 +1,7 @@
1
+ target :lib do
2
+ signature "sig"
3
+
4
+ check "lib"
5
+
6
+ library "set"
7
+ end
@@ -0,0 +1,8 @@
1
+ target :app do
2
+ signature "sig"
3
+ signature "../sig"
4
+
5
+ check "lib"
6
+
7
+ library "set"
8
+ end
@@ -12,7 +12,7 @@ person = Schema.person.coerce(nil)
12
12
  # @type var name: String
13
13
  name = person[:name]
14
14
 
15
- # @type var contacts: Array<email | address>
15
+ # @type var contacts: Array[email | address]
16
16
  contacts = person[:contacts]
17
17
 
18
18
  contacts.each do |contact|
@@ -0,0 +1,11 @@
1
+ type address = { address: String, country: Symbol? }
2
+ type email = { email: String }
3
+
4
+ class AddressSchema < StrongJSON
5
+ def address: -> StrongJSON::Type::Object[address]
6
+ def email: -> StrongJSON::Type::Object[email]
7
+ def contact: -> StrongJSON::Type::Object[email | address]
8
+ def person: -> StrongJSON::Type::Object[{ name: String, contacts: Array[email | address] }]
9
+ end
10
+
11
+ Schema: AddressSchema
@@ -8,7 +8,7 @@ class StrongJSON
8
8
  end
9
9
 
10
10
  def to_s
11
- format() unless @string
11
+ format() unless defined?(@string)
12
12
  @string
13
13
  end
14
14
 
@@ -18,7 +18,7 @@ class StrongJSON
18
18
  format_trace(path: path)
19
19
  where = format_aliases(path: path, where: [])
20
20
 
21
- # @type var ty: Type::Enum<any>
21
+ # @type var ty: Type::Enum[untyped]
22
22
  if (ty = _ = path.type).is_a?(Type::Enum)
23
23
  ty.types.each do |t|
24
24
  if (a = t.alias)
@@ -67,7 +67,6 @@ class StrongJSON
67
67
  end
68
68
 
69
69
  def format_single_alias(name, type)
70
- # @type const PrettyPrint: any
71
70
  PrettyPrint.format do |pp|
72
71
  pp.text(name.to_s)
73
72
  pp.text(" = ")
@@ -78,7 +77,6 @@ class StrongJSON
78
77
  end
79
78
 
80
79
  def pretty_str(type, expand_alias: false)
81
- # @type const PrettyPrint: any
82
80
  PrettyPrint.singleline_format do |pp|
83
81
  pretty(type, pp, expand_alias: expand_alias)
84
82
  end
@@ -15,12 +15,12 @@ class StrongJSON
15
15
 
16
16
  module WithAlias
17
17
  def alias
18
- @alias
18
+ defined?(@alias) ? @alias : nil
19
19
  end
20
20
 
21
21
  def with_alias(name)
22
22
  _ = dup.tap do |copy|
23
- copy.instance_eval do
23
+ copy.instance_eval do |x|
24
24
  @alias = name
25
25
  end
26
26
  end
@@ -44,6 +44,8 @@ class StrongJSON
44
44
  true
45
45
  when :number
46
46
  value.is_a?(Numeric)
47
+ when :integer
48
+ value.is_a?(Integer)
47
49
  when :string
48
50
  value.is_a?(String)
49
51
  when :boolean
@@ -74,7 +76,6 @@ class StrongJSON
74
76
 
75
77
  def ==(other)
76
78
  if other.is_a?(Base)
77
- # @type var other: Base<any>
78
79
  other.type == type
79
80
  end
80
81
  end
@@ -109,7 +110,6 @@ class StrongJSON
109
110
 
110
111
  def ==(other)
111
112
  if other.is_a?(Optional)
112
- # @type var other: Optional<any>
113
113
  other.type == type
114
114
  end
115
115
  end
@@ -141,7 +141,6 @@ class StrongJSON
141
141
 
142
142
  def ==(other)
143
143
  if other.is_a?(Literal)
144
- # @type var other: Literal<any>
145
144
  other.value == value
146
145
  end
147
146
  end
@@ -178,7 +177,6 @@ class StrongJSON
178
177
 
179
178
  def ==(other)
180
179
  if other.is_a?(Array)
181
- # @type var other: Array<any>
182
180
  other.type == type
183
181
  end
184
182
  end
@@ -192,47 +190,45 @@ class StrongJSON
192
190
  include Match
193
191
  include WithAlias
194
192
 
195
- # @dynamic fields, ignored_attributes, prohibited_attributes
196
- attr_reader :fields, :ignored_attributes, :prohibited_attributes
193
+ # @dynamic fields, on_unknown, exceptions
194
+ attr_reader :fields, :on_unknown, :exceptions
197
195
 
198
- def initialize(fields, ignored_attributes:, prohibited_attributes:)
196
+ def initialize(fields, on_unknown:, exceptions:)
199
197
  @fields = fields
200
- @ignored_attributes = ignored_attributes
201
- @prohibited_attributes = prohibited_attributes
198
+ @on_unknown = on_unknown
199
+ @exceptions = exceptions
202
200
  end
203
201
 
204
202
  def coerce(object, path: ErrorPath.root(self))
205
- unless object.is_a?(Hash)
203
+ unless object.is_a?(::Hash)
206
204
  raise TypeError.new(path: path, value: object)
207
205
  end
208
206
 
209
- unless (intersection = Set.new(object.keys).intersection(prohibited_attributes)).empty?
210
- raise UnexpectedAttributeError.new(path: path, attribute: intersection.to_a.first)
211
- end
212
-
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)
207
+ object = object.dup
208
+ unknown_attributes = Set.new(object.keys) - fields.keys
209
+
210
+ case on_unknown
211
+ when :reject
212
+ unknown_attributes.each do |attr|
213
+ if exceptions.member?(attr)
214
+ object.delete(attr)
215
+ else
216
+ raise UnexpectedAttributeError.new(path: path, attribute: attr)
217
+ end
219
218
  end
220
- when Set
221
- object = object.dup
222
- attrs.each do |key|
223
- object.delete(key)
219
+ when :ignore
220
+ unknown_attributes.each do |attr|
221
+ if exceptions.member?(attr)
222
+ raise UnexpectedAttributeError.new(path: path, attribute: attr)
223
+ else
224
+ object.delete(attr)
225
+ end
224
226
  end
225
227
  end
226
228
 
227
- # @type var result: ::Hash<Symbol, any>
229
+ # @type var result: ::Hash[Symbol, untyped]
228
230
  result = {}
229
231
 
230
- object.each do |key, _|
231
- unless fields.key?(key)
232
- raise UnexpectedAttributeError.new(path: path, attribute: key)
233
- end
234
- end
235
-
236
232
  fields.each do |key, type|
237
233
  result[key] = type.coerce(object[key], path: path.dig(key: key, type: type))
238
234
  end
@@ -240,29 +236,35 @@ class StrongJSON
240
236
  _ = result
241
237
  end
242
238
 
243
- def ignore(attrs)
244
- Object.new(fields, ignored_attributes: attrs, prohibited_attributes: prohibited_attributes)
245
- end
246
-
247
- def ignore!(attrs)
248
- @ignored_attributes = attrs
249
- self
250
- end
251
-
252
- def prohibit(attrs)
253
- Object.new(fields, ignored_attributes: ignored_attributes, prohibited_attributes: attrs)
239
+ def ignore(*ignores, except: nil)
240
+ if ignores.empty? && !except
241
+ Object.new(fields, on_unknown: :ignore, exceptions: Set[])
242
+ else
243
+ if except
244
+ Object.new(fields, on_unknown: :ignore, exceptions: except)
245
+ else
246
+ Object.new(fields, on_unknown: :reject, exceptions: Set.new(ignores))
247
+ end
248
+ end
254
249
  end
255
250
 
256
- def prohibit!(attrs)
257
- @prohibited_attributes = attrs
258
- self
251
+ def reject(*rejecteds, except: nil)
252
+ if rejecteds.empty? && !except
253
+ Object.new(fields, on_unknown: :reject, exceptions: Set[])
254
+ else
255
+ if except
256
+ Object.new(fields, on_unknown: :reject, exceptions: except)
257
+ else
258
+ Object.new(fields, on_unknown: :ignore, exceptions: Set.new(rejecteds))
259
+ end
260
+ end
259
261
  end
260
262
 
261
263
  def update_fields
262
264
  fields.dup.yield_self do |fields|
263
265
  yield fields
264
266
 
265
- Object.new(fields, ignored_attributes: ignored_attributes, prohibited_attributes: prohibited_attributes)
267
+ Object.new(fields, on_unknown: on_unknown, exceptions: exceptions)
266
268
  end
267
269
  end
268
270
 
@@ -276,10 +278,9 @@ class StrongJSON
276
278
 
277
279
  def ==(other)
278
280
  if other.is_a?(Object)
279
- # @type var other: Object<any>
280
281
  other.fields == fields &&
281
- other.ignored_attributes == ignored_attributes &&
282
- other.prohibited_attributes == prohibited_attributes
282
+ other.on_unknown == on_unknown &&
283
+ other.exceptions == exceptions
283
284
  end
284
285
  end
285
286
 
@@ -325,7 +326,6 @@ class StrongJSON
325
326
 
326
327
  def ==(other)
327
328
  if other.is_a?(Enum)
328
- # @type var other: Enum<any>
329
329
  other.types == types &&
330
330
  other.detector == detector
331
331
  end
@@ -336,6 +336,36 @@ class StrongJSON
336
336
  end
337
337
  end
338
338
 
339
+ class Hash
340
+ include Match
341
+ include WithAlias
342
+
343
+ # @dynamic type
344
+ attr_reader :type
345
+
346
+ def initialize(type)
347
+ @type = type
348
+ end
349
+
350
+ def coerce(value, path: ErrorPath.root(self))
351
+ if value.is_a?(::Hash)
352
+ (_ = {}).tap do |result|
353
+ value.each do |k, v|
354
+ result[k] = type.coerce(v, path: path.dig(key: k, type: type))
355
+ end
356
+ end
357
+ else
358
+ raise TypeError.new(path: path, value: value)
359
+ end
360
+ end
361
+
362
+ def ==(other)
363
+ if other.is_a?(Hash)
364
+ other.type == type
365
+ end
366
+ end
367
+ end
368
+
339
369
  class UnexpectedAttributeError < StandardError
340
370
  # @dynamic path, attribute
341
371
  attr_reader :path, :attribute