sorbet-schema 0.4.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.standard.yml +2 -1
  3. data/CHANGELOG.md +16 -0
  4. data/Gemfile +1 -0
  5. data/Gemfile.lock +45 -44
  6. data/README.md +230 -7
  7. data/lib/sorbet-schema/t/struct.rb +38 -0
  8. data/lib/sorbet-schema/version.rb +1 -1
  9. data/lib/sorbet-schema.rb +7 -1
  10. data/lib/typed/serializer.rb +1 -1
  11. data/sorbet/rbi/gems/ansi@1.5.0.rbi +1 -0
  12. data/sorbet/rbi/gems/ast@2.4.2.rbi +1 -0
  13. data/sorbet/rbi/gems/bigdecimal@3.1.8.rbi +9 -0
  14. data/sorbet/rbi/gems/builder@3.2.4.rbi +1 -0
  15. data/sorbet/rbi/gems/erubi@1.12.0.rbi +1 -0
  16. data/sorbet/rbi/gems/io-console@0.7.2.rbi +1 -0
  17. data/sorbet/rbi/gems/{json@2.7.1.rbi → json@2.7.2.rbi} +77 -68
  18. data/sorbet/rbi/gems/language_server-protocol@3.17.0.3.rbi +1 -0
  19. data/sorbet/rbi/gems/lint_roller@1.1.0.rbi +1 -0
  20. data/sorbet/rbi/gems/minitest-focus@1.4.0.rbi +14 -13
  21. data/sorbet/rbi/gems/minitest-reporters@1.6.1.rbi +27 -26
  22. data/sorbet/rbi/gems/{minitest@5.22.3.rbi → minitest@5.23.1.rbi} +160 -141
  23. data/sorbet/rbi/gems/netrc@0.11.0.rbi +1 -0
  24. data/sorbet/rbi/gems/parallel@1.24.0.rbi +1 -0
  25. data/sorbet/rbi/gems/{parser@3.3.0.5.rbi → parser@3.3.1.0.rbi} +233 -186
  26. data/sorbet/rbi/gems/{prism@0.24.0.rbi → prism@0.29.0.rbi} +19135 -12188
  27. data/sorbet/rbi/gems/psych@5.1.2.rbi +1 -0
  28. data/sorbet/rbi/gems/{racc@1.7.3.rbi → racc@1.8.0.rbi} +38 -33
  29. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +1 -0
  30. data/sorbet/rbi/gems/{rake@13.1.0.rbi → rake@13.2.1.rbi} +56 -55
  31. data/sorbet/rbi/gems/{rbi@0.1.9.rbi → rbi@0.1.13.rbi} +226 -154
  32. data/sorbet/rbi/gems/{regexp_parser@2.9.0.rbi → regexp_parser@2.9.2.rbi} +3 -2
  33. data/sorbet/rbi/gems/{reline@0.4.3.rbi → reline@0.5.7.rbi} +1 -0
  34. data/sorbet/rbi/gems/{rexml@3.2.6.rbi → rexml@3.2.8.rbi} +121 -108
  35. data/sorbet/rbi/gems/{rubocop-ast@1.31.2.rbi → rubocop-ast@1.31.3.rbi} +4 -6
  36. data/sorbet/rbi/gems/{rubocop-performance@1.20.2.rbi → rubocop-performance@1.21.0.rbi} +1 -0
  37. data/sorbet/rbi/gems/rubocop-sorbet@0.7.8.rbi +1 -0
  38. data/sorbet/rbi/gems/{rubocop@1.62.1.rbi → rubocop@1.63.5.rbi} +612 -371
  39. data/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +1 -0
  40. data/sorbet/rbi/gems/sorbet-result@1.1.0.rbi +83 -82
  41. data/sorbet/rbi/gems/sorbet-struct-comparable@1.3.0.rbi +1 -0
  42. data/sorbet/rbi/gems/{spoom@1.2.4.rbi → spoom@1.3.2.rbi} +1057 -413
  43. data/sorbet/rbi/gems/standard-custom@1.0.2.rbi +1 -0
  44. data/sorbet/rbi/gems/{standard-performance@1.3.1.rbi → standard-performance@1.4.0.rbi} +1 -0
  45. data/sorbet/rbi/gems/standard-sorbet@0.0.2.rbi +1 -0
  46. data/sorbet/rbi/gems/{standard@1.35.1.rbi → standard@1.36.0.rbi} +61 -60
  47. data/sorbet/rbi/gems/stringio@3.1.0.rbi +1 -0
  48. data/sorbet/rbi/gems/strscan@3.1.0.rbi +9 -0
  49. data/sorbet/rbi/gems/{tapioca@0.12.0.rbi → tapioca@0.14.2.rbi} +148 -113
  50. data/sorbet/rbi/gems/thor@1.3.1.rbi +1 -0
  51. data/sorbet/rbi/gems/unicode-display_width@2.5.0.rbi +1 -0
  52. data/sorbet/rbi/gems/yard-sorbet@0.8.1.rbi +2 -1
  53. data/sorbet/rbi/gems/yard@0.9.36.rbi +1 -0
  54. data/sorbet/rbi/gems/{zeitwerk@2.6.13.rbi → zeitwerk@2.6.15.rbi} +47 -36
  55. metadata +24 -23
  56. data/sorbet/rbi/gems/prettier_print@1.2.1.rbi +0 -951
  57. data/sorbet/rbi/gems/syntax_tree@6.2.0.rbi +0 -23133
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3fc3ec89812a6517931d09a61a6f4049b85db1661b92e5cb7353c7c62207afab
4
- data.tar.gz: 45daa92727807fb2c80bff1fad0b84684c435cde7c1460478741da74fcc0d9c3
3
+ metadata.gz: c06e03c3878ac6b3cf04305124e1d3786de57f0543ee320181565a8b59f8c4b3
4
+ data.tar.gz: 97d11102e65875b5f77f06095fdacf1dea0fabdd1fd1a91b5428a509dd6bac4b
5
5
  SHA512:
6
- metadata.gz: 5ad688ac1f0b61c309d9e016baffcab03b61f6c1c0b17b86721580d9f9ed75b86901f9c31a2b3407356f3abaf0ed64c75294d8a6a60a39d11220320c80253308
7
- data.tar.gz: d4ec8208184dfe939122d345ec1e1cbb2a8b3f9d840af984e772c6c85d7985fe34f5678ba6b2fa7ef9f85157af90c4e5990f1f2472a025c195255f0d9a5527fe
6
+ metadata.gz: 84e7f4308162784ee84b063ff743293cd6c72487479f23de3cf1ed31089f13ff8a148c4aad85854394343aa97eafbbab15551d54fd17ce8e390db343db5083c9
7
+ data.tar.gz: 37301dfecd29623d2f5be76220e70d0aa1a650dc2c792c3e01e15b4bce17abb2bf468f04dbfd5026686c11aa594590a91b369b49ef2a1de5393ff77af07d5d8b
data/.standard.yml CHANGED
@@ -1,5 +1,6 @@
1
1
  parallel: true
2
- ruby_version: 3.0
2
+ format: progress
3
+ ruby_version: 3.1
3
4
  ignore:
4
5
  - 'vendor/**/*'
5
6
  plugins:
data/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.5.0](https://github.com/maxveldink/sorbet-schema/compare/v0.4.2...v0.5.0) (2024-04-19)
8
+
9
+
10
+ ### ⚠ BREAKING CHANGES
11
+
12
+ * Set minimum Ruby to 3.1 ([#69](https://github.com/maxveldink/sorbet-schema/issues/69))
13
+
14
+ ### Features
15
+
16
+ * Add schema method to structs ([#60](https://github.com/maxveldink/sorbet-schema/issues/60)) ([4b7ff34](https://github.com/maxveldink/sorbet-schema/commit/4b7ff34bc6a48c42d3ece8d1fad07bdecf0bdc11))
17
+
18
+
19
+ ### Miscellaneous Chores
20
+
21
+ * Set minimum Ruby to 3.1 ([#69](https://github.com/maxveldink/sorbet-schema/issues/69)) ([d2b4ba2](https://github.com/maxveldink/sorbet-schema/commit/d2b4ba2099da24f93a22849e059627221bbda081))
22
+
7
23
  ## [0.4.2](https://github.com/maxveldink/sorbet-schema/compare/v0.4.1...v0.4.2) (2024-03-21)
8
24
 
9
25
 
data/Gemfile CHANGED
@@ -15,6 +15,7 @@ group :development do
15
15
  end
16
16
 
17
17
  group :development, :test do
18
+ gem "bigdecimal" # used for testing un-matched coercer
18
19
  gem "minitest"
19
20
  gem "minitest-focus"
20
21
  gem "minitest-reporters"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sorbet-schema (0.4.2)
4
+ sorbet-schema (0.5.0)
5
5
  sorbet-result (~> 1.1)
6
6
  sorbet-runtime (~> 0.5)
7
7
  sorbet-struct-comparable (~> 1.3)
@@ -12,19 +12,20 @@ GEM
12
12
  specs:
13
13
  ansi (1.5.0)
14
14
  ast (2.4.2)
15
+ bigdecimal (3.1.8)
15
16
  builder (3.2.4)
16
- debug (1.9.1)
17
+ debug (1.9.2)
17
18
  irb (~> 1.10)
18
19
  reline (>= 0.3.8)
19
20
  erubi (1.12.0)
20
21
  io-console (0.7.2)
21
- irb (1.12.0)
22
- rdoc
22
+ irb (1.13.1)
23
+ rdoc (>= 4.0.0)
23
24
  reline (>= 0.4.2)
24
- json (2.7.1)
25
+ json (2.7.2)
25
26
  language_server-protocol (3.17.0.3)
26
27
  lint_roller (1.1.0)
27
- minitest (5.22.3)
28
+ minitest (5.23.1)
28
29
  minitest-focus (1.4.0)
29
30
  minitest (>= 4, < 6)
30
31
  minitest-reporters (1.6.1)
@@ -34,26 +35,26 @@ GEM
34
35
  ruby-progressbar
35
36
  netrc (0.11.0)
36
37
  parallel (1.24.0)
37
- parser (3.3.0.5)
38
+ parser (3.3.1.0)
38
39
  ast (~> 2.4.1)
39
40
  racc
40
- prettier_print (1.2.1)
41
- prism (0.24.0)
41
+ prism (0.29.0)
42
42
  psych (5.1.2)
43
43
  stringio
44
- racc (1.7.3)
44
+ racc (1.8.0)
45
45
  rainbow (3.1.1)
46
- rake (13.1.0)
47
- rbi (0.1.9)
48
- prism (>= 0.18.0, < 0.25)
46
+ rake (13.2.1)
47
+ rbi (0.1.13)
48
+ prism (>= 0.18.0, < 1.0.0)
49
49
  sorbet-runtime (>= 0.5.9204)
50
- rdoc (6.6.2)
50
+ rdoc (6.7.0)
51
51
  psych (>= 4.0.0)
52
- regexp_parser (2.9.0)
53
- reline (0.4.3)
52
+ regexp_parser (2.9.2)
53
+ reline (0.5.7)
54
54
  io-console (~> 0.5)
55
- rexml (3.2.6)
56
- rubocop (1.62.1)
55
+ rexml (3.2.8)
56
+ strscan (>= 3.0.9)
57
+ rubocop (1.63.5)
57
58
  json (~> 2.3)
58
59
  language_server-protocol (>= 3.17.0)
59
60
  parallel (~> 1.10)
@@ -64,56 +65,55 @@ GEM
64
65
  rubocop-ast (>= 1.31.1, < 2.0)
65
66
  ruby-progressbar (~> 1.7)
66
67
  unicode-display_width (>= 2.4.0, < 3.0)
67
- rubocop-ast (1.31.2)
68
- parser (>= 3.3.0.4)
69
- rubocop-performance (1.20.2)
68
+ rubocop-ast (1.31.3)
69
+ parser (>= 3.3.1.0)
70
+ rubocop-performance (1.21.0)
70
71
  rubocop (>= 1.48.1, < 2.0)
71
- rubocop-ast (>= 1.30.0, < 2.0)
72
+ rubocop-ast (>= 1.31.1, < 2.0)
72
73
  rubocop-sorbet (0.7.8)
73
74
  rubocop (>= 0.90.0)
74
75
  ruby-progressbar (1.13.0)
75
- sorbet (0.5.11295)
76
- sorbet-static (= 0.5.11295)
76
+ sorbet (0.5.11391)
77
+ sorbet-static (= 0.5.11391)
77
78
  sorbet-result (1.1.0)
78
79
  sorbet-runtime (~> 0.5)
79
- sorbet-runtime (0.5.11295)
80
- sorbet-static (0.5.11295-universal-darwin)
81
- sorbet-static (0.5.11295-x86_64-linux)
82
- sorbet-static-and-runtime (0.5.11295)
83
- sorbet (= 0.5.11295)
84
- sorbet-runtime (= 0.5.11295)
80
+ sorbet-runtime (0.5.11391)
81
+ sorbet-static (0.5.11391-universal-darwin)
82
+ sorbet-static (0.5.11391-x86_64-linux)
83
+ sorbet-static-and-runtime (0.5.11391)
84
+ sorbet (= 0.5.11391)
85
+ sorbet-runtime (= 0.5.11391)
85
86
  sorbet-struct-comparable (1.3.0)
86
87
  sorbet-runtime (>= 0.5)
87
- spoom (1.2.4)
88
+ spoom (1.3.2)
88
89
  erubi (>= 1.10.0)
90
+ prism (>= 0.19.0)
89
91
  sorbet-static-and-runtime (>= 0.5.10187)
90
- syntax_tree (>= 6.1.1)
91
92
  thor (>= 0.19.2)
92
- standard (1.35.1)
93
+ standard (1.36.0)
93
94
  language_server-protocol (~> 3.17.0.2)
94
95
  lint_roller (~> 1.0)
95
- rubocop (~> 1.62.0)
96
+ rubocop (~> 1.63.0)
96
97
  standard-custom (~> 1.0.0)
97
- standard-performance (~> 1.3)
98
+ standard-performance (~> 1.4)
98
99
  standard-custom (1.0.2)
99
100
  lint_roller (~> 1.0)
100
101
  rubocop (~> 1.50)
101
- standard-performance (1.3.1)
102
+ standard-performance (1.4.0)
102
103
  lint_roller (~> 1.1)
103
- rubocop-performance (~> 1.20.2)
104
+ rubocop-performance (~> 1.21.0)
104
105
  standard-sorbet (0.0.2)
105
106
  lint_roller (~> 1.1)
106
107
  rubocop-sorbet (~> 0.7.0)
107
108
  stringio (3.1.0)
108
- syntax_tree (6.2.0)
109
- prettier_print (>= 1.2.0)
110
- tapioca (0.12.0)
109
+ strscan (3.1.0)
110
+ tapioca (0.14.2)
111
111
  bundler (>= 2.2.25)
112
112
  netrc (>= 0.11.0)
113
113
  parallel (>= 1.21.0)
114
114
  rbi (>= 0.1.4, < 0.2)
115
- sorbet-static-and-runtime (>= 0.5.10820)
116
- spoom (~> 1.2.0, >= 1.2.0)
115
+ sorbet-static-and-runtime (>= 0.5.11087)
116
+ spoom (>= 1.2.0)
117
117
  thor (>= 1.2.0)
118
118
  yard-sorbet
119
119
  thor (1.3.1)
@@ -122,7 +122,7 @@ GEM
122
122
  yard-sorbet (0.8.1)
123
123
  sorbet-runtime (>= 0.5)
124
124
  yard (>= 0.9)
125
- zeitwerk (2.6.13)
125
+ zeitwerk (2.6.15)
126
126
 
127
127
  PLATFORMS
128
128
  arm64-darwin-22
@@ -130,6 +130,7 @@ PLATFORMS
130
130
  x86_64-linux
131
131
 
132
132
  DEPENDENCIES
133
+ bigdecimal
133
134
  debug
134
135
  minitest
135
136
  minitest-focus
@@ -144,4 +145,4 @@ DEPENDENCIES
144
145
  tapioca
145
146
 
146
147
  BUNDLED WITH
147
- 2.5.6
148
+ 2.5.9
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Sorbet Schema
2
2
 
3
-
3
+ Extendable serialization and deserialization to various formats for Sorbet `T::Struct`s.
4
4
 
5
5
  ## Installation
6
6
 
@@ -14,21 +14,244 @@ If bundler is not being used to manage dependencies, install the gem by executin
14
14
 
15
15
  ## Usage
16
16
 
17
+ Sorbet Schema is designed to be compatible with Sorbet's `T::Struct` class, and seeks to update many of the common pitfalls developers encountering when deserializing to and serializing from a `T::Struct`.
18
+
17
19
  ### Getting Started
18
20
 
19
- ### Chaining
21
+ While you can directly define a `Typed::Schema` to be used for your serialization needs, you'll typically use the provided helper class method to generate a `Schema` from an existing `T::Struct`.
22
+
23
+ ```ruby
24
+ class Person < T::Struct
25
+ const :name, String
26
+ const :age, Integer
27
+ end
28
+
29
+ schema = Person.schema # => <Typed::Schema
30
+ # fields=[....]
31
+ # target=Person>
32
+ ```
33
+
34
+ Once you have a schema, you can use the built-in serializers (or a [custom one](#implementing-custom-serializers) that inherits from the [Typed::Serializer](https://github.com/maxveldink/sorbet-schema/blob/main/lib/typed/serializer.rb) abstract base class) to create new instances of the struct or convert an instance of the struct to the target format.
35
+
36
+ ```ruby
37
+ json_serializer = Typed::JSONSerializer.new(schema: Person.schema)
38
+
39
+ # Deserialize from target format
40
+ result = json_serializer.deserialize('{"name":"Max","age":29}')
41
+ max = result.payload # == Person.new(name: "Max", age: 29)
42
+
43
+ result = json_serializer.serialize(max)
44
+ result.payload # == '{"name":"Max","age":29}'
45
+ ```
46
+
47
+ Alternatively, you can use the built-in helper methods added to `T::Struct`s to quickly use the built-in serializers.
48
+
49
+ ```ruby
50
+ result = Person.deserialize_from(:json, '{"name":"Max","age":29}')
51
+ max = result.payload # == Person.new(name: "Max", age: 29)
52
+
53
+ result = max.serialize_to(:json)
54
+ result.payload # == '{"name":"Max","age":29}'
55
+ ```
56
+
57
+ Notice that both `deserialize` and `serialize` return `Typed::Result`s (from the [sorbet-result gem](https://github.com/maxveldink/sorbet-result)) that need to be checked for success or failure before being used. Check out that gem's README for more information on how to interact with `Result`s.
58
+
59
+ One benefit of using `Result`s is we can add much more details information about why a format is unsuccessfully deserialized or serialized, to provide call sites with more information for error handling, messaging and formatting.
60
+
61
+ ```ruby
62
+ # Unparsable JSON
63
+ result = json_serializer.deserialize('{"name""Max","age":29}')
64
+ result.error # == Typed::ParseError: json could not be parsed. Check for typos.
65
+
66
+ # Missing required field
67
+ result = json_serializer.deserialize('{"age": 29}')
68
+ result.error # == Typed::Validations::RequiredFieldError: name is required.
69
+
70
+ result = json_serializer.deserialize('{"age":"29-0"}')
71
+ result.error # == Typed::Validations::MultipleValidationError: Multiple validation errors found: name is required. | '29-0' cannot be coerced into Integer.
72
+ ```
73
+
74
+ Finally, there are built-in coercers that do their best effort to convert common types from the source format to the required schema type.
75
+
76
+ ```ruby
77
+ # Deserialize from target format, with integer coercion
78
+ result = json_serializer.deserialize('{"name":"Max","age":"29"}')
79
+ max = result.payload # == Person.new(name: "Max", age: 29)
80
+ ```
81
+
82
+ ### Rails Example
83
+
84
+ Here's an extended example of how Sorbet Schema can be combined with a normal Rails request to easily convert between formats.
85
+
86
+ ```ruby
87
+ def verify
88
+ Typed::HashSerializer
89
+ .new(schema: Address.schema) # Generate schema from the `Address` Struct
90
+ .deserialize(address_params.to_h) # Use Rails' strong parameters to deserialize into the struct
91
+ .and_then { |address| VerifyAddress.new.call(address: T.cast(address, Address)) } # Use sorbet-result's chaining
92
+ .and_then do |address|
93
+ return render json: Typed::JSONSerializer.new(schema: Address.schema).serialize(address).payload # return a JSON response from the Address struct instance
94
+ end
95
+ .on_error do |failure| # Use sorbet-result's error handling
96
+ case failure
97
+ when AddressNotFoundError
98
+ head :not_found
99
+ when GeoNotSupportedError
100
+ head :not_implemented
101
+ else
102
+ render json: failure, status: :bad_request # use `Typed::Failure`s built-in `to_json` behavior
103
+ end
104
+ end
105
+ end
106
+ ```
107
+
108
+ ### Available Serializers
109
+
110
+ These are the currently available serializers. For more information about implementing a custom one (or contributing one back!), see [Custom Coercers](#custom-coercers).
111
+
112
+ #### JSONSerializer
113
+
114
+ See [Getting Started](#getting-started) for more information on how to use the JSONSerializer.
115
+
116
+ #### HashSerializer
117
+
118
+ While not strictly serialization, converting `T::Struct`s to and from Ruby `Hash`es has traditionally had many pitfalls ([well-documented](https://sorbet.org/docs/tstruct#legacy-code-and-historical-context) in the Sorbet docs). The `Typed::HashSerializer` aims to address several common issues, while providing the same `Result` handling for invalid or missing data and coercion behavior.
119
+
120
+ To use it, simply instantiate and use it like the `JSONSerializer`:
121
+
122
+ ```ruby
123
+ hash_serializer = Typed::HashSerializer.new(schema: Person.schema)
124
+
125
+ # Deserialize from target format
126
+ result = hash_serializer.deserialize({"name" => "Max", age: 29})
127
+ max = result.payload # == Person.new(name: "Max", age: 29)
128
+ ```
129
+
130
+ By default, the `HashSerializer` will _not_ serialize values when converting to a Hash. For instance, if a field is an `T::Enum` type, when it is serialized to a `Hash` the value will be the `Enum` and not the `String` representation. The `should_serialize_values` option can be passed during initialization to serialize the values when converting to the `Hash`.
131
+
132
+ ### Customization
133
+
134
+ From the get-go, Sorbet Schema is designed to be extensible to model more complex data validation requirements and many serialization formats. We try out best to include built-in, battle-tested coercers and serializers from real world use cases and would love to see/upstream any customizations that the community have found useful!
135
+
136
+ #### Custom Coercers
137
+
138
+ At their simplest forms, coercers are any class that inherit from the [Typed::Coercion::Coercer](https://github.com/maxveldink/sorbet-schema/blob/main/lib/typed/coercion/coercer.rb) abstract base class. The list of default coercers that are applied can be found in the [CoercerRegistry](https://github.com/maxveldink/sorbet-schema/blob/main/lib/typed/coercion/coercer_registry.rb). Let's look at the [DateCoercer's implementation](https://github.com/maxveldink/sorbet-schema/blob/main/lib/typed/coercion/date_coercer.rb):
139
+
140
+ ```ruby
141
+ require "date"
142
+
143
+ class DateCoercer < Coercer
144
+ extend T::Generic
145
+
146
+ Target = type_member { {fixed: Date} }
147
+
148
+ sig { override.params(type: T::Types::Base).returns(T::Boolean) }
149
+ def used_for_type?(type)
150
+ T::Utils.coerce(type) == T::Utils.coerce(Date)
151
+ end
152
+
153
+ sig { override.params(type: T::Types::Base, value: Value).returns(Result[Target, CoercionError]) }
154
+ def coerce(type:, value:)
155
+ return Failure.new(CoercionError.new("Type must be a Date.")) unless used_for_type?(type)
156
+
157
+ return Success.new(value) if value.is_a?(Date)
158
+
159
+ Success.new(Date.parse(value))
160
+ rescue Date::Error, TypeError
161
+ Failure.new(CoercionError.new("'#{value}' cannot be coerced into Date."))
162
+ end
163
+ end
164
+ ```
165
+
166
+ Notice that this utilizes sorbet generic, so the target type must be defined using `type_member`. For dates, this is the built-in std lib type `Date`.
167
+
168
+ From there, implement the `used_for_type?` method which receives a type and returns `true` if the coercer can be used to coerce to that type or `false` if it should not be used. Notice that we use the `T::Types` module directly from Sorbet, which allows us to model the built-in Sorbet types, such as `T::Boolean` and `T::Array`. Typically, `T::Utils.coerce(TargetType)` is used to match the target type. For dates, this is a very simple type check for a `Date`.
169
+
170
+ Finally, implement the `coerce` method. If a coercion is successful, return a `Success.new(coerced_value)`. If not, return a Failure with a coercion error `Failure.new(CoercionError.new("I can't coerce to the type"))`. Take care to handle any exceptions that could arise from the attempted coercion. For dates, first it checks and make sure the type given matches the target type. This is a common check and is largely an edge case check for completeness. Next, if the value is already a Date we simply return a `Success` with it. Finally, we use the built-in `Date.parse` method to actually attempt a coercion. Since this can throw a `Date::Error` and a `TypeError`, rescue from those with a `Failure`.
171
+
172
+ Once a custom coercer is defined, the last step is to register it with Sorbet Schema during initialization. Typically, this is after `sorbet-schema` has been required or during the bootstrapping step of a framework, such as Rails' initializers. Call `register_coercer` like so:
173
+
174
+ ```ruby
175
+ Typed::Coercion.register_coercer(MyCoercer) # make sure `MyCoercer` is loaded by this point
176
+ ```
177
+
178
+ **Note** Custom coercers are prepended to the list of available coercers so that they are checked during deserialization before the built-in coercers. This allows consuming projects to override default behavior by creating a coercer that re-implements the `coerce` method for that type.
179
+
180
+ #### Inline Serializers
181
+
182
+ Sometimes, there is custom behavior that needs to be added to how a field is serialized (represented as a `String`), such as when you need to use a different `strftime` format for `Date`s and `Time`s. This can be accomplished with an `InlineSerializer` (defined in [Typed::Field](https://github.com/maxveldink/sorbet-schema/blob/main/lib/typed/field.rb)), which is a `Proc` that takes the value and returns a different representation. At present, these are both very loose `T.untyped` types to allow for flexibility. Typically, a `String` is returned.
183
+
184
+ The serializer can be used when creating a `Schema` and defining its `fields`, or with the `add_serializer` helper on `Schema`s.
185
+
186
+ ```ruby
187
+ my_date_serializer = ->(date) { date.strftime("%Y/%m") }
188
+
189
+ # use directly on a Schema
190
+ Typed::Schema.new(
191
+ target: SchemaWithDateField,
192
+ fields: [
193
+ Typed::Field.new(name: :date, type: Date, serializer: my_date_serializer)
194
+ ]
195
+ )
196
+
197
+ # use `add_serializer` helper
198
+ SchemaWithDateField.schema.add_serializer(:date, my_date_serializer)
199
+ ```
200
+
201
+ #### Implementing Custom Serializers
202
+
203
+ While Sorbet Schema ships with popular serializers, you can define your own by inheriting from [Typed::Serializer](https://github.com/maxveldink/sorbet-schema/blob/main/lib/typed/serializer.rb). Let's look at the `JSONSerializer`:
204
+
205
+ ```ruby
206
+ require "json"
207
+
208
+ class JSONSerializer < Serializer
209
+ Input = type_member { {fixed: String} }
210
+ Output = type_member { {fixed: String} }
211
+
212
+ sig { override.params(source: Input).returns(Result[T::Struct, DeserializeError]) }
213
+ def deserialize(source)
214
+ parsed_json = JSON.parse(source)
215
+
216
+ creation_params = schema.fields.each_with_object(T.let({}, Params)) do |field, hsh|
217
+ hsh[field.name] = parsed_json[field.name.to_s]
218
+ end
219
+
220
+ deserialize_from_creation_params(creation_params)
221
+ rescue JSON::ParserError
222
+ Failure.new(ParseError.new(format: :json))
223
+ end
224
+
225
+ sig { override.params(struct: T::Struct).returns(Result[Output, SerializeError]) }
226
+ def serialize(struct)
227
+ return Failure.new(SerializeError.new("'#{struct.class}' cannot be serialized to target type of '#{schema.target}'.")) if struct.class != schema.target
228
+
229
+ Success.new(JSON.generate(serialize_from_struct(struct: struct, should_serialize_values: true)))
230
+ end
231
+ end
232
+ ```
233
+
234
+ Since `Serializer` is a generic class, we need to define our `Input` and `Output` types. For JSON, deserialization and serialization both use JSON strings, so these are both strings.
235
+
236
+ Next, the `deserialize` and `serialize` methods must be implemented. Notice that both of these return `Result`s.
237
+
238
+ For deserialization, the JSON is parsed (and a parse error is handled). Then we build up a creation params hash from the parsed json to pass to the `deserialize_from_creation_params` helper, defined on `Serializer`.
239
+
240
+ For serialization, the passed struct is checked to make sure it matches the `Schema`. Then it uses the `serialize_from_struct` helper and passes the resulting `Hash` to generate JSON.
241
+
242
+ ## Inspirations
20
243
 
21
- ## Why use Options?
244
+ This project is heavily inspired by [serde](https://serde.rs/) from the Rust community and the [dry-rb](https://dry-rb.org/) family of gems.
22
245
 
23
246
  ## Development
24
247
 
25
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake` to run Rubocop and the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
248
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake` to run Standard and the tests. `bin/console` for an interactive prompt that aids with experimentation.
26
249
 
27
- To install this gem onto your local machine, run `bundle exec rake install`.
250
+ To install this gem onto a local machine, run `bundle exec rake install`.
28
251
 
29
252
  ## Contributing
30
253
 
31
- Bug reports and pull requests are welcome on GitHub at https://github.com/maxveldink/sorbet-option. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/maxveldink/sorbet-option/blob/master/CODE_OF_CONDUCT.md).
254
+ Bug reports and pull requests are welcome on GitHub at https://github.com/maxveldink/sorbet-schema. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/maxveldink/sorbet-schema/blob/master/CODE_OF_CONDUCT.md).
32
255
 
33
256
  ## License
34
257
 
@@ -36,7 +259,7 @@ The gem is available as open source under the terms of the [MIT License](https:/
36
259
 
37
260
  ## Code of Conduct
38
261
 
39
- Everyone interacting in this project's codebase, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/maxveldink/sorbet-option/blob/master/CODE_OF_CONDUCT.md).
262
+ Everyone interacting in this project's codebase, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/maxveldink/sorbet-schema/blob/master/CODE_OF_CONDUCT.md).
40
263
 
41
264
  ## Sponsorships
42
265
 
@@ -0,0 +1,38 @@
1
+ # typed: strict
2
+
3
+ module T
4
+ class Struct
5
+ extend T::Sig
6
+
7
+ class << self
8
+ extend T::Sig
9
+
10
+ sig { overridable.returns(Typed::Schema) }
11
+ def schema
12
+ Typed::Schema.from_struct(self)
13
+ end
14
+
15
+ sig { params(type: Symbol).returns(Typed::Serializer[T.untyped, T.untyped]) }
16
+ def serializer(type)
17
+ case type
18
+ when :hash
19
+ Typed::HashSerializer.new(schema:)
20
+ when :json
21
+ Typed::JSONSerializer.new(schema:)
22
+ else
23
+ raise ArgumentError, "unknown serializer for #{type}"
24
+ end
25
+ end
26
+
27
+ sig { params(serializer_type: Symbol, source: T.untyped).returns(Typed::Serializer::DeserializeResult) }
28
+ def deserialize_from(serializer_type, source)
29
+ serializer(serializer_type).deserialize(source)
30
+ end
31
+ end
32
+
33
+ sig { params(serializer_type: Symbol).returns(Typed::Result[T.untyped, Typed::SerializeError]) }
34
+ def serialize_to(serializer_type)
35
+ self.class.serializer(serializer_type).serialize(self)
36
+ end
37
+ end
38
+ end
@@ -1,5 +1,5 @@
1
1
  # typed: strict
2
2
 
3
3
  module SorbetSchema
4
- VERSION = "0.4.2"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/sorbet-schema.rb CHANGED
@@ -10,7 +10,7 @@ require "zeitwerk"
10
10
  loader = Zeitwerk::Loader.new
11
11
  loader.push_dir(__dir__.to_s)
12
12
  loader.ignore(__FILE__)
13
- loader.ignore("#{__dir__}/sorbet-schema/*.rb")
13
+ loader.ignore("#{__dir__}/sorbet-schema/**/*.rb")
14
14
  loader.inflector.inflect(
15
15
  "json_serializer" => "JSONSerializer"
16
16
  )
@@ -21,6 +21,12 @@ loader.setup
21
21
  # but contains extensions, so we need to manually require it.
22
22
  require_relative "sorbet-schema/hash_transformer"
23
23
 
24
+ # We want to add a default `schema` method to structs
25
+ # that will guarentee a schema can be created for use
26
+ # with serialization. This can (and should) be overridden
27
+ # in child struct classes.
28
+ require_relative "sorbet-schema/t/struct"
29
+
24
30
  # Sorbet-aware namespace to super-charge your projects
25
31
  module Typed
26
32
  Value = T.type_alias { T.untyped }
@@ -10,7 +10,7 @@ module Typed
10
10
  Input = type_member
11
11
  Output = type_member
12
12
  Params = T.type_alias { T::Hash[Symbol, T.untyped] }
13
- DeserializeResult = T.type_alias { Typed::Result[T::Struct, DeserializeError] }
13
+ DeserializeResult = T.type_alias { Result[T::Struct, DeserializeError] }
14
14
 
15
15
  sig { returns(Schema) }
16
16
  attr_reader :schema
@@ -4,6 +4,7 @@
4
4
  # This is an autogenerated file for types exported from the `ansi` gem.
5
5
  # Please instead update this file by running `bin/tapioca gem ansi`.
6
6
 
7
+
7
8
  # ANSI namespace module contains all the ANSI related classes.
8
9
  #
9
10
  # source://ansi//lib/ansi/code.rb#1
@@ -4,6 +4,7 @@
4
4
  # This is an autogenerated file for types exported from the `ast` gem.
5
5
  # Please instead update this file by running `bin/tapioca gem ast`.
6
6
 
7
+
7
8
  # {AST} is a library for manipulating abstract syntax trees.
8
9
  #
9
10
  # It embraces immutability; each AST node is inherently frozen at
@@ -0,0 +1,9 @@
1
+ # typed: true
2
+
3
+ # DO NOT EDIT MANUALLY
4
+ # This is an autogenerated file for types exported from the `bigdecimal` gem.
5
+ # Please instead update this file by running `bin/tapioca gem bigdecimal`.
6
+
7
+
8
+ # THIS IS AN EMPTY RBI FILE.
9
+ # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem
@@ -4,6 +4,7 @@
4
4
  # This is an autogenerated file for types exported from the `builder` gem.
5
5
  # Please instead update this file by running `bin/tapioca gem builder`.
6
6
 
7
+
7
8
  # If the Builder::XChar module is not currently defined, fail on any
8
9
  # name clashes in standard library classes.
9
10
  #
@@ -4,6 +4,7 @@
4
4
  # This is an autogenerated file for types exported from the `erubi` gem.
5
5
  # Please instead update this file by running `bin/tapioca gem erubi`.
6
6
 
7
+
7
8
  # source://erubi//lib/erubi.rb#3
8
9
  module Erubi
9
10
  class << self
@@ -4,5 +4,6 @@
4
4
  # This is an autogenerated file for types exported from the `io-console` gem.
5
5
  # Please instead update this file by running `bin/tapioca gem io-console`.
6
6
 
7
+
7
8
  # THIS IS AN EMPTY RBI FILE.
8
9
  # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem