paradocs 1.0.22 → 1.1.2

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
+ ```
@@ -22,7 +22,8 @@ module Paradocs
22
22
  explicit_errors: false,
23
23
  whitelisted_keys: [],
24
24
  default_schema_name: :schema,
25
- meta_prefix: "_"
25
+ meta_prefix: "_",
26
+ whitelist_coercion: nil
26
27
  )
27
28
  end
28
29
 
@@ -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
@@ -107,7 +107,6 @@ module Paradocs
107
107
  define_method(:meta_data) do
108
108
  meta = super()
109
109
  meta[policy][:limit] = limit
110
- binding.pry unless meta.dig(policy, :limit)
111
110
  meta
112
111
  end
113
112
 
@@ -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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Paradocs
2
- VERSION = "1.0.22"
2
+ VERSION = "1.1.2"
3
3
  end