blood_contracts-core 0.3.5 → 0.4.0
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/.rubocop.yml +31 -0
- data/.travis.yml +16 -4
- data/CHANGELOG.md +14 -0
- data/README.md +363 -5
- data/Rakefile +1 -1
- data/blood_contracts-core.gemspec +18 -25
- data/examples/json_response.rb +33 -41
- data/examples/tariff_contract.rb +35 -32
- data/examples/tuple.rb +11 -12
- data/lib/blood_contracts/core/anything.rb +23 -0
- data/lib/blood_contracts/core/contract.rb +37 -23
- data/lib/blood_contracts/core/contract_failure.rb +50 -0
- data/lib/blood_contracts/core/pipe.rb +143 -77
- data/lib/blood_contracts/core/refined.rb +148 -125
- data/lib/blood_contracts/core/sum.rb +81 -49
- data/lib/blood_contracts/core/tuple.rb +151 -66
- data/lib/blood_contracts/core/tuple_contract_failure.rb +31 -0
- data/lib/blood_contracts/core.rb +16 -10
- data/spec/blood_contracts/core_spec.rb +314 -0
- data/spec/spec_helper.rb +26 -0
- metadata +36 -10
- data/lib/blood_contracts/core/version.rb +0 -5
| @@ -1,145 +1,168 @@ | |
| 1 | 
            -
            module BloodContracts
         | 
| 2 | 
            -
               | 
| 3 | 
            -
             | 
| 4 | 
            -
             | 
| 5 | 
            -
             | 
| 6 | 
            -
             | 
| 7 | 
            -
             | 
| 8 | 
            -
             | 
| 9 | 
            -
             | 
| 10 | 
            -
             | 
| 11 | 
            -
             | 
| 12 | 
            -
             | 
| 13 | 
            -
             | 
| 14 | 
            -
             | 
| 15 | 
            -
             | 
| 16 | 
            -
             | 
| 17 | 
            -
             | 
| 18 | 
            -
             | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 22 | 
            -
             | 
| 23 | 
            -
                     | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 31 | 
            -
             | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 35 | 
            -
                     | 
| 36 | 
            -
             | 
| 37 | 
            -
                     | 
| 38 | 
            -
                     | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
                   | 
| 44 | 
            -
             | 
| 45 | 
            -
             | 
| 46 | 
            -
                  def initialize(value, context: Hash.new { |h,k| h[k] = Hash.new }, **)
         | 
| 47 | 
            -
                    @errors = []
         | 
| 48 | 
            -
                    @context = context
         | 
| 49 | 
            -
                    @value = value
         | 
| 50 | 
            -
                  end
         | 
| 51 | 
            -
             | 
| 52 | 
            -
                  def match
         | 
| 53 | 
            -
                    return @match if defined? @match
         | 
| 54 | 
            -
                    return @match = (yield || self) if block_given?
         | 
| 55 | 
            -
                    return @match = (_match || self) if respond_to?(:_match)
         | 
| 56 | 
            -
                    self
         | 
| 57 | 
            -
                  end
         | 
| 58 | 
            -
                  alias :call :match
         | 
| 59 | 
            -
             | 
| 60 | 
            -
                  def valid?
         | 
| 61 | 
            -
                    match.errors.empty?
         | 
| 62 | 
            -
                  end
         | 
| 63 | 
            -
                  def invalid?; !valid?; end
         | 
| 64 | 
            -
             | 
| 65 | 
            -
                  def unpack
         | 
| 66 | 
            -
                    return @unpack if defined? @unpack
         | 
| 67 | 
            -
                    raise "This is not what you're looking for" if match.invalid?
         | 
| 68 | 
            -
                    return yield(match) if block_given?
         | 
| 69 | 
            -
                    return @unpack = _unpack(match) if respond_to?(:_unpack)
         | 
| 70 | 
            -
             | 
| 71 | 
            -
                    unpack_refined @value
         | 
| 1 | 
            +
            module BloodContracts::Core
         | 
| 2 | 
            +
              # Base class for refinement type validations
         | 
| 3 | 
            +
              class Refined
         | 
| 4 | 
            +
                class << self
         | 
| 5 | 
            +
                  # Compose types in a Sum check
         | 
| 6 | 
            +
                  # Sum passes data from type to type in parallel, only one type
         | 
| 7 | 
            +
                  # have to match
         | 
| 8 | 
            +
                  #
         | 
