strong_json 1.0.1 → 2.1.2

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 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