parametric 0.1.3 → 0.2.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 +5 -5
- data/README.md +166 -8
- data/lib/parametric/dsl.rb +5 -0
- data/lib/parametric/field.rb +1 -1
- data/lib/parametric/schema.rb +39 -5
- data/lib/parametric/struct.rb +80 -0
- data/lib/parametric/version.rb +1 -1
- data/spec/expand_spec.rb +29 -0
- data/spec/struct_spec.rb +277 -0
- metadata +8 -3
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 | 
            -
             | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 2 | 
            +
            SHA256:
         | 
| 3 | 
            +
              metadata.gz: f5dfee33e2a3f175505899e0238390e2795e0a862469b851a3a550aef152e8be
         | 
| 4 | 
            +
              data.tar.gz: c1225c196aeb662384da471dd31b62ea8e072a90049eecb609976dfa21eec1d6
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 2645e19eda4c75f82f8620fa16cce48fad7b60a14d1749e78a3de05411b55f1daf2ae9e41149467ccd1ccecd613178972d8283ead6a8becdf0c35bae1d4a7123
         | 
| 7 | 
            +
              data.tar.gz: 84ecd165ff1355b17f5596f7f7c47a1b2965a2a7c50e6211801400f019aa8d318847ebf68ecbbf2c6c75da6cd8f3bb257707f9e69a4d9b5500ddf82c52733ff2
         | 
    
        data/README.md
    CHANGED
    
    | @@ -474,10 +474,10 @@ create_user_schema.structure[:friends].structure[:name].label # => "Friend full | |
| 474 474 | 
             
            Note that many field policies add field meta data.
         | 
| 475 475 |  | 
| 476 476 | 
             
            ```ruby
         | 
| 477 | 
            -
            create_user_schema. | 
| 478 | 
            -
            create_user_schema. | 
| 479 | 
            -
            create_user_schema. | 
| 480 | 
            -
            create_user_schema. | 
| 477 | 
            +
            create_user_schema.structure[:name][:type] # => :string
         | 
| 478 | 
            +
            create_user_schema.structure[:name][:required] # => true
         | 
| 479 | 
            +
            create_user_schema.structure[:status][:options] # => ["published", "unpublished"]
         | 
| 480 | 
            +
            create_user_schema.structure[:status][:default] # => "published"
         | 
| 481 481 | 
             
            ```
         | 
| 482 482 |  | 
| 483 483 | 
             
            ## #walk
         | 
| @@ -487,7 +487,7 @@ The `#walk` method can recursively walk a schema definition and extract meta dat | |
| 487 487 | 
             
            ```ruby
         | 
| 488 488 | 
             
            schema_documentation = create_user_schema.walk do |field|
         | 
| 489 489 | 
             
              {type: field.meta_data[:type], label: field.meta_data[:label]}
         | 
| 490 | 
            -
            end
         | 
| 490 | 
            +
            end.output
         | 
| 491 491 |  | 
| 492 492 | 
             
            # Returns
         | 
| 493 493 |  | 
| @@ -507,7 +507,7 @@ end | |
| 507 507 | 
             
            When passed a _symbol_, it will collect that key from field meta data.
         | 
| 508 508 |  | 
| 509 509 | 
             
            ```ruby
         | 
| 510 | 
            -
            schema_labels = create_user_schema.walk(:label)
         | 
| 510 | 
            +
            schema_labels = create_user_schema.walk(:label).output
         | 
| 511 511 |  | 
| 512 512 | 
             
            # returns
         | 
| 513 513 |  | 
| @@ -690,8 +690,7 @@ class CreateUserForm | |
| 690 690 | 
             
              attr_reader :errors, :params
         | 
| 691 691 |  | 
| 692 692 | 
             
              def initialize(payload: {})
         | 
| 693 | 
            -
                 | 
| 694 | 
            -
                results = self.class.schema.resolve(params)
         | 
| 693 | 
            +
                results = self.class.schema.resolve(payload)
         | 
| 695 694 | 
             
                @errors = results.errors
         | 
| 696 695 | 
             
                @params = results.output
         | 
| 697 696 | 
             
              end
         | 
| @@ -743,6 +742,165 @@ class CreateUserForm | |
| 743 742 | 
             
            end
         | 
| 744 743 | 
             
            ```
         | 
| 745 744 |  | 
| 745 | 
            +
            ## Expanding fields dynamically
         | 
| 746 | 
            +
             | 
| 747 | 
            +
            Sometimes you don't know the exact field names but you want to allow arbitrary fields depending on a given pattern.
         | 
| 748 | 
            +
             | 
| 749 | 
            +
            ```ruby
         | 
| 750 | 
            +
            # with this payload:
         | 
| 751 | 
            +
            # {
         | 
| 752 | 
            +
            #   title: "A title",
         | 
| 753 | 
            +
            #   :"custom_attr_Color" => "red",
         | 
| 754 | 
            +
            #   :"custom_attr_Material" => "leather"
         | 
| 755 | 
            +
            # }
         | 
| 756 | 
            +
             | 
| 757 | 
            +
            schema = Parametric::Schema.new do
         | 
| 758 | 
            +
              field(:title).type(:string).present
         | 
| 759 | 
            +
              # here we allow any field starting with /^custom_attr/
         | 
| 760 | 
            +
              # this yields a MatchData object to the block
         | 
| 761 | 
            +
              # where you can define a Field and validations on the fly
         | 
| 762 | 
            +
              # https://ruby-doc.org/core-2.2.0/MatchData.html
         | 
| 763 | 
            +
              expand(/^custom_attr_(.+)/) do |match|
         | 
| 764 | 
            +
                field(match[1]).type(:string).present
         | 
| 765 | 
            +
              end
         | 
| 766 | 
            +
            end
         | 
| 767 | 
            +
             | 
| 768 | 
            +
            results = schema.resolve({
         | 
| 769 | 
            +
              title: "A title",
         | 
| 770 | 
            +
              :"custom_attr_Color" => "red",
         | 
| 771 | 
            +
              :"custom_attr_Material" => "leather",
         | 
| 772 | 
            +
              :"custom_attr_Weight" => "",
         | 
| 773 | 
            +
            })
         | 
| 774 | 
            +
             | 
| 775 | 
            +
            results.ouput[:Color] # => "red"
         | 
| 776 | 
            +
            results.ouput[:Material] # => "leather"
         | 
| 777 | 
            +
            results.errors["$.Weight"] # => ["is required and value must be present"]
         | 
| 778 | 
            +
            ```
         | 
| 779 | 
            +
             | 
| 780 | 
            +
            NOTES: dynamically expanded field names are not included in `Schema#structure` metadata, and they are only processes if fields with the given expressions are present in the payload. This means that validations applied to those fields only run if keys are present in the first place.
         | 
| 781 | 
            +
             | 
| 782 | 
            +
            ## Structs
         | 
| 783 | 
            +
             | 
| 784 | 
            +
            Structs turn schema definitions into objects graphs with attribute readers.
         | 
| 785 | 
            +
             | 
| 786 | 
            +
            Add optional `Parametrict::Struct` module to define struct-like objects with schema definitions.
         | 
| 787 | 
            +
             | 
| 788 | 
            +
            ```ruby
         | 
| 789 | 
            +
            require 'parametric/struct'
         | 
| 790 | 
            +
             | 
| 791 | 
            +
            class User
         | 
| 792 | 
            +
              include Parametric::Struct
         | 
| 793 | 
            +
             | 
| 794 | 
            +
              schema do
         | 
| 795 | 
            +
                field(:name).type(:string).present
         | 
| 796 | 
            +
                field(:friends).type(:array).schema do
         | 
| 797 | 
            +
                  field(:name).type(:string).present
         | 
| 798 | 
            +
                  field(:age).type(:integer)
         | 
| 799 | 
            +
                end
         | 
| 800 | 
            +
              end
         | 
| 801 | 
            +
            end
         | 
| 802 | 
            +
            ```
         | 
| 803 | 
            +
             | 
| 804 | 
            +
            `User` objects can be instantiated with hash data, which will be coerced and validated as per the schema definition.
         | 
| 805 | 
            +
             | 
| 806 | 
            +
            ```ruby
         | 
| 807 | 
            +
            user = User.new(
         | 
| 808 | 
            +
              name: 'Joe',
         | 
| 809 | 
            +
              friends: [
         | 
| 810 | 
            +
                {name: 'Jane', age: 40},
         | 
| 811 | 
            +
                {name: 'John', age: 30},
         | 
| 812 | 
            +
              ]
         | 
| 813 | 
            +
            )
         | 
| 814 | 
            +
             | 
| 815 | 
            +
            # properties
         | 
| 816 | 
            +
            user.name # => 'Joe'
         | 
| 817 | 
            +
            user.friends.first.name # => 'Jane'
         | 
| 818 | 
            +
            user.friends.last.age # => 30
         | 
| 819 | 
            +
            ```
         | 
| 820 | 
            +
             | 
| 821 | 
            +
            ### Errors
         | 
| 822 | 
            +
             | 
| 823 | 
            +
            Both the top-level and nested instances contain error information:
         | 
| 824 | 
            +
             | 
| 825 | 
            +
            ```ruby
         | 
| 826 | 
            +
            user = User.new(
         | 
| 827 | 
            +
              name: '', # invalid
         | 
| 828 | 
            +
              friends: [
         | 
| 829 | 
            +
                # friend name also invalid
         | 
| 830 | 
            +
                {name: '', age: 40},
         | 
| 831 | 
            +
              ]
         | 
| 832 | 
            +
            )
         | 
| 833 | 
            +
             | 
| 834 | 
            +
            user.valid? # false
         | 
| 835 | 
            +
            user.errors['$.name'] # => "is required and must be present"
         | 
| 836 | 
            +
            user.errors['$.friends[0].name'] # => "is required and must be present"
         | 
| 837 | 
            +
             | 
| 838 | 
            +
            # also access error in nested instances directly
         | 
| 839 | 
            +
            user.friends.first.valid? # false
         | 
| 840 | 
            +
            user.friends.first.errors['$.name'] # "is required and must be valid"
         | 
| 841 | 
            +
            ```
         | 
