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
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "paradocs"
|
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,123 @@
|
|
1
|
+
module Paradocs
|
2
|
+
class BasePolicy
|
3
|
+
def self.build(name, meth, &block)
|
4
|
+
klass = Class.new(self)
|
5
|
+
klass.public_send(meth, &block)
|
6
|
+
klass.policy_name = name
|
7
|
+
klass
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.message(&block)
|
11
|
+
@message_block = block if block_given?
|
12
|
+
@message_block
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.validate(&validate_block)
|
16
|
+
@validate_block = validate_block if block_given?
|
17
|
+
@validate_block
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.coerce(&coerce_block)
|
21
|
+
@coerce_block = coerce_block if block_given?
|
22
|
+
@coerce_block
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.eligible(&block)
|
26
|
+
@eligible_block = block if block_given?
|
27
|
+
@eligible_block
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.meta_data(&block)
|
31
|
+
@meta_data_block = block if block_given?
|
32
|
+
@meta_data_block
|
33
|
+
end
|
34
|
+
|
35
|
+
%w(error silent_error).each do |name|
|
36
|
+
getter = "#{name}s"
|
37
|
+
define_singleton_method(getter) do
|
38
|
+
parent_errors = superclass.respond_to?(getter) ? superclass.send(getter) : []
|
39
|
+
parent_errors | (instance_variable_get("@#{getter}") || instance_variable_set("@#{getter}", []))
|
40
|
+
end
|
41
|
+
|
42
|
+
define_singleton_method("register_#{name}") do |*exceptions| # TODO: spec
|
43
|
+
# [Exception, as: :helper_method] or [Exception1, Exception2....]
|
44
|
+
only_errors = []
|
45
|
+
exceptions.each_with_index do |ex, index|
|
46
|
+
if ex.is_a? Hash
|
47
|
+
exception = exceptions[index - 1]
|
48
|
+
define_method(ex[:as]) { exception }
|
49
|
+
next
|
50
|
+
end
|
51
|
+
next unless ex.is_a?(Class) || ex < StandardError
|
52
|
+
only_errors << ex
|
53
|
+
end
|
54
|
+
instance_variable_set("@#{getter}", ((self.public_send(getter) || []) + only_errors).uniq)
|
55
|
+
end
|
56
|
+
|
57
|
+
define_method(getter) do
|
58
|
+
instance_variable_set("@#{getter}", self.class.send("register_#{name}") || [])
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.policy_name=(name)
|
63
|
+
@policy_name = name
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.policy_name
|
67
|
+
@policy_name || self.name.split("::").last.downcase.to_sym
|
68
|
+
end
|
69
|
+
|
70
|
+
attr_accessor :environment
|
71
|
+
def initialize(*args)
|
72
|
+
@init_params = args
|
73
|
+
end
|
74
|
+
|
75
|
+
def eligible?(value, key, payload)
|
76
|
+
args = (init_params + [value, key, payload])
|
77
|
+
(self.class.eligible || ->(*) { true }).call(*args)
|
78
|
+
end
|
79
|
+
|
80
|
+
def coerce(value, key, context)
|
81
|
+
(self.class.coerce || ->(v, *_) { v }).call(value, key, context)
|
82
|
+
end
|
83
|
+
|
84
|
+
def valid?(value, key, payload)
|
85
|
+
args = (init_params + [value, key, payload])
|
86
|
+
@message = self.class.message.call(*args) if self.class.message
|
87
|
+
validate(*args)
|
88
|
+
end
|
89
|
+
|
90
|
+
def meta_data
|
91
|
+
return self.class.meta_data.call(*init_params) if self.class.meta_data
|
92
|
+
meta
|
93
|
+
end
|
94
|
+
|
95
|
+
def validate(*args)
|
96
|
+
(self.class.validate || ->(*) { true }).call(*args)
|
97
|
+
end
|
98
|
+
|
99
|
+
def policy_name
|
100
|
+
(self.class.policy_name || self.to_s.demodulize.underscore).to_sym
|
101
|
+
end
|
102
|
+
|
103
|
+
def message
|
104
|
+
@message ||= 'is invalid'
|
105
|
+
end
|
106
|
+
|
107
|
+
protected
|
108
|
+
|
109
|
+
def validate(*args)
|
110
|
+
(self.class.validate || ->(*args) { true }).call(*args)
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def meta
|
116
|
+
@meta = {self.class.policy_name => {errors: self.class.errors}}
|
117
|
+
end
|
118
|
+
|
119
|
+
def init_params
|
120
|
+
@init_params ||= [] # safe default if #initialize was overwritten
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Paradocs
|
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
|
+
attr_reader :environment
|
17
|
+
def initialize(path=nil, top=Top.new, environment={}, subschemes={})
|
18
|
+
@top = top
|
19
|
+
@path = Array(path).compact
|
20
|
+
@environment = environment
|
21
|
+
@subschemes = subschemes
|
22
|
+
end
|
23
|
+
|
24
|
+
def subschema(subschema_name)
|
25
|
+
subschema = @subschemes[subschema_name]
|
26
|
+
return unless subschema
|
27
|
+
@subschemes.merge!(subschema.subschemes)
|
28
|
+
subschema
|
29
|
+
end
|
30
|
+
|
31
|
+
def errors
|
32
|
+
top.errors
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_error(msg)
|
36
|
+
top.add_error(string_path, msg)
|
37
|
+
end
|
38
|
+
|
39
|
+
def sub(key)
|
40
|
+
self.class.new(path + [key], top, environment, @subschemes)
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
attr_reader :path, :top
|
45
|
+
|
46
|
+
def string_path
|
47
|
+
path.reduce(['$']) do |m, segment|
|
48
|
+
m << (segment.is_a?(Integer) ? "[#{segment}]" : ".#{segment}")
|
49
|
+
m
|
50
|
+
end.join
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require "date"
|
2
|
+
|
3
|
+
module Paradocs
|
4
|
+
# type coercions
|
5
|
+
Paradocs.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
|
+
Paradocs.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
|
+
Paradocs.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
|
+
Paradocs.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
|
+
Paradocs.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
|
+
Paradocs.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
|
+
Paradocs.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
|
+
Paradocs.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
|
data/lib/paradocs/dsl.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
require "paradocs"
|
2
|
+
|
3
|
+
module Paradocs
|
4
|
+
module DSL
|
5
|
+
# Example
|
6
|
+
# class Foo
|
7
|
+
# include Paradocs::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
|
+
|
26
|
+
def self.included(base)
|
27
|
+
base.extend(ClassMethods)
|
28
|
+
base.schemas = {Paradocs.config.default_schema_name => Paradocs::Schema.new}
|
29
|
+
end
|
30
|
+
|
31
|
+
module ClassMethods
|
32
|
+
def schema=(sc)
|
33
|
+
@schemas[Paradocs.config.default_schema_name] = sc
|
34
|
+
end
|
35
|
+
|
36
|
+
def schemas=(sc)
|
37
|
+
@schemas = sc
|
38
|
+
end
|
39
|
+
|
40
|
+
def inherited(subclass)
|
41
|
+
subclass.schemas = @schemas.each_with_object({}) do |(key, sc), hash|
|
42
|
+
hash[key] = sc.merge(Paradocs::Schema.new)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def schema(*args, &block)
|
47
|
+
options = args.last.is_a?(Hash) ? args.last : {}
|
48
|
+
key = args.first.is_a?(Symbol) ? args.shift : Paradocs.config.default_schema_name
|
49
|
+
current_schema = @schemas.fetch(key) { Paradocs::Schema.new }
|
50
|
+
new_schema = if block_given? || options.any?
|
51
|
+
Paradocs::Schema.new(options, &block)
|
52
|
+
elsif args.first.is_a?(Paradocs::Schema)
|
53
|
+
args.first
|
54
|
+
end
|
55
|
+
|
56
|
+
return current_schema unless new_schema
|
57
|
+
|
58
|
+
@schemas[key] = current_schema ? current_schema.merge(new_schema) : new_schema
|
59
|
+
paradocs_after_define_schema(@schemas[key])
|
60
|
+
@schemas[key]
|
61
|
+
end
|
62
|
+
|
63
|
+
def paradocs_after_define_schema(sc)
|
64
|
+
# noop hook
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Paradocs
|
2
|
+
module Extensions
|
3
|
+
module Insides
|
4
|
+
def structure(ignore_transparent: true, root: "", &block)
|
5
|
+
flush!
|
6
|
+
fields.each_with_object({meta_keys[:errors] => [], meta_keys[:subschemes] => {}}) do |(_, field), obj|
|
7
|
+
meta, sc = collect_meta(field, root)
|
8
|
+
if sc
|
9
|
+
meta[:structure] = sc.structure(ignore_transparent: ignore_transparent, root: meta[:json_path], &block)
|
10
|
+
obj[meta_keys[:errors]] += meta[:structure].delete(meta_keys[:errors])
|
11
|
+
else
|
12
|
+
obj[meta_keys[:errors]] += field.possible_errors
|
13
|
+
end
|
14
|
+
obj[field.key] = meta unless ignore_transparent && field.transparent?
|
15
|
+
yield(field.key, meta) if block_given?
|
16
|
+
|
17
|
+
next unless field.mutates_schema?
|
18
|
+
subschemes.each do |name, subschema|
|
19
|
+
obj[meta_keys[:subschemes]][name] = subschema.structure(ignore_transparent: ignore_transparent, root: root, &block)
|
20
|
+
obj[meta_keys[:errors]] += obj[meta_keys[:subschemes]][name][meta_keys[:errors]]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def flatten_structure(ignore_transparent: true, root: "", &block)
|
26
|
+
flush!
|
27
|
+
fields.each_with_object({meta_keys[:errors] => [], meta_keys[:subschemes] => {}}) do |(_, field), obj|
|
28
|
+
meta, sc = collect_meta(field, root)
|
29
|
+
humanized_name = meta.delete(:nested_name)
|
30
|
+
obj[humanized_name] = meta unless ignore_transparent && field.transparent?
|
31
|
+
|
32
|
+
if sc
|
33
|
+
deep_result = sc.flatten_structure(ignore_transparent: ignore_transparent, root: meta[:json_path], &block)
|
34
|
+
obj[meta_keys[:errors]] += deep_result.delete(meta_keys[:errors])
|
35
|
+
obj[meta_keys[:subschemes]].merge!(deep_result.delete(meta_keys[:subschemes]))
|
36
|
+
obj.merge!(deep_result)
|
37
|
+
else
|
38
|
+
obj[meta_keys[:errors]] += field.possible_errors
|
39
|
+
end
|
40
|
+
yield(humanized_name, meta) if block_given?
|
41
|
+
next unless field.mutates_schema?
|
42
|
+
subschemes.each do |name, subschema|
|
43
|
+
obj[meta_keys[:subschemes]][name] ||= subschema.flatten_structure(ignore_transparent: ignore_transparent, root: root, &block)
|
44
|
+
obj[meta_keys[:errors]] += obj[meta_keys[:subschemes]][name][meta_keys[:errors]]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def walk(meta_key = nil, &visitor)
|
50
|
+
r = visit(meta_key, &visitor)
|
51
|
+
Results.new(r, {}, {})
|
52
|
+
end
|
53
|
+
|
54
|
+
def visit(meta_key = nil, &visitor)
|
55
|
+
fields.each_with_object({}) do |(_, field), m|
|
56
|
+
m[field.key] = field.visit(meta_key, &visitor)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def collect_meta(field, root)
|
63
|
+
json_path = root.empty? ? "$.#{field.key}" : "#{root}.#{field.key}"
|
64
|
+
meta = field.meta_data.merge(json_path: json_path)
|
65
|
+
sc = meta.delete(:schema)
|
66
|
+
meta[:mutates_schema] = true if meta.delete(:mutates_schema)
|
67
|
+
json_path << "[]" if meta[:type] == :array
|
68
|
+
meta[:nested_name] = json_path.gsub("[]", "")[2..-1]
|
69
|
+
[meta, sc]
|
70
|
+
end
|
71
|
+
|
72
|
+
def meta_keys
|
73
|
+
%i(errors subschemes).map! { |key| [key, "#{Paradocs.config.meta_prefix}#{key}".to_sym] }.to_h
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require "paradocs/field_dsl"
|
2
|
+
|
3
|
+
module Paradocs
|
4
|
+
class Field
|
5
|
+
include FieldDSL
|
6
|
+
|
7
|
+
attr_reader :key, :meta_data
|
8
|
+
Result = Struct.new(:eligible?, :value)
|
9
|
+
|
10
|
+
def initialize(key)
|
11
|
+
@key = key
|
12
|
+
@policies = []
|
13
|
+
@default_block = nil
|
14
|
+
@meta_data = {}
|
15
|
+
@policies = []
|
16
|
+
@mutation_block = nil
|
17
|
+
@expects_mutation = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def meta(hash = nil)
|
21
|
+
@meta_data = @meta_data.merge(hash) if hash.is_a?(Hash)
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def possible_errors
|
26
|
+
meta_data.map { |_, v| v[:errors] if v.is_a?(Hash) }.flatten.compact
|
27
|
+
end
|
28
|
+
|
29
|
+
def default(value)
|
30
|
+
meta default: value
|
31
|
+
@default_block = (value.respond_to?(:call) ? value : ->(key, payload, context) { value })
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def mutates_schema!(&block)
|
36
|
+
@mutation_block ||= block if block_given?
|
37
|
+
@expects_mutation = @expects_mutation.nil? && true
|
38
|
+
meta mutates_schema: @mutation_block
|
39
|
+
@mutation_block
|
40
|
+
end
|
41
|
+
|
42
|
+
def mutates_schema?
|
43
|
+
!!@mutation_block
|
44
|
+
end
|
45
|
+
|
46
|
+
def expects_mutation?
|
47
|
+
mutates_schema? && @expects_mutation
|
48
|
+
end
|
49
|
+
|
50
|
+
def policy(key, *args)
|
51
|
+
pol = lookup(key, args)
|
52
|
+
|
53
|
+
meta pol.meta_data
|
54
|
+
policies << pol
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
alias_method :type, :policy
|
59
|
+
alias_method :rule, :policy
|
60
|
+
|
61
|
+
def schema(sc = nil, &block)
|
62
|
+
sc = (sc ? sc : Schema.new(&block))
|
63
|
+
meta schema: sc
|
64
|
+
policy sc.schema
|
65
|
+
end
|
66
|
+
|
67
|
+
def transparent?
|
68
|
+
!!meta_data[:transparent]
|
69
|
+
end
|
70
|
+
|
71
|
+
def visit(meta_key = nil, &visitor)
|
72
|
+
if sc = meta_data[:schema]
|
73
|
+
r = sc.visit(meta_key, &visitor)
|
74
|
+
(meta_data[:type] == :array) ? [r] : r
|
75
|
+
else
|
76
|
+
meta_key ? meta_data[meta_key] : yield(self)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def subschema_for_mutation(payload, env)
|
81
|
+
subschema_name = @mutation_block.call(payload[key], key, payload, env) if @mutation_block
|
82
|
+
@expects_mutation = false
|
83
|
+
subschema_name
|
84
|
+
end
|
85
|
+
|
86
|
+
def resolve(payload, context)
|
87
|
+
eligible = payload.key?(key)
|
88
|
+
value = payload[key] # might be nil
|
89
|
+
|
90
|
+
if !eligible && has_default?
|
91
|
+
eligible = true
|
92
|
+
value = default_block.call(key, payload, context)
|
93
|
+
payload[key] = value
|
94
|
+
end
|
95
|
+
policies.each do |policy|
|
96
|
+
# pass schema additional data to the each policy
|
97
|
+
policy.environment = context.environment if policy.respond_to?(:environment=)
|
98
|
+
if !policy.eligible?(value, key, payload)
|
99
|
+
eligible = false
|
100
|
+
if has_default?
|
101
|
+
eligible = true
|
102
|
+
value = default_block.call(key, payload, context)
|
103
|
+
end
|
104
|
+
break
|
105
|
+
else
|
106
|
+
value, valid = resolve_one(policy, value, payload, context)
|
107
|
+
|
108
|
+
unless valid
|
109
|
+
eligible = true # eligible, but has errors
|
110
|
+
break # only one error at a time
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
Result.new(eligible, value)
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
attr_reader :policies, :default_block
|
120
|
+
|
121
|
+
def resolve_one(policy, value, payload, context)
|
122
|
+
begin
|
123
|
+
value = policy.coerce(value, key, context)
|
124
|
+
valid = policy.valid?(value, key, payload)
|
125
|
+
|
126
|
+
context.add_error(policy.message) unless valid
|
127
|
+
[value, valid]
|
128
|
+
rescue *(policy.try(:errors) || []) => e
|
129
|
+
# context.add_error e.message # NOTE: do we need it?
|
130
|
+
raise e
|
131
|
+
rescue *(policy.try(:silent_errors) || []) => e
|
132
|
+
context.add_error e.message
|
133
|
+
rescue StandardError => e
|
134
|
+
raise e if policy.is_a? Paradocs::Schema # from the inner level, just reraise
|
135
|
+
raise ConfigurationError.new("#{e.class} should be registered in the policy") if Paradocs.config.explicit_errors
|
136
|
+
context.add_error policy.message unless Paradocs.config.explicit_errors
|
137
|
+
[value, false]
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def has_default?
|
142
|
+
!!default_block
|
143
|
+
end
|
144
|
+
|
145
|
+
def lookup(key, args)
|
146
|
+
obj = key.is_a?(Symbol) ? Paradocs.registry.policies[key] : key
|
147
|
+
|
148
|
+
raise ConfigurationError, "No policies defined for #{key.inspect}" unless obj
|
149
|
+
obj.respond_to?(:new) ? obj.new(*args) : obj
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Paradocs
|
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
|
+
|
24
|
+
def whitelisted
|
25
|
+
policy :whitelisted
|
26
|
+
end
|
27
|
+
|
28
|
+
def transparent
|
29
|
+
meta transparent: true
|
30
|
+
end
|
31
|
+
|
32
|
+
def length(opts)
|
33
|
+
policy :length, opts
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|