paradocs 1.0.22

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
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
@@ -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