| 9 | 
            +
                  # @return [BC::Sum]
         | 
| 10 | 
            +
                  #
         | 
| 11 | 
            +
                  def or_a(other_type)
         | 
| 12 | 
            +
                    BC::Sum.new(self, other_type)
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
                  alias or_an or_a
         | 
| 15 | 
            +
                  alias | or_a
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  # Compose types in a Pipe check
         | 
| 18 | 
            +
                  # Pipe passes data from type to type sequentially
         | 
| 19 | 
            +
                  #
         | 
| 20 | 
            +
                  # @return [BC::Pipe]
         | 
| 21 | 
            +
                  #
         | 
| 22 | 
            +
                  def and_then(other_type)
         | 
| 23 | 
            +
                    BC::Pipe.new(self, other_type)
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
                  alias > and_then
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  # Validate data over refinement type conditions
         | 
| 28 | 
            +
                  # Result is ALWAYS a Refined, but in cases when validation failed,
         | 
| 29 | 
            +
                  # we return ContractFailure ancestor  or ContractFailure itself
         | 
| 30 | 
            +
                  # (which is Refined anyway)
         | 
| 31 | 
            +
                  #
         | 
| 32 | 
            +
                  # @return [Refined]
         | 
| 33 | 
            +
                  #
         | 
| 34 | 
            +
                  def match(*args, **kwargs, &block)
         | 
| 35 | 
            +
                    instance = new(*args, **kwargs)
         | 
| 36 | 
            +
                    match = instance.match(&block) || instance
         | 
| 37 | 
            +
                    instance.instance_variable_set(:@match, match)
         | 
| 38 | 
            +
                    match
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
                  alias call match
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  # Override of case equality operator, to handle Tuple correctly
         | 
| 43 | 
            +
                  def ===(object)
         | 
| 44 | 
            +
                    return object.to_ary.any?(self) if object.is_a?(Tuple)
         | 
| 45 | 
            +
                    super
         | 
| 72 46 | 
             
                  end
         | 
| 73 47 |  | 
| 74 | 
            -
                   | 
| 75 | 
            -
             | 
| 76 | 
            -
             | 
| 77 | 
            -
             | 
| 78 | 
            -
             | 
| 79 | 
            -
             | 
| 80 | 
            -
                   | 
| 81 | 
            -
             | 
| 82 | 
            -
                  protected
         | 
| 83 | 
            -
             | 
| 84 | 
            -
                  def refined?(object)
         | 
| 85 | 
            -
                    object.class < BloodContracts::Core::Refined
         | 
| 48 | 
            +
                  # Accessor to define alternative to ContractFailure for #failure
         | 
| 49 | 
            +
                  # method to use
         | 
| 50 | 
            +
                  #
         | 
| 51 | 
            +
                  # @return [ContractFailure]
         | 
| 52 | 
            +
                  #
         | 
| 53 | 
            +
                  attr_accessor :failure_klass
         | 
| 54 | 
            +
                  def inherited(new_klass)
         | 
| 55 | 
            +
                    new_klass.failure_klass ||= ContractFailure
         | 
| 86 56 | 
             
                  end
         | 
| 57 | 
            +
                end
         | 
| 87 58 |  | 
| 88 | 
            -
             | 
| 89 | 
            -
             | 
| 90 | 
            -
             | 
| 91 | 
            -
             | 
| 59 | 
            +
                # Matching context, contains extra debugging and output data
         | 
| 60 | 
            +
                #
         | 
| 61 | 
            +
                # @return [Hash<Symbol, Object>]
         | 
| 62 | 
            +
                #
         | 
| 63 | 
            +
                attr_accessor :context
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                # List of errors per type
         | 
| 66 | 
            +
                #
         | 
| 67 | 
            +
                # @return [Array<Hash<Refined, String>>]
         | 
| 68 | 
            +
                #
         | 
| 69 | 
            +
                attr_reader :errors
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                # Refinement type constructor
         | 
| 72 | 
            +
                #
         | 
| 73 | 
            +
                # @param [Object] value that Refined holds and should match
         | 
| 74 | 
            +
                # @option [Hash<Symbol, Object>] context to share between types
         | 
| 75 | 
            +
                #
         | 
| 76 | 
            +
                def initialize(value, context: Hash.new { |h, k| h[k] = {} }, **)
         | 
| 77 | 
            +
                  @errors = []
         | 
| 78 | 
            +
                  @context = context
         | 
| 79 | 
            +
                  @value = value
         | 
| 80 | 
            +
                end
         | 
| 92 81 |  | 
| 93 | 
            -
             | 
| 94 | 
            -
             | 
| 95 | 
            -
             | 
| 82 | 
            +
                # The type which is the result of data matching process
         | 
| 83 | 
            +
                #
         | 
| 84 | 
            +
                # @return [BC::Refined]
         | 
| 85 | 
            +
                #
         | 
| 86 | 
            +
                protected def match
         | 
| 87 | 
            +
                  fail NotImplementedError
         | 
| 88 | 
            +
                end
         | 
| 89 | 
            +
                alias call match
         | 
| 96 90 |  | 
| 97 | 
            -
             | 
| 98 | 
            -
             | 
| 99 | 
            -
                   | 
| 91 | 
            +
                # Transform the value before unpacking
         | 
| 92 | 
            +
                protected def mapped
         | 
| 93 | 
            +
                  value
         | 
| 94 | 
            +
                end
         | 
| 100 95 |  | 
| 101 | 
            -
             | 
| 102 | 
            -
             | 
| 103 | 
            -
             | 
| 96 | 
            +
                # Checks whether the data matches the expectations or not
         | 
| 97 | 
            +
                #
         | 
| 98 | 
            +
                # @return [Boolean]
         | 
| 99 | 
            +
                #
         | 
| 100 | 
            +
                def valid?
         | 
| 101 | 
            +
                  @match.errors.empty?
         | 
| 104 102 | 
             
                end
         | 
| 105 103 |  | 
| 106 | 
            -
                 | 
| 107 | 
            -
             | 
| 108 | 
            -
             | 
| 109 | 
            -
             | 
| 110 | 
            -
             | 
| 111 | 
            -
             | 
| 104 | 
            +
                # Checks whether the data matches the expectations or not
         | 
| 105 | 
            +
                # (just negation of #valid?)
         | 
| 106 | 
            +
                #
         | 
| 107 | 
            +
                # @return [Boolean]
         | 
| 108 | 
            +
                #
         | 
| 109 | 
            +
                def invalid?
         | 
| 110 | 
            +
                  !valid?
         | 
| 111 | 
            +
                end
         | 
| 112 112 |  | 
| 113 | 
            -
             | 
| 114 | 
            -
             | 
| 115 | 
            -
             | 
| 113 | 
            +
                # Unpack the original value from the refinement type
         | 
| 114 | 
            +
                #
         | 
| 115 | 
            +
                # @return [Object]
         | 
| 116 | 
            +
                #
         | 
| 117 | 
            +
                def unpack
         | 
| 118 | 
            +
                  @unpack ||= mapped
         | 
| 119 | 
            +
                end
         | 
| 116 120 |  | 
| 117 | 
            -
             | 
| 118 | 
            -
             | 
| 119 | 
            -
             | 
| 120 | 
            -
             | 
| 121 | 
            +
                protected
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                # Helper to build a ContractFailure with shared context
         | 
| 124 | 
            +
                #
         | 
| 125 | 
            +
                # @return [ContractFailure]
         | 
| 126 | 
            +
                #
         | 
| 127 | 
            +
                def failure(error = nil, errors: @errors, **kwargs)
         | 
| 128 | 
            +
                  error ||= kwargs unless kwargs.empty?
         | 
| 129 | 
            +
                  errors << error if error
         | 
| 130 | 
            +
                  self.class.failure_klass.new(
         | 
| 131 | 
            +
                    { self.class => errors }, context: @context
         | 
| 132 | 
            +
                  )
         | 
| 133 | 
            +
                end
         | 
| 121 134 |  | 
| 122 | 
            -
             | 
| 123 | 
            -
             | 
| 124 | 
            -
             | 
| 135 | 
            +
                # Helper to turn value into raw data
         | 
| 136 | 
            +
                #
         | 
| 137 | 
            +
                # @return [Object]
         | 
| 138 | 
            +
                def value
         | 
| 139 | 
            +
                  unpack_refined(@value)
         | 
| 140 | 
            +
                end
         | 
| 125 141 |  | 
| 126 | 
            -
             | 
| 127 | 
            -
             | 
| 128 | 
            -
                  end
         | 
| 142 | 
            +
                def refined?(object)
         | 
| 143 | 
            +
                  object.class < BloodContracts::Core::Refined
         | 
