paradocs 1.0.24 → 1.1.0
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 +4 -4
- data/.readthedocs.yml +5 -0
- data/docs/custom_configuration.md +10 -0
- data/docs/documentation_generation.md +291 -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 +104 -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/extensions/payload_builder.rb +43 -0
- data/lib/paradocs/extensions/structure.rb +118 -0
- data/lib/paradocs/schema.rb +30 -9
- data/lib/paradocs/version.rb +1 -1
- data/mkdocs.yml +16 -0
- data/paradocs.gemspec +2 -2
- data/requirements.txt +1 -0
- data/spec/extensions/payload_builder_spec.rb +70 -0
- data/spec/extensions/structures_spec.rb +237 -0
- data/spec/schema_spec.rb +1 -1
- data/spec/subschema_spec.rb +7 -4
- metadata +26 -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
|
+
```
|
@@ -0,0 +1,43 @@
|
|
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!(&block)
|
12
|
+
structure.all_nested.map { |name, struct| [name, build_simple_structure(struct, &block)] }.to_h
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def build_simple_structure(struct, &block)
|
18
|
+
struct.map do |key, value|
|
19
|
+
key = key.to_s
|
20
|
+
next if key.start_with?(Paradocs.config.meta_prefix) # skip all the meta fields
|
21
|
+
ex_value = restore_one(key, value, &block)
|
22
|
+
next if ex_value == @skip_word
|
23
|
+
[key, ex_value]
|
24
|
+
end.compact.to_h
|
25
|
+
end
|
26
|
+
|
27
|
+
def restore_one(key, value, &block)
|
28
|
+
default = value[:default]
|
29
|
+
ex_value = if value[:structure]
|
30
|
+
data = build_simple_structure(value[:structure], &block)
|
31
|
+
value[:type] == :array ? [data] : data
|
32
|
+
elsif default
|
33
|
+
default.is_a?(Proc) ? default.call : default
|
34
|
+
elsif value[:options] && !value[:options].empty?
|
35
|
+
options = value[:options]
|
36
|
+
value[:type] == :array ? options : options.sample
|
37
|
+
end
|
38
|
+
return ex_value unless block_given?
|
39
|
+
yield(key, value, ex_value, @skip_word)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,118 @@
|
|
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 flush!
|
18
|
+
@nested, @all_nested, @flatten, @all_flatten = [nil] * 4
|
19
|
+
end
|
20
|
+
|
21
|
+
def nested(&block)
|
22
|
+
@nested ||= schema.fields.each_with_object({errors => [], subschemes => {}}) do |(_, field), result|
|
23
|
+
meta, sc = collect_meta(field, root)
|
24
|
+
if sc
|
25
|
+
meta[:structure] = self.class.new(sc, ignore_transparent, meta[:json_path]).nested(&block)
|
26
|
+
result[errors] += meta[:structure].delete(errors)
|
27
|
+
else
|
28
|
+
result[errors] += field.possible_errors
|
29
|
+
end
|
30
|
+
result[field.key] = meta unless ignore_transparent && field.transparent?
|
31
|
+
yield(field.key, meta) if block_given?
|
32
|
+
|
33
|
+
next unless field.mutates_schema?
|
34
|
+
schema.subschemes.each do |name, subschema|
|
35
|
+
result[subschemes][name] = self.class.new(subschema, ignore_transparent, root).nested(&block)
|
36
|
+
result[errors] += result[subschemes][name][errors]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def all_nested(&block)
|
42
|
+
@all_nested ||= all_flatten(&block).each_with_object({}) do |(name, struct), obj|
|
43
|
+
obj[name] = {}
|
44
|
+
# sort the flatten struct to have iterated 1lvl keys before 2lvl and so on...
|
45
|
+
struct.sort_by { |k, v| k.to_s.count(".") }.each do |key, value|
|
46
|
+
target = obj[name]
|
47
|
+
key, value = key.to_s, value.clone # clone the values, because we do mutation below
|
48
|
+
next target[key.to_sym] = value if key.start_with?(Paradocs.config.meta_prefix) # copy meta fields
|
49
|
+
|
50
|
+
parts = key.split(".")
|
51
|
+
next target[key] ||= value if parts.size == 1 # copy 1lvl key
|
52
|
+
parts.each.with_index do |subkey, index|
|
53
|
+
target[subkey] ||= value
|
54
|
+
next if parts.size == index + 1
|
55
|
+
target[subkey][:structure] ||= {}
|
56
|
+
target = target[subkey][:structure] # target goes deeper for each part
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def all_flatten(schema_structure=nil, &block)
|
63
|
+
return @all_flatten if @all_flatten
|
64
|
+
schema_structure ||= flatten(&block)
|
65
|
+
if schema_structure[subschemes].empty?
|
66
|
+
schema_structure.delete(subschemes) # don't include redundant key
|
67
|
+
return @all_flatten = {DEFAULT => schema_structure}
|
68
|
+
end
|
69
|
+
@all_flatten = schema_structure[subschemes].each_with_object({}) do |(name, subschema), result|
|
70
|
+
if subschema[subschemes].empty?
|
71
|
+
result[name] = schema_structure.merge(subschema)
|
72
|
+
result[name].delete(subschemes)
|
73
|
+
next result[name]
|
74
|
+
end
|
75
|
+
|
76
|
+
all_flatten(subschema).each do |sub_name, schema|
|
77
|
+
result["#{name}_#{sub_name}".to_sym] = schema_structure.merge(schema)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def flatten(&block)
|
83
|
+
@flatten ||= schema.fields.each_with_object({errors => [], subschemes => {}}) do |(_, field), obj|
|
84
|
+
meta, sc = collect_meta(field, root)
|
85
|
+
humanized_name = meta.delete(:nested_name)
|
86
|
+
obj[humanized_name] = meta unless ignore_transparent && field.transparent?
|
87
|
+
|
88
|
+
if sc
|
89
|
+
deep_result = self.class.new(sc, ignore_transparent, meta[:json_path]).flatten(&block)
|
90
|
+
obj[errors] += deep_result.delete(errors)
|
91
|
+
obj[subschemes].merge!(deep_result.delete(subschemes))
|
92
|
+
obj.merge!(deep_result)
|
93
|
+
else
|
94
|
+
obj[errors] += field.possible_errors
|
95
|
+
end
|
96
|
+
yield(humanized_name, meta) if block_given?
|
97
|
+
next unless field.mutates_schema?
|
98
|
+
schema.subschemes.each do |name, subschema|
|
99
|
+
obj[subschemes][name] ||= self.class.new(subschema, ignore_transparent, root).flatten(&block)
|
100
|
+
obj[errors] += obj[subschemes][name][errors]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def collect_meta(field, root)
|
108
|
+
json_path = root.empty? ? "$.#{field.key}" : "#{root}.#{field.key}"
|
109
|
+
meta = field.meta_data.merge(json_path: json_path)
|
110
|
+
sc = meta.delete(:schema)
|
111
|
+
meta[:mutates_schema] = true if meta.delete(:mutates_schema)
|
112
|
+
json_path << "[]" if meta[:type] == :array
|
113
|
+
meta[:nested_name] = json_path.gsub("[]", "")[2..-1]
|
114
|
+
[meta, sc]
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
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/version.rb
CHANGED
data/mkdocs.yml
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
site_name: Paradocs
|
2
|
+
theme:
|
3
|
+
name: readthedocs
|
4
|
+
nav_style: dark
|
5
|
+
nav:
|
6
|
+
- 'index.md'
|
7
|
+
- 'policies.md'
|
8
|
+
- 'schema.md'
|
9
|
+
- 'struct.md'
|
10
|
+
- 'form_objects_dsl.md'
|
11
|
+
- 'subschema.md'
|
12
|
+
- 'documentation_generation.md'
|
13
|
+
- 'payload_builder.md'
|
14
|
+
- 'custom_configuration.md'
|
15
|
+
- 'faq.md'
|
16
|
+
|
data/paradocs.gemspec
CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.email = ["tkachenko.maxim.w@gmail.com", "ismaelct@gmail.com"]
|
11
11
|
spec.description = %q{Flexible DSL for declaring allowed parameters focused on DRY validation that gives you opportunity to generate API documentation on-the-fly.}
|
12
12
|
spec.summary = %q{A huge add-on for original gem mostly focused on retrieving the more metadata from declared schemas as possible.}
|
13
|
-
spec.homepage = "https://
|
13
|
+
spec.homepage = "https://paradocs.readthedocs.io/en/latest"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
16
16
|
spec.files = `git ls-files -z`.split("\x0")
|
@@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
21
|
spec.add_development_dependency "bundler", "~> 2.1"
|
22
|
-
spec.add_development_dependency "rake",
|
22
|
+
spec.add_development_dependency "rake", '~> 12.3'
|
23
23
|
spec.add_development_dependency "rspec", '3.4.0'
|
24
24
|
spec.add_development_dependency "pry", "~> 0"
|
25
25
|
end
|
data/requirements.txt
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
mkdocs==1.1.2
|