parametric 0.0.5 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,253 @@
1
+ require 'spec_helper'
2
+
3
+ describe Parametric::Schema do
4
+ before do
5
+ Parametric.policy :flexible_bool do
6
+ coerce do |v, k, c|
7
+ case v
8
+ when '1', 'true', 'TRUE', true
9
+ true
10
+ else
11
+ false
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ subject do
18
+ described_class.new do
19
+ field(:title).policy(:string).present
20
+ field(:price).policy(:integer).meta(label: "A price")
21
+ field(:status).policy(:string).options(['visible', 'hidden'])
22
+ field(:tags).policy(:split).policy(:array)
23
+ field(:description).policy(:string)
24
+ field(:variants).policy(:array).schema do
25
+ field(:name).policy(:string).present
26
+ field(:sku)
27
+ field(:stock).policy(:integer).default(1)
28
+ field(:available_if_no_stock).policy(:boolean).policy(:flexible_bool).default(false)
29
+ end
30
+ end
31
+ end
32
+
33
+ describe "#structure" do
34
+ it "represents data structure and meta data" do
35
+ sc = subject.structure
36
+ expect(sc[:title][:present]).to be true
37
+ expect(sc[:title][:type]).to eq :string
38
+ expect(sc[:price][:type]).to eq :integer
39
+ expect(sc[:price][:label]).to eq "A price"
40
+ expect(sc[:variants][:type]).to eq :array
41
+ sc[:variants][:structure].tap do |sc|
42
+ expect(sc[:name][:type]).to eq :string
43
+ expect(sc[:name][:present]).to be true
44
+ expect(sc[:stock][:default]).to eq 1
45
+ end
46
+ end
47
+ end
48
+
49
+ def resolve(schema, payload, &block)
50
+ yield schema.resolve(payload)
51
+ end
52
+
53
+ def test_schema(schema, payload, result)
54
+ resolve(schema, payload) do |results|
55
+ expect(results.output).to eq result
56
+ end
57
+ end
58
+
59
+ it 'works' do
60
+ test_schema(subject, {
61
+ title: 'iPhone 6 Plus',
62
+ price: '100.0',
63
+ status: 'visible',
64
+ tags: 'tag1, tag2',
65
+ description: 'A description',
66
+ variants: [{name: 'v1', sku: 'ABC', stock: '10', available_if_no_stock: true}]
67
+ },
68
+ {
69
+ title: 'iPhone 6 Plus',
70
+ price: 100,
71
+ status: 'visible',
72
+ tags: ['tag1', 'tag2'],
73
+ description: 'A description',
74
+ variants: [{name: 'v1', sku: 'ABC', stock: 10, available_if_no_stock: true}]
75
+ })
76
+
77
+ test_schema(subject, {
78
+ title: 'iPhone 6 Plus',
79
+ variants: [{name: 'v1', available_if_no_stock: '1'}]
80
+ },
81
+ {
82
+ title: 'iPhone 6 Plus',
83
+ variants: [{name: 'v1', stock: 1, available_if_no_stock: true}]
84
+ })
85
+
86
+ resolve(subject, {}) do |results|
87
+ expect(results.valid?).to be false
88
+ expect(results.errors['$.title']).not_to be_nil
89
+ expect(results.errors['$.variants']).to be_nil
90
+ expect(results.errors['$.status']).to be_nil
91
+ end
92
+
93
+ resolve(subject, {title: 'Foobar', variants: [{name: 'v1'}, {sku: '345'}]}) do |results|
94
+ expect(results.valid?).to be false
95
+ expect(results.errors['$.variants[1].name']).not_to be_nil
96
+ end
97
+ end
98
+
99
+ describe "#policy" do
100
+ it "applies policy to all fields" do
101
+ subject.policy(:declared)
102
+
103
+ resolve(subject, {}) do |results|
104
+ expect(results.valid?).to be true
105
+ expect(results.errors.keys).to be_empty
106
+ end
107
+ end
108
+
109
+ it "replaces previous policies" do
110
+ subject.policy(:declared)
111
+ subject.policy(:present)
112
+
113
+ resolve(subject, {title: "hello"}) do |results|
114
+ expect(results.valid?).to be false
115
+ expect(results.errors.keys).to match_array(%w(
116
+ $.price
117
+ $.status
118
+ $.tags
119
+ $.description
120
+ $.variants
121
+ ))
122
+ end
123
+ end
124
+
125
+ it "applies :noop policy to all fields" do
126
+ subject.policy(:noop)
127
+
128
+ resolve(subject, {}) do |results|
129
+ expect(results.valid?).to be false
130
+ expect(results.errors['$.title']).not_to be_nil
131
+ end
132
+ end
133
+ end
134
+
135
+ describe "#merge" do
136
+ context "no options" do
137
+ let!(:schema1) {
138
+ described_class.new do
139
+ field(:title).policy(:string).present
140
+ field(:price).policy(:integer)
141
+ end
142
+ }
143
+
144
+ let!(:schema2) {
145
+ described_class.new do
146
+ field(:price).policy(:string)
147
+ field(:description).policy(:string)
148
+ end
149
+ }
150
+
151
+ it "returns a new schema adding new fields and updating existing ones" do
152
+ new_schema = schema1.merge(schema2)
153
+ expect(new_schema.fields.keys).to match_array([:title, :price, :description])
154
+
155
+ # did not mutate original
156
+ expect(schema1.fields[:price].meta_data[:type]).to eq :integer
157
+
158
+ expect(new_schema.fields[:title].meta_data[:type]).to eq :string
159
+ expect(new_schema.fields[:price].meta_data[:type]).to eq :string
160
+ end
161
+ end
162
+
163
+ context "with options" do
164
+ let!(:schema1) {
165
+ described_class.new(price_type: :integer, label: "Foo") do |opts|
166
+ field(:title).policy(:string).present
167
+ field(:price).policy(opts[:price_type]).meta(label: opts[:label])
168
+ end
169
+ }
170
+
171
+ let!(:schema2) {
172
+ described_class.new(price_type: :string) do
173
+ field(:description).policy(:string)
174
+ end
175
+ }
176
+
177
+ it "inherits options" do
178
+ new_schema = schema1.merge(schema2)
179
+ expect(new_schema.fields[:price].meta_data[:type]).to eq :string
180
+ expect(new_schema.fields[:price].meta_data[:label]).to eq "Foo"
181
+ end
182
+
183
+ it "re-applies blocks with new options" do
184
+ new_schema = schema1.merge(schema2)
185
+ expect(new_schema.fields.keys).to match_array([:title, :price, :description])
186
+
187
+ # did not mutate original
188
+ expect(schema1.fields[:price].meta_data[:type]).to eq :integer
189
+
190
+ expect(new_schema.fields[:title].meta_data[:type]).to eq :string
191
+ expect(new_schema.fields[:price].meta_data[:type]).to eq :string
192
+ end
193
+ end
194
+ end
195
+
196
+ describe "#clone" do
197
+ let!(:schema1) {
198
+ described_class.new do |opts|
199
+ field(:id).present
200
+ field(:title).policy(:string).present
201
+ field(:price)
202
+ end
203
+ }
204
+
205
+ it "returns a copy that can be further manipulated" do
206
+ schema2 = schema1.clone.policy(:declared).ignore(:id)
207
+ expect(schema1.fields.keys).to match_array([:id, :title, :price])
208
+ expect(schema2.fields.keys).to match_array([:title, :price])
209
+
210
+ results1 = schema1.resolve(id: "abc", price: 100)
211
+ expect(results1.errors.keys).to eq ["$.title"]
212
+
213
+ results2 = schema2.resolve(id: "abc", price: 100)
214
+ expect(results2.errors.keys).to eq []
215
+ end
216
+ end
217
+
218
+ describe "#ignore" do
219
+ it "ignores fields" do
220
+ s1 = described_class.new.ignore(:title, :status) do
221
+ field(:status)
222
+ field(:title).policy(:string).present
223
+ field(:price).policy(:integer)
224
+ end
225
+
226
+ output = s1.resolve(status: "draft", title: "foo", price: "100").output
227
+ expect(output).to eq({price: 100})
228
+ end
229
+
230
+ it "ignores when merging" do
231
+ s1 = described_class.new do
232
+ field(:status)
233
+ field(:title).policy(:string).present
234
+ end
235
+
236
+ s1 = described_class.new.ignore(:title, :status) do
237
+ field(:price).policy(:integer)
238
+ end
239
+
240
+ output = s1.resolve(title: "foo", status: "draft", price: "100").output
241
+ expect(output).to eq({price: 100})
242
+ end
243
+
244
+ it "returns self so it can be chained" do
245
+ s1 = described_class.new do
246
+ field(:status)
247
+ field(:title).policy(:string).present
248
+ end
249
+
250
+ expect(s1.ignore(:status)).to eq s1
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Schema#walk' do
4
+ let(:schema) do
5
+ Parametric::Schema.new do
6
+ field(:title).meta(example: 'a title', label: 'custom title')
7
+ field(:tags).policy(:array).meta(example: ['tag1', 'tag2'], label: 'comma-separated tags')
8
+ field(:friends).policy(:array).schema do
9
+ field(:name).meta(example: 'a friend', label: 'friend full name')
10
+ field(:age).meta(example: 34, label: 'age')
11
+ end
12
+ end
13
+ end
14
+
15
+ it "recursively walks the schema and collects meta data" do
16
+ results = schema.walk(:label)
17
+ expect(results.output).to eq({
18
+ title: 'custom title',
19
+ tags: 'comma-separated tags',
20
+ friends: [
21
+ {
22
+ name: 'friend full name',
23
+ age: 'age'
24
+ }
25
+ ]
26
+ })
27
+ end
28
+
29
+ it "works with blocks" do
30
+ results = schema.walk{|field| field.meta_data[:example]}
31
+ expect(results.output).to eq({
32
+ title: 'a title',
33
+ tags: ['tag1', 'tag2'],
34
+ friends: [
35
+ {
36
+ name: 'a friend',
37
+ age: 34
38
+ }
39
+ ]
40
+ })
41
+ end
42
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,2 +1,3 @@
1
1
  $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'byebug'
