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