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
 
    
        data/lib/plumb/composable.rb
    CHANGED
    
    | 
         @@ -13,7 +13,7 @@ module Plumb 
     | 
|
| 
       13 
13 
     | 
    
         
             
                def empty? = true
         
     | 
| 
       14 
14 
     | 
    
         
             
              end
         
     | 
| 
       15 
15 
     | 
    
         | 
| 
       16 
     | 
    
         
            -
               
     | 
| 
      
 16 
     | 
    
         
            +
              ParseError = Class.new(::TypeError)
         
     | 
| 
       17 
17 
     | 
    
         
             
              Undefined = UndefinedClass.new.freeze
         
     | 
| 
       18 
18 
     | 
    
         | 
| 
       19 
19 
     | 
    
         
             
              BLANK_STRING = ''
         
     | 
| 
         @@ -29,7 +29,7 @@ module Plumb 
     | 
|
| 
       29 
29 
     | 
    
         | 
| 
       30 
30 
     | 
    
         
             
                def parse(value = Undefined)
         
     | 
| 
       31 
31 
     | 
    
         
             
                  result = resolve(value)
         
     | 
| 
       32 
     | 
    
         
            -
                  raise  
     | 
| 
      
 32 
     | 
    
         
            +
                  raise ParseError, result.errors if result.invalid?
         
     | 
| 
       33 
33 
     | 
    
         | 
| 
       34 
34 
     | 
    
         
             
                  result.value
         
     | 
| 
       35 
35 
     | 
    
         
             
                end
         
     | 
| 
         @@ -87,6 +87,7 @@ module Plumb 
     | 
|
| 
       87 
87 
     | 
    
         
             
              #  Composable mixes in composition methods to classes.
         
     | 
| 
       88 
88 
     | 
    
         
             
              # such as #>>, #|, #not, and others.
         
     | 
| 
       89 
89 
     | 
    
         
             
              # Any Composable class can participate in Plumb compositions.
         
     | 
| 
      
 90 
     | 
    
         
            +
              # A host object only needs to implement the Step interface `call(Result::Valid) => Result::Valid | Result::Invalid`
         
     | 
| 
       90 
91 
     | 
    
         
             
              module Composable
         
     | 
| 
       91 
92 
     | 
    
         
             
                include Callable
         
     | 
| 
       92 
93 
     | 
    
         | 
| 
         @@ -96,11 +97,35 @@ module Plumb 
     | 
|
| 
       96 
97 
     | 
    
         
             
                  base.send(:include, Naming)
         
     | 
| 
       97 
98 
     | 
    
         
             
                end
         
     | 
| 
       98 
99 
     | 
    
         | 
| 
      
 100 
     | 
    
         
            +
                # Wrap an object in a Composable instance.
         
     | 
| 
      
 101 
     | 
    
         
            +
                # Anything that includes Composable is a noop.
         
     | 
| 
      
 102 
     | 
    
         
            +
                # A Hash is assumed to be a HashClass schema.
         
     | 
| 
      
 103 
     | 
    
         
            +
                # An Array with zero or 1 element is assumed to be an ArrayClass.
         
     | 
| 
      
 104 
     | 
    
         
            +
                # Any `#call(Result) => Result` interface is wrapped in a Step.
         
     | 
| 
      
 105 
     | 
    
         
            +
                # Anything else is assumed to be something you want to match against via `#===`.
         
     | 
| 
      
 106 
     | 
    
         
            +
                #
         
     | 
| 
      
 107 
     | 
    
         
            +
                # @example
         
     | 
| 
      
 108 
     | 
    
         
            +
                #   ten = Composable.wrap(10)
         
     | 
| 
      
 109 
     | 
    
         
            +
                #   ten.resolve(10) # => Result::Valid
         
     | 
| 
      
 110 
     | 
    
         
            +
                #   ten.resolve(11) # => Result::Invalid
         
     | 
| 
      
 111 
     | 
    
         
            +
                #
         
     | 
| 
      
 112 
     | 
    
         
            +
                # @param callable [Object]
         
     | 
| 
      
 113 
     | 
    
         
            +
                # @return [Composable]
         
     | 
| 
       99 
114 
     | 
    
         
             
                def self.wrap(callable)
         
     | 
| 
       100 
115 
     | 
    
         
             
                  if callable.is_a?(Composable)
         
     | 
| 
       101 
116 
     | 
    
         
             
                    callable
         
     | 
| 
       102 
117 
     | 
    
         
             
                  elsif callable.is_a?(::Hash)
         
     | 
| 
       103 
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]
         
     | 
| 
       104 
129 
     | 
    
         
             
                  elsif callable.respond_to?(:call)
         
     | 
| 
       105 
130 
     | 
    
         
             
                    Step.new(callable)
         
     | 
| 
       106 
131 
     | 
    
         
             
                  else
         
     | 
| 
         @@ -108,26 +133,66 @@ module Plumb 
     | 
|
| 
       108 
133 
     | 
    
         
             
                  end
         
     | 
| 
       109 
134 
     | 
    
         
             
                end
         
     | 
| 
       110 
135 
     | 
    
         | 
| 
      
 136 
     | 
    
         
            +
                # A helper to wrap a block in a Step that will defer execution.
         
     | 
| 
      
 137 
     | 
    
         
            +
                # This so that types can be used recursively in compositions.
         
     | 
| 
      
 138 
     | 
    
         
            +
                # @example
         
     | 
| 
      
 139 
     | 
    
         
            +
                #   LinkedList = Types::Hash[
         
     | 
| 
      
 140 
     | 
    
         
            +
                #     value: Types::Any,
         
     | 
| 
      
 141 
     | 
    
         
            +
                #     next: Types::Any.defer { LinkedList }
         
     | 
| 
      
 142 
     | 
    
         
            +
                #   ]
         
     | 
| 
       111 
143 
     | 
    
         
             
                def defer(definition = nil, &block)
         
     | 
| 
       112 
144 
     | 
    
         
             
                  Deferred.new(definition || block)
         
     | 
| 
       113 
145 
     | 
    
         
             
                end
         
     | 
| 
       114 
146 
     | 
    
         | 
| 
      
 147 
     | 
    
         
            +
                # Chain two composable objects together.
         
     | 
| 
      
 148 
     | 
    
         
            +
                # A.K.A "and" or "sequence"
         
     | 
| 
      
 149 
     | 
    
         
            +
                # @example
         
     | 
| 
      
 150 
     | 
    
         
            +
                #   Step1 >> Step2 >> Step3
         
     | 
| 
      
 151 
     | 
    
         
            +
                #
         
     | 
| 
      
 152 
     | 
    
         
            +
                # @param other [Composable]
         
     | 
| 
      
 153 
     | 
    
         
            +
                # @return [And]
         
     | 
| 
       115 
154 
     | 
    
         
             
                def >>(other)
         
     | 
| 
       116 
155 
     | 
    
         
             
                  And.new(self, Composable.wrap(other))
         
     | 
| 
       117 
156 
     | 
    
         
             
                end
         
     | 
| 
       118 
157 
     | 
    
         | 
| 
      
 158 
     | 
    
         
            +
                # Chain two composable objects together as a disjunction ("or").
         
     | 
| 
      
 159 
     | 
    
         
            +
                #
         
     | 
| 
      
 160 
     | 
    
         
            +
                # @param other [Composable]
         
     | 
| 
      
 161 
     | 
    
         
            +
                # @return [Or]
         
     | 
| 
       119 
162 
     | 
    
         
             
                def |(other)
         
     | 
| 
       120 
163 
     | 
    
         
             
                  Or.new(self, Composable.wrap(other))
         
     | 
| 
       121 
164 
     | 
    
         
             
                end
         
     | 
| 
       122 
165 
     | 
    
         | 
| 
      
 166 
     | 
    
         
            +
                # Transform value. Requires specifying the resulting type of the value after transformation.
         
     | 
| 
      
 167 
     | 
    
         
            +
                # @example
         
     | 
| 
      
 168 
     | 
    
         
            +
                #   Types::String.transform(Types::Symbol, &:to_sym)
         
     | 
| 
      
 169 
     | 
    
         
            +
                #
         
     | 
| 
      
 170 
     | 
    
         
            +
                # @param target_type [Class] what type this step will transform the value to
         
     | 
| 
      
 171 
     | 
    
         
            +
                # @param callable [#call, nil] a callable that will be applied to the value, or nil if block provided
         
     | 
| 
      
 172 
     | 
    
         
            +
                # @param block [Proc] a block that will be applied to the value, or nil if callable provided
         
     | 
| 
      
 173 
     | 
    
         
            +
                # @return [And]
         
     | 
| 
       123 
174 
     | 
    
         
             
                def transform(target_type, callable = nil, &block)
         
     | 
| 
       124 
175 
     | 
    
         
             
                  self >> Transform.new(target_type, callable || block)
         
     | 
| 
       125 
176 
     | 
    
         
             
                end
         
     | 
| 
       126 
177 
     | 
    
         | 
| 
      
 178 
     | 
    
         
            +
                # Pass the value through an arbitrary validation
         
     | 
| 
      
 179 
     | 
    
         
            +
                # @example
         
     | 
| 
      
 180 
     | 
    
         
            +
                #   type = Types::String.check('must start with "Role:"') { |value| value.start_with?('Role:') }
         
     | 
| 
      
 181 
     | 
    
         
            +
                #
         
     | 