| 129 144 | 
             
                end
         | 
| 130 145 |  | 
| 131 | 
            -
                 | 
| 132 | 
            -
             | 
| 133 | 
            -
             | 
| 134 | 
            -
                   | 
| 146 | 
            +
                # FIXME: do we need it?
         | 
| 147 | 
            +
                def share_context_with(match)
         | 
| 148 | 
            +
                  match.context = @context.merge!(match.context)
         | 
| 149 | 
            +
                  yield(match.context)
         | 
| 150 | 
            +
                end
         | 
| 135 151 |  | 
| 136 | 
            -
             | 
| 137 | 
            -
             | 
| 138 | 
            -
             | 
| 152 | 
            +
                # Turn data into refinement type if it is not already
         | 
| 153 | 
            +
                #
         | 
| 154 | 
            +
                # @return [Object]
         | 
| 155 | 
            +
                #
         | 
| 156 | 
            +
                def refine_value(value)
         | 
| 157 | 
            +
                  refined?(value) ? value.match : Anything.new(value)
         | 
| 158 | 
            +
                end
         | 
| 139 159 |  | 
| 140 | 
            -
             | 
| 141 | 
            -
             | 
| 142 | 
            -
             | 
| 160 | 
            +
                # Turn value into raw data if it is refined
         | 
| 161 | 
            +
                #
         | 
| 162 | 
            +
                # @return [Object]
         | 
| 163 | 
            +
                #
         | 
| 164 | 
            +
                def unpack_refined(value)
         | 
| 165 | 
            +
                  refined?(value) ? value.unpack : value
         | 
| 143 166 | 
             
                end
         | 
| 144 167 | 
             
              end
         | 
| 145 168 | 
             
            end
         | 
| @@ -1,67 +1,99 @@ | |
| 1 | 
            -
            require  | 
| 1 | 
            +
            require "set"
         | 
| 2 2 |  | 
| 3 | 
            -
            module BloodContracts
         | 
| 4 | 
            -
               | 
| 5 | 
            -
             | 
| 6 | 
            -
             | 
| 7 | 
            -
             | 
| 3 | 
            +
            module BloodContracts::Core
         | 
| 4 | 
            +
              # Meta refinement type, represents sum of several refinement types
         | 
| 5 | 
            +
              class Sum < Refined
         | 
| 6 | 
            +
                class << self
         | 
| 7 | 
            +
                  # Represents list of types in the sum
         | 
| 8 | 
            +
                  #
         | 
| 9 | 
            +
                  # @return [Array<Refined>]
         | 
| 10 | 
            +
                  #
         | 
| 11 | 
            +
                  attr_reader :sum_of
         | 
| 8 12 |  | 
| 9 | 
            -
             | 
| 10 | 
            -
             | 
| 13 | 
            +
                  # Metaprogramming around constructor
         | 
| 14 | 
            +
                  # Turns input into Sum meta-class
         | 
| 15 | 
            +
                  #
         | 
| 16 | 
            +
                  # @param (see #initialze)
         | 
| 17 | 
            +
                  #
         | 
| 18 | 
            +
                  # rubocop:disable Style/SingleLineMethods
         | 
| 19 | 
            +
                  def new(*args)
         | 
| 20 | 
            +
                    return super(*args) if @finalized
         | 
| 11 21 |  | 
| 12 | 
            -
             | 
| 13 | 
            -
             | 
| 14 | 
            -
                      sum = Class.new(Sum) { def inspect; super; end }
         | 
| 15 | 
            -
                      sum.instance_variable_set(:@sum_of, ::Set.new(new_sum.compact))
         | 
| 16 | 
            -
                      sum.instance_variable_set(:@finalized, true)
         | 
| 17 | 
            -
                      sum
         | 
| 22 | 
            +
                    new_sum = args.reduce([]) do |acc, type|
         | 
| 23 | 
            +
                      type.respond_to?(:sum_of) ? acc + type.sum_of.to_a : acc << type
         | 
| 18 24 | 
             
                    end
         | 
| 19 25 |  | 
| 20 | 
            -
                     | 
| 21 | 
            -
             | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
                        new_sum += other_type.sum_of.to_a
         | 
| 25 | 
            -
                      else
         | 
| 26 | 
            -
                        new_sum << other_type
         | 
| 27 | 
            -
                      end
         | 