| 842 | 
            +
             | 
| 843 | 
            +
            ### Nested structs
         | 
| 844 | 
            +
             | 
| 845 | 
            +
            You can also pass separate struct classes in a nested schema definition.
         | 
| 846 | 
            +
             | 
| 847 | 
            +
            ```ruby
         | 
| 848 | 
            +
            class Friend
         | 
| 849 | 
            +
              include Parametric::Struct
         | 
| 850 | 
            +
             | 
| 851 | 
            +
              schema do
         | 
| 852 | 
            +
                field(:name).type(:string).present
         | 
| 853 | 
            +
                field(:age).type(:integer)
         | 
| 854 | 
            +
              end
         | 
| 855 | 
            +
            end
         | 
| 856 | 
            +
             | 
| 857 | 
            +
            class User
         | 
| 858 | 
            +
              include Parametric::Struct
         | 
| 859 | 
            +
             | 
| 860 | 
            +
              schema do
         | 
| 861 | 
            +
                field(:name).type(:string).present
         | 
| 862 | 
            +
                # here we use the Friend class
         | 
| 863 | 
            +
                field(:friends).type(:array).schema Friend
         | 
| 864 | 
            +
              end
         | 
| 865 | 
            +
            end
         | 
| 866 | 
            +
            ```
         | 
| 867 | 
            +
             | 
| 868 | 
            +
            ### Inheritance
         | 
| 869 | 
            +
             | 
| 870 | 
            +
            Struct subclasses can add to inherited schemas, or override fields defined in the parent.
         | 
| 871 | 
            +
             | 
| 872 | 
            +
            ```ruby
         | 
| 873 | 
            +
            class AdminUser < User
         | 
| 874 | 
            +
              # inherits User schema, and can add stuff to its own schema
         | 
| 875 | 
            +
              schema do
         | 
| 876 | 
            +
                field(:permissions).type(:array)
         | 
| 877 | 
            +
              end
         | 
| 878 | 
            +
            end
         | 
| 879 | 
            +
            ```
         | 
| 880 | 
            +
             | 
| 881 | 
            +
            ### #to_h
         | 
| 882 | 
            +
             | 
| 883 | 
            +
            `Struct#to_h` returns the ouput hash, with values coerced and any defaults populated.
         | 
| 884 | 
            +
             | 
| 885 | 
            +
            ```ruby
         | 
| 886 | 
            +
            class User
         | 
| 887 | 
            +
              include Parametrict::Struct
         | 
| 888 | 
            +
              schema do
         | 
| 889 | 
            +
                field(:name).type(:string)
         | 
| 890 | 
            +
                field(:age).type(:integer).default(30)
         | 
| 891 | 
            +
              end
         | 
| 892 | 
            +
            end
         | 
| 893 | 
            +
             | 
| 894 | 
            +
            user = User.new(name: "Joe")
         | 
| 895 | 
            +
            user.to_h # {name: "Joe", age: 30}
         | 
| 896 | 
            +
            ```
         | 
| 897 | 
            +
             | 
| 898 | 
            +
            ### Struct equality
         | 
| 899 | 
            +
             | 
