smart_params 5.1.0 → 6.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +31 -31
- data/lib/smart_params/field.rb +35 -138
- data/lib/smart_params/fluent_language.rb +46 -0
- data/lib/smart_params/invalid_payload_exception.rb +16 -0
- data/lib/smart_params/invalid_property_type_exception.rb +33 -0
- data/lib/smart_params/missing_property_exception.rb +18 -0
- data/lib/smart_params/missing_type_annotation_exception.rb +16 -0
- data/lib/smart_params/namespace_already_defined_exception.rb +14 -0
- data/lib/smart_params/no_matching_namespace_exception.rb +15 -0
- data/lib/smart_params/path_already_defined_exception.rb +14 -0
- data/lib/smart_params/version.rb +1 -1
- data/lib/smart_params.rb +36 -89
- data/lib/smart_params_spec.rb +145 -290
- metadata +18 -27
- data/lib/smart_params/error/invalid_property_type.rb +0 -36
- data/lib/smart_params/error/invalid_property_type_spec.rb +0 -25
- data/lib/smart_params/error.rb +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f5a757ef69761d43ce38da683b79975b5f2a9f712ac79824cd386ea0d1c8b12f
|
4
|
+
data.tar.gz: a569e347394533b8466d49cf4c4ed9ac3b1df6a3a6997640d8a2a46aeb142541
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d6e2886fa056a1d5d5f66a2af46eb0e0034d3a0adcb7d2c75d5f54e1663ea261f67da8b3ba0fb7ffd1bfc8c5ee8104f8aaf90315ffaf18bc954cb199be2cad0
|
7
|
+
data.tar.gz: 6fc0347f0a76bdc4ec4863924a3f29200563dd0f3550216294da9899f7a22fe8c46f508527b3d5fffad515c05377c758fc449ab1d164f75d1395726293679a3a
|
data/README.md
CHANGED
@@ -8,22 +8,22 @@ Work smart, not strong. This gem gives developers an easy to understand and easy
|
|
8
8
|
So lets say you have a complex set of incoming data, say a JSON:API-specification compliant payload that contains the data to create an account on your server. Of course, your e-commerce platform is pretty flexible; You don't stop users from creating an account just because they don't have an email or password. So let's see how this would play out:
|
9
9
|
|
10
10
|
``` ruby
|
11
|
-
|
12
|
-
include SmartParams
|
13
|
-
|
14
|
-
schema do
|
15
|
-
field :data, subschema: true do
|
16
|
-
field :id, type: Coercible::String
|
17
|
-
field :type, type: Strict::String
|
18
|
-
field
|
19
|
-
field :email, type: Strict::String
|
20
|
-
field :username, type: Strict::String
|
21
|
-
field :name, type: Strict::String
|
22
|
-
field :password, type: Strict::String.default { SecureRandom.hex(32) }
|
11
|
+
module AccountSchema
|
12
|
+
include SmartParams::FluentLanguage
|
13
|
+
|
14
|
+
schema do |root|
|
15
|
+
field root, :data, subschema: true do |data|
|
16
|
+
field data, :id, type: Coercible::String.optional
|
17
|
+
field data, :type, type: Strict::String
|
18
|
+
field data, :attributes do |attributes|
|
19
|
+
field attributes, :email, type: Strict::String
|
20
|
+
field attributes, :username, type: Strict::String.optional
|
21
|
+
field attributes, :name, type: Strict::String.optional
|
22
|
+
field attributes, :password, type: Strict::String.default { SecureRandom.hex(32) }.optional
|
23
23
|
end
|
24
24
|
end
|
25
|
-
field :meta, Strict::Hash
|
26
|
-
field :included, type: Strict::Array
|
25
|
+
field :meta, Strict::Hash.optional
|
26
|
+
field :included, type: Strict::Array.optional
|
27
27
|
end
|
28
28
|
end
|
29
29
|
```
|
@@ -33,11 +33,11 @@ And now using that schema in the controller:
|
|
33
33
|
``` ruby
|
34
34
|
class AccountsController < ApplicationController
|
35
35
|
def create
|
36
|
-
|
36
|
+
payload = SmartParams.from(AccountSchema, params)
|
37
37
|
# parameters will be a SmartParams::Dataset, which will respond to the various fields you defined
|
38
38
|
|
39
39
|
# Here we're pulling out the id and data properties defined above
|
40
|
-
record = Account.create({id:
|
40
|
+
record = Account.create({id: payload[:data][:id], **payload[:data][:attributes]})
|
41
41
|
|
42
42
|
redirect_to account_url(record)
|
43
43
|
end
|
@@ -49,43 +49,43 @@ Okay, so lets look at some scenarios.
|
|
49
49
|
First, lets try an empty payload:
|
50
50
|
|
51
51
|
``` ruby
|
52
|
-
CreateAccountSchema.new({})
|
53
|
-
# raises SmartParams::Error::InvalidPropertyType, keychain: [:data], wanted: Hash, raw: nil
|
54
52
|
|
55
|
-
|
53
|
+
SmartParams.from(AccountSchema, {}).payload
|
54
|
+
# returns [InvalidPropertyTypeException | MissingPropertyException]
|
56
55
|
|
57
|
-
|
58
|
-
#
|
56
|
+
SmartParams.validate!(AccountSchema, {})
|
57
|
+
# raises SmartParams::InvalidPayloadException(failures: [InvalidPropertyTypeException | MissingPropertyException])
|
59
58
|
```
|
60
59
|
|
61
60
|
Great, we've told SmartParams we need `data` and it enforced this! The exception class knows the "key chain" path to the property that was missing and the value that was given. Lets experiment with that:
|
62
61
|
|
63
62
|
``` ruby
|
64
|
-
|
65
|
-
#
|
63
|
+
SmartParams.from(AccountSchema, {data: ""})
|
64
|
+
# returns [MissingPropertyException(path: [:data], last: {data: ""})]
|
66
65
|
```
|
67
66
|
|
68
67
|
Sweet, we can definitely catch this and give the client a meaningful error! Okay, so to show off a good payload I'm going to do two things: Examine the properties and turn it to a JSON compatible structure. Lets see a minimum viable account according to our schema:
|
69
68
|
|
70
69
|
|
71
70
|
``` ruby
|
72
|
-
|
71
|
+
payload = SmartParams.from(AccountSchema, {
|
73
72
|
data: {
|
74
|
-
type: "accounts"
|
73
|
+
type: "accounts",
|
74
|
+
attributes: {
|
75
|
+
email: "kurtis@example.com"
|
76
|
+
}
|
75
77
|
}
|
76
78
|
})
|
77
79
|
|
78
|
-
|
80
|
+
payload[:data][:type]
|
79
81
|
# "accounts"
|
80
82
|
|
81
|
-
|
82
|
-
# "accounts"
|
83
|
-
|
84
|
-
schema.as_json
|
83
|
+
payload.as_json
|
85
84
|
# {
|
86
85
|
# "data" => {
|
87
86
|
# "type" => "accounts",
|
88
87
|
# "attributes" => {
|
88
|
+
# "email" => "kurtis@example.com",
|
89
89
|
# "password" => "1a6c3ffa4e96ad1660cb819f52a3393d924ac20073e84a9a6943a721d49bab38"
|
90
90
|
# }
|
91
91
|
# }
|
@@ -136,7 +136,7 @@ Or install it yourself with:
|
|
136
136
|
|
137
137
|
## Contributing
|
138
138
|
|
139
|
-
1. Read the [Code of Conduct](/CONDUCT)
|
139
|
+
1. Read the [Code of Conduct](/CONDUCT.md)
|
140
140
|
2. Fork it
|
141
141
|
3. Create your feature branch (`git checkout -b my-new-feature`)
|
142
142
|
4. Test your code: `rake spec`
|
data/lib/smart_params/field.rb
CHANGED
@@ -2,156 +2,53 @@
|
|
2
2
|
|
3
3
|
module SmartParams
|
4
4
|
class Field
|
5
|
-
attr_reader :
|
6
|
-
attr_reader :subfields
|
5
|
+
attr_reader :path
|
7
6
|
attr_reader :type
|
8
|
-
attr_reader :nullable
|
9
|
-
attr_reader :key
|
10
7
|
|
11
|
-
def
|
12
|
-
|
13
|
-
('subschema' if @subschema),
|
14
|
-
("#/#{@keychain.join('/')}" if @keychain),
|
15
|
-
("-> #{type.name}" if @type),
|
16
|
-
("= #{@value.inspect}" if @value)
|
17
|
-
].compact.join(' ')}>"
|
18
|
-
end
|
19
|
-
|
20
|
-
def initialize(keychain:, type:, key: nil, subschema: false, nullable: false, &nesting)
|
21
|
-
@key = key
|
22
|
-
@keychain = Array(keychain)
|
23
|
-
@subfields = Set.new
|
8
|
+
def initialize(path:, type:, optional: false, **)
|
9
|
+
@path = path
|
24
10
|
@type = type
|
25
|
-
@
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
11
|
+
@optional = !!optional
|
12
|
+
end
|
13
|
+
|
14
|
+
def map(raw)
|
15
|
+
case [*dig_until(@path, raw), @type, @type&.optional? || @optional]
|
16
|
+
in [:error, last, _, false]
|
17
|
+
[:error, SmartParams::MissingPropertyException.new(path:, last:)]
|
18
|
+
in [:error, _, _, true]
|
19
|
+
:skip
|
20
|
+
in [:ok, value, nil, _]
|
21
|
+
[:ok, value]
|
22
|
+
in [:ok, value, type, _]
|
23
|
+
type.try(value)
|
36
24
|
end
|
37
|
-
|
25
|
+
rescue Dry::Types::ConstraintError => constraint_error
|
26
|
+
Dry::Types::Result::Failure.new(value, constraint_error)
|
38
27
|
end
|
39
28
|
|
40
|
-
def
|
41
|
-
|
42
|
-
# subfields have been passed, not just ones that match the schema.
|
43
|
-
return false if nullable? && @specified
|
29
|
+
def update_in(result, value)
|
30
|
+
*body, butt = @path
|
44
31
|
|
45
|
-
|
46
|
-
|
32
|
+
body.reduce(result) do |mapping, key|
|
33
|
+
mapping[key]
|
34
|
+
end.store(butt, value)
|
47
35
|
|
48
|
-
|
49
|
-
keychain.empty?
|
36
|
+
result
|
50
37
|
end
|
51
38
|
|
52
|
-
def
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
else
|
64
|
-
!!@specified
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
# For nullable hashes: Any keys not in the schema make the hash dirty.
|
69
|
-
# If a key is found that matches the schema, we can consider the hash
|
70
|
-
# clean.
|
71
|
-
def dirty?
|
72
|
-
!!@dirty
|
73
|
-
end
|
74
|
-
|
75
|
-
def clean?
|
76
|
-
return false if dirty?
|
77
|
-
return true if empty? || subfields.reject(&:empty?).any?
|
78
|
-
|
79
|
-
false
|
80
|
-
end
|
81
|
-
|
82
|
-
# Check if we should consider this value even when empty.
|
83
|
-
def allow_empty?
|
84
|
-
return true if specified? && nullable?
|
85
|
-
|
86
|
-
subfields.any?(&:allow_empty?)
|
87
|
-
end
|
88
|
-
|
89
|
-
def claim(raw)
|
90
|
-
return type[dug(raw)] if deep?
|
91
|
-
|
92
|
-
@value = type[dug(raw)]
|
93
|
-
rescue Dry::Types::ConstraintError => _constraint_exception
|
94
|
-
raise SmartParams::Error::InvalidPropertyType.new(keychain:, wanted: type, raw: keychain.empty? ? raw : raw.dig(*keychain))
|
95
|
-
rescue Dry::Types::MissingKeyError => missing_key_exception
|
96
|
-
raise SmartParams::Error::InvalidPropertyType.new(keychain:, wanted: type, raw: keychain.empty? ? raw : raw.dig(*keychain), missing_key: missing_key_exception.key)
|
97
|
-
end
|
98
|
-
|
99
|
-
def to_hash
|
100
|
-
keychain.reverse.reduce(value) do |accumulation, key|
|
101
|
-
{ key => accumulation }
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
def empty?
|
106
|
-
value.nil?
|
107
|
-
end
|
108
|
-
|
109
|
-
# Should this field be removed from resulting hash?
|
110
|
-
def removable?
|
111
|
-
empty? && !allow_empty?
|
112
|
-
end
|
113
|
-
|
114
|
-
def weight
|
115
|
-
keychain.map(&:to_s)
|
116
|
-
end
|
117
|
-
|
118
|
-
private def field(key, subschema: false, type: SmartParams::Hash, nullable: false, &subfield)
|
119
|
-
@subfields << self.class.new(key:, keychain: [*keychain, key], type:, nullable:, subschema:, &subfield)
|
120
|
-
end
|
121
|
-
|
122
|
-
private def subschema(key, nullable: false, &subfield)
|
123
|
-
field(key, subschema: true, type: SmartParams::Hash, nullable:, &subfield)
|
124
|
-
end
|
125
|
-
|
126
|
-
# Very busy method with recent changes. TODO: clean-up
|
127
|
-
private def dug(raw)
|
128
|
-
return raw if keychain.empty?
|
129
|
-
|
130
|
-
# If value provided is a hash, check if it's dirty. See #dirty? for
|
131
|
-
# more info.
|
132
|
-
if nullable?
|
133
|
-
hash = raw.dig(*keychain)
|
134
|
-
if hash.respond_to?(:keys)
|
135
|
-
others = hash.keys - [keychain.last]
|
136
|
-
@dirty = others.any?
|
39
|
+
private def dig_until(keychain, raw)
|
40
|
+
keychain.reduce([:ok, raw]) do |result, key|
|
41
|
+
case result
|
42
|
+
in [:ok, current]
|
43
|
+
if current.respond_to?(:key?) && current.key?(key)
|
44
|
+
[:ok, current[key]]
|
45
|
+
else
|
46
|
+
[:error, current]
|
47
|
+
end
|
48
|
+
in [:error, exception]
|
49
|
+
[:error, exception]
|
137
50
|
end
|
138
51
|
end
|
139
|
-
|
140
|
-
# Trace the keychain to find out if the field is explicitly set in the
|
141
|
-
# input hash.
|
142
|
-
at = raw
|
143
|
-
exact = true
|
144
|
-
keychain.each do |key|
|
145
|
-
if at.respond_to?(:key?) && at.key?(key)
|
146
|
-
at = at[key]
|
147
|
-
else
|
148
|
-
exact = false
|
149
|
-
break
|
150
|
-
end
|
151
|
-
end
|
152
|
-
@specified = exact
|
153
|
-
|
154
|
-
raw.dig(*keychain)
|
155
52
|
end
|
156
53
|
end
|
157
54
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SmartParams
|
4
|
+
module FluentLanguage
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
include Dry.Types()
|
9
|
+
|
10
|
+
mattr_accessor :namespaces
|
11
|
+
end
|
12
|
+
|
13
|
+
class_methods do
|
14
|
+
private def schema(namespace = :default)
|
15
|
+
self.namespaces ||= {}
|
16
|
+
|
17
|
+
raise SmartParams::NamespaceAlreadyDefinedException.new(namespace:) if self.namespaces.key?(namespace)
|
18
|
+
|
19
|
+
self.namespaces[namespace] = []
|
20
|
+
|
21
|
+
yield(namespace)
|
22
|
+
|
23
|
+
self.namespaces
|
24
|
+
end
|
25
|
+
|
26
|
+
private def field(prefix, name, type = nil, **)
|
27
|
+
root, *remaining = Kernel.Array(prefix)
|
28
|
+
|
29
|
+
path = [root, *remaining, name]
|
30
|
+
|
31
|
+
raise SmartParams::MissingTypeAnnotationException.new(path:) if type.nil? && !block_given?
|
32
|
+
|
33
|
+
raise SmartParams::PathAlreadyDefinedException.new(path:) if self.namespaces[root].any? { |field| field.path == path }
|
34
|
+
|
35
|
+
self.namespaces[root] = [
|
36
|
+
*self.namespaces[root],
|
37
|
+
SmartParams::Field.new(path: [*remaining, name], type:, subschema: block_given?, **)
|
38
|
+
]
|
39
|
+
|
40
|
+
yield(path) if block_given?
|
41
|
+
|
42
|
+
self.namespaces[root]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SmartParams
|
4
|
+
class InvalidPayloadException < StandardError
|
5
|
+
attr_reader :failures
|
6
|
+
|
7
|
+
def initialize(failures:)
|
8
|
+
@failures = failures
|
9
|
+
super(message)
|
10
|
+
end
|
11
|
+
|
12
|
+
def message
|
13
|
+
"structure failed to validate: \n\t#{@failures.map(&:message).join("\n\t")}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SmartParams
|
4
|
+
class InvalidPropertyTypeException < StandardError
|
5
|
+
attr_reader :path
|
6
|
+
attr_reader :wanted
|
7
|
+
attr_reader :raw
|
8
|
+
attr_reader :type
|
9
|
+
attr_reader :grievance
|
10
|
+
|
11
|
+
def initialize(path:, wanted:, raw:, grievance:)
|
12
|
+
@path = path
|
13
|
+
@wanted = wanted
|
14
|
+
@raw = raw
|
15
|
+
@type = raw.inspect
|
16
|
+
@grievance = grievance
|
17
|
+
super(message)
|
18
|
+
end
|
19
|
+
|
20
|
+
def message
|
21
|
+
"expected /#{path.join('/')} to be #{wanted.name}, but is #{type} and #{grievance}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def as_json
|
25
|
+
{
|
26
|
+
"path" => path,
|
27
|
+
"wanted" => wanted,
|
28
|
+
"grievance" => grievance,
|
29
|
+
"raw" => raw
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SmartParams
|
4
|
+
class MissingPropertyException < StandardError
|
5
|
+
attr_accessor :path
|
6
|
+
attr_accessor :last
|
7
|
+
|
8
|
+
def initialize(path:, last:)
|
9
|
+
@path = path
|
10
|
+
@last = last
|
11
|
+
super(message)
|
12
|
+
end
|
13
|
+
|
14
|
+
def message
|
15
|
+
"/#{@path.join('/')} is missing from the structure, last node was #{@last.inspect}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SmartParams
|
4
|
+
class MissingTypeAnnotationException < StandardError
|
5
|
+
attr_accessor :path
|
6
|
+
|
7
|
+
def initialize(path:)
|
8
|
+
@path = path
|
9
|
+
super(message)
|
10
|
+
end
|
11
|
+
|
12
|
+
def message
|
13
|
+
"/#{@path.join('/')} was expected to define a type or a block, but did neither"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SmartParams
|
4
|
+
class NamespaceAlreadyDefinedException < StandardError
|
5
|
+
def initialize(namespace:)
|
6
|
+
@namespace = namespace
|
7
|
+
super(message)
|
8
|
+
end
|
9
|
+
|
10
|
+
def message
|
11
|
+
"#{@namespace} was already taken as a schema namespace"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SmartParams
|
4
|
+
class NoMatchingNamespaceException < StandardError
|
5
|
+
def initialize(namespace:, available:)
|
6
|
+
@namespace = namespace
|
7
|
+
@available = available
|
8
|
+
super(message)
|
9
|
+
end
|
10
|
+
|
11
|
+
def message
|
12
|
+
"#{@namespace} does not exist, only #{@available.inspect}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SmartParams
|
4
|
+
class PathAlreadyDefinedException < StandardError
|
5
|
+
def initialize(path:)
|
6
|
+
@path = path
|
7
|
+
super(message)
|
8
|
+
end
|
9
|
+
|
10
|
+
def message
|
11
|
+
"/#{@path.join('/')} was already taken as a field path"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/smart_params/version.rb
CHANGED
data/lib/smart_params.rb
CHANGED
@@ -1,108 +1,55 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "dry-types"
|
4
|
-
require "recursive-open-struct"
|
5
4
|
require "active_support/concern"
|
6
5
|
require "active_support/core_ext/object"
|
7
|
-
require "active_support/core_ext/module
|
6
|
+
require "active_support/core_ext/module"
|
8
7
|
|
9
8
|
module SmartParams
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
require_relative "smart_params/invalid_payload_exception"
|
10
|
+
require_relative "smart_params/invalid_property_type_exception"
|
11
|
+
require_relative "smart_params/missing_property_exception"
|
12
|
+
require_relative "smart_params/namespace_already_defined_exception"
|
13
|
+
require_relative "smart_params/no_matching_namespace_exception"
|
13
14
|
require_relative "smart_params/field"
|
14
|
-
require_relative "smart_params/
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
@safe = safe
|
23
|
-
@raw = raw
|
24
|
-
@schema = self.class.instance_variable_get(:@schema)[name]
|
25
|
-
|
26
|
-
@fields = [@schema, *unfold(@schema.subfields)].sort_by(&:weight).each { |field| field.claim(raw) }
|
27
|
-
rescue SmartParams::Error::InvalidPropertyType => invalid_property_exception
|
28
|
-
raise invalid_property_exception if safe?
|
29
|
-
|
30
|
-
@exception = invalid_property_exception
|
31
|
-
end
|
32
|
-
|
33
|
-
def inspect
|
34
|
-
"#<#{self.class.name}:#{__id__} @fields=#{@fields.inspect} @raw=#{@raw.inspect}>"
|
35
|
-
end
|
36
|
-
|
37
|
-
def payload
|
38
|
-
if @exception.present?
|
39
|
-
@exception
|
40
|
-
else
|
41
|
-
RecursiveOpenStruct.new(structure)
|
15
|
+
require_relative "smart_params/fluent_language"
|
16
|
+
|
17
|
+
def self.validate!(schema, raw, namespace = :default)
|
18
|
+
case map(fetch_namespace(schema, namespace), raw)
|
19
|
+
in [result, []]
|
20
|
+
result
|
21
|
+
in [_, failures]
|
22
|
+
raise InvalidPayloadException.new(failures:)
|
42
23
|
end
|
43
24
|
end
|
44
25
|
|
45
|
-
def
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
26
|
+
def self.from(schema, raw, namespace = :default)
|
27
|
+
case map(fetch_namespace(schema, namespace), raw)
|
28
|
+
in [result, []]
|
29
|
+
result
|
30
|
+
in [_, failures]
|
31
|
+
failures
|
50
32
|
end
|
51
33
|
end
|
52
|
-
alias as_json to_hash
|
53
34
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
def respond_to_missing?(name, include_private = false)
|
68
|
-
payload.respond_to?(name) || super
|
69
|
-
end
|
70
|
-
|
71
|
-
def method_missing(name, *arguments, &)
|
72
|
-
if payload.respond_to?(name)
|
73
|
-
payload.public_send(name)
|
74
|
-
else
|
75
|
-
super
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
# This function basically takes a list of fields and reduces them into a tree of values
|
80
|
-
private def structure
|
81
|
-
fields
|
82
|
-
.reject(&:removable?)
|
83
|
-
.map(&:to_hash)
|
84
|
-
.reduce(&:deep_merge)
|
85
|
-
end
|
86
|
-
|
87
|
-
# This funcion takes a nested field tree and turns it into a list of fields
|
88
|
-
private def unfold(subfields)
|
89
|
-
subfields.to_a.reduce([]) do |list, field|
|
90
|
-
if field.deep?
|
91
|
-
[*list, field, *unfold(field.subfields.to_a)]
|
92
|
-
else
|
93
|
-
[*list, field]
|
35
|
+
private_class_method def self.map(fields, raw)
|
36
|
+
fields.reduce([Hash.new { |h, k| h[k] = h.class.new(&h.default_proc) }, []]) do |(result, failures), field|
|
37
|
+
case field.map(raw)
|
38
|
+
in :skip
|
39
|
+
[result, failures]
|
40
|
+
in [:ok, value]
|
41
|
+
[field.update_in(result, value), failures]
|
42
|
+
in Dry::Types::Result::Success => success
|
43
|
+
[field.update_in(result, success.input), failures]
|
44
|
+
in Dry::Types::Result::Failure => failure
|
45
|
+
[result, [*failures, InvalidPropertyTypeException.new(path: field.path, wanted: field.type, raw: failure.input, grievance: failure.error)]]
|
46
|
+
in [:error, value]
|
47
|
+
[result, [*failures, value]]
|
94
48
|
end
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
private def safe?
|
99
|
-
@safe
|
49
|
+
end
|
100
50
|
end
|
101
51
|
|
102
|
-
|
103
|
-
|
104
|
-
@schema ||= {}
|
105
|
-
@schema[name] = Field.new(keychain: [], type: SmartParams::Hash, subschema: false, &definitions)
|
106
|
-
end
|
52
|
+
private_class_method def self.fetch_namespace(schema, namespace)
|
53
|
+
schema.namespaces[namespace] || raise(NoMatchingNamespaceException.new(namespace:, available: schema.namespaces.keys))
|
107
54
|
end
|
108
55
|
end
|