paradocs 1.0.24 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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