plumb 0.0.4 → 0.0.5
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 +223 -10
- data/bench/compare_parametric_schema.rb +102 -0
- data/bench/compare_parametric_struct.rb +68 -0
- data/bench/parametric_schema.rb +229 -0
- data/bench/plumb_hash.rb +109 -0
- data/examples/event_registry.rb +34 -27
- data/examples/weekdays.rb +1 -1
- data/lib/plumb/attributes.rb +13 -7
- data/lib/plumb/composable.rb +123 -4
- data/lib/plumb/json_schema_visitor.rb +11 -2
- data/lib/plumb/match_class.rb +1 -1
- data/lib/plumb/pipeline.rb +21 -2
- data/lib/plumb/tagged_hash.rb +1 -1
- data/lib/plumb/types.rb +24 -0
- data/lib/plumb/version.rb +1 -1
- metadata +6 -2
@@ -0,0 +1,229 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'parametric'
|
4
|
+
require 'money'
|
5
|
+
require 'monetize'
|
6
|
+
|
7
|
+
PARAMETRIC_MONEY_EXP = /(\W{1}|\w{3})?[\d+,.]/
|
8
|
+
|
9
|
+
Parametric.policy(:nullable_money) do
|
10
|
+
coerce do |value, _key, _context|
|
11
|
+
money = case value
|
12
|
+
when String
|
13
|
+
value = value.strip
|
14
|
+
if value.blank?
|
15
|
+
nil
|
16
|
+
elsif value =~ PARAMETRIC_MONEY_EXP
|
17
|
+
Monetize.parse!(value.gsub(',', ''))
|
18
|
+
else
|
19
|
+
raise ArgumentError, "#{value} is not a monetary amount"
|
20
|
+
end
|
21
|
+
when Numeric
|
22
|
+
Monetize.parse!(value.to_s)
|
23
|
+
when Money
|
24
|
+
value
|
25
|
+
when NilClass
|
26
|
+
nil
|
27
|
+
else
|
28
|
+
raise ArgumentError, "don't know how to coerce #{value.inspect} into Money"
|
29
|
+
end
|
30
|
+
|
31
|
+
if money && money.currency != Money.default_currency
|
32
|
+
raise ArgumentError,
|
33
|
+
"expected #{Money.default_currency.name} as currency, but got #{money.currency.name}"
|
34
|
+
end
|
35
|
+
|
36
|
+
money
|
37
|
+
end
|
38
|
+
|
39
|
+
meta_data do
|
40
|
+
{ type: :number, config_type: :nullable_money }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
Parametric.policy(:nullable_date) do
|
45
|
+
# rubocop:disable Style/RedundantBegin
|
46
|
+
coerce do |value, _key, _context|
|
47
|
+
begin
|
48
|
+
value = value.to_s.strip
|
49
|
+
value == '' ? nil : Date.parse(value)
|
50
|
+
rescue Date::Error
|
51
|
+
nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
# rubocop:enable Style/RedundantBegin
|
55
|
+
|
56
|
+
meta_data do
|
57
|
+
{ type: :date, config_type: :nullable_date }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
PARAMETRIC_DATE_EXP = /\A\d{4}-\d{2}-\d{2}\z/
|
62
|
+
PARAMETRIC_DATE_INFINITY = 'infinity'
|
63
|
+
PARAMETRIC_IS_DATE = ->(value) { value.is_a?(::Date) || (value.is_a?(::String) && value =~ PARAMETRIC_DATE_EXP) }
|
64
|
+
PARAMETRIC_PARSE_DATE = ->(value) { value.is_a?(::Date) ? value : ::Date.parse(value) }
|
65
|
+
|
66
|
+
Parametric.policy :nullable_date_range do
|
67
|
+
validate do |value, _key, _context|
|
68
|
+
if value.blank? || value.is_a?(Switcher::DateRange)
|
69
|
+
true
|
70
|
+
else
|
71
|
+
value.is_a?(Hash) && !!(PARAMETRIC_IS_DATE.call(value[:min]) && (value[:max].nil? || PARAMETRIC_IS_DATE.call(value[:max] || value[:max] == PARAMETRIC_DATE_INFINITY)))
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
coerce do |value, _key, _context|
|
76
|
+
if value.is_a?(Switcher::DateRange)
|
77
|
+
value
|
78
|
+
elsif value.blank? || (value[:min].blank? && value[:max].blank?)
|
79
|
+
nil
|
80
|
+
else
|
81
|
+
min = value[:min].present? ? PARAMETRIC_PARSE_DATE.call(value[:min]) : nil
|
82
|
+
max = value[:max].present? && value[:max] != PARAMETRIC_DATE_INFINITY ? PARAMETRIC_PARSE_DATE.call(value[:max]) : nil
|
83
|
+
Switcher::DateRange.new(min, max)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
meta_data do
|
88
|
+
{ type: :object, config_type: :nullable_date_range }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
PARAMETRIC_INT_EXP = /^\d+$/
|
93
|
+
|
94
|
+
Parametric.policy(:nullable_integer) do
|
95
|
+
coerce do |value, _key, _context|
|
96
|
+
if value.to_s =~ PARAMETRIC_INT_EXP
|
97
|
+
value.to_i
|
98
|
+
else
|
99
|
+
nil
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
meta_data do
|
104
|
+
{ type: :integer, config_type: :nullable_integer }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
PARAMETRIC_FLOAT_EXP = /^\d+(\.\d+)?$/
|
109
|
+
|
110
|
+
Parametric.policy(:nullable_number) do
|
111
|
+
coerce do |value, _key, _context|
|
112
|
+
if value.to_s =~ PARAMETRIC_FLOAT_EXP
|
113
|
+
value.to_f
|
114
|
+
else
|
115
|
+
nil
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
meta_data do
|
120
|
+
{ type: :number, config_type: :nullable_number }
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
Parametric.policy :size do
|
125
|
+
message do |opts, object|
|
126
|
+
"must have size of #{opts}, but got #{object.size}"
|
127
|
+
end
|
128
|
+
|
129
|
+
validate do |opts, object, _key, _payload|
|
130
|
+
size = object.size
|
131
|
+
(opts[:min].nil? || size >= opts[:min]) && (opts[:max].nil? || size <= opts[:max])
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
Parametric.policy(:nullable_string) do
|
136
|
+
coerce do |value, _key, _context|
|
137
|
+
if value.to_s.strip != ''
|
138
|
+
value.to_s
|
139
|
+
else
|
140
|
+
nil
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
meta_data do
|
145
|
+
{ type: :string, config_type: :nullable_string }
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
module ParametricSchema
|
150
|
+
TERM = Parametric::Schema.new do
|
151
|
+
field(:name).type(:string).default('')
|
152
|
+
field(:url).type(:string).default('')
|
153
|
+
field(:terms_text).type(:string).default('')
|
154
|
+
field(:start_date).type(:nullable_date)
|
155
|
+
field(:end_date).type(:nullable_date)
|
156
|
+
end
|
157
|
+
TV_COMPONENT = Parametric::Schema.new do
|
158
|
+
field(:slug).type(:string) # .policy(:parameterize).present
|
159
|
+
field(:name).type(:string).present
|
160
|
+
field(:search_tags).type(:array).default([])
|
161
|
+
field(:description).type(:string)
|
162
|
+
field(:channels).type(:integer).default(0)
|
163
|
+
field(:discount_price).type(:nullable_money).default(Money.zero)
|
164
|
+
field(:discount_period).type(:nullable_integer)
|
165
|
+
field(:ongoing_price).type(:nullable_money)
|
166
|
+
field(:contract_length).type(:nullable_integer)
|
167
|
+
field(:upfront_cost).type(:nullable_money)
|
168
|
+
field(:commission).type(:nullable_money)
|
169
|
+
end
|
170
|
+
RECORD = Parametric::Schema.new do
|
171
|
+
field(:supplier_name).type(:string).present
|
172
|
+
field(:start_date).type(:nullable_date).meta(example: nil, admin_ui: true)
|
173
|
+
field(:end_date).type(:nullable_date).meta(example: nil, admin_ui: true)
|
174
|
+
field(:countdown_date).type(:nullable_date).meta(example: nil)
|
175
|
+
field(:name).type(:string).present.meta(example: 'Visa Platinum', admin_ui: true)
|
176
|
+
field(:upfront_cost_description).type(:string).default('')
|
177
|
+
field(:tv_channels_count).type(:integer).default(0)
|
178
|
+
field(:terms).type(:array).policy(:size, min: 1).default([]).schema TERM
|
179
|
+
field(:tv_included).type(:boolean)
|
180
|
+
field(:additional_info).type(:string)
|
181
|
+
field(:product_type).type(:nullable_string) # computed on ingestion
|
182
|
+
field(:annual_price_increase_applies).type(:boolean).default(false)
|
183
|
+
field(:annual_price_increase_description).type(:string).default('')
|
184
|
+
field(:broadband_components).type(:array).default([]).schema do
|
185
|
+
field(:name).type(:string)
|
186
|
+
field(:technology).type(:string)
|
187
|
+
field(:technology_tags).type(:array).default([])
|
188
|
+
field(:is_mobile).type(:boolean).default(false) # computed on ingestion based on technology
|
189
|
+
field(:description).type(:string)
|
190
|
+
field(:download_speed_measurement).type(:string).default('')
|
191
|
+
field(:download_speed).type(:nullable_number).default(0)
|
192
|
+
field(:upload_speed_measurement).type(:string)
|
193
|
+
field(:upload_speed).type(:nullable_number).default(0)
|
194
|
+
field(:download_usage_limit).type(:nullable_integer).default(nil)
|
195
|
+
field(:discount_price).type(:nullable_money)
|
196
|
+
field(:discount_period).type(:nullable_integer)
|
197
|
+
field(:speed_description).type(:string).default('')
|
198
|
+
field(:ongoing_price).type(:nullable_money)
|
199
|
+
field(:contract_length).type(:nullable_integer)
|
200
|
+
field(:upfront_cost).type(:nullable_money)
|
201
|
+
field(:commission).type(:nullable_money)
|
202
|
+
end
|
203
|
+
field(:tv_components).type(:array).default([]).schema TV_COMPONENT
|
204
|
+
field(:call_package_types).type(:array).default([]).meta(example: ['Everything']) # computed on ingestion
|
205
|
+
field(:phone_components).type(:array).default([]).schema do
|
206
|
+
field(:name).type(:string)
|
207
|
+
field(:description).type(:string)
|
208
|
+
field(:discount_price).type(:nullable_money)
|
209
|
+
field(:discount_period).type(:nullable_integer)
|
210
|
+
field(:ongoing_price).type(:nullable_money)
|
211
|
+
field(:contract_length).type(:nullable_integer)
|
212
|
+
field(:upfront_cost).type(:nullable_money)
|
213
|
+
field(:commission).type(:nullable_money)
|
214
|
+
field(:call_package_type).type(:array).default([])
|
215
|
+
end
|
216
|
+
field(:payment_methods).type(:array).default([])
|
217
|
+
field(:discounts).type(:array).default([]).schema do
|
218
|
+
field(:period).type(:integer)
|
219
|
+
field(:price).type(:nullable_money)
|
220
|
+
end
|
221
|
+
field(:ongoing_price).type(:nullable_money).meta(admin_ui: true)
|
222
|
+
field(:contract_length).type(:nullable_integer)
|
223
|
+
field(:upfront_cost).type(:nullable_money)
|
224
|
+
field(:year_1_price).type(:nullable_money).meta(admin_ui: true)
|
225
|
+
field(:savings).type(:nullable_money).meta(admin_ui: true)
|
226
|
+
field(:commission).type(:nullable_money)
|
227
|
+
field(:max_broadband_download_speed).type(:integer).default(0)
|
228
|
+
end
|
229
|
+
end
|
data/bench/plumb_hash.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'plumb'
|
4
|
+
require 'date'
|
5
|
+
require 'money'
|
6
|
+
require 'monetize'
|
7
|
+
|
8
|
+
module PlumbHash
|
9
|
+
include Plumb::Types
|
10
|
+
|
11
|
+
BLANK_ARRAY = [].freeze
|
12
|
+
BLANK_STRING = ''
|
13
|
+
MONEY_EXP = /(\W{1}|\w{3})?[\d+,.]/
|
14
|
+
|
15
|
+
PARSE_DATE = proc do |result|
|
16
|
+
date = ::Date.parse(result.value)
|
17
|
+
result.valid(date)
|
18
|
+
rescue ::Date::Error
|
19
|
+
result.invalid(errors: 'invalid date')
|
20
|
+
end
|
21
|
+
|
22
|
+
PARSE_MONEY = proc do |result|
|
23
|
+
value = Monetize.parse!(result.value.to_s.gsub(',', ''))
|
24
|
+
result.valid(value)
|
25
|
+
end
|
26
|
+
|
27
|
+
Date = Any[::Date] \
|
28
|
+
| (String[MONEY_EXP] >> PARSE_DATE)
|
29
|
+
|
30
|
+
BlankStringOrDate = Forms::Nil | Date
|
31
|
+
|
32
|
+
Money = Any[::Money] \
|
33
|
+
| (String.present >> PARSE_MONEY) \
|
34
|
+
| (Numeric >> PARSE_MONEY)
|
35
|
+
|
36
|
+
Term = Hash[
|
37
|
+
name: String.default(BLANK_STRING),
|
38
|
+
url: String.default(BLANK_STRING),
|
39
|
+
terms_text: String.default(BLANK_STRING),
|
40
|
+
start_date?: BlankStringOrDate.nullable,
|
41
|
+
end_date?: BlankStringOrDate.nullable
|
42
|
+
]
|
43
|
+
|
44
|
+
TvComponent = Hash[
|
45
|
+
slug: String,
|
46
|
+
name: String.present,
|
47
|
+
search_tags: Array[String].default(BLANK_ARRAY),
|
48
|
+
description: String.default(BLANK_STRING),
|
49
|
+
channels: Integer.default(0),
|
50
|
+
discount_price: Money.default(::Money.zero.freeze)
|
51
|
+
]
|
52
|
+
|
53
|
+
Record = Hash[
|
54
|
+
supplier_name: String.present,
|
55
|
+
start_date: BlankStringOrDate.nullable.metadata(admin_ui: true),
|
56
|
+
end_date: BlankStringOrDate.nullable.metadata(admin_ui: true),
|
57
|
+
countdown_date: BlankStringOrDate.nullable,
|
58
|
+
name: String.present,
|
59
|
+
upfront_cost_description: String.default(BLANK_STRING),
|
60
|
+
tv_channels_count: Integer.default(0),
|
61
|
+
terms: Array[Term].policy(size: 1..).default(BLANK_ARRAY),
|
62
|
+
tv_included: Boolean,
|
63
|
+
additional_info: String,
|
64
|
+
product_type: String.nullable,
|
65
|
+
annual_price_increase_applies: Boolean.default(false),
|
66
|
+
annual_price_increase_description: String.default(BLANK_STRING),
|
67
|
+
broadband_components: Array[
|
68
|
+
name: String,
|
69
|
+
technology: String,
|
70
|
+
is_mobile: Boolean.default(false),
|
71
|
+
desciption: String,
|
72
|
+
technology_tags: Array[String].default(BLANK_ARRAY),
|
73
|
+
download_speed_measurement: String.default(BLANK_STRING),
|
74
|
+
download_speed: Numeric.default(0),
|
75
|
+
upload_speed_measurement: String.default(BLANK_STRING),
|
76
|
+
upload_speed: Numeric.default(0),
|
77
|
+
download_usage_limit: Integer.nullable,
|
78
|
+
discount_price: Money.nullable,
|
79
|
+
discount_period: Integer.nullable,
|
80
|
+
speed_description: String.default(BLANK_STRING),
|
81
|
+
ongoing_price: Money.nullable,
|
82
|
+
contract_length: Integer.nullable,
|
83
|
+
upfront_cost: Money.nullable,
|
84
|
+
commission: Money.nullable
|
85
|
+
],
|
86
|
+
tv_components: Array[TvComponent].default(BLANK_ARRAY),
|
87
|
+
call_package_types: Array[String].default(BLANK_ARRAY).metadata(example: ['Everything']),
|
88
|
+
phone_components: Array[
|
89
|
+
name: String,
|
90
|
+
description: String,
|
91
|
+
discount_price: Money.nullable,
|
92
|
+
discount_period: Integer.nullable,
|
93
|
+
ongoing_price: Money.nullable,
|
94
|
+
contract_length: Integer.nullable,
|
95
|
+
upfront_cost: Money.nullable,
|
96
|
+
commission: Money.nullable,
|
97
|
+
call_package_type: Array[String].default(BLANK_ARRAY)
|
98
|
+
].default(BLANK_ARRAY),
|
99
|
+
payment_methods: Array[String].default(BLANK_ARRAY),
|
100
|
+
discounts: Array[period: Integer, price: Money.nullable],
|
101
|
+
ongoing_price: Money.nullable.metadata(admin_ui: true),
|
102
|
+
contract_length: Integer.nullable,
|
103
|
+
upfront_cost: Money.nullable,
|
104
|
+
year_1_price: Money.nullable.metadata(admin_ui: true),
|
105
|
+
savings: Money.nullable.metadata(admin_ui: true),
|
106
|
+
commission: Money.nullable,
|
107
|
+
max_broadband_download_speed: Integer.default(0)
|
108
|
+
]
|
109
|
+
end
|
data/examples/event_registry.rb
CHANGED
@@ -11,19 +11,14 @@ require 'debug'
|
|
11
11
|
module Types
|
12
12
|
include Plumb::Types
|
13
13
|
|
14
|
-
# Turn an ISO8601
|
15
|
-
ISOTime = String.build(::Time, :parse)
|
14
|
+
# Turn an ISO8601 string into a Time object
|
15
|
+
ISOTime = String.build(::Time, :parse).policy(:rescue, ArgumentError)
|
16
16
|
|
17
17
|
# A type that can be a Time object or an ISO8601 string >> Time
|
18
18
|
Time = Any[::Time] | ISOTime
|
19
19
|
|
20
|
-
# A UUID string
|
21
|
-
UUID = String[/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i]
|
22
|
-
|
23
20
|
# A UUID string, or generate a new one
|
24
|
-
AutoUUID = UUID.default { SecureRandom.uuid }
|
25
|
-
|
26
|
-
Email = String[URI::MailTo::EMAIL_REGEXP]
|
21
|
+
AutoUUID = UUID::V4.default { SecureRandom.uuid }
|
27
22
|
end
|
28
23
|
|
29
24
|
# A superclass and registry to define event types
|
@@ -57,7 +52,7 @@ end
|
|
57
52
|
# user_created.stream_id == create_user.stream_id
|
58
53
|
#
|
59
54
|
# ## JSON Schemas
|
60
|
-
# Plumb data structs support `.to_json_schema`,
|
55
|
+
# Plumb data structs support `.to_json_schema`, so you can document all events in the registry with something like
|
61
56
|
#
|
62
57
|
# Event.registry.values.map(&:to_json_schema)
|
63
58
|
#
|
@@ -66,17 +61,22 @@ class Event < Types::Data
|
|
66
61
|
attribute :stream_id, Types::String.present
|
67
62
|
attribute :type, Types::String
|
68
63
|
attribute(:created_at, Types::Time.default { ::Time.now })
|
69
|
-
attribute? :causation_id, Types::UUID
|
70
|
-
attribute? :correlation_id, Types::UUID
|
64
|
+
attribute? :causation_id, Types::UUID::V4
|
65
|
+
attribute? :correlation_id, Types::UUID::V4
|
71
66
|
attribute :payload, Types::Static[nil]
|
72
67
|
|
73
68
|
def self.registry
|
74
69
|
@registry ||= {}
|
75
70
|
end
|
76
71
|
|
72
|
+
# Custom node_name to trigger specialiesed JSON Schema visitor handler.
|
73
|
+
def self.node_name = :event
|
74
|
+
|
77
75
|
def self.define(type_str, &payload_block)
|
78
76
|
type_str.freeze unless type_str.frozen?
|
79
77
|
registry[type_str] = Class.new(self) do
|
78
|
+
def self.node_name = :data
|
79
|
+
|
80
80
|
attribute :type, Types::Static[type_str]
|
81
81
|
attribute :payload, &payload_block if block_given?
|
82
82
|
end
|
@@ -99,22 +99,29 @@ end
|
|
99
99
|
# Example command and events for a simple event-sourced system
|
100
100
|
#
|
101
101
|
# ## Commands
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
102
|
+
CreateUser = Event.define('users.create') do
|
103
|
+
attribute :name, Types::String.present
|
104
|
+
attribute :email, Types::Email
|
105
|
+
end
|
106
|
+
|
107
|
+
UpdateUserName = Event.define('users.update_name') do
|
108
|
+
attribute :name, Types::String.present
|
109
|
+
end
|
110
110
|
#
|
111
111
|
# ## Events
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
112
|
+
UserCreated = Event.define('users.created') do
|
113
|
+
attribute :name, Types::String
|
114
|
+
attribute :email, Types::Email
|
115
|
+
end
|
116
|
+
|
117
|
+
UserNameUpdated = Event.define('users.name_updated') do
|
118
|
+
attribute :name, Types::String
|
119
|
+
end
|
120
|
+
|
121
|
+
# Register a JSON Schema visitor handlers to render Event.registry as a "AnyOf" list of event types
|
122
|
+
Plumb::JSONSchemaVisitor.on(:event) do |node, props|
|
123
|
+
props.merge('type' => 'object', 'anyOf' => node.registry.values.map { |v| visit(v) })
|
124
|
+
end
|
125
|
+
|
126
|
+
p Plumb::JSONSchemaVisitor.call(Event)
|
120
127
|
# debugger
|
data/examples/weekdays.rb
CHANGED
@@ -52,7 +52,7 @@ p Types::DayName.parse('TueSday') # => "tuesday
|
|
52
52
|
p Types::Week.parse([3, 2, 1, 4, 5, 6, 7]) # => [1, 2, 3, 4, 5, 6, 7]
|
53
53
|
p Types::Week.parse([1, 'Tuesday', 3, 4, 5, 'saturday', 7]) # => [1, 2, 3, 4, 5, 6, 7]
|
54
54
|
|
55
|
-
# p Types::Week[[1, 1, 3, 4, 5, 6, 7]] # raises Plumb::
|
55
|
+
# p Types::Week[[1, 1, 3, 4, 5, 6, 7]] # raises Plumb::ParseError: repeated days
|
56
56
|
#
|
57
57
|
# Or use these types as part of other composite types, ex.
|
58
58
|
#
|
data/lib/plumb/attributes.rb
CHANGED
@@ -110,6 +110,7 @@ module Plumb
|
|
110
110
|
#
|
111
111
|
def self.included(base)
|
112
112
|
base.send(:extend, ClassMethods)
|
113
|
+
base.define_singleton_method(:__plumb_struct_class__) { base }
|
113
114
|
end
|
114
115
|
|
115
116
|
attr_reader :errors, :attributes
|
@@ -155,10 +156,10 @@ module Plumb
|
|
155
156
|
private
|
156
157
|
|
157
158
|
def assign_attributes(attrs = BLANK_HASH)
|
158
|
-
raise ArgumentError, 'Must be a Hash of attributes' unless attrs.
|
159
|
+
raise ArgumentError, 'Must be a Hash of attributes' unless attrs.respond_to?(:to_h)
|
159
160
|
|
160
161
|
@errors = BLANK_HASH
|
161
|
-
result = self.class._schema.resolve(attrs)
|
162
|
+
result = self.class._schema.resolve(attrs.to_h)
|
162
163
|
@attributes = result.value
|
163
164
|
@errors = result.errors unless result.valid?
|
164
165
|
end
|
@@ -180,14 +181,15 @@ module Plumb
|
|
180
181
|
# @return [Plumb::Result::Valid, Plumb::Result::Invalid]
|
181
182
|
def call(result)
|
182
183
|
return result if result.value.is_a?(self)
|
183
|
-
return result.invalid(errors: ['Must be a Hash of attributes']) unless result.value.
|
184
|
+
return result.invalid(errors: ['Must be a Hash of attributes']) unless result.value.respond_to?(:to_h)
|
184
185
|
|
185
|
-
instance = new(result.value)
|
186
|
-
instance.valid? ? result.valid(instance) : result.invalid(instance, errors: instance.errors)
|
186
|
+
instance = new(result.value.to_h)
|
187
|
+
instance.valid? ? result.valid(instance) : result.invalid(instance, errors: instance.errors.to_h)
|
187
188
|
end
|
188
189
|
|
189
190
|
# Person = Data[:name => String, :age => Integer, title?: String]
|
190
191
|
def [](type_specs)
|
192
|
+
type_specs = type_specs._schema if type_specs.is_a?(Plumb::HashClass)
|
191
193
|
klass = Class.new(self)
|
192
194
|
type_specs.each do |key, type|
|
193
195
|
klass.attribute(key, type)
|
@@ -210,11 +212,11 @@ module Plumb
|
|
210
212
|
name = key.to_sym
|
211
213
|
type = Composable.wrap(type)
|
212
214
|
if block_given? # :foo, Array[Data] or :foo, Struct
|
213
|
-
type =
|
215
|
+
type = __plumb_struct_class__ if type == Types::Any
|
214
216
|
type = Plumb.decorate(type) do |node|
|
215
217
|
if node.is_a?(Plumb::ArrayClass)
|
216
218
|
child = node.children.first
|
217
|
-
child =
|
219
|
+
child = __plumb_struct_class__ if child == Types::Any
|
218
220
|
Types::Array[build_nested(name, child, &block)]
|
219
221
|
elsif node.is_a?(Plumb::Step)
|
220
222
|
build_nested(name, node, &block)
|
@@ -227,6 +229,10 @@ module Plumb
|
|
227
229
|
end
|
228
230
|
|
229
231
|
@_schema = _schema + { key => type }
|
232
|
+
__plumb_define_attribute_method__(name)
|
233
|
+
end
|
234
|
+
|
235
|
+
def __plumb_define_attribute_method__(name)
|
230
236
|
define_method(name) { @attributes[name] }
|
231
237
|
end
|
232
238
|
|