typelizer 0.7.0 → 0.8.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/CHANGELOG.md +30 -1
- data/README.md +75 -0
- data/lib/tasks/generate.rake +4 -4
- data/lib/typelizer/config.rb +3 -0
- data/lib/typelizer/configuration.rb +1 -2
- data/lib/typelizer/delegate_tracker.rb +34 -0
- data/lib/typelizer/generator.rb +2 -27
- data/lib/typelizer/interface.rb +19 -2
- data/lib/typelizer/model_plugins/active_record.rb +23 -0
- data/lib/typelizer/openapi.rb +124 -0
- data/lib/typelizer/property.rb +3 -2
- data/lib/typelizer/serializer_plugins/alba/trait_interface.rb +1 -1
- data/lib/typelizer/version.rb +1 -1
- data/lib/typelizer.rb +30 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a0b21f5f78121c64717aa08d6556efbddc0b0b80a3ec6f272d63f699133fff12
|
|
4
|
+
data.tar.gz: d8df7c3542950c82e6b25d3f9c39ea808aded01fab0e5b5eaada3801a99031a3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cc42979e0f00e3aec338fdd4cf3848639ba12a2019293a46e864e908a1dfd8f2f2258ef62d07ed15ec3da66ca7ad740907ac63c6e04f9aae29f18db0eae46584
|
|
7
|
+
data.tar.gz: c9504167e013d6aeff1208207b4531f012fe942018e72368296d2a676fb04460ece7b0582a5ef297cf6c037e071ea0868e39c4632765030bdb014fe9a49884ea
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning].
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.0] - 2026-02-19
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- OpenAPI schema generation from serializers, supporting both OpenAPI 3.0 and 3.1. ([@skryukov])
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
# Get all schemas as a hash
|
|
18
|
+
Typelizer.openapi_schemas
|
|
19
|
+
# => { "Post" => { type: :object, properties: { ... }, required: [...] }, ... }
|
|
20
|
+
|
|
21
|
+
# OpenAPI 3.1 output
|
|
22
|
+
Typelizer.openapi_schemas(openapi_version: "3.1")
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Column types are automatically mapped to OpenAPI types with proper formats (`integer`, `int64`, `uuid`, `date-time`, etc.).
|
|
26
|
+
Enums, nullable fields, arrays, deprecated flags, and `$ref` associations are all handled automatically.
|
|
27
|
+
|
|
28
|
+
- Type inference for delegated attributes (`delegate :name, to: :user`). Typelizer now tracks `delegate` calls on ActiveRecord models and resolves types from the target association's model, including support for `prefix` and `allow_nil` options. ([@skryukov])
|
|
29
|
+
|
|
30
|
+
- Reference other serializers in `typelize` method by passing the class directly. ([@skryukov])
|
|
31
|
+
|
|
32
|
+
- Per-writer `reject_class` configuration. Each writer can now define its own `reject_class` filter, enabling scoped output (e.g., only V1 serializers for a V1 writer). ([@skryukov])
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- `typelize` DSL metadata (optional, comment, type overrides) now correctly applies to renamed attributes (e.g., via `key:`, `alias_name`, `value_from`). Previously, metadata was looked up only by `column_name`, missing attributes where the output name differs. ([@skryukov])
|
|
37
|
+
|
|
10
38
|
## [0.7.0] - 2026-01-15
|
|
11
39
|
|
|
12
40
|
### Changed
|
|
@@ -358,7 +386,8 @@ and this project adheres to [Semantic Versioning].
|
|
|
358
386
|
[@prog-supdex]: https://github.com/prog-supdex
|
|
359
387
|
[@ventsislaf]: https://github.com/ventsislaf
|
|
360
388
|
|
|
361
|
-
[Unreleased]: https://github.com/skryukov/typelizer/compare/v0.
|
|
389
|
+
[Unreleased]: https://github.com/skryukov/typelizer/compare/v0.8.0...HEAD
|
|
390
|
+
[0.8.0]: https://github.com/skryukov/typelizer/compare/v0.7.0...v0.8.0
|
|
362
391
|
[0.7.0]: https://github.com/skryukov/typelizer/compare/v0.6.0...v0.7.0
|
|
363
392
|
[0.6.0]: https://github.com/skryukov/typelizer/compare/v0.5.6...v0.6.0
|
|
364
393
|
[0.5.6]: https://github.com/skryukov/typelizer/compare/v0.5.5...v0.5.6
|
data/README.md
CHANGED
|
@@ -16,6 +16,7 @@ Typelizer generates TypeScript types from your Ruby serializers. It supports mul
|
|
|
16
16
|
- [Manual Generation](#manual-generation)
|
|
17
17
|
- [Automatic Generation in Development](#automatic-generation-in-development)
|
|
18
18
|
- [Disabling Typelizer](#disabling-typelizer)
|
|
19
|
+
- [OpenAPI Schema Generation](#openapi-schema-generation)
|
|
19
20
|
- [Configuration](#configuration)
|
|
20
21
|
- [Global Configuration](#simple-configuration)
|
|
21
22
|
- [Writers (multiple outputs)](#defining-multiple-writers)
|
|
@@ -128,6 +129,26 @@ class PostResource < ApplicationResource
|
|
|
128
129
|
end
|
|
129
130
|
```
|
|
130
131
|
|
|
132
|
+
You can reference other serializers directly by passing the class. Typelizer resolves the class to its generated type name automatically:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
class PostResource < ApplicationResource
|
|
136
|
+
attributes :id, :title
|
|
137
|
+
|
|
138
|
+
# Reference another serializer — resolves to its generated TypeScript type
|
|
139
|
+
typelize reviewer: [AuthorResource, {optional: true, nullable: true}]
|
|
140
|
+
attribute :reviewer do |post|
|
|
141
|
+
post.reviewer
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Self-reference works too
|
|
145
|
+
typelize previous_post: PostResource
|
|
146
|
+
attribute :previous_post do |post|
|
|
147
|
+
post.previous_post
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
131
152
|
For more complex type definitions, use the full API:
|
|
132
153
|
|
|
133
154
|
```ruby
|
|
@@ -310,6 +331,60 @@ Typelizer.listen = false
|
|
|
310
331
|
|
|
311
332
|
Sometimes we want to use Typelizer only with manual generation. To disable Typelizer during development, we can set `DISABLE_TYPELIZER` environment variable to `true`. This doesn't affect manual generation.
|
|
312
333
|
|
|
334
|
+
## OpenAPI Schema Generation
|
|
335
|
+
|
|
336
|
+
Typelizer can generate [OpenAPI](https://swagger.io/specification/) component schemas from your serializers. This is useful for documenting your API or integrating with tools like [rswag](https://github.com/rswag/rswag).
|
|
337
|
+
|
|
338
|
+
Get all schemas as a hash:
|
|
339
|
+
|
|
340
|
+
```ruby
|
|
341
|
+
Typelizer.openapi_schemas
|
|
342
|
+
# => {
|
|
343
|
+
# "Post" => {
|
|
344
|
+
# type: :object,
|
|
345
|
+
# properties: {
|
|
346
|
+
# id: { type: :integer },
|
|
347
|
+
# title: { type: :string },
|
|
348
|
+
# published_at: { type: :string, format: :"date-time", nullable: true }
|
|
349
|
+
# },
|
|
350
|
+
# required: [:id, :title]
|
|
351
|
+
# },
|
|
352
|
+
# "Author" => { ... }
|
|
353
|
+
# }
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
By default, schemas are generated for OpenAPI 3.0. Pass `openapi_version: "3.1"` for OpenAPI 3.1 output (e.g., `type: [:string, :null]` instead of `nullable: true`):
|
|
357
|
+
|
|
358
|
+
```ruby
|
|
359
|
+
Typelizer.openapi_schemas(openapi_version: "3.1")
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Generate a schema for a single interface:
|
|
363
|
+
|
|
364
|
+
```ruby
|
|
365
|
+
interfaces = Typelizer.interfaces
|
|
366
|
+
post_interface = interfaces.find { |i| i.name == "Post" }
|
|
367
|
+
Typelizer::OpenAPI.schema_for(post_interface)
|
|
368
|
+
Typelizer::OpenAPI.schema_for(post_interface, openapi_version: "3.1")
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Column types are mapped to OpenAPI types automatically:
|
|
372
|
+
|
|
373
|
+
| Column type | OpenAPI type | Format |
|
|
374
|
+
|---|---|---|
|
|
375
|
+
| `integer` | `integer` | |
|
|
376
|
+
| `bigint` | `integer` | `int64` |
|
|
377
|
+
| `float` | `number` | `float` |
|
|
378
|
+
| `decimal` | `number` | `double` |
|
|
379
|
+
| `boolean` | `boolean` | |
|
|
380
|
+
| `string`, `text`, `citext` | `string` | |
|
|
381
|
+
| `uuid` | `string` | `uuid` |
|
|
382
|
+
| `date` | `string` | `date` |
|
|
383
|
+
| `datetime` | `string` | `date-time` |
|
|
384
|
+
| `time` | `string` | `time` |
|
|
385
|
+
|
|
386
|
+
Enums, nullable fields, arrays, deprecated flags, and `$ref` associations are all handled automatically.
|
|
387
|
+
|
|
313
388
|
## Configuration
|
|
314
389
|
|
|
315
390
|
Typelizer provides several global configuration options:
|
data/lib/tasks/generate.rake
CHANGED
|
@@ -19,13 +19,13 @@ namespace :typelizer do
|
|
|
19
19
|
ENV["DISABLE_TYPELIZER"] = "false"
|
|
20
20
|
|
|
21
21
|
puts "Generating TypeScript interfaces..."
|
|
22
|
-
serializers = []
|
|
23
22
|
time = Benchmark.realtime do
|
|
24
|
-
|
|
23
|
+
block.call
|
|
25
24
|
end
|
|
26
25
|
|
|
26
|
+
interfaces = Typelizer.interfaces
|
|
27
27
|
puts "Finished in #{time} seconds"
|
|
28
|
-
puts "Found #{
|
|
29
|
-
puts
|
|
28
|
+
puts "Found #{interfaces.size} serializers:"
|
|
29
|
+
puts interfaces.map { |i| "\t#{i.name}" }.join("\n")
|
|
30
30
|
end
|
|
31
31
|
end
|
data/lib/typelizer/config.rb
CHANGED
|
@@ -51,6 +51,7 @@ module Typelizer
|
|
|
51
51
|
output_dir
|
|
52
52
|
inheritance_strategy
|
|
53
53
|
associations_strategy
|
|
54
|
+
reject_class
|
|
54
55
|
].freeze
|
|
55
56
|
|
|
56
57
|
Config = Struct.new(
|
|
@@ -70,6 +71,7 @@ module Typelizer
|
|
|
70
71
|
:verbatim_module_syntax,
|
|
71
72
|
:inheritance_strategy,
|
|
72
73
|
:associations_strategy,
|
|
74
|
+
:reject_class,
|
|
73
75
|
:comments,
|
|
74
76
|
:prefer_double_quotes,
|
|
75
77
|
keyword_init: true
|
|
@@ -105,6 +107,7 @@ module Typelizer
|
|
|
105
107
|
null_strategy: :nullable,
|
|
106
108
|
inheritance_strategy: :none,
|
|
107
109
|
associations_strategy: :database,
|
|
110
|
+
reject_class: ->(serializer:) { false },
|
|
108
111
|
comments: false,
|
|
109
112
|
prefer_double_quotes: false,
|
|
110
113
|
|
|
@@ -18,12 +18,11 @@ module Typelizer
|
|
|
18
18
|
class Configuration
|
|
19
19
|
DEFAULT_WRITER_NAME = :default
|
|
20
20
|
|
|
21
|
-
attr_accessor :dirs, :
|
|
21
|
+
attr_accessor :dirs, :listen
|
|
22
22
|
attr_reader :writers, :global_settings
|
|
23
23
|
|
|
24
24
|
def initialize
|
|
25
25
|
@dirs = []
|
|
26
|
-
@reject_class = ->(serializer:) { false }
|
|
27
26
|
@listen = nil
|
|
28
27
|
|
|
29
28
|
default = Config.build
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typelizer
|
|
4
|
+
module DelegateTracker
|
|
5
|
+
@registry = {} # { Class => { method_name => { to:, allow_nil:, original: } } }
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
attr_reader :registry
|
|
9
|
+
|
|
10
|
+
def [](klass, method)
|
|
11
|
+
registry.dig(klass, method)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module Hook
|
|
16
|
+
def delegate(*methods, to:, allow_nil: nil, prefix: nil, **)
|
|
17
|
+
super.tap do
|
|
18
|
+
next unless is_a?(Class) && defined?(ActiveRecord::Base) && self < ActiveRecord::Base
|
|
19
|
+
|
|
20
|
+
method_prefix = if prefix == true
|
|
21
|
+
"#{to}_"
|
|
22
|
+
else
|
|
23
|
+
prefix ? "#{prefix}_" : ""
|
|
24
|
+
end
|
|
25
|
+
methods.each do |m|
|
|
26
|
+
(DelegateTracker.registry[self] ||= {})[:"#{method_prefix}#{m}"] = {to: to, allow_nil: !!allow_nil, original: m.to_sym}
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
Module.prepend(Typelizer::DelegateTracker::Hook) if Typelizer.enabled?
|
data/lib/typelizer/generator.rb
CHANGED
|
@@ -9,37 +9,12 @@ module Typelizer
|
|
|
9
9
|
def call(force: false)
|
|
10
10
|
return [] unless Typelizer.enabled?
|
|
11
11
|
|
|
12
|
-
load_serializers
|
|
13
|
-
serializers = target_serializers
|
|
14
|
-
|
|
15
12
|
Typelizer.configuration.writers.each do |writer_name, writer_config|
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
interfaces = Typelizer.interfaces(writer_name: writer_name)
|
|
14
|
+
raise ArgumentError, "No serializers found. Please ensure all your serializers include Typelizer::DSL." if interfaces.empty?
|
|
18
15
|
|
|
19
16
|
Writer.new(writer_config).call(interfaces, force: force)
|
|
20
17
|
end
|
|
21
|
-
|
|
22
|
-
serializers
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
private
|
|
26
|
-
|
|
27
|
-
def load_serializers
|
|
28
|
-
Typelizer.dirs.flat_map { |dir| Dir["#{dir}/**/*.rb"] }.each do |file|
|
|
29
|
-
require file
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def target_serializers
|
|
34
|
-
base_classes = Typelizer.base_classes.filter_map do |base_class|
|
|
35
|
-
Object.const_get(base_class) if Object.const_defined?(base_class)
|
|
36
|
-
end.tap do |base_classes|
|
|
37
|
-
raise ArgumentError, "Please ensure all your serializers include Typelizer::DSL." if base_classes.none?
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
(base_classes + base_classes.flat_map(&:descendants)).uniq
|
|
41
|
-
.reject { |serializer| Typelizer.reject_class.call(serializer: serializer) }
|
|
42
|
-
.sort_by(&:name)
|
|
43
18
|
end
|
|
44
19
|
end
|
|
45
20
|
end
|
data/lib/typelizer/interface.rb
CHANGED
|
@@ -175,7 +175,7 @@ module Typelizer
|
|
|
175
175
|
multi_attrs = serializer.respond_to?(:_typelizer_multi_attributes) ? serializer._typelizer_multi_attributes : Set.new
|
|
176
176
|
|
|
177
177
|
props.map do |prop|
|
|
178
|
-
has_dsl = dsl_attrs
|
|
178
|
+
has_dsl = dsl_attrs_for(prop, dsl_attrs)&.any?
|
|
179
179
|
|
|
180
180
|
prop
|
|
181
181
|
.then { |p| apply_dsl_type(p, dsl_attrs) }
|
|
@@ -185,13 +185,30 @@ module Typelizer
|
|
|
185
185
|
end
|
|
186
186
|
end
|
|
187
187
|
|
|
188
|
+
def dsl_attrs_for(prop, dsl_attrs)
|
|
189
|
+
dsl_attrs[prop.column_name.to_sym] || dsl_attrs[prop.name.to_sym]
|
|
190
|
+
end
|
|
191
|
+
|
|
188
192
|
def apply_dsl_type(prop, dsl_attrs)
|
|
189
|
-
dsl_type = dsl_attrs
|
|
193
|
+
dsl_type = dsl_attrs_for(prop, dsl_attrs)
|
|
190
194
|
return prop unless dsl_type&.any?
|
|
191
195
|
|
|
196
|
+
dsl_type = resolve_class_type(dsl_type)
|
|
192
197
|
prop.with(**dsl_type)
|
|
193
198
|
end
|
|
194
199
|
|
|
200
|
+
def resolve_class_type(attrs)
|
|
201
|
+
type = attrs[:type]
|
|
202
|
+
return attrs unless type.is_a?(String) || type.is_a?(Symbol)
|
|
203
|
+
|
|
204
|
+
klass = Object.const_get(type.to_s)
|
|
205
|
+
return attrs unless klass.respond_to?(:typelizer_config)
|
|
206
|
+
|
|
207
|
+
attrs.merge(type: context.interface_for(klass))
|
|
208
|
+
rescue NameError
|
|
209
|
+
attrs
|
|
210
|
+
end
|
|
211
|
+
|
|
195
212
|
def apply_model_inference(prop)
|
|
196
213
|
model_plugin.infer_types(prop)
|
|
197
214
|
end
|
|
@@ -12,6 +12,7 @@ module Typelizer
|
|
|
12
12
|
infer_types_for_association(prop) ||
|
|
13
13
|
infer_types_for_column(prop) ||
|
|
14
14
|
infer_types_for_association_ids(prop) ||
|
|
15
|
+
infer_types_for_delegate(prop) ||
|
|
15
16
|
infer_types_for_attribute(prop)
|
|
16
17
|
|
|
17
18
|
prop
|
|
@@ -66,6 +67,7 @@ module Typelizer
|
|
|
66
67
|
column = model_class&.columns_hash&.dig(prop.column_name.to_s)
|
|
67
68
|
return nil unless column
|
|
68
69
|
|
|
70
|
+
prop.column_type = column.type
|
|
69
71
|
prop.multi = !!column.try(:array)
|
|
70
72
|
case config.null_strategy
|
|
71
73
|
when :nullable
|
|
@@ -100,6 +102,25 @@ module Typelizer
|
|
|
100
102
|
prop
|
|
101
103
|
end
|
|
102
104
|
|
|
105
|
+
def infer_types_for_delegate(prop)
|
|
106
|
+
return nil unless model_class
|
|
107
|
+
|
|
108
|
+
info = DelegateTracker[model_class, prop.column_name.to_sym]
|
|
109
|
+
return nil unless info
|
|
110
|
+
|
|
111
|
+
assoc = model_class.reflect_on_association(info[:to])
|
|
112
|
+
return nil unless assoc
|
|
113
|
+
|
|
114
|
+
target = assoc.klass
|
|
115
|
+
col = target.columns_hash[info[:original].to_s]
|
|
116
|
+
return nil unless col
|
|
117
|
+
|
|
118
|
+
prop.type = @config.type_mapping[col.type]
|
|
119
|
+
prop.multi = !!col.try(:array)
|
|
120
|
+
prop.nullable = col.null || info[:allow_nil]
|
|
121
|
+
prop
|
|
122
|
+
end
|
|
123
|
+
|
|
103
124
|
def infer_types_for_attribute(prop)
|
|
104
125
|
return nil unless model_class.respond_to?(:attribute_types)
|
|
105
126
|
|
|
@@ -111,9 +132,11 @@ module Typelizer
|
|
|
111
132
|
end
|
|
112
133
|
|
|
113
134
|
if attribute_type_obj.respond_to?(:subtype)
|
|
135
|
+
prop.column_type = attribute_type_obj.subtype.type
|
|
114
136
|
prop.type = @config.type_mapping[attribute_type_obj.subtype.type]
|
|
115
137
|
prop.multi = true
|
|
116
138
|
elsif attribute_type_obj.respond_to?(:type)
|
|
139
|
+
prop.column_type = attribute_type_obj.type
|
|
117
140
|
prop.type = @config.type_mapping[attribute_type_obj.type]
|
|
118
141
|
end
|
|
119
142
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typelizer
|
|
4
|
+
class OpenAPI
|
|
5
|
+
SUPPORTED_VERSIONS = ["3.0", "3.1"].freeze
|
|
6
|
+
OPENAPI_TYPES = %i[integer number string boolean object array null].freeze
|
|
7
|
+
|
|
8
|
+
COLUMN_TYPE_MAP = {
|
|
9
|
+
integer: {type: :integer},
|
|
10
|
+
bigint: {type: :integer, format: :int64},
|
|
11
|
+
decimal: {type: :number, format: :double},
|
|
12
|
+
float: {type: :number, format: :float},
|
|
13
|
+
boolean: {type: :boolean},
|
|
14
|
+
string: {type: :string},
|
|
15
|
+
text: {type: :string},
|
|
16
|
+
citext: {type: :string},
|
|
17
|
+
uuid: {type: :string, format: :uuid},
|
|
18
|
+
date: {type: :string, format: :date},
|
|
19
|
+
datetime: {type: :string, format: :"date-time"},
|
|
20
|
+
time: {type: :string, format: :time},
|
|
21
|
+
json: {type: :object},
|
|
22
|
+
jsonb: {type: :object},
|
|
23
|
+
binary: {type: :string, format: :binary},
|
|
24
|
+
inet: {type: :string},
|
|
25
|
+
cidr: {type: :string}
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
def self.schema_for(interface, openapi_version: "3.0")
|
|
29
|
+
raise ArgumentError, "Unsupported openapi_version: #{openapi_version}. Must be one of: #{SUPPORTED_VERSIONS.join(", ")}" unless SUPPORTED_VERSIONS.include?(openapi_version.to_s)
|
|
30
|
+
|
|
31
|
+
required_props = interface.properties.reject(&:optional).map(&:name)
|
|
32
|
+
schema = {
|
|
33
|
+
type: :object,
|
|
34
|
+
properties: interface.properties.to_h { |prop| [prop.name, property_schema(prop, openapi_version: openapi_version)] }
|
|
35
|
+
}
|
|
36
|
+
schema[:required] = required_props if required_props.any?
|
|
37
|
+
schema
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.property_schema(property, openapi_version: "3.0")
|
|
41
|
+
raise ArgumentError, "Unsupported openapi_version: #{openapi_version}. Must be one of: #{SUPPORTED_VERSIONS.join(", ")}" unless SUPPORTED_VERSIONS.include?(openapi_version.to_s)
|
|
42
|
+
|
|
43
|
+
definition = base_type(property)
|
|
44
|
+
ref = definition.delete("$ref")
|
|
45
|
+
|
|
46
|
+
definition = if ref
|
|
47
|
+
ref_schema(ref, property, openapi_version: openapi_version)
|
|
48
|
+
else
|
|
49
|
+
inline_schema(definition, property, openapi_version: openapi_version)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
if property.multi
|
|
53
|
+
definition = {type: :array, items: definition}
|
|
54
|
+
if property.nullable
|
|
55
|
+
if openapi_version.to_s >= "3.1"
|
|
56
|
+
definition[:type] = [:array, :null]
|
|
57
|
+
else
|
|
58
|
+
definition[:nullable] = true
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
definition
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.ref_schema(ref, property, openapi_version:)
|
|
67
|
+
has_siblings = property.nullable || property.comment.is_a?(String) || property.deprecated
|
|
68
|
+
ref_obj = {"$ref" => ref}
|
|
69
|
+
|
|
70
|
+
if openapi_version.to_s >= "3.1"
|
|
71
|
+
definition = property.nullable ? {oneOf: [ref_obj, {type: :null}]} : ref_obj
|
|
72
|
+
else
|
|
73
|
+
# In 3.0, $ref must stand alone — use allOf wrapper when siblings are needed
|
|
74
|
+
definition = has_siblings ? {allOf: [ref_obj]} : ref_obj
|
|
75
|
+
definition[:nullable] = true if property.nullable
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
definition[:description] = property.comment if property.comment.is_a?(String)
|
|
79
|
+
definition[:deprecated] = true if property.deprecated
|
|
80
|
+
definition
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.inline_schema(definition, property, openapi_version:)
|
|
84
|
+
# For multi properties, nullable is applied to the array container in property_schema
|
|
85
|
+
unless property.multi
|
|
86
|
+
if property.nullable
|
|
87
|
+
if openapi_version.to_s >= "3.1"
|
|
88
|
+
definition[:type] = [definition[:type], :null]
|
|
89
|
+
else
|
|
90
|
+
definition[:nullable] = true
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
definition[:description] = property.comment if property.comment.is_a?(String)
|
|
95
|
+
if property.enum.is_a?(Array)
|
|
96
|
+
items_nullable = !property.multi && property.nullable
|
|
97
|
+
definition[:enum] = (items_nullable && !property.enum.include?(nil)) ? property.enum + [nil] : property.enum
|
|
98
|
+
end
|
|
99
|
+
definition[:deprecated] = true if property.deprecated
|
|
100
|
+
definition
|
|
101
|
+
end
|
|
102
|
+
private_class_method :ref_schema, :inline_schema
|
|
103
|
+
|
|
104
|
+
def self.base_type(property)
|
|
105
|
+
if property.type.respond_to?(:properties)
|
|
106
|
+
{"$ref" => "#/components/schemas/#{property.type.name}"}
|
|
107
|
+
elsif property.column_type && COLUMN_TYPE_MAP.key?(property.column_type)
|
|
108
|
+
result = COLUMN_TYPE_MAP[property.column_type].dup
|
|
109
|
+
result[:type] = :string if property.enum
|
|
110
|
+
result
|
|
111
|
+
elsif property.type.is_a?(String) && !OPENAPI_TYPES.include?(property.type.to_sym)
|
|
112
|
+
{"$ref" => "#/components/schemas/#{property.type}"}
|
|
113
|
+
else
|
|
114
|
+
type = property.type.to_s.to_sym
|
|
115
|
+
if OPENAPI_TYPES.include?(type)
|
|
116
|
+
{type: type}
|
|
117
|
+
else
|
|
118
|
+
{type: :object}
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
private_class_method :base_type
|
|
123
|
+
end
|
|
124
|
+
end
|
data/lib/typelizer/property.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module Typelizer
|
|
2
2
|
Property = Struct.new(
|
|
3
3
|
:name, :type, :optional, :nullable,
|
|
4
|
-
:multi, :column_name, :comment, :enum, :enum_type_name, :deprecated,
|
|
4
|
+
:multi, :column_name, :column_type, :comment, :enum, :enum_type_name, :deprecated,
|
|
5
5
|
:with_traits,
|
|
6
6
|
keyword_init: true
|
|
7
7
|
) do
|
|
@@ -52,7 +52,8 @@ module Typelizer
|
|
|
52
52
|
def fingerprint
|
|
53
53
|
# Use array format for consistent output across Ruby versions
|
|
54
54
|
# (Hash#inspect format changed in Ruby 3.4)
|
|
55
|
-
|
|
55
|
+
# Exclude fields that do not affect generated TypeScript output
|
|
56
|
+
to_h.except(:column_type).merge(type: UnionTypeSorter.sort(type_name(sort_order: :alphabetical), :alphabetical))
|
|
56
57
|
.to_a.inspect
|
|
57
58
|
end
|
|
58
59
|
|
|
@@ -34,7 +34,7 @@ module Typelizer
|
|
|
34
34
|
def infer_types(props, typelizes)
|
|
35
35
|
props.map do |prop|
|
|
36
36
|
# First check for typelize DSL in the trait
|
|
37
|
-
dsl_type = typelizes[prop.column_name.to_sym]
|
|
37
|
+
dsl_type = typelizes[prop.column_name.to_sym] || typelizes[prop.name.to_sym]
|
|
38
38
|
if dsl_type&.any?
|
|
39
39
|
next prop.with(**dsl_type).tap do |property|
|
|
40
40
|
property.comment ||= model_plugin.comment_for(property) if config.comments && property.comment != false
|
data/lib/typelizer/version.rb
CHANGED
data/lib/typelizer.rb
CHANGED
|
@@ -16,6 +16,7 @@ require_relative "typelizer/import_sorter"
|
|
|
16
16
|
require_relative "typelizer/interface"
|
|
17
17
|
require_relative "typelizer/renderer"
|
|
18
18
|
require_relative "typelizer/writer"
|
|
19
|
+
require_relative "typelizer/openapi"
|
|
19
20
|
require_relative "typelizer/generator"
|
|
20
21
|
require_relative "typelizer/type_parser"
|
|
21
22
|
require_relative "typelizer/dsl"
|
|
@@ -62,8 +63,35 @@ module Typelizer
|
|
|
62
63
|
yield configuration
|
|
63
64
|
end
|
|
64
65
|
|
|
66
|
+
def interfaces(writer_name: nil)
|
|
67
|
+
load_serializers
|
|
68
|
+
context = WriterContext.new(writer_name: writer_name)
|
|
69
|
+
target_serializers(context.writer_config.reject_class)
|
|
70
|
+
.map { |klass| context.interface_for(klass) }
|
|
71
|
+
.reject(&:empty?)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def openapi_schemas(writer_name: nil, openapi_version: "3.0")
|
|
75
|
+
interfaces(writer_name: writer_name).to_h { |i| [i.name, OpenAPI.schema_for(i, openapi_version: openapi_version)] }
|
|
76
|
+
end
|
|
77
|
+
|
|
65
78
|
private
|
|
66
79
|
|
|
80
|
+
def load_serializers
|
|
81
|
+
dirs.flat_map { |dir| Dir["#{dir}/**/*.rb"] }.each { |file| require file }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def target_serializers(reject_class)
|
|
85
|
+
resolved = base_classes.filter_map do |base_class|
|
|
86
|
+
Object.const_get(base_class) if Object.const_defined?(base_class)
|
|
87
|
+
end
|
|
88
|
+
raise ArgumentError, "No serializers found. Please ensure all your serializers include Typelizer::DSL." if base_classes.any? && resolved.none?
|
|
89
|
+
|
|
90
|
+
(resolved + resolved.flat_map(&:descendants)).uniq
|
|
91
|
+
.reject { |serializer| reject_class.call(serializer: serializer) }
|
|
92
|
+
.sort_by(&:name)
|
|
93
|
+
end
|
|
94
|
+
|
|
67
95
|
attr_writer :base_classes
|
|
68
96
|
end
|
|
69
97
|
|
|
@@ -72,3 +100,5 @@ module Typelizer
|
|
|
72
100
|
|
|
73
101
|
self.base_classes = Set.new
|
|
74
102
|
end
|
|
103
|
+
|
|
104
|
+
require_relative "typelizer/delegate_tracker"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: typelizer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Svyatoslav Kryukov
|
|
@@ -51,6 +51,7 @@ files:
|
|
|
51
51
|
- lib/typelizer/config.rb
|
|
52
52
|
- lib/typelizer/configuration.rb
|
|
53
53
|
- lib/typelizer/contexts/writer_context.rb
|
|
54
|
+
- lib/typelizer/delegate_tracker.rb
|
|
54
55
|
- lib/typelizer/dsl.rb
|
|
55
56
|
- lib/typelizer/dsl/hooks.rb
|
|
56
57
|
- lib/typelizer/dsl/hooks/alba.rb
|
|
@@ -64,6 +65,7 @@ files:
|
|
|
64
65
|
- lib/typelizer/model_plugins/active_record.rb
|
|
65
66
|
- lib/typelizer/model_plugins/auto.rb
|
|
66
67
|
- lib/typelizer/model_plugins/poro.rb
|
|
68
|
+
- lib/typelizer/openapi.rb
|
|
67
69
|
- lib/typelizer/property.rb
|
|
68
70
|
- lib/typelizer/property_sorter.rb
|
|
69
71
|
- lib/typelizer/railtie.rb
|