| 28 | 
            -
                      sum.instance_variable_set(:@sum_of, ::Set.new(new_sum.compact))
         | 
| 29 | 
            -
                      sum.instance_variable_set(:@finalized, true)
         | 
| 30 | 
            -
                      sum
         | 
| 31 | 
            -
                    end
         | 
| 32 | 
            -
                    alias :or_an :or_a
         | 
| 33 | 
            -
                    alias :| :or_a
         | 
| 26 | 
            +
                    sum = Class.new(Sum) { def inspect; super; end }
         | 
| 27 | 
            +
                    finalize!(sum, new_sum)
         | 
| 28 | 
            +
                    sum
         | 
| 29 | 
            +
                  end
         | 
| 34 30 |  | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 31 | 
            +
                  # Compose types in a Sum check
         | 
| 32 | 
            +
                  # Sum passes data from type to type in parallel, only one type
         | 
| 33 | 
            +
                  # have to match
         | 
| 34 | 
            +
                  #
         | 
| 35 | 
            +
                  # @return [BC::Sum]
         | 
| 36 | 
            +
                  #
         | 
| 37 | 
            +
                  def or_a(other_type)
         | 
| 38 | 
            +
                    sum = Class.new(Sum) { def inspect; super; end }
         | 
| 39 | 
            +
                    new_sum = sum_of.to_a
         | 
| 40 | 
            +
                    if other_type.respond_to?(:sum_of)
         | 
| 41 | 
            +
                      new_sum += other_type.sum_of.to_a
         | 
| 42 | 
            +
                    else
         | 
| 43 | 
            +
                      new_sum << other_type
         | 
| 38 44 | 
             
                    end
         | 
| 45 | 
            +
                    finalize!(sum, new_sum)
         | 
| 46 | 
            +
                    sum
         | 
| 39 47 | 
             
                  end
         | 
| 48 | 
            +
                  # rubocop:enable Style/SingleLineMethods
         | 
| 49 | 
            +
                  alias or_an or_a
         | 
| 50 | 
            +
                  alias | or_a
         | 
| 40 51 |  | 
| 41 | 
            -
                   | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 45 | 
            -
             | 
| 52 | 
            +
                  # @private
         | 
| 53 | 
            +
                  private def finalize!(new_class, new_sum)
         | 
| 54 | 
            +
                    new_class.instance_variable_set(:@sum_of, ::Set.new(new_sum.compact))
         | 
| 55 | 
            +
                    new_class.instance_variable_set(:@finalized, true)
         | 
| 56 | 
            +
                  end
         | 
| 46 57 |  | 
| 47 | 
            -
             | 
| 48 | 
            -
             | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
             | 
| 52 | 
            -
             | 
| 53 | 
            -
             | 
| 54 | 
            -
                    end
         | 
| 58 | 
            +
                  # Returns text representation of Sum meta-class
         | 
| 59 | 
            +
                  #
         | 
| 60 | 
            +
                  # @return [String]
         | 
| 61 | 
            +
                  #
         | 
| 62 | 
            +
                  def inspect
         | 
| 63 | 
            +
                    return super if name
         | 
| 64 | 
            +
                    "Sum(#{sum_of.map(&:inspect).join(',')})"
         | 
| 55 65 | 
             
                  end
         | 
| 66 | 
            +
                end
         | 
| 56 67 |  | 
| 57 | 
            -
             | 
| 58 | 
            -
             | 
| 68 | 
            +
                # The type which is the result of data matching process
         | 
| 69 | 
            +
                # For Tuple it verifies that all the attributes data are valid types
         | 
| 70 | 
            +
                #
         | 
| 71 | 
            +
                # @return [BC::Refined]
         | 
| 72 | 
            +
                #
         | 
| 73 | 
            +
                def match
         | 
| 74 | 
            +
                  or_matches = self.class.sum_of.map do |type|
         | 
| 75 | 
            +
                    type.match(@value, context: @context)
         | 
| 59 76 | 
             
                  end
         | 
| 60 77 |  | 
| 61 | 
            -
                   | 
| 62 | 
            -
                     | 
| 78 | 
            +
                  if (match = or_matches.find(&:valid?))
         | 
| 79 | 
            +
                    match
         | 
| 80 | 
            +
                  else
         | 
| 81 | 
            +
                    failure(:no_matches)
         | 
| 63 82 | 
             
                  end
         | 
| 64 83 | 
             
                end
         | 
