paradocs 1.0.22 → 1.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.readthedocs.yml +5 -0
- data/README.md +7 -1045
- data/docs/changelog.md +17 -0
- data/docs/custom_configuration.md +10 -0
- data/docs/documentation_generation.md +304 -0
- data/docs/faq.md +21 -0
- data/docs/form_objects_dsl.md +90 -0
- data/docs/index.md +106 -0
- data/docs/payload_builder.md +105 -0
- data/docs/policies.md +309 -0
- data/docs/schema.md +294 -0
- data/docs/struct.md +135 -0
- data/docs/subschema.md +29 -0
- data/lib/paradocs.rb +2 -1
- data/lib/paradocs/extensions/payload_builder.rb +44 -0
- data/lib/paradocs/extensions/structure.rb +116 -0
- data/lib/paradocs/policies.rb +0 -1
- data/lib/paradocs/schema.rb +30 -9
- data/lib/paradocs/struct.rb +4 -4
- data/lib/paradocs/version.rb +1 -1
- data/lib/paradocs/whitelist.rb +11 -10
- data/mkdocs.yml +17 -0
- data/paradocs.gemspec +4 -4
- data/requirements.txt +1 -0
- data/spec/extensions/payload_builder_spec.rb +70 -0
- data/spec/extensions/structures_spec.rb +244 -0
- data/spec/schema_spec.rb +1 -1
- data/spec/struct_spec.rb +37 -9
- data/spec/subschema_spec.rb +4 -4
- data/spec/whitelist_spec.rb +33 -0
- metadata +27 -10
- data/lib/paradocs/extensions/insides.rb +0 -77
- data/spec/schema_structures_spec.rb +0 -169
data/docs/struct.md
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
# Structs
|
2
|
+
## Overview
|
3
|
+
Structs turn schema definitions into objects graphs with attribute readers.
|
4
|
+
|
5
|
+
Add optional `Paradocs::Struct` module to define struct-like objects with schema definitions.
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
require 'parametric/struct'
|
9
|
+
|
10
|
+
class User
|
11
|
+
include Paradocs::Struct
|
12
|
+
|
13
|
+
schema do
|
14
|
+
field(:name).type(:string).present
|
15
|
+
field(:friends).type(:array).schema do
|
16
|
+
field(:name).type(:string).present
|
17
|
+
field(:age).type(:integer)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
```
|
22
|
+
|
23
|
+
`User` objects can be instantiated with hash data, which will be coerced and validated as per the schema definition.
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
user = User.new(
|
27
|
+
name: 'Joe',
|
28
|
+
friends: [
|
29
|
+
{name: 'Jane', age: 40},
|
30
|
+
{name: 'John', age: 30},
|
31
|
+
]
|
32
|
+
)
|
33
|
+
|
34
|
+
# properties
|
35
|
+
user.name # => 'Joe'
|
36
|
+
user.friends.first.name # => 'Jane'
|
37
|
+
user.friends.last.age # => 30
|
38
|
+
```
|
39
|
+
|
40
|
+
## Errors
|
41
|
+
|
42
|
+
Both the top-level and nested instances contain error information:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
user = User.new(
|
46
|
+
name: '', # invalid
|
47
|
+
friends: [
|
48
|
+
# friend name also invalid
|
49
|
+
{name: '', age: 40},
|
50
|
+
]
|
51
|
+
)
|
52
|
+
|
53
|
+
user.valid? # false
|
54
|
+
user.errors['$.name'] # => "is required and must be present"
|
55
|
+
user.errors['$.friends[0].name'] # => "is required and must be present"
|
56
|
+
|
57
|
+
# also access error in nested instances directly
|
58
|
+
user.friends.first.valid? # false
|
59
|
+
user.friends.first.errors['$.name'] # "is required and must be valid"
|
60
|
+
```
|
61
|
+
|
62
|
+
## .new!(hash)
|
63
|
+
|
64
|
+
Instantiating structs with `.new!(hash)` will raise a `Paradocs::InvalidStructError` exception if the data is validations fail. It will return the struct instance otherwise.
|
65
|
+
|
66
|
+
`Paradocs::InvalidStructError` includes an `#errors` property to inspect the errors raised.
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
begin
|
70
|
+
user = User.new!(name: '')
|
71
|
+
rescue Paradocs::InvalidStructError => e
|
72
|
+
e.errors['$.name'] # "is required and must be present"
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
## Nested structs
|
77
|
+
|
78
|
+
You can also pass separate struct classes in a nested schema definition.
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
class Friend
|
82
|
+
include Paradocs::Struct
|
83
|
+
|
84
|
+
schema do
|
85
|
+
field(:name).type(:string).present
|
86
|
+
field(:age).type(:integer)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class User
|
91
|
+
include Paradocs::Struct
|
92
|
+
|
93
|
+
schema do
|
94
|
+
field(:name).type(:string).present
|
95
|
+
# here we use the Friend class
|
96
|
+
field(:friends).type(:array).schema Friend
|
97
|
+
end
|
98
|
+
end
|
99
|
+
```
|
100
|
+
|
101
|
+
## Inheritance
|
102
|
+
|
103
|
+
Struct subclasses can add to inherited schemas, or override fields defined in the parent.
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
class AdminUser < User
|
107
|
+
# inherits User schema, and can add stuff to its own schema
|
108
|
+
schema do
|
109
|
+
field(:permissions).type(:array)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
## #to_h
|
115
|
+
|
116
|
+
`Struct#to_h` returns the ouput hash, with values coerced and any defaults populated.
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
class User
|
120
|
+
include Paradocs::Struct
|
121
|
+
schema do
|
122
|
+
field(:name).type(:string)
|
123
|
+
field(:age).type(:integer).default(30)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
user = User.new(name: "Joe")
|
128
|
+
user.to_h # {name: "Joe", age: 30}
|
129
|
+
```
|
130
|
+
|
131
|
+
## Struct equality
|
132
|
+
|
133
|
+
`Paradocs::Struct` implements `#==()` to compare two structs Hash representation (same as `struct1.to_h.eql?(struct2.to_h)`.
|
134
|
+
|
135
|
+
Users can override `#==()` in their own classes to do whatever they need.
|
data/docs/subschema.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Subchemas
|
2
|
+
> When your schema can change on-the-fly.
|
3
|
+
|
4
|
+
## Subschemas and mutations.
|
5
|
+
Sometimes depending on the data the structure may vary. Most frequently used option is to use `:declared` policy (a.k.a. conditional, but below is another option:
|
6
|
+
|
7
|
+
- Mutations are blocks that are assigned to a field and called during the validation (in #resolve), block receives all the related data and should return a subschema name.
|
8
|
+
- Subschemas are conditional schemas declared inside schemas. They doesn't exist until mutation block is called and decides to invoke a subschema.
|
9
|
+
```ruby
|
10
|
+
person_schema = Paradocs::Schema.new do
|
11
|
+
field(:role).type(:string).options(["admin", "user"]).mutates_schema! do |value, key, payload, env|
|
12
|
+
value == :admin ? :admin_schema : :user_schema
|
13
|
+
end
|
14
|
+
|
15
|
+
subschema(:admin_schema) do
|
16
|
+
field(:permissions).present.type(:string).options(["superuser"])
|
17
|
+
field(:admin_field)
|
18
|
+
end
|
19
|
+
subschema(:user_schema) do
|
20
|
+
field(:permissions).present.type(:string).options(["readonly"])
|
21
|
+
field(:user_field)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
results = person_schema.resolve(name: "John", age: 20, role: :admin, permissions: "superuser")
|
26
|
+
results.output # => {name: "John", age: 20, role: :admin, permissions: "superuser", admin_field: nil}
|
27
|
+
results = person_schema.resolve(name: "John", age: 20, role: :admin, permissions: "readonly")
|
28
|
+
results.errors => {"$.permissions"=>["must be one of superuser, but got readonly"]}
|
29
|
+
```
|
data/lib/paradocs.rb
CHANGED
@@ -0,0 +1,44 @@
|
|
1
|
+
module Paradocs
|
2
|
+
module Extensions
|
3
|
+
class PayloadBuilder
|
4
|
+
attr_reader :structure, :result
|
5
|
+
attr_accessor :skip_word
|
6
|
+
def initialize(schema, skip_word: :skip)
|
7
|
+
@structure = schema.structure
|
8
|
+
@skip_word = skip_word
|
9
|
+
end
|
10
|
+
|
11
|
+
def build!(sort_by_schema: false, &block)
|
12
|
+
result = structure.all_nested.map { |name, struct| [name, build_simple_structure(struct, &block)] }.to_h
|
13
|
+
sort_by_schema ? schema.resolve(result).output : result
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def build_simple_structure(struct, &block)
|
19
|
+
struct.map do |key, value|
|
20
|
+
key = key.to_s
|
21
|
+
next if key.start_with?(Paradocs.config.meta_prefix) # skip all the meta fields
|
22
|
+
ex_value = restore_one(key, value, &block)
|
23
|
+
next if ex_value == @skip_word
|
24
|
+
[key, ex_value]
|
25
|
+
end.compact.to_h
|
26
|
+
end
|
27
|
+
|
28
|
+
def restore_one(key, value, &block)
|
29
|
+
default = value[:default]
|
30
|
+
ex_value = if value[:structure]
|
31
|
+
data = build_simple_structure(value[:structure], &block)
|
32
|
+
value[:type] == :array ? [data] : data
|
33
|
+
elsif default
|
34
|
+
default.is_a?(Proc) ? default.call : default
|
35
|
+
elsif value[:options] && !value[:options].empty?
|
36
|
+
options = value[:options]
|
37
|
+
value[:type] == :array ? options : options.sample
|
38
|
+
end
|
39
|
+
return ex_value unless block_given?
|
40
|
+
yield(key, value, ex_value, @skip_word)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module Paradocs
|
2
|
+
module Extensions
|
3
|
+
class Structure
|
4
|
+
DEFAULT = :generic
|
5
|
+
%w(errors subschemes).each do |key|
|
6
|
+
define_method(key) { "#{Paradocs.config.meta_prefix}#{key}".to_sym }
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :schema, :ignore_transparent, :root
|
10
|
+
attr_accessor :ignore_transparent
|
11
|
+
def initialize(schema, ignore_transparent=true, root="")
|
12
|
+
@schema = schema
|
13
|
+
@ignore_transparent = ignore_transparent
|
14
|
+
@root = root
|
15
|
+
end
|
16
|
+
|
17
|
+
def nested(&block)
|
18
|
+
schema.fields.each_with_object({errors => [], subschemes => {}}) do |(_, field), result|
|
19
|
+
meta, sc = collect_meta(field, root)
|
20
|
+
if sc
|
21
|
+
meta[:structure] = self.class.new(sc, ignore_transparent, meta[:json_path]).nested(&block)
|
22
|
+
result[errors] += meta[:structure].delete(errors)
|
23
|
+
else
|
24
|
+
result[errors] += field.possible_errors
|
25
|
+
end
|
26
|
+
result[field.key] = meta unless ignore_transparent && field.transparent?
|
27
|
+
yield(field.key, meta) if block_given?
|
28
|
+
|
29
|
+
next unless field.mutates_schema?
|
30
|
+
schema.subschemes.each do |name, subschema|
|
31
|
+
result[subschemes][name] = self.class.new(subschema, ignore_transparent, root).nested(&block)
|
32
|
+
result[errors] += result[subschemes][name][errors]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def all_nested(&block)
|
38
|
+
all_flatten(&block).each_with_object({}) do |(name, struct), obj|
|
39
|
+
obj[name] = {}
|
40
|
+
# sort the flatten struct to have iterated 1lvl keys before 2lvl and so on...
|
41
|
+
struct.sort_by { |k, v| k.to_s.count(".") }.each do |key, value|
|
42
|
+
target = obj[name]
|
43
|
+
key, value = key.to_s, value.clone # clone the values, because we do mutation below
|
44
|
+
value.merge!(nested_name: key) if value.respond_to?(:merge) # it can be array (_errors)
|
45
|
+
next target[key.to_sym] = value if key.start_with?(Paradocs.config.meta_prefix) # copy meta fields
|
46
|
+
|
47
|
+
parts = key.split(".")
|
48
|
+
next target[key] ||= value if parts.size == 1 # copy 1lvl key
|
49
|
+
parts.each.with_index do |subkey, index|
|
50
|
+
target[subkey] ||= value
|
51
|
+
next if parts.size == index + 1
|
52
|
+
target[subkey][:structure] ||= {}
|
53
|
+
target = target[subkey][:structure] # target goes deeper for each part
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def all_flatten(schema_structure=nil, &block)
|
60
|
+
schema_structure ||= flatten(&block)
|
61
|
+
if schema_structure[subschemes].empty?
|
62
|
+
schema_structure.delete(subschemes) # don't include redundant key
|
63
|
+
return {DEFAULT => schema_structure}
|
64
|
+
end
|
65
|
+
schema_structure[subschemes].each_with_object({}) do |(name, subschema), result|
|
66
|
+
if subschema[subschemes].empty?
|
67
|
+
result[name] = schema_structure.merge(subschema)
|
68
|
+
result[name][errors] += schema_structure[errors]
|
69
|
+
result[name][errors].uniq!
|
70
|
+
result[name].delete(subschemes)
|
71
|
+
next result[name]
|
72
|
+
end
|
73
|
+
|
74
|
+
all_flatten(subschema).each do |sub_name, schema|
|
75
|
+
result["#{name}_#{sub_name}".to_sym] = schema_structure.merge(schema)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def flatten(&block)
|
81
|
+
schema.fields.each_with_object({errors => [], subschemes => {}}) do |(_, field), obj|
|
82
|
+
meta, sc = collect_meta(field, root)
|
83
|
+
humanized_name = meta.delete(:nested_name)
|
84
|
+
obj[humanized_name] = meta unless ignore_transparent && field.transparent?
|
85
|
+
|
86
|
+
if sc
|
87
|
+
deep_result = self.class.new(sc, ignore_transparent, meta[:json_path]).flatten(&block)
|
88
|
+
obj[errors] += deep_result.delete(errors)
|
89
|
+
obj[subschemes].merge!(deep_result.delete(subschemes))
|
90
|
+
obj.merge!(deep_result)
|
91
|
+
else
|
92
|
+
obj[errors] += field.possible_errors
|
93
|
+
end
|
94
|
+
yield(humanized_name, meta) if block_given?
|
95
|
+
next unless field.mutates_schema?
|
96
|
+
schema.subschemes.each do |name, subschema|
|
97
|
+
obj[subschemes][name] ||= self.class.new(subschema, ignore_transparent, root).flatten(&block)
|
98
|
+
obj[errors] += obj[subschemes][name][errors]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def collect_meta(field, root)
|
106
|
+
json_path = root.empty? ? "$.#{field.key}" : "#{root}.#{field.key}"
|
107
|
+
meta = field.meta_data.merge(json_path: json_path)
|
108
|
+
sc = meta.delete(:schema)
|
109
|
+
meta[:mutates_schema] = true if meta.delete(:mutates_schema)
|
110
|
+
json_path << "[]" if meta[:type] == :array
|
111
|
+
meta[:nested_name] = json_path.gsub("[]", "")[2..-1]
|
112
|
+
[meta, sc]
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/lib/paradocs/policies.rb
CHANGED
data/lib/paradocs/schema.rb
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
require "paradocs/context"
|
2
2
|
require "paradocs/results"
|
3
3
|
require "paradocs/field"
|
4
|
-
require "paradocs/extensions/
|
4
|
+
require "paradocs/extensions/structure"
|
5
|
+
require "paradocs/extensions/payload_builder"
|
5
6
|
|
6
7
|
module Paradocs
|
7
8
|
class Schema
|
8
|
-
include Extensions::Insides
|
9
|
-
|
10
9
|
attr_accessor :environment
|
11
|
-
attr_reader :subschemes
|
10
|
+
attr_reader :subschemes, :structure_builder
|
12
11
|
def initialize(options={}, &block)
|
13
12
|
@options = options
|
14
13
|
@fields = {}
|
@@ -18,6 +17,7 @@ module Paradocs
|
|
18
17
|
@default_field_policies = []
|
19
18
|
@ignored_field_keys = []
|
20
19
|
@expansions = {}
|
20
|
+
@structure_builder = Paradocs::Extensions::Structure.new(self)
|
21
21
|
end
|
22
22
|
|
23
23
|
def schema
|
@@ -29,6 +29,27 @@ module Paradocs
|
|
29
29
|
f.mutates_schema!(&block)
|
30
30
|
end
|
31
31
|
|
32
|
+
def structure(ignore_transparent: true)
|
33
|
+
flush!
|
34
|
+
structure_builder.ignore_transparent = ignore_transparent
|
35
|
+
structure_builder
|
36
|
+
end
|
37
|
+
|
38
|
+
def example_payloads(&block)
|
39
|
+
@example_payloads ||= Paradocs::Extensions::PayloadBuilder.new(self).build!(&block)
|
40
|
+
end
|
41
|
+
|
42
|
+
def walk(meta_key = nil, &visitor)
|
43
|
+
r = visit(meta_key, &visitor)
|
44
|
+
Results.new(r, {}, {})
|
45
|
+
end
|
46
|
+
|
47
|
+
def visit(meta_key = nil, &visitor)
|
48
|
+
fields.each_with_object({}) do |(_, field), m|
|
49
|
+
m[field.key] = field.visit(meta_key, &visitor)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
32
53
|
def subschema(*args, &block)
|
33
54
|
options = args.last.is_a?(Hash) ? args.last : {}
|
34
55
|
name = args.first.is_a?(Symbol) ? args.shift : Paradocs.config.default_schema_name
|
@@ -143,6 +164,11 @@ module Paradocs
|
|
143
164
|
end
|
144
165
|
end
|
145
166
|
|
167
|
+
def flush!
|
168
|
+
@fields = {}
|
169
|
+
@applied = false
|
170
|
+
end
|
171
|
+
|
146
172
|
protected
|
147
173
|
|
148
174
|
attr_reader :definitions, :options
|
@@ -205,10 +231,5 @@ module Paradocs
|
|
205
231
|
end
|
206
232
|
@applied = true
|
207
233
|
end
|
208
|
-
|
209
|
-
def flush!
|
210
|
-
@fields = {}
|
211
|
-
@applied = false
|
212
|
-
end
|
213
234
|
end
|
214
235
|
end
|
data/lib/paradocs/struct.rb
CHANGED
@@ -18,8 +18,8 @@ module Paradocs
|
|
18
18
|
base.extend ClassMethods
|
19
19
|
end
|
20
20
|
|
21
|
-
def initialize(attrs = {})
|
22
|
-
@_results = self.class.schema.resolve(attrs)
|
21
|
+
def initialize(attrs = {}, environment = {})
|
22
|
+
@_results = self.class.schema.resolve(attrs, environment)
|
23
23
|
@_graph = self.class.build(@_results.output)
|
24
24
|
end
|
25
25
|
|
@@ -48,8 +48,8 @@ module Paradocs
|
|
48
48
|
attr_reader :_graph, :_results
|
49
49
|
|
50
50
|
module ClassMethods
|
51
|
-
def new!(attrs = {})
|
52
|
-
st = new(attrs)
|
51
|
+
def new!(attrs = {}, environment = {})
|
52
|
+
st = new(attrs, environment)
|
53
53
|
raise InvalidStructError.new(st) unless st.valid?
|
54
54
|
st
|
55
55
|
end
|
data/lib/paradocs/version.rb
CHANGED