paradocs 1.0.22

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