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.
@@ -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.
@@ -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
@@ -1,14 +1,13 @@
1
1
  require "paradocs/context"
2
2
  require "paradocs/results"
3
3
  require "paradocs/field"
4
- require "paradocs/extensions/insides"
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
@@ -1,3 +1,3 @@
1
1
  module Paradocs
2
- VERSION = "1.0.24"
2
+ VERSION = "1.1.0"
3
3
  end
@@ -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
+
@@ -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://github.com/mtkachenk0/paradocs"
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", ">= 12.3.3"
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
@@ -0,0 +1 @@
1
+ mkdocs==1.1.2