parametric 0.0.1 → 0.2.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,194 @@
1
+ require "parametric/context"
2
+ require "parametric/results"
3
+ require "parametric/field"
4
+
5
+ module Parametric
6
+ class Schema
7
+ def initialize(options = {}, &block)
8
+ @options = options
9
+ @fields = {}
10
+ @definitions = []
11
+ @definitions << block if block_given?
12
+ @default_field_policies = []
13
+ @ignored_field_keys = []
14
+ @expansions = {}
15
+ end
16
+
17
+ def schema
18
+ self
19
+ end
20
+
21
+ def fields
22
+ apply!
23
+ @fields
24
+ end
25
+
26
+ def policy(*names, &block)
27
+ @default_field_policies = names
28
+ definitions << block if block_given?
29
+
30
+ self
31
+ end
32
+
33
+ def ignore(*field_keys, &block)
34
+ @ignored_field_keys += field_keys
35
+ @ignored_field_keys.uniq!
36
+
37
+ definitions << block if block_given?
38
+
39
+ self
40
+ end
41
+
42
+ def clone
43
+ instance = self.class.new(options)
44
+ copy_into instance
45
+ end
46
+
47
+ def merge(other_schema = nil, &block)
48
+ raise ArgumentError, '#merge takes either a schema instance or a block' if other_schema.nil? && !block_given?
49
+
50
+ if other_schema
51
+ instance = self.class.new(options.merge(other_schema.options))
52
+ copy_into(instance)
53
+ other_schema.copy_into(instance)
54
+ else
55
+ merge(self.class.new(&block))
56
+ end
57
+ end
58
+
59
+ def copy_into(instance)
60
+ instance.policy(*default_field_policies) if default_field_policies.any?
61
+
62
+ definitions.each do |d|
63
+ instance.definitions << d
64
+ end
65
+
66
+ instance.ignore *ignored_field_keys
67
+ instance
68
+ end
69
+
70
+ def structure
71
+ fields.each_with_object({}) do |(_, field), obj|
72
+ meta = field.meta_data.dup
73
+ sc = meta.delete(:schema)
74
+ meta[:structure] = sc.structure if sc
75
+ obj[field.key] = meta
76
+ end
77
+ end
78
+
79
+ def field(field_or_key)
80
+ f, key = if field_or_key.kind_of?(Field)
81
+ [field_or_key, field_or_key.key]
82
+ else
83
+ [Field.new(field_or_key), field_or_key.to_sym]
84
+ end
85
+
86
+ if ignored_field_keys.include?(f.key)
87
+ f
88
+ else
89
+ @fields[key] = apply_default_field_policies_to(f)
90
+ end
91
+ end
92
+
93
+ def expand(exp, &block)
94
+ expansions[exp] = block
95
+ self
96
+ end
97
+
98
+ def resolve(payload)
99
+ context = Context.new
100
+ output = coerce(payload, nil, context)
101
+ Results.new(output, context.errors)
102
+ end
103
+
104
+ def walk(meta_key = nil, &visitor)
105
+ r = visit(meta_key, &visitor)
106
+ Results.new(r, {})
107
+ end
108
+
109
+ def eligible?(value, key, payload)
110
+ payload.key? key
111
+ end
112
+
113
+ def valid?(*_)
114
+ true
115
+ end
116
+
117
+ def meta_data
118
+ {}
119
+ end
120
+
121
+ def visit(meta_key = nil, &visitor)
122
+ fields.each_with_object({}) do |(_, field), m|
123
+ m[field.key] = field.visit(meta_key, &visitor)
124
+ end
125
+ end
126
+
127
+ def coerce(val, _, context)
128
+ if val.is_a?(Array)
129
+ val.map.with_index{|v, idx|
130
+ subcontext = context.sub(idx)
131
+ out = coerce_one(v, subcontext)
132
+ resolve_expansions(v, out, subcontext)
133
+ }
134
+ else
135
+ out = coerce_one(val, context)
136
+ resolve_expansions(val, out, context)
137
+ end
138
+ end
139
+
140
+ protected
141
+
142
+ attr_reader :definitions, :options
143
+
144
+ private
145
+
146
+ attr_reader :default_field_policies, :ignored_field_keys, :expansions
147
+
148
+ def coerce_one(val, context, flds: fields)
149
+ flds.each_with_object({}) do |(_, field), m|
150
+ r = field.resolve(val, context.sub(field.key))
151
+ if r.eligible?
152
+ m[field.key] = r.value
153
+ end
154
+ end
155
+ end
156
+
157
+ class MatchContext
158
+ def field(key)
159
+ Field.new(key.to_sym)
160
+ end
161
+ end
162
+
163
+ def resolve_expansions(payload, into, context)
164
+ expansions.each do |exp, block|
165
+ payload.each do |key, value|
166
+ if match = exp.match(key.to_s)
167
+ fld = MatchContext.new.instance_exec(match, &block)
168
+ if fld
169
+ into.update(coerce_one({fld.key => value}, context, flds: {fld.key => apply_default_field_policies_to(fld)}))
170
+ end
171
+ end
172
+ end
173
+ end
174
+
175
+ into
176
+ end
177
+
178
+ def apply_default_field_policies_to(field)
179
+ default_field_policies.reduce(field) {|f, policy_name| f.policy(policy_name) }
180
+ end
181
+
182
+ def apply!
183
+ return if @applied
184
+ definitions.each do |d|
185
+ if d.arity == 2 # pass schema instance and options, preserve block context
186
+ d.call(self, options)
187
+ else # run block in context of current instance
188
+ self.instance_exec(options, &d)
189
+ end
190
+ end
191
+ @applied = true
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,102 @@
1
+ require 'parametric/dsl'
2
+
3
+ module Parametric
4
+ class InvalidStructError < ArgumentError
5
+ attr_reader :errors
6
+ def initialize(struct)
7
+ @errors = struct.errors
8
+ msg = @errors.map do |k, strings|
9
+ "#{k} #{strings.join(', ')}"
10
+ end.join('. ')
11
+ super "#{struct.class} is not a valid struct: #{msg}"
12
+ end
13
+ end
14
+
15
+ module Struct
16
+ def self.included(base)
17
+ base.send(:include, Parametric::DSL)
18
+ base.extend ClassMethods
19
+ end
20
+
21
+ def initialize(attrs = {})
22
+ @_results = self.class.schema.resolve(attrs)
23
+ @_graph = self.class.build(@_results.output)
24
+ end
25
+
26
+ def valid?
27
+ !_results.errors.any?
28
+ end
29
+
30
+ def errors
31
+ _results.errors
32
+ end
33
+
34
+ # returns a shallow copy.
35
+ def to_h
36
+ _results.output.clone
37
+ end
38
+
39
+ def ==(other)
40
+ other.respond_to?(:to_h) && other.to_h.eql?(to_h)
41
+ end
42
+
43
+ def merge(attrs = {})
44
+ self.class.new(to_h.merge(attrs))
45
+ end
46
+
47
+ private
48
+ attr_reader :_graph, :_results
49
+
50
+ module ClassMethods
51
+ def new!(attrs = {})
52
+ st = new(attrs)
53
+ raise InvalidStructError.new(st) unless st.valid?
54
+ st
55
+ end
56
+
57
+ # this hook is called after schema definition in DSL module
58
+ def parametric_after_define_schema(schema)
59
+ schema.fields.keys.each do |key|
60
+ define_method key do
61
+ _graph[key]
62
+ end
63
+ end
64
+ end
65
+
66
+ def build(attrs)
67
+ attrs.each_with_object({}) do |(k, v), obj|
68
+ obj[k] = wrap(k, v)
69
+ end
70
+ end
71
+
72
+ def parametric_build_class_for_child(key, child_schema)
73
+ klass = Class.new do
74
+ include Struct
75
+ end
76
+ klass.schema = child_schema
77
+ klass
78
+ end
79
+
80
+ def wrap(key, value)
81
+ field = schema.fields[key]
82
+ return value unless field
83
+
84
+ case value
85
+ when Hash
86
+ # find constructor for field
87
+ cons = field.meta_data[:schema]
88
+ if cons.kind_of?(Parametric::Schema)
89
+ klass = parametric_build_class_for_child(key, cons)
90
+ klass.parametric_after_define_schema(cons)
91
+ cons = klass
92
+ end
93
+ cons ? cons.new(value) : value.freeze
94
+ when Array
95
+ value.map{|v| wrap(key, v) }.freeze
96
+ else
97
+ value.freeze
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -1,3 +1,3 @@
1
1
  module Parametric
