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
 
    
        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
    
    | 
         @@ -10,6 +10,8 @@ If you're after raw performance and versatility I strongly recommend you use the 
     | 
|
| 
       10 
10 
     | 
    
         | 
| 
       11 
11 
     | 
    
         
             
            For a description of the core architecture you can read [this article](https://ismaelcelis.com/posts/composable-pipelines-in-ruby/).
         
     | 
| 
       12 
12 
     | 
    
         | 
| 
      
 13 
     | 
    
         
            +
            Some use cases in the [examples directory](https://github.com/ismasan/plumb/tree/main/examples)
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
       13 
15 
     | 
    
         
             
            ## Installation
         
     | 
| 
       14 
16 
     | 
    
         | 
| 
       15 
17 
     | 
    
         
             
            Install in your environment with `gem install plumb`, or in your `Gemfile` with
         
     | 
| 
         @@ -58,7 +60,7 @@ module Types 
     | 
|
| 
       58 
60 
     | 
    
         
             
            end
         
     | 
| 
       59 
61 
     | 
    
         | 
| 
       60 
62 
     | 
    
         
             
            Types::String.parse("hello") # => "hello"
         
     | 
| 
       61 
     | 
    
         
            -
            Types::String.parse(10) # raises "Must be a String" (Plumb:: 
     | 
| 
      
 63 
     | 
    
         
            +
            Types::String.parse(10) # raises "Must be a String" (Plumb::ParseError)
         
     | 
| 
       62 
64 
     | 
    
         
             
            ```
         
     | 
| 
       63 
65 
     | 
    
         | 
| 
       64 
66 
     | 
    
         
             
            Plumb ships with basic types already defined, such as `Types::String` and `Types::Integer`. See the full list below.
         
     | 
| 
         @@ -75,7 +77,7 @@ Email.parse('hello@server.com') # 'hello@server.com' 
     | 
|
| 
       75 
77 
     | 
    
         
             
            # Or a Range
         
     | 
| 
       76 
78 
     | 
    
         
             
            AdultAge = Types::Integer[18..]
         
     | 
| 
       77 
79 
     | 
    
         
             
            AdultAge.parse(20) # 20
         
     | 
| 
       78 
     | 
    
         
            -
            AdultAge.parse(17) # raises "Must be within 18.."" (Plumb:: 
     | 
| 
      
 80 
     | 
    
         
            +
            AdultAge.parse(17) # raises "Must be within 18.."" (Plumb::ParseError)
         
     | 
| 
       79 
81 
     | 
    
         | 
| 
       80 
82 
     | 
    
         
             
            # Or literal values
         
     | 
| 
       81 
83 
     | 
    
         
             
            Twenty = Types::Integer[20]
         
     | 
| 
         @@ -113,7 +115,7 @@ result.errors # 'must be an Integer' 
     | 
|
| 
       113 
115 
     | 
    
         | 
| 
       114 
116 
     | 
    
         
             
            ```ruby
         
     | 
| 
       115 
117 
     | 
    
         
             
            Types::Integer.parse(10) # 10
         
     | 
| 
       116 
     | 
    
         
            -
            Types::Integer.parse('10') # raises Plumb:: 
     | 
| 
      
 118 
     | 
    
         
            +
            Types::Integer.parse('10') # raises Plumb::ParseError
         
     | 
| 
       117 
119 
     | 
    
         
             
            ```
         
     | 
| 
       118 
120 
     | 
    
         | 
| 
       119 
121 
     | 
    
         | 
| 
         @@ -133,7 +135,7 @@ joe = User.parse({ name: 'Joe', email: 'joe@email.com', age: 20}) # returns vali 
     | 
|
| 
       133 
135 
     | 
    
         
             
            Users.parse([joe]) # returns valid array of user hashes
         
     | 
| 
       134 
136 
     | 
    
         
             
            ```
         
     | 
| 
       135 
137 
     | 
    
         | 
| 
       136 
     | 
    
         
            -
            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.
         
     | 
| 
       137 
139 
     | 
    
         | 
| 
       138 
140 
     | 
    
         
             
            ### Type composition
         
     | 
| 
       139 
141 
     | 
    
         | 
| 
         @@ -161,7 +163,7 @@ In other words, `A >> B` means "if A succeeds, pass its result to B. Otherwise r 
     | 
|
| 
       161 
163 
     | 
    
         
             
            StringOrInt = Types::String | Types::Integer
         
     | 
| 
       162 
164 
     | 
    
         
             
            StringOrInt.parse('hello') # "hello"
         
     | 
| 
       163 
165 
     | 
    
         
             
            StringOrInt.parse(10) # 10
         
     | 
| 
       164 
     | 
    
         
            -
            StringOrInt.parse({}) # raises Plumb:: 
     | 
| 
      
 166 
     | 
    
         
            +
            StringOrInt.parse({}) # raises Plumb::ParseError
         
     | 
| 
       165 
167 
     | 
    
         
             
            ```
         
     | 
| 
       166 
168 
     | 
    
         | 
| 
       167 
169 
     | 
    
         
             
            Custom default value logic for non-emails
         
     | 
| 
         @@ -230,6 +232,13 @@ You can see more use cases in [the examples directory](https://github.com/ismasa 
     | 
|
| 
       230 
232 
     | 
    
         
             
            * `Types::Numeric`
         
     | 
| 
       231 
233 
     | 
    
         
             
            * `Types::String`
         
     | 
| 
       232 
234 
     | 
    
         
             
            * `Types::Hash`
         
     | 
| 
      
 235 
     | 
    
         
            +
            * `Types::UUID::V4`
         
     | 
| 
      
 236 
     | 
    
         
            +
            * `Types::Email`
         
     | 
| 
      
 237 
     | 
    
         
            +
            * `Types::Date`
         
     | 
| 
      
 238 
     | 
    
         
            +
            * `Types::Time`
         
     | 
| 
      
 239 
     | 
    
         
            +
            * `Types::URI::Generic`
         
     | 
| 
      
 240 
     | 
    
         
            +
            * `Types::URI::HTTP`
         
     | 
| 
      
 241 
     | 
    
         
            +
            * `Types::URI::File`
         
     | 
| 
       233 
242 
     | 
    
         
             
            * `Types::Lax::Integer`
         
     | 
| 
       234 
243 
     | 
    
         
             
            * `Types::Lax::String`
         
     | 
| 
       235 
244 
     | 
    
         
             
            * `Types::Lax::Symbol`
         
     | 
| 
         @@ -237,8 +246,13 @@ You can see more use cases in [the examples directory](https://github.com/ismasa 
     | 
|
| 
       237 
246 
     | 
    
         
             
            * `Types::Forms::Nil`
         
     | 
| 
       238 
247 
     | 
    
         
             
            * `Types::Forms::True`
         
     | 
| 
       239 
248 
     | 
    
         
             
            * `Types::Forms::False`
         
     | 
| 
      
 249 
     | 
    
         
            +
            * `Types::Forms::Date`
         
     | 
| 
      
 250 
     | 
    
         
            +
            * `Types::Forms::Time`
         
     | 
| 
      
 251 
     | 
    
         
            +
            * `Types::Forms::URI::Generic`
         
     | 
| 
      
 252 
     | 
    
         
            +
            * `Types::Forms::URI::HTTP`
         
     | 
| 
      
 253 
     | 
    
         
            +
            * `Types::Forms::URI::File`
         
     | 
| 
       240 
254 
     | 
    
         | 
| 
       241 
     | 
    
         
            -
            TODO:  
     | 
| 
      
 255 
     | 
    
         
            +
            TODO: datetime, others.
         
     | 
| 
       242 
256 
     | 
    
         | 
| 
       243 
257 
     | 
    
         
             
            ### Policies
         
     | 
| 
       244 
258 
     | 
    
         | 
| 
         @@ -261,7 +275,7 @@ Allow `nil` values. 
     | 
|
| 
       261 
275 
     | 
    
         
             
            nullable_str = Types::String.nullable
         
     | 
| 
       262 
276 
     | 
    
         
             
            nullable_srt.parse(nil) # nil
         
     | 
| 
       263 
277 
     | 
    
         
             
            nullable_str.parse('hello') # 'hello'
         
     | 
| 
       264 
     | 
    
         
            -
            nullable_str.parse(10) #  
     | 
| 
      
 278 
     | 
    
         
            +
            nullable_str.parse(10) # ParseError
         
     | 
| 
       265 
279 
     | 
    
         
             
            ```
         
     | 
| 
       266 
280 
     | 
    
         | 
| 
       267 
281 
     | 
    
         
             
            Note that this just encapsulates the following composition:
         
     | 
| 
         @@ -522,7 +536,52 @@ CSVLine = Types::String.split(/\s*;\s*/) 
     | 
|
| 
       522 
536 
     | 
    
         
             
            CSVLine.parse('a;b;c') # => ['a', 'b', 'c']
         
     | 
| 
       523 
537 
     | 
    
         
             
            ```
         
     | 
| 
       524 
538 
     | 
    
         | 
| 
      
 539 
     | 
    
         
            +
            #### `:rescue`
         
     | 
| 
      
 540 
     | 
    
         
            +
             
     | 
| 
      
 541 
     | 
    
         
            +
            Wraps a step's execution, rescues a specific exception and returns an invalid result.
         
     | 
| 
      
 542 
     | 
    
         
            +
             
     | 
| 
      
 543 
     | 
    
         
            +
            Useful for turning a 3rd party library's exception into an invalid result that plays well with Plumb's type compositions.
         
     | 
| 
      
 544 
     | 
    
         
            +
             
     | 
| 
      
 545 
     | 
    
         
            +
            Example: this is how `Types::Forms::Date` uses the `:rescue` policy to parse strings with `Date.parse` and turn `Date::Error` exceptions into Plumb errors.
         
     | 
| 
      
 546 
     | 
    
         
            +
             
     | 
| 
      
 547 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 548 
     | 
    
         
            +
            # Accept a string that can be parsed into a Date
         
     | 
| 
      
 549 
     | 
    
         
            +
            # via Date.parse
         
     | 
| 
      
 550 
     | 
    
         
            +
            # If Date.parse raises a Date::Error, return a Result::Invalid with
         
     | 
| 
      
 551 
     | 
    
         
            +
            # the exception's message as error message.
         
     | 
| 
      
 552 
     | 
    
         
            +
            type = Types::String
         
     | 
| 
      
 553 
     | 
    
         
            +
            	.build(::Date, :parse)
         
     | 
| 
      
 554 
     | 
    
         
            +
            	.policy(:rescue, ::Date::Error)
         
     | 
| 
      
 555 
     | 
    
         
            +
             
     | 
| 
      
 556 
     | 
    
         
            +
            type.resolve('2024-02-02') # => Result::Valid with Date object
         
     | 
| 
      
 557 
     | 
    
         
            +
            type.resolve('2024-') # => Result::Invalid with error message
         
     | 
| 
      
 558 
     | 
    
         
            +
            ```
         
     | 
| 
      
 559 
     | 
    
         
            +
             
     | 
| 
      
 560 
     | 
    
         
            +
            ### `Types::Interface`
         
     | 
| 
       525 
561 
     | 
    
         | 
| 
      
 562 
     | 
    
         
            +
            Use this for objects that must respond to one or more methods.
         
     | 
| 
      
 563 
     | 
    
         
            +
             
     | 
| 
      
 564 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 565 
     | 
    
         
            +
            Iterable = Types::Interface[:each, :map]
         
     | 
| 
      
 566 
     | 
    
         
            +
            Iterable.parse([1,2,3]) # => [1,2,3]
         
     | 
| 
      
 567 
     | 
    
         
            +
            Iterable.parse(10) # => raises error
         
     | 
| 
      
 568 
     | 
    
         
            +
            ```
         
     | 
| 
      
 569 
     | 
    
         
            +
             
     | 
| 
      
 570 
     | 
    
         
            +
            This can be useful combined with `case` statements, too:
         
     | 
| 
      
 571 
     | 
    
         
            +
             
     | 
| 
      
 572 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 573 
     | 
    
         
            +
            value = [1,2,3]
         
     | 
| 
      
 574 
     | 
    
         
            +
            case value
         
     | 
| 
      
 575 
     | 
    
         
            +
            when Iterable
         
     | 
| 
      
 576 
     | 
    
         
            +
              # do something with array
         
     | 
| 
      
 577 
     | 
    
         
            +
            when Stringable
         
     | 
| 
      
 578 
     | 
    
         
            +
              # do something with string
         
     | 
| 
      
 579 
     | 
    
         
            +
            when Readable
         
     | 
| 
      
 580 
     | 
    
         
            +
              # do something with IO or similar
         
     | 
| 
      
 581 
     | 
    
         
            +
            end
         
     | 
| 
      
 582 
     | 
    
         
            +
            ```
         
     | 
| 
      
 583 
     | 
    
         
            +
             
     | 
| 
      
 584 
     | 
    
         
            +
            TODO: make this a bit more advanced. Check for method arity.
         
     | 
| 
       526 
585 
     | 
    
         | 
| 
       527 
586 
     | 
    
         
             
            ### `Types::Hash`
         
     | 
| 
       528 
587 
     | 
    
         | 
| 
         @@ -773,13 +832,15 @@ Images = Types::Array[ImageDownload].concurrent 
     | 
|
| 
       773 
832 
     | 
    
         
             
            Images.parse(['https://images.com/1.png', 'https://images.com/2.png'])
         
     | 
| 
       774 
833 
     | 
    
         
             
            ```
         
     | 
| 
       775 
834 
     | 
    
         | 
| 
      
 835 
     | 
    
         
            +
            See the [concurrent downloads example](https://github.com/ismasan/plumb/blob/main/examples/concurrent_downloads.rb).
         
     | 
| 
      
 836 
     | 
    
         
            +
             
     | 
| 
       776 
837 
     | 
    
         
             
            TODO: pluggable concurrency engines (Async?)
         
     | 
| 
       777 
838 
     | 
    
         | 
| 
       778 
839 
     | 
    
         
             
            #### `#stream`
         
     | 
| 
       779 
840 
     | 
    
         | 
| 
       780 
841 
     | 
    
         
             
            Turn an Array definition into an enumerator that yields each element wrapped in `Result::Valid` or `Result::Invalid`.
         
     | 
| 
       781 
842 
     | 
    
         | 
| 
       782 
     | 
    
         
            -
            See `Types::Stream` below for more.
         
     | 
| 
      
 843 
     | 
    
         
            +
            See [`Types::Stream`](#typesstream) below for more.
         
     | 
| 
       783 
844 
     | 
    
         | 
| 
       784 
845 
     | 
    
         
             
            #### `#filtered`
         
     | 
| 
       785 
846 
     | 
    
         | 
| 
         @@ -848,6 +909,8 @@ stream.each.with_index(1) do |result, line| 
     | 
|
| 
       848 
909 
     | 
    
         
             
            end
         
     | 
| 
       849 
910 
     | 
    
         
             
            ```
         
     | 
| 
       850 
911 
     | 
    
         | 
| 
      
 912 
     | 
    
         
            +
            See a more complete the [CSV Stream example](https://github.com/ismasan/plumb/blob/main/examples/csv_stream.rb)
         
     | 
| 
      
 913 
     | 
    
         
            +
             
     | 
| 
       851 
914 
     | 
    
         
             
            #### `Types::Stream#filtered`
         
     | 
| 
       852 
915 
     | 
    
         | 
| 
       853 
916 
     | 
    
         
             
            Use `#filtered` to turn a `Types::Stream` into a stream that only yields valid elements.
         
     | 
| 
         @@ -904,6 +967,13 @@ person.valid? # false 
     | 
|
| 
       904 
967 
     | 
    
         
             
            person.errors[:age] # 'must be an integer'
         
     | 
| 
       905 
968 
     | 
    
         
             
            ```
         
     | 
| 
       906 
969 
     | 
    
         | 
| 
      
 970 
     | 
    
         
            +
            Data structs can also be defined from `Types::Hash` instances.
         
     | 
| 
      
 971 
     | 
    
         
            +
             
     | 
| 
      
 972 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 973 
     | 
    
         
            +
            PersonHash = Types::Hash[name: String, age?: Integer]
         
     | 
| 
      
 974 
     | 
    
         
            +
            PersonStruct = Types::Data[PersonHash]
         
     | 
| 
      
 975 
     | 
    
         
            +
            ```
         
     | 
| 
      
 976 
     | 
    
         
            +
             
     | 
| 
       907 
977 
     | 
    
         
             
            #### `#with`
         
     | 
| 
       908 
978 
     | 
    
         | 
| 
       909 
979 
     | 
    
         
             
            Note that these instances cannot be mutated (there's no attribute setters), but they can be copied with partial attributes with `#with`
         
     | 
| 
         @@ -995,7 +1065,25 @@ Note that this does NOT work with union'd or piped structs. 
     | 
|
| 
       995 
1065 
     | 
    
         
             
            attribute :company, Company | Person do
         
     | 
| 
       996 
1066 
     | 
    
         
             
            ```
         
     | 
| 
       997 
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 
     | 
    
         
            +
             
     | 
| 
       998 
1085 
     | 
    
         
             
            #### Optional Attributes
         
     | 
| 
      
 1086 
     | 
    
         
            +
             
     | 
| 
       999 
1087 
     | 
    
         
             
            Using `attribute?` allows for optional attributes. If the attribute is not present, these attribute values will be `nil`
         
     | 
| 
       1000 
1088 
     | 
    
         | 
| 
       1001 
1089 
     | 
    
         
             
            ```ruby
         
     | 
| 
         @@ -1062,13 +1150,168 @@ person.friend.name # 'joan' 
     | 
|
| 
       1062 
1150 
     | 
    
         
             
            person.friend.friend # nil
         
     | 
| 
       1063 
1151 
     | 
    
         
             
            ```
         
     | 
| 
       1064 
1152 
     | 
    
         | 
| 
      
 1153 
     | 
    
         
            +
            ### Plumb::Pipeline
         
     | 
| 
       1065 
1154 
     | 
    
         | 
| 
      
 1155 
     | 
    
         
            +
            `Plumb::Pipeline` offers a sequential, step-by-step syntax for composing processing steps, as well as a simple middleware API to wrap steps for metrics, logging, debugging, caching and more. See the [command objects](https://github.com/ismasan/plumb/blob/main/examples/command_objects.rb) example for a worked use case.
         
     | 
| 
       1066 
1156 
     | 
    
         | 
| 
       1067 
     | 
    
         
            -
             
     | 
| 
      
 1157 
     | 
    
         
            +
            #### `#pipeline` helper
         
     | 
| 
       1068 
1158 
     | 
    
         | 
| 
       1069 
     | 
    
         
            -
             
     | 
| 
      
 1159 
     | 
    
         
            +
            All plumb steps have a `#pipeline` helper.
         
     | 
| 
       1070 
1160 
     | 
    
         | 
| 
       1071 
     | 
    
         
            -
             
     | 
| 
      
 1161 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 1162 
     | 
    
         
            +
            User = Types::Data[name: String, age: Integer]
         
     | 
| 
      
 1163 
     | 
    
         
            +
             
     | 
| 
      
 1164 
     | 
    
         
            +
            CreateUser = User.pipeline do |pl|
         
     | 
| 
      
 1165 
     | 
    
         
            +
              # Add steps as #call(Result) => Result interfaces
         
     | 
| 
      
 1166 
     | 
    
         
            +
              pl.step ValidateUser.new
         
     | 
| 
      
 1167 
     | 
    
         
            +
              
         
     | 
| 
      
 1168 
     | 
    
         
            +
              # Or as procs
         
     | 
| 
      
 1169 
     | 
    
         
            +
              pl.step do |result|
         
     | 
| 
      
 1170 
     | 
    
         
            +
                Logger.info "We have a valid user #{result.value}"
         
     | 
| 
      
 1171 
     | 
    
         
            +
                result
         
     | 
| 
      
 1172 
     | 
    
         
            +
              end
         
     | 
| 
      
 1173 
     | 
    
         
            +
              
         
     | 
| 
      
 1174 
     | 
    
         
            +
              # Or as other Plumb steps
         
     | 
| 
      
 1175 
     | 
    
         
            +
              pl.step User.transform(User) { |user| user.with(name: user.name.upcase) }
         
     | 
| 
      
 1176 
     | 
    
         
            +
              
         
     | 
| 
      
 1177 
     | 
    
         
            +
              pl.step do |result|
         
     | 
| 
      
 1178 
     | 
    
         
            +
                DB.create(result.value)
         
     | 
| 
      
 1179 
     | 
    
         
            +
              end
         
     | 
| 
      
 1180 
     | 
    
         
            +
            end
         
     | 
| 
      
 1181 
     | 
    
         
            +
             
     | 
| 
      
 1182 
     | 
    
         
            +
            # Use normally as any other Plumb step
         
     | 
| 
      
 1183 
     | 
    
         
            +
            result = CreateUser.resolve(name: 'Joe', age: 40)
         
     | 
| 
      
 1184 
     | 
    
         
            +
            # result.valid?
         
     | 
| 
      
 1185 
     | 
    
         
            +
            # result.errors
         
     | 
| 
      
 1186 
     | 
    
         
            +
            # result.value => User
         
     | 
| 
      
 1187 
     | 
    
         
            +
            ```
         
     | 
| 
      
 1188 
     | 
    
         
            +
             
     | 
| 
      
 1189 
     | 
    
         
            +
            Pipelines are Plumb steps, so they can be composed further.
         
     | 
| 
      
 1190 
     | 
    
         
            +
             
     | 
| 
      
 1191 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 1192 
     | 
    
         
            +
            IsJoe = User.check('must be named joe') { |user| 
         
     | 
| 
      
 1193 
     | 
    
         
            +
              result.value.name == 'Joe' 
         
     | 
| 
      
 1194 
     | 
    
         
            +
            }
         
     | 
| 
      
 1195 
     | 
    
         
            +
             
     | 
| 
      
 1196 
     | 
    
         
            +
            CreateIfJoe = IsJoe >> CreateUser
         
     | 
| 
      
 1197 
     | 
    
         
            +
            ```
         
     | 
| 
      
 1198 
     | 
    
         
            +
             
     | 
| 
      
 1199 
     | 
    
         
            +
            ##### `#around`
         
     | 
| 
      
 1200 
     | 
    
         
            +
             
     | 
| 
      
 1201 
     | 
    
         
            +
            Use `#around` in a pipeline definition to add a middleware step that wraps all other steps registered.
         
     | 
| 
      
 1202 
     | 
    
         
            +
             
     | 
| 
      
 1203 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 1204 
     | 
    
         
            +
            # The #around interface is #call(Step, Result::Valid) => Result::Valid | Result::Invalid
         
     | 
| 
      
 1205 
     | 
    
         
            +
            StepLogger = proc do |step, result|
         
     | 
| 
      
 1206 
     | 
    
         
            +
              Logger.info "Processing step #{step}"
         
     | 
| 
      
 1207 
     | 
    
         
            +
              step.call(result)
         
     | 
| 
      
 1208 
     | 
    
         
            +
            end
         
     | 
| 
      
 1209 
     | 
    
         
            +
             
     | 
| 
      
 1210 
     | 
    
         
            +
            CreateUser = User.pipeline do |pl|
         
     | 
| 
      
 1211 
     | 
    
         
            +
              # Around middleware will wrap all other steps registered below
         
     | 
| 
      
 1212 
     | 
    
         
            +
              pl.around StepLogger
         
     | 
| 
      
 1213 
     | 
    
         
            +
              
         
     | 
| 
      
 1214 
     | 
    
         
            +
              pl.step ValidateUser.new
         
     | 
| 
      
 1215 
     | 
    
         
            +
              pl.step ...etc
         
     | 
| 
      
 1216 
     | 
    
         
            +
            end
         
     | 
| 
      
 1217 
     | 
    
         
            +
            ```
         
     | 
| 
      
 1218 
     | 
    
         
            +
             
     | 
| 
      
 1219 
     | 
    
         
            +
            Note that order matters: an _around_ step will only wrap steps registered _after it_.
         
     | 
| 
      
 1220 
     | 
    
         
            +
             
     | 
| 
      
 1221 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 1222 
     | 
    
         
            +
            # This step will not be wrapped by StepLogger
         
     | 
| 
      
 1223 
     | 
    
         
            +
            pl.step Step1
         
     | 
| 
      
 1224 
     | 
    
         
            +
             
     | 
| 
      
 1225 
     | 
    
         
            +
            pl.around StepLogger
         
     | 
| 
      
 1226 
     | 
    
         
            +
            # This step WILL be wrapped
         
     | 
| 
      
 1227 
     | 
    
         
            +
            pl.step Step2
         
     | 
| 
      
 1228 
     | 
    
         
            +
            ```
         
     | 
| 
      
 1229 
     | 
    
         
            +
             
     | 
| 
      
 1230 
     | 
    
         
            +
            Like regular steps, `around` middleware can be a class, an instance, a proc, or anything that implements the middleware interface.
         
     | 
| 
      
 1231 
     | 
    
         
            +
             
     | 
| 
      
 1232 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 1233 
     | 
    
         
            +
            # As class instance
         
     | 
| 
      
 1234 
     | 
    
         
            +
            #   pl.around StepLogger.new(:warn)
         
     | 
| 
      
 1235 
     | 
    
         
            +
            class StepLogger
         
     | 
| 
      
 1236 
     | 
    
         
            +
              def initialize(level = :info)
         
     | 
| 
      
 1237 
     | 
    
         
            +
                @level = level
         
     | 
| 
      
 1238 
     | 
    
         
            +
              end
         
     | 
| 
      
 1239 
     | 
    
         
            +
              
         
     | 
| 
      
 1240 
     | 
    
         
            +
              def call(step, result)
         
     | 
| 
      
 1241 
     | 
    
         
            +
                Logger.send(@level) "Processing step #{step}"
         
     | 
| 
      
 1242 
     | 
    
         
            +
                step.call(result)
         
     | 
| 
      
 1243 
     | 
    
         
            +
              end
         
     | 
| 
      
 1244 
     | 
    
         
            +
            end
         
     | 
| 
      
 1245 
     | 
    
         
            +
             
     | 
| 
      
 1246 
     | 
    
         
            +
            # As proc
         
     | 
| 
      
 1247 
     | 
    
         
            +
            pl.around do |step, result|
         
     | 
| 
      
 1248 
     | 
    
         
            +
              Logger.info "Processing step #{step}"
         
     | 
| 
      
 1249 
     | 
    
         
            +
              step.call(result)
         
     | 
| 
      
 1250 
     | 
    
         
            +
            end
         
     | 
| 
      
 1251 
     | 
    
         
            +
            ```
         
     | 
| 
      
 1252 
     | 
    
         
            +
             
     | 
| 
      
 1253 
     | 
    
         
            +
            #### As stand-alone `Plumb::Pipeline` class
         
     | 
| 
      
 1254 
     | 
    
         
            +
             
     | 
| 
      
 1255 
     | 
    
         
            +
            `Plumb::Pipeline` can also be used on its own, sub-classed, and it can take class-level `around` middleware.
         
     | 
| 
      
 1256 
     | 
    
         
            +
             
     | 
| 
      
 1257 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 1258 
     | 
    
         
            +
            class LoggedPipeline < Plumb::Pipeline
         
     | 
| 
      
 1259 
     | 
    
         
            +
              # class-level midleware will be inherited by sub-classes
         
     | 
| 
      
 1260 
     | 
    
         
            +
              around StepLogger
         
     | 
| 
      
 1261 
     | 
    
         
            +
            end
         
     | 
| 
      
 1262 
     | 
    
         
            +
             
     | 
| 
      
 1263 
     | 
    
         
            +
            # Subclass inherits class-level middleware stack,
         
     | 
| 
      
 1264 
     | 
    
         
            +
            # and it can also add its own class or instance-level middleware
         
     | 
| 
      
 1265 
     | 
    
         
            +
            class ChildPipeline < LoggedPipeline
         
     | 
| 
      
 1266 
     | 
    
         
            +
              # class-level middleware
         
     | 
| 
      
 1267 
     | 
    
         
            +
              around Telemetry.new
         
     | 
| 
      
 1268 
     | 
    
         
            +
            end
         
     | 
| 
      
 1269 
     | 
    
         
            +
             
     | 
| 
      
 1270 
     | 
    
         
            +
            # Instantiate and add instance-level middleware
         
     | 
| 
      
 1271 
     | 
    
         
            +
            pipe = ChildPipeline.new do |pl|
         
     | 
| 
      
 1272 
     | 
    
         
            +
              pl.around NotifyErrors
         
     | 
| 
      
 1273 
     | 
    
         
            +
              pl.step Step1
         
     | 
| 
      
 1274 
     | 
    
         
            +
              pl.step Step2
         
     | 
| 
      
 1275 
     | 
    
         
            +
            end
         
     | 
| 
      
 1276 
     | 
    
         
            +
            ```
         
     | 
| 
      
 1277 
     | 
    
         
            +
             
     | 
| 
      
 1278 
     | 
    
         
            +
            Sub-classing `Plumb::Pipeline` can be useful to add helpers or domain-specific functionality
         
     | 
| 
      
 1279 
     | 
    
         
            +
             
     | 
| 
      
 1280 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 1281 
     | 
    
         
            +
            class DebuggablePipeline < LoggedPipeline
         
     | 
| 
      
 1282 
     | 
    
         
            +
              # Use #debug! for inserting a debugger between steps
         
     | 
| 
      
 1283 
     | 
    
         
            +
              def debug!
         
     | 
| 
      
 1284 
     | 
    
         
            +
                step do |result|
         
     | 
| 
      
 1285 
     | 
    
         
            +
                  debugger
         
     | 
| 
      
 1286 
     | 
    
         
            +
                  result
         
     | 
| 
      
 1287 
     | 
    
         
            +
                end
         
     | 
| 
      
 1288 
     | 
    
         
            +
              end
         
     | 
| 
      
 1289 
     | 
    
         
            +
            end
         
     | 
| 
      
 1290 
     | 
    
         
            +
             
     | 
| 
      
 1291 
     | 
    
         
            +
            pipe = DebuggablePipeline.new do |pl|
         
     | 
| 
      
 1292 
     | 
    
         
            +
              pl.step Step1
         
     | 
| 
      
 1293 
     | 
    
         
            +
              pl.debug!
         
     | 
| 
      
 1294 
     | 
    
         
            +
              pl.step Step2
         
     | 
| 
      
 1295 
     | 
    
         
            +
            end
         
     | 
| 
      
 1296 
     | 
    
         
            +
            ```
         
     | 
| 
      
 1297 
     | 
    
         
            +
             
     | 
| 
      
 1298 
     | 
    
         
            +
            #### Pipelines all the way down :turtle:
         
     | 
| 
      
 1299 
     | 
    
         
            +
             
     | 
| 
      
 1300 
     | 
    
         
            +
            Pipelines are full Plumb steps, so they can themselves be used as steps.
         
     | 
| 
      
 1301 
     | 
    
         
            +
             
     | 
| 
      
 1302 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 1303 
     | 
    
         
            +
            Pipe1 = DebuggablePipeline.new do |pl|
         
     | 
| 
      
 1304 
     | 
    
         
            +
              pl.step Step1
         
     | 
| 
      
 1305 
     | 
    
         
            +
              pl.step Step2
         
     | 
| 
      
 1306 
     | 
    
         
            +
            end
         
     | 
| 
      
 1307 
     | 
    
         
            +
             
     | 
| 
      
 1308 
     | 
    
         
            +
            Pipe2 = DebuggablePipeline.new do |pl|
         
     | 
| 
      
 1309 
     | 
    
         
            +
              pl.step Pipe1 # <= A pipeline instance as step
         
     | 
| 
      
 1310 
     | 
    
         
            +
              pl.step Step3
         
     | 
| 
      
 1311 
     | 
    
         
            +
            end
         
     | 
| 
      
 1312 
     | 
    
         
            +
            ```
         
     | 
| 
      
 1313 
     | 
    
         
            +
             
     | 
| 
      
 1314 
     | 
    
         
            +
            ### Plumb::Schema
         
     | 
| 
       1072 
1315 
     | 
    
         | 
| 
       1073 
1316 
     | 
    
         
             
            TODO
         
     | 
| 
       1074 
1317 
     | 
    
         | 
| 
         @@ -1388,7 +1631,7 @@ Types::DateTime.to_json_schema 
     | 
|
| 
       1388 
1631 
     | 
    
         
             
            - [ ] benchmarks and performace. Compare with `Parametric`, `ActiveModel::Attributes`, `ActionController::StrongParameters`
         
     | 
| 
       1389 
1632 
     | 
    
         
             
            - [ ] flesh out `Plumb::Schema`
         
     | 
| 
       1390 
1633 
     | 
    
         
             
            - [x] `Plumb::Struct`
         
     | 
| 
       1391 
     | 
    
         
            -
            - [ 
     | 
| 
      
 1634 
     | 
    
         
            +
            - [x] flesh out and document `Plumb::Pipeline`
         
     | 
| 
       1392 
1635 
     | 
    
         
             
            - [ ] document custom visitors
         
     | 
| 
       1393 
1636 
     | 
    
         
             
            - [ ] Improve errors, support I18n ?
         
     | 
| 
       1394 
1637 
     | 
    
         | 
| 
         @@ -0,0 +1,102 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'bundler'
         
     | 
| 
      
 4 
     | 
    
         
            +
            Bundler.setup(:benchmark)
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            require 'benchmark/ips'
         
     | 
| 
      
 7 
     | 
    
         
            +
            require 'money'
         
     | 
| 
      
 8 
     | 
    
         
            +
            Money.rounding_mode = BigDecimal::ROUND_HALF_EVEN
         
     | 
| 
      
 9 
     | 
    
         
            +
            Money.default_currency = 'GBP'
         
     | 
| 
      
 10 
     | 
    
         
            +
            require_relative './parametric_schema'
         
     | 
| 
      
 11 
     | 
    
         
            +
            require_relative './plumb_hash'
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
            data = {
         
     | 
| 
      
 14 
     | 
    
         
            +
              supplier_name: 'Vodafone',
         
     | 
| 
      
 15 
     | 
    
         
            +
              start_date: '2020-01-01',
         
     | 
| 
      
 16 
     | 
    
         
            +
              end_date: '2021-01-11',
         
     | 
| 
      
 17 
     | 
    
         
            +
              countdown_date: '2021-01-11',
         
     | 
| 
      
 18 
     | 
    
         
            +
              name: 'Vodafone TV',
         
     | 
| 
      
 19 
     | 
    
         
            +
              upfront_cost_description: 'Upfront cost description',
         
     | 
| 
      
 20 
     | 
    
         
            +
              tv_channels_count: 100,
         
     | 
| 
      
 21 
     | 
    
         
            +
              terms: [
         
     | 
| 
      
 22 
     | 
    
         
            +
                { name: 'Foo', url: 'http://foo.com', terms_text: 'Foo terms', start_date: '2020-01-01', end_date: '2021-01-01' },
         
     | 
| 
      
 23 
     | 
    
         
            +
                { name: 'Foo2', url: 'http://foo2.com', terms_text: 'Foo terms', start_date: '2020-01-01', end_date: '2021-01-01' }
         
     | 
| 
      
 24 
     | 
    
         
            +
              ],
         
     | 
| 
      
 25 
     | 
    
         
            +
              tv_included: true,
         
     | 
| 
      
 26 
     | 
    
         
            +
              additional_info: 'Additional info',
         
     | 
| 
      
 27 
     | 
    
         
            +
              product_type: 'TV',
         
     | 
| 
      
 28 
     | 
    
         
            +
              annual_price_increase_applies: true,
         
     | 
| 
      
 29 
     | 
    
         
            +
              annual_price_increase_description: 'Annual price increase description',
         
     | 
| 
      
 30 
     | 
    
         
            +
              broadband_components: [
         
     | 
| 
      
 31 
     | 
    
         
            +
                {
         
     | 
| 
      
 32 
     | 
    
         
            +
                  name: 'Broadband 1',
         
     | 
| 
      
 33 
     | 
    
         
            +
                  technology: 'FTTP',
         
     | 
| 
      
 34 
     | 
    
         
            +
                  technology_tags: ['FTTP'],
         
     | 
| 
      
 35 
     | 
    
         
            +
                  is_mobile: false,
         
     | 
| 
      
 36 
     | 
    
         
            +
                  description: 'Broadband 1 description',
         
     | 
| 
      
 37 
     | 
    
         
            +
                  download_speed_measurement: 'Mbps',
         
     | 
| 
      
 38 
     | 
    
         
            +
                  download_speed: 100,
         
     | 
| 
      
 39 
     | 
    
         
            +
                  upload_speed_measurement: 'Mbps',
         
     | 
| 
      
 40 
     | 
    
         
            +
                  upload_speed: 100,
         
     | 
| 
      
 41 
     | 
    
         
            +
                  download_usage_limit: 1000,
         
     | 
| 
      
 42 
     | 
    
         
            +
                  discount_price: 100,
         
     | 
| 
      
 43 
     | 
    
         
            +
                  discount_period: 12,
         
     | 
| 
      
 44 
     | 
    
         
            +
                  speed_description: 'Speed description',
         
     | 
| 
      
 45 
     | 
    
         
            +
                  ongoing_price: 100,
         
     | 
| 
      
 46 
     | 
    
         
            +
                  contract_length: 12,
         
     | 
| 
      
 47 
     | 
    
         
            +
                  upfront_cost: 100,
         
     | 
| 
      
 48 
     | 
    
         
            +
                  commission: 100
         
     | 
| 
      
 49 
     | 
    
         
            +
                }
         
     | 
| 
      
 50 
     | 
    
         
            +
              ],
         
     | 
| 
      
 51 
     | 
    
         
            +
              tv_components: [
         
     | 
| 
      
 52 
     | 
    
         
            +
                {
         
     | 
| 
      
 53 
     | 
    
         
            +
                  slug: 'vodafone-tv',
         
     | 
| 
      
 54 
     | 
    
         
            +
                  name: 'Vodafone TV',
         
     | 
| 
      
 55 
     | 
    
         
            +
                  search_tags: %w[Vodafone TV],
         
     | 
| 
      
 56 
     | 
    
         
            +
                  description: 'Vodafone TV description',
         
     | 
| 
      
 57 
     | 
    
         
            +
                  channels: 100,
         
     | 
| 
      
 58 
     | 
    
         
            +
                  discount_price: 100
         
     | 
| 
      
 59 
     | 
    
         
            +
                }
         
     | 
| 
      
 60 
     | 
    
         
            +
              ],
         
     | 
| 
      
 61 
     | 
    
         
            +
              call_package_types: ['Everything'],
         
     | 
| 
      
 62 
     | 
    
         
            +
              phone_components: [
         
     | 
| 
      
 63 
     | 
    
         
            +
                {
         
     | 
| 
      
 64 
     | 
    
         
            +
                  name: 'Phone 1',
         
     | 
| 
      
 65 
     | 
    
         
            +
                  description: 'Phone 1 description',
         
     | 
| 
      
 66 
     | 
    
         
            +
                  discount_price: 100,
         
     | 
| 
      
 67 
     | 
    
         
            +
                  disount_period: 12,
         
     | 
| 
      
 68 
     | 
    
         
            +
                  ongoing_price: 100,
         
     | 
| 
      
 69 
     | 
    
         
            +
                  contract_length: 12,
         
     | 
| 
      
 70 
     | 
    
         
            +
                  upfront_cost: 100,
         
     | 
| 
      
 71 
     | 
    
         
            +
                  commission: 100,
         
     | 
| 
      
 72 
     | 
    
         
            +
                  call_package_types: ['Everything']
         
     | 
| 
      
 73 
     | 
    
         
            +
                }
         
     | 
| 
      
 74 
     | 
    
         
            +
              ],
         
     | 
| 
      
 75 
     | 
    
         
            +
              payment_methods: ['Credit Card', 'Paypal'],
         
     | 
| 
      
 76 
     | 
    
         
            +
              discounts: [
         
     | 
| 
      
 77 
     | 
    
         
            +
                { period: 12, price: 100 }
         
     | 
| 
      
 78 
     | 
    
         
            +
              ],
         
     | 
| 
      
 79 
     | 
    
         
            +
              ongoing_price: 100,
         
     | 
| 
      
 80 
     | 
    
         
            +
              contract_length: 12,
         
     | 
| 
      
 81 
     | 
    
         
            +
              upfront_cost: 100,
         
     | 
| 
      
 82 
     | 
    
         
            +
              year_1_price: 100,
         
     | 
| 
      
 83 
     | 
    
         
            +
              savings: 100,
         
     | 
| 
      
 84 
     | 
    
         
            +
              commission: 100,
         
     | 
| 
      
 85 
     | 
    
         
            +
              max_broadband_download_speed: 100
         
     | 
| 
      
 86 
     | 
    
         
            +
            }
         
     | 
| 
      
 87 
     | 
    
         
            +
             
     | 
| 
      
 88 
     | 
    
         
            +
            # p V1Schemas::RECORD.resolve(data).errors
         
     | 
| 
      
 89 
     | 
    
         
            +
            # p V2Schemas::Record.resolve(data)
         
     | 
| 
      
 90 
     | 
    
         
            +
            # result = Parametric::V2::Result.wrap(data)
         
     | 
| 
      
 91 
     | 
    
         
            +
             
     | 
| 
      
 92 
     | 
    
         
            +
            # p result
         
     | 
| 
      
 93 
     | 
    
         
            +
            # p V2Schema.call(result)
         
     | 
| 
      
 94 
     | 
    
         
            +
            Benchmark.ips do |x|
         
     | 
| 
      
 95 
     | 
    
         
            +
              x.report('Parametric::Schema') do
         
     | 
| 
      
 96 
     | 
    
         
            +
                ParametricSchema::RECORD.resolve(data)
         
     | 
| 
      
 97 
     | 
    
         
            +
              end
         
     | 
| 
      
 98 
     | 
    
         
            +
              x.report('Plumb') do
         
     | 
| 
      
 99 
     | 
    
         
            +
                PlumbHash::Record.resolve(data)
         
     | 
| 
      
 100 
     | 
    
         
            +
              end
         
     | 
| 
      
 101 
     | 
    
         
            +
              x.compare!
         
     | 
| 
      
 102 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,68 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'bundler'
         
     | 
| 
      
 4 
     | 
    
         
            +
            Bundler.setup(:benchmark)
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            require 'benchmark/ips'
         
     | 
| 
      
 7 
     | 
    
         
            +
            require 'parametric/struct'
         
     | 
| 
      
 8 
     | 
    
         
            +
            require 'plumb'
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
            module ParametricStruct
         
     | 
| 
      
 11 
     | 
    
         
            +
              class User
         
     | 
| 
      
 12 
     | 
    
         
            +
                include Parametric::Struct
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                schema do
         
     | 
| 
      
 15 
     | 
    
         
            +
                  field(:name).type(:string).present
         
     | 
| 
      
 16 
     | 
    
         
            +
                  field(:friends).type(:array).schema do
         
     | 
| 
      
 17 
     | 
    
         
            +
                    field(:name).type(:string).present
         
     | 
| 
      
 18 
     | 
    
         
            +
                    field(:age).type(:integer)
         
     | 
| 
      
 19 
     | 
    
         
            +
                  end
         
     | 
| 
      
 20 
     | 
    
         
            +
                end
         
     | 
| 
      
 21 
     | 
    
         
            +
              end
         
     | 
| 
      
 22 
     | 
    
         
            +
            end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
            module PlumbStruct
         
     | 
| 
      
 25 
     | 
    
         
            +
              include Plumb::Types
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
              class User < Data
         
     | 
| 
      
 28 
     | 
    
         
            +
                attribute :name, String.present
         
     | 
| 
      
 29 
     | 
    
         
            +
                attribute :friends, Array do
         
     | 
| 
      
 30 
     | 
    
         
            +
                  attribute :name, String.present
         
     | 
| 
      
 31 
     | 
    
         
            +
                  attribute :age, Integer
         
     | 
| 
      
 32 
     | 
    
         
            +
                end
         
     | 
| 
      
 33 
     | 
    
         
            +
              end
         
     | 
| 
      
 34 
     | 
    
         
            +
            end
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
            module DataBaseline
         
     | 
| 
      
 37 
     | 
    
         
            +
              Friend = Data.define(:name, :age)
         
     | 
| 
      
 38 
     | 
    
         
            +
              User = Data.define(:name, :friends) do
         
     | 
| 
      
 39 
     | 
    
         
            +
                def self.build(data)
         
     | 
| 
      
 40 
     | 
    
         
            +
                  data = data.merge(friends: data[:friends].map { |friend| Friend.new(**friend) })
         
     | 
| 
      
 41 
     | 
    
         
            +
                  new(**data)
         
     | 
| 
      
 42 
     | 
    
         
            +
                end
         
     | 
| 
      
 43 
     | 
    
         
            +
              end
         
     | 
| 
      
 44 
     | 
    
         
            +
            end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
            data = {
         
     | 
| 
      
 47 
     | 
    
         
            +
              name: 'John',
         
     | 
| 
      
 48 
     | 
    
         
            +
              friends: [
         
     | 
| 
      
 49 
     | 
    
         
            +
                { name: 'Jane', age: 30 },
         
     | 
| 
      
 50 
     | 
    
         
            +
                { name: 'Joan', age: 38 }
         
     | 
| 
      
 51 
     | 
    
         
            +
              ]
         
     | 
| 
      
 52 
     | 
    
         
            +
            }
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
            Benchmark.ips do |x|
         
     | 
| 
      
 55 
     | 
    
         
            +
              # x.report('Ruby Data') do
         
     | 
| 
      
 56 
     | 
    
         
            +
              #   user = DataBaseline::User.build(data)
         
     | 
| 
      
 57 
     | 
    
         
            +
              #   user.name
         
     | 
| 
      
 58 
     | 
    
         
            +
              # end
         
     | 
| 
      
 59 
     | 
    
         
            +
              x.report('Parametric::Struct') do
         
     | 
| 
      
 60 
     | 
    
         
            +
                user = ParametricStruct::User.new(data)
         
     | 
| 
      
 61 
     | 
    
         
            +
                user.name
         
     | 
| 
      
 62 
     | 
    
         
            +
              end
         
     | 
| 
      
 63 
     | 
    
         
            +
              x.report('Plumb::Types::Data') do
         
     | 
| 
      
 64 
     | 
    
         
            +
                user = PlumbStruct::User.new(data)
         
     | 
| 
      
 65 
     | 
    
         
            +
                user.name
         
     | 
| 
      
 66 
     | 
    
         
            +
              end
         
     | 
| 
      
 67 
     | 
    
         
            +
              x.compare!
         
     | 
| 
      
 68 
     | 
    
         
            +
            end
         
     |