plumb 0.0.14 → 0.0.15

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: bfaf49dcfcb3b25cc3dbca406efe1c666284df83466ee87c5b7faf63d613ab04
4
- data.tar.gz: 7a95598acd4e07180d68b17a6b72d3fddf00aa293503bdc353a37c585dcc95c0
3
+ metadata.gz: 104313a59a2f9187f582058b8f0f3c6dce43763079518d68fde511aef6112400
4
+ data.tar.gz: 4fc765c09d840145ebf5777cfd4e326f3705db5cfda8ee5324c5f6f4f81e0070
5
5
  SHA512:
6
- metadata.gz: 44fb44f7f2c6650ad1dc9a68b7945200bd87df874246ee0785d11e4fc936db89a552dca473753303e34cbecb8764fc2272d9c6829427664175bfaba1fe876623
7
- data.tar.gz: 6b27b375b6b2df1188ec821a918325a9ff97bd0b35c65ee16d4826ef8b018ee497f18e461a2e472281f50a3754c835347a49943dd3fdf0dac1aa949aea3b3145
6
+ metadata.gz: 2ca83f0ed1b4fbb2a83e11ff05bded28ed99550de9bfbec2d20d79943d886209c6b4cabeb2055857e24e60748642f0f7b541dcf5dc433107073772b085c5025a
7
+ data.tar.gz: 9d7b79dc0509f481730331bd3e65a1b85378d3609279b6c575ec8d77d8e786ccbe33b8dd54b0abfa826df06454793b235c1f8086a581c9a143359899cebb0318
data/README.md CHANGED
@@ -232,6 +232,7 @@ You can see more use cases in [the examples directory](https://github.com/ismasa
232
232
  * `Types::Numeric`
233
233
  * `Types::String`
234
234
  * `Types::Hash`
235
+ * `Types::SymbolizedHash`
235
236
  * `Types::UUID::V4`
236
237
  * `Types::Email`
237
238
  * `Types::Date`
@@ -838,7 +839,19 @@ User.parse(name: 'Joe', age: 40) # => { name: 'Joe', age: 40 }
838
839
  User.parse(name: 'Joe', age: 'nope') # => { name: 'Joe' }
839
840
  ```
840
841
 
841
- ### Hash maps
842
+ ### `Types::SymbolizedHash`
843
+
844
+ This type turns a hash's keys into symbols by calling `#to_sym` on them, and returning a new Hash.
845
+
846
+ ```ruby
847
+ # Make sure to symbolize keys first
848
+ type = Types::SymbolizedHash > Types::Hash[name: String, age: Integer]
849
+ type.parse('name' => 'Joe', 'age' => 20) # {name: 'Joe', age: 20}
850
+ ```
851
+
852
+
853
+
854
+ ### maps
842
855
 
843
856
  You can also use Hash syntax to define a hash map with specific types for all keys and values:
844
857
 
@@ -1177,7 +1190,51 @@ Using `attribute?` allows for optional attributes. If the attribute is not prese
1177
1190
  attribute? :company, Company
1178
1191
  ```
1179
1192
 
1193
+ #### Before steps, symbolizing keys
1194
+
1195
+ The optional `.step` helper adds arbitrary Plumb steps to a Data constructor's internal pipeline.
1196
+
1197
+ This pipeline processes input data when initialising a Data instance.
1198
+
1199
+ This example adds the built-in `Types::SymbolizedHash` type to make sure struct inputs are symbolised before processing.
1200
+
1201
+ ```ruby
1202
+ class Person < Types::Data
1203
+ step Types::SymbolizedHash
1204
+
1205
+ attribute :name, String
1206
+ attribute :age, Integer
1207
+ end
1208
+
1209
+ # String keys will be symbolised now
1210
+ person = Person.new('name' => 'Joe', 'age' => 40)
1211
+ person.name # 'Joe'
1212
+ person.to_h # => { name: 'Joe', age: 40 }
1213
+ ```
1214
+
1215
+ Inline blocks can be registered as steps
1216
+
1217
+ ```ruby
1218
+ class Person < Types::Data
1219
+ # upcase all values
1220
+ step do |r|
1221
+ upcased = r.value.transform_values(&:upcase)
1222
+ r.valid upcased
1223
+ end
1224
+
1225
+ attribute :name, String
1226
+ attribute :last_name, String
1227
+ end
1228
+
1229
+ person = Person.new(name: 'joe', last_name: 'bloggs')
1230
+ person.name # => 'JOE'
1231
+ person.last_name # => 'BLOGGS'
1232
+ ```
1233
+
1234
+ A Data class steps are inherited to its child classes.
1235
+
1180
1236
  #### Inheritance
1237
+
1181
1238
  Data structs can inherit from other structs. This is useful for defining a base struct with common attributes.
1182
1239
 
1183
1240
  ```ruby
@@ -116,7 +116,7 @@ module Plumb
116
116
  attr_reader :errors, :attributes
117
117
 
118
118
  def initialize(attrs = {})
119
- assign_attributes(attrs)
119
+ assign_attributes(self.class._pipeline.parse(attrs))
120
120
  freeze
121
121
  end
122
122
 
@@ -135,8 +135,8 @@ module Plumb
135
135
 
136
136
  def inspect
137
137
  %(#<#{self.class}:#{object_id} [#{valid? ? 'valid' : 'invalid'}] #{attributes.map do |k, v|
138
- [k, v.inspect].join(':')
139
- end.join(' ')}>)
138
+ [k, v.inspect].join(':')
139
+ end.join(' ')}>)
140
140
  end