2
- VERSION = "0.0.1"
2
+ VERSION = "0.2.10"
3
3
  end
data/lib/parametric.rb CHANGED
@@ -1,9 +1,20 @@
1
+ require "parametric/version"
2
+ require "parametric/registry"
3
+ require "parametric/field"
4
+ require "parametric/results"
5
+ require "parametric/schema"
6
+ require "parametric/context"
7
+
1
8
  module Parametric
2
9
 
10
+ def self.registry
11
+ @registry ||= Registry.new
12
+ end
13
+
14
+ def self.policy(name, plcy = nil, &block)
15
+ registry.policy name, plcy, &block
16
+ end
3
17
  end
4
18
 
5
- require "parametric/utils"
6
- require "parametric/version"
7
- require "parametric/policies"
8
- require "parametric/params"
9
- require "parametric/hash"
19
+ require 'parametric/default_types'
20
+ require 'parametric/policies'
data/parametric.gemspec CHANGED
@@ -14,11 +14,11 @@ Gem::Specification.new do |spec|
14
14
  spec.license = "MIT"
15
15
 
16
16
  spec.files = `git ls-files -z`.split("\x0")
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
17
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
18
  spec.require_paths = ["lib"]
20
19
 
21
20
  spec.add_development_dependency "bundler", "~> 1.5"
