parametric 0.0.1 → 0.2.10
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/.travis.yml +2 -1
- data/README.md +893 -96
- data/bin/console +14 -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 +68 -0
- data/lib/parametric/field.rb +111 -0
- data/lib/parametric/field_dsl.rb +24 -0
- data/lib/parametric/policies.rb +109 -38
- data/lib/parametric/registry.rb +21 -0
- data/lib/parametric/results.rb +13 -0
- data/lib/parametric/schema.rb +194 -0
- data/lib/parametric/struct.rb +102 -0
- data/lib/parametric/version.rb +1 -1
- data/lib/parametric.rb +16 -5
- data/parametric.gemspec +2 -2
- data/spec/custom_block_validator_spec.rb +21 -0
- data/spec/dsl_spec.rb +176 -0
- data/spec/expand_spec.rb +29 -0
- data/spec/field_spec.rb +430 -0
- data/spec/policies_spec.rb +72 -0
- data/spec/schema_spec.rb +289 -0
- data/spec/schema_walk_spec.rb +42 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/struct_spec.rb +324 -0
- data/spec/validators_spec.rb +106 -0
- metadata +46 -8
- data/lib/parametric/hash.rb +0 -36
- data/lib/parametric/params.rb +0 -60
- data/lib/parametric/utils.rb +0 -24
- data/spec/parametric_spec.rb +0 -182
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "parametric"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
@@ -0,0 +1,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 if instance_variable_defined?('@message_block')
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.validate(&validate_block)
|
15
|
+
@validate_block = validate_block if block_given?
|
16
|
+
@validate_block if instance_variable_defined?('@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 if instance_variable_defined?('@eligible_block')
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.meta_data(&block)
|
30
|
+
@meta_data_block = block if block_given?
|
31
|
+
@meta_data_block if instance_variable_defined?('@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 || ->(*args) { {} }
|
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?(Integer) ? "[#{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,68 @@
|
|
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
|
+
DEFAULT_SCHEMA_NAME = :schema
|
26
|
+
|
27
|
+
def self.included(base)
|
28
|
+
base.extend(ClassMethods)
|
29
|
+
base.schemas = {DEFAULT_SCHEMA_NAME => Parametric::Schema.new}
|
30
|
+
end
|
31
|
+
|
32
|
+
module ClassMethods
|
33
|
+
def schema=(sc)
|
34
|
+
@schemas[DEFAULT_SCHEMA_NAME] = sc
|
35
|
+
end
|
36
|
+
|
37
|
+
def schemas=(sc)
|
38
|
+
@schemas = sc
|
39
|
+
end
|
40
|
+
|
41
|
+
def inherited(subclass)
|
42
|
+
subclass.schemas = @schemas.each_with_object({}) do |(key, sc), hash|
|
43
|
+
hash[key] = sc.merge(Parametric::Schema.new)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def schema(*args, &block)
|
48
|
+
options = args.last.is_a?(Hash) ? args.last : {}
|
49
|
+
key = args.first.is_a?(Symbol) ? args.first : DEFAULT_SCHEMA_NAME
|
50
|
+
current_schema = @schemas.fetch(key) { Parametric::Schema.new }
|
51
|
+
new_schema = if block_given? || options.any?
|
52
|
+
Parametric::Schema.new(options, &block)
|
53
|
+
elsif args.first.respond_to?(:schema)
|
54
|
+
args.first
|
55
|
+
end
|
56
|
+
|
57
|
+
return current_schema unless new_schema
|
58
|
+
|
59
|
+
@schemas[key] = current_schema ? current_schema.merge(new_schema) : new_schema
|
60
|
+
parametric_after_define_schema(@schemas[key])
|
61
|
+
end
|
62
|
+
|
63
|
+
def parametric_after_define_schema(sc)
|
64
|
+
# noop hook
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
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.schema
|
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 && !meta_data[:skip_default]
|
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,24 @@
|
|
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 declared
|
17
|
+
policy :declared
|
18
|
+
end
|
19
|
+
|
20
|
+
def options(opts)
|
21
|
+
policy :options, opts
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/parametric/policies.rb
CHANGED
@@ -1,62 +1,133 @@
|
|
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.any? ? 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
|
-
end
|
41
|
+
Parametric.policy :declared do
|
42
|
+
eligible do |value, key, payload|
|
43
|
+
payload.key? key
|
43
44
|
end
|
45
|
+
end
|
44
46
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
47
|
+
Parametric.policy :declared_no_default do
|
48
|
+
eligible do |value, key, payload|
|
49
|
+
payload.key? key
|
50
|
+
end
|
51
|
+
|
52
|
+
meta_data do
|
53
|
+
{skip_default: true}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
Parametric.policy :required do
|
58
|
+
message do |*|
|
59
|
+
"is required"
|
51
60
|
end
|
52
61
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
62
|
+
validate do |value, key, payload|
|
63
|
+
payload.key? key
|
64
|
+
end
|
65
|
+
|
66
|
+
meta_data do
|
67
|
+
{required: true}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
Parametric.policy :present do
|
72
|
+
message do |*|
|
73
|
+
"is required and value must be present"
|
74
|
+
end
|
75
|
+
|
76
|
+
validate do |value, key, payload|
|
77
|
+
case value
|
78
|
+
when String
|
79
|
+
value.strip != ''
|
80
|
+
when Array, Hash
|
81
|
+
value.any?
|
82
|
+
else
|
83
|
+
!value.nil?
|
58
84
|
end
|
59
85
|
end
|
60
86
|
|
87
|
+
meta_data do
|
88
|
+
{present: true}
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
Parametric.policy :gt do
|
93
|
+
message do |num, actual|
|
94
|
+
"must be greater 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 :lt do
|
103
|
+
message do |num, actual|
|
104
|
+
"must be less than #{num}, but got #{actual}"
|
105
|
+
end
|
106
|
+
|
107
|
+
validate do |num, actual, key, payload|
|
108
|
+
!payload[key] || actual.to_i < num.to_i
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
Parametric.policy :options do
|
113
|
+
message do |options, actual|
|
114
|
+
"must be one of #{options.join(', ')}, but got #{actual}"
|
115
|
+
end
|
116
|
+
|
117
|
+
eligible do |options, actual, key, payload|
|
118
|
+
payload.key?(key)
|
119
|
+
end
|
120
|
+
|
121
|
+
validate do |options, actual, key, payload|
|
122
|
+
!payload.key?(key) || ok?(options, actual)
|
123
|
+
end
|
124
|
+
|
125
|
+
meta_data do |opts|
|
126
|
+
{options: opts}
|
127
|
+
end
|
128
|
+
|
129
|
+
def ok?(options, actual)
|
130
|
+
[actual].flatten.all?{|v| options.include?(v)}
|
131
|
+
end
|
61
132
|
end
|
62
|
-
end
|
133
|
+
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
|
+
|