jet-contract 0.1.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 +7 -0
- data/LICENSE.txt +18 -0
- data/README.md +3 -0
- data/lib/jet-contract.rb +3 -0
- data/lib/jet/contract.rb +160 -0
- data/lib/jet/contract/attribute.rb +106 -0
- data/lib/jet/contract/attribute/builder.rb +119 -0
- data/lib/jet/contract/builder.rb +40 -0
- data/lib/jet/contract/check.rb +72 -0
- data/lib/jet/contract/check/set.rb +49 -0
- data/lib/jet/contract/version.rb +14 -0
- metadata +139 -0
    
        checksums.yaml
    ADDED
    
    | @@ -0,0 +1,7 @@ | |
| 1 | 
            +
            ---
         | 
| 2 | 
            +
            SHA256:
         | 
| 3 | 
            +
              metadata.gz: fcbe0e6e51fb3e7a5f0938d993c0b95242e0ab3a0a2f61166e71f675c242066b
         | 
| 4 | 
            +
              data.tar.gz: 802842c9c22218e87e0e57ccc24d7fc00b4f4028ec526a8a74eae6f97e36e81b
         | 
| 5 | 
            +
            SHA512:
         | 
| 6 | 
            +
              metadata.gz: ea07b6d7c5786f3b196a4dd77c812dac7fe2d500b1c86d0a11276c9ec48b49e543d361be7358f2b4ce8a1ee892799c761cf82a2e76cf2579eab74443b4a9f929
         | 
| 7 | 
            +
              data.tar.gz: 765fb5fcb4db348647445aa7d6e3fa282e8b6ac1fbab0d4923a285ab63c21442834d19438c66df88bded01f569d4f002dcfac134ad2c7e9f54e698644774def1
         | 
    
        data/LICENSE.txt
    ADDED
    
    | @@ -0,0 +1,18 @@ | |
| 1 | 
            +
            Copyright (c) 2019 Joshua Hansen
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            Permission is hereby granted, free of charge, to any person obtaining a copy
         | 
| 4 | 
            +
            of this software and associated documentation files (the "Software"), to
         | 
| 5 | 
            +
            deal in the Software without restriction, including without limitation the
         | 
| 6 | 
            +
            rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
         | 
| 7 | 
            +
            sell copies of the Software, and to permit persons to whom the Software is
         | 
| 8 | 
            +
            furnished to do so, subject to the following conditions:
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            The above copyright notice and this permission notice shall be included in
         | 
| 11 | 
            +
            all copies or substantial portions of the Software.
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
         | 
| 14 | 
            +
            IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
         | 
| 15 | 
            +
            FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
         | 
| 16 | 
            +
            THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
         | 
| 17 | 
            +
            IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
         | 
| 18 | 
            +
            CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
         | 
    
        data/README.md
    ADDED
    
    
    
        data/lib/jet-contract.rb
    ADDED
    
    
    
        data/lib/jet/contract.rb
    ADDED
    
    | @@ -0,0 +1,160 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "jet/type"
         | 
| 4 | 
            +
            require "jet/contract/attribute"
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module Jet
         | 
| 7 | 
            +
              class Contract
         | 
| 8 | 
            +
                FLATTEN_ERROR_TYPES = %i[
         | 
| 9 | 
            +
                  contract_validation_failure
         | 
| 10 | 
            +
                  check_each_failure
         | 
| 11 | 
            +
                ].freeze
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                @checks = Contract::Check::BuiltIn
         | 
| 14 | 
            +
                @types = Type::JSON
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                class << self
         | 
| 17 | 
            +
                  attr_reader :checks, :types
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  def build(*args, &blk)
         | 
| 20 | 
            +
                    raise ArgumentError, "no block given" unless block_given?
         | 
| 21 | 
            +
                    Builder.new.tap { |b| b.instance_eval(&blk) }.call(*args)
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  def checks!(checks)
         | 
| 25 | 
            +
                    validate_registry!("checks", checks, Check, :eql)
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  def checks=(checks)
         | 
| 29 | 
            +
                    @checks = checks!(checks)
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  def types!(types)
         | 
| 33 | 
            +
                    case types
         | 
| 34 | 
            +
                    when :http
         | 
| 35 | 
            +
                      Type::HTTP
         | 
| 36 | 
            +
                    when :json
         | 
| 37 | 
            +
                      Type::JSON
         | 
