plumb 0.0.5 → 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 +35 -5
- data/bench/plumb_hash.rb +1 -11
- data/examples/command_objects.rb +0 -3
- data/examples/concurrent_downloads.rb +2 -5
- data/examples/weekdays.rb +1 -1
- data/lib/plumb/attributes.rb +3 -0
- data/lib/plumb/composable.rb +11 -0
- data/lib/plumb/hash_class.rb +2 -11
- data/lib/plumb/json_schema_visitor.rb +12 -0
- data/lib/plumb/types.rb +18 -0
- data/lib/plumb/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 308e76909c6466b0a6c2cc9443498a267186344b9508b8f485975479e0ff165a
|
4
|
+
data.tar.gz: 8498e5a4619437b8f91b3baae4b2d208c27031a5617dba174d52893cd4e3a54a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d41ebdf232099770d04abc85f81ead1e8dc1d4f55eb1bc9265484401cfd0418e984d7cf97a67a6ef452d67f05c3f92e66e3e3fe64f11622acbb89e5c223c73b1
|
7
|
+
data.tar.gz: 5e2749e954fae81753d63d6d27b95a53f239b5ac6ad776755646d794fe7819b56087f48bd99394aeab2f40c64d45606cc413a1475512f6943279d51a7dd7d2b2
|
data/README.md
CHANGED
@@ -135,7 +135,7 @@ joe = User.parse({ name: 'Joe', email: 'joe@email.com', age: 20}) # returns vali
|
|
135
135
|
Users.parse([joe]) # returns valid array of user hashes
|
136
136
|
```
|
137
137
|
|
138
|
-
More about [Types::
|
138
|
+
More about [Types::Hash](#typeshash) and [Types::Array](#typesarray). There's also [tuples](#typestuple), [hash maps](#hash-maps), [data structs](#typesdata) and [streams](#typesstream), and it's possible to create your own composite types.
|
139
139
|
|
140
140
|
### Type composition
|
141
141
|
|
@@ -235,6 +235,10 @@ You can see more use cases in [the examples directory](https://github.com/ismasa
|
|
235
235
|
* `Types::UUID::V4`
|
236
236
|
* `Types::Email`
|
237
237
|
* `Types::Date`
|
238
|
+
* `Types::Time`
|
239
|
+
* `Types::URI::Generic`
|
240
|
+
* `Types::URI::HTTP`
|
241
|
+
* `Types::URI::File`
|
238
242
|
* `Types::Lax::Integer`
|
239
243
|
* `Types::Lax::String`
|
240
244
|
* `Types::Lax::Symbol`
|
@@ -243,6 +247,10 @@ You can see more use cases in [the examples directory](https://github.com/ismasa
|
|
243
247
|
* `Types::Forms::True`
|
244
248
|
* `Types::Forms::False`
|
245
249
|
* `Types::Forms::Date`
|
250
|
+
* `Types::Forms::Time`
|
251
|
+
* `Types::Forms::URI::Generic`
|
252
|
+
* `Types::Forms::URI::HTTP`
|
253
|
+
* `Types::Forms::URI::File`
|
246
254
|
|
247
255
|
TODO: datetime, others.
|
248
256
|
|
@@ -824,13 +832,15 @@ Images = Types::Array[ImageDownload].concurrent
|
|
824
832
|
Images.parse(['https://images.com/1.png', 'https://images.com/2.png'])
|
825
833
|
```
|
826
834
|
|
835
|
+
See the [concurrent downloads example](https://github.com/ismasan/plumb/blob/main/examples/concurrent_downloads.rb).
|
836
|
+
|
827
837
|
TODO: pluggable concurrency engines (Async?)
|
828
838
|
|
829
839
|
#### `#stream`
|
830
840
|
|
831
841
|
Turn an Array definition into an enumerator that yields each element wrapped in `Result::Valid` or `Result::Invalid`.
|
832
842
|
|
833
|
-
See `Types::Stream` below for more.
|
843
|
+
See [`Types::Stream`](#typesstream) below for more.
|
834
844
|
|
835
845
|
#### `#filtered`
|
836
846
|
|
@@ -899,6 +909,8 @@ stream.each.with_index(1) do |result, line|
|
|
899
909
|
end
|
900
910
|
```
|
901
911
|
|
912
|
+
See a more complete the [CSV Stream example](https://github.com/ismasan/plumb/blob/main/examples/csv_stream.rb)
|
913
|
+
|
902
914
|
#### `Types::Stream#filtered`
|
903
915
|
|
904
916
|
Use `#filtered` to turn a `Types::Stream` into a stream that only yields valid elements.
|
@@ -1053,7 +1065,25 @@ Note that this does NOT work with union'd or piped structs.
|
|
1053
1065
|
attribute :company, Company | Person do
|
1054
1066
|
```
|
1055
1067
|
|
1068
|
+
#### Shorthand array syntax
|
1069
|
+
|
1070
|
+
```ruby
|
1071
|
+
attribute :things, [] # Same as attribute :things, Types::Array
|
1072
|
+
attribute :numbers, [Integer] # Same as attribute :numbers, Types::Array[Integer]
|
1073
|
+
attribute :people, [Person] # same as attribute :people, Types::Array[Person]
|
1074
|
+
attribute :friends, [Person] do # same as attribute :friends, Types::Array[Person] do...
|
1075
|
+
attribute :phone_number, Integer
|
1076
|
+
end
|
1077
|
+
```
|
1078
|
+
|
1079
|
+
Note that, if you want to match an attribute value against a literal array, you need to use `#value`
|
1080
|
+
|
1081
|
+
```ruby
|
1082
|
+
attribute :one_two_three, Types::Array.value[[1, 2, 3]])
|
1083
|
+
```
|
1084
|
+
|
1056
1085
|
#### Optional Attributes
|
1086
|
+
|
1057
1087
|
Using `attribute?` allows for optional attributes. If the attribute is not present, these attribute values will be `nil`
|
1058
1088
|
|
1059
1089
|
```ruby
|
@@ -1149,7 +1179,7 @@ CreateUser = User.pipeline do |pl|
|
|
1149
1179
|
end
|
1150
1180
|
end
|
1151
1181
|
|
1152
|
-
#
|
1182
|
+
# Use normally as any other Plumb step
|
1153
1183
|
result = CreateUser.resolve(name: 'Joe', age: 40)
|
1154
1184
|
# result.valid?
|
1155
1185
|
# result.errors
|
@@ -1189,7 +1219,7 @@ end
|
|
1189
1219
|
Note that order matters: an _around_ step will only wrap steps registered _after it_.
|
1190
1220
|
|
1191
1221
|
```ruby
|
1192
|
-
# This step will not be
|
1222
|
+
# This step will not be wrapped by StepLogger
|
1193
1223
|
pl.step Step1
|
1194
1224
|
|
1195
1225
|
pl.around StepLogger
|
@@ -1227,7 +1257,7 @@ end
|
|
1227
1257
|
```ruby
|
1228
1258
|
class LoggedPipeline < Plumb::Pipeline
|
1229
1259
|
# class-level midleware will be inherited by sub-classes
|
1230
|
-
around
|
1260
|
+
around StepLogger
|
1231
1261
|
end
|
1232
1262
|
|
1233
1263
|
# Subclass inherits class-level middleware stack,
|
data/bench/plumb_hash.rb
CHANGED
@@ -12,22 +12,12 @@ module PlumbHash
|
|
12
12
|
BLANK_STRING = ''
|
13
13
|
MONEY_EXP = /(\W{1}|\w{3})?[\d+,.]/
|
14
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
15
|
PARSE_MONEY = proc do |result|
|
23
16
|
value = Monetize.parse!(result.value.to_s.gsub(',', ''))
|
24
17
|
result.valid(value)
|
25
18
|
end
|
26
19
|
|
27
|
-
|
28
|
-
| (String[MONEY_EXP] >> PARSE_DATE)
|
29
|
-
|
30
|
-
BlankStringOrDate = Forms::Nil | Date
|
20
|
+
BlankStringOrDate = Forms::Nil | Forms::Date
|
31
21
|
|
32
22
|
Money = Any[::Money] \
|
33
23
|
| (String.present >> PARSE_MONEY) \
|
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/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.
|
data/lib/plumb/attributes.rb
CHANGED
@@ -205,12 +205,15 @@ module Plumb
|
|
205
205
|
# attribute(:name, String)
|
206
206
|
# attribute(:friends, Types::Array) { attribute(:name, String) }
|
207
207
|
# attribute(:friends, Types::Array) # same as Types::Array[Types::Any]
|
208
|
+
# attribute(:friends, []) # same as Types::Array[Types::Any]
|
208
209
|
# attribute(:friends, Types::Array[Person])
|
210
|
+
# attribute(:friends, [Person])
|
209
211
|
#
|
210
212
|
def attribute(name, type = Types::Any, &block)
|
211
213
|
key = Key.wrap(name)
|
212
214
|
name = key.to_sym
|
213
215
|
type = Composable.wrap(type)
|
216
|
+
|
214
217
|
if block_given? # :foo, Array[Data] or :foo, Struct
|
215
218
|
type = __plumb_struct_class__ if type == Types::Any
|
216
219
|
type = Plumb.decorate(type) do |node|
|
data/lib/plumb/composable.rb
CHANGED
@@ -100,6 +100,7 @@ module Plumb
|
|
100
100
|
# Wrap an object in a Composable instance.
|
101
101
|
# Anything that includes Composable is a noop.
|
102
102
|
# A Hash is assumed to be a HashClass schema.
|
103
|
+
# An Array with zero or 1 element is assumed to be an ArrayClass.
|
103
104
|
# Any `#call(Result) => Result` interface is wrapped in a Step.
|
104
105
|
# Anything else is assumed to be something you want to match against via `#===`.
|
105
106
|
#
|
@@ -115,6 +116,16 @@ module Plumb
|
|
115
116
|
callable
|
116
117
|
elsif callable.is_a?(::Hash)
|
117
118
|
HashClass.new(schema: callable)
|
119
|
+
elsif callable.is_a?(::Array)
|
120
|
+
element_type = case callable.size
|
121
|
+
when 0
|
122
|
+
Types::Any
|
123
|
+
when 1
|
124
|
+
callable.first
|
125
|
+
else
|
126
|
+
raise ArgumentError, '[element_type] syntax allows a single element type'
|
127
|
+
end
|
128
|
+
Types::Array[element_type]
|
118
129
|
elsif callable.respond_to?(:call)
|
119
130
|
Step.new(callable)
|
120
131
|
else
|
data/lib/plumb/hash_class.rb
CHANGED
@@ -139,17 +139,8 @@ module Plumb
|
|
139
139
|
end
|
140
140
|
|
141
141
|
def wrap_keys_and_values(hash)
|
142
|
-
|
143
|
-
|
144
|
-
hash.map { |e| wrap_keys_and_values(e) }
|
145
|
-
when ::Hash
|
146
|
-
hash.each.with_object({}) do |(k, v), ret|
|
147
|
-
ret[Key.wrap(k)] = wrap_keys_and_values(v)
|
148
|
-
end
|
149
|
-
when Callable
|
150
|
-
hash
|
151
|
-
else # leaf values
|
152
|
-
Composable.wrap(hash)
|
142
|
+
hash.each.with_object({}) do |(k, v), ret|
|
143
|
+
ret[Key.wrap(k)] = Composable.wrap(v)
|
153
144
|
end
|
154
145
|
end
|
155
146
|
|
@@ -255,6 +255,18 @@ module Plumb
|
|
255
255
|
props.merge(TYPE => 'string', FORMAT => 'date')
|
256
256
|
end
|
257
257
|
|
258
|
+
on(::URI::Generic) do |_node, props|
|
259
|
+
props.merge(TYPE => 'string', FORMAT => 'uri')
|
260
|
+
end
|
261
|
+
|
262
|
+
on(::URI::HTTP) do |_node, props|
|
263
|
+
props.merge(TYPE => 'string', FORMAT => 'uri')
|
264
|
+
end
|
265
|
+
|
266
|
+
on(::URI::File) do |_node, props|
|
267
|
+
props.merge(TYPE => 'string', FORMAT => 'uri')
|
268
|
+
end
|
269
|
+
|
258
270
|
on(::Hash) do |_node, props|
|
259
271
|
props.merge(TYPE => 'object')
|
260
272
|
end
|
data/lib/plumb/types.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'bigdecimal'
|
4
4
|
require 'uri'
|
5
5
|
require 'date'
|
6
|
+
require 'time'
|
6
7
|
|
7
8
|
module Plumb
|
8
9
|
# Define core policies
|
@@ -161,11 +162,18 @@ module Plumb
|
|
161
162
|
Interface = InterfaceClass.new
|
162
163
|
Email = String[URI::MailTo::EMAIL_REGEXP].as_node(:email)
|
163
164
|
Date = Any[::Date]
|
165
|
+
Time = Any[::Time]
|
164
166
|
|
165
167
|
module UUID
|
166
168
|
V4 = String[/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i].as_node(:uuid)
|
167
169
|
end
|
168
170
|
|
171
|
+
module URI
|
172
|
+
Generic = Any[::URI::Generic]
|
173
|
+
HTTP = Any[::URI::HTTP]
|
174
|
+
File = Any[::URI::File]
|
175
|
+
end
|
176
|
+
|
169
177
|
class Data
|
170
178
|
extend Composable
|
171
179
|
include Plumb::Attributes
|
@@ -214,6 +222,16 @@ module Plumb
|
|
214
222
|
# Accept a Date, or a string that can be parsed into a Date
|
215
223
|
# via Date.parse
|
216
224
|
Date = Date | (String >> Any.build(::Date, :parse).policy(:rescue, ::Date::Error))
|
225
|
+
Time = Time | (String >> Any.build(::Time, :parse).policy(:rescue, ::ArgumentError))
|
226
|
+
|
227
|
+
# Turn strings into different URI types
|
228
|
+
module URI
|
229
|
+
# URI.parse is very permisive - a blank string is valid.
|
230
|
+
# We want to ensure that a generic URI at least starts with a scheme as per RFC 3986
|
231
|
+
Generic = Types::URI::Generic | (String[/^([a-z][a-z0-9+\-.]*)/].build(::URI, :parse))
|
232
|
+
HTTP = Generic[::URI::HTTP]
|
233
|
+
File = Generic[::URI::File]
|
234
|
+
end
|
217
235
|
end
|
218
236
|
end
|
219
237
|
end
|
data/lib/plumb/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: plumb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ismael Celis
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-09-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bigdecimal
|