plumb 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
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