| 38 | 
            +
                    when :strict
         | 
| 39 | 
            +
                      Type::Strict
         | 
| 40 | 
            +
                    else
         | 
| 41 | 
            +
                      validate_registry!("types", types, Type, :string)
         | 
| 42 | 
            +
                    end
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                  def types=(types)
         | 
| 46 | 
            +
                    @types = types!(types)
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  private
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  def validate_registry!(name, registry, type, key)
         | 
| 52 | 
            +
                    return registry if registry.respond_to?(:[]) && registry[key].is_a?(type)
         | 
| 53 | 
            +
                    raise ArgumentError, "`#{name}` must be a registry of #{type}"
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                def initialize(attributes, keys_in: [String, Symbol], keys_out: :to_sym, **)
         | 
| 58 | 
            +
                  @attributes = Jet.type_check_hash!("`attributes`", attributes, Attribute)
         | 
| 59 | 
            +
                                   .transform_keys(&:to_s)
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  @opts = { keys_in: _keys_in(keys_in), keys_out: _keys_out(keys_out) }
         | 
| 62 | 
            +
                end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                def call(input, **)
         | 
| 65 | 
            +
                  results = check_attributes(filter_keys(input.to_h))
         | 
| 66 | 
            +
                  failure(results, input) || success(results)
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                def [](key)
         | 
| 70 | 
            +
                  @attributes[key]
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                def attributes
         | 
| 74 | 
            +
                  @attributes.dup
         | 
| 75 | 
            +
                end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                def opts
         | 
| 78 | 
            +
                  @opts.dup
         | 
| 79 | 
            +
                end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                def rebuild(*args)
         | 
| 82 | 
            +
                  to_builder.(*args)
         | 
| 83 | 
            +
                end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                def to_builder
         | 
| 86 | 
            +
                  Builder.new(@attributes.transform_values(&:to_builder))
         | 
| 87 | 
            +
                end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                def with(*other_contracts, **opts)
         | 
| 90 | 
            +
                  Jet.type_check_each!("`other_contracts`", other_contracts, Contract)
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                  self.class.new(
         | 
| 93 | 
            +
                    other_contracts.each_with_object(attributes) { |c, atts| atts.merge!(c.attributes) },
         | 
| 94 | 
            +
                    **self.opts.merge(opts)
         | 
| 95 | 
            +
                  )
         | 
| 96 | 
            +
                end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                private
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                def _keys_in(classes)
         | 
| 101 | 
            +
                  Array(classes).map do |c|
         | 
| 102 | 
            +
                    next String if c == :string
         | 
| 103 | 
            +
                    next Symbol if c == :symbol
         | 
| 104 | 
            +
                    Jet.type_check!(":keys_in element #{c}", Class, Module)
         | 
| 105 | 
            +
                    c
         | 
| 106 | 
            +
                  end.uniq
         | 
| 107 | 
            +
                end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                def _keys_out(key_type)
         | 
| 110 | 
            +
                  if [Symbol, :symbol, :to_sym].include?(key_type)
         | 
| 111 | 
            +
                    :to_sym
         | 
| 112 | 
            +
                  elsif [String, :string, :to_s].include?(key_type)
         | 
| 113 | 
            +
                    :to_s
         | 
| 114 | 
            +
                  else
         | 
| 115 | 
            +
                    raise ArgumentError, ":keys_out must equal either :symbol or :string"
         | 
| 116 | 
            +
                  end
         | 
| 117 | 
            +
                end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                def check_attributes(input)
         | 
| 120 | 
            +
                  @attributes.each_with_object({}) do |(k, att), h|
         | 
| 121 | 
            +
                    if input.key?(k)
         | 
| 122 | 
            +
                      h[k] = att.(input[k], k.to_sym)
         | 
| 123 | 
            +
                    else
         | 
| 124 | 
            +
                      next if att.optional?
         | 
| 125 | 
            +
                      h[k] = Result.failure(:key_missing_failure, at: k.to_sym)
         | 
| 126 | 
            +
                    end
         | 
| 127 | 
            +
                  end
         | 
| 128 | 
            +
                end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                def failure(results, input)
         | 
| 131 | 
            +
                  return unless results.values.any?(&:failure?)
         | 
