datacaster 0.9.1 → 2.0.1
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 +4 -4
- data/.github/workflows/rspec.yml +17 -0
- data/.gitignore +1 -0
- data/.gitlab-ci.yml +8 -0
- data/.rspec +0 -0
- data/.travis.yml +0 -0
- data/Gemfile +0 -0
- data/LICENSE.txt +0 -0
- data/README.md +298 -10
- data/Rakefile +0 -0
- data/bin/setup +0 -0
- data/datacaster.gemspec +0 -0
- data/lib/datacaster/absent.rb +0 -0
- data/lib/datacaster/and_node.rb +1 -2
- data/lib/datacaster/and_with_error_aggregation_node.rb +1 -2
- data/lib/datacaster/array_schema.rb +2 -2
- data/lib/datacaster/base.rb +33 -2
- data/lib/datacaster/caster.rb +1 -1
- data/lib/datacaster/checker.rb +1 -1
- data/lib/datacaster/comparator.rb +1 -1
- data/lib/datacaster/config.rb +19 -0
- data/lib/datacaster/{runner_context.rb → definition_context.rb} +5 -6
- data/lib/datacaster/hash_mapper.rb +30 -14
- data/lib/datacaster/hash_schema.rb +1 -2
- data/lib/datacaster/message_keys_merger.rb +98 -0
- data/lib/datacaster/or_node.rb +1 -2
- data/lib/datacaster/predefined.rb +5 -0
- data/lib/datacaster/result.rb +0 -0
- data/lib/datacaster/terminator.rb +72 -51
- data/lib/datacaster/then_node.rb +2 -3
- data/lib/datacaster/transformer.rb +1 -1
- data/lib/datacaster/trier.rb +1 -1
- data/lib/datacaster/validator.rb +1 -1
- data/lib/datacaster/version.rb +1 -1
- data/lib/datacaster.rb +29 -17
- metadata +8 -5
- data/Gemfile.lock +0 -59
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 2d6493682ec13481f878adb35c36562ac7f38dd9fa761a7447d6f05f2812c98c
         | 
| 4 | 
            +
              data.tar.gz: 32ca64152333af7cf9e9125ddeb01783f06249e2949dfdd139ca6bcb12fe4790
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 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
    
    
    
        data/.gitlab-ci.yml
    ADDED
    
    
    
        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. | 
| 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 | 
            -
             | 
| 107 | 
            -
             | 
| 108 | 
            -
             | 
| 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 | 
            -
            # =>  | 
| 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)  | 
| 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
         | 
    
        data/lib/datacaster/absent.rb
    CHANGED
    
    | 
            File without changes
         | 
    
        data/lib/datacaster/and_node.rb
    CHANGED
    
    
| @@ -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  | 
| 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) | 
| 5 | 
            +
                  @element_caster = shortcut_definition(element_caster)
         | 
| 6 6 | 
             
                end
         | 
| 7 7 |  | 
| 8 | 
            -
                def  | 
| 8 | 
            +
                def cast(object)
         | 
| 9 9 | 
             
                  object = super(object)
         | 
| 10 10 | 
             
                  checked_schema = object.meta[:checked_schema] || []
         | 
| 11 11 |  | 
    
        data/lib/datacaster/base.rb
    CHANGED
    
    | @@ -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 | 
            -
                   | 
| 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:  | 
| 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
         | 
    
        data/lib/datacaster/caster.rb
    CHANGED
    
    
    
        data/lib/datacaster/checker.rb
    CHANGED
    
    
| @@ -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  | 
| 6 | 
            -
                include Singleton
         | 
| 5 | 
            +
              class DefinitionContext
         | 
| 7 6 | 
             
                include Datacaster::Predefined
         | 
| 8 7 | 
             
                include Dry::Monads[:result]
         | 
| 9 8 |  | 
| 10 | 
            -
                 | 
| 9 | 
            +
                attr_accessor :context
         | 
