datacaster 0.9.1 → 2.0.1

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: '0945e72cd8cba6c89e6eed3b202e8241eb25ecc12e4a1c44bfeca2be78206306'
4
- data.tar.gz: 9c02c5a237e1d2853a804fa1e31fd2fc13b4e46c5fc207b16461fb5ce9f30570
3
+ metadata.gz: 2d6493682ec13481f878adb35c36562ac7f38dd9fa761a7447d6f05f2812c98c
4
+ data.tar.gz: 32ca64152333af7cf9e9125ddeb01783f06249e2949dfdd139ca6bcb12fe4790
5
5
  SHA512:
6
- metadata.gz: f0a9b9b2aca6f58001550631cd31c381f38b95d95ea652e2a3d1184698fe82cc24bdf8a79074a606a4b3d5437d0cf1f31e8fee997765b83cac6cb4d4ce6670c5
7
- data.tar.gz: eb99b873a43d7c73eeebade0d9a25111c47d3dcd32686f718c2ba1aaa738aca69aece76c096f0c48e21c4f55cd1d754683d02d0c8618c270a4515d039f29c5f4
6
+ metadata.gz: 8f354a530cb8371fe744b851cfd81714ffbac336ac6df79c09fe6e75ca14ccfff77a30efebf84205495f1262ec860c2c8a415734f945af256bb6f84698d52003
7
+ data.tar.gz: 3232aeaa2d583530e3402d61fb321e3f99f0333a0a676defb9e2ae3a8f42e1de358f169b844924737d764514787e498286e6ff7271dcbad8432347e4434670ad
@@ -0,0 +1,17 @@
1
+ name: Rspec
2
+ on: [push, pull_request]
3
+ jobs:
4
+ test:
5
+ runs-on: ubuntu-latest
6
+ strategy:
7
+ matrix:
8
+ ruby-version: ['3.1', '2.7']
9
+ steps:
10
+ - uses: actions/checkout@v3
11
+ - uses: ruby/setup-ruby@v1
12
+ with:
13
+ ruby-version: ${{ matrix.ruby }}
14
+ - name: Install dependencies
15
+ run: bundle install
16
+ - name: Run tests
17
+ run: bundle exec rspec
data/.gitignore CHANGED
@@ -6,6 +6,7 @@
6
6
  /pkg/
7
7
  /spec/reports/
8
8
  /tmp/
9
+ Gemfile.lock
9
10
 
10
11
  # rspec failure tracking
11
12
  .rspec_status
data/.gitlab-ci.yml ADDED
@@ -0,0 +1,8 @@
1
+ image: ruby:latest
2
+
3
+ before_script:
4
+ - bundle install
5
+
6
+ rspec:
7
+ script:
8
+ - bundle exec rspec
data/.rspec CHANGED
File without changes
data/.travis.yml CHANGED
File without changes
data/Gemfile CHANGED
File without changes
data/LICENSE.txt CHANGED
File without changes
data/README.md CHANGED
@@ -4,6 +4,70 @@ This gem provides run-time type checking and mapping of composite data structure
4
4
 
5
5
  Its main use is in the validation and preliminary transformation of API params requests.
6
6
 