| 
      
 182 
     | 
    
         
            +
                # @param errors [String] error message to use when validation fails
         
     | 
| 
      
 183 
     | 
    
         
            +
                # @param block [Proc] a block that will be applied to the value
         
     | 
| 
      
 184 
     | 
    
         
            +
                # @return [And]
         
     | 
| 
       127 
185 
     | 
    
         
             
                def check(errors = 'did not pass the check', &block)
         
     | 
| 
       128 
186 
     | 
    
         
             
                  self >> MatchClass.new(block, error: errors, label: errors)
         
     | 
| 
       129 
187 
     | 
    
         
             
                end
         
     | 
| 
       130 
188 
     | 
    
         | 
| 
      
 189 
     | 
    
         
            +
                # Return a new Step with added metadata, or build step metadata if no argument is provided.
         
     | 
| 
      
 190 
     | 
    
         
            +
                # @example
         
     | 
| 
      
 191 
     | 
    
         
            +
                #   type = Types::String.metadata(label: 'Name')
         
     | 
| 
      
 192 
     | 
    
         
            +
                #   type.metadata # => { type: String, label: 'Name' }
         
     | 
| 
      
 193 
     | 
    
         
            +
                #
         
     | 
| 
      
 194 
     | 
    
         
            +
                # @param data [Hash] metadata to add to the step
         
     | 
| 
      
 195 
     | 
    
         
            +
                # @return [Hash, And]
         
     | 
| 
       131 
196 
     | 
    
         
             
                def metadata(data = Undefined)
         
     | 
| 
       132 
197 
     | 
    
         
             
                  if data == Undefined
         
     | 
| 
       133 
198 
     | 
    
         
             
                    MetadataVisitor.call(self)
         
     | 
| 
         @@ -136,24 +201,54 @@ module Plumb 
     | 
|
| 
       136 
201 
     | 
    
         
             
                  end
         
     | 
| 
       137 
202 
     | 
    
         
             
                end
         
     | 
| 
       138 
203 
     | 
    
         | 
| 
      
 204 
     | 
    
         
            +
                # Negate the result of a step.
         
     | 
| 
      
 205 
     | 
    
         
            +
                # Ie. if the step is valid, it will be invalid, and vice versa.
         
     | 
| 
      
 206 
     | 
    
         
            +
                # @example
         
     | 
| 
      
 207 
     | 
    
         
            +
                #   type = Types::String.not
         
     | 
| 
      
 208 
     | 
    
         
            +
                #   type.resolve('foo') # invalid
         
     | 
| 
      
 209 
     | 
    
         
            +
                #   type.resolve(10) # valid
         
     | 
| 
      
 210 
     | 
    
         
            +
                #
         
     | 
| 
      
 211 
     | 
    
         
            +
                # @return [Not]
         
     | 
| 
       139 
212 
     | 
    
         
             
                def not(other = self)
         
     | 
| 
       140 
213 
     | 
    
         
             
                  Not.new(other)
         
     | 
| 
       141 
214 
     | 
    
         
             
                end
         
     | 
| 
       142 
215 
     | 
    
         | 
| 
      
 216 
     | 
    
         
            +
                # Like #not, but with a custom error message.
         
     | 
| 
      
 217 
     | 
    
         
            +
                #
         
     | 
| 
      
 218 
     | 
    
         
            +
                # @option errors [String] error message to use when validation fails
         
     | 
| 
      
 219 
     | 
    
         
            +
                # @return [Not]
         
     | 
| 
       143 
220 
     | 
    
         
             
                def invalid(errors: nil)
         
     | 
| 
       144 
221 
     | 
    
         
             
                  Not.new(self, errors:)
         
     | 
| 
       145 
222 
     | 
    
         
             
                end
         
     | 
| 
       146 
223 
     | 
    
         | 
| 
      
 224 
     | 
    
         
            +
                #  Match a value using `#==`
         
     | 
| 
      
 225 
     | 
    
         
            +
                # Normally you'll build matchers via ``#[]`, which uses `#===`.
         
     | 
| 
      
 226 
     | 
    
         
            +
                # Use this if you want to match against concrete instances of things that respond to `#===`
         
     | 
| 
      
 227 
     | 
    
         
            +
                # @example
         
     | 
| 
      
 228 
     | 
    
         
            +
                #   regex = Types::Any.value(/foo/)
         
     | 
| 
      
 229 
     | 
    
         
            +
                #   regex.resolve('foo') # invalid. We're matching against the regex itself.
         
     | 
| 
      
 230 
     | 
    
         
            +
                #   regex.resolve(/foo/) # valid
         
     | 
| 
      
 231 
     | 
    
         
            +
                #
         
     | 
| 
      
 232 
     | 
    
         
            +
                # @param value [Object]
         
     | 
| 
      
 233 
     | 
    
         
            +
                # @rerurn [And]
         
     | 