2
3
  require 'parametric'
@@ -0,0 +1,97 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'default validators' do
4
+
5
+ def test_validator(payload, key, name, eligible, valid, *args)
6
+ validator = Parametric.registry.policies[name]
7
+ validator = validator.new(*args) if validator.respond_to?(:new)
8
+ expect(validator.eligible?(payload[key], key, payload)).to eq eligible
9
+ expect(validator.valid?(payload[key], key, payload)).to eq valid
10
+ end
11
+
12
+ describe ':format' do
13
+ it {
14
+ test_validator({key: 'Foobar'}, :key, :format, true, true, /^Foo/)
15
+ test_validator({key: 'Foobar'}, :key, :format, true, false, /^Bar/)
16
+ test_validator({foo: 'Foobar'}, :key, :format, false, true, /^Foo/)
17
+ }
18
+ end
19
+
20
+ describe ':email' do
21
+ it {
22
+ test_validator({key: 'foo@bar.com'}, :key, :email, true, true)
23
+ test_validator({key: 'foo@'}, :key, :email, true, false)
24
+ test_validator({foo: 'foo@bar.com'}, :key, :email, false, true)
25
+ }
26
+ end
27
+
28
+ describe ':required' do
29
+ it {
30
+ test_validator({key: 'foo'}, :key, :required, true, true)
31
+ test_validator({key: ''}, :key, :required, true, true)
32
+ test_validator({key: nil}, :key, :required, true, true)
33
+ test_validator({foo: 'foo'}, :key, :required, true, false)
34
+ }
35
+ end
36
+
37
+ describe ':present' do
38
+ it {
39
+ test_validator({key: 'foo'}, :key, :present, true, true)
40
+ test_validator({key: ''}, :key, :present, true, false)
41
+ test_validator({key: nil}, :key, :present, true, false)
42
+ test_validator({foo: 'foo'}, :key, :present, true, false)
43
+ }
44
+ end
45
+
46
+ describe ':gt' do
47
+ it {
48
+ test_validator({key: 10}, :key, :gt, true, true, 9)
49
+ test_validator({key: '10'}, :key, :gt, true, true, 9)
50
+ test_validator({key: 10}, :key, :gt, true, false, 11)
51
+ test_validator({key: '10'}, :key, :gt, true, false, 11)
52
+ test_validator({foo: '10'}, :key, :gt, true, true, 11)
53
+ }
54
+ end
55
+
56
+ describe ':lt' do
57
+ it {
58
+ test_validator({key: 10}, :key, :lt, true, true, 11)
59
+ test_validator({key: '10'}, :key, :lt, true, true, 11)
60
+ test_validator({key: 10}, :key, :lt, true, false, 9)
61
+ test_validator({key: '10'}, :key, :lt, true, false, 9)
62
+ test_validator({foo: '10'}, :key, :lt, true, true, 9)
63
+ }
64
+ end
65
+
66
+ describe ':options' do
67
+ it {
68
+ test_validator({key: 'b'}, :key, :options, true, true, %w(a b c))
69
+ test_validator({key: 'd'}, :key, :options, true, false, %w(a b c))
70
+ test_validator({key: ['c', 'b']}, :key, :options, true, true, %w(a b c))
71
+ test_validator({key: ['c', 'b', 'd']}, :key, :options, true, false, %w(a b c))
72
+ test_validator({foo: 'b'}, :key, :options, false, true, %w(a b c))
73
+ }
74
+ end
75
+
76
+ describe ':array' do
77
+ it {
78
+ test_validator({key: ['a', 'b']}, :key, :array, true, true)
79
+ test_validator({key: []}, :key, :array, true, true)
80
+ test_validator({key: nil}, :key, :array, true, false)
81
+ test_validator({key: 'hello'}, :key, :array, true, false)
82
+ test_validator({key: 123}, :key, :array, true, false)
83
+ test_validator({foo: []}, :key, :array, true, true)
84
+ }
85
+ end
86
+
87
+ describe ':object' do
88
+ it {
89
+ test_validator({key: {'a' =>'b'}}, :key, :object, true, true)
90
+ test_validator({key: {}}, :key, :object, true, true)
91
+ test_validator({key: ['a', 'b']}, :key, :object, true, false)
92
+ test_validator({key: nil}, :key, :object, true, false)
93
+ test_validator({key: 123}, :key, :object, true, false)
94
+ test_validator({foo: {}}, :key, :object, true, true)
95
+ }
96
+ end
97
+ end
metadata CHANGED
@@ -1,41 +1,41 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parametric
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.1.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: 2014-07-27 00:00:00.000000000 Z
11
+ date: 2016-12-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ~>
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
19
  version: '1.5'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ~>
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.5'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - '>='
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: '0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - '>='
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
@@ -44,41 +44,67 @@ dependencies:
44
44
  requirements:
45
45
  - - '='
46
46
  - !ruby/object:Gem::Version
47
- version: 2.14.1
47
+ version: 3.4.0
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - '='
53
53
  - !ruby/object:Gem::Version
54
- version: 2.14.1
54
+ version: 3.4.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: byebug
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  description: Useful for modelling search or form objects, white-listed query parameters
56
70
  and safe parameter defaults.
57
71
  email:
58
72
  - ismaelct@gmail.com
59
- executables: []
73
+ executables:
74
+ - console
75
+ - setup
60
76
  extensions: []
61
77
  extra_rdoc_files: []
62
78
  files:
63
- - .gitignore
64
- - .rspec
65
- - .travis.yml
79
+ - ".gitignore"
80
+ - ".rspec"
81
+ - ".travis.yml"
66
82
  - Gemfile
67
83
  - LICENSE.txt
68
84
  - README.md
69
85
  - Rakefile
86
+ - bin/console
87
+ - bin/setup
70
88
  - lib/parametric.rb
71
- - lib/parametric/hash.rb
72
- - lib/parametric/params.rb
89
+ - lib/parametric/block_validator.rb
90
+ - lib/parametric/context.rb
91
+ - lib/parametric/default_types.rb
92
+ - lib/parametric/dsl.rb
93
+ - lib/parametric/field.rb
94
+ - lib/parametric/field_dsl.rb
73
95
  - lib/parametric/policies.rb
