plumb 0.0.4 → 0.0.6
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 +255 -12
- 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 +99 -0
- data/examples/command_objects.rb +0 -3
- data/examples/concurrent_downloads.rb +2 -5
- data/examples/event_registry.rb +34 -27
- data/examples/weekdays.rb +2 -2
- data/lib/plumb/attributes.rb +16 -7
- data/lib/plumb/composable.rb +134 -4
- data/lib/plumb/hash_class.rb +2 -11
- data/lib/plumb/json_schema_visitor.rb +23 -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 +42 -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,99 @@
|
|
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_MONEY = proc do |result|
|
16
|
+
value = Monetize.parse!(result.value.to_s.gsub(',', ''))
|
17
|
+
result.valid(value)
|
18
|
+
end
|
19
|
+
|
20
|
+
BlankStringOrDate = Forms::Nil | Forms::Date
|
21
|
+
|
22
|
+
Money = Any[::Money] \
|
23
|
+
| (String.present >> PARSE_MONEY) \
|
24
|
+
| (Numeric >> PARSE_MONEY)
|
25
|
+
|
26
|
+
Term = Hash[
|
27
|
+
name: String.default(BLANK_STRING),
|
28
|
+
url: String.default(BLANK_STRING),
|
29
|
+
terms_text: String.default(BLANK_STRING),
|
30
|
+
start_date?: BlankStringOrDate.nullable,
|
31
|
+
end_date?: BlankStringOrDate.nullable
|
32
|
+
]
|
33
|
+
|
34
|
+
TvComponent = Hash[
|
35
|
+
slug: String,
|
36
|
+
name: String.present,
|
37
|
+
search_tags: Array[String].default(BLANK_ARRAY),
|
38
|
+
description: String.default(BLANK_STRING),
|
39
|
+
channels: Integer.default(0),
|
40
|
+
discount_price: Money.default(::Money.zero.freeze)
|
41
|
+
]
|
42
|
+
|
43
|
+
Record = Hash[
|
44
|
+
supplier_name: String.present,
|
45
|
+
start_date: BlankStringOrDate.nullable.metadata(admin_ui: true),
|
46
|
+
end_date: BlankStringOrDate.nullable.metadata(admin_ui: true),
|
47
|
+
countdown_date: BlankStringOrDate.nullable,
|
48
|
+
name: String.present,
|
49
|
+
upfront_cost_description: String.default(BLANK_STRING),
|
50
|
+
tv_channels_count: Integer.default(0),
|
51
|
+
terms: Array[Term].policy(size: 1..).default(BLANK_ARRAY),
|
52
|
+
tv_included: Boolean,
|
53
|
+
additional_info: String,
|
54
|
+
product_type: String.nullable,
|
55
|
+
annual_price_increase_applies: Boolean.default(false),
|
56
|
+
annual_price_increase_description: String.default(BLANK_STRING),
|
57
|
+
broadband_components: Array[
|
58
|
+
name: String,
|
59
|
+
technology: String,
|
60
|
+
is_mobile: Boolean.default(false),
|
61
|
+
desciption: String,
|
62
|
+
technology_tags: Array[String].default(BLANK_ARRAY),
|
63
|
+
download_speed_measurement: String.default(BLANK_STRING),
|
64
|
+
download_speed: Numeric.default(0),
|
65
|
+
upload_speed_measurement: String.default(BLANK_STRING),
|
66
|
+
upload_speed: Numeric.default(0),
|
67
|
+
download_usage_limit: Integer.nullable,
|
68
|
+
discount_price: Money.nullable,
|
69
|
+
discount_period: Integer.nullable,
|
70
|
+
speed_description: String.default(BLANK_STRING),
|
71
|
+
ongoing_price: Money.nullable,
|
72
|
+
contract_length: Integer.nullable,
|
73
|
+
upfront_cost: Money.nullable,
|
74
|
+
commission: Money.nullable
|
75
|
+
],
|
76
|
+
tv_components: Array[TvComponent].default(BLANK_ARRAY),
|
77
|
+
call_package_types: Array[String].default(BLANK_ARRAY).metadata(example: ['Everything']),
|
78
|
+
phone_components: Array[
|
79
|
+
name: String,
|
80
|
+
description: String,
|
81
|
+
discount_price: Money.nullable,
|
82
|
+
discount_period: Integer.nullable,
|
83
|
+
ongoing_price: Money.nullable,
|
84
|
+
contract_length: Integer.nullable,
|
85
|
+
upfront_cost: Money.nullable,
|
86
|
+
commission: Money.nullable,
|
87
|
+
call_package_type: Array[String].default(BLANK_ARRAY)
|
88
|
+
].default(BLANK_ARRAY),
|
89
|
+
payment_methods: Array[String].default(BLANK_ARRAY),
|
90
|
+
discounts: Array[period: Integer, price: Money.nullable],
|
91
|
+
ongoing_price: Money.nullable.metadata(admin_ui: true),
|
92
|
+
contract_length: Integer.nullable,
|
93
|
+
upfront_cost: Money.nullable,
|
94
|
+
year_1_price: Money.nullable.metadata(admin_ui: true),
|
95
|
+
savings: Money.nullable.metadata(admin_ui: true),
|
96
|
+
commission: Money.nullable,
|
97
|
+
max_broadband_download_speed: Integer.default(0)
|
98
|
+
]
|
99
|
+
end
|
data/examples/command_objects.rb
CHANGED
@@ -12,9 +12,6 @@ require 'digest/md5'
|
|
12
12
|
module Types
|
13
13
|
include Plumb::Types
|
14
14
|
|
15
|
-
# Turn a string into an URI
|
16
|
-
URL = String[/^https?:/].build(::URI, :parse)
|
17
|
-
|
18
15
|
# a Struct to hold image data
|
19
16
|
Image = ::Data.define(:url, :io)
|
20
17
|
|
@@ -24,7 +21,7 @@ module Types
|
|
24
21
|
# required by all Plumb steps.
|
25
22
|
# URI => Image
|
26
23
|
Download = Plumb::Step.new do |result|
|
27
|
-
io = URI.open(result.value)
|
24
|
+
io = ::URI.open(result.value)
|
28
25
|
result.valid(Image.new(result.value.to_s, io))
|
29
26
|
end
|
30
27
|
|
@@ -81,7 +78,7 @@ cache = Types::Cache.new('./examples/data/downloads')
|
|
81
78
|
# 1). Take a valid URL string.
|
82
79
|
# 2). Attempt reading the file from the cache. Return that if it exists.
|
83
80
|
# 3). Otherwise, download the file from the internet and write it to the cache.
|
84
|
-
IdempotentDownload = Types::
|
81
|
+
IdempotentDownload = Types::Forms::URI::HTTP >> (cache.read | (Types::Download >> cache.write))
|
85
82
|
|
86
83
|
# An array of downloadable images,
|
87
84
|
# marked as concurrent so that all IO operations are run in threads.
|
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
@@ -4,7 +4,7 @@ require 'bundler'
|
|
4
4
|
Bundler.setup(:examples)
|
5
5
|
require 'plumb'
|
6
6
|
|
7
|
-
# bundle exec examples/weekdays.rb
|
7
|
+
# bundle exec ruby examples/weekdays.rb
|
8
8
|
#
|
9
9
|
# Data types to represent and parse an array of days of the week.
|
10
10
|
# Input data can be an array of day names or numbers, ex.
|
@@ -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)
|
@@ -203,18 +205,21 @@ module Plumb
|
|
203
205
|
# attribute(:name, String)
|
204
206
|
# attribute(:friends, Types::Array) { attribute(:name, String) }
|
205
207
|
# attribute(:friends, Types::Array) # same as Types::Array[Types::Any]
|
208
|
+
# attribute(:friends, []) # same as Types::Array[Types::Any]
|
206
209
|
# attribute(:friends, Types::Array[Person])
|
210
|
+
# attribute(:friends, [Person])
|
207
211
|
#
|
208
212
|
def attribute(name, type = Types::Any, &block)
|
209
213
|
key = Key.wrap(name)
|
210
214
|
name = key.to_sym
|
211
215
|
type = Composable.wrap(type)
|
216
|
+
|
212
217
|
if block_given? # :foo, Array[Data] or :foo, Struct
|
213
|
-
type =
|
218
|
+
type = __plumb_struct_class__ if type == Types::Any
|
214
219
|
type = Plumb.decorate(type) do |node|
|
215
220
|
if node.is_a?(Plumb::ArrayClass)
|
216
221
|
child = node.children.first
|
217
|
-
child =
|
222
|
+
child = __plumb_struct_class__ if child == Types::Any
|
218
223
|
Types::Array[build_nested(name, child, &block)]
|
219
224
|
elsif node.is_a?(Plumb::Step)
|
220
225
|
build_nested(name, node, &block)
|
@@ -227,6 +232,10 @@ module Plumb
|
|
227
232
|
end
|
228
233
|
|
229
234
|
@_schema = _schema + { key => type }
|
235
|
+
__plumb_define_attribute_method__(name)
|
236
|
+
end
|
237
|
+
|
238
|
+
def __plumb_define_attribute_method__(name)
|
230
239
|
define_method(name) { @attributes[name] }
|
231
240
|
end
|
232
241
|
|