| 
       147 
234 
     | 
    
         
             
                def value(val)
         
     | 
| 
       148 
235 
     | 
    
         
             
                  self >> ValueClass.new(val)
         
     | 
| 
       149 
236 
     | 
    
         
             
                end
         
     | 
| 
       150 
237 
     | 
    
         | 
| 
      
 238 
     | 
    
         
            +
                # Alias of `#[]`
         
     | 
| 
      
 239 
     | 
    
         
            +
                # Match a value using `#===`
         
     | 
| 
      
 240 
     | 
    
         
            +
                # @example
         
     | 
| 
      
 241 
     | 
    
         
            +
                #   email = Types::String['@']
         
     | 
| 
      
 242 
     | 
    
         
            +
                #
         
     | 
| 
      
 243 
     | 
    
         
            +
                # @param args [Array<Object>]
         
     | 
| 
      
 244 
     | 
    
         
            +
                # @return [And]
         
     | 
| 
       151 
245 
     | 
    
         
             
                def match(*args)
         
     | 
| 
       152 
246 
     | 
    
         
             
                  self >> MatchClass.new(*args)
         
     | 
| 
       153 
247 
     | 
    
         
             
                end
         
     | 
| 
       154 
248 
     | 
    
         | 
| 
       155 
249 
     | 
    
         
             
                def [](val) = match(val)
         
     | 
| 
       156 
250 
     | 
    
         | 
| 
      
 251 
     | 
    
         
            +
                #  Support #as_node.
         
     | 
| 
       157 
252 
     | 
    
         
             
                class Node
         
     | 
| 
       158 
253 
     | 
    
         
             
                  include Composable
         
     | 
| 
       159 
254 
     | 
    
         | 
| 
         @@ -169,6 +264,14 @@ module Plumb 
     | 
|
| 
       169 
264 
     | 
    
         
             
                  def call(result) = type.call(result)
         
     | 
| 
       170 
265 
     | 
    
         
             
                end
         
     | 
| 
       171 
266 
     | 
    
         | 
| 
      
 267 
     | 
    
         
            +
                #  Wrap a Step in a node with a custom #node_name
         
     | 
| 
      
 268 
     | 
    
         
            +
                # which is expected by visitors.
         
     | 
| 
      
 269 
     | 
    
         
            +
                # So that we can define special visitors for certain compositions.
         
     | 
| 
      
 270 
     | 
    
         
            +
                # Ex. Types::Boolean is a compoition of Types::True | Types::False, but we want to treat it as a single node.
         
     | 
| 
      
 271 
     | 
    
         
            +
                #
         
     | 
| 
      
 272 
     | 
    
         
            +
                # @param node_name [Symbol]
         
     | 
| 
      
 273 
     | 
    
         
            +
                # @param metadata [Hash]
         
     | 
| 
      
 274 
     | 
    
         
            +
                # @return [Node]
         
     | 
| 
       172 
275 
     | 
    
         
             
                def as_node(node_name, metadata = BLANK_HASH)
         
     | 
| 
       173 
276 
     | 
    
         
             
                  Node.new(node_name, self, metadata)
         
     | 
| 
       174 
277 
     | 
    
         
             
                end
         
     | 
| 
         @@ -186,7 +289,7 @@ module Plumb 
     | 
|
| 
       186 
289 
     | 
    
         | 
| 
       187 
290 
     | 
    
         
             
                    bargs = [self]
         
     | 
| 
       188 
291 
     | 
    
         
             
                    arg = Undefined
         
     | 
| 
       189 
     | 
    
         
            -
                    if rest. 
     | 
| 
      
 292 
     | 
    
         
            +
                    if rest.size.positive?
         
     | 
| 
       190 
293 
     | 
    
         
             
                      bargs << rest.first
         
     | 
| 
       191 
294 
     | 
    
         
             
                      arg = rest.first
         
     | 
| 
       192 
295 
     | 
    
         
             
                    end
         
     | 
| 
         @@ -201,6 +304,9 @@ module Plumb 
     | 
|
| 
       201 
304 
     | 
    
         
             
                  end
         
     | 
| 
       202 
305 
     | 
    
         
             
                end
         
     | 
| 
       203 
306 
     | 
    
         | 
| 
      
 307 
     | 
    
         
            +
                # `#===` equality. So that Plumb steps can be used in case statements and pattern matching.
         
     | 
| 
      
 308 
     | 
    
         
            +
                # @param other [Object]
         
     | 
| 
      
 309 
     | 
    
         
            +
                # @return [Boolean]
         
     | 
| 
       204 
310 
     | 
    
         
             
                def ===(other)
         
     | 
| 
       205 
311 
     | 
    
         
             
                  case other
         
     | 
| 
       206 
