typelizer 0.7.0 → 0.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 819c25a9bfc5dd74c3b7c77045bb5f5ef968f518f8a8d228502badb6936ea50d
4
- data.tar.gz: c8d8d9affec2d6eb07cf1e58942fe46009fad2782c57689218b8357a85c227f0
3
+ metadata.gz: d8a6cb6187a0670a589346d969c61a17ae0b9fd60a4e6926c74c2e72c9594130
4
+ data.tar.gz: 4ef1a1e6e728df5ab1d5198a0254cfa68e5e477da57fb9c3c6b7ae2f7efa945d
5
5
  SHA512:
6
- metadata.gz: b4b39192ff398bb25c6b537fcb5c73893de40d82f14a09a254e4ef8317a08232819c3285b7e6806938f1c7058f895d0100b03884574158bbaa2e329982518683
7
- data.tar.gz: 8971c5a2cc78992fd38dc101e7b2fc1eaefe82ad756f828bca4416a4a6ed54d1ff25eace61fd80cd293cca3dc66b6b6c24706a0fa45156fb275eb9bfe4976c46
6
+ metadata.gz: 56296749f1d5478e98457afac0d2e8599e360d5b7864d596772c825e9970471de8b1d0103ada0120b9adeddab2805e65976c496ce0963785dfd39ddc3dffae91
7
+ data.tar.gz: 25f8667ef0f54560e1c84e8d906b50abc55e01ea761dacff012657bc7d94f7127263166b81c4e317350ee89219f92a070aa042004f42cfad3a6482fe31a0c86f
data/CHANGELOG.md CHANGED
@@ -7,6 +7,60 @@ and this project adheres to [Semantic Versioning].
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.0] - 2026-02-26
11
+
12
+ ### Added
13
+
14
+ - Alba: nested attributes (`nested` / `nested_attribute`) now generate inline nested TypeScript types with full type inference support, including within traits. ([@pgiblock])
15
+
16
+ - OpenAPI: support for traits in schema generation. ([@skryukov])
17
+
18
+ - Union types in `typelize` for polymorphic associations. Supports serializer class references, pipe-delimited strings, and plain TypeScript type names. ([@skryukov])
19
+
20
+ ```ruby
21
+ typelize commentable: [UserResource, CommentResource]
22
+ typelize approver: "AuthorResource | null"
23
+ typelize content: "TextBlock | ImageBlock"
24
+ ```
25
+
26
+ ### Fixed
27
+
28
+ - OpenAPI: TypeScript-only types (`any`, `unknown`, `never`) and generic types (`Record<string, unknown>`, `Partial<T>`, etc.) no longer produce invalid `$ref` entries. They are mapped to `{type: :object}` instead. ([@skryukov])
29
+ - OpenAPI: fix nullable arrays producing incorrect schemas. ([@skryukov])
30
+ - Fix Typelizer not loading gracefully when required gems are missing at boot time. ([@skryukov])
31
+
32
+ ### Changed
33
+
34
+ - **Internal:** Union types are now stored as arrays of symbols instead of pipe-delimited strings. This fixes import resolution for serializer classes inside unions and eliminates redundant string splitting/joining across the DSL, Interface, and OpenAPI layers. ([@skryukov])
35
+
36
+ ## [0.8.0] - 2026-02-19
37
+
38
+ ### Added
39
+
40
+ - OpenAPI schema generation from serializers, supporting both OpenAPI 3.0 and 3.1. ([@skryukov])
41
+
42
+ ```ruby
43
+ # Get all schemas as a hash
44
+ Typelizer.openapi_schemas
45
+ # => { "Post" => { type: :object, properties: { ... }, required: [...] }, ... }
46
+
47
+ # OpenAPI 3.1 output
48
+ Typelizer.openapi_schemas(openapi_version: "3.1")
49
+ ```
50
+
51
+ Column types are automatically mapped to OpenAPI types with proper formats (`integer`, `int64`, `uuid`, `date-time`, etc.).
52
+ Enums, nullable fields, arrays, deprecated flags, and `$ref` associations are all handled automatically.
53
+
54
+ - 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])
55
+
56
+ - Reference other serializers in `typelize` method by passing the class directly. ([@skryukov])
57
+
58
+ - 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])
59
+
60
+ ### Fixed
61
+
62
+ - `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])
63
+
10
64
  ## [0.7.0] - 2026-01-15
11
65
 
12
66
  ### Changed
@@ -353,12 +407,15 @@ and this project adheres to [Semantic Versioning].
353
407
  [@NOX73]: https://github.com/NOX73
354
408
  [@okuramasafumi]: https://github.com/okuramasafumi
355
409
  [@patvice]: https://github.com/patvice
410
+ [@pgiblock]: https://github.com/pgiblock
411
+ [@prog-supdex]: https://github.com/prog-supdex
356
412
  [@PedroAugustoRamalhoDuarte]: https://github.com/PedroAugustoRamalhoDuarte
357
413
  [@skryukov]: https://github.com/skryukov
358
- [@prog-supdex]: https://github.com/prog-supdex
359
414
  [@ventsislaf]: https://github.com/ventsislaf
360
415
 
361
- [Unreleased]: https://github.com/skryukov/typelizer/compare/v0.7.0...HEAD
416
+ [Unreleased]: https://github.com/skryukov/typelizer/compare/v0.9.0...HEAD
417
+ [0.9.0]: https://github.com/skryukov/typelizer/compare/v0.8.0...v0.9.0
418
+ [0.8.0]: https://github.com/skryukov/typelizer/compare/v0.7.0...v0.8.0
362
419
  [0.7.0]: https://github.com/skryukov/typelizer/compare/v0.6.0...v0.7.0
363
420
  [0.6.0]: https://github.com/skryukov/typelizer/compare/v0.5.6...v0.6.0
364
421
  [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,77 @@ 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
+
152
+ Union types are supported for polymorphic associations. You can use serializer class references, which resolve to their generated type names:
153
+
154
+ ```ruby
155
+ class PostResource < ApplicationResource
156
+ attributes :id, :title
157
+
158
+ # Union of two serializers — resolves to generated type names
159
+ typelize commentable: [UserResource, CommentResource]
160
+ attribute :commentable
161
+
162
+ # Nullable union — extracts null and marks as nullable
163
+ typelize approver: "AuthorResource | null"
164
+ attribute :approver
165
+
166
+ # Pipe-delimited string with serializer names
167
+ typelize target: "UserResource | CommentResource"
168
+ attribute :target
169
+
170
+ # String and class constant can be mixed
171
+ typelize item: ["Namespace::UserResource", CommentResource]
172
+ attribute :item
173
+ end
174
+ ```
175
+
176
+ You can also use plain TypeScript type names for custom types that aren't backed by serializers:
177
+
178
+ ```ruby
179
+ class PostResource < ApplicationResource
180
+ attributes :id, :title
181
+
182
+ # Plain type names — passed through as-is to TypeScript
183
+ typelize content: "TextBlock | ImageBlock"
184
+ attribute :content
185
+
186
+ # Works with arrays too
187
+ typelize sections: ["TextBlock", "ImageBlock"]
188
+ attribute :sections
189
+ end
190
+ ```
191
+
192
+ This generates:
193
+
194
+ ```typescript
195
+ type Post = {
196
+ id: number;
197
+ title: string;
198
+ content: TextBlock | ImageBlock;
199
+ sections: TextBlock | ImageBlock;
200
+ }
201
+ ```
202
+
131
203
  For more complex type definitions, use the full API:
132
204
 
133
205
  ```ruby