| 900 | 
            +
            `Parametric::Struct` implements `#==()` to compare two structs Hash representation (same as `struct1.to_h.eql?(struct2.to_h)`.
         | 
| 901 | 
            +
             | 
| 902 | 
            +
            Users can override `#==()` in their own classes to do whatever they need.
         | 
| 903 | 
            +
             | 
| 746 904 | 
             
            ## Installation
         | 
| 747 905 |  | 
| 748 906 | 
             
            Add this line to your application's Gemfile:
         | 
    
        data/lib/parametric/dsl.rb
    CHANGED
    
    
    
        data/lib/parametric/field.rb
    CHANGED
    
    
    
        data/lib/parametric/schema.rb
    CHANGED
    
    | @@ -11,6 +11,11 @@ module Parametric | |
| 11 11 | 
             
                  @definitions << block if block_given?
         | 
| 12 12 | 
             
                  @default_field_policies = []
         | 
| 13 13 | 
             
                  @ignored_field_keys = []
         | 
| 14 | 
            +
                  @expansions = {}
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def schema
         | 
| 18 | 
            +
                  self
         | 
| 14 19 | 
             
                end
         | 
| 15 20 |  | 
| 16 21 | 
             
                def fields
         | 
| @@ -80,6 +85,11 @@ module Parametric | |
| 80 85 | 
             
                  end
         | 
| 81 86 | 
             
                end
         | 
| 82 87 |  | 
| 88 | 
            +
                def expand(exp, &block)
         | 
| 89 | 
            +
                  expansions[exp] = block
         | 
| 90 | 
            +
                  self
         | 
| 91 | 
            +
                end
         | 
| 92 | 
            +
             | 
| 83 93 | 
             
                def resolve(payload)
         | 
| 84 94 | 
             
                  context = Context.new
         | 
| 85 95 | 
             
                  output = coerce(payload, nil, context)
         | 
| @@ -112,10 +122,13 @@ module Parametric | |
| 112 122 | 
             
                def coerce(val, _, context)
         | 
| 113 123 | 
             
                  if val.is_a?(Array)
         | 
| 114 124 | 
             
                    val.map.with_index{|v, idx|
         | 
| 115 | 
            -
                       | 
| 125 | 
            +
                      subcontext = context.sub(idx)
         | 
| 126 | 
            +
                      out = coerce_one(v, subcontext)
         | 
| 127 | 
            +
                      resolve_expansions(v, out, subcontext)
         | 
| 116 128 | 
             
                    }
         | 
| 117 129 | 
             
                  else
         | 
| 118 | 
            -
                    coerce_one | 
| 130 | 
            +
                    out = coerce_one(val, context)
         | 
| 131 | 
            +
                    resolve_expansions(val, out, context)
         | 
| 119 132 | 
             
                  end
         | 
| 120 133 | 
             
                end
         | 
| 121 134 |  | 
| @@ -125,10 +138,10 @@ module Parametric | |
| 125 138 |  | 
| 126 139 | 
             
                private
         | 
| 127 140 |  | 
| 128 | 
            -
                attr_reader :default_field_policies, :ignored_field_keys
         | 
| 141 | 
            +
                attr_reader :default_field_policies, :ignored_field_keys, :expansions
         | 
| 129 142 |  | 
| 130 | 
            -
                def coerce_one(val, context)
         | 
| 131 | 
            -
                   | 
| 143 | 
            +
                def coerce_one(val, context, flds: fields)
         | 
| 144 | 
            +
                  flds.each_with_object({}) do |(_, field), m|
         | 
| 132 145 | 
             
                    r = field.resolve(val, context.sub(field.key))
         | 
| 133 146 | 
             
                    if r.eligible?
         | 
| 134 147 | 
             
                      m[field.key] = r.value
         | 
| @@ -136,6 +149,27 @@ module Parametric | |
| 136 149 | 
             
                  end
         | 
| 137 150 | 
             
                end
         | 
| 138 151 |  | 
| 152 | 
            +
                class MatchContext
         | 
| 153 | 
            +
                  def field(key)
         | 
| 154 | 
            +
                    Field.new(key.to_sym)
         | 
| 155 | 
            +
                  end
         | 
| 156 | 
            +
                end
         | 
| 157 | 
            +
             | 
| 158 | 
            +
                def resolve_expansions(payload, into, context)
         | 
| 159 | 
            +
                  expansions.each do |exp, block|
         | 
| 160 | 
            +
                    payload.each do |key, value|
         | 
| 161 | 
            +
                      if match = exp.match(key.to_s)
         | 
| 162 | 
            +
                        fld = MatchContext.new.instance_exec(match, &block)
         | 
| 163 | 
            +
                        if fld
         | 
| 164 | 
            +
                          into.update(coerce_one({fld.key => value}, context, flds: {fld.key => apply_default_field_policies_to(fld)}))
         | 
| 165 | 
            +
                        end
         | 
| 166 | 
            +
                      end
         | 
| 167 | 
            +
                    end
         | 
| 168 | 
            +
                  end
         | 
| 169 | 
            +
             | 
| 170 | 
            +
                  into
         | 
| 171 | 
            +
                end
         | 
| 172 | 
            +
             | 
| 139 173 | 
             
                def apply_default_field_policies_to(field)
         | 
| 140 174 | 
             
                  default_field_policies.reduce(field) {|f, policy_name| f.policy(policy_name) }
         | 
| 141 175 | 
             
                end
         | 
| @@ -0,0 +1,80 @@ | |
| 1 | 
            +
            require 'parametric/dsl'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Parametric
         | 
| 4 | 
            +
              module Struct
         | 
| 5 | 
            +
                def self.included(base)
         | 
| 6 | 
            +
                  base.send(:include, Parametric::DSL)
         | 
| 7 | 
            +
                  base.extend ClassMethods
         | 
| 8 | 
            +
                end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                def initialize(attrs = {})
         | 
| 11 | 
            +
                  @_results = self.class.schema.resolve(attrs)
         | 
| 12 | 
            +
                  @_graph = self.class.build(@_results.output)
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def valid?
         | 
| 16 | 
            +
                  !_results.errors.any?
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def errors
         | 
| 20 | 
            +
                  _results.errors
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                # returns a shallow copy.
         | 
| 24 | 
            +
                def to_h
         | 
| 25 | 
            +
                  _results.output.clone
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def ==(other)
         | 
| 29 | 
            +
                  other.respond_to?(:to_h) && other.to_h.eql?(to_h)
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def merge(attrs = {})
         | 
| 33 | 
            +
                  self.class.new(to_h.merge(attrs))
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                private
         | 
| 37 | 
            +
                attr_reader :_graph, :_results
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                module ClassMethods
         | 
| 40 | 
            +
                  # this hook is called after schema definition in DSL module
         | 
| 41 | 
            +
                  def after_define_schema(schema)
         | 
| 42 | 
            +
                    schema.fields.keys.each do |key|
         | 
| 43 | 
            +
                      define_method key do
         | 
| 44 | 
            +
                        _graph[key]
         | 
| 45 | 
            +
                      end
         | 
| 46 | 
            +
                    end
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  def build(attrs)
         | 
| 50 | 
            +
                    attrs.each_with_object({}) do |(k, v), obj|
         | 
| 51 | 
            +
                      obj[k] = wrap(k, v)
         | 
| 52 | 
            +
                    end
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  def wrap(key, value)
         | 
| 56 | 
            +
                    field = schema.fields[key]
         | 
| 57 | 
            +
                    return value unless field
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                    case value
         | 
| 60 | 
            +
                    when Hash
         | 
| 61 | 
            +
                      # find constructor for field
         | 
| 62 | 
            +
                      cons = field.meta_data[:schema]
         | 
| 63 | 
            +
                      if cons.kind_of?(Parametric::Schema)
         | 
| 64 | 
            +
                        klass = Class.new do
         | 
| 65 | 
            +
                          include Struct
         | 
| 66 | 
            +
                        end
         | 
| 67 | 
            +
                        klass.schema = cons
         | 
| 68 | 
            +
                        klass.after_define_schema(cons)
         | 
| 69 | 
            +
                        cons = klass
         | 
| 70 | 
            +
                      end
         | 
| 71 | 
            +
                      cons ? cons.new(value) : value.freeze
         | 
| 72 | 
            +
                    when Array
         | 
| 73 | 
            +
                      value.map{|v| wrap(key, v) }.freeze
         | 
| 74 | 
            +
                    else
         | 
| 75 | 
            +
                      value.freeze
         | 
| 76 | 
            +
                    end
         | 
| 77 | 
            +
                  end
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
              end
         | 
| 80 | 
            +
            end
         | 
    
        data/lib/parametric/version.rb
    CHANGED
    
    
    
        data/spec/expand_spec.rb
    ADDED
    
    | @@ -0,0 +1,29 @@ | |
| 1 | 
            +
            require 'spec_helper'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            describe Parametric::Schema do
         | 
| 4 | 
            +
              it "expands fields dynamically" do
         | 
| 5 | 
            +
                schema = described_class.new do
         | 
| 6 | 
            +
                  field(:title).type(:string).present
         | 
| 7 | 
            +
                  expand(/^attr_(.+)/) do |match|
         | 
| 8 | 
            +
                    field(match[1]).type(:string)
         | 
| 9 | 
            +
                  end
         | 
| 10 | 
            +
                  expand(/^validate_(.+)/) do |match|
         | 
| 11 | 
            +
                    field(match[1]).type(:string).present
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                out = schema.resolve({
         | 
| 16 | 
            +
                  title: "foo",
         | 
| 17 | 
            +
                  :"attr_Attribute 1" => "attr 1",
         | 
| 18 | 
            +
                  :"attr_Attribute 2" => "attr 2",
         | 
| 19 | 
            +
                  :"validate_valid_attr" => "valid",
         | 
| 20 | 
            +
                  :"validate_invalid_attr" => "",
         | 
| 21 | 
            +
                })
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                expect(out.output[:title]).to eq 'foo'
         | 
| 24 | 
            +
                expect(out.output[:"Attribute 1"]).to eq 'attr 1'
         | 
| 25 | 
            +
                expect(out.output[:"Attribute 2"]).to eq 'attr 2'
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                expect(out.errors['$.invalid_attr']).to eq ['is required and value must be present']
         | 
| 28 | 
            +
              end
         | 
| 29 | 
            +
            end
         | 
    
        data/spec/struct_spec.rb
    ADDED
    
    | @@ -0,0 +1,277 @@ | |
| 1 | 
            +
            require 'spec_helper'
         | 
| 2 | 
            +
            require 'parametric/struct'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            describe Parametric::Struct do
         | 
| 5 | 
            +
              it "works" do
         | 
| 6 | 
            +
                friend_class = Class.new do
         | 
| 7 | 
            +
                  include Parametric::Struct
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  schema do
         | 
| 10 | 
            +
                    field(:name).type(:string).present
         | 
| 11 | 
            +
                    field(:age).type(:integer)
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                klass = Class.new do
         | 
| 16 | 
            +
                  include Parametric::Struct
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  schema do
         | 
| 19 | 
            +
                    field(:title).type(:string).present
         | 
| 20 | 
            +
                    field(:friends).type(:array).default([]).schema friend_class
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                new_instance = klass.new
         | 
| 25 | 
            +
                expect(new_instance.title).to eq ''
         | 
| 26 | 
            +
                expect(new_instance.friends).to eq []
         | 
| 27 | 
            +
                expect(new_instance.valid?).to be false
         | 
| 28 | 
            +
                expect(new_instance.errors['$.title']).not_to be_nil
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                instance = klass.new({
         | 
| 31 | 
            +
                  title: 'foo',
         | 
| 32 | 
            +
                  friends: [
         | 
| 33 | 
            +
                    {name: 'Ismael', age: 40},
         | 
| 34 | 
            +
                    {name: 'Joe', age: 39},
         | 
| 35 | 
            +
                  ]
         | 
| 36 | 
            +
                })
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                expect(instance.title).to eq 'foo'
         | 
| 39 | 
            +
                expect(instance.friends.size).to eq 2
         | 
| 40 | 
            +
                expect(instance.friends.first.name).to eq 'Ismael'
         | 
| 41 | 
            +
                expect(instance.friends.first).to be_a friend_class
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                invalid_instance = klass.new({
         | 
| 44 | 
            +
                  friends: [
         | 
| 45 | 
            +
                    {name: 'Ismael', age: 40},
         | 
| 46 | 
            +
                    {age: 39},
         | 
| 47 | 
            +
                  ]
         | 
| 48 | 
            +
                })
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                expect(invalid_instance.valid?).to be false
         | 
| 51 | 
            +
                expect(invalid_instance.errors['$.title']).not_to be_nil
         | 
| 52 | 
            +
                expect(invalid_instance.errors['$.friends[1].name']).not_to be_nil
         | 
| 53 | 
            +
                expect(invalid_instance.friends[1].errors['$.name']).not_to be_nil
         | 
| 54 | 
            +
              end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
              it "is inmutable by default" do
         | 
| 57 | 
            +
                klass = Class.new do
         | 
| 58 | 
            +
                  include Parametric::Struct
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                  schema do
         | 
| 61 | 
            +
                    field(:title).type(:string).present
         | 
| 62 | 
            +
                    field(:friends).type(:array).default([])
         | 
| 63 | 
            +
                    field(:friend).type(:object)
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                instance = klass.new
         | 
| 68 | 
            +
                expect {
         | 
| 69 | 
            +
                  instance.title = "foo"
         | 
| 70 | 
            +
                }.to raise_error NoMethodError
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                expect {
         | 
| 73 | 
            +
                  instance.friends << 1
         | 
| 74 | 
            +
                }.to raise_error RuntimeError
         | 
| 75 | 
            +
              end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
              it "works with anonymous nested schemas" do
         | 
| 78 | 
            +
                klass = Class.new do
         | 
| 79 | 
            +
                  include Parametric::Struct
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                  schema do
         | 
| 82 | 
            +
                    field(:title).type(:string).present
         | 
| 83 | 
            +
                    field(:friends).type(:array).schema do
         | 
| 84 | 
            +
                      field(:age).type(:integer)
         | 
| 85 | 
            +
                    end
         | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
                end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                instance = klass.new({
         | 
| 90 | 
            +
                  title: 'foo',
         | 
| 91 | 
            +
                  friends: [
         | 
| 92 | 
            +
                    {age: 10},
         | 
| 93 | 
            +
                    {age: 39},
         | 
| 94 | 
            +
                  ]
         | 
| 95 | 
            +
                })
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                expect(instance.title).to eq 'foo'
         | 
| 98 | 
            +
                expect(instance.friends.size).to eq 2
         | 
| 99 | 
            +
                expect(instance.friends.first.age).to eq 10
         | 
| 100 | 
            +
              end
         | 
| 101 | 
            +
             | 
| 102 | 
            +
              it "wraps regular schemas in structs" do
         | 
| 103 | 
            +
                friend_schema = Parametric::Schema.new do
         | 
| 104 | 
            +
                  field(:name)
         | 
| 105 | 
            +
                end
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                klass = Class.new do
         | 
| 108 | 
            +
                  include Parametric::Struct
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                  schema do
         | 
| 111 | 
            +
                    field(:title).type(:string).present
         | 
| 112 | 
            +
                    field(:friends).type(:array).schema friend_schema
         | 
| 113 | 
            +
                  end
         | 
| 114 | 
            +
                end
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                instance = klass.new({
         | 
| 117 | 
            +
                  title: 'foo',
         | 
| 118 | 
            +
                  friends: [{name: 'Ismael'}]
         | 
| 119 | 
            +
                })
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                expect(instance.friends.first.name).to eq 'Ismael'
         | 
| 122 | 
            +
              end
         | 
| 123 | 
            +
             | 
| 124 | 
            +
              it "#to_h" do
         | 
| 125 | 
            +
                klass = Class.new do
         | 
| 126 | 
            +
                  include Parametric::Struct
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                  schema do
         | 
| 129 | 
            +
                    field(:title).type(:string).present
         | 
| 130 | 
            +
                    field(:friends).type(:array).schema do
         | 
| 131 | 
            +
                      field(:name).type(:string)
         | 
| 132 | 
            +
                      field(:age).type(:integer).default(20)
         | 
| 133 | 
            +
                    end
         | 
| 134 | 
            +
                  end
         | 
| 135 | 
            +
                end
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                instance = klass.new({
         | 
| 138 | 
            +
                  title: 'foo',
         | 
| 139 | 
            +
                  friends: [
         | 
| 140 | 
            +
                    {name: 'Jane'},
         | 
| 141 | 
            +
                    {name: 'Joe', age: '39'},
         | 
| 142 | 
            +
                  ]
         | 
| 143 | 
            +
                })
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                expect(instance.to_h).to eq({
         | 
| 146 | 
            +
                  title: 'foo',
         | 
| 147 | 
            +
                  friends: [
         | 
| 148 | 
            +
                    {name: 'Jane', age: 20},
         | 
| 149 | 
            +
                    {name: 'Joe', age: 39},
         | 
| 150 | 
            +
                  ]
         | 
| 151 | 
            +
                })
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                new_instance = klass.new(instance.to_h)
         | 
| 154 | 
            +
                expect(new_instance.title).to eq 'foo'
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                # it returns a copy so we can't break things!
         | 
| 157 | 
            +
                data = new_instance.to_h
         | 
| 158 | 
            +
                data[:title] = 'nope'
         | 
| 159 | 
            +
                expect(new_instance.to_h[:title]).to eq 'foo'
         | 
| 160 | 
            +
              end
         | 
| 161 | 
            +
             | 
| 162 | 
            +
              it "works with inheritance" do
         | 
| 163 | 
            +
                klass = Class.new do
         | 
| 164 | 
            +
                  include Parametric::Struct
         | 
| 165 | 
            +
             | 
| 166 | 
            +
                  schema do
         | 
| 167 | 
            +
                    field(:title).type(:string).present
         | 
| 168 | 
            +
                    field(:friends).type(:array).schema do
         | 
| 169 | 
            +
                      field(:name).type(:string)
         | 
| 170 | 
            +
                      field(:age).type(:integer).default(20)
         | 
| 171 | 
            +
                    end
         | 
| 172 | 
            +
                  end
         | 
| 173 | 
            +
                end
         | 
| 174 | 
            +
             | 
| 175 | 
            +
                subclass = Class.new(klass) do
         | 
| 176 | 
            +
                  schema do
         | 
| 177 | 
            +
                    field(:email)
         | 
| 178 | 
            +
                  end
         | 
| 179 | 
            +
                end
         | 
| 180 | 
            +
             | 
| 181 | 
            +
                instance = subclass.new(
         | 
| 182 | 
            +
                  title: 'foo',
         | 
| 183 | 
            +
                  email: 'email@me.com',
         | 
| 184 | 
            +
                  friends: [
         | 
| 185 | 
            +
                    {name: 'Jane', age: 20},
         | 
| 186 | 
            +
                    {name: 'Joe', age: 39},
         | 
| 187 | 
            +
                  ]
         | 
| 188 | 
            +
                )
         | 
| 189 | 
            +
             | 
| 190 | 
            +
                expect(instance.title).to eq 'foo'
         | 
| 191 | 
            +
                expect(instance.email).to eq 'email@me.com'
         | 
| 192 | 
            +
                expect(instance.friends.size).to eq 2
         | 
| 193 | 
            +
              end
         | 
| 194 | 
            +
             | 
| 195 | 
            +
              it "implements deep struct equality" do
         | 
| 196 | 
            +
                klass = Class.new do
         | 
| 197 | 
            +
                  include Parametric::Struct
         | 
| 198 | 
            +
             | 
| 199 | 
            +
                  schema do
         | 
| 200 | 
            +
                    field(:title).type(:string).present
         | 
| 201 | 
            +
                    field(:friends).type(:array).schema do
         | 
| 202 | 
            +
                      field(:age).type(:integer)
         | 
| 203 | 
            +
                    end
         | 
| 204 | 
            +
                  end
         | 
| 205 | 
            +
                end
         | 
| 206 | 
            +
             | 
| 207 | 
            +
                s1 = klass.new({
         | 
| 208 | 
            +
                  title: 'foo',
         | 
| 209 | 
            +
                  friends: [
         | 
| 210 | 
            +
                    {age: 10},
         | 
| 211 | 
            +
                    {age: 39},
         | 
| 212 | 
            +
                  ]
         | 
| 213 | 
            +
                })
         | 
| 214 | 
            +
             | 
| 215 | 
            +
             | 
| 216 | 
            +
                s2 = klass.new({
         | 
| 217 | 
            +
                  title: 'foo',
         | 
| 218 | 
            +
                  friends: [
         | 
| 219 | 
            +
                    {age: 10},
         | 
| 220 | 
            +
                    {age: 39},
         | 
| 221 | 
            +
                  ]
         | 
| 222 | 
            +
                })
         | 
| 223 | 
            +
             | 
| 224 | 
            +
                s3 = klass.new({
         | 
| 225 | 
            +
                  title: 'foo',
         | 
| 226 | 
            +
                  friends: [
         | 
| 227 | 
            +
                    {age: 11},
         | 
| 228 | 
            +
                    {age: 39},
         | 
| 229 | 
            +
                  ]
         | 
| 230 | 
            +
                })
         | 
| 231 | 
            +
             | 
| 232 | 
            +
                s4 = klass.new({
         | 
| 233 | 
            +
                  title: 'bar',
         | 
| 234 | 
            +
                  friends: [
         | 
| 235 | 
            +
                    {age: 10},
         | 
| 236 | 
            +
                    {age: 39},
         | 
| 237 | 
            +
                  ]
         | 
| 238 | 
            +
                })
         | 
| 239 | 
            +
             | 
| 240 | 
            +
                expect(s1 == s2).to be true
         | 
| 241 | 
            +
                expect(s1 == s3).to be false
         | 
| 242 | 
            +
                expect(s1 == s4).to be false
         | 
| 243 | 
            +
              end
         | 
| 244 | 
            +
             | 
| 245 | 
            +
              it "#merge returns a new instance" do
         | 
| 246 | 
            +
                klass = Class.new do
         | 
| 247 | 
            +
                  include Parametric::Struct
         | 
| 248 | 
            +
             | 
| 249 | 
            +
                  schema do
         | 
| 250 | 
            +
                    field(:title).type(:string).present
         | 
| 251 | 
            +
                    field(:desc)
         | 
| 252 | 
            +
                    field(:friends).type(:array).schema do
         | 
| 253 | 
            +
                      field(:name).type(:string)
         | 
| 254 | 
            +
                    end
         | 
| 255 | 
            +
                  end
         | 
| 256 | 
            +
                end
         | 
| 257 | 
            +
             | 
| 258 | 
            +
                original = klass.new(
         | 
| 259 | 
            +
                  title: 'foo',
         | 
| 260 | 
            +
                  desc: 'no change',
         | 
| 261 | 
            +
                  friends: [{name: 'joe'}]
         | 
| 262 | 
            +
                )
         | 
| 263 | 
            +
             | 
| 264 | 
            +
                copy = original.merge(
         | 
| 265 | 
            +
                  title: 'bar',
         | 
| 266 | 
            +
                  friends: [{name: 'jane'}]
         | 
| 267 | 
            +
                )
         | 
| 268 | 
            +
             | 
| 269 | 
            +
                expect(original.title).to eq 'foo'
         | 
| 270 | 
            +
                expect(original.desc).to eq 'no change'
         | 
| 271 | 
            +
                expect(original.friends.first.name).to eq 'joe'
         | 
| 272 | 
            +
             | 
| 273 | 
            +
                expect(copy.title).to eq 'bar'
         | 
| 274 | 
            +
                expect(copy.desc).to eq 'no change'
         | 
| 275 | 
            +
                expect(copy.friends.first.name).to eq 'jane'
         | 
| 276 | 
            +
              end
         | 
| 277 | 
            +
            end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: parametric
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.2.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Ismael Celis
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date:  | 
| 11 | 
            +
            date: 2018-08-06 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: bundler
         | 
| @@ -96,14 +96,17 @@ files: | |
| 96 96 | 
             
            - lib/parametric/registry.rb
         | 
| 97 97 | 
             
            - lib/parametric/results.rb
         | 
| 98 98 | 
             
            - lib/parametric/schema.rb
         | 
| 99 | 
            +
            - lib/parametric/struct.rb
         | 
| 99 100 | 
             
            - lib/parametric/version.rb
         | 
| 100 101 | 
             
            - parametric.gemspec
         | 
| 101 102 | 
             
            - spec/dsl_spec.rb
         | 
| 103 | 
            +
            - spec/expand_spec.rb
         | 
| 102 104 | 
             
            - spec/field_spec.rb
         | 
| 103 105 | 
             
            - spec/policies_spec.rb
         | 
| 104 106 | 
             
            - spec/schema_spec.rb
         | 
| 105 107 | 
             
            - spec/schema_walk_spec.rb
         | 
| 106 108 | 
             
            - spec/spec_helper.rb
         | 
| 109 | 
            +
            - spec/struct_spec.rb
         | 
| 107 110 | 
             
            - spec/validators_spec.rb
         | 
| 108 111 | 
             
            homepage: ''
         | 
| 109 112 | 
             
            licenses:
         | 
| @@ -125,16 +128,18 @@ required_rubygems_version: !ruby/object:Gem::Requirement | |
| 125 128 | 
             
                  version: '0'
         | 
| 126 129 | 
             
            requirements: []
         | 
| 127 130 | 
             
            rubyforge_project: 
         | 
| 128 | 
            -
            rubygems_version: 2. | 
| 131 | 
            +
            rubygems_version: 2.7.6
         | 
| 129 132 | 
             
            signing_key: 
         | 
| 130 133 | 
             
            specification_version: 4
         | 
| 131 134 | 
             
            summary: DSL for declaring allowed parameters with options, regexp patern and default
         | 
| 132 135 | 
             
              values.
         | 
| 133 136 | 
             
            test_files:
         | 
| 134 137 | 
             
            - spec/dsl_spec.rb
         | 
| 138 | 
            +
            - spec/expand_spec.rb
         | 
| 135 139 | 
             
            - spec/field_spec.rb
         | 
| 136 140 | 
             
            - spec/policies_spec.rb
         | 
| 137 141 | 
             
            - spec/schema_spec.rb
         | 
| 138 142 | 
             
            - spec/schema_walk_spec.rb
         | 
| 139 143 | 
             
            - spec/spec_helper.rb
         | 
| 144 | 
            +
            - spec/struct_spec.rb
         | 
| 140 145 | 
             
            - spec/validators_spec.rb
         |