sorbet-schema 0.9.2 → 0.10.0

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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/AGENTS.md +15 -0
  4. data/CHANGELOG.md +34 -0
  5. data/CLAUDE.md +1 -0
  6. data/Gemfile +5 -1
  7. data/Gemfile.lock +54 -9
  8. data/README.md +84 -0
  9. data/benchmarks/helpers.rb +238 -0
  10. data/benchmarks/many_fields_deserialization.rb +23 -0
  11. data/benchmarks/many_fields_serialization.rb +26 -0
  12. data/benchmarks/simple_deserialization.rb +23 -0
  13. data/benchmarks/simple_serialization.rb +26 -0
  14. data/lib/sorbet-schema/t/struct.rb +10 -0
  15. data/lib/sorbet-schema/version.rb +1 -1
  16. data/lib/sorbet-schema.rb +3 -1
  17. data/lib/typed/active_record_serializer.rb +85 -0
  18. data/lib/typed/coercion/boolean_coercer.rb +2 -2
  19. data/lib/typed/coercion/coercer.rb +1 -1
  20. data/lib/typed/coercion/coercer_registry.rb +1 -1
  21. data/lib/typed/coercion/date_coercer.rb +2 -2
  22. data/lib/typed/coercion/date_time_coercer.rb +2 -2
  23. data/lib/typed/coercion/enum_coercer.rb +2 -2
  24. data/lib/typed/coercion/float_coercer.rb +2 -2
  25. data/lib/typed/coercion/integer_coercer.rb +2 -2
  26. data/lib/typed/coercion/string_coercer.rb +2 -2
  27. data/lib/typed/coercion/struct_coercer.rb +2 -2
  28. data/lib/typed/coercion/symbol_coercer.rb +2 -2
  29. data/lib/typed/coercion/typed_array_coercer.rb +2 -2
  30. data/lib/typed/coercion/typed_hash_coercer.rb +2 -2
  31. data/lib/typed/csv_serializer.rb +57 -0
  32. data/lib/typed/message_pack_serializer.rb +40 -0
  33. data/lib/typed/schema.rb +49 -2
  34. data/lib/typed/serializer.rb +41 -24
  35. data/lib/typed/yml_serializer.rb +46 -0
  36. data/sorbet/rbi/gems/activemodel@8.1.2.rbi +9 -0
  37. data/sorbet/rbi/gems/activerecord@8.1.2.rbi +9 -0
  38. data/sorbet/rbi/gems/activesupport@8.1.2.rbi +11 -0
  39. data/sorbet/rbi/gems/base64@0.3.0.rbi +9 -0
  40. data/sorbet/rbi/gems/benchmark-ips@2.14.0.rbi +981 -0
  41. data/sorbet/rbi/gems/concurrent-ruby@1.3.6.rbi +9 -0
  42. data/sorbet/rbi/gems/connection_pool@3.0.2.rbi +9 -0
  43. data/sorbet/rbi/gems/csv@3.3.6.rbi +4810 -0
  44. data/sorbet/rbi/gems/drb@2.2.3.rbi +9 -0
  45. data/sorbet/rbi/gems/i18n@1.14.8.rbi +9 -0
  46. data/sorbet/rbi/gems/logger@1.7.0.rbi +963 -0
  47. data/sorbet/rbi/gems/msgpack@1.8.4.rbi +320 -0
  48. data/sorbet/rbi/gems/securerandom@0.4.1.rbi +75 -0
  49. data/sorbet/rbi/gems/sqlite3@2.9.0.rbi +1901 -0
  50. data/sorbet/rbi/gems/timeout@0.6.0.rbi +194 -0
  51. data/sorbet/rbi/gems/tzinfo@2.0.6.rbi +9 -0
  52. data/sorbet/rbi/gems/uri@1.1.1.rbi +2447 -0
  53. data/sorbet/rbi/shims/active_record.rbi +37 -0
  54. data/sorbet/rbi/shims/active_support.rbi +9 -0
  55. data/sorbet/tapioca/require.rb +2 -0
  56. metadata +62 -14
  57. data/sorbet/rbi/gems/erubi@1.13.0.rbi +0 -150
  58. data/sorbet/rbi/gems/netrc@0.11.0.rbi +0 -159
  59. data/sorbet/rbi/gems/prism@0.30.0.rbi +0 -39212
  60. data/sorbet/rbi/gems/thor@1.3.1.rbi +0 -4352
  61. data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +0 -435
  62. data/sorbet/rbi/gems/yard@0.9.36.rbi +0 -18221
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4546ee5b80f3b538d7c3f9813501f066ef1f033693f5328f8789f26f9d7537dc
4
- data.tar.gz: 3c658049c62dcd8ab506bc3cc72fa926731fd8a712c1436f0277339edc88d290
3
+ metadata.gz: b0c3bb89fee8f842f919e0cf1f53d8f97fb12e4464effb88c7e821134c531a1f
4
+ data.tar.gz: a78684855bb1bd76e6688b0daa04f026fcb55c23fd2f354daaea8e7a2f0f5cd3
5
5
  SHA512:
6
- metadata.gz: a2f8a6bec49a48abd11c0d6ee1eac7eb9d2a85965a02633635b9361d82bad46bedb6c02d213cf226f6cb644e803c575ded85e3eaf6c06cbe9ae34068e7dd1985
7
- data.tar.gz: db1a6c0bf48b2fd6628b77dd324e06b42d9cb7e8e222b54523e0beeb7e10395b5a86d2a3fd7b3afe36453f2b8386f3bde758cbd9893aefee9e8d1fca8490f533
6
+ metadata.gz: 28656d268e7861ae7e069ff30b024a2d063ee33f5824ca9bc1319cbe91555b2e80b402ee2438b975d299c58f624c4e8afba1ba8649a62c8d05a6efc3fa10fbac
7
+ data.tar.gz: 9faf4f7458d56d9a66645d1d25aa4d70a83eea9abba3e1021a5fe58a5978d0c309a987d20e5af1dfc538f5d0a550f40ed1afa6a35c39ff2e5173a9b44a6cd118
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.3.4
1
+ 3.4.7
data/AGENTS.md ADDED
@@ -0,0 +1,15 @@
1
+ # Project agent memory
2
+
3
+ This file is the project's committed home for project-intrinsic agent knowledge: build, test, release, architecture, and sharp-edge notes that should travel with the code.
4
+
5
+ - Add durable project-specific notes here as they are discovered through real work.
6
+ - Ruby 3.4+ unbundles former stdlib gems (e.g. `csv`) from the default load path, so `require "csv"` is not free — it needs the gem actually installed.
7
+ - Format-specific serializers (`CSVSerializer`, `MessagePackSerializer`, `ActiveRecordSerializer`) treat their backing gem as optional: no top-level `require`, gem is `add_development_dependency` only (not `add_runtime_dependency`), the `require`/gem-presence check happens lazily in `initialize` with a clear `ArgumentError` on failure, and `T::Struct.serializer`'s factory (`lib/sorbet-schema/t/struct.rb`) mirrors that same guard via `defined?(...)`. Follow this pattern for any new serializer/coercer with a non-stdlib dependency; see `lib/typed/csv_serializer.rb` for the exact shape.
8
+ - After adding a new *required* runtime gem dependency (not this optional-serializer case), run `bundle exec tapioca gem` (add the gem to `sorbet/tapioca/require.rb` first if it isn't required anywhere else) so `bundle exec rake` (which runs `srb tc`) has an RBI to check against.
9
+
10
+ ## Maintaining this file
11
+
12
+ Keep this file for knowledge useful to almost every future agent session in this project.
13
+ Do not repeat what the codebase already shows; point to the authoritative file or command instead.
14
+ Prefer rewriting or pruning existing entries over appending new ones.
15
+ When updating this file, preserve this bar for all agents and keep entries concise.
data/CHANGELOG.md CHANGED
@@ -4,6 +4,40 @@ 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.10.0](https://github.com/maxveldink/sorbet-schema/compare/v0.9.3...v0.10.0) (2026-08-07)
8
+
9
+
10
+ ### ⚠ BREAKING CHANGES
11
+
12
+ * bump local Ruby to 3.4, add to matrix ([#138](https://github.com/maxveldink/sorbet-schema/issues/138))
13
+
14
+ ### Features
15
+
16
+ * add Typed::CSVSerializer ([#12](https://github.com/maxveldink/sorbet-schema/issues/12)) ([#144](https://github.com/maxveldink/sorbet-schema/issues/144)) ([b2377f0](https://github.com/maxveldink/sorbet-schema/commit/b2377f083782c0eb39ab442b14cb4bf18a2a8922))
17
+ * add Typed::MessagePackSerializer ([#145](https://github.com/maxveldink/sorbet-schema/issues/145)) ([d367da8](https://github.com/maxveldink/sorbet-schema/commit/d367da83b2d9e72f5099b70594243178c3df432c))
18
+ * add Typed::YMLSerializer ([#13](https://github.com/maxveldink/sorbet-schema/issues/13)) ([#143](https://github.com/maxveldink/sorbet-schema/issues/143)) ([bcc7eb8](https://github.com/maxveldink/sorbet-schema/commit/bcc7eb810f32b46d454df3a0c19468b0de09f498))
19
+ * **typed:** add ActiveRecord serializer ([#140](https://github.com/maxveldink/sorbet-schema/issues/140)) ([a815f9f](https://github.com/maxveldink/sorbet-schema/commit/a815f9f42cb36c26ead45630384eb6b5022cd43d))
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * **typed:** make csv and msgpack gem dependencies optional ([#146](https://github.com/maxveldink/sorbet-schema/issues/146)) ([db22258](https://github.com/maxveldink/sorbet-schema/commit/db222586207129fc46d981a8efd28413aea99c7b))
25
+
26
+
27
+ ### Miscellaneous Chores
28
+
29
+ * bump local Ruby to 3.4, add to matrix ([#138](https://github.com/maxveldink/sorbet-schema/issues/138)) ([1cf6a5b](https://github.com/maxveldink/sorbet-schema/commit/1cf6a5bdb77137ee34aa5c1bca66dd1ede37f875))
30
+
31
+ ## [0.9.3](https://github.com/maxveldink/sorbet-schema/compare/v0.9.2...v0.9.3) (2025-12-06)
32
+
33
+
34
+ ### Performance Improvements
35
+
36
+ * add deserialization benchmark ([#135](https://github.com/maxveldink/sorbet-schema/issues/135)) ([21d23bb](https://github.com/maxveldink/sorbet-schema/commit/21d23bb51c40ee199cb79a54f06184bb15634909))
37
+ * add serialization benchmarks, refactor to helpers ([#137](https://github.com/maxveldink/sorbet-schema/issues/137)) ([dc43409](https://github.com/maxveldink/sorbet-schema/commit/dc434094511f26ddc37f853295b2f07586826f00))
38
+ * Implement caching for serializers and coercers to improve performance ([#136](https://github.com/maxveldink/sorbet-schema/issues/136)) ([d980292](https://github.com/maxveldink/sorbet-schema/commit/d980292b64630c9ee93f886b10b3b9334b694879))
39
+ * Improve coercer performance by using class methods ([#134](https://github.com/maxveldink/sorbet-schema/issues/134)) ([0336299](https://github.com/maxveldink/sorbet-schema/commit/033629914c7ac0e19d21a725336ff7b9dbc8fc90))
40
+
7
41
  ## [0.9.2](https://github.com/maxveldink/sorbet-schema/compare/v0.9.1...v0.9.2) (2024-09-04)
8
42
 
9
43
 
data/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ AGENTS.md
data/Gemfile CHANGED
@@ -6,12 +6,13 @@ source "https://rubygems.org"
6
6
  gemspec
7
7
 
8
8
  group :development do
9
+ gem "benchmark-ips"
9
10
  gem "rake"
10
11
  gem "standard"
11
12
  gem "standard-performance"
12
13
  gem "standard-sorbet"
13
14
  gem "sorbet"
14
- gem "tapioca", require: false
15
+ gem "tapioca", "~> 0.16.6", require: false
15
16
  end
16
17
 
17
18
  group :development, :test do
@@ -23,4 +24,7 @@ group :development, :test do
23
24
  gem "debug"
24
25
 
25
26
  gem "sorbet-struct-comparable"
27
+
28
+ gem "activerecord", ">= 6.0"
29
+ gem "sqlite3"
26
30
  end
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sorbet-schema (0.9.2)
4
+ sorbet-schema (0.10.0)
5
5
  sorbet-result (~> 1.1)
6
6
  sorbet-runtime (~> 0.5)
7
7
  sorbet-struct-comparable (~> 1.3)
@@ -10,14 +10,41 @@ PATH
10
10
  GEM
11
11
  remote: https://rubygems.org/
12
12
  specs:
13
+ activemodel (8.1.2)
14
+ activesupport (= 8.1.2)
15
+ activerecord (8.1.2)
16
+ activemodel (= 8.1.2)
17
+ activesupport (= 8.1.2)
18
+ timeout (>= 0.4.0)
19
+ activesupport (8.1.2)
20
+ base64
21
+ bigdecimal
22
+ concurrent-ruby (~> 1.0, >= 1.3.1)
23
+ connection_pool (>= 2.2.5)
24
+ drb
25
+ i18n (>= 1.6, < 2)
26
+ json
27
+ logger (>= 1.4.2)
28
+ minitest (>= 5.1)
29
+ securerandom (>= 0.3)
30
+ tzinfo (~> 2.0, >= 2.0.5)
31
+ uri (>= 0.13.1)
13
32
  ansi (1.5.0)
14
33
  ast (2.4.2)
34
+ base64 (0.3.0)
35
+ benchmark-ips (2.14.0)
15
36
  bigdecimal (3.1.8)
16
37
  builder (3.3.0)
38
+ concurrent-ruby (1.3.6)
39
+ connection_pool (3.0.2)
40
+ csv (3.3.6)
17
41
  debug (1.9.2)
18
42
  irb (~> 1.10)
19
43
  reline (>= 0.3.8)
44
+ drb (2.2.3)
20
45
  erubi (1.13.0)
46
+ i18n (1.14.8)
47
+ concurrent-ruby (~> 1.0)
21
48
  io-console (0.7.2)
22
49
  irb (1.14.0)
23
50
  rdoc (>= 4.0.0)
@@ -25,6 +52,7 @@ GEM
25
52
  json (2.7.2)
26
53
  language_server-protocol (3.17.0.3)
27
54
  lint_roller (1.1.0)
55
+ logger (1.7.0)
28
56
  minitest (5.24.1)
29
57
  minitest-focus (1.4.0)
30
58
  minitest (>= 4, < 6)
@@ -33,20 +61,23 @@ GEM
33
61
  builder
34
62
  minitest (>= 5.0)
35
63
  ruby-progressbar
64
+ msgpack (1.8.4)
36
65
  netrc (0.11.0)
37
66
  parallel (1.25.1)
38
67
  parser (3.3.3.0)
39
68
  ast (~> 2.4.1)
40
69
  racc
41
- prism (0.30.0)
70
+ prism (1.6.0)
42
71
  psych (5.1.2)
43
72
  stringio
44
73
  racc (1.8.0)
45
74
  rainbow (3.1.1)
46
75
  rake (13.2.1)
47
- rbi (0.1.13)
48
- prism (>= 0.18.0, < 1.0.0)
49
- sorbet-runtime (>= 0.5.9204)
76
+ rbi (0.3.7)
77
+ prism (~> 1.0)
78
+ rbs (>= 3.4.4)
79
+ rbs (3.9.5)
80
+ logger
50
81
  rdoc (6.7.0)
51
82
  psych (>= 4.0.0)
52
83
  regexp_parser (2.9.2)
@@ -73,6 +104,7 @@ GEM
73
104
  rubocop-sorbet (0.7.8)
74
105
  rubocop (>= 0.90.0)
75
106
  ruby-progressbar (1.13.0)
107
+ securerandom (0.4.1)
76
108
  sorbet (0.5.11475)
77
109
  sorbet-static (= 0.5.11475)
78
110
  sorbet-result (1.1.0)
@@ -90,6 +122,8 @@ GEM
90
122
  prism (>= 0.28.0)
91
123
  sorbet-static-and-runtime (>= 0.5.10187)
92
124
  thor (>= 0.19.2)
125
+ sqlite3 (2.9.0-arm64-darwin)
126
+ sqlite3 (2.9.0-x86_64-linux-gnu)
93
127
  standard (1.39.1)
94
128
  language_server-protocol (~> 3.17.0.2)
95
129
  lint_roller (~> 1.0)
@@ -107,17 +141,21 @@ GEM
107
141
  rubocop-sorbet (~> 0.7.0)
108
142
  stringio (3.1.1)
109
143
  strscan (3.1.0)
110
- tapioca (0.15.0)
144
+ tapioca (0.16.6)
111
145
  bundler (>= 2.2.25)
112
146
  netrc (>= 0.11.0)
113
147
  parallel (>= 1.21.0)
114
- rbi (>= 0.1.4, < 0.2)
148
+ rbi (~> 0.2)
115
149
  sorbet-static-and-runtime (>= 0.5.11087)
116
150
  spoom (>= 1.2.0)
117
151
  thor (>= 1.2.0)
118
152
  yard-sorbet
119
153
  thor (1.3.1)
154
+ timeout (0.6.0)
155
+ tzinfo (2.0.6)
156
+ concurrent-ruby (~> 1.0)
120
157
  unicode-display_width (2.5.0)
158
+ uri (1.1.1)
121
159
  yard (0.9.36)
122
160
  yard-sorbet (0.9.0)
123
161
  sorbet-runtime
@@ -127,22 +165,29 @@ GEM
127
165
  PLATFORMS
128
166
  arm64-darwin-22
129
167
  arm64-darwin-23
168
+ arm64-darwin-25
169
+ arm64-darwin-27
130
170
  x86_64-linux
131
171
 
132
172
  DEPENDENCIES
173
+ activerecord (>= 6.0)
174
+ benchmark-ips
133
175
  bigdecimal
176
+ csv (~> 3.3)
134
177
  debug
135
178
  minitest
136
179
  minitest-focus
137
180
  minitest-reporters
181
+ msgpack (~> 1.7)
138
182
  rake
139
183
  sorbet
140
184
  sorbet-schema!
141
185
  sorbet-struct-comparable
186
+ sqlite3
142
187
  standard
143
188
  standard-performance
144
189
  standard-sorbet
145
- tapioca
190
+ tapioca (~> 0.16.6)
146
191
 
147
192
  BUNDLED WITH
148
- 2.5.15
193
+ 2.7.2
data/README.md CHANGED
@@ -54,6 +54,8 @@ result = max.serialize_to(:json)
54
54
  result.payload # == '{"name":"Max","age":29}'
55
55
  ```
56
56
 
57
+ `:hash`, `:json`, `:csv`, `:yml`, and `:msgpack` are all supported as the first argument to `deserialize_from`/`serialize_to` (and the equivalent `from_hash`/`from_json`/`from_csv`/`from_yml`/`from_msgpack` methods on `Typed::Schema`), corresponding to the built-in serializers below.
58
+
57
59
  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
60
 
59
61
  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.
@@ -109,6 +111,8 @@ end
109
111
 
110
112
  These are the currently available serializers. For more information about implementing a custom one (or contributing one back!), see [Custom Coercers](#custom-coercers).
111
113
 
114
+ `HashSerializer`, `JSONSerializer`, and `YMLSerializer` work out of the box with no extra dependencies. `CSVSerializer` and `MessagePackSerializer` require you to add the `csv` and `msgpack` gems, respectively, to your own Gemfile - they are not pulled in as runtime dependencies of `sorbet-schema` itself. Referencing `:csv`/`:msgpack` via `serializer`/`deserialize_from`/`serialize_to` without the gem installed raises a clear `ArgumentError` telling you which gem to add.
115
+
112
116
  #### JSONSerializer
113
117
 
114
118
  See [Getting Started](#getting-started) for more information on how to use the JSONSerializer.
@@ -129,6 +133,86 @@ max = result.payload # == Person.new(name: "Max", age: 29)
129
133
 
130
134
  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
135
 
136
+ #### CSVSerializer
137
+
138
+ Accepts and returns a CSV string representing a single record, with a header row followed by one data row:
139
+
140
+ ```ruby
141
+ csv_serializer = Typed::CSVSerializer.new(schema: Person.schema)
142
+
143
+ # Deserialize from target format
144
+ result = csv_serializer.deserialize("name,age\nMax,29\n")
145
+ max = result.payload # == Person.new(name: "Max", age: 29)
146
+
147
+ # Serialize to target format
148
+ result = csv_serializer.serialize(max)
149
+ result.payload # == "name,age\nMax,29\n"
150
+ ```
151
+
152
+ CSV is a flat, row-based format, so nested `T::Struct`s, `Hash`es, and `Array`s cannot be represented as their own columns. Rather than writing a lossy representation into a cell that can never be parsed back correctly, `CSVSerializer#serialize` returns a `Typed::SerializeError` naming the offending field(s) when a struct has one. Prefer this serializer for schemas with scalar fields only, or use an `inline_serializer` (see [Inline Serializers](#inline-serializers)) to flatten a nested field into a scalar before serializing it to CSV.
153
+
154
+ #### YMLSerializer
155
+
156
+ Works just like the `JSONSerializer`, but with YAML strings:
157
+
158
+ ```ruby
159
+ yml_serializer = Typed::YMLSerializer.new(schema: Person.schema)
160
+
161
+ # Deserialize from target format
162
+ result = yml_serializer.deserialize("---\nname: Max\nage: 29\n")
163
+ max = result.payload # == Person.new(name: "Max", age: 29)
164
+
165
+ # Serialize to target format
166
+ result = yml_serializer.serialize(max)
167
+ result.payload # == "---\nname: Max\nage: 29\n"
168
+ ```
169
+
170
+ #### MessagePackSerializer
171
+
172
+ Works just like the `JSONSerializer`, but with [MessagePack](https://msgpack.org/) binary-encoded strings, using the [`msgpack`](https://github.com/msgpack/msgpack-ruby) gem:
173
+
174
+ ```ruby
175
+ message_pack_serializer = Typed::MessagePackSerializer.new(schema: Person.schema)
176
+
177
+ # Deserialize from target format
178
+ result = message_pack_serializer.deserialize(MessagePack.pack({"name" => "Max", "age" => 29}))
179
+ max = result.payload # == Person.new(name: "Max", age: 29)
180
+
181
+ # Serialize to target format
182
+ result = message_pack_serializer.serialize(max)
183
+ result.payload # == MessagePack.pack({"name" => "Max", "age" => 29})
184
+ ```
185
+
186
+ Unlike `CSVSerializer`, MessagePack natively supports nested maps and arrays, so nested `T::Struct`s, `Hash`es, and `Array`s round-trip without any `inline_serializer` needed.
187
+
188
+ #### ActiveRecordSerializer
189
+
190
+ Requires the `activerecord` gem to be available. This is only checked when going through `.serializer(:activerecord)`, which raises an `ArgumentError` if the gem isn't loaded; calling `Typed::ActiveRecordSerializer.new` directly without ActiveRecord loaded instead raises a `NameError`, since the class itself references the `ActiveRecord` constant. The `Typed::ActiveRecordSerializer` converts between `ActiveRecord::Base` model instances and `T::Struct`s, matching fields by attribute name and mapping associated records through the model's declared associations. It requires a `model_class` option, in addition to `schema`, so it knows which `ActiveRecord::Base` subclass to deserialize from and serialize to:
191
+
192
+ ```ruby
193
+ ar_serializer = Typed::ActiveRecordSerializer.new(schema: Person.schema, model_class: PersonModel)
194
+
195
+ # Deserialize from target format
196
+ result = ar_serializer.deserialize(PersonModel.new(name: "Max", age: 29))
197
+ max = result.payload # == Person.new(name: "Max", age: 29)
198
+
199
+ result = ar_serializer.serialize(max)
200
+ result.payload # == PersonModel instance with name: "Max", age: 29
201
+ ```
202
+
203
+ When serializing, only fields that map to a column (or to an association's declared foreign key) on `model_class` are assigned; the rest are ignored.
204
+
205
+ Nested `belongs_to` associations are deserialized recursively to arbitrary depth, so a `T::Struct` field whose type is itself a `T::Struct` backed by a `belongs_to` association will have its own nested associations resolved too.
206
+
207
+ Deserializing reads through the model's association readers, so it is subject to the same N+1 query behavior as any other Rails association access. Preloading with `includes` works transparently and is the recommended way to avoid a query per record per association when deserializing a collection:
208
+
209
+ ```ruby
210
+ # 1 query for people + 1 query for locations, instead of 1 + N
211
+ PersonModel.includes(:location).find_each do |person_model|
212
+ ar_serializer.deserialize(person_model)
213
+ end
214
+ ```
215
+
132
216
  ### Customization
133
217
 
134
218
  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!
@@ -0,0 +1,238 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
5
+
6
+ require "json"
7
+ require "sorbet-schema"
8
+
9
+ module BenchmarkHelpers
10
+ extend T::Sig
11
+
12
+ class Rank < T::Enum
13
+ enums do
14
+ Ace = new("Ace")
15
+ King = new("King")
16
+ Queen = new("Queen")
17
+ Jack = new("Jack")
18
+ Ten = new("Ten")
19
+ Nine = new("Nine")
20
+ Eight = new("Eight")
21
+ Seven = new("Seven")
22
+ Six = new("Six")
23
+ Five = new("Five")
24
+ Four = new("Four")
25
+ Three = new("Three")
26
+ Two = new("Two")
27
+ end
28
+ end
29
+
30
+ class Suit < T::Enum
31
+ enums do
32
+ Spades = new("Spades")
33
+ Hearts = new("Hearts")
34
+ Clubs = new("Clubs")
35
+ Diamonds = new("Diamonds")
36
+ end
37
+ end
38
+
39
+ # Define some structs for the benchmark
40
+ class Card < T::Struct
41
+ const :rank, Rank
42
+ const :suit, Suit
43
+ end
44
+
45
+ class Player < T::Struct
46
+ const :name, String
47
+ const :hand, T::Array[Card]
48
+ end
49
+
50
+ class Round < T::Struct
51
+ const :number, Integer
52
+ const :winner, T.nilable(Player)
53
+ end
54
+
55
+ class Game < T::Struct
56
+ const :name, String
57
+ const :players, T::Array[Player]
58
+ const :rounds, T::Array[Round]
59
+ const :deck_size, Integer
60
+ const :shuffled, T::Boolean
61
+ end
62
+
63
+ # Define a struct with MANY fields of the same types that require coercion
64
+ # This will stress-test coercer caching by reusing the same coercers many times
65
+ class Humongous < T::Struct
66
+ const :field_1, Integer
67
+ const :field_2, Integer
68
+ const :field_3, Integer
69
+ const :field_4, Integer
70
+ const :field_5, Integer
71
+ const :field_6, Integer
72
+ const :field_7, Integer
73
+ const :field_8, Integer
74
+ const :field_9, Integer
75
+ const :field_10, Integer
76
+ const :field_11, Integer
77
+ const :field_12, Integer
78
+ const :field_13, Integer
79
+ const :field_14, Integer
80
+ const :field_15, Integer
81
+ const :field_16, Integer
82
+ const :field_17, Integer
83
+ const :field_18, Integer
84
+ const :field_19, Integer
85
+ const :field_20, Integer
86
+ const :field_21, Integer
87
+ const :field_22, Integer
88
+ const :field_23, Integer
89
+ const :field_24, Integer
90
+ const :field_25, Integer
91
+ const :field_26, Integer
92
+ const :field_27, Integer
93
+ const :field_28, Integer
94
+ const :field_29, Integer
95
+ const :field_30, Integer
96
+ const :field_31, Integer
97
+ const :field_32, Integer
98
+ const :field_33, Integer
99
+ const :field_34, Integer
100
+ const :field_35, Integer
101
+ const :field_36, Integer
102
+ const :field_37, Integer
103
+ const :field_38, Integer
104
+ const :field_39, Integer
105
+ const :field_40, Integer
106
+ const :field_41, Integer
107
+ const :field_42, Integer
108
+ const :field_43, Integer
109
+ const :field_44, Integer
110
+ const :field_45, Integer
111
+ const :field_46, Integer
112
+ const :field_47, Integer
113
+ const :field_48, Integer
114
+ const :field_49, Integer
115
+ const :field_50, Integer
116
+
117
+ const :float_1, Float
118
+ const :float_2, Float
119
+ const :float_3, Float
120
+ const :float_4, Float
121
+ const :float_5, Float
122
+ const :float_6, Float
123
+ const :float_7, Float
124
+ const :float_8, Float
125
+ const :float_9, Float
126
+ const :float_10, Float
127
+ const :float_11, Float
128
+ const :float_12, Float
129
+ const :float_13, Float
130
+ const :float_14, Float
131
+ const :float_15, Float
132
+ const :float_16, Float
133
+ const :float_17, Float
134
+ const :float_18, Float
135
+ const :float_19, Float
136
+ const :float_20, Float
137
+
138
+ const :bool_1, T::Boolean
139
+ const :bool_2, T::Boolean
140
+ const :bool_3, T::Boolean
141
+ const :bool_4, T::Boolean
142
+ const :bool_5, T::Boolean
143
+ const :bool_6, T::Boolean
144
+ const :bool_7, T::Boolean
145
+ const :bool_8, T::Boolean
146
+ const :bool_9, T::Boolean
147
+ const :bool_10, T::Boolean
148
+ const :bool_11, T::Boolean
149
+ const :bool_12, T::Boolean
150
+ const :bool_13, T::Boolean
151
+ const :bool_14, T::Boolean
152
+ const :bool_15, T::Boolean
153
+ const :bool_16, T::Boolean
154
+ const :bool_17, T::Boolean
155
+ const :bool_18, T::Boolean
156
+ const :bool_19, T::Boolean
157
+ const :bool_20, T::Boolean
158
+
159
+ const :sym_1, Symbol
160
+ const :sym_2, Symbol
161
+ const :sym_3, Symbol
162
+ const :sym_4, Symbol
163
+ const :sym_5, Symbol
164
+ const :sym_6, Symbol
165
+ const :sym_7, Symbol
166
+ const :sym_8, Symbol
167
+ const :sym_9, Symbol
168
+ const :sym_10, Symbol
169
+ const :sym_11, Symbol
170
+ const :sym_12, Symbol
171
+ const :sym_13, Symbol
172
+ const :sym_14, Symbol
173
+ const :sym_15, Symbol
174
+ const :sym_16, Symbol
175
+ const :sym_17, Symbol
176
+ const :sym_18, Symbol
177
+ const :sym_19, Symbol
178
+ const :sym_20, Symbol
179
+ end
180
+
181
+ sig { returns(T::Hash[Symbol, T.untyped]) }
182
+ def self.game_data
183
+ {
184
+ name: "Poker",
185
+ players: Array.new(10) do |i|
186
+ {
187
+ name: "Player #{i}",
188
+ hand: [
189
+ {rank: "Ace", suit: "Spades"},
190
+ {rank: "King", suit: "Spades"}
191
+ ]
192
+ }
193
+ end,
194
+ rounds: Array.new(5) do |i|
195
+ {
196
+ number: i + 1,
197
+ winner: {
198
+ name: "Player #{i % 2}",
199
+ hand: [
200
+ {rank: "Queen", suit: "Hearts"},
201
+ {rank: "Jack", suit: "Hearts"}
202
+ ]
203
+ }
204
+ }
205
+ end,
206
+ deck_size: 52,
207
+ shuffled: true
208
+ }
209
+ end
210
+
211
+ sig { returns(T::Hash[Symbol, T.untyped]) }
212
+ def self.humongous_data
213
+ {
214
+ field_1: "1", field_2: "2", field_3: "3", field_4: "4", field_5: "5",
215
+ field_6: "6", field_7: "7", field_8: "8", field_9: "9", field_10: "10",
216
+ field_11: "11", field_12: "12", field_13: "13", field_14: "14", field_15: "15",
217
+ field_16: "16", field_17: "17", field_18: "18", field_19: "19", field_20: "20",
218
+ field_21: "21", field_22: "22", field_23: "23", field_24: "24", field_25: "25",
219
+ field_26: "26", field_27: "27", field_28: "28", field_29: "29", field_30: "30",
220
+ field_31: "31", field_32: "32", field_33: "33", field_34: "34", field_35: "35",
221
+ field_36: "36", field_37: "37", field_38: "38", field_39: "39", field_40: "40",
222
+ field_41: "41", field_42: "42", field_43: "43", field_44: "44", field_45: "45",
223
+ field_46: "46", field_47: "47", field_48: "48", field_49: "49", field_50: "50",
224
+ float_1: "1.1", float_2: "2.2", float_3: "3.3", float_4: "4.4", float_5: "5.5",
225
+ float_6: "6.6", float_7: "7.7", float_8: "8.8", float_9: "9.9", float_10: "10.1",
226
+ float_11: "11.1", float_12: "12.2", float_13: "13.3", float_14: "14.4", float_15: "15.5",
227
+ float_16: "16.6", float_17: "17.7", float_18: "18.8", float_19: "19.9", float_20: "20.1",
228
+ bool_1: "true", bool_2: "false", bool_3: "true", bool_4: "false", bool_5: "true",
229
+ bool_6: "false", bool_7: "true", bool_8: "false", bool_9: "true", bool_10: "false",
230
+ bool_11: "true", bool_12: "false", bool_13: "true", bool_14: "false", bool_15: "true",
231
+ bool_16: "false", bool_17: "true", bool_18: "false", bool_19: "true", bool_20: "false",
232
+ sym_1: "sym1", sym_2: "sym2", sym_3: "sym3", sym_4: "sym4", sym_5: "sym5",
233
+ sym_6: "sym6", sym_7: "sym7", sym_8: "sym8", sym_9: "sym9", sym_10: "sym10",
234
+ sym_11: "sym11", sym_12: "sym12", sym_13: "sym13", sym_14: "sym14", sym_15: "sym15",
235
+ sym_16: "sym16", sym_17: "sym17", sym_18: "sym18", sym_19: "sym19", sym_20: "sym20"
236
+ }
237
+ end
238
+ end
@@ -0,0 +1,23 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
5
+
6
+ require "benchmark/ips"
7
+ require_relative "helpers"
8
+
9
+ humongous_schema = Typed::Schema.from_struct(BenchmarkHelpers::Humongous)
10
+ humongous_data = BenchmarkHelpers.humongous_data
11
+ humongous_json = JSON.generate(humongous_data)
12
+
13
+ Benchmark.ips do |x|
14
+ x.report("hash deserialization with many fields") do
15
+ humongous_schema.from_hash(humongous_data)
16
+ end
17
+
18
+ x.report("json deserialization with many fields") do
19
+ humongous_schema.from_json(humongous_json)
20
+ end
21
+
22
+ x.compare!
23
+ end
@@ -0,0 +1,26 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
5
+
6
+ require "benchmark/ips"
7
+ require_relative "helpers"
8
+
9
+ humongous_schema = Typed::Schema.from_struct(BenchmarkHelpers::Humongous)
10
+ humongous_data = BenchmarkHelpers.humongous_data
11
+ humongous_struct = humongous_schema.from_hash(humongous_data).payload
12
+
13
+ hash_serializer = Typed::HashSerializer.new(schema: humongous_schema)
14
+ json_serializer = Typed::JSONSerializer.new(schema: humongous_schema)
15
+
16
+ Benchmark.ips do |x|
17
+ x.report("hash serialization with many fields") do
18
+ hash_serializer.serialize(humongous_struct)
19
+ end
20
+
21
+ x.report("json serialization with many fields") do
22
+ json_serializer.serialize(humongous_struct)
23
+ end
24
+
25
+ x.compare!
26
+ end
@@ -0,0 +1,23 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
5
+
6
+ require "benchmark/ips"
7
+ require_relative "helpers"
8
+
9
+ game_schema = Typed::Schema.from_struct(BenchmarkHelpers::Game)
10
+ game_data = BenchmarkHelpers.game_data
11
+ game_json = JSON.generate(game_data)
12
+
13
+ Benchmark.ips do |x|
14
+ x.report("simple hash deserialization") do
15
+ game_schema.from_hash(game_data)
16
+ end
17
+
18
+ x.report("simple json deserialization") do
19
+ game_schema.from_json(game_json)
20
+ end
21
+
22
+ x.compare!
23
+ end