parametric 0.0.5 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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