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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/AGENTS.md +15 -0
- data/CHANGELOG.md +34 -0
- data/CLAUDE.md +1 -0
- data/Gemfile +5 -1
- data/Gemfile.lock +54 -9
- data/README.md +84 -0
- data/benchmarks/helpers.rb +238 -0
- data/benchmarks/many_fields_deserialization.rb +23 -0
- data/benchmarks/many_fields_serialization.rb +26 -0
- data/benchmarks/simple_deserialization.rb +23 -0
- data/benchmarks/simple_serialization.rb +26 -0
- data/lib/sorbet-schema/t/struct.rb +10 -0
- data/lib/sorbet-schema/version.rb +1 -1
- data/lib/sorbet-schema.rb +3 -1
- data/lib/typed/active_record_serializer.rb +85 -0
- data/lib/typed/coercion/boolean_coercer.rb +2 -2
- data/lib/typed/coercion/coercer.rb +1 -1
- data/lib/typed/coercion/coercer_registry.rb +1 -1
- data/lib/typed/coercion/date_coercer.rb +2 -2
- data/lib/typed/coercion/date_time_coercer.rb +2 -2
- data/lib/typed/coercion/enum_coercer.rb +2 -2
- data/lib/typed/coercion/float_coercer.rb +2 -2
- data/lib/typed/coercion/integer_coercer.rb +2 -2
- data/lib/typed/coercion/string_coercer.rb +2 -2
- data/lib/typed/coercion/struct_coercer.rb +2 -2
- data/lib/typed/coercion/symbol_coercer.rb +2 -2
- data/lib/typed/coercion/typed_array_coercer.rb +2 -2
- data/lib/typed/coercion/typed_hash_coercer.rb +2 -2
- data/lib/typed/csv_serializer.rb +57 -0
- data/lib/typed/message_pack_serializer.rb +40 -0
- data/lib/typed/schema.rb +49 -2
- data/lib/typed/serializer.rb +41 -24
- data/lib/typed/yml_serializer.rb +46 -0
- data/sorbet/rbi/gems/activemodel@8.1.2.rbi +9 -0
- data/sorbet/rbi/gems/activerecord@8.1.2.rbi +9 -0
- data/sorbet/rbi/gems/activesupport@8.1.2.rbi +11 -0
- data/sorbet/rbi/gems/base64@0.3.0.rbi +9 -0
- data/sorbet/rbi/gems/benchmark-ips@2.14.0.rbi +981 -0
- data/sorbet/rbi/gems/concurrent-ruby@1.3.6.rbi +9 -0
- data/sorbet/rbi/gems/connection_pool@3.0.2.rbi +9 -0
- data/sorbet/rbi/gems/csv@3.3.6.rbi +4810 -0
- data/sorbet/rbi/gems/drb@2.2.3.rbi +9 -0
- data/sorbet/rbi/gems/i18n@1.14.8.rbi +9 -0
- data/sorbet/rbi/gems/logger@1.7.0.rbi +963 -0
- data/sorbet/rbi/gems/msgpack@1.8.4.rbi +320 -0
- data/sorbet/rbi/gems/securerandom@0.4.1.rbi +75 -0
- data/sorbet/rbi/gems/sqlite3@2.9.0.rbi +1901 -0
- data/sorbet/rbi/gems/timeout@0.6.0.rbi +194 -0
- data/sorbet/rbi/gems/tzinfo@2.0.6.rbi +9 -0
- data/sorbet/rbi/gems/uri@1.1.1.rbi +2447 -0
- data/sorbet/rbi/shims/active_record.rbi +37 -0
- data/sorbet/rbi/shims/active_support.rbi +9 -0
- data/sorbet/tapioca/require.rb +2 -0
- metadata +62 -14
- data/sorbet/rbi/gems/erubi@1.13.0.rbi +0 -150
- data/sorbet/rbi/gems/netrc@0.11.0.rbi +0 -159
- data/sorbet/rbi/gems/prism@0.30.0.rbi +0 -39212
- data/sorbet/rbi/gems/thor@1.3.1.rbi +0 -4352
- data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +0 -435
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b0c3bb89fee8f842f919e0cf1f53d8f97fb12e4464effb88c7e821134c531a1f
|
|
4
|
+
data.tar.gz: a78684855bb1bd76e6688b0daa04f026fcb55c23fd2f354daaea8e7a2f0f5cd3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 28656d268e7861ae7e069ff30b024a2d063ee33f5824ca9bc1319cbe91555b2e80b402ee2438b975d299c58f624c4e8afba1ba8649a62c8d05a6efc3fa10fbac
|
|
7
|
+
data.tar.gz: 9faf4f7458d56d9a66645d1d25aa4d70a83eea9abba3e1021a5fe58a5978d0c309a987d20e5af1dfc538f5d0a550f40ed1afa6a35c39ff2e5173a9b44a6cd118
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
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.
|
|
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 (
|
|
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.
|
|
48
|
-
prism (
|
|
49
|
-
|
|
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.
|
|
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 (
|
|
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.
|
|
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
|