312 
     | 
    
         
             
                  when Composable
         
     | 
| 
         @@ -211,15 +317,39 @@ module Plumb 
     | 
|
| 
       211 
317 
     | 
    
         
             
                end
         
     | 
| 
       212 
318 
     | 
    
         | 
| 
       213 
319 
     | 
    
         
             
                def ==(other)
         
     | 
| 
       214 
     | 
    
         
            -
                  other.is_a?(self.class) && other.children == children
         
     | 
| 
      
 320 
     | 
    
         
            +
                  other.is_a?(self.class) && other.respond_to?(:children) && other.children == children
         
     | 
| 
       215 
321 
     | 
    
         
             
                end
         
     | 
| 
       216 
322 
     | 
    
         | 
| 
      
 323 
     | 
    
         
            +
                # Visitors expect a #node_name and #children interface.
         
     | 
| 
      
 324 
     | 
    
         
            +
                # @return [Array<Composable>]
         
     | 
| 
       217 
325 
     | 
    
         
             
                def children = BLANK_ARRAY
         
     | 
| 
       218 
326 
     | 
    
         | 
| 
      
 327 
     | 
    
         
            +
                # Compose a step that instantiates a class.
         
     | 
| 
      
 328 
     | 
    
         
            +
                # @example
         
     | 
| 
      
 329 
     | 
    
         
            +
                #   type = Types::String.build(MyClass, :new)
         
     | 
| 
      
 330 
     | 
    
         
            +
                #   thing = type.parse('foo') # same as MyClass.new('foo')
         
     | 
| 
      
 331 
     | 
    
         
            +
                #
         
     | 
| 
      
 332 
     | 
    
         
            +
                # It sets the class as the output type of the step.
         
     | 
| 
      
 333 
     | 
    
         
            +
                # Optionally takes a block.
         
     | 
| 
      
 334 
     | 
    
         
            +
                #
         
     | 
| 
      
 335 
     | 
    
         
            +
                #   type = Types::String.build(Money) { |value| Monetize.parse(value) }
         
     | 
| 
      
 336 
     | 
    
         
            +
                #
         
     | 
| 
      
 337 
     | 
    
         
            +
                # @param cns [Class] constructor class or object.
         
     | 
| 
      
 338 
     | 
    
         
            +
                # @param factory_method [Symbol] method to call on the class to instantiate it.
         
     | 
| 
      
 339 
     | 
    
         
            +
                # @return [And]
         
     | 
| 
       219 
340 
     | 
    
         
             
                def build(cns, factory_method = :new, &block)
         
     | 
| 
       220 
341 
     | 
    
         
             
                  self >> Build.new(cns, factory_method:, &block)
         
     | 
| 
       221 
342 
     | 
    
         
             
                end
         
     | 
| 
       222 
343 
     | 
    
         | 
| 
      
 344 
     | 
    
         
            +
                # Build a Plumb::Pipeline with this object as the starting step.
         
     | 
| 
      
 345 
     | 
    
         
            +
                # @example
         
     | 
| 
      
 346 
     | 
    
         
            +
                #   pipe = Types::Data[name: String].pipeline do |pl|
         
     | 
| 
      
 347 
     | 
    
         
            +
                #     pl.step Validate
         
     | 
| 
      
 348 
     | 
    
         
            +
                #     pl.step Debug
         
     | 
| 
      
 349 
     | 
    
         
            +
                #     pl.step Log
         
     | 
| 
      
 350 
     | 
    
         
            +
                # end
         
     | 
| 
      
 351 
     | 
    
         
            +
                #
         
     | 
| 
      
 352 
     | 
    
         
            +
                # @return [Pipeline]
         
     | 
| 
       223 
353 
     | 
    
         
             
                def pipeline(&block)
         
     | 
| 
       224 
354 
     | 
    
         
             
                  Pipeline.new(self, &block)
         
     | 
| 
       225 
355 
     | 
    
         
             
                end
         
     | 
    
        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 
     | 
    
         | 
| 
         @@ -24,6 +24,7 @@ module Plumb 
     | 
|
| 
       24 
24 
     | 
    
         
             
                MAX_ITEMS = 'maxItems'
         
     | 
| 
       25 
25 
     | 
    
         
             
                MIN_LENGTH = 'minLength'
         
     | 
| 
       26 
26 
     | 
    
         
             
                MAX_LENGTH = 'maxLength'
         
     | 
| 
      
 27 
     | 
    
         
            +
                FORMAT = 'format'
         
     | 
| 
       27 
28 
     | 
    
         
             
                ENVELOPE = {
         
     | 
| 
       28 
29 
     | 
    
         
             
                  '$schema' => 'https://json-schema.org/draft-08/schema#'
         
     | 
| 
       29 
30 
     | 
    
         
             
                }.freeze
         
     | 