| 11 10 |  | 
| 12 | 
            -
                def m( | 
| 13 | 
            -
                  raise  | 
| 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? ?  | 
| 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  | 
| 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 | 
            -
                     | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
                       | 
| 25 | 
            -
                        " | 
| 26 | 
            -
             | 
| 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 | 
            -
                       | 
| 31 | 
            -
                         | 
| 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 | 
            -
                       | 
| 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  | 
| 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
         | 
    
        data/lib/datacaster/or_node.rb
    CHANGED
    
    
| @@ -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
         | 
    
        data/lib/datacaster/result.rb
    CHANGED
    
    | 
            File without changes
         | 
| @@ -2,75 +2,96 @@ require 'singleton' | |
| 2 2 | 
             
            require 'dry-monads'
         | 
| 3 3 |  | 
| 4 4 | 
             
            module Datacaster
         | 
| 5 | 
            -
              class Terminator | 
| 6 | 
            -
                 | 
| 7 | 
            -
             | 
| 8 | 
            -
             | 
| 9 | 
            -
             | 
| 10 | 
            -
             | 
| 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 | 
            -
             | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 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 | 
            -
             | 
| 27 | 
            +
                  def inspect
         | 
| 28 | 
            +
                    "#<Datacaster::Terminator>"
         | 
| 29 | 
            +
                  end
         | 
| 28 30 |  | 
| 29 | 
            -
             | 
| 30 | 
            -
                  return Datacaster.ValidResult(array) unless checked_schema
         | 
| 31 | 
            +
                  private
         | 
| 31 32 |  | 
| 32 | 
            -
                   | 
| 33 | 
            +
                  def check_array(array, checked_schema)
         | 
| 34 | 
            +
                    return Datacaster.ValidResult(array) unless checked_schema
         | 
| 33 35 |  | 
| 34 | 
            -
             | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 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 | 
            -
             | 
| 42 | 
            -
             | 
| 45 | 
            +
                  def check_hash(hash, checked_schema)
         | 
| 46 | 
            +
                    return Datacaster.ValidResult(hash) unless checked_schema
         | 
| 43 47 |  | 
| 44 | 
            -
             | 
| 45 | 
            -
             | 
| 48 | 
            +
                    errors = {}
         | 
| 49 | 
            +
                    result = {}
         | 
| 46 50 |  | 
| 47 | 
            -
             | 
| 48 | 
            -
             | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            +
                    hash.each do |(k, v)|
         | 
| 52 | 
            +
                      if v == Datacaster.absent
         | 
| 53 | 
            +
                        next
         | 
| 54 | 
            +
                      end
         | 
| 51 55 |  | 
| 52 | 
            -
             | 
| 53 | 
            -
             | 
| 54 | 
            -
             | 
| 55 | 
            -
             | 
| 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 | 
            -
             | 
| 58 | 
            -
                       | 
| 59 | 
            -
             | 
| 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 | 
            -
                     | 
| 63 | 
            -
             | 
| 64 | 
            -
                      result[k] = nested_value.value
         | 
| 74 | 
            +
                    if errors.empty?
         | 
| 75 | 
            +
                      Datacaster.ValidResult(result)
         | 
| 65 76 | 
             
                    else
         | 
| 66 | 
            -
                       | 
| 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 | 
            -
             | 
| 71 | 
            -
             | 
| 72 | 
            -
             | 
| 73 | 
            -
             | 
| 90 | 
            +
                class Sweeping < Base
         | 
| 91 | 
            +
                  include TerminatorBase
         | 
| 92 | 
            +
              
         | 
| 93 | 
            +
                  def inspect
         | 
| 94 | 
            +
                    "#<Datacaster::Terminator::Sweeping>"
         | 
| 74 95 | 
             
                  end
         | 
| 75 96 | 
             
                end
         | 
| 76 97 | 
             
              end
         | 
    
        data/lib/datacaster/then_node.rb
    CHANGED
    
    | @@ -6,19 +6,18 @@ module Datacaster | |
| 6 6 | 
             
                end
         | 
| 7 7 |  | 
| 8 8 | 
             
                def else(else_caster)
         | 
| 9 | 
            -
                  raise ArgumentError.new( | 
| 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  | 
| 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?
         | 
    
        data/lib/datacaster/trier.rb
    CHANGED
    
    
    
        data/lib/datacaster/validator.rb
    CHANGED
    
    
    
        data/lib/datacaster/version.rb
    CHANGED
    
    
    
        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/ | 
| 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 | 
            -
               | 
| 26 | 
            -
                raise "Expected block" unless block
         | 
| 27 | 
            +
              extend self
         | 
| 27 28 |  | 
| 28 | 
            -
             | 
| 29 | 
            -
                 | 
| 30 | 
            -
             | 
| 31 | 
            -
             | 
| 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 | 
            -
             | 
| 37 | 
            +
              def partial_schema(&block)
         | 
| 38 | 
            +
                build_schema(nil, &block)
         | 
| 34 39 | 
             
              end
         | 
| 35 40 |  | 
| 36 | 
            -
              def  | 
| 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 | 
            -
                 | 
| 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 ' | 
| 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. | 
| 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:  | 
| 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 | 
| 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
         |