plumb 0.0.16 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 13814b82e58449726341c0a26a398fee38818f852fe09f19ba7754a99b7c54ce
4
- data.tar.gz: 2205b2bc5f48b19b417afe8403cdbd7afbcac3689307b6ba69237ad094718815
3
+ metadata.gz: c718a272ada163dfc399bd4a6ae0353ac95b99210ac5450e5adf5e77881ed2d5
4
+ data.tar.gz: 2721e793e2cba293fcf8859d6b1ff960b7bc5e0ae33cf4374aa09a7532c6724b
5
5
  SHA512:
6
- metadata.gz: 604b2d74cf5a78578471d5e6cf523f956025ea95c2e9b17dec7337cca41de10dc2254bc66b69ea2255ee18059b885ef6bb5bd64ea32a34a763cf095168c80022
7
- data.tar.gz: 50e065d933d9c3a00ff04a0e3e2375997d43039fc8d5c3ba6c7c1a6dc9455bda9d056bfefb11e81b4c75f0a26c01919d7f18c5b0da6aefa33c1f668ddabbaa29
6
+ metadata.gz: c1ab0c74145b108e9b23503fe701a7237defcd72a463d05aa9500da2db1835559b279e3a0199c68774285000012b400d04026636dfbe21e53ee1872058bf7aed
7
+ data.tar.gz: fb65d7c1ebd803e08137a4473260a7050ec8025311696742ae8762b4b1fea77327ce5059a9b08a28c1ea78d3a9d8851378152c6b7f33d7489bc628f4375e2b9c
data/README.md CHANGED
@@ -669,6 +669,40 @@ when Readable
669
669
  end
