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.
- checksums.yaml +7 -0
- data/.github/workflows/spec.yml +25 -0
- data/.gitignore +19 -0
- data/.gitlab-ci.yml +20 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +23 -0
- data/README.md +1078 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/lib/paradocs/base_policy.rb +123 -0
- data/lib/paradocs/context.rb +54 -0
- data/lib/paradocs/default_types.rb +95 -0
- data/lib/paradocs/dsl.rb +68 -0
- data/lib/paradocs/extensions/insides.rb +77 -0
- data/lib/paradocs/field.rb +152 -0
- data/lib/paradocs/field_dsl.rb +36 -0
- data/lib/paradocs/policies.rb +170 -0
- data/lib/paradocs/registry.rb +42 -0
- data/lib/paradocs/results.rb +13 -0
- data/lib/paradocs/schema.rb +214 -0
- data/lib/paradocs/struct.rb +102 -0
- data/lib/paradocs/support.rb +47 -0
- data/lib/paradocs/version.rb +3 -0
- data/lib/paradocs/whitelist.rb +91 -0
- data/lib/paradocs.rb +36 -0
- data/paradocs.gemspec +25 -0
- data/spec/custom_block_validator_spec.rb +88 -0
- data/spec/custom_validator.rb +61 -0
- data/spec/dsl_spec.rb +175 -0
- data/spec/expand_spec.rb +29 -0
- data/spec/field_spec.rb +416 -0
- data/spec/helpers.rb +18 -0
- data/spec/policies_spec.rb +159 -0
- data/spec/schema_spec.rb +299 -0
- data/spec/schema_structures_spec.rb +169 -0
- data/spec/schema_walk_spec.rb +42 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/struct_spec.rb +324 -0
- data/spec/subschema_spec.rb +178 -0
- data/spec/validators_spec.rb +86 -0
- data/spec/whitelist_spec.rb +97 -0
- metadata +162 -0
@@ -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,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,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
|