parametric 0.0.1 → 0.2.12
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 +5 -5
- data/.gitignore +1 -0
- data/.travis.yml +2 -1
- data/Gemfile +4 -0
- data/README.md +1017 -96
- data/bench/struct_bench.rb +53 -0
- data/bin/console +14 -0
- data/lib/parametric/block_validator.rb +66 -0
- data/lib/parametric/context.rb +49 -0
- data/lib/parametric/default_types.rb +97 -0
- data/lib/parametric/dsl.rb +70 -0
- data/lib/parametric/field.rb +113 -0
- data/lib/parametric/field_dsl.rb +26 -0
- data/lib/parametric/policies.rb +111 -38
- data/lib/parametric/registry.rb +23 -0
- data/lib/parametric/results.rb +15 -0
- data/lib/parametric/schema.rb +228 -0
- data/lib/parametric/struct.rb +108 -0
- data/lib/parametric/version.rb +3 -1
- data/lib/parametric.rb +18 -5
- data/parametric.gemspec +2 -3
- data/spec/custom_block_validator_spec.rb +21 -0
- data/spec/dsl_spec.rb +176 -0
- data/spec/expand_spec.rb +29 -0
- data/spec/field_spec.rb +430 -0
- data/spec/policies_spec.rb +72 -0
- data/spec/schema_lifecycle_hooks_spec.rb +133 -0
- data/spec/schema_spec.rb +289 -0
- data/spec/schema_walk_spec.rb +42 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/struct_spec.rb +298 -0
- data/spec/validators_spec.rb +106 -0
- metadata +49 -23
- data/lib/parametric/hash.rb +0 -36
- data/lib/parametric/params.rb +0 -60
- data/lib/parametric/utils.rb +0 -24
- data/spec/parametric_spec.rb +0 -182
| @@ -0,0 +1,53 @@ | |
| 1 | 
            +
            require 'benchmark/ips'
         | 
| 2 | 
            +
            require 'parametric/struct'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            StructAccount = Struct.new(:id, :email, keyword_init: true)
         | 
| 5 | 
            +
            StructFriend = Struct.new(:name, keyword_init: true)
         | 
| 6 | 
            +
            StructUser = Struct.new(:name, :age, :friends, :account, keyword_init: true)
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            class ParametricAccount
         | 
| 9 | 
            +
              include Parametric::Struct
         | 
| 10 | 
            +
              schema do
         | 
| 11 | 
            +
                field(:id).type(:integer).present
         | 
| 12 | 
            +
                field(:email).type(:string)
         | 
| 13 | 
            +
              end
         | 
| 14 | 
            +
            end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            class ParametricUser
         | 
| 17 | 
            +
              include Parametric::Struct
         | 
| 18 | 
            +
              schema do
         | 
| 19 | 
            +
                field(:name).type(:string).present
         | 
| 20 | 
            +
                field(:age).type(:integer).default(42)
         | 
| 21 | 
            +
                field(:friends).type(:array).schema do
         | 
| 22 | 
            +
                  field(:name).type(:string).present
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
                field(:account).type(:object).schema ParametricAccount
         | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
            end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            Benchmark.ips do |x|
         | 
| 29 | 
            +
              x.report("Struct") {
         | 
| 30 | 
            +
                StructUser.new(
         | 
| 31 | 
            +
                  name: 'Ismael',
         | 
| 32 | 
            +
                  age: 42,
         | 
| 33 | 
            +
                  friends: [
         | 
| 34 | 
            +
                    StructFriend.new(name: 'Joe'),
         | 
| 35 | 
            +
                    StructFriend.new(name: 'Joan'),
         | 
| 36 | 
            +
                  ],
         | 
| 37 | 
            +
                  account: StructAccount.new(id: 123, email: 'my@account.com')
         | 
| 38 | 
            +
                )
         | 
| 39 | 
            +
              }
         | 
