smart_params 5.1.0 → 6.0.1
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/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
|