plumb 0.0.3 → 0.0.4

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.
data/README.md CHANGED
@@ -12,11 +12,15 @@ For a description of the core architecture you can read [this article](https://i
12
12
 
13
13
  ## Installation
14
14
 
15
- TODO
15
+ Install in your environment with `gem install plumb`, or in your `Gemfile` with
16
+
17
+ ```ruby
18
+ gem 'plumb'
19
+ ```
16
20
 
17
21
  ## Usage
18
22
 
19
- ### Include base types
23
+ ### Include base types.
20
24
 
21
25
  Include base types in your own namespace:
22
26
 
@@ -39,6 +43,8 @@ result.valid? # false
39
43
  result.errors # ""
40
44
  ```
41
45
 
46
+ Note that this is not mandatory. You can also work with the `Plumb::Types` module directly, ex. `Plumb::Types::String`
47
+
42
48
  ### Specialize your types with `#[]`
43
49
 
44
50
  Use `#[]` to make your types match a class.
@@ -47,8 +53,8 @@ Use `#[]` to make your types match a class.
47
53
  module Types
48
54
  include Plumb::Types
49
55
 
50
- String = Types::Any[::String]
51
- Integer = Types::Any[::Integer]
56
+ String = Any[::String]
57
+ Integer = Any[::Integer]
52
58
  end
53
59
 
54
60
  Types::String.parse("hello") # => "hello"
@@ -127,13 +133,13 @@ joe = User.parse({ name: 'Joe', email: 'joe@email.com', age: 20}) # returns vali
127
133
  Users.parse([joe]) # returns valid array of user hashes
128
134
  ```
129
135
 
130
- More about [Types::Array](#typeshash) and [Types::Array](#typesarray). There's also tuples and hash maps, and it's possible to create your own composite types.
136
+ More about [Types::Array](#typeshash) and [Types::Array](#typesarray). There's also [tuples](#typestuple), [hash maps](#hash-maps) and [data structs](#typesdata), and it's possible to create your own composite types.
131
137
 
132
- ## Type composition
138
+ ### Type composition
133
139
 
134
- At the core, Plumb types are little [Railway-oriented pipelines](https://ismaelcelis.com/posts/composable-pipelines-in-ruby/) that can be composed together with _and_, _or_ and _not_ semantics. Everything else builds on top of these two ideas.
140
+ At the core, Plumb types are little [Railway-oriented pipelines](https://ismaelcelis.com/posts/composable-pipelines-in-ruby/) that can be composed together with _AND_, _OR_ and _NOT_ semantics. Everything else builds on top of these two ideas.
135
141
 
136
- ### Composing types with `#>>` ("And")
142
+ #### Composing types with `#>>` ("And")
137
143
 
138
144
  ```ruby
139
145
  Email = Types::String[/@/]
@@ -143,7 +149,13 @@ Greeting = Email >> ->(result) { result.valid("Your email is #{result.value}") }
143
149
  Greeting.parse('joe@bloggs.com') # "Your email is joe@bloggs.com"
144
150
  ```
145
151
 
146
- ### Disjunction with `#|` ("Or")
152
+ Similar to Ruby's built-in [function composition](https://thoughtbot.com/blog/proc-composition-in-ruby), `#>>` pipes the output of a "type" to the input of the next type. However, if a type returns an "invalid" result, the chain is halted there and subsequent steps are never run.
153
+
154
+ In other words, `A >> B` means "if A succeeds, pass its result to B. Otherwise return A's failed result."
155
+
156
+ #### Disjunction with `#|` ("Or")
157
+
158
+ `A | B` means "if A returns a valid result, return that. Otherwise try B with the original input."
147
159
 
148
160
  ```ruby
149
161
  StringOrInt = Types::String | Types::Integer
@@ -160,9 +172,13 @@ EmailOrDefault.parse('joe@bloggs.com') # "Your email is joe@bloggs.com"
160
172
  EmailOrDefault.parse('nope') # "no email"
161
173
  ```
162
174
 
163
- ## Composing with `#>>` and `#|`
175
+ #### Composing with `#>>` and `#|`
176
+
177
+ Combine `#>>` and `#|` to compose branching workflows, or types that accept and output several possible data types.
178
+
179
+ `((A >> B) | C | D) >> E)`
164
180
 
165
- This more elaborate example defines a combination of types which, when composed together with `>>` and `|`, can coerce strings or integers into Money instances with currency.
181
+ This more elaborate example defines a combination of types which, when composed together with `>>` and `|`, can coerce strings or integers into Money instances with currency. It also shows some of the built-in [policies](#policies) or helpers.
166
182
 
167
183
  ```ruby
168
184
  require 'money'
@@ -170,12 +186,22 @@ require 'money'
170
186
  module Types
171
187
  include Plumb::Types
172
188
 
189
+ # Match any Money instance
173
190
  Money = Any[::Money]
191
+
192
+ # Transform Integers into Money instances
174
193
  IntToMoney = Integer.transform(::Money) { |v| ::Money.new(v, 'USD') }
194
+
195
+ # Transform integer-looking Strings into Integers
175
196
  StringToInt = String.match(/^\d+$/).transform(::Integer, &:to_i)
197
+
198
+ # Validate that a Money instance is USD
176
199
  USD = Money.check { |amount| amount.currency.code == 'UDS' }
200
+
201
+ # Exchange a non-USD Money instance into USD
177
202
  ToUSD = Money.transform(::Money) { |amount| amount.exchange_to('USD') }
178
203
 
204
+ # Compose a pipeline that accepts Strings, Integers or Money and returns USD money.
179
205
  FlexibleUSD = (Money | ((Integer | StringToInt) >> IntToMoney)) >> (USD | ToUSD)
180
206
  end
181
207
 
@@ -184,9 +210,9 @@ FlexibleUSD.parse(1000) # Money(USD 10.00)
184
210
  FlexibleUSD.parse(Money.new(1000, 'GBP')) # Money(USD 15.00)
185
211
  ```
186
212
 
187
- You can see more use cases in [the examples directory](/ismasan/plumb/tree/main/examples)
213
+ You can see more use cases in [the examples directory](https://github.com/ismasan/plumb/tree/main/examples)
188
214
 
189
- ## Built-in types
215
+ ### Built-in types
190
216
 
191
217
  * `Types::Value`
192
218
  * `Types::Array`
@@ -196,7 +222,6 @@ You can see more use cases in [the examples directory](/ismasan/plumb/tree/main/
196
222
  * `Types::Interface`
197
223
  * `Types::False`
198
224
  * `Types::Tuple`
199
- * `Types::Split`
200
225
  * `Types::Any`
201
226
  * `Types::Static`
202
227
  * `Types::Undefined`
@@ -213,13 +238,13 @@ You can see more use cases in [the examples directory](/ismasan/plumb/tree/main/
213
238
  * `Types::Forms::True`
214
239
  * `Types::Forms::False`
215
240
 
216
-
241
+ TODO: date and datetime, UUIDs, Email, others.
217
242
 
218
243
  ### Policies
219
244
 
220
- Policies are methods that encapsulate common compositions. Plumb ships with some, listed below, and you can also define your own.
245
+ Policies are helpers that encapsulate common compositions. Plumb ships with some handy ones, listed below, and you can also define your own.
221
246
 
222
- ### `#present`
247
+ #### `#present`
223
248
 
224
249
  Checks that the value is not blank (`""` if string, `[]` if array, `{}` if Hash, or `nil`)
225
250
 
@@ -228,7 +253,7 @@ Types::String.present.resolve('') # Failure with errors
228
253
  Types::Array[Types::String].resolve([]) # Failure with errors
229
254
  ```
230
255
 
231
- ### `#nullable`
256
+ #### `#nullable`
232
257
 
233
258
  Allow `nil` values.
234
259
 
@@ -245,7 +270,7 @@ Note that this just encapsulates the following composition:
245
270
  nullable_str = Types::String | Types::Nil
246
271
  ```
247
272
 
248
- ### `#not`
273
+ #### `#not`
249
274
 
250
275
  Negates a type.
251
276
  ```ruby
@@ -255,7 +280,7 @@ NotEmail.parse('hello') # "hello"
255
280
  NotEmail.parse('hello@server.com') # error
256
281
  ```
257
282
 
258
- ### `#options`
283
+ #### `#options`
259
284
 
260
285
  Sets allowed options for value.
261
286
 
@@ -273,7 +298,7 @@ type.resolve(['a', 'a', 'b']) # Valid
273
298
  type.resolve(['a', 'x', 'b']) # Failure
274
299
  ```
275
300
 
276
- ### `#transform`
301
+ #### `#transform`
277
302
 
278
303
  Transform value. Requires specifying the resulting type of the value after transformation.
279
304
 
@@ -285,7 +310,7 @@ StringToInt = Types::String.transform(Integer, &:to_i)
285
310
  StringToInteger.parse('10') # => 10
286
311
  ```
287
312
 
288
- ### `#invoke`
313
+ #### `#invoke`
289
314
 
290
315
  `#invoke` builds a Step that will invoke one or more methods on the value.
291
316
 
@@ -311,7 +336,7 @@ UpcaseToSym = Types::String.invoke(%i[downcase to_sym])
311
336
  UpcaseToSym.parse('FOO_BAR') # :foo_bar
312
337
  ```
313
338
 
314
- That that, as opposed to `#transform`, this modified does not register a type in `#metadata[:type]`, which can be valuable for introspection or documentation (ex. JSON Schema).
339
+ Note, as opposed to `#transform`, this helper does not register a type in `#metadata[:type]`, which can be valuable for introspection or documentation (ex. JSON Schema).
315
340
 
316
341
  Also, there's no definition-time checks that the method names are actually supported by the input values.
317
342
 
@@ -322,7 +347,7 @@ type.parse([1, 2]) # raises NoMethodError because Array doesn't respond to #stri
322
347
 
323
348
  Use with caution.
324
349
 
325
- ### `#default`
350
+ #### `#default`
326
351
 
327
352
  Default value when no value given (ie. when key is missing in Hash payloads. See `Types::Hash` below).
328
353
 
@@ -356,7 +381,7 @@ Same if you want to apply a default to several cases.
356
381
  str = Types::String | ((Types::Nil | Types::Undefined) >> Types::Static['nope'.freeze])
357
382
  ```
358
383
 
359
- ### `#build`
384
+ #### `#build`
360
385
 
361
386
  Build a custom object or class.
362
387
 
@@ -389,7 +414,7 @@ Note that this case is identical to `#transform` with a block.
389
414
  StringToMoney = Types::String.transform(Money) { |value| Monetize.parse(value) }
390
415
  ```
391
416
 
392
- ### `#check`
417
+ #### `#check`
393
418
 
394
419
  Pass the value through an arbitrary validation
395
420
 
@@ -399,9 +424,7 @@ type.parse('Role: Manager') # 'Role: Manager'
399
424
  type.parse('Manager') # fails
400
425
  ```
401
426
 
402
-
403
-
404
- ### `#value`
427
+ #### `#value`
405
428
 
406
429
  Constrain a type to a specific value. Compares with `#==`
407
430
 
@@ -418,19 +441,21 @@ All scalar types support this:
418
441
  ten = Types::Integer.value(10)
419
442
  ```
420
443
 
421
- ### `#meta` and `#metadata`
444
+ #### `#metadata`
422
445
 
423
446
  Add metadata to a type
424
447
 
425
448
  ```ruby
426
- type = Types::String.meta(description: 'A long text')
449
+ # A new type with metadata
450
+ type = Types::String.metadata(description: 'A long text')
451
+ # Read a type's metadata
427
452
  type.metadata[:description] # 'A long text'
428
453
  ```
429
454
 
430
455
  `#metadata` combines keys from type compositions.
431
456
 
432
457
  ```ruby
433
- type = Types::String.meta(description: 'A long text') >> Types::String.match(/@/).meta(note: 'An email address')
458
+ type = Types::String.metadata(description: 'A long text') >> Types::String.match(/@/).metadata(note: 'An email address')
434
459
  type.metadata[:description] # 'A long text'
435
460
  type.metadata[:note] # 'An email address'
436
461
  ```
@@ -614,11 +639,9 @@ intersection = User & Employee # Hash[:name]
614
639
 
615
640
  Use `#tagged_by` to resolve what definition to use based on the value of a common key.
616
641
 
617
- Key used as index must be a `Types::Static`
618
-
619
642
  ```ruby
620
- NameUpdatedEvent = Types::Hash[type: Types::Static['name_updated'], name: Types::String]
621
- AgeUpdatedEvent = Types::Hash[type: Types::Static['age_updated'], age: Types::Integer]
643
+ NameUpdatedEvent = Types::Hash[type: 'name_updated', name: Types::String]
644
+ AgeUpdatedEvent = Types::Hash[type: 'age_updated', age: Types::Integer]
622
645
 
623
646
  Events = Types::Hash.tagged_by(
624
647
  :type,
@@ -664,7 +687,7 @@ InputHandler.parse(price: 100_000, name: 'iPhone 15', category: 'smartphones')
664
687
  The `#filtered` modifier returns a valid Hash with the subset of values that were valid, instead of failing the entire result if one or more values are invalid.
665
688
 
666
689
  ```ruby
667
- User = Types::Hash[name: String, age: Integer]
690
+ User = Types::Hash[name: String, age: Integer].filtered
668
691
  User.parse(name: 'Joe', age: 40) # => { name: 'Joe', age: 40 }
669
692
  User.parse(name: 'Joe', age: 'nope') # => { name: 'Joe' }
670
693
  ```
@@ -729,7 +752,7 @@ emails = Types::Array[/@/]
729
752
  emails = Types::Array[Types::String[/@/]]
730
753
  ```
731
754
 
732
- Prefer the latter (`Types::Array[Types::String[/@/]]`), as that first validates that each element is a `String` before matching agains the regular expression.
755
+ Prefer the latter (`Types::Array[Types::String[/@/]]`), as that first validates that each element is a `String` before matching against the regular expression.
733
756
 
734
757
  #### Concurrent arrays
735
758
 
@@ -851,15 +874,201 @@ Str.parse(data).each do |row|
851
874
  end
852
875
  ```
853
876
 
854
- ### Plumb::Schema
877
+ ### Types::Data
855
878
 
856
- TODO
879
+ `Types::Data` provides a superclass to define **inmutable** structs or value objects with typed / coercible attributes.
857
880
 
858
- ### Plumb::Pipeline
881
+ #### `[]` Syntax
882
+
883
+ The `[]` syntax is a short-hand for struct definition.
884
+ Like `Plumb::Types::Hash`, suffixing a key with `?` makes it optional.
885
+
886
+ ```ruby
887
+ Person = Types::Data[name: String, age?: Integer]
888
+ person = Person.new(name: 'Jane')
889
+ ```
890
+
891
+ This syntax creates subclasses too.
892
+
893
+ ```ruby
894
+ # Subclass Person with and redefine the :age type.
895
+ Adult = Person[age?: Types::Integer[18..]]
896
+ ```
897
+
898
+ These classes can be instantiated normally, and expose `#valid?` and `#error`
899
+
900
+ ```ruby
901
+ person = Person.new(name: 'Joe')
902
+ person.name # 'Joe'
903
+ person.valid? # false
904
+ person.errors[:age] # 'must be an integer'
905
+ ```
906
+
907
+ #### `#with`
908
+
909
+ Note that these instances cannot be mutated (there's no attribute setters), but they can be copied with partial attributes with `#with`
910
+
911
+ ```ruby
912
+ another_person = person.with(age: 20)
913
+ ```
914
+
915
+ #### `.attribute` syntax
916
+
917
+ This syntax allows defining struct classes with typed attributes, including nested structs.
918
+
919
+ ```ruby
920
+ class Person < Types::Data
921
+ attribute :name, Types::String.present
922
+ attribute :age, Types::Integer
923
+ end
924
+ ```
925
+
926
+ It supports nested attributes:
927
+
928
+ ```ruby
929
+ class Person < Types::Data
930
+ attribute :friend do
931
+ attribute :name, String
932
+ end
933
+ end
934
+
935
+ person = Person.new(friend: { name: 'John' })
936
+ person.friend_count # 1
937
+ ```
938
+
939
+ Or arrays of nested attributes:
940
+
941
+ ```ruby
942
+ class Person < Types::Data
943
+ attribute :friends, Types::Array do
944
+ atrribute :name, String
945
+ end
946
+
947
+ # Custom methods like any other class
948
+ def friend_count = friends.size
949
+ end
950
+
951
+ person = Person.new(friends: [{ name: 'John' }])
952
+ ```
953
+
954
+ Or use struct classes defined separately:
955
+
956
+ ```ruby
957
+ class Company < Types::Data
958
+ attribute :name, String
959
+ end
960
+
961
+ class Person < Types::Data
962
+ # Single nested struct
963
+ attribute :company, Company
964
+
965
+ # Array of nested structs
966
+ attribute :companies, Types::Array[Company]
967
+ end
968
+ ```
969
+
970
+ Arrays and other types support composition and helpers. Ex. `#default`.
971
+
972
+ ```ruby
973
+ attribute :companies, Types::Array[Company].default([].freeze)
974
+ ```
975
+
976
+ Passing a named struct class AND a block will subclass the struct and extend it with new attributes:
977
+
978
+ ```ruby
979
+ attribute :company, Company do
980
+ attribute :address, String
981
+ end
982
+ ```
983
+
984
+ The same works with arrays:
985
+
986
+ ```ruby
987
+ attribute :companies, Types::Array[Company] do
988
+ attribute :address, String
989
+ end
990
+ ```
991
+
992
+ Note that this does NOT work with union'd or piped structs.
993
+
994
+ ```ruby
995
+ attribute :company, Company | Person do
996
+ ```
997
+
998
+ #### Optional Attributes
999
+ Using `attribute?` allows for optional attributes. If the attribute is not present, these attribute values will be `nil`
1000
+
1001
+ ```ruby
1002
+ attribute? :company, Company
1003
+ ```
1004
+
1005
+ #### Inheritance
1006
+ Data structs can inherit from other structs. This is useful for defining a base struct with common attributes.
1007
+
1008
+ ```ruby
1009
+ class BasePerson < Types::Data
1010
+ attribute :name, String
1011
+ end
1012
+
1013
+ class Person < BasePerson
1014
+ attribute :age, Integer
1015
+ end
1016
+ ```
1017
+
1018
+ #### Equality with `#==`
1019
+
1020
+ `#==` is implemented to compare attributes, recursively.
1021
+
1022
+ ```ruby
1023
+ person1 = Person.new(name: 'Joe', age: 20)
1024
+ person2 = Person.new(name: 'Joe', age: 20)
1025
+ person1 == person2 # true
1026
+ ```
1027
+
1028
+ #### Struct composition
1029
+
1030
+ `Types::Data` supports all the composition operators and helpers.
1031
+
1032
+ Note however that, once you wrap a struct in a composition, you can't instantiate it with `.new` anymore (but you can still use `#parse` or `#resolve` like any other Plumb type).
1033
+
1034
+ ```ruby
1035
+ Person = Types::Data[name: String]
1036
+ Animal = Types::Data[species: String]
1037
+ # Compose with |
1038
+ Being = Person | Animal
1039
+ Being.parse(name: 'Joe') # <Person [valid] name: 'Joe'>
1040
+
1041
+ # Compose with other types
1042
+ Beings = Types::Array[Person | Animal]
1043
+
1044
+ # Default
1045
+ Payload = Types::Hash[
1046
+ being: Being.default(Person.new(name: 'Joe Bloggs'))
1047
+ ]
1048
+ ```
1049
+
1050
+ #### Recursive struct definitions
1051
+
1052
+ You can use `#defer`. See [recursive types](#recursive-types).
1053
+
1054
+ ```ruby
1055
+ Person = Types::Data[
1056
+ name: String,
1057
+ friend?: Types::Any.defer { Person }
1058
+ ]
1059
+
1060
+ person = Person.new(name: 'Joe', friend: { name: 'Joan'})
1061
+ person.friend.name # 'joan'
1062
+ person.friend.friend # nil
1063
+ ```
1064
+
1065
+
1066
+
1067
+ ### Plumb::Schema
859
1068
 
860
1069
  TODO
861
1070
 
862
- ### Plumb::Struct
1071
+ ### Plumb::Pipeline
863
1072
 
864
1073
  TODO
865
1074
 
@@ -898,13 +1107,46 @@ LinkedList = Types::Hash[
898
1107
 
899
1108
  ### Custom types
900
1109
 
901
- Compose procs or lambdas directly
1110
+ Every Plumb type exposes the following one-method interface:
1111
+
1112
+ ```
1113
+ #call(Result::Valid) => Result::Valid | Result::Invalid
1114
+ ```
1115
+
1116
+ As long as an object implements this interface, it can be composed into Plumb workflows.
1117
+
1118
+ The `Result::Valid` class has helper methods `#valid(value) => Result::Valid` and `#invalid(errors:) => Result::Invalid` to facilitate returning valid or invalid values from your own steps.
1119
+
1120
+ #### Compose procs or lambdas directly
1121
+
1122
+ Piping any `#call` object onto Plumb types will wrap your object in a `Plumb::Step` with all methods necessary for further composition.
902
1123
 
903
1124
  ```ruby
904
1125
  Greeting = Types::String >> ->(result) { result.valid("Hello #{result.value}") }
905
1126
  ```
906
1127
 
907
- or a custom class that responds to `#call(Result::Valid) => Result::Valid | Result::Invalid`
1128
+ #### Wrap a `#call` object in `Plumb::Step` explicitely
1129
+
1130
+ You can also wrap a proc in `Plumb::Step` explicitly.
1131
+
1132
+ ```ruby
1133
+ Greeting = Plumb::Step.new do |result|
1134
+ result.valid("Hello #{result.value}")
1135
+ end
1136
+ ```
1137
+
1138
+ Note that this example is not prefixed by `Types::String`, so it doesn't first validate that the input is indeed a string.
1139
+
1140
+ However, this means that `Greeting` is a `Plumb::Step` which comes with all the Plumb methods and policies.
1141
+
1142
+ ```ruby
1143
+ # Greeting responds to #>>, #|, #default, #transform, etc etc
1144
+ LoudGreeting = Greeting.default('no greeting').invoke(:upcase)
1145
+ ```
1146
+
1147
+ #### A custom `#call` class
1148
+
1149
+ Or write a custom class that responds to `#call(Result::Valid) => Result::Valid | Result::Invalid`
908
1150
 
909
1151
  ```ruby
910
1152
  class Greeting
@@ -912,6 +1154,9 @@ class Greeting
912
1154
  @gr = gr
913
1155
  end
914
1156
 
1157
+ # The Plumb Step interface
1158
+ # @param result [Plumb::Result::Valid]
1159
+ # @return [Plumb::Result::Valid, Plumb::Result::Invalid]
915
1160
  def call(result)
916
1161
  result.valid("#{gr} #{result.value}")
917
1162
  end
@@ -920,6 +1165,55 @@ end
920
1165
  MyType = Types::String >> Greeting.new('Hola')
921
1166
  ```
922
1167
 
1168
+ This is useful when you want to parameterize your custom steps, for example by initialising them with arguments like the example above.
1169
+
1170
+ #### Include `Plumb::Composable` to make instance of a class full "steps"
1171
+
1172
+ The class above will be wrapped by `Plumb::Step` when piped into other steps, but it doesn't support Plumb methods on its own.
1173
+
1174
+ Including `Plumb::Composable` makes it support all Plumb methods directly.
1175
+
1176
+ ```ruby
1177
+ class Greeting
1178
+ # This module mixes in Plumb methods such as #>>, #|, #default, #[],
1179
+ # #transform, #policy, etc etc
1180
+ include Plumb::Composable
1181
+
1182
+ def initialize(gr = 'Hello')
1183
+ @gr = gr
1184
+ end
1185
+
1186
+ # The Step interface
1187
+ def call(result)
1188
+ result.valid("#{gr} #{result.value}")
1189
+ end
1190
+
1191
+ # This is optional, but it allows you to control your object's #inspect
1192
+ private def _inspect = "Greeting[#{@gr}]"
1193
+ end
1194
+ ```
1195
+
1196
+ Now you can use your class as a composition starting point directly.
1197
+
1198
+ ```ruby
1199
+ LoudGreeting = Greeting.new('Hola').default('no greeting').invoke(:upcase)
1200
+ ```
1201
+
1202
+ #### Extend a class with `Plumb::Composable` to make the class itself a composable step.
1203
+
1204
+ ```ruby
1205
+ class User
1206
+ extend Composable
1207
+
1208
+ def self.class(result)
1209
+ # do something here. Perhaps returning a Result with an instance of this class
1210
+ return result.valid(new)
1211
+ end
1212
+ end
1213
+ ```
1214
+
1215
+ This is how [Plumb::Types::Data](#typesdata) is implemented.
1216
+
923
1217
  ### Custom policies
924
1218
 
925
1219
  `Plumb.policy` can be used to encapsulate common type compositions, or compositions that can be configurable by parameters.
@@ -946,8 +1240,6 @@ The `#policy` helper supports applying multiply policies.
946
1240
  Types::String.policy(default_if_nil: 'nothing here', size: (10..20))
947
1241
  ```
948
1242
 
949
-
950
-
951
1243
  #### Policies as helper methods
952
1244
 
953
1245
  Use the `helper: true` option to register the policy as a method you can call on types directly.
@@ -963,9 +1255,21 @@ StringWithDefault = Types::String.default_if_nil('nothing here')
963
1255
 
964
1256
  Many built-in helpers such as `#default` and `#options` are implemented as policies. This means that you can overwrite their default behaviour by defining a policy with the same name (use with caution!).
965
1257
 
1258
+ This other example adds a boolean to type metadata.
1259
+
1260
+ ```ruby
1261
+ Plumb.policy :admin, helper: true do |type|
1262
+ type.metadata(admin: true)
1263
+ end
1264
+
1265
+ # Usage: annotate fields in a schema
1266
+ AccountName = Types::String.admin
1267
+ AccountName.metadata # => { type: String, admin: true }
1268
+ ```
1269
+
966
1270
  #### Type-specific policies
967
1271
 
968
- You can use the `for_type:` option to define policies that only apply to steps that output certain types. This example only applies for types that return `Integer` values.
1272
+ You can use the `for_type:` option to define policies that only apply to steps that output certain types. This example is only applicable for types that return `Integer` values.
969
1273
 
970
1274
  ```ruby
971
1275
  Plumb.policy :multiply_by, for_type: Integer, helper: true do |type, factor|
@@ -975,7 +1279,7 @@ end
975
1279
  Doubled = Types::Integer.multiply_by(2)
976
1280
  Doubled.parse(2) # 4
977
1281
 
978
- # Tryin to apply this policy to a non Integer will raise an exception
1282
+ # Trying to apply this policy to a non Integer will raise an exception
979
1283
  DoubledString = Types::String.multiply_by(2) # raises error
980
1284
  ```
981
1285
 
@@ -997,7 +1301,7 @@ DoubledMoney = Types::Any[Money].multiply_by(2)
997
1301
 
998
1302
  #### Self-contained policy modules
999
1303
 
1000
- You can register a module, class or module with a three-method interface as a policy. This is so that policies can have their own namespace if they need local constants or private methods. For example, this is how the `:split` policy for strings is defined.
1304
+ You can register a module, class or object with a three-method interface as a policy. This is so that policies can have their own namespace if they need local constants or private methods. For example, this is how the `:split` policy for strings is defined.
1001
1305
 
1002
1306
  ```ruby
1003
1307
  module SplitPolicy
@@ -1016,6 +1320,21 @@ Plumb.policy :split, SplitPolicy
1016
1320
 
1017
1321
  ### JSON Schema
1018
1322
 
1323
+ Plumb ships with a JSON schema visitor that compiles a type composition into a JSON Schema Hash. All Plumb types support a `#to_json_schema` method.
1324
+
1325
+ ```ruby
1326
+ Payload = Types::Hash[name: String]
1327
+ Payload.to_json_schema(root: true)
1328
+ # {
1329
+ # "$schema"=>"https://json-schema.org/draft-08/schema#",
1330
+ # "type"=>"object",
1331
+ # "properties"=>{"name"=>{"type"=>"string"}},
1332
+ # "required"=>["name"]
1333
+ # }
1334
+ ```
1335
+
1336
+ The visitor can be used directly, too.
1337
+
1019
1338
  ```ruby
1020
1339
  User = Types::Hash[
1021
1340
  name: Types::String,
@@ -1035,7 +1354,7 @@ json_schema = Plumb::JSONSchemaVisitor.call(User)
1035
1354
  }
1036
1355
  ```
1037
1356
 
1038
- The built-in JSON Schema generator handles most standard types and compositions. You can add or override handles on a per-type basis with:
1357
+ The built-in JSON Schema generator handles most standard types and compositions. You can add or override handlers on a per-type basis with:
1039
1358
 
1040
1359
  ```ruby
1041
1360
  Plumb::JSONSchemaVisitor.on(:not) do |node, props|
@@ -1047,11 +1366,31 @@ type = Types::Decimal.not
1047
1366
  schema = Plumb::JSONSchemaVisitor.visit(type) # { 'not' => { 'type' => 'number' } }
1048
1367
  ```
1049
1368
 
1050
- #### JSON Schema handlers for custom policies
1369
+ You can also register custom classes or types that are wrapped by Plumb steps.
1370
+
1371
+ ```ruby
1372
+ module Types
1373
+ DateTime = Any[::DateTime]
1374
+ end
1375
+
1376
+ Plumb::JSONSchemaVisitor.on(::DateTime) do |node, props|
1377
+ props.merge('type' => 'string', 'format' => 'date-time')
1378
+ end
1379
+
1380
+ Types::DateTime.to_json_schema
1381
+ # {"type"=>"string", "format"=>"date-time"}
1382
+ ```
1383
+
1051
1384
 
1052
- TODO. See `Plumb::JSONSchemaVisitor`.
1053
1385
 
1386
+ ## TODO:
1054
1387
 
1388
+ - [ ] benchmarks and performace. Compare with `Parametric`, `ActiveModel::Attributes`, `ActionController::StrongParameters`
1389
+ - [ ] flesh out `Plumb::Schema`
1390
+ - [x] `Plumb::Struct`
1391
+ - [ ] flesh out and document `Plumb::Pipeline`
1392
+ - [ ] document custom visitors
1393
+ - [ ] Improve errors, support I18n ?
1055
1394
 
1056
1395
  ## Development
1057
1396