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
|