| 132 | 
            +
                  Result.failure(
         | 
| 133 | 
            +
                    :contract_validation_failure,
         | 
| 134 | 
            +
                    errors: flatten_errors(results.values),
         | 
| 135 | 
            +
                    input: input
         | 
| 136 | 
            +
                  )
         | 
| 137 | 
            +
                end
         | 
| 138 | 
            +
             | 
| 139 | 
            +
                def filter_keys(input)
         | 
| 140 | 
            +
                  input.select { |k, _| @opts[:keys_in].any? { |t| k.is_a?(t) } }
         | 
| 141 | 
            +
                       .transform_keys(&:to_s)
         | 
| 142 | 
            +
                       .select { |k, _| attributes.keys.include?(k) }
         | 
| 143 | 
            +
                end
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                def flatten_errors(results)
         | 
| 146 | 
            +
                  results.select(&:failure?).each_with_object([]) do |r, errs|
         | 
| 147 | 
            +
                    next errs.concat(flatten_errors(r.errors)) if FLATTEN_ERROR_TYPES.include?(r.output)
         | 
| 148 | 
            +
                    errs << r
         | 
| 149 | 
            +
                  end
         | 
| 150 | 
            +
                end
         | 
| 151 | 
            +
             | 
| 152 | 
            +
                def success(results)
         | 
| 153 | 
            +
                  results
         | 
| 154 | 
            +
                    .each_with_object({}) { |(k, r), h| h[k.send(opts[:keys_out])] = r.output }
         | 
| 155 | 
            +
                    .yield_self { |output| Result.success(output) }
         | 
| 156 | 
            +
                end
         | 
| 157 | 
            +
              end
         | 
| 158 | 
            +
            end
         | 
| 159 | 
            +
             | 
| 160 | 
            +
            require "jet/contract/builder"
         | 
| @@ -0,0 +1,106 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "jet/contract/check"
         | 
| 4 | 
            +
            require "jet/contract/check/set"
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module Jet
         | 
| 7 | 
            +
              class Contract
         | 
| 8 | 
            +
                class Attribute
         | 
| 9 | 
            +
                  attr_reader :checks, :type
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  def self.build(*args, &blk)
         | 
| 12 | 
            +
                    raise ArgumentError, "no block given" unless block_given?
         | 
| 13 | 
            +
                    Builder.new.instance_eval(&blk).call(*args)
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def initialize(type, checks = nil, contract: nil, each: nil, required: true, **)
         | 
| 17 | 
            +
                    @type = Jet.type_check!("`type`", type, Type)
         | 
| 18 | 
            +
                    @checks = Jet.type_check!("`checks`", checks, Check::Set, NilClass)
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                    raise ArgumentError, "cannot set both :contract and :each" if contract && each
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                    @opts = {
         | 
| 23 | 
            +
                      contract: Jet.type_check!(":contract", contract, Contract, NilClass),
         | 
| 24 | 
            +
                      each: Jet.type_check!(":each", each, Attribute, NilClass),
         | 
| 25 | 
            +
                      required: required ? true : false
         | 
| 26 | 
            +
                    }
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  def call(input, at = [])
         | 
| 30 | 
            +
                    coerce(input).yield_self { |r| result_at(Jet.failure?(r) ? r : check(r.output), at) }
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  def check(output)
         | 
| 34 | 
            +
                    return Result.success if output.nil?
         | 
| 35 | 
            +
                    checks&.(output)&.tap { |r| return r if r.failure? }
         | 
| 36 | 
            +
                    check_contract(output) || check_each(output) || Result.success(output)
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  def coerce(input)
         | 
| 40 | 
            +
                    type.(input)
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  def is?
         | 
| 44 | 
            +
                    !maybe?
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                  def maybe?
         | 
| 48 | 
            +
                    type.maybe?
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  def optional?
         | 
| 52 | 
            +
                    !required?
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  def opts
         | 
| 56 | 
            +
                    @opts.dup
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                  def required?
         | 
| 60 | 
            +
                    @opts[:required]
         | 
| 61 | 
            +
                  end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  def to_builder
         | 
| 64 | 
            +
                    Builder.new(
         | 
| 65 | 
            +
                      checks: @checks&.to_builder,
         | 
| 66 | 
            +
                      contract: @opts[:contract]&.to_builder,
         | 
| 67 | 
            +
                      each: @opts[:each]&.to_builder,
         | 
| 68 | 
            +
                      is: is?,
         | 
| 69 | 
            +
                      required: required?,
         | 
| 70 | 
            +
                      type: type.name
         | 
| 71 | 
            +
                    )
         | 