| 65 | 
            -
             | 
| 84 | 
            +
             | 
| 85 | 
            +
                # List of errors per type during the matching
         | 
| 86 | 
            +
                #
         | 
| 87 | 
            +
                # @return [Array<Hash<Refined, String>>]
         | 
| 88 | 
            +
                #
         | 
| 89 | 
            +
                def errors
         | 
| 90 | 
            +
                  @context[:errors]
         | 
| 91 | 
            +
                end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                # @private
         | 
| 94 | 
            +
                private def inspect
         | 
| 95 | 
            +
                  "#<sum #{self.class.name} is #{self.class.sum_of.to_a.join(' or ')}"\
         | 
| 96 | 
            +
                  " (value=#{@value})>"
         | 
| 97 | 
            +
                end
         | 
| 66 98 | 
             
              end
         | 
| 67 99 | 
             
            end
         | 
| @@ -1,82 +1,167 @@ | |
| 1 | 
            -
            module BloodContracts
         | 
| 2 | 
            -
               | 
| 3 | 
            -
             | 
| 4 | 
            -
             | 
| 5 | 
            -
             | 
| 6 | 
            -
             | 
| 7 | 
            -
             | 
| 8 | 
            -
             | 
| 9 | 
            -
                      if args.last.is_a?(Hash)
         | 
| 10 | 
            -
                        names = args.pop.delete(:names)
         | 
| 11 | 
            -
                      end
         | 
| 12 | 
            -
             | 
| 13 | 
            -
                      raise ArgumentError unless args.all?(Class)
         | 
| 14 | 
            -
                      tuple = Class.new(Tuple) { def inspect; super; end }
         | 
| 15 | 
            -
                      tuple.instance_variable_set(:@attributes, args)
         | 
| 16 | 
            -
                      tuple.instance_variable_set(:@names, names.to_a)
         | 
| 17 | 
            -
                      tuple.instance_variable_set(:@finalized, true)
         | 
| 18 | 
            -
                      tuple
         | 
| 19 | 
            -
                    end
         | 
| 1 | 
            +
            module BloodContracts::Core
         | 
| 2 | 
            +
              # Meta refinement type, represents product of several refinement types
         | 
| 3 | 
            +
              class Tuple < Refined
         | 
| 4 | 
            +
                class << self
         | 
| 5 | 
            +
                  # List of types in the Tuple
         | 
| 6 | 
            +
                  #
         | 
| 7 | 
            +
                  # @return [Array<Refined>]
         | 
| 8 | 
            +
                  attr_reader :attributes
         | 
| 20 9 |  | 
| 21 | 
            -
             | 
| 10 | 
            +
                  # Names of attributes
         | 
| 11 | 
            +
                  #
         | 
| 12 | 
            +
                  # @return [Array<Symbol>]
         | 
| 13 | 
            +
                  #
         | 
| 14 | 
            +
                  attr_reader :names
         | 
| 22 15 |  | 
| 23 | 
            -
             | 
| 24 | 
            -
                   | 
| 16 | 
            +
                  # Metaprogramming around constructor
         | 
| 17 | 
            +
                  # Turns input into Tuple meta-class
         | 
| 18 | 
            +
                  #
         | 
| 19 | 
            +
                  # @param (see #initialze)
         | 
| 20 | 
            +
                  #
         | 
| 21 | 
            +
                  # rubocop:disable Style/SingleLineMethods
         | 
| 22 | 
            +
                  def new(*args, **kwargs, &block)
         | 
| 23 | 
            +
                    return super(*args, **kwargs) if @finalized
         | 
| 24 | 
            +
                    names = args.pop.delete(:names) if args.last.is_a?(Hash)
         | 
| 25 25 |  | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 28 | 
            -
                     | 
| 29 | 
            -
                     | 
| 30 | 
            -
                     | 
| 31 | 
            -
                     | 
| 26 | 
            +
                    raise ArgumentError unless args.all? { |type| type < Refined }
         | 
| 27 | 
            +
                    tuple = Class.new(Tuple) { def inspect; super; end }
         | 
| 28 | 
            +
                    tuple.instance_variable_set(:@attributes, args)
         | 
| 29 | 
            +
                    tuple.instance_variable_set(:@names, names.to_a)
         | 
| 30 | 
            +
                    tuple.instance_variable_set(:@finalized, true)
         | 
| 31 | 
            +
                    tuple.class_eval(&block) if block_given?
         | 