141
141
 
142
142
  # @return [Hash]
@@ -175,11 +175,53 @@ module Plumb
175
175
  def prepare_attributes(attrs) = attrs
176
176
 
177
177
  module ClassMethods
178
+ def _set_pipeline(pl)
179
+ @_pipeline = pl
180
+ end
181
+
182
+ def _pipeline
183
+ @_pipeline || Plumb::Types::Any
184
+ end
185
+
186
+ # Add a step to the processing pipeline that runs before attribute validation.
187
+ # This allows you to transform or validate the input data before it's assigned to attributes.
188
+ #
189
+ # @param st [Plumb::Step, nil] A step object to add to the pipeline
190
+ # @param block [Proc, nil] A block to use as a step (if st is nil)
191
+ # @return [Class] Returns self for method chaining
192
+ #
193
+ # @example Transform input before validation
194
+ # class Person
195
+ # include Plumb::Attributes
196
+ #
197
+ # step { |result| result.valid(result.value.transform_keys(&:to_sym)) }
198
+ # attribute :name, Types::String
199
+ # end
200
+ #
201
+ # @example Add custom validation
202
+ # class Person
203
+ # include Plumb::Attributes
204
+ #
205
+ # step do |result|
206
+ # if result.value[:name].nil?
207
+ # result.invalid(errors: 'Name is required')
208
+ # else
209
+ # result
210
+ # end
211
+ # end
212
+ # attribute :name, Types::String
213
+ # end
214
+ def step(st = nil, &block)
215
+ @_pipeline = _pipeline >> (st || block)
216
+ self
217
+ end
218
+
178
219
  def _schema
179
220
  @_schema ||= HashClass.new
180
221
  end
181
222
 
182
223
  def inherited(subclass)
224
+ subclass._set_pipeline _pipeline
183
225
  _schema._schema.each do |key, type|
184
226
  subclass.attribute(key, type)
185
227
  end
@@ -220,7 +262,9 @@ module Plumb
220
262
  # attribute(:friends, [Person])
221
263
  #
222
264
  def attribute(name, type = Types::Any, writer: false, &block)
223
- key = Key.wrap(name)
265
+ # Key accepts String or Symbol, with optional '?' suffix for optional keys
266
+ # for Data structs, we always convert to Symbol keys
267
+ key = Key.wrap(name, symbolize: true)
224
268
  name = key.to_sym
225
269
  type = Composable.wrap(type)