| 40 | 
            +
              x.report("Parametric::Struct")  {
         | 
| 41 | 
            +
                ParametricUser.new!(
         | 
| 42 | 
            +
                  name: 'Ismael',
         | 
| 43 | 
            +
                  age: 42,
         | 
| 44 | 
            +
                  friends: [
         | 
| 45 | 
            +
                    { name: 'Joe' },
         | 
| 46 | 
            +
                    { name: 'Joan' }
         | 
| 47 | 
            +
                  ],
         | 
| 48 | 
            +
                  account: { id: 123, email: 'my@account.com' }
         | 
| 49 | 
            +
                )
         | 
| 50 | 
            +
              }
         | 
| 51 | 
            +
              x.compare!
         | 
| 52 | 
            +
            end
         | 
| 53 | 
            +
             | 
    
        data/bin/console
    ADDED
    
    | @@ -0,0 +1,14 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "bundler/setup"
         | 
| 4 | 
            +
            require "parametric"
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            # You can add fixtures and/or initialization code here to make experimenting
         | 
| 7 | 
            +
            # with your gem easier. You can also use a different console, if you like.
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            # (If you use this, don't forget to add pry to your Gemfile!)
         | 
| 10 | 
            +
            # require "pry"
         | 
| 11 | 
            +
            # Pry.start
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            require "irb"
         | 
| 14 | 
            +
            IRB.start
         | 
| @@ -0,0 +1,66 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Parametric
         | 
| 4 | 
            +
              class BlockValidator
         | 
| 5 | 
            +
                def self.build(meth, &block)
         | 
| 6 | 
            +
                  klass = Class.new(self)
         | 
| 7 | 
            +
                  klass.public_send(meth, &block)
         | 
| 8 | 
            +
                  klass
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def self.message(&block)
         | 
| 12 | 
            +
                  @message_block = block if block_given?
         | 
| 13 | 
            +
                  @message_block if instance_variable_defined?('@message_block')
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def self.validate(&validate_block)
         | 
| 17 | 
            +
                  @validate_block = validate_block if block_given?
         | 
| 18 | 
            +
                  @validate_block if instance_variable_defined?('@validate_block')
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def self.coerce(&coerce_block)
         | 
| 22 | 
            +
                  @coerce_block = coerce_block if block_given?
         | 
| 23 | 
            +
                  @coerce_block
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def self.eligible(&block)
         | 
| 27 | 
            +
                  @eligible_block = block if block_given?
         | 
| 28 | 
            +
                  @eligible_block if instance_variable_defined?('@eligible_block')
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def self.meta_data(&block)
         | 
| 32 | 
            +
                  @meta_data_block = block if block_given?
         | 
| 33 | 
            +
                  @meta_data_block if instance_variable_defined?('@meta_data_block')
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                attr_reader :message
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                def initialize(*args)
         | 
| 39 | 
            +
                  @args = args
         | 
| 40 | 
            +
                  @message = 'is invalid'
         | 
| 41 | 
            +
                  @validate_block = self.class.validate || ->(*args) { true }
         | 
| 42 | 
            +
                  @coerce_block = self.class.coerce || ->(v, *_) { v }
         | 
| 43 | 
            +
                  @eligible_block = self.class.eligible || ->(*args) { true }
         | 
| 44 | 
            +
                  @meta_data_block = self.class.meta_data || ->(*args) { {} }
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                def eligible?(value, key, payload)
         | 
| 48 | 
            +
                  args = (@args + [value, key, payload])
         | 
| 49 | 
            +
                  @eligible_block.call(*args)
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                def coerce(value, key, context)
         | 
| 53 | 
            +
                  @coerce_block.call(value, key, context)
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                def valid?(value, key, payload)
         | 
| 57 | 
            +
                  args = (@args + [value, key, payload])
         | 
| 58 | 
            +
                  @message = self.class.message.call(*args) if self.class.message
         | 
| 59 | 
            +
                  @validate_block.call(*args)
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                def meta_data
         | 
| 63 | 
            +
                  @meta_data_block.call *@args
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
              end
         | 
| 66 | 
            +
            end
         | 
| @@ -0,0 +1,49 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Parametric
         | 
| 4 | 
            +
              class Top
         | 
| 5 | 
            +
                attr_reader :errors
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def initialize
         | 
| 8 | 
            +
                  @errors = {}
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def add_error(key, msg)
         | 
| 12 | 
            +
                  errors[key] ||= []
         | 