| 32 | 
            +
                    tuple
         | 
| 32 33 | 
             
                  end
         | 
| 34 | 
            +
                  # rubocop:enable Style/SingleLineMethods
         | 
| 33 35 |  | 
| 34 | 
            -
                   | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 40 | 
            -
                       | 
| 36 | 
            +
                  # Helper which registers attribute in the Tuple, also defines a reader
         | 
| 37 | 
            +
                  def attribute(name, type)
         | 
| 38 | 
            +
                    raise ArgumentError unless type < Refined
         | 
| 39 | 
            +
                    @attributes << type
         | 
| 40 | 
            +
                    @names << name
         | 
| 41 | 
            +
                    define_method(name) do
         | 
| 42 | 
            +
                      match.context.dig(:attributes, name)
         | 
| 41 43 | 
             
                    end
         | 
| 42 44 | 
             
                  end
         | 
| 43 45 |  | 
| 44 | 
            -
                   | 
| 45 | 
            -
             | 
| 46 | 
            -
                   | 
| 47 | 
            -
                   | 
| 48 | 
            -
                   | 
| 49 | 
            -
             | 
| 50 | 
            -
                  def  | 
| 51 | 
            -
                     | 
| 52 | 
            -
             | 
| 53 | 
            -
             | 
| 54 | 
            -
             | 
| 55 | 
            -
             | 
| 56 | 
            -
                    ]
         | 
| 57 | 
            -
                  end
         | 
| 58 | 
            -
                  alias :to_hash :unpack_h
         | 
| 59 | 
            -
                  alias :to_h :unpack_h
         | 
| 60 | 
            -
             | 
| 61 | 
            -
                  private def values_by_names
         | 
| 62 | 
            -
                    if self.class.names.empty?
         | 
| 63 | 
            -
                      self.values
         | 
| 64 | 
            -
                    else
         | 
| 65 | 
            -
                      self.class.names.zip(values).map { |k, v| [k, v].join('=') }
         | 
| 66 | 
            -
                    end
         | 
| 46 | 
            +
                  # Accessor to define alternative to ContractFailure for #failure
         | 
| 47 | 
            +
                  # method to use
         | 
| 48 | 
            +
                  #
         | 
| 49 | 
            +
                  # @return [ContractFailure]
         | 
| 50 | 
            +
                  #
         | 
| 51 | 
            +
                  attr_accessor :failure_klass
         | 
| 52 | 
            +
                  def inherited(new_klass)
         | 
| 53 | 
            +
                    new_klass.instance_variable_set(:@attributes, [])
         | 
| 54 | 
            +
                    new_klass.instance_variable_set(:@names, [])
         | 
| 55 | 
            +
                    new_klass.instance_variable_set(:@finalized, true)
         | 
| 56 | 
            +
                    new_klass.failure_klass ||= TupleContractFailure
         | 
| 57 | 
            +
                    super
         | 
| 67 58 | 
             
                  end
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
             | 
| 62 | 
            +
                # List of values in Tuple
         | 
| 63 | 
            +
                #
         | 
| 64 | 
            +
                # @return [Array<Object>]
         | 
| 65 | 
            +
                #
         | 
| 66 | 
            +
                attr_reader :values
         | 
| 68 67 |  | 
| 69 | 
            -
             | 
| 70 | 
            -
             | 
| 71 | 
            -
             | 
| 72 | 
            -
             | 
| 73 | 
            -
             | 
| 74 | 
            -
             | 
| 68 | 
            +
                # Tuple constructor, builds Tuple from list of data values
         | 
| 69 | 
            +
                #
         | 
| 70 | 
            +
                # @param [Array<Object>] *values that we'll keep inside the Tuple
         | 
| 71 | 
            +
                # @option [Hash<Symbol, Object>] context to share between types
         | 
| 72 | 
            +
                #
         | 
| 73 | 
            +
                def initialize(*values, context: Hash.new { |h, k| h[k] = {} }, **)
         | 
| 74 | 
            +
                  @context = context
         | 
| 75 | 
            +
                  @context[:attributes] ||= {}
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                  additional_context = values.last if values.last.is_a?(Hash)
         | 
| 78 | 
            +
                  additional_context ||= {}
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                  @values = parse_values_from_context(context.merge(additional_context))
         | 
| 81 | 
            +
                  @values ||= values
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                  @errors = []
         | 
