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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 896f2ec4a63cde86dd22aaec579696e19c980b09c09e4a4d9d4690f9505f742d
4
- data.tar.gz: 3296480dd0026e8050b624e3e1d65a020fde376a87c4ba269c6481b57c40d19e
3
+ metadata.gz: 308e76909c6466b0a6c2cc9443498a267186344b9508b8f485975479e0ff165a
4
+ data.tar.gz: 8498e5a4619437b8f91b3baae4b2d208c27031a5617dba174d52893cd4e3a54a
5
5
  SHA512:
6
- metadata.gz: 76ecebbe1dc408630107170a213d0145af08bc8d9cec3ef28d66f8aa1eb3fee09352ae89c00aed1eb11b3a52709100f65e6f0a2a43b4a38ed777709f096f9198
7
- data.tar.gz: 0cdbf9c24900fdbcd8393dc4f1d260063b303cc44cf84caa86bdb4d34a6edf77e1f75e431481121b9004c7fbac695f3021b4a7be965c34c1d4b96146f17c2c4c
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::Array](#typeshash) and [Types::Array](#typesarray). There's also [tuples](#typestuple), [hash maps](#hash-maps) and [data structs](#typesdata), and it's possible to create your own composite 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
- # User normally as any other Plumb step
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 wrapper by StepLogger
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 StepLogged
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
- Date = Any[::Date] \
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) \
@@ -26,9 +26,6 @@ module Types
26
26
  # Turn integers into Money objects (requires the money gem)
27
27
  Amount = Integer.build(Money)
28
28
 
29
- # A naive email check
30
- Email = String[/\w+@\w+\.\w+/]
31
-
32
29
  # A valid customer type
33
30
  Customer = Hash[
34
31
  name: String.present,
@@ -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::URL >> (cache.read | (Types::Download >> cache.write))
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.
@@ -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|
@@ -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
@@ -139,17 +139,8 @@ module Plumb
139
139
  end
140
140
 
141
141
  def wrap_keys_and_values(hash)
142
- case hash
143
- when ::Array
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plumb
4
- VERSION = '0.0.5'
4
+ VERSION = '0.0.6'
5
5
  end
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.5
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-08-30 00:00:00.000000000 Z
11
+ date: 2024-09-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bigdecimal