| 72 | 
            +
                  end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                  def to_sym
         | 
| 75 | 
            +
                    name
         | 
| 76 | 
            +
                  end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                  private
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                  def check_contract(output)
         | 
| 81 | 
            +
                    @opts[:contract]&.(output)
         | 
| 82 | 
            +
                  end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                  def check_each(output)
         | 
| 85 | 
            +
                    return unless @opts[:each]
         | 
| 86 | 
            +
                    results = output.map.with_index { |v, i| @opts[:each].(v).with(at: [i]) }
         | 
| 87 | 
            +
                    return Result.success(results.map(&:output)) if results.all?(&:success?)
         | 
| 88 | 
            +
                    Result.failure(
         | 
| 89 | 
            +
                      :check_each_failure,
         | 
| 90 | 
            +
                      errors: results.select(&:failure?),
         | 
| 91 | 
            +
                      input: output
         | 
| 92 | 
            +
                    )
         | 
| 93 | 
            +
                  end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                  def result_at(result, at)
         | 
| 96 | 
            +
                    new_at = Array(at) + Array(result.at)
         | 
| 97 | 
            +
                    result.with(
         | 
| 98 | 
            +
                      at: new_at,
         | 
| 99 | 
            +
                      errors: result.errors.map { |r| result_at(r, new_at) }
         | 
| 100 | 
            +
                    )
         | 
| 101 | 
            +
                  end
         | 
| 102 | 
            +
                end
         | 
| 103 | 
            +
              end
         | 
| 104 | 
            +
            end
         | 
| 105 | 
            +
             | 
| 106 | 
            +
            require "jet/contract/attribute/builder"
         | 
| @@ -0,0 +1,119 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Jet
         | 
| 4 | 
            +
              class Contract
         | 
| 5 | 
            +
                class Attribute
         | 
| 6 | 
            +
                  class Builder
         | 
| 7 | 
            +
                    def initialize(opts = {})
         | 
| 8 | 
            +
                      @opts = { required: true }.merge(opts)
         | 
| 9 | 
            +
                      checks(opts[:checks]) if opts[:checks]
         | 
| 10 | 
            +
                      contract(opts[:contract]) if opts[:contract]
         | 
| 11 | 
            +
                      each(*opts[:each]) if opts[:each]
         | 
| 12 | 
            +
                    end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                    def call(types = nil, checks = nil)
         | 
| 15 | 
            +
                      types = types.nil? ? Contract.types : Contract.types!(types)
         | 
| 16 | 
            +
                      checks = checks.nil? ? Contract.checks : Contract.checks!(checks)
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                      Attribute.new(
         | 
| 19 | 
            +
                        type_with(types),
         | 
| 20 | 
            +
                        check_set_with(checks),
         | 
| 21 | 
            +
                        contract: @opts[:contract]&.(types, checks),
         | 
| 22 | 
            +
                        each: @opts[:each]&.(types, checks),
         | 
| 23 | 
            +
                        required: @opts[:required]
         | 
| 24 | 
            +
                      )
         | 
| 25 | 
            +
                    end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                    def checks(checks)
         | 
| 28 | 
            +
                      @opts[:checks] = checks.each_with_object([]) do |check, a|
         | 
| 29 | 
            +
                        case check
         | 
| 30 | 
            +
                        when Hash
         | 
| 31 | 
            +
                          check.each { |(k, v)| a << [k.to_sym, v] }
         | 
| 32 | 
            +
                        when Array
         | 
| 33 | 
            +
                          a << [check.first.to_sym].concat(check[1..-1])
         | 
| 34 | 
            +
                        else
         | 
| 35 | 
            +
                          a << [check.to_sym]
         | 
| 36 | 
            +
                        end
         | 
| 37 | 
            +
                      end
         | 
| 38 | 
            +
                      self
         | 
| 39 | 
            +
                    end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                    def contract(contract = nil, &blk)
         | 
| 42 | 
            +
                      raise ArgumentError, "cannot provide :contract if :each is set" if @opts[:each]
         | 
| 43 | 
            +
                      auto_type!("contract", :hash)
         | 
| 44 | 
            +
                      raise ArgumentError, "must provide either `contract` or a block" unless
         | 