@@ -310,6 +382,60 @@ Typelizer.listen = false
310
382
 
311
383
  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
384
 
385
+ ## OpenAPI Schema Generation
386
+
387
+ 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).
388
+
389
+ Get all schemas as a hash:
390
+
391
+ ```ruby
392
+ Typelizer.openapi_schemas
393
+ # => {
394
+ # "Post" => {
395
+ # type: :object,
396
+ # properties: {
397
+ # id: { type: :integer },
398
+ # title: { type: :string },
399
+ # published_at: { type: :string, format: :"date-time", nullable: true }
400
+ # },
401
+ # required: [:id, :title]
402
+ # },
403
+ # "Author" => { ... }
404
+ # }
405
+ ```
406
+
407
+ 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`):
408
+
409
+ ```ruby
410
+ Typelizer.openapi_schemas(openapi_version: "3.1")
411
+ ```
412
+
413
+ Generate a schema for a single interface:
414
+
415
+ ```ruby
416
+ interfaces = Typelizer.interfaces
417
+ post_interface = interfaces.find { |i| i.name == "Post" }
418
+ Typelizer::OpenAPI.schema_for(post_interface)
419
+ Typelizer::OpenAPI.schema_for(post_interface, openapi_version: "3.1")
420
+ ```
421
+
422
+ Column types are mapped to OpenAPI types automatically:
423
+
424
+ | Column type | OpenAPI type | Format |
425
+ |---|---|---|
426
+ | `integer` | `integer` | |
427
+ | `bigint` | `integer` | `int64` |
428
+ | `float` | `number` | `float` |
429
+ | `decimal` | `number` | `double` |
430
+ | `boolean` | `boolean` | |
431
+ | `string`, `text`, `citext` | `string` | |
432
+ | `uuid` | `string` | `uuid` |
433
+ | `date` | `string` | `date` |
434
+ | `datetime` | `string` | `date-time` |
435
+ | `time` | `string` | `time` |
436
+
437
+ Enums, nullable fields, arrays, deprecated flags, and `$ref` associations are all handled automatically.
438
+
313
439
  ## Configuration
314
440
 
315
441
  Typelizer provides several global configuration options:
@@ -19,13 +19,15 @@ 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
- serializers = block.call
23
+ block.call
25
24
  end
26
25
 
26
+ interfaces = Typelizer.interfaces
27
+ raise ArgumentError, "No serializers found. Please ensure all your serializers include Typelizer::DSL." if interfaces.empty?
28
+
27
29
  puts "Finished in #{time} seconds"
28
- puts "Found #{serializers.size} serializers:"
29
- puts serializers.map { |s| "\t#{s.name}" }.join("\n")
30
+ puts "Found #{interfaces.size} serializers:"
31
+ puts interfaces.map { |i| "\t#{i.name}" }.join("\n")
30
32
  end
31
33
  end
@@ -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, :reject_class, :listen
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) && !ActiveRecord.autoload?(: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/dsl.rb CHANGED
@@ -104,16 +104,16 @@ module Typelizer
104
104
  options = attrs.last.is_a?(Hash) ? attrs.pop : {}
105
105
 
106
106
  if attrs.any?
107
- # Parse type shortcuts and merge options
108
107
  parsed_types = attrs.map { |t| TypeParser.parse(t) }
109
- type_names = parsed_types.map { |p| p[:type] }
110
- options[:type] = type_names.join(" | ")
111
-
112
- # Merge modifier flags from all parsed types
108
+ all_types = parsed_types.flat_map { |p| Array(p[:type]) }
113
109
  parsed_types.each do |parsed|
114
110
  options[:optional] = true if parsed[:optional]
115
111
  options[:multi] = true if parsed[:multi]
112
+ options[:nullable] = true if parsed[:nullable]
116
113
  end
114
+ options[:nullable] = true if all_types.delete(:null)
115
+ # Unwrap single-element arrays: typelize field: ["string"] behaves like typelize field: "string"
116
+ options[:type] = (all_types.size == 1) ? all_types.first : all_types
117
117
  end
118
118
 
119
119
  instance_variable_get(instance_variable)[name.to_sym] ||= {}
@@ -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
- context = WriterContext.new(writer_name: writer_name)
17
- interfaces = serializers.map { |klass| context.interface_for(klass) }
13
+ interfaces = Typelizer.interfaces(writer_name: writer_name)
14
+ next 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
@@ -1,5 +1,9 @@
1
+ require_relative "type_inference"
2
+
1
3
  module Typelizer
2
4
  class Interface
5
+ include TypeInference
6
+
3
7
  attr_reader :serializer, :context
4
8
 
5
9
  def initialize(serializer:, context:)
@@ -60,7 +64,7 @@ module Typelizer
60
64
 
61
65
  def enum_types
62
66
  @enum_types ||= begin
63
- all_properties = properties + trait_interfaces.flat_map(&:properties)
67
+ all_properties = collect_all_properties(properties + trait_interfaces.flat_map(&:properties))
64
68
  all_properties
65
69
  .select(&:enum_definition)
66
70
  .uniq(&:enum_type_name)
@@ -104,12 +108,12 @@ module Typelizer
104
108
 
105
109
  def imports
106
110
  @imports ||= begin
107
- # Include both main properties and trait properties for import collection
108
- all_properties = properties_to_print + trait_interfaces.flat_map(&:properties)
111
+ # Include both main properties and trait properties for import collection,
112
+ # recursively including nested sub-properties
113
+ all_properties = collect_all_properties(properties_to_print + trait_interfaces.flat_map(&:properties))
109
114
 
110
- association_serializers, attribute_types = all_properties.filter_map(&:type)
111
- .uniq
112
- .partition { |type| type.is_a?(Interface) }
115
+ flat_types = all_properties.filter_map(&:type).flat_map { |t| Array(t) }.uniq
116
+ association_serializers, attribute_types = flat_types.partition { |type| type.is_a?(Interface) }
113
117
 
114
118
  serializer_types = association_serializers
115
119
  .filter_map { |interface| interface.name if interface.name != name && !interface.inline? }
@@ -158,6 +162,16 @@ module Typelizer
158
162
 
159
163
  private
160
164
 
165
+ def collect_all_properties(props)
166
+ props.flat_map do |prop|
167
+ if prop.nested_properties&.any?
168
+ [prop] + collect_all_properties(prop.nested_properties)
169
+ else
170
+ [prop]
171
+ end
172
+ end
173
+ end
174
+
161
175
  def self_type_name
162
176
  serializer.name.match(/(\w+::)?(\w+)(Serializer|Resource)/)[2]
163
177
  end
@@ -175,54 +189,63 @@ module Typelizer
175
189
  multi_attrs = serializer.respond_to?(:_typelizer_multi_attributes) ? serializer._typelizer_multi_attributes : Set.new
176
190
 
177
191
  props.map do |prop|
178
- has_dsl = dsl_attrs[prop.column_name.to_sym]&.any?
192
+ has_dsl = dsl_attrs_for(prop, dsl_attrs)&.any?
179
193
 
180
194
  prop
181
195
  .then { |p| apply_dsl_type(p, dsl_attrs) }
182
196
  .then { |p| has_dsl ? p : apply_model_inference(p) }
183
197
  .then { |p| apply_multi_flag(p, multi_attrs) }
184
198
  .then { |p| apply_metadata(p) }
199
+ .then { |p| infer_nested_property_types(p) }
185
200
  end
186
201
  end
187
202
 
203
+ def dsl_attrs_for(prop, dsl_attrs)
204
+ dsl_attrs[prop.column_name.to_sym] || dsl_attrs[prop.name.to_sym]
205
+ end
206
+
188
207
  def apply_dsl_type(prop, dsl_attrs)
189
- dsl_type = dsl_attrs[prop.column_name.to_sym]
208
+ dsl_type = dsl_attrs_for(prop, dsl_attrs)
190
209
  return prop unless dsl_type&.any?
191
210
 
211
+ dsl_type = resolve_class_type(dsl_type)
192
212
  prop.with(**dsl_type)
193
213
  end
194
214
 
195
- def apply_model_inference(prop)
196
- model_plugin.infer_types(prop)
197
- end
215
+ def resolve_class_type(attrs)
216
+ type = attrs[:type]
198
217
 
199
- def apply_multi_flag(prop, multi_attrs)
200
- return prop unless multi_attrs.include?(prop.column_name.to_sym)
201
-
202
- prop.with(multi: true)
203
- end
204
-
205
- def apply_metadata(prop)
206
- prop.tap do |p|
207
- p.comment ||= model_plugin.comment_for(p) if config.comments && p.comment != false
208
- p.enum ||= model_plugin.enum_for(p) if p.enum != false
218
+ case type
219
+ when Array
220
+ resolve_union_class_types(attrs)
221
+ when String, Symbol
222
+ resolve_single_class_type(attrs)
223
+ else
224
+ attrs
209
225
  end
210
226
  end
211
227
 
212
- def model_class
213
- return serializer._typelizer_model_name if serializer.respond_to?(:_typelizer_model_name)
228
+ def resolve_single_class_type(attrs)
229
+ attrs.merge(type: resolve_type_part(attrs[:type]))
230
+ end
214
231
 
215
- # Execute the `serializer_model_mapper` lambda in the context of the `config` object
216
- # This giving a possibility to access other lambdas, for example, `serializer_name_mapper`
217
- config.instance_exec(serializer, &config.serializer_model_mapper)
218
- rescue NameError => e
219
- Typelizer.logger.debug("model_mapper failed for serializer #{serializer.name}: #{e.class}: #{e.message}")
232
+ def resolve_union_class_types(attrs)
233
+ resolved = attrs[:type].map { |part| resolve_type_part(part) }
234
+ # Unwrap single-element arrays (e.g., after null extraction from ["Serializer", null])
235
+ attrs.merge(type: (resolved.size == 1) ? resolved.first : resolved)
236
+ end
220
237
 
221
- nil
238
+ def resolve_type_part(part)
239
+ klass = Object.const_get(part.to_s)
240
+ klass.respond_to?(:typelizer_config) ? context.interface_for(klass) : part
241
+ rescue NameError
242
+ part
222
243
  end
223
244
 
224
- def model_plugin
225
- @model_plugin ||= config.model_plugin.new(model_class: model_class, config: config)
245
+ def apply_multi_flag(prop, multi_attrs)
246
+ return prop unless multi_attrs.include?(prop.column_name.to_sym)
247
+
248
+ prop.with(multi: true)
226
249
  end
227
250
  end
228
251
  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,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typelizer
4
+ module OpenAPI
5
+ SUPPORTED_VERSIONS = ["3.0", "3.1"].freeze
6
+
7
+ OPENAPI_TYPES = %i[integer number string boolean object array null].freeze
8
+ TS_OBJECT_TYPES = %w[any unknown never Record Partial Pick Omit].freeze
9
+
10
+ COLUMN_TYPE_MAP = {
11
+ integer: {type: :integer},
12
+ bigint: {type: :integer, format: :int64},
13
+ decimal: {type: :number, format: :double},
14
+ float: {type: :number, format: :float},
15
+ boolean: {type: :boolean},
16
+ string: {type: :string},
17
+ text: {type: :string},
18
+ citext: {type: :string},
19
+ uuid: {type: :string, format: :uuid},
20
+ date: {type: :string, format: :date},
21
+ datetime: {type: :string, format: :"date-time"},
22
+ time: {type: :string, format: :time},
23
+ json: {type: :object},
24
+ jsonb: {type: :object},
25
+ binary: {type: :string, format: :binary},
26
+ inet: {type: :string},
27
+ cidr: {type: :string}
28
+ }.freeze
29
+
30
+ class << self
31
+ def schema_for(interface, openapi_version: "3.0")
32
+ validate_version!(openapi_version)
33
+
34
+ required_props = interface.properties.reject(&:optional).map(&:name)
35
+ schema = {
36
+ type: :object,
37
+ properties: interface.properties.to_h { |prop| [prop.name, property_schema(prop, openapi_version: openapi_version)] }
38
+ }
39
+ schema[:required] = required_props if required_props.any?
40
+ schema
41
+ end
42
+
43
+ def property_schema(property, openapi_version: "3.0")
44
+ if property.type.is_a?(Array)
45
+ return union_schema(property, openapi_version: openapi_version)
46
+ end
47
+
48
+ definition = base_type(property, openapi_version: openapi_version)
49
+ ref = definition.delete("$ref")
50
+
51
+ definition = if ref
52
+ ref_schema(ref, property, openapi_version: openapi_version)
53
+ else
54
+ inline_schema(definition, property, openapi_version: openapi_version)
55
+ end
56
+
57
+ definition = wrap_traits(definition, property, openapi_version: openapi_version)
58
+ wrap_multi(definition, property, openapi_version: openapi_version)
59
+ end
60
+
61
+ private
62
+
63
+ def ref_schema(ref, property, openapi_version:)
64
+ ref_obj = {"$ref" => ref}
65
+ item_nullable = !property.multi && property.nullable
66
+
67
+ if v31?(openapi_version)
68
+ definition = item_nullable ? {oneOf: [ref_obj, {type: :null}]} : ref_obj
69
+ else
70
+ needs_wrapper = item_nullable || (!property.multi && (property.comment.is_a?(String) || property.deprecated))
71
+ definition = needs_wrapper ? {allOf: [ref_obj]} : ref_obj
72
+ definition[:nullable] = true if item_nullable
73
+ end
74
+
75
+ apply_metadata(definition, property) unless property.multi
76
+ definition
77
+ end
78
+
79
+ def inline_schema(definition, property, openapi_version:)
80
+ unless property.multi
81
+ apply_nullable(definition, property, openapi_version: openapi_version)
82
+ apply_metadata(definition, property)
83
+ end
84
+ if property.enum.is_a?(Array)
85
+ items_nullable = !property.multi && property.nullable
86
+ definition[:enum] = (items_nullable && !property.enum.include?(nil)) ? property.enum + [nil] : property.enum
87
+ end
88
+ definition
89
+ end
90
+
91
+ def union_schema(property, openapi_version:)
92
+ schemas = property.type.map { |part| union_member_schema(part) }
93
+
94
+ definition = {anyOf: schemas}
95
+
96
+ unless property.multi
97
+ apply_nullable(definition, property, openapi_version: openapi_version)
98
+ apply_metadata(definition, property)
99
+ end
100
+
101
+ wrap_multi(definition, property, openapi_version: openapi_version)
102
+ end
103
+
104
+ def union_member_schema(type)
105
+ if type.respond_to?(:properties)
106
+ {"$ref" => "#/components/schemas/#{type.name}"}
107
+ else
108
+ sym = type.to_sym
109
+ if OPENAPI_TYPES.include?(sym)
110
+ {type: sym}
111
+ elsif ts_only_type?(type.to_s)
112
+ {type: :object}
113
+ else
114
+ {"$ref" => "#/components/schemas/#{type}"}
115
+ end
116
+ end
117
+ end
118
+
119
+ def wrap_traits(definition, property, openapi_version:)
120
+ return definition unless property.respond_to?(:with_traits) && property.with_traits&.any? && property.type.respond_to?(:name)
121
+
122
+ trait_refs = property.with_traits.map do |t|
123
+ {"$ref" => "#/components/schemas/#{property.type.name}#{t.to_s.camelize}Trait"}
124
+ end
125
+
126
+ base_ref = definition.delete("$ref")
127
+ if base_ref
128
+ definition = {allOf: [{"$ref" => base_ref}] + trait_refs}
129
+ elsif definition[:oneOf]
130
+ non_null = definition[:oneOf].reject { |s| s[:type] == :null }
131
+ null_schemas = definition[:oneOf].select { |s| s[:type] == :null }
132
+ all_of = non_null + trait_refs
133
+ definition = null_schemas.any? ? {oneOf: [{allOf: all_of}, *null_schemas]} : {allOf: all_of}
134
+ elsif definition[:allOf]
135
+ definition[:allOf].concat(trait_refs)
136
+ else
137
+ raise ArgumentError, "Unexpected schema shape for traits on property #{property.name}: #{definition.inspect}"
138
+ end
139
+
140
+ definition[:nullable] = true if !v31?(openapi_version) && property.nullable
141
+ definition
142
+ end
143
+
144
+ def apply_metadata(definition, property)
145
+ definition[:description] = property.comment if property.comment.is_a?(String)
146
+ definition[:deprecated] = true if property.deprecated
147
+ end
148
+
149
+ def apply_nullable(definition, property, openapi_version:)
150
+ return unless property.nullable
151
+
152
+ if definition[:anyOf]
153
+ v31?(openapi_version) ? definition[:anyOf] << {type: :null} : definition[:nullable] = true
154
+ elsif definition[:type]
155
+ v31?(openapi_version) ? definition[:type] = [definition[:type], :null] : definition[:nullable] = true
156
+ end
157
+ end
158
+
159
+ def wrap_multi(definition, property, openapi_version:)
160
+ return definition unless property.multi
161
+
162
+ definition = {type: :array, items: definition}
163
+ apply_metadata(definition, property)
164
+ if property.nullable
165
+ v31?(openapi_version) ? definition[:type] = [:array, :null] : definition[:nullable] = true
166
+ end
167
+ definition
168
+ end
169
+
170
+ def base_type(property, openapi_version:)
171
+ if property.type.respond_to?(:properties)
172
+ if property.type.respond_to?(:inline?) && property.type.inline?
173
+ schema_for(property.type, openapi_version: openapi_version)
174
+ else
175
+ {"$ref" => "#/components/schemas/#{property.type.name}"}
176
+ end
177
+ elsif property.type.nil? && property.respond_to?(:nested_properties) && property.nested_properties&.any?
178
+ nested_schema(property, openapi_version: openapi_version)
179
+ elsif property.column_type && COLUMN_TYPE_MAP.key?(property.column_type)
180
+ result = COLUMN_TYPE_MAP[property.column_type].dup
181
+ result[:type] = :string if property.enum
182
+ result
183
+ elsif (property.type.is_a?(String) || property.type.is_a?(Symbol)) && !OPENAPI_TYPES.include?(property.type.to_sym) && !ts_only_type?(property.type.to_s)
184
+ {"$ref" => "#/components/schemas/#{property.type}"}
185
+ else
186
+ type = property.type.to_s.to_sym
187
+ OPENAPI_TYPES.include?(type) ? {type: type} : {type: :object}
188
+ end
189
+ end
190
+
191
+ def nested_schema(property, openapi_version:)
192
+ required = property.nested_properties.reject(&:optional).map(&:name)
193
+ schema = {
194
+ type: :object,
195
+ properties: property.nested_properties.to_h { |p| [p.name, property_schema(p, openapi_version: openapi_version)] }
196
+ }
197
+ schema[:required] = required if required.any?
198
+ schema
199
+ end
200
+
201
+ def v31?(openapi_version)
202
+ openapi_version.to_s == "3.1"
203
+ end
204
+
205
+ def ts_only_type?(type_str)
206
+ type_str.start_with?("{") || type_str.include?("<") || TS_OBJECT_TYPES.include?(type_str)
207
+ end
208
+
209
+ def validate_version!(openapi_version)
210
+ raise ArgumentError, "Unsupported openapi_version: #{openapi_version}. Must be one of: #{SUPPORTED_VERSIONS.join(", ")}" unless SUPPORTED_VERSIONS.include?(openapi_version.to_s)
211
+ end
212
+ end
213
+ end
214
+ end
@@ -1,8 +1,8 @@
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,
5
- :with_traits,
4
+ :multi, :column_name, :column_type, :comment, :enum, :enum_type_name, :deprecated,
5
+ :with_traits, :nested_properties, :nested_typelizes,
6
6
  keyword_init: true
7
7
  ) do
8
8
  def with(**attrs)
@@ -52,7 +52,13 @@ 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
- to_h.merge(type: UnionTypeSorter.sort(type_name(sort_order: :alphabetical), :alphabetical))
55
+ # Exclude fields that do not affect generated TypeScript output.
56
+ # Exclude nested_properties/nested_typelizes from to_h to avoid changing
57
+ # fingerprints for properties that don't use them.
58
+ # nested_typelizes is excluded entirely as it only affects inference, not output.
59
+ to_h.except(:column_type, :nested_properties, :nested_typelizes)
60
+ .merge(type: UnionTypeSorter.sort(type_name(sort_order: :alphabetical), :alphabetical))
61
+ .then { |h| nested_properties&.any? ? h.merge(nested_properties: nested_properties.map(&:fingerprint)) : h }
56
62
  .to_a.inspect
57
63
  end
58
64
 
@@ -89,7 +95,20 @@ module Typelizer
89
95
  return enum_values.join(" | ")
90
96
  end
91
97
 
92
- type.respond_to?(:name) ? type.name : type || "unknown"
98
+ if type.nil? && nested_properties&.any?
99
+ inner = nested_properties.map { |p|
100
+ rendered = p.render(sort_order: sort_order, prefer_double_quotes: prefer_double_quotes) + ";"
101
+ rendered.gsub(/^/, " ")
102
+ }.join("\n")
103
+ return "{\n#{inner}\n}"
104
+ end
105
+
106
+ case type
107
+ when Array
108
+ type.map { |t| t.respond_to?(:name) ? t.name : t.to_s }.join(" | ")
109
+ else
110
+ type.respond_to?(:name) ? type.name : type&.to_s || "unknown"
111
+ end
93
112
  end
94
113
  end
95
114
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Typelizer
4
4
  module SerializerPlugins
5
- class Alba::TraitAttributeCollector
5
+ class Alba::BlockAttributeCollector
6
6
  attr_reader :collected_attributes, :collected_typelizes
7
7
 
8
8
  def initialize
@@ -53,15 +53,26 @@ module Typelizer
53
53
  end
54
54
  end
55
55
 
56
- # Simple struct to hold association info from traits
57
- TraitAssociation = Struct.new(:name, :resource, :with_traits, :multi, :key, keyword_init: true)
56
+ # Simple struct to hold association info from blocks
57
+ BlockAssociation = Struct.new(:name, :resource, :with_traits, :multi, :key, keyword_init: true)
58
+
59
+ # Struct to hold nested attribute info captured within a block
60
+ BlockNestedAttribute = Struct.new(:name, :block, keyword_init: true)
61
+
62
+ def nested_attribute(name, **options, &block)
63
+ raise ArgumentError, "Block is required for nested_attribute" unless block
64
+
65
+ @collected_attributes[name] = BlockNestedAttribute.new(name: name, block: block)
66
+ end
67
+
68
+ alias_method :nested, :nested_attribute
58
69
 
59
70
  # Support association methods that might be used in traits
60
71
  def one(name, **options, &block)
61
72
  resource = options[:resource] || options[:serializer]
62
73
  with_traits = options[:with_traits]
63
74
  key = options[:key] || name
64
- @collected_attributes[key] = TraitAssociation.new(
75
+ @collected_attributes[key] = BlockAssociation.new(
65
76
  name: name,
66
77
  resource: resource,
67
78
  with_traits: with_traits,
@@ -77,7 +88,7 @@ module Typelizer
77
88
  resource = options[:resource] || options[:serializer]
78
89
  with_traits = options[:with_traits]
79
90
  key = options[:key] || name
80
- @collected_attributes[key] = TraitAssociation.new(
91
+ @collected_attributes[key] = BlockAssociation.new(
81
92
  name: name,
82
93
  resource: resource,
83
94
  with_traits: with_traits,
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../type_inference"
4
+
3
5
  module Typelizer
4
6
  module SerializerPlugins
5
7
  class Alba::TraitInterface
8
+ include TypeInference
9
+
6
10
  attr_reader :serializer, :trait_name, :context, :plugin
7
11
 
8
12
  def initialize(serializer:, trait_name:, context:, plugin:)
@@ -33,31 +37,13 @@ module Typelizer
33
37
 
34
38
  def infer_types(props, typelizes)
35
39
  props.map do |prop|
36
- # First check for typelize DSL in the trait
37
- dsl_type = typelizes[prop.column_name.to_sym]
38
- if dsl_type&.any?
39
- next prop.with(**dsl_type).tap do |property|
40
- property.comment ||= model_plugin.comment_for(property) if config.comments && property.comment != false
41
- property.enum ||= model_plugin.enum_for(property) if property.enum != false
42
- end
43
- end
44
-
45
- # Fall back to model plugin for type inference
46
- model_plugin.infer_types(prop)
40
+ dsl_type = typelizes[prop.column_name.to_sym] || typelizes[prop.name.to_sym]
41
+ prop
42
+ .then { |p| dsl_type&.any? ? p.with(**dsl_type) : apply_model_inference(p) }
43
+ .then { |p| apply_metadata(p) }
44
+ .then { |p| infer_nested_property_types(p) }
47
45
  end
48
46
  end
49
-
50
- def model_class
51
- return serializer._typelizer_model_name if serializer.respond_to?(:_typelizer_model_name)
52
-
53
- config.instance_exec(serializer, &config.serializer_model_mapper)
54
- rescue NameError
55
- nil
56
- end
57
-
58
- def model_plugin
59
- @model_plugin ||= config.model_plugin.new(model_class: model_class, config: config)
60
- end
61
47
  end
62
48
  end
63
49
  end
@@ -48,19 +48,19 @@ module Typelizer
48
48
  return [], {} unless trait_block
49
49
 
50
50
  # Create a collector to capture attributes defined in the trait block
51
- collector = TraitAttributeCollector.new
51
+ collector = BlockAttributeCollector.new
52
52
  collector.instance_exec(&trait_block)
53
53
 
54
54
  props = collector.collected_attributes.map do |name, attr|
55
- build_trait_property(name.is_a?(Symbol) ? name.name : name, attr)
55
+ build_collected_property(name.is_a?(Symbol) ? name.name : name, attr)
56
56
  end
57
57
 
58
58
  [props, collector.collected_typelizes]
59
59
  end
60
60
 
61
- def build_trait_property(name, attr)
61
+ def build_collected_property(name, attr)
62
62
  case attr
63
- when TraitAttributeCollector::TraitAssociation
63
+ when BlockAttributeCollector::BlockAssociation
64
64
  with_traits = Array(attr.with_traits) if attr.with_traits
65
65
  resource = attr.resource || infer_resource_from_name(name)
66
66
 
@@ -73,6 +73,19 @@ module Typelizer
73
73
  column_name: name,
74
74
  with_traits: with_traits
75
75
  )
76
+ when BlockAttributeCollector::BlockNestedAttribute
77
+ prop_name = has_transform_key?(serializer) ? fetch_key(serializer, name) : name
78
+ nested_props, nested_typelizes = collect_nested_block(attr.block)
79
+ Property.new(
80
+ name: prop_name,
81
+ type: nil,
82
+ optional: false,
83
+ nullable: false,
84
+ multi: false,
85
+ column_name: name,
86
+ nested_properties: nested_props,
87
+ nested_typelizes: nested_typelizes
88
+ )
76
89
  else
77
90
  build_property(name, attr)
78
91
  end
@@ -95,7 +108,7 @@ module Typelizer
95
108
  end
96
109
 
97
110
  def trait_interfaces
98
- traits.map do |trait_name, _|
111
+ @trait_interfaces ||= traits.map do |trait_name, _|
99
112
  TraitInterface.new(
100
113
  serializer: serializer,
101
114
  trait_name: trait_name,
@@ -164,6 +177,8 @@ module Typelizer
164
177
  **options
165
178
  )
166
179
  when ::Alba::NestedAttribute
180
+ block = attr.instance_variable_get(:@block)
181
+ nested_props, nested_typelizes = collect_nested_block(block)
167
182
  Property.new(
168
183
  name: name,
169
184
  type: nil,
@@ -171,6 +186,8 @@ module Typelizer
171
186
  nullable: false,
172
187
  multi: false,
173
188
  column_name: column_name,
189
+ nested_properties: nested_props,
190
+ nested_typelizes: nested_typelizes,
174
191
  **options
175
192
  )
176
193
  when ::Alba::ConditionalAttribute
@@ -192,14 +209,24 @@ module Typelizer
192
209
  ::Alba.transform_key(key, transform_type: serializer._transform_type)
193
210
  end
194
211
 
195
- private
196
-
197
212
  def ts_mapper
198
213
  config.plugin_configs.dig(:alba, :ts_mapper) || ALBA_TS_MAPPER
199
214
  end
215
+
216
+ def collect_nested_block(block)
217
+ collector = BlockAttributeCollector.new
218
+ collector.instance_exec(&block)
219
+
220
+ props = collector.collected_attributes.map do |attr_name, attr|
221
+ attr_name_str = attr_name.is_a?(Symbol) ? attr_name.name : attr_name
222
+ build_collected_property(attr_name_str, attr)
223
+ end
224
+
225
+ [props, collector.collected_typelizes]
226
+ end
200
227
  end
201
228
  end
202
229
  end
203
230
 
204
- require_relative "alba/trait_attribute_collector"
231
+ require_relative "alba/block_attribute_collector"
205
232
  require_relative "alba/trait_interface"
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typelizer
4
+ module TypeInference
5
+ private
6
+
7
+ def apply_model_inference(prop)
8
+ model_plugin.infer_types(prop)
9
+ end
10
+
11
+ def apply_metadata(prop)
12
+ prop.tap do |p|
13
+ p.comment ||= model_plugin.comment_for(p) if config.comments && p.comment != false
14
+ p.enum ||= model_plugin.enum_for(p) if p.enum != false
15
+ end
16
+ end
17
+
18
+ def infer_nested_property_types(prop)
19
+ return prop unless prop.nested_properties&.any?
20
+
21
+ typelizes = prop.nested_typelizes || {}
22
+ inferred = prop.nested_properties.map do |sub_prop|
23
+ dsl_type = typelizes[sub_prop.column_name.to_sym] || typelizes[sub_prop.name.to_sym]
24
+ sub_prop
25
+ .then { |p| dsl_type&.any? ? p.with(**dsl_type) : apply_model_inference(p) }
26
+ .then { |p| apply_metadata(p) }
27
+ .then { |p| infer_nested_property_types(p) }
28
+ end
29
+
30
+ prop.with(nested_properties: inferred)
31
+ end
32
+
33
+ def model_class
34
+ return serializer._typelizer_model_name if serializer.respond_to?(:_typelizer_model_name)
35
+
36
+ config.instance_exec(serializer, &config.serializer_model_mapper)
37
+ rescue NameError => e
38
+ Typelizer.logger.debug("model_mapper failed for serializer #{serializer.name}: #{e.class}: #{e.message}")
39
+
40
+ nil
41
+ end
42
+
43
+ def model_plugin
44
+ @model_plugin ||= config.model_plugin.new(model_class: model_class, config: config)
45
+ end
46
+ end
47
+ end
@@ -14,6 +14,8 @@ module Typelizer
14
14
  return options if type_def.nil?
15
15
 
16
16
  type_str = type_def.to_s
17
+ return parse_union(type_str, **options) if type_str.include?("|")
18
+
17
19
  match = TYPE_PATTERN.match(type_str)
18
20
 
19
21
  return {type: type_def}.merge(options) unless match
@@ -34,6 +36,18 @@ module Typelizer
34
36
  type_str = type_def.to_s
35
37
  type_str.end_with?("?", "[]")
36
38
  end
39
+
40
+ private
41
+
42
+ def parse_union(type_str, **options)
43
+ parts = type_str.split(/\s*\|\s*/)
44
+ options[:nullable] = true if parts.delete("null")
45
+ if parts.size == 1
46
+ parse(parts.first, **options)
47
+ else
48
+ {type: parts.map(&:to_sym)}.merge(options)
49
+ end
50
+ end
37
51
  end
38
52
  end
39
53
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Typelizer
4
- VERSION = "0.7.0"
4
+ VERSION = "0.9.0"
5
5
  end
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,45 @@ 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
+ result = {}
76
+ interfaces(writer_name: writer_name).each do |i|
77
+ result[i.name] = OpenAPI.schema_for(i, openapi_version: openapi_version)
78
+ i.trait_interfaces.each do |trait|
79
+ result[trait.name] = OpenAPI.schema_for(trait, openapi_version: openapi_version)
80
+ end
81
+ end
82
+ result
83
+ end
84
+
65
85
  private
66
86
 
87
+ def load_serializers
88
+ dirs.flat_map { |dir| Dir["#{dir}/**/*.rb"] }.each { |file| require file }
89
+ end
90
+
91
+ def target_serializers(reject_class)
92
+ resolved = base_classes.filter_map do |base_class|
93
+ Object.const_get(base_class) if Object.const_defined?(base_class)
94
+ end
95
+ if base_classes.any? && resolved.none?
96
+ logger.warn("Typelizer: No serializers found. Ensure your serializers include Typelizer::DSL.")
97
+ return []
98
+ end
99
+
100
+ (resolved + resolved.flat_map(&:descendants)).uniq
101
+ .reject { |serializer| reject_class.call(serializer: serializer) }
102
+ .sort_by(&:name)
103
+ end
104
+
67
105
  attr_writer :base_classes
68
106
  end
69
107
 
@@ -72,3 +110,5 @@ module Typelizer
72
110
 
73
111
  self.base_classes = Set.new
74
112
  end
113
+
114
+ 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.7.0
4
+ version: 0.9.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,13 +65,14 @@ 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
70
72
  - lib/typelizer/renderer.rb
71
73
  - lib/typelizer/serializer_config_layer.rb
72
74
  - lib/typelizer/serializer_plugins/alba.rb
73
- - lib/typelizer/serializer_plugins/alba/trait_attribute_collector.rb
75
+ - lib/typelizer/serializer_plugins/alba/block_attribute_collector.rb
74
76
  - lib/typelizer/serializer_plugins/alba/trait_interface.rb
75
77
  - lib/typelizer/serializer_plugins/ams.rb
76
78
  - lib/typelizer/serializer_plugins/auto.rb
@@ -84,6 +86,7 @@ files:
84
86
  - lib/typelizer/templates/inheritance.ts.erb
85
87
  - lib/typelizer/templates/inline_type.ts.erb
86
88
  - lib/typelizer/templates/interface.ts.erb
89
+ - lib/typelizer/type_inference.rb
87
90
  - lib/typelizer/type_parser.rb
88
91
  - lib/typelizer/union_type_sorter.rb
89
92
  - lib/typelizer/version.rb