| 
         @@ -192,6 +193,14 @@ module Plumb 
     | 
|
| 
       192 
193 
     | 
    
         
             
                  props.merge(TYPE => 'boolean')
         
     | 
| 
       193 
194 
     | 
    
         
             
                end
         
     | 
| 
       194 
195 
     | 
    
         | 
| 
      
 196 
     | 
    
         
            +
                on(:uuid) do |_node, props|
         
     | 
| 
      
 197 
     | 
    
         
            +
                  props.merge(TYPE => 'string', FORMAT => 'uuid')
         
     | 
| 
      
 198 
     | 
    
         
            +
                end
         
     | 
| 
      
 199 
     | 
    
         
            +
             
     | 
| 
      
 200 
     | 
    
         
            +
                on(:email) do |_node, props|
         
     | 
| 
      
 201 
     | 
    
         
            +
                  props.merge(TYPE => 'string', FORMAT => 'email')
         
     | 
| 
      
 202 
     | 
    
         
            +
                end
         
     | 
| 
      
 203 
     | 
    
         
            +
             
     | 
| 
       195 
204 
     | 
    
         
             
                on(::String) do |_node, props|
         
     | 
| 
       196 
205 
     | 
    
         
             
                  props.merge(TYPE => 'string')
         
     | 
| 
       197 
206 
     | 
    
         
             
                end
         
     | 
| 
         @@ -239,11 +248,23 @@ module Plumb 
     | 
|
| 
       239 
248 
     | 
    
         
             
                end
         
     | 
| 
       240 
249 
     | 
    
         | 
| 
       241 
250 
     | 
    
         
             
                on(::Time) do |_node, props|
         
     | 