22
21
  spec.add_development_dependency "rake"
23
- spec.add_development_dependency "rspec"
22
+ spec.add_development_dependency "rspec", '3.4.0'
23
+ spec.add_development_dependency "byebug"
24
24
  end
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'custom block validator' do
4
+ Parametric.policy :validate_if do
5
+ eligible do |options, value, key, payload|
6
+ options.all? do |key, value|
7
+ payload[key] == value
8
+ end
9
+ end
10
+ end
11
+
12
+ it 'works if I just define an :eligible block' do
13
+ schema = Parametric::Schema.new do
14
+ field(:name).policy(:validate_if, age: 40).present
15
+ field(:age).type(:integer)
16
+ end
17
+
18
+ expect(schema.resolve(age: 30).errors.any?).to be false
19
+ expect(schema.resolve(age: 40).errors.any?).to be true # name is missing
20
+ end
21
+ end
data/spec/dsl_spec.rb ADDED
@@ -0,0 +1,176 @@
1
+ require 'spec_helper'
2
+ require "parametric/dsl"
3
+
4
+ describe "classes including DSL module" do
5
+ class Parent
6
+ include Parametric::DSL
7
+
8
+ schema :extras, search_type: :string do |opts|
9
+ field(:search).policy(opts[:search_type])
10
+ end
11
+
12
+ schema(age_type: :integer) do |opts|
13
+ field(:title).policy(:string)
14
+ field(:age).policy(opts[:age_type])
15
+ end
16
+ end
17
+
18
+ class Child < Parent
19
+ schema :extras do
20
+ field(:query).type(:string)
21
+ end
22
+
23
+ schema(age_type: :string) do
24
+ field(:description).policy(:string)
25
+ end
26
+ end
27
+
28
+ class GrandChild < Child
29
+ schema :extras, search_type: :integer
30
+
31
+ schema(age_type: :integer)
32
+ end
33
+
34
+ describe "#schema" do
35
+ let(:input) {
36
+ {
37
+ title: "A title",
38
+ age: 38,
39
+ description: "A description"
40
+ }
41
+ }
42
+
43
+ it "merges parent's schema into child's" do
44
+ parent_output = Parent.schema.resolve(input).output
45
+ child_output = Child.schema.resolve(input).output
46
+
47
+ expect(parent_output.keys).to match_array([:title, :age])
48
+ expect(parent_output[:title]).to eq "A title"
49
+ expect(parent_output[:age]).to eq 38
50
+
51
+ expect(child_output.keys).to match_array([:title, :age, :description])
52
+ expect(child_output[:title]).to eq "A title"
53
+ expect(child_output[:age]).to eq "38"
54
+ expect(child_output[:description]).to eq "A description"
55
+
56
+ # named schema
57
+ parent_output = Parent.schema(:extras).resolve(search: 10, query: 'foo').output
58
+ child_output = Child.schema(:extras).resolve(search: 10, query: 'foo').output
59
+
60
+ expect(parent_output.keys).to match_array([:search])
61
+ expect(parent_output[:search]).to eq "10"
62
+ expect(child_output.keys).to match_array([:search, :query])
63
+ expect(child_output[:search]).to eq "10"
64
+ expect(child_output[:query]).to eq "foo"
65
+ end
66
+
67
+ it "inherits options" do
68
+ grand_child_output = GrandChild.schema.resolve(input).output
69
+
70
+ expect(grand_child_output.keys).to match_array([:title, :age, :description])
71
+ expect(grand_child_output[:title]).to eq "A title"
72
+ expect(grand_child_output[:age]).to eq 38
73
+ expect(grand_child_output[:description]).to eq "A description"
74
+
75
+ # named schema
76
+ grand_child_output = GrandChild.schema(:extras).resolve(search: "100", query: "bar").output
77
+ expect(grand_child_output.keys).to match_array([:search, :query])
78
+ expect(grand_child_output[:search]).to eq 100
79
+ end
80
+ end
81
+
82
+ describe "inheriting schema policy" do
83
+ let!(:a) {
84
+ Class.new do
85
+ include Parametric::DSL
86
+
87
+ schema.policy(:present) do
88
+ field(:title).policy(:string)
89
+ end
90
+ end
91
+ }
92
+
93
+ let!(:b) {
94
+ Class.new(a)
95
+ }
96
+
97
+ it "inherits policy" do
98
+ results = a.schema.resolve({})
99
+ expect(results.errors["$.title"]).not_to be_empty
100
+
101
+ results = b.schema.resolve({})
102
+ expect(results.errors["$.title"]).not_to be_empty
103
+ end
104
+ end
105
+
106
+ describe "overriding schema policy" do
107
+ let!(:a) {
108
+ Class.new do
109
+ include Parametric::DSL
110
+
111
+ schema.policy(:present) do
112
+ field(:title).policy(:string)
113
+ end
114
+ end
115
+ }
116
+
117
+ let!(:b) {
118
+ Class.new(a) do
119
+ schema.policy(:declared)
120
+ end
121
+ }
122
+
123
+ it "does not mutate parent schema" do
124
+ results = a.schema.resolve({})
125
+ expect(results.errors).not_to be_empty
126
+
127
+ results = b.schema.resolve({})
128
+ expect(results.errors).to be_empty
129
+ end
130
+ end
131
+
132
+ describe "removes fields defined in the parent class" do
133
+ let!(:a) {
134
+ Class.new do
135
+ include Parametric::DSL
136
+
137
+ schema do
138
+ field(:title).policy(:string)
139
+ end
140
+ end
141
+ }
142
+
143
+ let!(:b) {
144
+ Class.new(a) do
145
+ schema.ignore(:title) do
146
+ field(:age)
147
+ end
148
+ end
149
+ }
150
+
151
+ it "removes inherited field from child class" do
152
+ results = a.schema.resolve({title: "Mr.", age: 20})
153
+ expect(results.output).to eq({title: "Mr."})
154
+
155
+ results = b.schema.resolve({title: "Mr.", age: 20})
156
+ expect(results.output).to eq({age: 20})
157
+ end
158
+ end
159
+
160
+ describe "passing other schema or form in definition" do
161
+ it 'applies schema' do
162
+ a = Parametric::Schema.new do
163
+ field(:name).policy(:string)
164
+ field(:age).policy(:integer).default(40)
165
+ end
166
+ b = Class.new do
167
+ include Parametric::DSL
168
+ schema a
169
+ end
170
+
171
+ results = b.schema.resolve(name: 'Neil')
172
+ expect(results.output).to eq({name: 'Neil', age: 40})
173
+ end
174
+ end
175
+ end
176
+
@@ -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