226
270
 
@@ -113,7 +113,7 @@ module Plumb
113
113
  initial = {}
114
114
  initial = initial.merge(input) if @inclusive
115
115
  output = _schema.each.with_object(initial) do |(key, field), ret|
116
- key_s = key.to_sym
116
+ key_s = key.to_key
117
117
  if input.key?(key_s)
118
118
  r = field.call(field_result.reset(input[key_s]))
119
119
  errors[key_s] = r.errors unless r.valid?
data/lib/plumb/key.rb CHANGED
@@ -2,28 +2,30 @@
2
2
 
3
3
  module Plumb
4
4
  class Key
5
- OPTIONAL_EXP = /(\w+)(\?)?$/
5
+ # OPTIONAL_EXP = /(\w+)(\?)?$/
6
+ OPTIONAL_EXP = /(?<word>[A-Za-z0-9_$]+)(?<qmark>\?)?/
6
7
 
7
- def self.wrap(key)
8
- key.is_a?(Key) ? key : new(key)
8
+ def self.wrap(key, symbolize: false)
9
+ key.is_a?(Key) ? key : new(key, symbolize:)
9
10
  end
10
11
 
11
- attr_reader :to_sym, :node_name
12
+ attr_reader :to_key, :to_sym, :node_name
12
13
 
13
- def initialize(key, optional: false)
14
- key_s = key.to_s
15
- match = OPTIONAL_EXP.match(key_s)
14
+ def initialize(key, optional: false, symbolize: false)
15
+ key_type = symbolize ? Symbol : key.class
16
+ match = OPTIONAL_EXP.match(key.to_s)
17
+ key = match[:word]
18
+ @to_key = key_type == Symbol ? key.to_sym : key
19
+ @to_sym = @to_key.to_sym
20
+ @optional = !match[:qmark].nil? ? true : optional
16
21
  @node_name = :key
17
- @key = match[1]
18
- @to_sym = @key.to_sym
19
- @optional = !match[2].nil? ? true : optional
20
22
  freeze
21
23
  end
22
24
 
23
- def to_s = @key
25
+ def to_s = @to_key.to_s
24
26
 
25
27
  def hash
26
- @key.hash
28
+ @to_key.hash
27
29
  end
28
30
 
29
31
  def eql?(other)
@@ -35,7 +37,7 @@ module Plumb
35
37
  end
36
38
 
37
39
  def inspect
38
- "#{@key}#{'?' if @optional}"
40
+ "#{@to_key}#{'?' if @optional}"
39
41
  end
40
42
  end
41
43
  end
data/lib/plumb/types.rb CHANGED
@@ -155,6 +155,22 @@ module Plumb
155
155
  Date = Any[::Date]
156
156
  Time = Any[::Time]
157
157
 
158
+ # A type that recursively converts string keys to symbols in nested hashes.
159
+ # This is commonly used for normalizing payload data in commands and events.
160
+ #
161
+ # @example Simple hash symbolization
162
+ # SymbolizedHash.parse({ 'name' => 'John' }) # => { name: 'John' }
163
+ # @example Nested hash symbolization
164
+ # SymbolizedHash.parse({ 'user' => { 'name' => 'John' } }) # => { user: { name: 'John' } }
165
+ # @example Mixed types preserved
166
+ # SymbolizedHash.parse({ 'count' => 1, 'active' => true }) # => { count: 1, active: true }
167
+ SymbolizedHash = Hash[
168
+ # String keys are converted to symbols, existing symbols are preserved
169
+ (Symbol | String.transform(::Symbol, &:to_sym)),
170
+ # Hash values are recursively symbolized, other types pass through unchanged
171
+ Any.defer { SymbolizedHash } | Any
172
+ ]
173
+
158
174
  module UUID
159
175
  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)
160
176
  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.14'
4
+ VERSION = '0.0.15'
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.14
4
+ version: 0.0.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ismael Celis
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-10-03 00:00:00.000000000 Z
11
+ date: 2025-10-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bigdecimal