parametric 0.0.1 → 0.2.10

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -1,62 +1,133 @@
1
1
  module Parametric
2
2
  module Policies
3
+ class Format
4
+ attr_reader :message
3
5
 
4
- class Policy
5
- def initialize(value, options, decorated = nil)
6
- @value, @options = value, options
7
- @decorated = decorated
6
+ def initialize(fmt, msg = "invalid format")
7
+ @message = msg
8
+ @fmt = fmt
8
9
  end
9
10
 
10
- def wrap(decoratedClass)
11
- decoratedClass.new(@value, @options, self)
11
+ def eligible?(value, key, payload)
12
+ payload.key?(key)
12
13
  end
13
14
 
14
- def value
15
- Array(@value)
15
+ def coerce(value, key, context)
16
+ value
16
17
  end
17
18
 
18
- protected
19
- attr_reader :decorated, :options
20
- end
19
+ def valid?(value, key, payload)
20
+ !payload.key?(key) || !!(value.to_s =~ @fmt)
21
+ end
21
22
 
22
- class DefaultPolicy < Policy
23
- def value
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
- class MultiplePolicy < Policy
30
- OPTION_SEPARATOR = /\s*,\s*/.freeze
29
+ # Default validators
30
+ EMAIL_REGEXP = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i.freeze
31
31
 
32
- def value
33
- v = decorated.value.first
34
- v = v.split(options.fetch(:separator, OPTION_SEPARATOR)) if v.is_a?(String)
35
- Array(v)
36
- end
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
- class SinglePolicy < Policy
40
- def value
41
- decorated.value.first
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
- class OptionsPolicy < Policy
46
- def value
47
- decorated.value.each_with_object([]){|a,arr|
48
- arr << a if options[:options].include?(a)
49
- }
50
- end
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
- class MatchPolicy < Policy
54
- def value
55
- decorated.value.each_with_object([]){|a,arr|
56
- arr << a if a.to_s =~ options[:match]
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
+
@@ -0,0 +1,13 @@
1
+ module Parametric
2
+ class Results
3
+ attr_reader :output, :errors
4
+
5
+ def initialize(output, errors)
6
+ @output, @errors = output, errors
7
+ end
8
+
9
+ def valid?
10
+ !errors.keys.any?
11
+ end
12
+ end
13
+ end