| 45 | 
            +
                        !contract.nil? ^ block_given?
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                      @opts[:contract] =
         | 
| 48 | 
            +
                        if block_given?
         | 
| 49 | 
            +
                          Contract::Builder.new.tap { |b| b.instance_eval(&blk) }
         | 
| 50 | 
            +
                        else
         | 
| 51 | 
            +
                          Jet.type_check!(":contract", contract, Contract, Contract::Builder)
         | 
| 52 | 
            +
                        end
         | 
| 53 | 
            +
                      self
         | 
| 54 | 
            +
                    end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                    def each(*args, &blk)
         | 
| 57 | 
            +
                      raise ArgumentError, "cannot provide :each if :contract is set" if @opts[:contract]
         | 
| 58 | 
            +
                      auto_type!("each", :array)
         | 
| 59 | 
            +
                      raise ArgumentError, "must provide either `args` or a block" unless
         | 
| 60 | 
            +
                        args.any? ^ block_given?
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                      @opts[:each] =
         | 
| 63 | 
            +
                        if block_given?
         | 
| 64 | 
            +
                          self.class.new.instance_eval(&blk)
         | 
| 65 | 
            +
                        elsif args.size == 1 && args.first.is_a?(self.class)
         | 
| 66 | 
            +
                          args.first
         | 
| 67 | 
            +
                        else
         | 
| 68 | 
            +
                          self.class.new.is(*args)
         | 
| 69 | 
            +
                        end
         | 
| 70 | 
            +
                      self
         | 
| 71 | 
            +
                    end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                    def is(type, *checks)
         | 
| 74 | 
            +
                      @opts[:maybe] = false
         | 
| 75 | 
            +
                      type(type, *checks)
         | 
| 76 | 
            +
                      self
         | 
| 77 | 
            +
                    end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                    def maybe(type, *checks)
         | 
| 80 | 
            +
                      @opts[:maybe] = true
         | 
| 81 | 
            +
                      type(type, *checks)
         | 
| 82 | 
            +
                      self
         | 
| 83 | 
            +
                    end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                    def type(type, *checks)
         | 
| 86 | 
            +
                      @opts[:type] = Jet.type_check!("`type`", type, Symbol, Type).to_sym
         | 
| 87 | 
            +
                      checks(checks)
         | 
| 88 | 
            +
                      self
         | 
| 89 | 
            +
                    end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                    def opts
         | 
| 92 | 
            +
                      @opts.dup
         | 
| 93 | 
            +
                    end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                    private
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                    def auto_type!(method, type)
         | 
| 98 | 
            +
                      type(type) unless @opts[:type]
         | 
| 99 | 
            +
                      raise ArgumentError, "##{method} can only be used with type :#{type}" unless
         | 
| 100 | 
            +
                        @opts[:type] == type
         | 
| 101 | 
            +
                      type
         | 
| 102 | 
            +
                    end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                    def check_set_with(checks)
         | 
| 105 | 
            +
                      return unless @opts[:checks]&.any?
         | 
| 106 | 
            +
                      Check::Set.new(*@opts[:checks].map { |name, *args| [checks[name]].concat(args) })
         | 
| 107 | 
            +
                    end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                    def type_with(types)
         | 
| 110 | 
            +
                      type = types.fetch(@opts[:type].to_sym)
         | 
| 111 | 
            +
                      return type.maybe if @opts[:maybe]
         | 
| 112 | 
            +
                      raise "#{type.inspect} is a maybe? type (hint: use #maybe instead of #is)" if
         | 
| 113 | 
            +
                        type.maybe?
         | 
| 114 | 
            +
                      type
         | 
| 115 | 
            +
                    end
         | 
| 116 | 
            +
                  end
         | 
| 117 | 
            +
                end
         | 
| 118 | 
            +
              end
         | 
| 119 | 
            +
            end
         | 
| @@ -0,0 +1,40 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Jet
         | 
| 4 | 
            +
              class Contract
         | 
| 5 | 
            +
                class Builder
         | 
| 6 | 
            +
                  attr_reader :checks, :types
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def initialize(attribute_builders = {})
         | 
| 9 | 
            +
                    @attribute_builders = attribute_builders.dup
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def [](key)
         | 
| 13 | 
            +
                    @attribute_builders[key]
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def call(*args)
         | 
| 17 | 
            +
                    Contract.new(@attribute_builders.transform_values { |ab| ab.(*args) })
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  def attribute_builders
         | 