| 13 | 
            +
                  errors[key] << msg
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
              end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
              class Context
         | 
| 18 | 
            +
                def initialize(path = nil, top = Top.new)
         | 
| 19 | 
            +
                  @top = top
         | 
| 20 | 
            +
                  @path = Array(path).compact
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def errors
         | 
| 24 | 
            +
                  top.errors
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def add_error(msg)
         | 
| 28 | 
            +
                  top.add_error(string_path, msg)
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def add_base_error(key, msg)
         | 
| 32 | 
            +
                  top.add_error(key, msg)
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                def sub(key)
         | 
| 36 | 
            +
                  self.class.new(path + [key], top)
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                protected
         | 
| 40 | 
            +
                attr_reader :path, :top
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                def string_path
         | 
| 43 | 
            +
                  path.reduce(['$']) do |m, segment|
         | 
| 44 | 
            +
                    m << (segment.is_a?(Integer) ? "[#{segment}]" : ".#{segment}")
         | 
| 45 | 
            +
                    m
         | 
| 46 | 
            +
                  end.join
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
              end
         | 
| 49 | 
            +
            end
         | 
| @@ -0,0 +1,97 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "date"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Parametric
         | 
| 6 | 
            +
              # type coercions
         | 
| 7 | 
            +
              Parametric.policy :integer do
         | 
| 8 | 
            +
                coerce do |v, k, c|
         | 
| 9 | 
            +
                  v.to_i
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                meta_data do
         | 
| 13 | 
            +
                  {type: :integer}
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
              end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
              Parametric.policy :number do
         | 
| 18 | 
            +
                coerce do |v, k, c|
         | 
| 19 | 
            +
                  v.to_f
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                meta_data do
         | 
| 23 | 
            +
                  {type: :number}
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
              Parametric.policy :string do
         | 
| 28 | 
            +
                coerce do |v, k, c|
         | 
| 29 | 
            +
                  v.to_s
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                meta_data do
         | 
| 33 | 
            +
                  {type: :string}
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
              end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
              Parametric.policy :boolean do
         | 
| 38 | 
            +
                coerce do |v, k, c|
         | 
| 39 | 
            +
                  !!v
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                meta_data do
         | 
| 43 | 
            +
                  {type: :boolean}
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
              end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
              # type validations
         | 
| 48 | 
            +
              Parametric.policy :array do
         | 
| 49 | 
            +
                message do |actual|
         | 
| 50 | 
            +
                  "expects an array, but got #{actual.inspect}"
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                validate do |value, key, payload|
         | 
| 54 | 
            +
                  !payload.key?(key) || value.is_a?(Array)
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                meta_data do
         | 
| 58 | 
            +
                  {type: :array}
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
              end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
              Parametric.policy :object do
         | 
| 63 | 
            +
                message do |actual|
         | 
| 64 | 
            +
                  "expects a hash, but got #{actual.inspect}"
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                validate do |value, key, payload|
         | 
| 68 | 
            +
                  !payload.key?(key) ||
         | 
| 69 | 
            +
                    value.respond_to?(:[]) &&
         | 
| 70 | 
            +
                    value.respond_to?(:key?)
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                meta_data do
         | 
| 74 | 
            +
                  {type: :object}
         | 
| 75 | 
            +
                end
         | 
| 76 | 
            +
              end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
              Parametric.policy :split do
         | 
| 79 | 
            +
                coerce do |v, k, c|
         | 
| 80 | 
            +
                  v.kind_of?(Array) ? v : v.to_s.split(/\s*,\s*/)
         | 
| 81 | 
            +
                end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                meta_data do
         | 
| 84 | 
            +
                  {type: :array}
         | 
| 85 | 
            +
                end
         | 
| 86 | 
            +
              end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
              Parametric.policy :datetime do
         | 
| 89 | 
            +
                coerce do |v, k, c|
         | 
| 90 | 
            +
                  DateTime.parse(v.to_s)
         | 
| 91 | 
            +
                end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                meta_data do
         | 
| 94 | 
            +
                  {type: :datetime}
         | 
| 95 | 
            +
                end
         | 
| 96 | 
            +
              end
         | 
| 97 | 
            +
            end
         | 