670
670
  ```
671
671
 
672
+ Or pattern matching
673
+
674
+ ```ruby
675
+ case args
676
+ in [Iterable => list, String => id]
677
+ # etc
678
+ in [Resolvable => r]
679
+ # etc
680
+ end
681
+ ```
682
+
683
+ #### Merging interfaces
684
+
685
+ Use the `+` operator to merge two interfaces into a new one that must support both sets of method names.
686
+
687
+ ```ruby
688
+ Iterable = Types::Interface[:each, :map]
689
+ Countable = Types::Interface[:size]
690
+ # This one expects objects with methods :each, :map and :size
691
+ CountableIterable = Iterable + Countable
692
+ ```
693
+
694
+ #### Intersecting interfaces
695
+
696
+ Use the `&` operator to produce a new interface with the intersection of method names
697
+
698
+ ```ruby
699
+ I1 = Types::Interface[:a, :b, :c]
700
+ I2 = Types::Interface[:b, :c, :d]
701
+ # This one expects methods :b and :c
702
+ I3 = Types::Interface[:b, :c]
703
+ ```
704
+
705
+
672
706
  TODO: make this a bit more advanced. Check for method arity.
673
707
 
674
708
  ### `Types::Hash`
@@ -65,7 +65,7 @@ module Plumb
65
65
  # Steps might return the same result instance, so we map the values directly
66
66
  # separate from the errors.
67
67
  element_result = result.dup
68
- errors = {}
68
+ errors = Hash.new(capacity: result.value.size)
69
69
  values = result.value.map.with_index do |e, idx|
70
70
  re = element_type.call(element_result.reset(e))
71
71
  errors[idx] = re.errors unless re.valid?
@@ -228,12 +228,14 @@ module Plumb
228
228
  super
229
229
  end
230
230
 
231
+ MUST_BE_HASH = ['Must be a Hash of attributes'].freeze
232
+
231
233
  # The Plumb::Step interface
232
234
  # @param result [Plumb::Result::Valid]
233
235
  # @return [Plumb::Result::Valid, Plumb::Result::Invalid]
234
236
  def call(result)
235
237
  return result if result.value.is_a?(self)
236
- return result.invalid(errors: ['Must be a Hash of attributes']) unless result.value.respond_to?(:to_h)
238
+ return result.invalid(errors: MUST_BE_HASH) unless result.value.respond_to?(:to_h)
237
239
 
238
240
  instance = new(result.value.to_h)
239
241
  instance.valid? ? result.valid(instance) : result.invalid(instance, errors: instance.errors.to_h)
@@ -295,6 +295,18 @@ module Plumb
295
295
  end
296
296
 
297
297
  def call(result) = type.call(result)
298
+
299
+ # Two nodes are equal when they wrap the same type with the same
300
+ # node_name and args. The default Composable#== compares #children,
301
+ # but a Node holds its identity in @node_name/@type/@args, so without
302
+ # this every as_node-wrapped type (Email, Boolean, etc.) would compare
303
+ # equal to every other.
304
+ def ==(other)
305
+ other.is_a?(self.class) &&
306
+ other.node_name == node_name &&
307
+ other.type == type &&
308
+ other.args == args
309
+ end
298
310
  end
299
311
 
300
312
  # Wrap a Step in a node with a custom #node_name
@@ -108,24 +108,30 @@ module Plumb
108
108
  return result unless _schema.any?
109
109
 
110
110
  input = result.value
111
- errors = {}
112
- field_result = result.dup
113
- initial = {}
114
- initial = initial.merge(input) if @inclusive
115
- output = _schema.each.with_object(initial) do |(key, field), ret|
111
+ errors = nil # Do not allocate errors unless needed
112
+ output = @inclusive ? input.dup : {}
113
+ field_result = Result.valid(nil)
114
+
115
+ _schema.each do |key, field|
116
116
  key_s = key.to_key
117
117
  if input.key?(key_s)
118
118
  r = field.call(field_result.reset(input[key_s]))
119
- errors[key_s] = r.errors unless r.valid?
120
- ret[key_s] = r.value
119
+ output[key_s] = r.value
120
+ unless r.valid?
121
+ errors ||= {}
122
+ errors[key_s] = r.errors
123
+ end
121
124
  elsif !key.optional?
122
125
  r = field.call(BLANK_RESULT)
123
- errors[key_s] = r.errors unless r.valid?
124
- ret[key_s] = r.value unless r.value == Undefined
126
+ output[key_s] = r.value unless r.value == Undefined
127
+ unless r.valid?
128
+ errors ||= {}
129
+ errors[key_s] = r.errors
130
+ end
125
131
  end
126
132
  end
127
133
 
128
- errors.any? ? result.invalid(output, errors:) : result.valid(output)
134
+ errors ? result.invalid(output, errors:) : result.valid(output)
129
135
  end
130
136
 
131
137
  def ==(other)
@@ -28,10 +28,38 @@ module Plumb
28
28
 
29
29
  alias [] of
30
30
 
31
+ # Merge two interfaces into a new one with the method names of both
32
+ # @example
33
+ # i1 = Types::Interface[:foo]
34
+ # i2 = Types::Interface[:bar, :lol]
35
+ # i3 = i1 + i2 # expects objects with methods :foo, :bar, :lol
36
+ #
37
+ # @param other [InterfaceClass]
38
+ # @return [InterfaceClass]
39
+ def +(other)
40
+ raise ArgumentError, "expected another Types::Interface, but got #{other.inspect}" unless other.is_a?(self.class)
41
+
42
+ self.class.new((method_names + other.method_names).uniq)
43
+ end
44
+
45
+ # Produce a new Interface with the intersection of two interfaces
46
+ # @example
47
+ # i1 = Types::Interface[:foo, :bar]
48
+ # i2 = Types::Interface[:bar, :lol]
49
+ # i3 = i1 + i2 # expects objects with methods :bar
50
+ #
51
+ # @param other [InterfaceClass]
52
+ # @return [InterfaceClass]
53
+ def &(other)
54
+ raise ArgumentError, "expected another Types::Interface, but got #{other.inspect}" unless other.is_a?(self.class)
55
+
56
+ self.class.new(method_names & other.method_names)
57
+ end
58
+
31
59
  def call(result)
32
60
  obj = result.value
33
61
  missing_methods = @method_names.reject { |m| obj.respond_to?(m) }
34
- return result.invalid(errors: "missing methods: #{missing_methods.join(', ')}") if missing_methods.any?
62
+ return result.invalid(errors: "Invalid #{self.name}. Missing methods: #{missing_methods.join(', ')}") if missing_methods.any?
35
63
 
36
64
  result
37
65
  end
data/lib/plumb/or.rb CHANGED
@@ -27,8 +27,13 @@ module Plumb
27
27
  if right_result.valid?
28
28
  right_result
29
29
  else
30
- right_result.invalid(errors: [left_result.errors,
31
- right_result.errors].flatten)
30
+ # Decrease Array allocations slightly
31
+ # OR can be really expensive in composite ORed types
32
+ left_errors = left_result.errors.is_a?(Array) ? left_result.errors : [left_result.errors]
33
+ right_errors = right_result.errors.is_a?(Array) ? right_result.errors.first : right_result.errors
34
+ left_errors << right_errors
35
+
36
+ right_result.invalid(errors: left_errors)
32
37
  end
33
38
  end
34
39
  end
data/lib/plumb/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plumb
4
- VERSION = '0.0.16'
4
+ VERSION = '0.0.18'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plumb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.16
4
+ version: 0.0.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ismael Celis
@@ -53,7 +53,6 @@ files:
53
53
  - bench/compare_parametric_struct.rb
54
54
  - bench/parametric_schema.rb
55
55
  - bench/plumb_hash.rb
56
- - docs/styles.css
57
56
  - examples/command_objects.rb
58
57
  - examples/concurrent_downloads.rb
59
58
  - examples/csv_stream.rb
@@ -116,7 +115,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
116
115
  - !ruby/object:Gem::Version
117
116
  version: '0'
118
117
  requirements: []
119
- rubygems_version: 3.6.9
118
+ rubygems_version: 4.0.8
120
119
  specification_version: 4
121
120
  summary: Data validation and transformation library.
122
121
  test_files: []
data/docs/styles.css DELETED
@@ -1,540 +0,0 @@
1
- /* Reset and Base Styles */
2
- * {
3
- margin: 0;
4
- padding: 0;
5
- box-sizing: border-box;
6
- }
7
-
8
- :root {
9
- --primary-color: #2563eb;
10
- --primary-dark: #1e40af;
11
- --secondary-color: #64748b;
12
- --bg-color: #f8fafc;
13
- --sidebar-bg: #1e293b;
14
- --sidebar-text: #cbd5e1;
15
- --sidebar-hover: #334155;
16
- --content-bg: #ffffff;
17
- --text-color: #1e293b;
18
- --text-light: #64748b;
19
- --border-color: #e2e8f0;
20
- --code-bg: #f1f5f9;
21
- --code-border: #cbd5e1;
22
- --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
23
- --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
24
- }
25
-
26
- html {
27
- scroll-behavior: smooth;
28
- }
29
-
30
- body {
31
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
32
- line-height: 1.6;
33
- color: var(--text-color);
34
- background-color: var(--bg-color);
35
- padding-top: 60px;
36
- }
37
-
38
- /* Top Menu Bar */
39
- .top-menu {
40
- position: fixed;
41
- top: 0;
42
- left: 0;
43
- right: 0;
44
- height: 60px;
45
- background-color: var(--content-bg);
46
- border-bottom: 1px solid var(--border-color);
47
- box-shadow: var(--shadow);
48
- z-index: 1000;
49
- }
50
-
51
- .top-menu-content {
52
- max-width: 100%;
53
- height: 100%;
54
- padding: 0 2rem;
55
- display: flex;
56
- justify-content: space-between;
57
- align-items: center;
58
- }
59
-
60
- .top-menu-brand {
61
- display: flex;
62
- align-items: baseline;
63
- gap: 0.75rem;
64
- }
65
-
66
- .brand-name {
67
- font-size: 1.5rem;
68
- font-weight: 700;
69
- color: var(--text-color);
70
- }
71
-
72
- .brand-tagline {
73
- font-size: 0.875rem;
74
- color: var(--text-light);
75
- font-weight: 400;
76
- }
77
-
78
- .top-menu .github-link {
79
- display: flex;
80
- align-items: center;
81
- gap: 0.5rem;
82
- padding: 0.5rem 1rem;
83
- background-color: var(--text-color);
84
- color: #ffffff;
85
- border-radius: 6px;
86
- text-decoration: none;
87
- font-weight: 600;
88
- font-size: 0.875rem;
89
- transition: all 0.2s ease;
90
- }
91
-
92
- .top-menu .github-link:hover {
93
- background-color: #000000;
94
- text-decoration: none;
95
- }
96
-
97
- .top-menu .github-link svg {
98
- width: 20px;
99
- height: 20px;
100
- }
101
-
102
- /* Layout */
103
- .container {
104
- display: flex;
105
- min-height: calc(100vh - 60px);
106
- }
107
-
108
- /* Sidebar */
109
- .sidebar {
110
- width: 280px;
111
- background-color: var(--sidebar-bg);
112
- color: var(--sidebar-text);
113
- padding: 2rem 0;
114
- position: fixed;
115
- top: 60px;
116
- height: calc(100vh - 60px);
117
- overflow-y: auto;
118
- box-shadow: var(--shadow-lg);
119
- }
120
-
121
- .sidebar::-webkit-scrollbar {
122
- width: 8px;
123
- }
124
-
125
- .sidebar::-webkit-scrollbar-track {
126
- background: var(--sidebar-bg);
127
- }
128
-
129
- .sidebar::-webkit-scrollbar-thumb {
130
- background: var(--sidebar-hover);
131
- border-radius: 4px;
132
- }
133
-
134
- .logo {
135
- padding: 0 1.5rem 1.5rem;
136
- border-bottom: 1px solid var(--sidebar-hover);
137
- margin-bottom: 1.5rem;
138
- }
139
-
140
- .logo h2 {
141
- font-size: 1.75rem;
142
- color: #ffffff;
143
- margin-bottom: 0.25rem;
144
- font-weight: 700;
145
- }
146
-
147
- .logo .tagline {
148
- font-size: 0.875rem;
149
- color: var(--sidebar-text);
150
- font-weight: 400;
151
- }
152
-
153
- .nav-menu {
154
- list-style: none;
155
- padding-left: 0;
156
- }
157
-
158
- .nav-menu li {
159
- margin: 0;
160
- }
161
-
162
- .nav-menu a {
163
- display: block;
164
- padding: 0.625rem 1.5rem;
165
- color: var(--sidebar-text);
166
- text-decoration: none;
167
- transition: all 0.2s ease;
168
- font-size: 0.9375rem;
169
- }
170
-
171
- .nav-menu li:not(.nav-submenu) > a {
172
- font-weight: 600;
173
- margin-top: 0.5rem;
174
- }
175
-
176
- .nav-submenu a {
177
- padding-left: 2.5rem;
178
- font-size: 0.875rem;
179
- color: var(--sidebar-text);
180
- opacity: 0.9;
181
- }
182
-
183
- .nav-menu a:hover {
184
- background-color: var(--sidebar-hover);
185
- color: #ffffff;
186
- }
187
-
188
- .nav-menu a.active {
189
- background-color: var(--primary-color);
190
- color: #ffffff;
191
- border-left: 3px solid #ffffff;
192
- padding-left: calc(1.5rem - 3px);
193
- }
194
-
195
- .nav-submenu a.active {
196
- padding-left: calc(2.5rem - 3px);
197
- }
198
-
199
- /* Accordion Menus */
200
- .nav-accordion {
201
- position: relative;
202
- }
203
-
204
- .accordion-toggle {
205
- display: flex;
206
- justify-content: space-between;
207
- align-items: center;
208
- cursor: pointer;
209
- }
210
-
211
- .accordion-icon {
212
- font-size: 0.75rem;
213
- transition: transform 0.2s ease;
214
- margin-left: 0.5rem;
215
- }
216
-
217
- .nav-accordion.expanded .accordion-icon {
218
- transform: rotate(-180deg);
219
- }
220
-
221
- .accordion-content {
222
- list-style: none;
223
- padding-left: 0;
224
- max-height: 0;
225
- overflow: hidden;
226
- transition: max-height 0.3s ease;
227
- }
228
-
229
- .nav-accordion.expanded .accordion-content {
230
- max-height: 500px;
231
- }
232
-
233
- .nav-h4 a {
234
- padding-left: 3.5rem;
235
- font-size: 0.8125rem;
236
- color: var(--sidebar-text);
237
- opacity: 0.85;
238
- }
239
-
240
- .nav-h4 a.active {
241
- padding-left: calc(3.5rem - 3px);
242
- }
243
-
244
- /* Main Content */
245
- .content {
246
- flex: 1;
247
- margin-left: 280px;
248
- padding: 3rem;
249
- max-width: 1200px;
250
- width: 79vw;
251
- }
252
-
253
- /* Page Header */
254
- .page-header {
255
- margin-bottom: 3rem;
256
- padding-bottom: 2rem;
257
- border-bottom: 2px solid var(--border-color);
258
- }
259
-
260
- .page-header h1 {
261
- font-size: 3rem;
262
- font-weight: 800;
263
- color: var(--text-color);
264
- margin-bottom: 0.5rem;
265
- letter-spacing: -0.025em;
266
- }
267
-
268
- .page-header .subtitle {
269
- font-size: 1.25rem;
270
- color: var(--text-light);
271
- font-weight: 400;
272
- }
273
-
274
- /* Sections */
275
- .section {
276
- margin-bottom: 4rem;
277
- }
278
-
279
- .section h2 {
280
- font-size: 2rem;
281
- font-weight: 700;
282
- color: var(--text-color);
283
- margin-bottom: 1.5rem;
284
- padding-bottom: 0.5rem;
285
- border-bottom: 2px solid var(--primary-color);
286
- }
287
-
288
- .subsection {
289
- margin-bottom: 2.5rem;
290
- background-color: var(--content-bg);
291
- padding: 2rem;
292
- border-radius: 8px;
293
- box-shadow: var(--shadow);
294
- }
295
-
296
- .subsection h3 {
297
- font-size: 1.5rem;
298
- font-weight: 600;
299
- color: var(--text-color);
300
- margin-bottom: 1rem;
301
- }
302
-
303
- .subsection h4 {
304
- font-size: 1.25rem;
305
- font-weight: 600;
306
- color: var(--text-color);
307
- margin: 1.5rem 0 1rem;
308
- }
309
-
310
- /* Typography */
311
- p {
312
- margin-bottom: 1rem;
313
- line-height: 1.75;
314
- }
315
-
316
- a {
317
- color: var(--primary-color);
318
- text-decoration: none;
319
- }
320
-
321
- a:hover {
322
- color: var(--primary-dark);
323
- text-decoration: underline;
324
- }
325
-
326
- strong {
327
- font-weight: 600;
328
- color: var(--text-color);
329
- }
330
-
331
- /* Lists */
332
- ul, ol {
333
- margin-bottom: 1rem;
334
- padding-left: 1.5rem;
335
- }
336
-
337
- li {
338
- margin-bottom: 0.5rem;
339
- }
340
-
341
- .feature-list {
342
- list-style: none;
343
- padding-left: 0;
344
- }
345
-
346
- .feature-list li {
347
- padding-left: 1.5rem;
348
- position: relative;
349
- margin-bottom: 0.75rem;
350
- }
351
-
352
- .feature-list li::before {
353
- content: "→";
354
- position: absolute;
355
- left: 0;
356
- color: var(--primary-color);
357
- font-weight: bold;
358
- }
359
-
360
- /* Code Blocks */
361
- code {
362
- font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
363
- font-size: 0.875em;
364
- background-color: var(--code-bg);
365
- padding: 0.2em 0.4em;
366
- border-radius: 3px;
367
- border: 1px solid var(--code-border);
368
- }
369
-
370
- pre {
371
- margin: 1.5rem 0;
372
- padding: 0;
373
- overflow-x: auto;
374
- border-radius: 6px;
375
- box-shadow: var(--shadow);
376
- background-color: #282c34;
377
- }
378
-
379
- pre code {
380
- display: block;
381
- padding: 1.25rem;
382
- background-color: transparent;
383
- color: inherit;
384
- border: none;
385
- border-radius: 6px;
386
- line-height: 1.6;
387
- overflow-x: auto;
388
- }
389
-
390
- pre code::-webkit-scrollbar {
391
- height: 8px;
392
- }
393
-
394
- pre code::-webkit-scrollbar-track {
395
- background: var(--sidebar-hover);
396
- border-radius: 4px;
397
- }
398
-
399
- pre code::-webkit-scrollbar-thumb {
400
- background: var(--sidebar-text);
401
- border-radius: 4px;
402
- }
403
-
404
- /* Images */
405
- .image-container {
406
- margin: 2rem 0;
407
- text-align: center;
408
- }
409
-
410
- .image-container img {
411
- max-width: 100%;
412
- height: auto;
413
- }
414
-
415
- .image-container .caption {
416
- margin-top: 0.75rem;
417
- font-size: 0.875rem;
418
- color: var(--text-light);
419
- font-style: italic;
420
- }
421
-
422
- @media (max-width: 1248px) {
423
- .content p img {
424
- width: 64vw;
425
- height: auto;
426
- }
427
- }
428
-
429
- /* Responsive Design */
430
- @media (max-width: 1024px) {
431
- .sidebar {
432
- width: 240px;
433
- }
434
-
435
- .content {
436
- width: 76vw;
437
- margin-left: 240px;
438
- padding: 2rem;
439
- }
440
- }
441
-
442
- @media (max-width: 768px) {
443
- /* Stack sidebar and content vertically on handhelds */
444
- .container {
445
- flex-direction: column;
446
- }
447
- .top-menu-content {
448
- padding: 0 1rem;
449
- }
450
-
451
- .brand-tagline {
452
- display: none;
453
- }
454
-
455
- .top-menu .github-link span {
456
- display: none;
457
- }
458
-
459
- .top-menu .github-link {
460
- padding: 0.5rem;
461
- }
462
-
463
- .sidebar {
464
- display:none;
465
- }
466
-
467
- .content {
468
- margin-left: 0;
469
- width: 100vw;
470
- padding: 1.5rem;
471
- }
472
-
473
- .content p img {
474
- width: 78vw;
475
- height: auto;
476
- }
477
- .page-header h1 {
478
- font-size: 2rem;
479
- }
480
-
481
- .page-header .subtitle {
482
- font-size: 1rem;
483
- }
484
-
485
- .section h2 {
486
- font-size: 1.5rem;
487
- }
488
-
489
- .subsection {
490
- padding: 1.5rem;
491
- }
492
-
493
- .subsection h3 {
494
- font-size: 1.25rem;
495
- }
496
-
497
- pre code {
498
- padding: 1rem;
499
- font-size: 0.8125rem;
500
- }
501
- }
502
-
503
- /* Smooth Scrolling and Anchor Offset */
504
- section {
505
- scroll-margin-top: 5rem;
506
- }
507
-
508
- article {
509
- scroll-margin-top: 5rem;
510
- }
511
-
512
- /* Print Styles */
513
- @media print {
514
- .top-menu {
515
- display: none;
516
- }
517
-
518
- body {
519
- padding-top: 0;
520
- }
521
-
522
- .sidebar {
523
- display: none;
524
- }
525
-
526
- .content {
527
- margin-left: 0;
528
- max-width: 100%;
529
- }
530
-
531
- .subsection {
532
- box-shadow: none;
533
- border: 1px solid var(--border-color);
534
- }
535
-
536
- pre code {
537
- background-color: var(--code-bg);
538
- color: var(--text-color);
539
- }
540
- }