| 21 | 
            +
                    @attribute_builders.dup
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
                  alias to_h attribute_builders
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  def optional(key)
         | 
| 26 | 
            +
                    attribute_builder(key, false)
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  def required(key)
         | 
| 30 | 
            +
                    attribute_builder(key, true)
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  private
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  def attribute_builder(key, required)
         | 
| 36 | 
            +
                    @attribute_builders[key.to_sym] = Attribute::Builder.new(required: required)
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
              end
         | 
| 40 | 
            +
            end
         | 
| @@ -0,0 +1,72 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Jet
         | 
| 4 | 
            +
              class Contract
         | 
| 5 | 
            +
                class Check
         | 
| 6 | 
            +
                  def self.[](key)
         | 
| 7 | 
            +
                    BuiltIn[key]
         | 
| 8 | 
            +
                  end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  attr_reader :check, :name
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def initialize(name, &check)
         | 
| 13 | 
            +
                    raise ArgumentError, "no block given" unless block_given?
         | 
| 14 | 
            +
                    @check = lambda(&check)
         | 
| 15 | 
            +
                    @name = name
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  def call(output, *args)
         | 
| 19 | 
            +
                    result = check.(output, *args)
         | 
| 20 | 
            +
                    return Result.success(output, args: args) if Jet.success?(result)
         | 
| 21 | 
            +
                    Result.failure(error(result), Jet.context(result, args: args, input: output))
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  def inspect
         | 
| 25 | 
            +
                    "#<#{self.class.name}:#{name}>"
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  def to_sym
         | 
| 29 | 
            +
                    name
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  private
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  def error(result)
         | 
| 35 | 
            +
                    [:check_failure, name].tap { |errors| errors << result.output if result }
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  module BuiltIn
         | 
| 39 | 
            +
                    extend Core::InstanceRegistry
         | 
| 40 | 
            +
                    type Check
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                    [
         | 
| 43 | 
            +
                      Check.new(:any?, &:any?),
         | 
| 44 | 
            +
                      Check.new(:empty?, &:empty?),
         | 
| 45 | 
            +
                      Check.new(:eql) { |output, other| output == other },
         | 
| 46 | 
            +
                      Check.new(:gt) { |output, other| output > other },
         | 
| 47 | 
            +
                      Check.new(:gte) { |output, other| output >= other },
         | 
| 48 | 
            +
                      Check.new(:in) { |output, collection| collection.include?(output) },
         | 
| 49 | 
            +
                      Check.new(:lt) { |output, other| output < other },
         | 
| 50 | 
            +
                      Check.new(:lte) { |output, other| output <= other },
         | 
| 51 | 
            +
                      Check.new(:match) { |output, regex| output.match?(regex) },
         | 
| 52 | 
            +
                      Check.new(:max_size) { |output, size| output.size <= size },
         | 
| 53 | 
            +
                      Check.new(:min_size) { |output, size| output.size >= size },
         | 
| 54 | 
            +
                      Check.new(:negative?, &:negative?),
         | 
| 55 | 
            +
                      Check.new(:nin) { |output, collection| !collection.include?(output) },
         | 
| 56 | 
            +
                      Check.new(:not) { |output, other| output != other },
         | 
| 57 | 
            +
                      Check.new(:positive?, &:positive?),
         | 
| 58 | 
            +
                      Check.new(:size) do |output, size_or_range|
         | 
| 59 | 
            +
                        case size_or_range
         | 
| 60 | 
            +
                        when Range
         | 
| 61 | 
            +
                          return true if size_or_range.include?(output.size)
         | 
| 62 | 
            +
                          Result.failure(:range, max: size_or_range.max, min: size_or_range.min)
         | 
| 63 | 
            +
                        else
         | 
| 64 | 
            +
                          return true if output.size == size_or_range
         | 
| 65 | 
            +
                          Result.failure(:exact, size: size_or_range)
         | 
| 66 | 
            +
                        end
         | 
| 67 | 
            +
                      end
         | 
| 68 | 
            +
                    ].map { |c| [c.name, c] }.to_h.tap { |checks| register(checks).freeze }
         | 
| 69 | 
            +
                  end
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
              end
         | 
| 72 | 
            +
            end
         | 
| @@ -0,0 +1,49 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Jet
         | 