| @@ -0,0 +1,70 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "parametric"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Parametric
         | 
| 6 | 
            +
              module DSL
         | 
| 7 | 
            +
                # Example
         | 
| 8 | 
            +
                #   class Foo
         | 
| 9 | 
            +
                #     include Parametric::DSL
         | 
| 10 | 
            +
                #
         | 
| 11 | 
            +
                #     schema do
         | 
| 12 | 
            +
                #       field(:title).type(:string).present
         | 
| 13 | 
            +
                #       field(:age).type(:integer).default(20)
         | 
| 14 | 
            +
                #     end
         | 
| 15 | 
            +
                #
         | 
| 16 | 
            +
                #      attr_reader :params
         | 
| 17 | 
            +
                #
         | 
| 18 | 
            +
                #      def initialize(input)
         | 
| 19 | 
            +
                #        @params = self.class.schema.resolve(input)
         | 
| 20 | 
            +
                #      end
         | 
| 21 | 
            +
                #   end
         | 
| 22 | 
            +
                #
         | 
| 23 | 
            +
                #   foo = Foo.new(title: "A title", nope: "hello")
         | 
| 24 | 
            +
                #
         | 
| 25 | 
            +
                #   foo.params # => {title: "A title", age: 20}
         | 
| 26 | 
            +
                #
         | 
| 27 | 
            +
                DEFAULT_SCHEMA_NAME = :schema
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def self.included(base)
         | 
| 30 | 
            +
                  base.extend(ClassMethods)
         | 
| 31 | 
            +
                  base.schemas = {DEFAULT_SCHEMA_NAME => Parametric::Schema.new}
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                module ClassMethods
         | 
| 35 | 
            +
                  def schema=(sc)
         | 
| 36 | 
            +
                    @schemas[DEFAULT_SCHEMA_NAME] = sc
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  def schemas=(sc)
         | 
| 40 | 
            +
                    @schemas = sc
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  def inherited(subclass)
         | 
| 44 | 
            +
                    subclass.schemas = @schemas.each_with_object({}) do |(key, sc), hash|
         | 
| 45 | 
            +
                      hash[key] = sc.merge(Parametric::Schema.new)
         | 
| 46 | 
            +
                    end
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  def schema(*args, &block)
         | 
| 50 | 
            +
                    options = args.last.is_a?(Hash) ? args.last : {}
         | 
| 51 | 
            +
                    key = args.first.is_a?(Symbol) ? args.first : DEFAULT_SCHEMA_NAME
         | 
| 52 | 
            +
                    current_schema = @schemas.fetch(key) { Parametric::Schema.new }
         | 
| 53 | 
            +
                    new_schema = if block_given? || options.any?
         | 
| 54 | 
            +
                      Parametric::Schema.new(options, &block)
         | 
| 55 | 
            +
                    elsif args.first.respond_to?(:schema)
         | 
| 56 | 
            +
                      args.first
         | 
| 57 | 
            +
                    end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                    return current_schema unless new_schema
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                    @schemas[key] = current_schema ? current_schema.merge(new_schema) : new_schema
         | 
| 62 | 
            +
                    parametric_after_define_schema(@schemas[key])
         | 
| 63 | 
            +
                  end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                  def parametric_after_define_schema(sc)
         | 
| 66 | 
            +
                    # noop hook
         | 
| 67 | 
            +
                  end
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
              end
         | 
| 70 | 
            +
            end
         | 
| @@ -0,0 +1,113 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "parametric/field_dsl"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Parametric
         | 
| 6 | 
            +
              class ConfigurationError < StandardError; end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
              class Field
         | 
| 9 | 
            +
                include FieldDSL
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                attr_reader :key, :meta_data
         | 
| 12 | 
            +
                Result = Struct.new(:eligible?, :value)
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                def initialize(key, registry = Parametric.registry)
         | 
| 15 | 
            +
                  @key = key
         | 
| 16 | 
            +
                  @policies = []
         | 
| 17 | 
            +
                  @registry = registry
         | 
| 18 | 
            +
                  @default_block = nil
         | 
| 19 | 
            +
                  @meta_data = {}
         | 
