parametric 0.0.1 → 0.2.10

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
@@ -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