74
- - lib/parametric/typed_params.rb
75
- - lib/parametric/utils.rb
96
+ - lib/parametric/registry.rb
97
+ - lib/parametric/results.rb
98
+ - lib/parametric/schema.rb
76
99
  - lib/parametric/version.rb
77
- - lib/support/class_attribute.rb
78
100
  - parametric.gemspec
79
- - spec/nested_params_spec.rb
80
- - spec/parametric_spec.rb
101
+ - spec/dsl_spec.rb
102
+ - spec/field_spec.rb
103
+ - spec/policies_spec.rb
104
+ - spec/schema_spec.rb
105
+ - spec/schema_walk_spec.rb
81
106
  - spec/spec_helper.rb
107
+ - spec/validators_spec.rb
82
108
  homepage: ''
83
109
  licenses:
84
110
  - MIT
@@ -89,22 +115,26 @@ require_paths:
89
115
  - lib
90
116
  required_ruby_version: !ruby/object:Gem::Requirement
91
117
  requirements:
92
- - - '>='
118
+ - - ">="
93
119
  - !ruby/object:Gem::Version
94
120
  version: '0'
95
121
  required_rubygems_version: !ruby/object:Gem::Requirement
96
122
  requirements:
97
- - - '>='
123
+ - - ">="
98
124
  - !ruby/object:Gem::Version
99
125
  version: '0'
100
126
  requirements: []
101
127
  rubyforge_project:
102
- rubygems_version: 2.0.3
128
+ rubygems_version: 2.4.5
103
129
  signing_key:
104
130
  specification_version: 4