| 20 | 
            +
                  @policies = []
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def meta(hash = nil)
         | 
| 24 | 
            +
                  @meta_data = @meta_data.merge(hash) if hash.is_a?(Hash)
         | 
| 25 | 
            +
                  self
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def default(value)
         | 
| 29 | 
            +
                  meta default: value
         | 
| 30 | 
            +
                  @default_block = (value.respond_to?(:call) ? value : ->(key, payload, context) { value })
         | 
| 31 | 
            +
                  self
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def policy(key, *args)
         | 
| 35 | 
            +
                  pol = lookup(key, args)
         | 
| 36 | 
            +
                  meta pol.meta_data
         | 
| 37 | 
            +
                  policies << pol
         | 
| 38 | 
            +
                  self
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
                alias_method :type, :policy
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                def schema(sc = nil, &block)
         | 
| 43 | 
            +
                  sc = (sc ? sc : Schema.new(&block))
         | 
| 44 | 
            +
                  meta schema: sc
         | 
| 45 | 
            +
                  policy sc.schema
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                def visit(meta_key = nil, &visitor)
         | 
| 49 | 
            +
                  if sc = meta_data[:schema]
         | 
| 50 | 
            +
                    r = sc.visit(meta_key, &visitor)
         | 
| 51 | 
            +
                    (meta_data[:type] == :array) ? [r] : r
         | 
| 52 | 
            +
                  else
         | 
| 53 | 
            +
                    meta_key ? meta_data[meta_key] : yield(self)
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                def resolve(payload, context)
         | 
| 58 | 
            +
                  eligible = payload.key?(key)
         | 
| 59 | 
            +
                  value = payload[key] # might be nil
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  if !eligible && has_default?
         | 
| 62 | 
            +
                    eligible = true
         | 
| 63 | 
            +
                    value = default_block.call(key, payload, context)
         | 
| 64 | 
            +
                    return Result.new(eligible, value)
         | 
| 65 | 
            +
                  end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                  policies.each do |policy|
         | 
| 68 | 
            +
                    if !policy.eligible?(value, key, payload)
         | 
| 69 | 
            +
                      eligible = false
         | 
| 70 | 
            +
                      if has_default?
         | 
| 71 | 
            +
                        eligible = true
         | 
| 72 | 
            +
                        value = default_block.call(key, payload, context)
         | 
| 73 | 
            +
                      end
         | 
| 74 | 
            +
                      break
         | 
| 75 | 
            +
                    else
         | 
| 76 | 
            +
                      value = resolve_one(policy, value, context)
         | 
| 77 | 
            +
                      if !policy.valid?(value, key, payload)
         | 
| 78 | 
            +
                        eligible = true # eligible, but has errors
         | 
| 79 | 
            +
                        context.add_error policy.message
         | 
| 80 | 
            +
                        break # only one error at a time
         | 
| 81 | 
            +
                      end
         | 
| 82 | 
            +
                    end
         | 
| 83 | 
            +
                  end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                  Result.new(eligible, value)
         | 
| 86 | 
            +
                end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                private
         | 
| 89 | 
            +
                attr_reader :policies, :registry, :default_block
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                def resolve_one(policy, value, context)
         | 
| 92 | 
            +
                  begin
         | 
| 93 | 
            +
                    policy.coerce(value, key, context)
         | 
| 94 | 
            +
                  rescue StandardError => e
         | 
| 95 | 
            +
                    context.add_error e.message
         | 
| 96 | 
            +
                    value
         | 
| 97 | 
            +
                  end
         | 
| 98 | 
            +
                end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                def has_default?
         | 
| 101 | 
            +
                  !!default_block && !meta_data[:skip_default]
         | 
| 102 | 
            +
                end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                def lookup(key, args)
         | 
| 105 | 
            +
                  obj = key.is_a?(Symbol) ? registry.policies[key] : key
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                  raise ConfigurationError, "No policies defined for #{key.inspect}" unless obj
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                  obj.respond_to?(:new) ? obj.new(*args) : obj
         | 
| 110 | 
            +
                end
         | 
| 111 | 
            +
              end
         | 
| 112 | 
            +
            end
         | 
| 113 | 
            +
             | 