| 
       242 
     | 
    
         
            -
                  props.merge(TYPE => 'string',  
     | 
| 
      
 251 
     | 
    
         
            +
                  props.merge(TYPE => 'string', FORMAT => 'date-time')
         
     | 
| 
       243 
252 
     | 
    
         
             
                end
         
     | 
| 
       244 
253 
     | 
    
         | 
| 
       245 
254 
     | 
    
         
             
                on(::Date) do |_node, props|
         
     | 
| 
       246 
     | 
    
         
            -
                  props.merge(TYPE => 'string',  
     | 
| 
      
 255 
     | 
    
         
            +
                  props.merge(TYPE => 'string', FORMAT => 'date')
         
     | 
| 
      
 256 
     | 
    
         
            +
                end
         
     | 
| 
      
 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')
         
     | 
| 
       247 
268 
     | 
    
         
             
                end
         
     | 
| 
       248 
269 
     | 
    
         | 
| 
       249 
270 
     | 
    
         
             
                on(::Hash) do |_node, props|
         
     | 
    
        data/lib/plumb/match_class.rb
    CHANGED
    
    | 
         @@ -9,7 +9,7 @@ module Plumb 
     | 
|
| 
       9 
9 
     | 
    
         
             
                attr_reader :children
         
     | 
| 
       10 
10 
     | 
    
         | 
| 
       11 
11 
     | 
    
         
             
                def initialize(matcher = Undefined, error: nil, label: nil)
         
     | 
| 
       12 
     | 
    
         
            -
                  raise  
     | 
| 
      
 12 
     | 
    
         
            +
                  raise ParseError 'matcher must respond to #===' unless matcher.respond_to?(:===)
         
     | 
| 
       13 
13 
     | 
    
         | 
| 
       14 
14 
     | 
    
         
             
                  @matcher = matcher
         
     | 
| 
       15 
15 
     | 
    
         
             
                  @error = error.nil? ? build_error(matcher) : (error % matcher)
         
     | 
    
        data/lib/plumb/pipeline.rb
    CHANGED
    
    | 
         @@ -19,12 +19,28 @@ module Plumb 
     | 
|
| 
       19 
19 
     | 
    
         
             
                  end
         
     | 
| 
       20 
20 
     | 
    
         
             
                end
         
     | 
| 
       21 
21 
     | 
    
         | 
| 
      
 22 
     | 
    
         
            +
                class << self
         
     | 
| 
      
 23 
     | 
    
         
            +
                  def around_blocks
         
     | 
| 
      
 24 
     | 
    
         
            +
                    @around_blocks ||= []
         
     | 
| 
      
 25 
     | 
    
         
            +
                  end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                  def around(callable = nil, &block)
         
     | 
| 
      
 28 
     | 
    
         
            +
                    around_blocks << (callable || block)
         
     | 
| 
      
 29 
     | 
    
         
            +
                    self
         
     | 
| 
      
 30 
     | 
    
         
            +
                  end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                  def inherited(subclass)
         
     | 
| 
      
 33 
     | 
    
         
            +
                    around_blocks.each { |block| subclass.around(block) }
         
     | 
| 
      
 34 
     | 
    
         
            +
                    super
         
     | 
| 
      
 35 
     | 
    
         
            +
                  end
         
     | 
| 
      
 36 
     | 
    
         
            +
                end
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
       22 
38 
     | 
    
         
             
                attr_reader :children
         
     | 
| 
       23 
39 
     | 
    
         | 
| 
       24 
40 
     | 
    
         
             
                def initialize(type = Types::Any, &setup)
         
     | 
| 
       25 
41 
     | 
    
         
             
                  @type = type
         
     | 
| 
       26 
42 
     | 
    
         
             
                  @children = [type].freeze
         
     | 
| 
       27 
     | 
    
         
            -
                  @around_blocks =  
     | 
| 
      
 43 
     | 
    
         
            +
                  @around_blocks = self.class.around_blocks.dup
         
     | 
| 
       28 
44 
     | 
    
         
             
                  return unless block_given?
         
     | 
| 
       29 
45 
     | 
    
         | 
| 
       30 
46 
     | 
    
         
             
                  configure(&setup)
         
     | 
| 
         @@ -42,7 +58,8 @@ module Plumb 
     | 
|
| 
       42 
58 
     | 
    
         
             
                          "#step expects an interface #call(Result) Result, but got #{callable.inspect}"
         
     | 
| 
       43 
59 
     | 
    
         
             
                  end
         
     | 
| 
       44 
60 
     | 
    
         | 
| 
       45 
     | 
    
         
            -
                  callable =  
     | 
| 
      
 61 
     | 
    
         
            +
                  callable = prepare_step(callable)
         
     | 
| 
      
 62 
     | 
    
         
            +
                  callable = @around_blocks.reverse.reduce(callable) { |cl, bl| AroundStep.new(cl, bl) } if @around_blocks.any?
         
     | 
| 
       46 
63 
     | 
    
         
             
                  @type >>= callable
         
     | 
| 
       47 
64 
     | 
    
         
             
                  self
         
     | 
| 
       48 
65 
     | 
    
         
             
                end
         
     | 
| 
         @@ -70,5 +87,7 @@ module Plumb 
     | 
|
| 
       70 
87 
     | 
    
         | 
| 
       71 
88 
     | 
    
         
             
                  true
         
     | 
| 
       72 
89 
     | 
    
         
             
                end
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
                def prepare_step(callable) = callable
         
     | 
| 
       73 
92 
     | 
    
         
             
              end
         
     | 
| 
       74 
93 
     | 
    
         
             
            end
         
     | 
    
        data/lib/plumb/tagged_hash.rb
    CHANGED
    
    | 
         @@ -21,7 +21,7 @@ module Plumb 
     | 
|
| 
       21 
21 
     | 
    
         
             
                  # types are assumed to have literal values for the index field :key
         
     | 
| 
       22 
22 
     | 
    
         
             
                  @index = @children.each.with_object({}) do |t, memo|
         
     | 
| 
       23 
23 
     | 
    
         
             
                    key_type = t.at_key(@key)
         
     | 
| 
       24 
     | 
    
         
            -
                    raise  
     | 
| 
      
 24 
     | 
    
         
            +
                    raise ParseError, "key type at :#{@key} #{key_type} must be a Match type" unless key_type.is_a?(MatchClass)
         
     | 
| 
       25 
25 
     | 
    
         | 
| 
       26 
26 
     | 
    
         
             
                    memo[key_type.children[0]] = t
         
     | 
| 
       27 
27 
     | 
    
         
             
                  end
         
     | 
    
        data/lib/plumb/types.rb
    CHANGED
    
    | 
         @@ -1,6 +1,9 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # frozen_string_literal: true
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
3 
     | 
    
         
             
            require 'bigdecimal'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'uri'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require 'date'
         
     | 
| 
      
 6 
     | 
    
         
            +
            require 'time'
         
     | 
| 
       4 
7 
     | 
    
         | 
| 
       5 
8 
     | 
    
         
             
            module Plumb
         
     | 
| 
       6 
9 
     | 
    
         
             
              # Define core policies
         
     | 
| 
         @@ -104,6 +107,18 @@ module Plumb 
     | 
|
| 
       104 
107 
     | 
    
         
             
                (Types::Undefined >> val_type) | type
         
     | 
| 
       105 
108 
     | 
    
         
             
              end
         
     | 
| 
       106 
109 
     | 
    
         | 
| 
      
 110 
     | 
    
         
            +
              # Wrap a step execution in a rescue block.
         
     | 
| 
      
 111 
     | 
    
         
            +
              # Expect a specific exception class, and return an invalid result if it is raised.
         
     | 
| 
      
 112 
     | 
    
         
            +
              # Usage:
         
     | 
| 
      
 113 
     | 
    
         
            +
              #   type = Types::String.build(Date, :parse).policy(:rescue, Date::Error)
         
     | 
| 
      
 114 
     | 
    
         
            +
              policy :rescue do |type, exception_class|
         
     | 
| 
      
 115 
     | 
    
         
            +
                Step.new(nil, 'Rescue') do |result|
         
     | 
| 
      
 116 
     | 
    
         
            +
                  type.call(result)
         
     | 
| 
      
 117 
     | 
    
         
            +
                rescue exception_class => e
         
     | 
| 
      
 118 
     | 
    
         
            +
                  result.invalid(errors: e.message)
         
     | 
| 
      
 119 
     | 
    
         
            +
                end
         
     | 
| 
      
 120 
     | 
    
         
            +
              end
         
     | 
| 
      
 121 
     | 
    
         
            +
             
     | 
| 
       107 
122 
     | 
    
         
             
              # Split a string into an array. Default separator is /\s*,\s*/
         
     | 
| 
       108 
123 
     | 
    
         
             
              # Usage:
         
     | 
| 
       109 
124 
     | 
    
         
             
              #   type = Types::String.split
         
     | 
| 
         @@ -145,6 +160,19 @@ module Plumb 
     | 
|
| 
       145 
160 
     | 
    
         
             
                Tuple = TupleClass.new
         
     | 
| 
       146 
161 
     | 
    
         
             
                Hash = HashClass.new
         
     | 
| 
       147 
162 
     | 
    
         
             
                Interface = InterfaceClass.new
         
     | 
| 
      
 163 
     | 
    
         
            +
                Email = String[URI::MailTo::EMAIL_REGEXP].as_node(:email)
         
     | 
| 
      
 164 
     | 
    
         
            +
                Date = Any[::Date]
         
     | 
| 
      
 165 
     | 
    
         
            +
                Time = Any[::Time]
         
     | 
| 
      
 166 
     | 
    
         
            +
             
     | 
| 
      
 167 
     | 
    
         
            +
                module UUID
         
     | 
| 
      
 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)
         
     | 
| 
      
 169 
     | 
    
         
            +
                end
         
     | 
| 
      
 170 
     | 
    
         
            +
             
     | 
| 
      
 171 
     | 
    
         
            +
                module URI
         
     | 
| 
      
 172 
     | 
    
         
            +
                  Generic = Any[::URI::Generic]
         
     | 
| 
      
 173 
     | 
    
         
            +
                  HTTP = Any[::URI::HTTP]
         
     | 
| 
      
 174 
     | 
    
         
            +
                  File = Any[::URI::File]
         
     | 
| 
      
 175 
     | 
    
         
            +
                end
         
     | 
| 
       148 
176 
     | 
    
         | 
| 
       149 
177 
     | 
    
         
             
                class Data
         
     | 
| 
       150 
178 
     | 
    
         
             
                  extend Composable
         
     | 
| 
         @@ -190,6 +218,20 @@ module Plumb 
     | 
|
| 
       190 
218 
     | 
    
         
             
                  Boolean = True | False
         
     | 
| 
       191 
219 
     | 
    
         | 
| 
       192 
220 
     | 
    
         
             
                  Nil = Nil | (String[BLANK_STRING] >> nil)
         
     | 
| 
      
 221 
     | 
    
         
            +
             
     | 
| 
      
 222 
     | 
    
         
            +
                  # Accept a Date, or a string that can be parsed into a Date
         
     | 
| 
      
 223 
     | 
    
         
            +
                  # via Date.parse
         
     | 
| 
      
 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
         
     | 
| 
       193 
235 
     | 
    
         
             
                end
         
     | 
| 
       194 
236 
     | 
    
         
             
              end
         
     | 
| 
       195 
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
         
     | 
| 
         @@ -50,6 +50,10 @@ files: 
     | 
|
| 
       50 
50 
     | 
    
         
             
            - LICENSE.txt
         
     | 
| 
       51 
51 
     | 
    
         
             
            - README.md
         
     | 
| 
       52 
52 
     | 
    
         
             
            - Rakefile
         
     | 
| 
      
 53 
     | 
    
         
            +
            - bench/compare_parametric_schema.rb
         
     | 
| 
      
 54 
     | 
    
         
            +
            - bench/compare_parametric_struct.rb
         
     | 
| 
      
 55 
     | 
    
         
            +
            - bench/parametric_schema.rb
         
     | 
| 
      
 56 
     | 
    
         
            +
            - bench/plumb_hash.rb
         
     | 
| 
       53 
57 
     | 
    
         
             
            - examples/command_objects.rb
         
     | 
| 
       54 
58 
     | 
    
         
             
            - examples/concurrent_downloads.rb
         
     | 
| 
       55 
59 
     | 
    
         
             
            - examples/csv_stream.rb
         
     |