| 84 | 
            +
                end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                # The type which is the result of data matching process
         | 
| 87 | 
            +
                # For Tuple it verifies that all the attributes data are valid types
         | 
| 88 | 
            +
                #
         | 
| 89 | 
            +
                # @return [BC::Refined]
         | 
| 90 | 
            +
                #
         | 
| 91 | 
            +
                def match
         | 
| 92 | 
            +
                  @matches = attributes_enumerator.map do |(type, value), index|
         | 
| 93 | 
            +
                    attribute_name = self.class.names[index]
         | 
| 94 | 
            +
                    attributes.store(attribute_name, type.match(value, context: @context))
         | 
| 75 95 | 
             
                  end
         | 
| 96 | 
            +
                  return if @matches.find(&:invalid?).nil?
         | 
| 97 | 
            +
                  failure(:invalid_tuple)
         | 
| 98 | 
            +
                end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                # Turns match into array of unpacked values
         | 
| 101 | 
            +
                #
         | 
| 102 | 
            +
                # @return [Array<Object>]
         | 
| 103 | 
            +
                #
         | 
| 104 | 
            +
                def mapped
         | 
| 105 | 
            +
                  @matches.map(&:unpack)
         | 
| 106 | 
            +
                end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                # (see #mapped)
         | 
| 109 | 
            +
                alias to_ary unpack
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                # (see #mapped)
         | 
| 112 | 
            +
                alias to_a unpack
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                # Unpacked value in form of a hash per attribute
         | 
| 115 | 
            +
                #
         | 
| 116 | 
            +
                # @return [Hash<String, ContractFailure>]
         | 
| 117 | 
            +
                #
         | 
| 118 | 
            +
                def unpack_h
         | 
| 119 | 
            +
                  @unpack_h ||= attributes.transform_values(&:unpack)
         | 
| 120 | 
            +
                end
         | 
| 121 | 
            +
                alias to_hash unpack_h
         | 
| 122 | 
            +
                alias to_h unpack_h
         | 
| 123 | 
            +
                alias unpack_attributes unpack_h
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                # Hash of attributes (name & type pairs)
         | 
| 126 | 
            +
                #
         | 
| 127 | 
            +
                # @return [Hash<String, Refined>]
         | 
| 128 | 
            +
                #
         | 
| 129 | 
            +
                def attributes
         | 
| 130 | 
            +
                  @context[:attributes]
         | 
| 131 | 
            +
                end
         | 
| 76 132 |  | 
| 77 | 
            -
             | 
| 78 | 
            -
             | 
| 133 | 
            +
                # Subset of attributes which are invalid
         | 
| 134 | 
            +
                #
         | 
| 135 | 
            +
                # @return [Hash<String, ContractFailure>]
         | 
| 136 | 
            +
                #
         | 
| 137 | 
            +
                def attribute_errors
         | 
| 138 | 
            +
                  {}
         | 
| 139 | 
            +
                end
         | 
| 140 | 
            +
             | 
| 141 | 
            +
                # @private
         | 
| 142 | 
            +
                private def parse_values_from_context(context)
         | 
| 143 | 
            +
                  return if context.empty?
         | 
| 144 | 
            +
                  return unless (self.class.names - context.keys).empty?
         | 
| 145 | 
            +
                  context.values_at(*self.class.names)
         | 
| 146 | 
            +
                end
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                # @private
         | 
| 149 | 
            +
                private def attributes_enumerator
         | 
| 150 | 
            +
                  self.class.attributes.zip(@values).each.with_index
         | 
| 151 | 
            +
                end
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                # @private
         | 
| 154 | 
            +
                private def values_by_names
         | 
| 155 | 
            +
                  if self.class.names.empty?
         | 
| 156 | 
            +
                    values
         | 
| 157 | 
            +
                  else
         | 
| 158 | 
            +
                    self.class.names.zip(values).map { |k, v| [k, v].join("=") }
         | 
| 79 159 | 
             
                  end
         | 
| 80 160 | 
             
                end
         | 
| 161 | 
            +
             | 
| 162 | 
            +
                # @private
         | 
| 163 | 
            +
                private def inspect
         | 
| 164 | 
            +
                  "#<tuple #{self.class.name} of (#{values_by_names.join(', ')})>"
         | 
| 165 | 
            +
                end
         | 
| 81 166 | 
             
              end
         | 
| 82 167 | 
             
            end
         |