| @@ -0,0 +1,26 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Parametric
         | 
| 4 | 
            +
              # Field DSL
         | 
| 5 | 
            +
              # host instance must implement:
         | 
| 6 | 
            +
              # #meta(options Hash)
         | 
| 7 | 
            +
              # #policy(key Symbol) self
         | 
| 8 | 
            +
              #
         | 
| 9 | 
            +
              module FieldDSL
         | 
| 10 | 
            +
                def required
         | 
| 11 | 
            +
                  policy :required
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                def present
         | 
| 15 | 
            +
                  required.policy :present
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def declared
         | 
| 19 | 
            +
                  policy :declared
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                def options(opts)
         | 
| 23 | 
            +
                  policy :options, opts
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
            end
         | 
    
        data/lib/parametric/policies.rb
    CHANGED
    
    | @@ -1,62 +1,135 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            module Parametric
         | 
| 2 4 | 
             
              module Policies
         | 
| 5 | 
            +
                class Format
         | 
| 6 | 
            +
                  attr_reader :message
         | 
| 3 7 |  | 
| 4 | 
            -
             | 
| 5 | 
            -
             | 
| 6 | 
            -
                    @ | 
| 7 | 
            -
                    @decorated = decorated
         | 
| 8 | 
            +
                  def initialize(fmt, msg = "invalid format")
         | 
| 9 | 
            +
                    @message = msg
         | 
| 10 | 
            +
                    @fmt = fmt
         | 
| 8 11 | 
             
                  end
         | 
| 9 12 |  | 
| 10 | 
            -
                  def  | 
| 11 | 
            -
                     | 
| 13 | 
            +
                  def eligible?(value, key, payload)
         | 
| 14 | 
            +
                    payload.key?(key)
         | 
| 12 15 | 
             
                  end
         | 
| 13 16 |  | 
| 14 | 
            -
                  def value
         | 
| 15 | 
            -
                     | 
| 17 | 
            +
                  def coerce(value, key, context)
         | 
| 18 | 
            +
                    value
         | 
| 16 19 | 
             
                  end
         | 
| 17 20 |  | 
| 18 | 
            -
                   | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 21 | 
            +
                  def valid?(value, key, payload)
         | 
| 22 | 
            +
                    !payload.key?(key) || !!(value.to_s =~ @fmt)
         | 
| 23 | 
            +
                  end
         | 
| 21 24 |  | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
                    v = decorated.value
         | 
| 25 | 
            -
                    v.any? ? v : Array(options[:default])
         | 
| 25 | 
            +
                  def meta_data
         | 
| 26 | 
            +
                    {}
         | 
| 26 27 | 
             
                  end
         | 
| 27 28 | 
             
                end
         | 
| 29 | 
            +
              end
         | 
| 28 30 |  | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 31 | 
            +
              # Default validators
         | 
| 32 | 
            +
              EMAIL_REGEXP = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i.freeze
         | 
| 31 33 |  | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 34 | 
            +
              Parametric.policy :format, Policies::Format
         | 
| 35 | 
            +
              Parametric.policy :email, Policies::Format.new(EMAIL_REGEXP, 'invalid email')
         | 
| 36 | 
            +
             | 
| 37 | 
            +
              Parametric.policy :noop do
         | 
| 38 | 
            +
                eligible do |value, key, payload|
         | 
| 39 | 
            +
                  true
         | 
| 37 40 | 
             
                end
         | 
| 41 | 
            +
              end
         | 
| 38 42 |  | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 42 | 
            -
                  end
         | 
| 43 | 
            +
              Parametric.policy :declared do
         | 
| 44 | 
            +
                eligible do |value, key, payload|
         | 
| 45 | 
            +
                  payload.key? key
         | 
| 43 46 | 
             
                end
         | 
| 47 | 
            +
              end
         | 
| 44 48 |  | 
| 45 | 
            -
             | 
| 46 | 
            -
             | 
| 47 | 
            -
             | 
| 48 | 
            -
             | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 49 | 
            +
              Parametric.policy :declared_no_default do
         | 
| 50 | 
            +
                eligible do |value, key, payload|
         | 
| 51 | 
            +
                  payload.key? key
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                meta_data do
         | 