| 4 | 
            +
              class Contract
         | 
| 5 | 
            +
                class Check
         | 
| 6 | 
            +
                  class Set
         | 
| 7 | 
            +
                    attr_reader :checks
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                    def initialize(*checks)
         | 
| 10 | 
            +
                      @checks = []
         | 
| 11 | 
            +
                      checks.each { |c| add!(c) }
         | 
| 12 | 
            +
                      @checks.freeze
         | 
| 13 | 
            +
                    end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                    def call(output)
         | 
| 16 | 
            +
                      @checks.each do |(check, *args)|
         | 
| 17 | 
            +
                        check.(output, *args).tap { |r| return r if Jet.failure?(r) }
         | 
| 18 | 
            +
                      end
         | 
| 19 | 
            +
                      Result.success(output)
         | 
| 20 | 
            +
                    end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                    def to_builder
         | 
| 23 | 
            +
                      @checks.map { |(c, *args)| [c.name].concat(args) }
         | 
| 24 | 
            +
                    end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                    private
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                    def add!(check)
         | 
| 29 | 
            +
                      case check
         | 
| 30 | 
            +
                      when Array
         | 
| 31 | 
            +
                        add_with_args!(check.first, *check[1..-1])
         | 
| 32 | 
            +
                      when Hash
         | 
| 33 | 
            +
                        check.each { |c, args| add_with_args!(c, args) }
         | 
| 34 | 
            +
                      when Check
         | 
| 35 | 
            +
                        add_with_args!(check)
         | 
| 36 | 
            +
                      else
         | 
| 37 | 
            +
                        Jet.type_check!(check.inspect, check, Array, Hash, Check)
         | 
| 38 | 
            +
                      end
         | 
| 39 | 
            +
                      @checks
         | 
| 40 | 
            +
                    end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                    def add_with_args!(check, *args)
         | 
| 43 | 
            +
                      Jet.type_check!(check.inspect, check, Check)
         | 
| 44 | 
            +
                      @checks << [check].concat(args)
         | 
| 45 | 
            +
                    end
         | 
| 46 | 
            +
                  end
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
              end
         | 
| 49 | 
            +
            end
         | 
    
        metadata
    ADDED
    
    | @@ -0,0 +1,139 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification
         | 
| 2 | 
            +
            name: jet-contract
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            +
              version: 0.1.0
         | 
| 5 | 
            +
            platform: ruby
         | 
| 6 | 
            +
            authors:
         | 
| 7 | 
            +
            - Joshua
         | 
| 8 | 
            +
            - Hansen
         | 
| 9 | 
            +
            autorequire: 
         | 
| 10 | 
            +
            bindir: bin
         | 
| 11 | 
            +
            cert_chain: []
         | 
| 12 | 
            +
            date: 2019-11-08 00:00:00.000000000 Z
         | 
| 13 | 
            +
            dependencies:
         | 
| 14 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 15 | 
            +
              name: jet-type
         | 
| 16 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 17 | 
            +
                requirements:
         | 
| 18 | 
            +
                - - "~>"
         | 
| 19 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 20 | 
            +
                    version: 0.1.0
         | 
| 21 | 
            +
              type: :runtime
         | 
| 22 | 
            +
              prerelease: false
         | 
| 23 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 24 | 
            +
                requirements:
         | 
| 25 | 
            +
                - - "~>"
         | 
| 26 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 27 | 
            +
                    version: 0.1.0
         | 
| 28 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 29 | 
            +
              name: bundler
         | 
| 30 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 31 | 
            +
                requirements:
         | 
| 32 | 
            +
                - - "~>"
         | 
| 33 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 34 | 
            +
                    version: '2.0'
         | 
| 35 | 
            +
              type: :development
         | 
| 36 | 
            +
              prerelease: false
         | 
| 37 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 38 | 
            +
                requirements:
         | 
| 39 | 
            +
                - - "~>"
         | 
| 40 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 41 | 
            +
                    version: '2.0'
         | 
| 42 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 43 | 
            +
              name: m
         | 
| 44 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 45 | 
            +
                requirements:
         | 
| 46 | 
            +
                - - "~>"
         | 
| 47 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 48 | 
            +
                    version: '1.5'
         | 
| 49 | 
            +
              type: :development
         | 
| 50 | 
            +
              prerelease: false
         | 
| 51 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 52 | 
            +
                requirements:
         | 
