parametric 0.0.5 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +1 -2
- data/README.md +596 -163
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/parametric/block_validator.rb +64 -0
- data/lib/parametric/context.rb +44 -0
- data/lib/parametric/default_types.rb +95 -0
- data/lib/parametric/dsl.rb +47 -0
- data/lib/parametric/field.rb +111 -0
- data/lib/parametric/field_dsl.rb +20 -0
- data/lib/parametric/policies.rb +94 -55
- data/lib/parametric/registry.rb +21 -0
- data/lib/parametric/results.rb +13 -0
- data/lib/parametric/schema.rb +151 -0
- data/lib/parametric/version.rb +1 -1
- data/lib/parametric.rb +16 -6
- data/parametric.gemspec +2 -1
- data/spec/dsl_spec.rb +135 -0
- data/spec/field_spec.rb +404 -0
- data/spec/policies_spec.rb +72 -0
- data/spec/schema_spec.rb +253 -0
- data/spec/schema_walk_spec.rb +42 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/validators_spec.rb +97 -0
- metadata +54 -24
- data/lib/parametric/hash.rb +0 -38
- data/lib/parametric/params.rb +0 -86
- data/lib/parametric/typed_params.rb +0 -23
- data/lib/parametric/utils.rb +0 -24
- data/lib/support/class_attribute.rb +0 -68
- data/spec/nested_params_spec.rb +0 -90
- data/spec/parametric_spec.rb +0 -261
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
|
data/bin/setup
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
module Parametric
|
2
|
+
class BlockValidator
|
3
|
+
def self.build(meth, &block)
|
4
|
+
klass = Class.new(self)
|
5
|
+
klass.public_send(meth, &block)
|
6
|
+
klass
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.message(&block)
|
10
|
+
@message_block = block if block_given?
|
11
|
+
@message_block
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.validate(&validate_block)
|
15
|
+
@validate_block = validate_block if block_given?
|
16
|
+
@validate_block
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.coerce(&coerce_block)
|
20
|
+
@coerce_block = coerce_block if block_given?
|
21
|
+
@coerce_block
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.eligible(&block)
|
25
|
+
@eligible_block = block if block_given?
|
26
|
+
@eligible_block
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.meta_data(&block)
|
30
|
+
@meta_data_block = block if block_given?
|
31
|
+
@meta_data_block
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :message
|
35
|
+
|
36
|
+
def initialize(*args)
|
37
|
+
@args = args
|
38
|
+
@message = 'is invalid'
|
39
|
+
@validate_block = self.class.validate || ->(*args) { true }
|
40
|
+
@coerce_block = self.class.coerce || ->(v, *_) { v }
|
41
|
+
@eligible_block = self.class.eligible || ->(*args) { true }
|
42
|
+
@meta_data_block = self.class.meta_data || ->() { {} }
|
43
|
+
end
|
44
|
+
|
45
|
+
def eligible?(value, key, payload)
|
46
|
+
args = (@args + [value, key, payload])
|
47
|
+
@eligible_block.call(*args)
|
48
|
+
end
|
49
|
+
|
50
|
+
def coerce(value, key, context)
|
51
|
+
@coerce_block.call(value, key, context)
|
52
|
+
end
|
53
|
+
|
54
|
+
def valid?(value, key, payload)
|
55
|
+
args = (@args + [value, key, payload])
|
56
|
+
@message = self.class.message.call(*args) if self.class.message
|
57
|
+
@validate_block.call(*args)
|
58
|
+
end
|
59
|
+
|
60
|
+
def meta_data
|
61
|
+
@meta_data_block.call *@args
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Parametric
|
2
|
+
class Top
|
3
|
+
attr_reader :errors
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@errors = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def add_error(key, msg)
|
10
|
+
errors[key] ||= []
|
11
|
+
errors[key] << msg
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Context
|
16
|
+
def initialize(path = nil, top = Top.new)
|
17
|
+
@top = top
|
18
|
+
@path = Array(path).compact
|
19
|
+
end
|
20
|
+
|
21
|
+
def errors
|
22
|
+
top.errors
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_error(msg)
|
26
|
+
top.add_error(string_path, msg)
|
27
|
+
end
|
28
|
+
|
29
|
+
def sub(key)
|
30
|
+
self.class.new(path + [key], top)
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
attr_reader :path, :top
|
35
|
+
|
36
|
+
def string_path
|
37
|
+
path.reduce(['$']) do |m, segment|
|
38
|
+
m << (segment.is_a?(Fixnum) ? "[#{segment}]" : ".#{segment}")
|
39
|
+
m
|
40
|
+
end.join
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require "date"
|
2
|
+
|
3
|
+
module Parametric
|
4
|
+
# type coercions
|
5
|
+
Parametric.policy :integer do
|
6
|
+
coerce do |v, k, c|
|
7
|
+
v.to_i
|
8
|
+
end
|
9
|
+
|
10
|
+
meta_data do
|
11
|
+
{type: :integer}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
Parametric.policy :number do
|
16
|
+
coerce do |v, k, c|
|
17
|
+
v.to_f
|
18
|
+
end
|
19
|
+
|
20
|
+
meta_data do
|
21
|
+
{type: :number}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
Parametric.policy :string do
|
26
|
+
coerce do |v, k, c|
|
27
|
+
v.to_s
|
28
|
+
end
|
29
|
+
|
30
|
+
meta_data do
|
31
|
+
{type: :string}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
Parametric.policy :boolean do
|
36
|
+
coerce do |v, k, c|
|
37
|
+
!!v
|
38
|
+
end
|
39
|
+
|
40
|
+
meta_data do
|
41
|
+
{type: :boolean}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# type validations
|
46
|
+
Parametric.policy :array do
|
47
|
+
message do |actual|
|
48
|
+
"expects an array, but got #{actual.inspect}"
|
49
|
+
end
|
50
|
+
|
51
|
+
validate do |value, key, payload|
|
52
|
+
!payload.key?(key) || value.is_a?(Array)
|
53
|
+
end
|
54
|
+
|
55
|
+
meta_data do
|
56
|
+
{type: :array}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
Parametric.policy :object do
|
61
|
+
message do |actual|
|
62
|
+
"expects a hash, but got #{actual.inspect}"
|
63
|
+
end
|
64
|
+
|
65
|
+
validate do |value, key, payload|
|
66
|
+
!payload.key?(key) ||
|
67
|
+
value.respond_to?(:[]) &&
|
68
|
+
value.respond_to?(:key?)
|
69
|
+
end
|
70
|
+
|
71
|
+
meta_data do
|
72
|
+
{type: :object}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
Parametric.policy :split do
|
77
|
+
coerce do |v, k, c|
|
78
|
+
v.kind_of?(Array) ? v : v.to_s.split(/\s*,\s*/)
|
79
|
+
end
|
80
|
+
|
81
|
+
meta_data do
|
82
|
+
{type: :array}
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
Parametric.policy :datetime do
|
87
|
+
coerce do |v, k, c|
|
88
|
+
DateTime.parse(v.to_s)
|
89
|
+
end
|
90
|
+
|
91
|
+
meta_data do
|
92
|
+
{type: :datetime}
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require "parametric"
|
2
|
+
|
3
|
+
module Parametric
|
4
|
+
module DSL
|
5
|
+
# Example
|
6
|
+
# class Foo
|
7
|
+
# include Parametric::DSL
|
8
|
+
#
|
9
|
+
# schema do
|
10
|
+
# field(:title).type(:string).present
|
11
|
+
# field(:age).type(:integer).default(20)
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# attr_reader :params
|
15
|
+
#
|
16
|
+
# def initialize(input)
|
17
|
+
# @params = self.class.schema.resolve(input)
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# foo = Foo.new(title: "A title", nope: "hello")
|
22
|
+
#
|
23
|
+
# foo.params # => {title: "A title", age: 20}
|
24
|
+
#
|
25
|
+
def self.included(base)
|
26
|
+
base.extend(ClassMethods)
|
27
|
+
base.schema = Parametric::Schema.new
|
28
|
+
end
|
29
|
+
|
30
|
+
module ClassMethods
|
31
|
+
def schema=(sc)
|
32
|
+
@schema = sc
|
33
|
+
end
|
34
|
+
|
35
|
+
def inherited(subclass)
|
36
|
+
subclass.schema = @schema.merge(Parametric::Schema.new)
|
37
|
+
end
|
38
|
+
|
39
|
+
def schema(options = {}, &block)
|
40
|
+
return @schema unless options.any? || block_given?
|
41
|
+
|
42
|
+
new_schema = Parametric::Schema.new(options, &block)
|
43
|
+
@schema = @schema.merge(new_schema)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require "parametric/field_dsl"
|
2
|
+
|
3
|
+
module Parametric
|
4
|
+
class ConfigurationError < StandardError; end
|
5
|
+
|
6
|
+
class Field
|
7
|
+
include FieldDSL
|
8
|
+
|
9
|
+
attr_reader :key, :meta_data
|
10
|
+
Result = Struct.new(:eligible?, :value)
|
11
|
+
|
12
|
+
def initialize(key, registry = Parametric.registry)
|
13
|
+
@key = key
|
14
|
+
@policies = []
|
15
|
+
@registry = registry
|
16
|
+
@default_block = nil
|
17
|
+
@meta_data = {}
|
18
|
+
@policies = []
|
19
|
+
end
|
20
|
+
|
21
|
+
def meta(hash = nil)
|
22
|
+
@meta_data = @meta_data.merge(hash) if hash.is_a?(Hash)
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def default(value)
|
27
|
+
meta default: value
|
28
|
+
@default_block = (value.respond_to?(:call) ? value : ->(key, payload, context) { value })
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def policy(key, *args)
|
33
|
+
pol = lookup(key, args)
|
34
|
+
meta pol.meta_data
|
35
|
+
policies << pol
|
36
|
+
self
|
37
|
+
end
|
38
|
+
alias_method :type, :policy
|
39
|
+
|
40
|
+
def schema(sc = nil, &block)
|
41
|
+
sc = (sc ? sc : Schema.new(&block))
|
42
|
+
meta schema: sc
|
43
|
+
policy sc
|
44
|
+
end
|
45
|
+
|
46
|
+
def visit(meta_key = nil, &visitor)
|
47
|
+
if sc = meta_data[:schema]
|
48
|
+
r = sc.visit(meta_key, &visitor)
|
49
|
+
(meta_data[:type] == :array) ? [r] : r
|
50
|
+
else
|
51
|
+
meta_key ? meta_data[meta_key] : yield(self)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def resolve(payload, context)
|
56
|
+
eligible = payload.key?(key)
|
57
|
+
value = payload[key] # might be nil
|
58
|
+
|
59
|
+
if !eligible && has_default?
|
60
|
+
eligible = true
|
61
|
+
value = default_block.call(key, payload, context)
|
62
|
+
return Result.new(eligible, value)
|
63
|
+
end
|
64
|
+
|
65
|
+
policies.each do |policy|
|
66
|
+
if !policy.eligible?(value, key, payload)
|
67
|
+
eligible = false
|
68
|
+
if has_default?
|
69
|
+
eligible = true
|
70
|
+
value = default_block.call(key, payload, context)
|
71
|
+
end
|
72
|
+
break
|
73
|
+
else
|
74
|
+
value = resolve_one(policy, value, context)
|
75
|
+
if !policy.valid?(value, key, payload)
|
76
|
+
eligible = true # eligible, but has errors
|
77
|
+
context.add_error policy.message
|
78
|
+
break # only one error at a time
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
Result.new(eligible, value)
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
attr_reader :policies, :registry, :default_block
|
88
|
+
|
89
|
+
def resolve_one(policy, value, context)
|
90
|
+
begin
|
91
|
+
policy.coerce(value, key, context)
|
92
|
+
rescue StandardError => e
|
93
|
+
context.add_error e.message
|
94
|
+
value
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def has_default?
|
99
|
+
!!default_block
|
100
|
+
end
|
101
|
+
|
102
|
+
def lookup(key, args)
|
103
|
+
obj = key.is_a?(Symbol) ? registry.policies[key] : key
|
104
|
+
|
105
|
+
raise ConfigurationError, "No policies defined for #{key.inspect}" unless obj
|
106
|
+
|
107
|
+
obj.respond_to?(:new) ? obj.new(*args) : obj
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Parametric
|
2
|
+
# Field DSL
|
3
|
+
# host instance must implement:
|
4
|
+
# #meta(options Hash)
|
5
|
+
# #policy(key Symbol) self
|
6
|
+
#
|
7
|
+
module FieldDSL
|
8
|
+
def required
|
9
|
+
policy :required
|
10
|
+
end
|
11
|
+
|
12
|
+
def present
|
13
|
+
required.policy :present
|
14
|
+
end
|
15
|
+
|
16
|
+
def options(opts)
|
17
|
+
policy :options, opts
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/parametric/policies.rb
CHANGED
@@ -1,84 +1,123 @@
|
|
1
1
|
module Parametric
|
2
2
|
module Policies
|
3
|
+
class Format
|
4
|
+
attr_reader :message
|
3
5
|
|
4
|
-
|
5
|
-
|
6
|
-
@
|
7
|
-
@decorated = decorated
|
6
|
+
def initialize(fmt, msg = "invalid format")
|
7
|
+
@message = msg
|
8
|
+
@fmt = fmt
|
8
9
|
end
|
9
10
|
|
10
|
-
def
|
11
|
-
|
11
|
+
def eligible?(value, key, payload)
|
12
|
+
payload.key?(key)
|
12
13
|
end
|
13
14
|
|
14
|
-
def value
|
15
|
-
|
15
|
+
def coerce(value, key, context)
|
16
|
+
value
|
16
17
|
end
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
19
|
+
def valid?(value, key, payload)
|
20
|
+
!payload.key?(key) || !!(value.to_s =~ @fmt)
|
21
|
+
end
|
21
22
|
|
22
|
-
|
23
|
-
|
24
|
-
v = decorated.value
|
25
|
-
v.size > 0 ? v : Array(options[:default])
|
23
|
+
def meta_data
|
24
|
+
{}
|
26
25
|
end
|
27
26
|
end
|
27
|
+
end
|
28
28
|
|
29
|
-
|
30
|
-
|
29
|
+
# Default validators
|
30
|
+
EMAIL_REGEXP = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i.freeze
|
31
31
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
32
|
+
Parametric.policy :format, Policies::Format
|
33
|
+
Parametric.policy :email, Policies::Format.new(EMAIL_REGEXP, 'invalid email')
|
34
|
+
|
35
|
+
Parametric.policy :noop do
|
36
|
+
eligible do |value, key, payload|
|
37
|
+
true
|
37
38
|
end
|
39
|
+
end
|
38
40
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
options[:nested].new(v)
|
43
|
-
end
|
44
|
-
end
|
41
|
+
Parametric.policy :declared do
|
42
|
+
eligible do |value, key, payload|
|
43
|
+
payload.key? key
|
45
44
|
end
|
45
|
+
end
|
46
46
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
if options[:coerce].is_a?(Symbol) && v.respond_to?(options[:coerce])
|
51
|
-
v.send(options[:coerce])
|
52
|
-
elsif options[:coerce].respond_to?(:call)
|
53
|
-
options[:coerce].call v
|
54
|
-
else
|
55
|
-
v
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
47
|
+
Parametric.policy :required do
|
48
|
+
message do |*|
|
49
|
+
"is required"
|
59
50
|
end
|
60
51
|
|
61
|
-
|
62
|
-
|
63
|
-
decorated.value.first
|
64
|
-
end
|
52
|
+
validate do |value, key, payload|
|
53
|
+
payload.key? key
|
65
54
|
end
|
66
55
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
56
|
+
meta_data do
|
57
|
+
{required: true}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
Parametric.policy :present do
|
62
|
+
message do |*|
|
63
|
+
"is required and value must be present"
|
73
64
|
end
|
74
65
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
66
|
+
validate do |value, key, payload|
|
67
|
+
case value
|
68
|
+
when String
|
69
|
+
value.strip != ''
|
70
|
+
when Array, Hash
|
71
|
+
value.any?
|
72
|
+
else
|
73
|
+
!value.nil?
|
80
74
|
end
|
81
75
|
end
|
82
76
|
|
77
|
+
meta_data do
|
78
|
+
{present: true}
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
Parametric.policy :gt do
|
83
|
+
message do |num, actual|
|
84
|
+
"must be greater than #{num}, but got #{actual}"
|
85
|
+
end
|
86
|
+
|
87
|
+
validate do |num, actual, key, payload|
|
88
|
+
!payload[key] || actual.to_i > num.to_i
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
Parametric.policy :lt do
|
93
|
+
message do |num, actual|
|
94
|
+
"must be less than #{num}, but got #{actual}"
|
95
|
+
end
|
96
|
+
|
97
|
+
validate do |num, actual, key, payload|
|
98
|
+
!payload[key] || actual.to_i < num.to_i
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
Parametric.policy :options do
|
103
|
+
message do |options, actual|
|
104
|
+
"must be one of #{options.join(', ')}, but got #{actual}"
|
105
|
+
end
|
106
|
+
|
107
|
+
eligible do |options, actual, key, payload|
|
108
|
+
payload.key?(key)
|
109
|
+
end
|
110
|
+
|
111
|
+
validate do |options, actual, key, payload|
|
112
|
+
!payload.key?(key) || ok?(options, actual)
|
113
|
+
end
|
114
|
+
|
115
|
+
meta_data do |opts|
|
116
|
+
{options: opts}
|
117
|
+
end
|
118
|
+
|
119
|
+
def ok?(options, actual)
|
120
|
+
[actual].flatten.all?{|v| options.include?(v)}
|
121
|
+
end
|
83
122
|
end
|
84
123
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'parametric/block_validator'
|
2
|
+
|
3
|
+
module Parametric
|
4
|
+
class Registry
|
5
|
+
attr_reader :policies
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@policies = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def coercions
|
12
|
+
policies
|
13
|
+
end
|
14
|
+
|
15
|
+
def policy(name, plcy = nil, &block)
|
16
|
+
policies[name] = (plcy || BlockValidator.build(:instance_eval, &block))
|
17
|
+
self
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|