paradocs 1.0.22

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,170 @@
1
+ require_relative './base_policy'
2
+
3
+ module Paradocs
4
+ module Policies
5
+ class Format < Paradocs::BasePolicy
6
+ attr_reader :message
7
+
8
+ def initialize(fmt, msg = "invalid format")
9
+ @message = msg
10
+ @fmt = fmt
11
+ end
12
+
13
+ def eligible?(value, key, payload)
14
+ payload.key?(key)
15
+ end
16
+
17
+ def validate(value, key, payload)
18
+ !payload.key?(key) || !!(value.to_s =~ @fmt)
19
+ end
20
+ end
21
+ end
22
+
23
+ # Default validators
24
+ EMAIL_REGEXP = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i.freeze
25
+
26
+ Paradocs.policy :format, Policies::Format
27
+ Paradocs.policy :email, Policies::Format.new(EMAIL_REGEXP, 'invalid email')
28
+
29
+ Paradocs.policy :noop do
30
+ eligible do |value, key, payload|
31
+ true
32
+ end
33
+ end
34
+
35
+ Paradocs.policy :declared do
36
+ eligible do |value, key, payload|
37
+ payload.key? key
38
+ end
39
+
40
+ meta_data do
41
+ {}
42
+ end
43
+ end
44
+
45
+ Paradocs.policy :whitelisted do
46
+ meta_data do
47
+ {whitelisted: true}
48
+ end
49
+ end
50
+
51
+ Paradocs.policy :required do
52
+ message do |*|
53
+ "is required"
54
+ end
55
+
56
+ validate do |value, key, payload|
57
+ payload.try(:key?, key)
58
+ end
59
+
60
+ meta_data do
61
+ {required: true}
62
+ end
63
+ end
64
+
65
+ Paradocs.policy :present do
66
+ message do |*|
67
+ "is required and value must be present"
68
+ end
69
+
70
+ validate do |value, key, payload|
71
+ case value
72
+ when String
73
+ value.strip != ''
74
+ when Array, Hash
75
+ value.any?
76
+ else
77
+ !value.nil?
78
+ end
79
+ end
80
+
81
+ meta_data do
82
+ {present: true}
83
+ end
84
+ end
85
+
86
+ {
87
+ gte: [:>=, "greater than or equal to"],
88
+ lte: [:<=, "less than or equal to"],
89
+ gt: [:>, "strictly greater than"],
90
+ lt: [:<, "strictly less than"]
91
+ }.each do |policy, (comparison, text)|
92
+ klass = Class.new(Paradocs::BasePolicy) do
93
+ attr_reader :limit
94
+ define_method(:initialize) do |limit|
95
+ @limit = limit.is_a?(Proc) ? limit.call : limit
96
+ end
97
+
98
+ define_method(:message) do
99
+ "value must be #{text} #{limit}"
100
+ end
101
+
102
+ define_method(:validate) do |value, key, _|
103
+ @key, @value = key, value
104
+ value.send(comparison, limit)
105
+ end
106
+
107
+ define_method(:meta_data) do
108
+ meta = super()
109
+ meta[policy][:limit] = limit
110
+ binding.pry unless meta.dig(policy, :limit)
111
+ meta
112
+ end
113
+
114
+ define_singleton_method(:policy_name) do
115
+ policy
116
+ end
117
+ end
118
+ Paradocs.policy(policy, klass)
119
+ end
120
+
121
+ Paradocs.policy :options do
122
+ message do |options, actual|
123
+ "must be one of #{options.join(', ')}, but got #{actual}"
124
+ end
125
+
126
+ eligible do |options, actual, key, payload|
127
+ payload.key?(key)
128
+ end
129
+
130
+ validate do |options, actual, key, payload|
131
+ !payload.key?(key) || ok?(options, actual)
132
+ end
133
+
134
+ meta_data do |opts|
135
+ {options: opts}
136
+ end
137
+
138
+ def ok?(options, actual)
139
+ [actual].flatten.all?{|v| options.include?(v)}
140
+ end
141
+ end
142
+
143
+ Paradocs.policy :length do
144
+ COMPARISONS = {
145
+ max: [:<=, "maximum"],
146
+ min: [:>=, "minimum"],
147
+ eq: [:==, "exactly"]
148
+ }.freeze
149
+
150
+ message do |options, actual, key|
151
+ "value must be " + options.each_with_object([]) do |(comparison, limit), obj|
152
+ obj << "#{COMPARISONS[comparison].last} #{limit} characters"
153
+ end.join(", ")
154
+ end
155
+
156
+ validate do |options, actual, key, payload|
157
+ !payload.key?(key) || ok?(options, actual)
158
+ end
159
+
160
+ meta_data do |opts|
161
+ {length: opts}
162
+ end
163
+
164
+ def ok?(options, actual)
165
+ options.all? do |comparison, limit|
166
+ actual.to_s.length.send(COMPARISONS[comparison].first, limit)
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,42 @@
1
+ require 'paradocs/base_policy'
2
+
3
+ module Paradocs
4
+ class ConfigurationError < StandardError; end
5
+
6
+ class Registry
7
+ attr_reader :policies
8
+
9
+ def initialize
10
+ @policies = {}
11
+ end
12
+
13
+ def coercions
14
+ policies
15
+ end
16
+
17
+ def policy(name, plcy = nil, &block)
18
+ validate_policy_class(plcy) if plcy
19
+
20
+ policies[name] = (plcy || BasePolicy.build(name, :instance_eval, &block))
21
+ self
22
+ end
23
+
24
+ private
25
+
26
+ def validate_policy_class(plcy)
27
+ plcy_cls = plcy.is_a?(Class) ? plcy : plcy.class
28
+ if plcy_cls < Paradocs::BasePolicy
29
+ valid_overriden = plcy_cls.instance_method(:valid?).source_location != Paradocs::BasePolicy.instance_method(:valid?).source_location
30
+ raise ConfigurationError.new("Overriding #valid? in #{plcy_cls} is forbidden. Override #validate instead") if valid_overriden
31
+ else
32
+ required_methods = [:valid?, :coerce, :eligible?, :meta_data, :policy_name] - plcy_cls.instance_methods
33
+ raise ConfigurationError.new("Policy #{plcy_cls} should respond to #{required_methods}") unless required_methods.empty?
34
+
35
+ return plcy unless Paradocs.config.explicit_errors
36
+ return plcy if plcy_cls.respond_to?(:errors)
37
+ raise ConfigurationError.new("Policy #{plcy_cls} should respond to .errors method")
38
+ end
39
+ end
40
+ end
41
+ end
42
+
@@ -0,0 +1,13 @@
1
+ module Paradocs
2
+ class Results
3
+ attr_reader :output, :errors, :environment
4
+
5
+ def initialize(output, errors, environment)
6
+ @output, @errors, @environment = output, errors, environment
7
+ end
8
+
9
+ def valid?
10
+ !errors.keys.any?
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,214 @@
1
+ require "paradocs/context"
2
+ require "paradocs/results"
3
+ require "paradocs/field"
4
+ require "paradocs/extensions/insides"
5
+
6
+ module Paradocs
7
+ class Schema
8
+ include Extensions::Insides
9
+
10
+ attr_accessor :environment
11
+ attr_reader :subschemes
12
+ def initialize(options={}, &block)
13
+ @options = options
14
+ @fields = {}
15
+ @subschemes = {}
16
+ @definitions = []
17
+ @definitions << block if block_given?
18
+ @default_field_policies = []
19
+ @ignored_field_keys = []
20
+ @expansions = {}
21
+ end
22
+
23
+ def schema
24
+ self
25
+ end
26
+
27
+ def mutation_by!(key, &block)
28
+ f = @fields.keys.include?(key) ? @fields[key] : field(key).transparent
29
+ f.mutates_schema!(&block)
30
+ end
31
+
32
+ def subschema(*args, &block)
33
+ options = args.last.is_a?(Hash) ? args.last : {}
34
+ name = args.first.is_a?(Symbol) ? args.shift : Paradocs.config.default_schema_name
35
+ current_schema = subschemes.fetch(name) { self.class.new }
36
+ new_schema = if block_given?
37
+ sc = self.class.new(options)
38
+ sc.definitions << block
39
+ sc
40
+ elsif args.first.is_a?(self.class)
41
+ args.first
42
+ else
43
+ self.class.new(options)
44
+ end
45
+ subschemes[name] = current_schema.merge(new_schema)
46
+ end
47
+
48
+ def fields
49
+ apply!
50
+ @fields
51
+ end
52
+
53
+ def policy(*names, &block)
54
+ @default_field_policies = names
55
+ definitions << block if block_given?
56
+
57
+ self
58
+ end
59
+
60
+ def ignore(*field_keys, &block)
61
+ @ignored_field_keys += field_keys
62
+ @ignored_field_keys.uniq!
63
+
64
+ definitions << block if block_given?
65
+
66
+ self
67
+ end
68
+
69
+ def clone
70
+ instance = self.class.new(options)
71
+ copy_into instance
72
+ end
73
+
74
+ def merge(other_schema)
75
+ instance = self.class.new(options.merge(other_schema.options))
76
+
77
+ copy_into(instance)
78
+ other_schema.copy_into(instance)
79
+ end
80
+
81
+ def copy_into(instance)
82
+ instance.policy(*default_field_policies) if default_field_policies.any?
83
+
84
+ definitions.each do |d|
85
+ instance.definitions << d
86
+ end
87
+
88
+ subschemes.each { |name, subsc| instance.subschema(name, subsc) }
89
+
90
+ instance.ignore *ignored_field_keys
91
+ instance
92
+ end
93
+
94
+ def field(field_or_key)
95
+ f, key = if field_or_key.kind_of?(Field)
96
+ [field_or_key, field_or_key.key]
97
+ else
98
+ [Field.new(field_or_key), field_or_key.to_sym]
99
+ end
100
+
101
+ if ignored_field_keys.include?(f.key)
102
+ f
103
+ else
104
+ @fields[key] = apply_default_field_policies_to(f)
105
+ end
106
+ end
107
+
108
+ def expand(exp, &block)
109
+ expansions[exp] = block
110
+ self
111
+ end
112
+
113
+ def resolve(payload, environment={})
114
+ @environment = environment
115
+ context = Context.new(nil, Top.new, @environment, subschemes)
116
+ output = coerce(payload, nil, context)
117
+ Results.new(output, context.errors, @environment)
118
+ end
119
+
120
+ def eligible?(value, key, payload)
121
+ payload.key? key
122
+ end
123
+
124
+ def valid?(*_)
125
+ true
126
+ end
127
+
128
+ def meta_data
129
+ {}
130
+ end
131
+
132
+ def coerce(val, _, context)
133
+ flush!
134
+ if val.is_a?(Array)
135
+ val.map.with_index do |v, idx|
136
+ subcontext = context.sub(idx)
137
+ out = coerce_one(v, subcontext)
138
+ resolve_expansions(v, out, subcontext)
139
+ end
140
+ else
141
+ out = coerce_one(val, context)
142
+ resolve_expansions(val, out, context)
143
+ end
144
+ end
145
+
146
+ protected
147
+
148
+ attr_reader :definitions, :options
149
+
150
+ private
151
+
152
+ attr_reader :default_field_policies, :ignored_field_keys, :expansions
153
+
154
+ def coerce_one(val, context, flds: fields)
155
+ invoke_subschemes!(val, context, flds: flds)
156
+ flds.each_with_object({}) do |(_, field), m|
157
+ r = field.resolve(val, context.sub(field.key))
158
+ m[field.key] = r.value if r.eligible?
159
+ end
160
+ end
161
+
162
+ def invoke_subschemes!(payload, context, flds: fields)
163
+ invoked_any = false
164
+ # recoursive definitions call depending on payload
165
+ flds.clone.each_pair do |_, field|
166
+ next unless field.expects_mutation?
167
+ subschema_name = field.subschema_for_mutation(payload, context.environment)
168
+ subschema = subschemes[subschema_name] || context.subschema(subschema_name)
169
+ next unless subschema # or may be raise error?
170
+ subschema.definitions.each { |block| self.instance_exec(&block) }
171
+ invoked_any = true
172
+ end
173
+ # if definitions are applied new subschemes may appear, apply them until they end
174
+ invoke_subschemes!(payload, context, flds: fields) if invoked_any
175
+ end
176
+
177
+ class MatchContext
178
+ def field(key)
179
+ Field.new(key.to_sym)
180
+ end
181
+ end
182
+
183
+ def resolve_expansions(payload, into, context)
184
+ expansions.each do |exp, block|
185
+ payload.each do |key, value|
186
+ match = exp.match(key.to_s)
187
+ next unless match
188
+ fld = MatchContext.new.instance_exec(match, &block)
189
+ next unless fld
190
+ into.update(coerce_one({fld.key => value}, context, flds: {fld.key => apply_default_field_policies_to(fld)}))
191
+ end
192
+ end
193
+
194
+ into
195
+ end
196
+
197
+ def apply_default_field_policies_to(field)
198
+ default_field_policies.reduce(field) {|f, policy_name| f.policy(policy_name) }
199
+ end
200
+
201
+ def apply!
202
+ return if @applied
203
+ definitions.each do |d|
204
+ self.instance_exec(options, &d)
205
+ end
206
+ @applied = true
207
+ end
208
+
209
+ def flush!
210
+ @fields = {}
211
+ @applied = false
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,102 @@
1
+ require 'paradocs/dsl'
2
+
3
+ module Paradocs
4
+ class InvalidStructError < ArgumentError
5
+ attr_reader :errors
6
+ def initialize(struct)
7
+ @errors = struct.errors
8
+ msg = @errors.map do |k, strings|
9
+ "#{k} #{strings.join(', ')}"
10
+ end.join('. ')
11
+ super "#{struct.class} is not a valid struct: #{msg}"
12
+ end
13
+ end
14
+
15
+ module Struct
16
+ def self.included(base)
17
+ base.send(:include, Paradocs::DSL)
18
+ base.extend ClassMethods
19
+ end
20
+
21
+ def initialize(attrs = {})
22
+ @_results = self.class.schema.resolve(attrs)
23
+ @_graph = self.class.build(@_results.output)
24
+ end
25
+
26
+ def valid?
27
+ !_results.errors.any?
28
+ end
29
+
30
+ def errors
31
+ _results.errors
32
+ end
33
+
34
+ # returns a shallow copy.
35
+ def to_h
36
+ _results.output.clone
37
+ end
38
+
39
+ def ==(other)
40
+ other.respond_to?(:to_h) && other.to_h.eql?(to_h)
41
+ end
42
+
43
+ def merge(attrs = {})
44
+ self.class.new(to_h.merge(attrs))
45
+ end
46
+
47
+ private
48
+ attr_reader :_graph, :_results
49
+
50
+ module ClassMethods
51
+ def new!(attrs = {})
52
+ st = new(attrs)
53
+ raise InvalidStructError.new(st) unless st.valid?
54
+ st
55
+ end
56
+
57
+ # this hook is called after schema definition in DSL module
58
+ def paradocs_after_define_schema(schema)
59
+ schema.fields.keys.each do |key|
60
+ define_method key do
61
+ _graph[key]
62
+ end
63
+ end
64
+ end
65
+
66
+ def build(attrs)
67
+ attrs.each_with_object({}) do |(k, v), obj|
68
+ obj[k] = wrap(k, v)
69
+ end
70
+ end
71
+
72
+ def paradocs_build_class_for_child(key, child_schema)
73
+ klass = Class.new do
74
+ include Struct
75
+ end
76
+ klass.schema = child_schema
77
+ klass
78
+ end
79
+
80
+ def wrap(key, value)
81
+ field = schema.fields[key]
82
+ return value unless field
83
+
84
+ case value
85
+ when Hash
86
+ # find constructor for field
87
+ cons = field.meta_data[:schema]
88
+ if cons.kind_of?(Paradocs::Schema)
89
+ klass = paradocs_build_class_for_child(key, cons)
90
+ klass.paradocs_after_define_schema(cons)
91
+ cons = klass
92
+ end
93
+ cons ? cons.new(value) : value.freeze
94
+ when Array
95
+ value.map{|v| wrap(key, v) }.freeze
96
+ else
97
+ value.freeze
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,47 @@
1
+ require "delegate"
2
+
3
+ module Support
4
+ module Tryable #:nodoc:
5
+ def try(method_name = nil, *args, &b)
6
+ if method_name.nil? && block_given?
7
+ if b.arity == 0
8
+ instance_eval(&b)
9
+ else
10
+ yield self
11
+ end
12
+ elsif respond_to?(method_name)
13
+ public_send(method_name, *args, &b)
14
+ end
15
+ end
16
+
17
+ def try!(method_name = nil, *args, &b)
18
+ if method_name.nil? && block_given?
19
+ if b.arity == 0
20
+ instance_eval(&b)
21
+ else
22
+ yield self
23
+ end
24
+ else
25
+ public_send(method_name, *args, &b)
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ class Object
32
+ include Support::Tryable
33
+ end
34
+
35
+ class Delegator
36
+ include Support::Tryable
37
+ end
38
+
39
+ class NilClass
40
+ def try(_method_name = nil, *, **)
41
+ nil
42
+ end
43
+
44
+ def try!(_method_name = nil, *, **)
45
+ nil
46
+ end
47
+ end
@@ -0,0 +1,3 @@
1
+ module Paradocs
2
+ VERSION = "1.0.22"
3
+ end
@@ -0,0 +1,91 @@
1
+ module Paradocs
2
+ module Whitelist
3
+ # Example
4
+ # class Foo
5
+ # include Paradocs::DSL
6
+ # include Paradocs::Whitelist
7
+ #
8
+ # schema(:test) do
9
+ # field(:title).type(:string).whitelisted
10
+ # field(:age).type(:integer).default(20)
11
+ # end
12
+ # end
13
+ #
14
+ # foo = Foo.new
15
+ # schema = foo.class.schema(:test)
16
+ # params = {title: "title", age: 25}
17
+ # foo.filter!(params, schema) # => {title: "title", age: "[FILTERED]"}
18
+ #
19
+ FILTERED = "[FILTERED]"
20
+ EMPTY = "[EMPTY]"
21
+
22
+ def self.included(base)
23
+ base.include(ClassMethods)
24
+ end
25
+
26
+ module ClassMethods
27
+ def filter!(payload, source_schema)
28
+ schema = source_schema.clone
29
+ context = Context.new(nil, Top.new, @environment, source_schema.subschemes.clone)
30
+ resolve(payload, schema, context)
31
+ end
32
+
33
+ def resolve(payload, schema, context)
34
+ filtered_payload = {}
35
+ payload.dup.each do |key, value|
36
+ key = key.to_sym
37
+ schema = Schema.new if schema.nil?
38
+ schema.send(:flush!)
39
+ schema.send(:invoke_subschemes!, payload, context)
40
+
41
+ if value.is_a?(Hash)
42
+ field_schema = find_schema_by(schema, key)
43
+ value = resolve(value, field_schema, context)
44
+ elsif value.is_a?(Array)
45
+ value = value.map do |v|
46
+ if v.is_a?(Hash)
47
+ field_schema = find_schema_by(schema, key)
48
+ resolve(v, field_schema, context)
49
+ else
50
+ v = FILTERED unless whitelisted?(schema, key)
51
+ v
52
+ end
53
+ end
54
+ else
55
+ value = if whitelisted?(schema, key)
56
+ value
57
+ elsif value.nil? || value.try(:blank?) || value.try(:empty?)
58
+ !!value == value ? value : EMPTY
59
+ else
60
+ FILTERED
61
+ end
62
+ value
63
+ end
64
+
65
+ filtered_payload[key] = value
66
+ end
67
+
68
+ filtered_payload
69
+ end
70
+
71
+ private
72
+
73
+ def find_schema_by(schema, key)
74
+ meta_data = get_meta_data(schema, key)
75
+ meta_data[:schema]
76
+ end
77
+
78
+ def whitelisted?(schema, key)
79
+ meta_data = get_meta_data(schema, key)
80
+ meta_data[:whitelisted] || Paradocs.config.whitelisted_keys.include?(key)
81
+ end
82
+
83
+ def get_meta_data(schema, key)
84
+ return {} unless schema.respond_to?(:fields)
85
+ return {} unless schema.fields[key]
86
+ return {} unless schema.fields[key].respond_to?(:meta_data)
87
+ meta_data = schema.fields[key].meta_data || {}
88
+ end
89
+ end
90
+ end
91
+ end