parametric 0.0.5 → 0.1.0

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
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
@@ -1,84 +1,123 @@
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
- [@value].flatten
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.size > 0 ? 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
- decorated.value.map do |v|
34
- v.is_a?(String) ? v.split(options.fetch(:separator, OPTION_SEPARATOR)) : v
35
- end.flatten
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 NestedPolicy < Policy
40
- def value
41
- decorated.value.find_all{|v| v.respond_to?(:has_key?)}.map do |v|
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
- class CoercePolicy < Policy
48
- def value
49
- decorated.value.map do |v|
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
- class SinglePolicy < Policy
62
- def value
63
- decorated.value.first
64
- end
52
+ validate do |value, key, payload|
53
+ payload.key? key
65
54
  end
66
55
 
67
- class OptionsPolicy < Policy
68
- def value
69
- decorated.value.each_with_object([]){|a,arr|
70
- arr << a if options[:options].include?(a)
71
- }
72
- end
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
- class MatchPolicy < Policy
76
- def value
77
- decorated.value.each_with_object([]){|a,arr|
78
- arr << a if a.to_s =~ options[:match]
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
+
@@ -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