plumb 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
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