| 55 | 
            +
                  {skip_default: true}
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
              end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
              Parametric.policy :required do
         | 
| 60 | 
            +
                message do |*|
         | 
| 61 | 
            +
                  "is required"
         | 
| 62 | 
            +
                end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                validate do |value, key, payload|
         | 
| 65 | 
            +
                  payload.key? key
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                meta_data do
         | 
| 69 | 
            +
                  {required: true}
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
              end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
              Parametric.policy :present do
         | 
| 74 | 
            +
                message do |*|
         | 
| 75 | 
            +
                  "is required and value must be present"
         | 
| 51 76 | 
             
                end
         | 
| 52 77 |  | 
| 53 | 
            -
                 | 
| 54 | 
            -
                   | 
| 55 | 
            -
             | 
| 56 | 
            -
             | 
| 57 | 
            -
             | 
| 78 | 
            +
                validate do |value, key, payload|
         | 
| 79 | 
            +
                  case value
         | 
| 80 | 
            +
                  when String
         | 
| 81 | 
            +
                    value.strip != ''
         | 
| 82 | 
            +
                  when Array, Hash
         | 
| 83 | 
            +
                    value.any?
         | 
| 84 | 
            +
                  else
         | 
| 85 | 
            +
                    !value.nil?
         | 
| 58 86 | 
             
                  end
         | 
| 59 87 | 
             
                end
         | 
| 60 88 |  | 
| 89 | 
            +
                meta_data do
         | 
| 90 | 
            +
                  {present: true}
         | 
| 91 | 
            +
                end
         | 
| 92 | 
            +
              end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
              Parametric.policy :gt do
         | 
| 95 | 
            +
                message do |num, actual|
         | 
| 96 | 
            +
                  "must be greater than #{num}, but got #{actual}"
         | 
| 97 | 
            +
                end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                validate do |num, actual, key, payload|
         | 
| 100 | 
            +
                  !payload[key] || actual.to_i > num.to_i
         | 
| 101 | 
            +
                end
         | 
| 102 | 
            +
              end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
              Parametric.policy :lt do
         | 
| 105 | 
            +
                message do |num, actual|
         | 
| 106 | 
            +
                  "must be less than #{num}, but got #{actual}"
         | 
| 107 | 
            +
                end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                validate do |num, actual, key, payload|
         | 
| 110 | 
            +
                  !payload[key] || actual.to_i < num.to_i
         | 
| 111 | 
            +
                end
         | 
| 112 | 
            +
              end
         | 
| 113 | 
            +
             | 
| 114 | 
            +
              Parametric.policy :options do
         | 
| 115 | 
            +
                message do |options, actual|
         | 
| 116 | 
            +
                  "must be one of #{options.join(', ')}, but got #{actual}"
         | 
| 117 | 
            +
                end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                eligible do |options, actual, key, payload|
         | 
| 120 | 
            +
                  payload.key?(key)
         | 
| 121 | 
            +
                end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                validate do |options, actual, key, payload|
         | 
| 124 | 
            +
                  !payload.key?(key) || ok?(options, actual)
         | 
| 125 | 
            +
                end
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                meta_data do |opts|
         | 
| 128 | 
            +
                  {options: opts}
         | 
| 129 | 
            +
                end
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                def ok?(options, actual)
         | 
| 132 | 
            +
                  [actual].flatten.all?{|v| options.include?(v)}
         | 
| 133 | 
            +
                end
         | 
| 61 134 | 
             
              end
         | 
| 62 | 
            -
            end
         | 
| 135 | 
            +
            end
         | 
| @@ -0,0 +1,23 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'parametric/block_validator'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Parametric
         | 
| 6 | 
            +
              class Registry
         | 
| 7 | 
            +
                attr_reader :policies
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def initialize
         | 
| 10 | 
            +
                  @policies = {}
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def coercions
         | 
| 14 | 
            +
                  policies
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def policy(name, plcy = nil, &block)
         | 
| 18 | 
            +
                  policies[name] = (plcy || BlockValidator.build(:instance_eval, &block))
         | 
| 19 | 
            +
                  self
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
            end
         | 
| 23 | 
            +
             |