105
131
  summary: DSL for declaring allowed parameters with options, regexp patern and default
106
132
  values.
107
133
  test_files:
108
- - spec/nested_params_spec.rb
109
- - spec/parametric_spec.rb
134
+ - spec/dsl_spec.rb
135
+ - spec/field_spec.rb
136
+ - spec/policies_spec.rb
137
+ - spec/schema_spec.rb
138
+ - spec/schema_walk_spec.rb
110
139
  - spec/spec_helper.rb
140
+ - spec/validators_spec.rb
@@ -1,38 +0,0 @@
1
- require 'forwardable'
2
- module Parametric
3
- class Hash
4
- include TypedParams
5
- include Enumerable
6
- extend ::Forwardable
7
- def_delegators(:params,
8
- :[],
9
- :[]=,
10
- :each,
11
- :each_value,
12
- :each_key,
13
- :each_pair,
14
- :keys,
15
- :values,
16
- :values_at,
17
- :fetch,
18
- :size,
19
- :to_hash,
20
- :merge,
21
- :merge!,
22
- :replace,
23
- :update,
24
- :has_key?,
25
- :key?,
26
- :key,
27
- :select,
28
- :select!,
29
- :delete,
30
- :store,
31
- :inspect,
32
- :stringify_keys,
33
- :symbolize_keys
34
- )
35
-
36
- end
37
-
38
- end
@@ -1,86 +0,0 @@
1
- require 'ostruct'
2
- require 'support/class_attribute'
3
- module Parametric
4
-
5
- class ParamsHash < Hash
6
- def flat(separator = ',')
7
- self.each_with_object({}) do |(k,v),memo|
8
- memo[k] = Utils.value(v, separator)
9
- end
10
- end
11
- end
12
-
13
- module Params
14
-
15
- def self.included(base)
16
- base.send(:attr_reader, :params)
17
- base.class_attribute :_allowed_params
18
- base._allowed_params = {}
19
- base.extend DSL
20
- end
21
-
22
- def initialize(raw_params = {})
23
- @params = _reduce(raw_params)
24
- end
25
-
26
- def available_params
27
- @available_params ||= params.each_with_object(ParamsHash.new) do |(k,v),memo|
28
- if Utils.present?(v)
29
- memo[k] = v.respond_to?(:available_params) ? v.available_params : v
30
- end
31
- end
32
- end
33
-
34
- def schema
35
- @schema ||= params.each_with_object({}) do |(k,v),memo|
36
- is_nested = v.kind_of?(Parametric::Hash)
37
- attrs = self.class._allowed_params[k].dup
38
- attrs[:value] = is_nested ? v : Utils.value(v)
39
- attrs[:schema] = v.schema if is_nested
40
- memo[k] = OpenStruct.new(attrs)
41
- end
42
- end
43
-
44
- protected
45
-
46
- def _reduce(raw_params)
47
- self.class._allowed_params.each_with_object(ParamsHash.new) do |(key,options),memo|
48
- has_key = raw_params.respond_to?(:has_key?) && raw_params.has_key?(key)
49
- value = has_key ? raw_params[key] : []
50
- policy = Policies::Policy.new(value, options)
51
- policy = policy.wrap(Policies::CoercePolicy) if options[:coerce]
52
- policy = policy.wrap(Policies::NestedPolicy) if options[:nested]
53
- policy = policy.wrap(Policies::MultiplePolicy) if options[:multiple]
54
- policy = policy.wrap(Policies::OptionsPolicy) if options[:options]
55
- policy = policy.wrap(Policies::MatchPolicy) if options[:match]
56
- policy = policy.wrap(Policies::DefaultPolicy) if options.has_key?(:default)
57
- policy = policy.wrap(Policies::SinglePolicy) unless options[:multiple]
58
- memo[key] = policy.value unless options[:nullable] && !has_key
59
- end
60
- end
61
-
62
- module DSL
63
-
64
- # When subclasses params definitions
65
- # we want to copy parent class definitions
66
- # so changes in the child class
67
- # don't mutate the parent definitions
68
- #
69
- def inherited(subclass)
70
- subclass._allowed_params = self._allowed_params.dup
71
- end
72
-
73
- def param(field_name, label = '', opts = {}, &block)
74
- opts[:label] = label
75
- if block_given?
76
- nested = Class.new(Parametric::Hash)
77
- nested.instance_eval &block
78
- opts[:nested] = nested
79
- end
80
- _allowed_params[field_name] = opts
81
- end
82
- end
83
-
84
- end
85
-
86
- end