| 53 | 
            +
                - - "~>"
         | 
| 54 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 55 | 
            +
                    version: '1.5'
         | 
| 56 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 57 | 
            +
              name: minitest
         | 
| 58 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 59 | 
            +
                requirements:
         | 
| 60 | 
            +
                - - "~>"
         | 
| 61 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 62 | 
            +
                    version: '5.0'
         | 
| 63 | 
            +
              type: :development
         | 
| 64 | 
            +
              prerelease: false
         | 
| 65 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 66 | 
            +
                requirements:
         | 
| 67 | 
            +
                - - "~>"
         | 
| 68 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 69 | 
            +
                    version: '5.0'
         | 
| 70 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 71 | 
            +
              name: rake
         | 
| 72 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 73 | 
            +
                requirements:
         | 
| 74 | 
            +
                - - "~>"
         | 
| 75 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 76 | 
            +
                    version: '10.0'
         | 
| 77 | 
            +
              type: :development
         | 
| 78 | 
            +
              prerelease: false
         | 
| 79 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 80 | 
            +
                requirements:
         | 
| 81 | 
            +
                - - "~>"
         | 
| 82 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 83 | 
            +
                    version: '10.0'
         | 
| 84 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 85 | 
            +
              name: rubocop
         | 
| 86 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 87 | 
            +
                requirements:
         | 
| 88 | 
            +
                - - "~>"
         | 
| 89 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 90 | 
            +
                    version: '0.56'
         | 
| 91 | 
            +
              type: :development
         | 
| 92 | 
            +
              prerelease: false
         | 
| 93 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 94 | 
            +
                requirements:
         | 
| 95 | 
            +
                - - "~>"
         | 
| 96 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 97 | 
            +
                    version: '0.56'
         | 
| 98 | 
            +
            description: Input validation DSL and support classes for the Jet Toolkit.
         | 
| 99 | 
            +
            email:
         | 
| 100 | 
            +
            - joshua@epicbanality.com
         | 
| 101 | 
            +
            executables: []
         | 
| 102 | 
            +
            extensions: []
         | 
| 103 | 
            +
            extra_rdoc_files: []
         | 
| 104 | 
            +
            files:
         | 
| 105 | 
            +
            - LICENSE.txt
         | 
| 106 | 
            +
            - README.md
         | 
| 107 | 
            +
            - lib/jet-contract.rb
         | 
| 108 | 
            +
            - lib/jet/contract.rb
         | 
| 109 | 
            +
            - lib/jet/contract/attribute.rb
         | 
| 110 | 
            +
            - lib/jet/contract/attribute/builder.rb
         | 
| 111 | 
            +
            - lib/jet/contract/builder.rb
         | 
| 112 | 
            +
            - lib/jet/contract/check.rb
         | 
| 113 | 
            +
            - lib/jet/contract/check/set.rb
         | 
| 114 | 
            +
            - lib/jet/contract/version.rb
         | 
| 115 | 
            +
            homepage: https://github.com/binarypaladin/jet-contract
         | 
| 116 | 
            +
            licenses:
         | 
| 117 | 
            +
            - MIT
         | 
| 118 | 
            +
            metadata: {}
         | 
| 119 | 
            +
            post_install_message: 
         | 
| 120 | 
            +
            rdoc_options: []
         | 
| 121 | 
            +
            require_paths:
         | 
| 122 | 
            +
            - lib
         | 
| 123 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 124 | 
            +
              requirements:
         | 
| 125 | 
            +
              - - ">="
         | 
| 126 | 
            +
                - !ruby/object:Gem::Version
         | 
| 127 | 
            +
                  version: 2.5.0
         | 
| 128 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 129 | 
            +
              requirements:
         | 
| 130 | 
            +
              - - ">="
         | 
| 131 | 
            +
                - !ruby/object:Gem::Version
         | 
| 132 | 
            +
                  version: '0'
         | 
| 133 | 
            +
            requirements: []
         | 
| 134 | 
            +
            rubyforge_project: 
         | 
| 135 | 
            +
            rubygems_version: 2.7.6.2
         | 
| 136 | 
            +
            signing_key: 
         | 
| 137 | 
            +
            specification_version: 4
         | 
| 138 | 
            +
            summary: Input validation DSL and support classes for the Jet Toolkit.
         | 
| 139 | 
            +
            test_files: []
         |