7
+
8
+ # Table of contents
9
+
10
+ - [Installing](#installing)
11
+ - [Why not ...](#why-not-)
12
+ - [Basics](#basics)
13
+ - [Conveyor belt](#conveyor-belt)
14
+ - [Result value](#result-value)
15
+ - [Hash schema](#hash-schema)
16
+ - [Logical operators](#logical-operators)
17
+ - [*AND operator*:](#and-operator)
18
+ - [*OR operator*:](#or-operator)
19
+ - [*IF... THEN... ELSE operator*:](#if-then-else-operator)
20
+ - [Built-in types](#built-in-types)
21
+ - [Basic types](#basic-types)
22
+ - [`string`](#string)
23
+ - [`integer`](#integer)
24
+ - [`float`](#float)
25
+ - [`decimal([digits = 8])`](#decimaldigits--8)
26
+ - [`array`](#array)
27
+ - [`hash_value`](#hash_value)
28
+ - [Convenience types](#convenience-types)
29
+ - [`non_empty_string`](#non_empty_string)
30
+ - [`hash_with_symbolized_keys`](#hash_with_symbolized_keys)
31
+ - [`integer32`](#integer32)
32
+ - [Special types](#special-types)
33
+ - [`absent`](#absent)
34
+ - [`any`](#any)
35
+ - [`transform_to_value(value)`](#transform_to_valuevalue)
36
+ - [`remove`](#remove)
37
+ - [`pass`](#pass)
38
+ - [`responds_to(method)`](#responds_tomethod)
39
+ - [`must_be(klass)`](#must_beklass)
40
+ - [`optional(base)`](#optionalbase)
41
+ - [`pick(key)`](#pickkey)
42
+ - [`merge_message_keys(*keys)`](#merge_message_keyskeys)
43
+ - ["Web-form" types](#web-form-types)
44
+ - [`to_integer`](#to_integer)
45
+ - [`to_float`](#to_float)
46
+ - [`to_boolean`](#to_boolean)
47
+ - [`iso8601`](#iso8601)
48
+ - [`optional_param(base)`](#optional_parambase)
49
+ - [Custom and fundamental types](#custom-and-fundamental-types)
50
+ - [`cast(name = 'Anonymous') { |value| ... }`](#castname--anonymous--value--)
51
+ - [`check(name = 'Anonymous', error = 'is invalid') { |value| ... }`](#checkname--anonymous-error--is-invalid--value--)
52
+ - [`try(name = 'Anonymous', error = 'is invalid', catched_exception:) { |value| ... }`](#tryname--anonymous-error--is-invalid-catched_exception--value--)
53
+ - [`validate(active_model_validations, name = 'Anonymous')`](#validateactive_model_validations-name--anonymous)
54
+ - [`compare(reference_value, name = 'Anonymous', error = nil)`](#comparereference_value-name--anonymous-error--nil)
55
+ - [`transform(name = 'Anonymous') { |value| ... }`](#transformname--anonymous--value--)
56
+ - [`transform_if_present(name = 'Anonymous') { |value| ... }`](#transform_if_presentname--anonymous--value--)
57
+ - [Passing additional context to schemas](#passing-additional-context-to-schemas)
58
+ - [Array schemas](#array-schemas)
59
+ - [Hash schemas](#hash-schemas)
60
+ - [Absent is not nil](#absent-is-not-nil)
61
+ - [Schema vs Partial schema](#schema-vs-partial-schema)
62
+ - [AND with error aggregation (`*`)](#and-with-error-aggregation-)
63
+ - [Shortcut nested definitions](#shortcut-nested-definitions)
64
+ - [Mapping hashes: `transform_to_hash`](#mapping-hashes-transform_to_hash)
65
+ - [Error remapping](#error-remapping)
66
+ - [Registering custom 'predefined' types](#registering-custom-predefined-types)
67
+ - [Contributing](#contributing)
68
+ - [Ideas/TODO](#ideastodo)
69
+ - [License](#license)
70
+
7
71
  ## Installing
8
72
 
9
73
  Add to your Gemfile:
@@ -50,7 +114,7 @@ validator.(1).value # nil
50
114
  validator.(1).errors # ["must be string"]
51
115
  ```
52
116
 
53
- Datacaster instances are created with a call to `Datacaster.schema { ... }` or `Datacaster.partial_schema { ... }` (described later in this file).
117
+ Datacaster instances are created with a call to `Datacaster.schema { ... }`, `Datacaster.partial_schema { ... }` or `Datacaster.choosy_schema { ... }` (described later in this file).
54
118
 
55
119
  Datacaster validators' results could be converted to [dry result monad](https://dry-rb.org/gems/dry-monads/1.0/result/):
56
120
 
@@ -103,9 +167,9 @@ Validating hashes is the main case scenario for datacaster. Several specific con
103
167
  Let's assume we want to validate that a hash (which represents data about a person):
104
168
 
105
169
  a) is, in fact, a Hash;
106
- a) has exactly 2 keys, `name` and `salary`,
107
- b) key 'name' is a string,
108
- c) key 'salary' is an integer:
170
+ b) has exactly 2 keys, `name` and `salary`,
171
+ c) key 'name' is a string,
172
+ d) key 'salary' is an integer:
109
173
 
110
174
  ```ruby
111
175
  person_validator =
@@ -135,7 +199,7 @@ person_validator.(name: "John Smith", salary: 100_000, title: "developer")
135
199
  # => Datacaster::ErrorResult({:title=>["must be absent"]})
136
200
  ```
137
201
 
138
- `Datacaster.schema` definitions don't permit, as you likely noticed from the example above, extra fields in the hash. In fact, `Datacaster.schema` automatically adds special built-in validator, called `Datacaster::Terminator`, at the end of your validation chain, which function is to ensure that all hash keys had been validated.
202
+ `Datacaster.schema` definitions don't permit, as you likely noticed from the example above, extra fields in the hash. In fact, `Datacaster.schema` automatically adds special built-in validator, called `Datacaster::Terminator::Raising`, at the end of your validation chain, which function is to ensure that all hash keys had been validated.
139
203
 
140
204
  If you want to permit your hashes to contain extra fields, use `Datacaster.partial_schema` (it's the only difference between `.schema` and `.partial_schema`):
141
205
 
@@ -152,6 +216,21 @@ person_with_extra_keys_validator.(name: "John Smith", salary: 100_000, title: "d
152
216
  # => Datacaster::ValidResult({:name=>"John Smith", :salary=>100000, :title=>"developer"})
153
217
  ```
154
218
 
219
+ Also if you want to delete extra fields, use `Datacaster.choosy_schema`:
220
+
221
+ ```ruby
222
+ person_with_extra_keys_validator =
223
+ Datacaster.choosy_schema do
224
+ hash_schema(
225
+ name: string,
226
+ salary: integer
227
+ )
228
+ end
229
+
230
+ person_with_extra_keys_validator.(name: "John Smith", salary: 100_000, age: 18)
231
+ # => Datacaster::ValidResult({:name=>"John Smith", :salary=>100000})
232
+ ```
233
+
155
234
  Datacaster 'hash schema' makes strict difference between absent and nil values, allows to use shortcuts for defining nested schemas (with no limitation on the level of nesting), and has convinient 'AND with error aggregation' (`*`, same symbol as in numbers multiplication) for joining validation errors of multiple failures. See below in the corresponding sections.
156
235
 
157
236
  ### Logical operators
@@ -177,7 +256,7 @@ even_number.(2)
177
256
  even_number.(3)
178
257
  # => Datacaster::ErrorResult(["is invalid"])
179
258
  even_number.("test")
180
- # => #<Datacaster::ErrorResult(["must be integer"])>
259
+ # => Datacaster::ErrorResult(["must be integer"])
181
260
  ```
182
261
 
183
262
  If left-hand validation of AND operator passes, *its result* (not the original value) is passed to the right-hand validation. See below in this file section on transformations where this might be relevant.
@@ -200,7 +279,7 @@ Notice that OR operator, if left-hand validation fails, passes the original valu
200
279
 
201
280
  Let's suppose we want to validate that incoming hash is either 'person' or 'entity', where
202
281
 
203
- - 'person' is a hash with 3 keys (kind: `:person`, name: string, salary: integer),
282
+ - 'person' is a hash with 3 keys (kind: `:person`, name: string, salary: integer),
204
283
  - 'entity' is a hash with 4 keys (kind: `:entity`, title: string, form: string, revenue: integer).
205
284
 
206
285
  ```ruby
@@ -325,7 +404,7 @@ max_concurrent_connections = Datacaster.schema { compare(nil).then(transform_to_
325
404
 
326
405
  max_concurrent_connections.(9) # => Datacaster::ValidResult(9)
327
406
  max_concurrent_connections.("9") # => Datacaster::ErrorResult(["must be integer"])
328
- max_concurrent_connections.(nil) #=> #<Datacaster::ValidResult(5)>
407
+ max_concurrent_connections.(nil) # => Datacaster::ValidResult(5)
329
408
  ```
330
409
 
331
410
  #### `remove`
@@ -398,6 +477,101 @@ pick_name_and_age.(last_name: "Johnson", age: 20) # => Datacaster::ValidResult([
398
477
  pick_name_and_age.("test") # => Datacaster::ErrorResult(["must be Enumerable"])
399
478
  ```
400
479
 
480
+ #### `merge_message_keys(*keys)`
481
+
482
+ Returns ValidResult only if value `#is_a?(Hash)`.
483
+
484
+ Maps incoming hash to Datacaster styled messages.
485
+
486
+ ```ruby
487
+ mapper =
488
+ Datacaster.schema do
489
+ merge_message_keys(:a, :b)
490
+ end
491
+
492
+ mapper.(a: "1", b: "2") # => Datacaster::ValidResult(["1", "2"])
493
+ ```
494
+
495
+ Arrays are merged. Merging `["1", "2"]` and `["2", "3"]` will produce `["1", "2", "3"]`.
496
+
497
+ Hash values are merged recursively (deeply) with one another:
498
+
499
+ ```ruby
500
+ mapper = Datacaster.schema do
501
+ transform_to_hash(
502
+ resourse: merge_message_keys(:resourse),
503
+ user: merge_message_keys(:user, :login_params),
504
+ login_params: remove
505
+ )
506
+ end
507
+
508
+ mapper.(
509
+ resourse: "request was rejected",
510
+ user: {
511
+ age: "too young", password: "too long"
512
+ },
513
+ login_params: {
514
+ password: "should contain special characters",
515
+ nickname: "too short"
516
+ }
517
+ )
518
+ # => Datacaster::ValidResult({
519
+ # :resourse=>["request was rejected"],
520
+ # :user=>{
521
+ # :age=>["too young"],
522
+ # :password=>["too long", "should contain special characters"],
523
+ # :nickname=>["too short"]
524
+ # }
525
+ # })
526
+ ```
527
+
528
+ Hash value merges non-Hash value by merging it with `:base` key (added if absent):
529
+
530
+ ```ruby
531
+ mapping = Datacaster.schema do
532
+ transform_to_hash(
533
+ resourse: merge_message_keys(:resourse),
534
+ user: merge_message_keys(:user, :user_error),
535
+ user_error: remove
536
+ )
537
+ end
538
+
539
+ mapping.(
540
+ resourse: "request was rejected",
541
+ user: {age: "too young", nickname: "too long"},
542
+ user_error: "user is invalid"
543
+ )
544
+ # => Datacaster::ValidResult({
545
+ # :resourse=>["request was rejected"],
546
+ # :user=>{
547
+ # :age=>["too young"],
548
+ # :nickname=>["too long"],
549
+ # :base=>["user is invalid"]
550
+ # }
551
+ # })
552
+ ```
553
+
554
+ Hash keys with `nil` and `[]` values are deeply ignored:
555
+
556
+ ```ruby
557
+ mapping = Datacaster.schema do
558
+ transform_to_hash(
559
+ user: merge_message_keys(:user),
560
+ )
561
+ end
562
+
563
+ mapping.(
564
+ user: {
565
+ age: "too young", nickname: [], user_error: nil
566
+ }
567
+ )
568
+ # => Datacaster::ValidResult({
569
+ # :user=> {
570
+ # :age=>["too young"]
571
+ # }
572
+ # })
573
+ ```
574
+
401
575
  ### "Web-form" types
402
576
 
403
577
  These types are convenient to parse and validate POST forms and decode JSON requests.
@@ -572,6 +746,49 @@ city.(name: "Denver", distance: "2.5") # => Datacaster::ValidResult({:name=>"Den
572
746
 
573
747
  Always returns ValidResult. If the value is `Datacaster.absent` (singleton instance, see below section on hash schemas), then `Datacaster.absent` is returned (block isn't called). Otherwise, works like `transform`.
574
748
 
749
+ ### Passing additional context to schemas
750
+
751
+ You can pass `context` to schema using `.with_context` method
752
+
753
+ ```ruby
754
+ # class User < ApplicationRecord
755
+ # ...
756
+ # end
757
+ #
758
+ # class Post < ApplicationRecord
759
+ # belongs_to :user
760
+ # ...
761
+ # end
762
+
763
+ schema =
764
+ Datacaster.schema do
765
+ hash_schema(
766
+ post_id: to_integer & check { |id| Post.where(id: id, user_id: context.current_user).exists? }
767
+ )
768
+ end
769
+
770
+ current_user = ...
771
+
772
+ schema.with_context(current_user: current_user).(post_id: 15)
773
+ ```
774
+
775
+ `context` is an [OpenStruct](https://ruby-doc.org/stdlib-3.1.0/libdoc/ostruct/rdoc/OpenStruct.html) instance which is initialized in `.with_context`
776
+
777
+ **Note**
778
+
779
+ `context` can be accesed only in types' blocks:
780
+ ```ruby
781
+ mail_transformer = Datacaster.schema { transform { |v| "#{v}#{context.postfix}" } }
782
+
783
+ mail_transformer.with_context(postfix: "@domen.com").("admin")
784
+ # => #<Datacaster::ValidResult("admin@domen.com")>
785
+ ```
786
+ It can't be used in schema definition block itself:
787
+ ```ruby
788
+ Datacaster.schema { context.error }
789
+ # leads to `NoMethodError`
790
+ ```
791
+
575
792
  ### Array schemas
576
793
 
577
794
  To define compound data type, array of 'something', use `array_schema(something)` (or, synonymically, `array_of(something)`). There is no way to define array wherein each element is of different type.
@@ -660,7 +877,7 @@ If a) fails, `ErrorResult(["must be hash"])` is returned.
660
877
  if b) fails, `ErrorResult(key1 => [errors...], key2 => [errors...])` is returned. Each key of wrapped "error hash" corresponds to the key of validated hash, and each value of "error hash" contains array of errors, returned by the corresponding validator.
661
878
  If b) fulfilled, then and only then validated hash is checked for extra keys. If they are found, `ErrorResult(extra_key_1 => ["must be absent"], ...)` is returned.
662
879
 
663
- Technically, last part is implemented with special singleton validator, called `#<Datacaster::Terminator>`, which is automatically added to the validation chain (with the use of `&` operator) by `Datacaster.schema` method. Don't be scared if you see it in the output of `#inspect` method of your validators (e.g. in `irb`).
880
+ Technically, last part is implemented with special singleton validator, called `#<Datacaster::Terminator::Raising>`, which is automatically added to the validation chain (with the use of `&` operator) by `Datacaster.schema` method. Don't be scared if you see it in the output of `#inspect` method of your validators (e.g. in `irb`).
664
881
 
665
882
  #### Absent is not nil
666
883
 
@@ -735,7 +952,7 @@ Sometimes it is necessary to omit that requirement and allow for hash to contain
735
952
 
736
953
  Let's say we have:
737
954
 
738
- * 'people' (hashes with `name: string`, `description: string` and `kind: 'person'` fields),
955
+ * 'people' (hashes with `name: string`, `description: string` and `kind: 'person'` fields),
739
956
  * 'entities' (hash with `title: string`, `description: string` and `kind: 'entity'` fields).
740
957
 
741
958
  In other words, we have some polymorphic resource, which type is defined by `kind` field, and which has common fields for all its "sub-kinds" (in this example: `description`), and also fields specific to each "kind" (in database we often model this as [STI](https://api.rubyonrails.org/v6.0.3.2/classes/ActiveRecord/Base.html#class-ActiveRecord::Base-label-Single+table+inheritance)).
@@ -967,6 +1184,77 @@ Here is what is happening when `city_with_distance` (from the example above) is
967
1184
 
968
1185
  Note: because of point e) above we need to explicitly delete `distance_in_meters` key, because otherwise `transform_to_hash` will copy it to the resultant hash without validation. And all non-validated keys at the end of `Datacaster.schema` block (as explained above in section on partial schemas) result in error.
969
1186
 
1187
+ ## Error remapping
1188
+
1189
+ In some cases it might be useful to remap resulting `Datacaster::ErrorResult`:
1190
+
1191
+ ```ruby
1192
+ schema =
1193
+ Datacaster.schema do
1194
+ transform = transform_to_hash(
1195
+ posts: pick(:user_id) & to_integer & transform { |user| Posts.where(user_id: user.id).to_a },
1196
+ user_id: remove
1197
+ )
1198
+ end
1199
+
1200
+ schema.(user_id: 'wrong') # => #<Datacaster::ErrorResult({:posts=>["must be integer"]})>
1201
+ # Instead of #<Datacaster::ErrorResult({:user_id=>["must be integer"]})>
1202
+ ```
1203
+
1204
+ `.cast_errors` can be used in such case:
1205
+
1206
+ ```ruby
1207
+ schema =
1208
+ Datacaster.schema do
1209
+ transform = transform_to_hash(
1210
+ posts: pick(:user_id) & to_integer & transform { |user| Posts.where(user_id: user.id).to_a },
1211
+ user_id: remove
1212
+ )
1213
+
1214
+ transform.cast_errors(
1215
+ transform_to_hash(
1216
+ user_id: pick(:posts),
1217
+ posts: remove
1218
+ )
1219
+ )
1220
+ end
1221
+
1222
+ schema.(user_id: 'wrong') # => #<Datacaster::ErrorResult({:user_id=>["must be integer"]})>
1223
+ ```
1224
+ any instance of `Datacaster` can be passed to `.cast_errors`
1225
+
1226
+
1227
+ ## Registering custom 'predefined' types
1228
+
1229
+ In order to extend `Datacaster` functionality, custom types can be added
1230
+
1231
+ There are two ways to add cutsom types to `Datacaster`:
1232
+
1233
+ 1\. Using lambda definition:
1234
+
1235
+ ```ruby
1236
+ Datacaster::Config.add_predefined_caster(:time_string, -> {
1237
+ string & validate(format: { with: /\A(0[0-9]|1[0-9]|2[0-3]):[03]0\z/ })
1238
+ })
1239
+
1240
+ schema = Datacaster.schema { time_string }
1241
+
1242
+ schema.("23:00") # => #<Datacaster::ValidResult("23:00")>
1243
+ schema.("no_time_string") # => #<Datacaster::ErrorResult(["is invalid"])>
1244
+ ```
1245
+
1246
+ 2\. Using `Datacaster` instance:
1247
+
1248
+ ```ruby
1249
+ css_color = Datacaster.partial_schema { string & validate(format: { with: /\A#(?:\h{3}){1,2}\z/ }) }
1250
+ Datacaster::Config.add_predefined_caster(:css_color, css_color)
1251
+
1252
+ schema = Datacaster.schema { css_color }
1253
+
1254
+ schema.("#123456") # => #<Datacaster::ValidResult("#123456")>
1255
+ schema.("no_css_color") # => #<Datacaster::ErrorResult(["is invalid"])>
1256
+ ```
1257
+
970
1258
  ## Contributing
971
1259
 
972
1260
  Fork, create issues and make PRs as usual.
data/Rakefile CHANGED
File without changes
data/bin/setup CHANGED
File without changes
data/datacaster.gemspec CHANGED
File without changes
File without changes
@@ -5,11 +5,10 @@ module Datacaster
5
5
  @right = right
6
6
  end
7
7
 
8
- def call(object)
8
+ def cast(object)
9
9
  object = super(object)
10
10
 
11
11
  left_result = @left.(object)
12
-
13
12
  return left_result unless left_result.valid?
14
13
 
15
14
  @right.(left_result)
@@ -7,9 +7,8 @@ module Datacaster
7
7
 
8
8
  # Works like AndNode, but doesn't stop at first error — in order to aggregate all Failures
9
9
  # Makes sense only for Hash Schemas
10
- def call(object)
10
+ def cast(object)
11
11
  object = super(object)
12
-
13
12
  left_result = @left.(object)
14
13
 
15
14
  if left_result.valid?
@@ -2,10 +2,10 @@ module Datacaster
2
2
  class ArraySchema < Base
3
3
  def initialize(element_caster)
4
4
  # support of shortcut nested validation definitions, e.g. array_schema({a: [integer], b: {c: integer}})
5
- @element_caster = shortcut_definition(element_caster) # & Terminator.instance
5
+ @element_caster = shortcut_definition(element_caster)
6
6
  end
7
7
 
8
- def call(object)
8
+ def cast(object)
9
9
  object = super(object)
10
10
  checked_schema = object.meta[:checked_schema] || []
11
11
 
@@ -1,3 +1,5 @@
1
+ require "ostruct"
2
+
1
3
  module Datacaster
2
4
  class Base
3
5
  def self.merge_errors(left, right)
@@ -45,8 +47,33 @@ module Datacaster
45
47
  ThenNode.new(self, other)
46
48
  end
47
49
 
50
+ def set_definition_context(definition_context)
51
+ @definition_context = definition_context
52
+ end
53
+
54
+ def with_context(additional_context)
55
+ @definition_context.context = OpenStruct.new(additional_context)
56
+ self
57
+ end
58
+
48
59
  def call(object)
49
- Datacaster.ValidResult(object)
60
+ object = cast(object)
61
+
62
+ return object if object.valid? || @cast_errors.nil?
63
+
64
+ error_cast = @cast_errors.(object.errors)
65
+
66
+ raise "#cast_errors must return Datacaster.ValidResult, currently it is #{error_cast.inspect}" unless error_cast.valid?
67
+
68
+ Datacaster.ErrorResult(
69
+ @cast_errors.(object.errors).value,
70
+ meta: object.meta
71
+ )
72
+ end
73
+
74
+ def cast_errors(object)
75
+ @cast_errors = shortcut_definition(object)
76
+ self
50
77
  end
51
78
 
52
79
  def inspect
@@ -55,6 +82,10 @@ module Datacaster
55
82
 
56
83
  private
57
84
 
85
+ def cast(object)
86
+ Datacaster.ValidResult(object)
87
+ end
88
+
58
89
  # Translates hashes like {a: <IntegerChecker>} to <HashSchema {a: <IntegerChecker>}>
59
90
  # and arrays like [<IntegerChecker>] to <ArraySchema <IntegerChecker>>
60
91
  def shortcut_definition(definition)
@@ -63,7 +94,7 @@ module Datacaster
63
94
  definition
64
95
  when Array
65
96
  if definition.length != 1
66
- raise ArgumentError.new("Datacaster: shorcut array definitions must have exactly 1 element in the array, e.g. [integer]")
97
+ raise ArgumentError.new("Datacaster: shortcut array definitions must have exactly 1 element in the array, e.g. [integer]")
67
98
  end
68
99
  ArraySchema.new(definition.first)
69
100
  when Hash
@@ -7,7 +7,7 @@ module Datacaster
7
7
  @cast = block
8
8
  end
9
9
 
10
- def call(object)
10
+ def cast(object)
11
11
  intermediary_result = super(object)
12
12
  object = intermediary_result.value
13
13
 
@@ -8,7 +8,7 @@ module Datacaster
8
8
  @check = block
9
9
  end
10
10
 
11
- def call(object)
11
+ def cast(object)
12
12
  intermediary_result = super(object)
13
13
  object = intermediary_result.value
14
14
 
@@ -6,7 +6,7 @@ module Datacaster
6
6
  @error = error || "must be equal to #{value.inspect}"
7
7
  end
8
8
 
9
- def call(object)
9
+ def cast(object)
10
10
  intermediary_result = super(object)
11
11
  object = intermediary_result.value
12
12
 
@@ -0,0 +1,19 @@
1
+ module Datacaster
2
+ module Config
3
+ extend self
4
+
5
+ def add_predefined_caster(name, definition)
6
+ caster =
7
+ case definition
8
+ when Proc
9
+ Datacaster.partial_schema(&definition)
10
+ when Base
11
+ definition
12
+ else
13
+ raise ArgumentError.new("Expected Datacaster defintion lambda or Datacaster instance")
14
+ end
15
+
16
+ Predefined.define_method(name.to_sym) { caster }
17
+ end
18
+ end
19
+ end
@@ -2,19 +2,18 @@ require 'bigdecimal'
2
2
  require 'date'
3
3
 
4
4
  module Datacaster
5
- class RunnerContext
6
- include Singleton
5
+ class DefinitionContext
7
6
  include Datacaster::Predefined
8
7
  include Dry::Monads[:result]
9
8
 
10
- alias_method :array_of, :array_schema
9
+ attr_accessor :context
11
10
 
12
- def m(definition)
13
- raise 'not implemented'
11
+ def m(_definition)
12
+ raise "not implemented"
14
13
  end
15
14
 
16
15
  def method_missing(m, *args)
17
- arg_string = args.empty? ? '' : "(#{args.map(&:inspect).join(', ')})"
16
+ arg_string = args.empty? ? "" : "(#{args.map(&:inspect).join(', ')})"
18
17
  raise "Datacaster: unknown definition '#{m}#{arg_string}'"
19
18
  end
20
19
  end
@@ -1,12 +1,20 @@
1
1
  module Datacaster
2
2
  class HashMapper < Base
3
3
  def initialize(fields)
4
+ keys = fields.keys.flatten
5
+ if keys.length != keys.uniq.length
6
+ intersection = keys.select { |k| keys.count(k) > 1 }.uniq.sort
7
+ raise ArgumentError.new("When using transform_to_hash([:a, :b, :c] => validator), " \
8
+ "each key should not be mentioned more than once on the left-hand-side. Instead, got these " \
9
+ "keys mentioned twice or more: #{intersection.inspect}."
10
+ )
11
+ end
12
+
4
13
  @fields = fields
5
14
  end
6
15
 
7
- def call(object)
16
+ def cast(object)
8
17
  object = super(object)
9
-
10
18
  # return Datacaster.ErrorResult(["must be hash"]) unless object.value.is_a?(Hash)
11
19
 
12
20
  checked_schema = object.meta[:checked_schema].dup || {}
@@ -18,24 +26,32 @@ module Datacaster
18
26
  new_value = validator.(object)
19
27
 
20
28
  # transform_to_hash([:a, :b, :c] => pick(:a, :b, :c) & ...)
21
- keys = Array(key)
22
- values_or_errors = Array(new_value.value || new_value.errors)
23
- if keys.length != values_or_errors.length
24
- raise TypeError.new("When using transform_to_hash([:a, :b, :c] => validator), validator should return Array "\
25
- "with number of elements equal to the number of elements in left-hand-side array.\n" \
26
- "Got the following (values or errors) instead: #{keys.inspect} => #{values_or_errors.inspect}.")
29
+ if key.is_a?(Array)
30
+ unwrapped = new_value.valid? ? new_value.value : new_value.errors
31
+
32
+ if key.length != unwrapped.length
33
+ raise TypeError.new("When using transform_to_hash([:a, :b, :c] => validator), validator should return Array "\
34
+ "with number of elements equal to the number of elements in left-hand-side array.\n" \
35
+ "Got the following (values or errors) instead: #{keys.inspect} => #{values_or_errors.inspect}.")
36
+ end
27
37
  end
28
38
 
29
39
  if new_value.valid?
30
- keys.each.with_index do |key, i|
31
- result[key] = values_or_errors[i]
40
+ if key.is_a?(Array)
41
+ key.zip(new_value.value) do |new_key, new_key_value|
42
+ result[new_key] = new_key_value
43
+ checked_schema[new_key] = true
44
+ end
45
+ else
46
+ result[key] = new_value.value
32
47
  checked_schema[key] = true
33
48
  end
34
-
35
- single_returned_schema = new_value.meta[:checked_schema].dup
36
- checked_schema[keys.first] = single_returned_schema if keys.length == 1 && single_returned_schema
37
49
  else
38
- errors.merge!(keys.zip(values_or_errors).to_h)
50
+ if key.is_a?(Array)
51
+ errors = self.class.merge_errors(errors, key.zip(new_value.errors).to_h)
52
+ else
53
+ errors = self.class.merge_errors(errors, {key => new_value.errors})
54
+ end
39
55
  end
40
56
  end
41
57
 
@@ -6,9 +6,8 @@ module Datacaster
6
6
  @fields.transform_values! { |validator| shortcut_definition(validator) }
7
7
  end
8
8
 
9
- def call(object)
9
+ def cast(object)
10
10
  object = super(object)
11
-
12
11
  return Datacaster.ErrorResult(["must be hash"]) unless object.value.is_a?(Hash)
13
12
 
14
13
  checked_schema = object.meta[:checked_schema].dup || {}
@@ -0,0 +1,98 @@
1
+ module Datacaster
2
+ class MessageKeysMerger < Base
3
+ def initialize(keys)
4
+ @keys = keys
5
+ end
6
+
7
+ def cast(object)
8
+ intermediary_result = super(object)
9
+ object = intermediary_result.value
10
+
11
+ return Datacaster.ErrorResult(["must be Hash"]) unless object.is_a?(Hash)
12
+
13
+ result = set_initial_value(object)
14
+
15
+ @keys.each do |k|
16
+ result =
17
+ if need_hash_merger?(object)
18
+ merge_hash(result, object[k])
19
+ else
20
+ merge_array_or_scalar(result, object[k])
21
+ end
22
+ end
23
+
24
+ result = clean(result)
25
+
26
+ Datacaster.ValidResult(
27
+ result.nil? ? Datacaster.absent : result
28
+ )
29
+ end
30
+
31
+ private
32
+
33
+ def set_initial_value(object)
34
+ need_hash_merger?(object) ? {} : []
35
+ end
36
+
37
+ def need_hash_merger?(object)
38
+ @need_hash_merger =
39
+ @need_hash_merger.nil? ? @keys.any? { |k| object[k].is_a?(Hash) } : @need_hash_merger
40
+ end
41
+
42
+ def merge_array_or_scalar(unit, merge_with)
43
+ merge_with = [merge_with] unless merge_with.is_a?(Array)
44
+ unit = [unit] unless unit.is_a?(Array)
45
+
46
+ result = clean(unit | merge_with)
47
+ result.uniq == [nil] || result == [] ? Datacaster.absent : result
48
+ end
49
+
50
+ def value_or(value, default)
51
+ value.nil? ? default : value
52
+ end
53
+
54
+ def merge_hash_with_hash(result, merge_with)
55
+ if merge_with.is_a?(Hash)
56
+ merge_with.each do |k, v|
57
+ if merge_with.is_a?(Hash)
58
+ result = value_or(result, {})
59
+ result[k] = merge_hash_with_hash(result[k], v)
60
+ else
61
+ result[k] = merge_array_or_scalar(value_or(result[k], []), v)
62
+ end
63
+ end
64
+
65
+ result
66
+ else
67
+ result = merge_array_or_scalar(value_or(result, []), merge_with)
68
+ end
69
+ end
70
+
71
+ def merge_hash(result, merge_with)
72
+ if merge_with.is_a?(Hash)
73
+ merge_with.each do |k, v|
74
+ result[k] = merge_hash_with_hash(result[k], v)
75
+ end
76
+ else
77
+ result[:base] = merge_array_or_scalar(value_or(result[:base], []), merge_with)
78
+ end
79
+
80
+ result
81
+ end
82
+
83
+ def clean(value)
84
+ case value
85
+ when Array
86
+ value.delete_if do |v|
87
+ clean(v) if v.is_a?(Hash) || v.is_a?(Array)
88
+ v == Datacaster.absent
89
+ end
90
+ when Hash
91
+ value.delete_if do |_k, v|
92
+ clean(v) if v.is_a?(Hash) || v.is_a?(Array)
93
+ v == Datacaster.absent
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -5,9 +5,8 @@ module Datacaster
5
5
  @right = right
6
6
  end
7
7
 
8
- def call(object)
8
+ def cast(object)
9
9
  object = super(object)
10
-
11
10
  left_result = @left.(object)
12
11
 
13
12
  return left_result if left_result.valid?
@@ -33,6 +33,7 @@ module Datacaster
33
33
  def array_schema(element_caster)
34
34
  ArraySchema.new(element_caster)
35
35
  end
36
+ alias_method :array_of, :array_schema
36
37
 
37
38
  def hash_schema(fields)
38
39
  HashSchema.new(fields)
@@ -85,6 +86,10 @@ module Datacaster
85
86
  }
86
87
  end
87
88
 
89
+ def merge_message_keys(*keys)
90
+ MessageKeysMerger.new(keys)
91
+ end
92
+
88
93
  def responds_to(method)
89
94
  check('RespondsTo', "must respond to #{method.inspect}") { |x| x.respond_to?(method) }
90
95
  end
File without changes
@@ -2,75 +2,96 @@ require 'singleton'
2
2
  require 'dry-monads'
3
3
 
4
4
  module Datacaster
5
- class Terminator < Base
6
- include Singleton
7
- include Dry::Monads[:result]
8
-
9
- def call(object, checked_schema = nil)
10
- object = super(object)
11
- checked_schema ||= object.meta[:checked_schema]
12
-
13
- case object.value
14
- when Array
15
- check_array(object.value, checked_schema)
16
- when Hash
17
- check_hash(object.value, checked_schema)
18
- else
19
- Datacaster.ValidResult(object.value)
5
+ class Terminator
6
+ module TerminatorBase
7
+ include Dry::Monads[:result]
8
+
9
+ def self.included(klass)
10
+ klass.include Singleton
20
11
  end
21
- end
22
12
 
23
- def inspect
24
- "#<Datacaster::Terminator>"
25
- end
13
+ def cast(object, checked_schema = nil)
14
+ object = super(object)
15
+ checked_schema ||= object.meta[:checked_schema]
16
+
17
+ case object.value
18
+ when Array
19
+ check_array(object.value, checked_schema)
20
+ when Hash
21
+ check_hash(object.value, checked_schema)
22
+ else
23
+ Datacaster.ValidResult(object.value)
24
+ end
25
+ end
26
26
 
27
- private
27
+ def inspect
28
+ "#<Datacaster::Terminator>"
29
+ end
28
30
 
29
- def check_array(array, checked_schema)
30
- return Datacaster.ValidResult(array) unless checked_schema
31
+ private
31
32
 
32
- result = array.zip(checked_schema).map { |x, schema| call(x, schema) }
33
+ def check_array(array, checked_schema)
34
+ return Datacaster.ValidResult(array) unless checked_schema
33
35
 
34
- if result.all?(&:valid?)
35
- Datacaster.ValidResult(result.map(&:value))
36
- else
37
- Datacaster.ErrorResult(result.each.with_index.reject { |x, _| x.valid? }.map { |x, i| [i, x.errors] }.to_h)
36
+ result = array.zip(checked_schema).map { |x, schema| cast(x, schema) }
37
+
38
+ if result.all?(&:valid?)
39
+ Datacaster.ValidResult(result.map(&:value))
40
+ else
41
+ Datacaster.ErrorResult(result.each.with_index.reject { |x, _| x.valid? }.map { |x, i| [i, x.errors] }.to_h)
42
+ end
38
43
  end
39
- end
40
44
 
41
- def check_hash(hash, checked_schema)
42
- return Datacaster.ValidResult(hash) unless checked_schema
45
+ def check_hash(hash, checked_schema)
46
+ return Datacaster.ValidResult(hash) unless checked_schema
43
47
 
44
- errors = {}
45
- result = {}
48
+ errors = {}
49
+ result = {}
46
50
 
47
- hash.each do |(k, v)|
48
- if v == Datacaster.absent
49
- next
50
- end
51
+ hash.each do |(k, v)|
52
+ if v == Datacaster.absent
53
+ next
54
+ end
51
55
 
52
- unless checked_schema.key?(k)
53
- errors[k] = ["must be absent"]
54
- next
55
- end
56
+ unless checked_schema.key?(k)
57
+ errors[k] = ["must be absent"] if is_a?(Raising)
58
+ next
59
+ end
60
+
61
+ if checked_schema[k] == true
62
+ result[k] = v
63
+ next
64
+ end
56
65
 
57
- if checked_schema[k] == true
58
- result[k] = v
59
- next
66
+ nested_value = cast(v, checked_schema[k])
67
+ if nested_value.valid?
68
+ result[k] = nested_value.value
69
+ else
70
+ errors[k] = nested_value.errors
71
+ end
60
72
  end
61
73
 
62
- nested_value = call(v, checked_schema[k])
63
- if nested_value.valid?
64
- result[k] = nested_value.value
74
+ if errors.empty?
75
+ Datacaster.ValidResult(result)
65
76
  else
66
- errors[k] = nested_value.errors
77
+ Datacaster.ErrorResult(errors)
67
78
  end
68
79
  end
80
+ end
81
+
82
+ class Raising < Base
83
+ include TerminatorBase
84
+
85
+ def inspect
86
+ "#<Datacaster::Terminator::Raising>"
87
+ end
88
+ end
69
89
 
70
- if errors.empty?
71
- Datacaster.ValidResult(result)
72
- else
73
- Datacaster.ErrorResult(errors)
90
+ class Sweeping < Base
91
+ include TerminatorBase
92
+
93
+ def inspect
94
+ "#<Datacaster::Terminator::Sweeping>"
74
95
  end
75
96
  end
76
97
  end
@@ -6,19 +6,18 @@ module Datacaster
6
6
  end
7
7
 
8
8
  def else(else_caster)
9
- raise ArgumentError.new('Datacaster: double else clause is not permitted') if @else
9
+ raise ArgumentError.new("Datacaster: double else clause is not permitted") if @else
10
10
 
11
11
  @else = else_caster
12
12
  self
13
13
  end
14
14
 
15
- def call(object)
15
+ def cast(object)
16
16
  unless @else
17
17
  raise ArgumentError.new('Datacaster: use "a & b" instead of "a.then(b)" when there is no else-clause')
18
18
  end
19
19
 
20
20
  object = super(object)
21
-
22
21
  left_result = @left.(object)
23
22
 
24
23
  if left_result.valid?
@@ -7,7 +7,7 @@ module Datacaster
7
7
  @transform = block
8
8
  end
9
9
 
10
- def call(object)
10
+ def cast(object)
11
11
  intermediary_result = super(object)
12
12
  object = intermediary_result.value
13
13
 
@@ -9,7 +9,7 @@ module Datacaster
9
9
  @transform = block
10
10
  end
11
11
 
12
- def call(object)
12
+ def cast(object)
13
13
  intermediary_result = super(object)
14
14
  object = intermediary_result.value
15
15
 
@@ -26,7 +26,7 @@ module Datacaster
26
26
  @validator = self.class.create_active_model(validations)
27
27
  end
28
28
 
29
- def call(object)
29
+ def cast(object)
30
30
  intermediary_result = super(object)
31
31
  object = intermediary_result.value
32
32
 
@@ -1,3 +1,3 @@
1
1
  module Datacaster
2
- VERSION = "0.9.1"
2
+ VERSION = "2.0.1"
3
3
  end
data/lib/datacaster.rb CHANGED
@@ -4,15 +4,17 @@ require_relative 'datacaster/version'
4
4
  require_relative 'datacaster/absent'
5
5
  require_relative 'datacaster/base'
6
6
  require_relative 'datacaster/predefined'
7
- require_relative 'datacaster/runner_context'
7
+ require_relative 'datacaster/definition_context'
8
8
  require_relative 'datacaster/terminator'
9
+ require_relative 'datacaster/config'
9
10
 
11
+ require_relative 'datacaster/array_schema'
10
12
  require_relative 'datacaster/caster'
11
13
  require_relative 'datacaster/checker'
12
14
  require_relative 'datacaster/comparator'
13
- require_relative 'datacaster/array_schema'
14
- require_relative 'datacaster/hash_schema'
15
15
  require_relative 'datacaster/hash_mapper'
16
+ require_relative 'datacaster/hash_schema'
17
+ require_relative 'datacaster/message_keys_merger'
16
18
  require_relative 'datacaster/transformer'
17
19
  require_relative 'datacaster/trier'
18
20
 
@@ -22,29 +24,39 @@ require_relative 'datacaster/or_node'
22
24
  require_relative 'datacaster/then_node'
23
25
 
24
26
  module Datacaster
25
- def self.schema(&block)
26
- raise "Expected block" unless block
27
+ extend self
27
28
 
28
- datacaster = RunnerContext.instance.instance_eval(&block)
29
- unless datacaster.is_a?(Base)
30
- raise "Datacaster instance should be returned from a block (e.g. result of 'hash_schema(...)' call)"
31
- end
29
+ def schema(&block)
30
+ build_schema(Terminator::Raising.instance, &block)
31
+ end
32
+
33
+ def choosy_schema(&block)
34
+ build_schema(Terminator::Sweeping.instance, &block)
35
+ end
32
36
 
33
- datacaster & Terminator.instance
37
+ def partial_schema(&block)
38
+ build_schema(nil, &block)
34
39
  end
35
40
 
36
- def self.partial_schema(&block)
41
+ def absent
42
+ Datacaster::Absent.instance
43
+ end
44
+
45
+ private
46
+
47
+ def build_schema(terminator, &block)
37
48
  raise "Expected block" unless block
38
49
 
39
- datacaster = RunnerContext.instance.instance_eval(&block)
50
+ definition_context = DefinitionContext.new
51
+
52
+ datacaster = definition_context.instance_exec(&block)
53
+
40
54
  unless datacaster.is_a?(Base)
41
- raise "Datacaster instance should be returned from a block (e.g. result of 'hash(...)' call)"
55
+ raise "Datacaster instance should be returned from a block (e.g. result of 'hash_schema(...)' call)"
42
56
  end
43
57
 
58
+ datacaster = (datacaster & terminator) if terminator
59
+ datacaster.set_definition_context(definition_context)
44
60
  datacaster
45
61
  end
46
-
47
- def self.absent
48
- Datacaster::Absent.instance
49
- end
50
62
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: datacaster
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.1
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eugene Zolotarev
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-08-14 00:00:00.000000000 Z
11
+ date: 2023-03-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -79,11 +79,12 @@ executables: []
79
79
  extensions: []
80
80
  extra_rdoc_files: []
81
81
  files:
82
+ - ".github/workflows/rspec.yml"
82
83
  - ".gitignore"
84
+ - ".gitlab-ci.yml"
83
85
  - ".rspec"
84
86
  - ".travis.yml"
85
87
  - Gemfile
86
- - Gemfile.lock
87
88
  - LICENSE.txt
88
89
  - README.md
89
90
  - Rakefile
@@ -99,12 +100,14 @@ files:
99
100
  - lib/datacaster/caster.rb
100
101
  - lib/datacaster/checker.rb
101
102
  - lib/datacaster/comparator.rb
103
+ - lib/datacaster/config.rb
104
+ - lib/datacaster/definition_context.rb
102
105
  - lib/datacaster/hash_mapper.rb
103
106
  - lib/datacaster/hash_schema.rb
107
+ - lib/datacaster/message_keys_merger.rb
104
108
  - lib/datacaster/or_node.rb
105
109
  - lib/datacaster/predefined.rb
106
110
  - lib/datacaster/result.rb
107
- - lib/datacaster/runner_context.rb
108
111
  - lib/datacaster/terminator.rb
109
112
  - lib/datacaster/then_node.rb
110
113
  - lib/datacaster/transformer.rb
@@ -131,7 +134,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
131
134
  - !ruby/object:Gem::Version
132
135
  version: '0'
133
136
  requirements: []
134
- rubygems_version: 3.1.2
137
+ rubygems_version: 3.4.1
135
138
  signing_key:
136
139
  specification_version: 4
137
140
  summary: Run-time type checker and transformer for Ruby
data/Gemfile.lock DELETED
@@ -1,59 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- datacaster (0.9.0)
5
- dry-monads (>= 1.3, < 1.4)
6
-
7
- GEM
8
- remote: https://rubygems.org/
9
- specs:
10
- activemodel (6.0.3.2)
11
- activesupport (= 6.0.3.2)
12
- activesupport (6.0.3.2)
13
- concurrent-ruby (~> 1.0, >= 1.0.2)
14
- i18n (>= 0.7, < 2)
15
- minitest (~> 5.1)
16
- tzinfo (~> 1.1)
17
- zeitwerk (~> 2.2, >= 2.2.2)
18
- concurrent-ruby (1.1.6)
19
- diff-lcs (1.3)
20
- dry-core (0.4.9)
21
- concurrent-ruby (~> 1.0)
22
- dry-equalizer (0.3.0)
23
- dry-monads (1.3.5)
24
- concurrent-ruby (~> 1.0)
25
- dry-core (~> 0.4, >= 0.4.4)
26
- dry-equalizer
27
- i18n (1.8.3)
28
- concurrent-ruby (~> 1.0)
29
- minitest (5.14.1)
30
- rake (12.3.3)
31
- rspec (3.9.0)
32
- rspec-core (~> 3.9.0)
33
- rspec-expectations (~> 3.9.0)
34
- rspec-mocks (~> 3.9.0)
35
- rspec-core (3.9.2)
36
- rspec-support (~> 3.9.3)
37
- rspec-expectations (3.9.2)
38
- diff-lcs (>= 1.2.0, < 2.0)
39
- rspec-support (~> 3.9.0)
40
- rspec-mocks (3.9.1)
41
- diff-lcs (>= 1.2.0, < 2.0)
42
- rspec-support (~> 3.9.0)
43
- rspec-support (3.9.3)
44
- thread_safe (0.3.6)
45
- tzinfo (1.2.7)
46
- thread_safe (~> 0.1)
47
- zeitwerk (2.3.0)
48
-
49
- PLATFORMS
50
- ruby
51
-
52
- DEPENDENCIES
53
- activemodel (>= 5.2)
54
- datacaster!
55
- rake (>= 12.0)
56
- rspec (~> 3.0)
57
-
58
- BUNDLED WITH
59
- 2.1.4