typelizer 0.6.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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -1
  3. data/README.md +75 -0
  4. data/lib/tasks/generate.rake +4 -4
  5. data/lib/typelizer/config.rb +37 -0
  6. data/lib/typelizer/configuration.rb +1 -2
  7. data/lib/typelizer/delegate_tracker.rb +34 -0
  8. data/lib/typelizer/dsl/hooks/alba.rb +17 -0
  9. data/lib/typelizer/dsl/hooks/ams.rb +15 -0
  10. data/lib/typelizer/dsl/hooks/oj_serializers.rb +15 -0
  11. data/lib/typelizer/dsl/hooks/panko.rb +16 -0
  12. data/lib/typelizer/dsl/hooks.rb +64 -0
  13. data/lib/typelizer/dsl.rb +20 -0
  14. data/lib/typelizer/generator.rb +2 -73
  15. data/lib/typelizer/interface.rb +60 -17
  16. data/lib/typelizer/model_plugins/active_record.rb +23 -0
  17. data/lib/typelizer/openapi.rb +124 -0
  18. data/lib/typelizer/property.rb +50 -10
  19. data/lib/typelizer/serializer_plugins/alba/trait_interface.rb +2 -2
  20. data/lib/typelizer/serializer_plugins/alba.rb +0 -23
  21. data/lib/typelizer/serializer_plugins/ams.rb +0 -13
  22. data/lib/typelizer/serializer_plugins/base.rb +0 -8
  23. data/lib/typelizer/serializer_plugins/oj_serializers.rb +0 -7
  24. data/lib/typelizer/serializer_plugins/panko.rb +0 -12
  25. data/lib/typelizer/templates/enums.ts.erb +1 -1
  26. data/lib/typelizer/templates/index.ts.erb +5 -3
  27. data/lib/typelizer/templates/inheritance.ts.erb +5 -1
  28. data/lib/typelizer/templates/inline_type.ts.erb +1 -1
  29. data/lib/typelizer/templates/interface.ts.erb +7 -6
  30. data/lib/typelizer/union_type_sorter.rb +148 -0
  31. data/lib/typelizer/version.rb +1 -1
  32. data/lib/typelizer/writer.rb +10 -4
  33. data/lib/typelizer.rb +31 -1
  34. metadata +9 -2
  35. data/lib/typelizer/contexts/scan_context.rb +0 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 57cc88a8063ebe9f00e0558f3dbd59aca579b5e191bdce38d3f8ca9b0451acee
4
- data.tar.gz: 32cb1941e3ece707061fd7f2b1e6287710a0f9e2e8f8fc4d56b7f39fe444a7f6
3
+ metadata.gz: a0b21f5f78121c64717aa08d6556efbddc0b0b80a3ec6f272d63f699133fff12
4
+ data.tar.gz: d8df7c3542950c82e6b25d3f9c39ea808aded01fab0e5b5eaada3801a99031a3
5
5
  SHA512:
6
- metadata.gz: a9fb264406d6ea58bc1b7cc7e875bef11e08cf8f758786f0f63e4d110db5b0e39832615167068baea973a5cf5236f76cac537ce1cfd3f21abd3def162a33edd2
7
- data.tar.gz: 74a8a3552d25fff6c5dee5837d42dc6f257d097944874ade9aee247d432ff7d85303ebb9a6e60ae000bb8634c2a47e4be2e52dc5f4b08d49bd06963473a19b43
6
+ metadata.gz: cc42979e0f00e3aec338fdd4cf3848639ba12a2019293a46e864e908a1dfd8f2f2258ef62d07ed15ec3da66ca7ad740907ac63c6e04f9aae29f18db0eae46584
7
+ data.tar.gz: c9504167e013d6aeff1208207b4531f012fe942018e72368296d2a676fb04460ece7b0582a5ef297cf6c037e071ea0868e39c4632765030bdb014fe9a49884ea
data/CHANGELOG.md CHANGED
@@ -7,6 +7,45 @@ 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
+
38
+ ## [0.7.0] - 2026-01-15
39
+
40
+ ### Changed
41
+
42
+ - Use DSL hooks instead of TracePoint for `typelize` method. ([@skryukov])
43
+
44
+ ### Fixed
45
+
46
+ - Apply sorting and quote style configs consistently to all generated files. ([@jonmarkgo], [@skryukov])
47
+ - Fix fingerprint calculations to include all config options. ([@skryukov])
48
+
10
49
  ## [0.6.0] - 2026-01-14
11
50
 
12
51
  ### Added
@@ -347,7 +386,9 @@ and this project adheres to [Semantic Versioning].
347
386
  [@prog-supdex]: https://github.com/prog-supdex
348
387
  [@ventsislaf]: https://github.com/ventsislaf
349
388
 
350
- [Unreleased]: https://github.com/skryukov/typelizer/compare/v0.6.0...HEAD
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
391
+ [0.7.0]: https://github.com/skryukov/typelizer/compare/v0.6.0...v0.7.0
351
392
  [0.6.0]: https://github.com/skryukov/typelizer/compare/v0.5.6...v0.6.0
352
393
  [0.5.6]: https://github.com/skryukov/typelizer/compare/v0.5.5...v0.5.6
353
394
  [0.5.5]: https://github.com/skryukov/typelizer/compare/v0.5.4...v0.5.5
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:
@@ -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
- serializers = block.call
23
+ block.call
25
24
  end
26
25
 
26
+ interfaces = Typelizer.interfaces
27
27
  puts "Finished in #{time} seconds"
28
- puts "Found #{serializers.size} serializers:"
29
- puts serializers.map { |s| "\t#{s.name}" }.join("\n")
28
+ puts "Found #{interfaces.size} serializers:"
29
+ puts interfaces.map { |i| "\t#{i.name}" }.join("\n")
30
30
  end
31
31
  end
@@ -19,6 +19,41 @@ module Typelizer
19
19
 
20
20
  DEFAULT_TYPES_GLOBAL = %w[Array Date Record File FileList].freeze
21
21
 
22
+ # Config keys that affect generated file content and must be included in fingerprints.
23
+ # When adding a new config, add it here if it affects output, or to CONFIGS_NOT_AFFECTING_OUTPUT.
24
+ CONFIGS_AFFECTING_OUTPUT = %i[
25
+ types_import_path
26
+ types_global
27
+ prefer_double_quotes
28
+ comments
29
+ verbatim_module_syntax
30
+ imports_sort_order
31
+ properties_sort_order
32
+ ].freeze
33
+
34
+ # Subset of CONFIGS_AFFECTING_OUTPUT that specifically affect index.ts output.
35
+ CONFIGS_AFFECTING_INDEX_OUTPUT = %i[
36
+ verbatim_module_syntax
37
+ prefer_double_quotes
38
+ imports_sort_order
39
+ ].freeze
40
+
41
+ # Config keys that don't affect file content (runtime behavior, or effects captured via properties).
42
+ CONFIGS_NOT_AFFECTING_OUTPUT = %i[
43
+ serializer_name_mapper
44
+ serializer_model_mapper
45
+ properties_transformer
46
+ model_plugin
47
+ serializer_plugin
48
+ plugin_configs
49
+ type_mapping
50
+ null_strategy
51
+ output_dir
52
+ inheritance_strategy
53
+ associations_strategy
54
+ reject_class
55
+ ].freeze
56
+
22
57
  Config = Struct.new(
23
58
  :serializer_name_mapper,
24
59
  :serializer_model_mapper,
@@ -36,6 +71,7 @@ module Typelizer
36
71
  :verbatim_module_syntax,
37
72
  :inheritance_strategy,
38
73
  :associations_strategy,
74
+ :reject_class,
39
75
  :comments,
40
76
  :prefer_double_quotes,
41
77
  keyword_init: true
@@ -71,6 +107,7 @@ module Typelizer
71
107
  null_strategy: :nullable,
72
108
  inheritance_strategy: :none,
73
109
  associations_strategy: :database,
110
+ reject_class: ->(serializer:) { false },
74
111
  comments: false,
75
112
  prefer_double_quotes: false,
76
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) && 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?
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typelizer
4
+ module DSL
5
+ module Hooks
6
+ module Alba
7
+ include Methods
8
+ extend Builder
9
+
10
+ hook :attribute, :association, :one, :has_one
11
+ hook :nested_attribute, :nested, :meta
12
+ hook :many, :has_many, multi: true
13
+ hook_method_added
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typelizer
4
+ module DSL
5
+ module Hooks
6
+ module AMS
7
+ include Methods
8
+ extend Builder
9
+
10
+ hook :attribute, :has_one, :belongs_to
11
+ hook :has_many, multi: true
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typelizer
4
+ module DSL
5
+ module Hooks
6
+ module OjSerializers
7
+ include Methods
8
+ extend Builder
9
+
10
+ hook :attribute, :has_one, :belongs_to, :flat_one
11
+ hook :has_many, multi: true
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typelizer
4
+ module DSL
5
+ module Hooks
6
+ module Panko
7
+ include Methods
8
+ extend Builder
9
+
10
+ hook :has_one
11
+ hook :has_many, multi: true
12
+ hook_method_added
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typelizer
4
+ module DSL
5
+ module Hooks
6
+ def self.install(base)
7
+ # Always install hooks to capture multi associations.
8
+ # The hooks only record data to a Set, so overhead is minimal.
9
+ if defined?(::Alba::Resource) && base.ancestors.include?(::Alba::Resource)
10
+ require_relative "hooks/alba"
11
+ base.singleton_class.prepend(Alba)
12
+ elsif defined?(::ActiveModel::Serializer) && base.ancestors.include?(::ActiveModel::Serializer)
13
+ require_relative "hooks/ams"
14
+ base.singleton_class.prepend(AMS)
15
+ elsif defined?(::Oj::Serializer) && base.ancestors.include?(::Oj::Serializer)
16
+ require_relative "hooks/oj_serializers"
17
+ base.singleton_class.prepend(OjSerializers)
18
+ elsif defined?(::Panko::Serializer) && base.ancestors.include?(::Panko::Serializer)
19
+ require_relative "hooks/panko"
20
+ base.singleton_class.prepend(Panko)
21
+ end
22
+ end
23
+
24
+ # Shared methods available to all hook modules
25
+ module Methods
26
+ private
27
+
28
+ def consume_keyless_type(name)
29
+ return unless keyless_type
30
+
31
+ type, attrs = keyless_type
32
+ typelize(name => [type, attrs])
33
+ self.keyless_type = nil
34
+ end
35
+
36
+ def record_multi(name)
37
+ _own_typelizer_multi_attributes << name.to_sym
38
+ end
39
+ end
40
+
41
+ # DSL for defining hooks with less boilerplate
42
+ module Builder
43
+ def hook(*methods, multi: false)
44
+ methods.each do |method|
45
+ define_method(method) do |name = nil, *args, **kwargs, &block|
46
+ if name
47
+ record_multi(name) if multi
48
+ consume_keyless_type(name)
49
+ end
50
+ super(name, *args, **kwargs, &block)
51
+ end
52
+ end
53
+ end
54
+
55
+ def hook_method_added
56
+ define_method(:method_added) do |method_name|
57
+ consume_keyless_type(method_name)
58
+ super(method_name)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
data/lib/typelizer/dsl.rb CHANGED
@@ -1,3 +1,6 @@
1
+ require "set"
2
+ require_relative "dsl/hooks"
3
+
1
4
  module Typelizer
2
5
  module DSL
3
6
  # typelize_from Model
@@ -6,11 +9,13 @@ module Typelizer
6
9
  def self.included(base)
7
10
  Typelizer.base_classes << base.to_s if base.name
8
11
  base.extend(ClassMethods)
12
+ Hooks.install(base)
9
13
  end
10
14
 
11
15
  def self.extended(base)
12
16
  Typelizer.base_classes << base.to_s if base.name
13
17
  base.extend(ClassMethods)
18
+ Hooks.install(base)
14
19
  end
15
20
 
16
21
  module ClassMethods
@@ -49,6 +54,21 @@ module Typelizer
49
54
 
50
55
  attr_accessor :keyless_type
51
56
 
57
+ # Returns union of own + ancestors' multi attributes
58
+ def _typelizer_multi_attributes
59
+ result = @_typelizer_multi_attributes || Set.new
60
+ if superclass.respond_to?(:_typelizer_multi_attributes)
61
+ superclass._typelizer_multi_attributes | result
62
+ else
63
+ result
64
+ end
65
+ end
66
+
67
+ # Returns own Set (initializing if needed) for writing
68
+ def _own_typelizer_multi_attributes
69
+ @_typelizer_multi_attributes ||= Set.new
70
+ end
71
+
52
72
  def typelize_meta(**attributes)
53
73
  assign_type_information(:_typelizer_meta_attributes, attributes)
54
74
  end
@@ -9,83 +9,12 @@ module Typelizer
9
9
  def call(force: false)
10
10
  return [] unless Typelizer.enabled?
11
11
 
12
- # plugin scan per run cache
13
- @scan_plugin_cache = {}
14
-
15
- read_serializers
16
- serializers = target_serializers
17
-
18
- # For each writer, build a dedicated WriterContext. The context holds that writer's
19
- # configuration and resolves the effective Config for every Interface (per serializer)
20
- # by merging global, writer, and per-serializer (DSL) overrides
21
12
  Typelizer.configuration.writers.each do |writer_name, writer_config|
22
- context = WriterContext.new(writer_name: writer_name)
23
- interfaces = serializers.map { |klass| context.interface_for(klass) }
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?
24
15
 
25
16
  Writer.new(writer_config).call(interfaces, force: force)
26
17
  end
27
-
28
- serializers
29
- end
30
-
31
- private
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
- end
44
-
45
- def read_serializers(files = nil)
46
- files ||= Typelizer.dirs.flat_map { |dir| Dir["#{dir}/**/*.rb"] }
47
- files.each do |file|
48
- trace = TracePoint.new(:call) do |tp|
49
- next unless typelized_class?(tp.self)
50
-
51
- serializer_plugin = build_scan_plugin_for(tp.self)
52
- next unless serializer_plugin
53
-
54
- if tp.callee_id.in?(serializer_plugin.methods_to_typelize)
55
- type, attrs = tp.self.keyless_type
56
- name = tp.binding.local_variable_get(:name) if tp.binding.local_variable_defined?(:name)
57
- tp.self.typelize(**serializer_plugin.typelize_method_transform(method: tp.callee_id, binding: tp.binding, name: name, type: type, attrs: attrs || {}))
58
- tp.self.keyless_type = nil
59
- end
60
- end
61
-
62
- trace.enable
63
- require file
64
- trace.disable
65
- end
66
- end
67
-
68
- def typelized_class?(klass)
69
- klass.is_a?(Class) && klass.respond_to?(:typelizer_config)
70
- rescue
71
- false
72
- end
73
-
74
- # Builds a minimal plugin instance used only during scan time for TracePoint
75
- def build_scan_plugin_for(serializer_klass)
76
- return @scan_plugin_cache[serializer_klass] if @scan_plugin_cache&.key?(serializer_klass)
77
-
78
- base = Typelizer.configuration.writer_config(:default)
79
- local_configuration = serializer_klass.typelizer_config.to_h.slice(:serializer_plugin, :plugin_configs)
80
- cfg = base.with_overrides(**local_configuration)
81
-
82
- @scan_plugin_cache[serializer_klass] = cfg.serializer_plugin.new(
83
- serializer: serializer_klass,
84
- config: cfg,
85
- context: Typelizer::ScanContext
86
- )
87
- rescue NameError
88
- nil
89
18
  end
90
19
  end
91
20
  end
@@ -25,7 +25,7 @@ module Typelizer
25
25
 
26
26
  def name
27
27
  if inline?
28
- Renderer.call("inline_type.ts.erb", properties: properties).strip
28
+ Renderer.call("inline_type.ts.erb", properties: properties, sort_order: config.properties_sort_order).strip
29
29
  else
30
30
  config.serializer_name_mapper.call(serializer).tr_s(":", "")
31
31
  end
@@ -141,12 +141,15 @@ module Typelizer
141
141
  end
142
142
 
143
143
  def fingerprint
144
- if trait_interfaces.empty?
145
- "<#{self.class.name} #{name} properties=[#{properties_to_print.map(&:fingerprint).join(", ")}]>"
146
- else
147
- traits_fingerprint = trait_interfaces.map { |t| "#{t.name}=[#{t.properties.map(&:fingerprint).join(", ")}]" }.join(", ")
148
- "<#{self.class.name} #{name} properties=[#{properties_to_print.map(&:fingerprint).join(", ")}] traits=[#{traits_fingerprint}]>"
149
- end
144
+ [
145
+ name,
146
+ properties_to_print.map(&:fingerprint),
147
+ parent_interface&.name,
148
+ root_key,
149
+ meta_fields.map(&:fingerprint),
150
+ trait_interfaces.map { |t| [t.name, t.properties.map(&:fingerprint)] },
151
+ CONFIGS_AFFECTING_OUTPUT.map { |key| config.public_send(key) }
152
+ ].inspect
150
153
  end
151
154
 
152
155
  def quote(str)
@@ -168,18 +171,58 @@ module Typelizer
168
171
  end
169
172
 
170
173
  def infer_types(props, hash_name = :_typelizer_attributes)
174
+ dsl_attrs = serializer.respond_to?(hash_name) ? serializer.public_send(hash_name) : {}
175
+ multi_attrs = serializer.respond_to?(:_typelizer_multi_attributes) ? serializer._typelizer_multi_attributes : Set.new
176
+
171
177
  props.map do |prop|
172
- if serializer.respond_to?(hash_name)
173
- dsl_type = serializer.public_send(hash_name)[prop.column_name.to_sym]
174
- if dsl_type&.any?
175
- next Property.new(prop.to_h.merge(dsl_type)).tap do |property|
176
- property.comment ||= model_plugin.comment_for(property) if config.comments && property.comment != false
177
- property.enum ||= model_plugin.enum_for(property) if property.enum != false
178
- end
179
- end
180
- end
178
+ has_dsl = dsl_attrs_for(prop, dsl_attrs)&.any?
179
+
180
+ prop
181
+ .then { |p| apply_dsl_type(p, dsl_attrs) }
182
+ .then { |p| has_dsl ? p : apply_model_inference(p) }
183
+ .then { |p| apply_multi_flag(p, multi_attrs) }
184
+ .then { |p| apply_metadata(p) }
185
+ end
186
+ end
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
+
192
+ def apply_dsl_type(prop, dsl_attrs)
193
+ dsl_type = dsl_attrs_for(prop, dsl_attrs)
194
+ return prop unless dsl_type&.any?
195
+
196
+ dsl_type = resolve_class_type(dsl_type)
197
+ prop.with(**dsl_type)
198
+ end
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
+
212
+ def apply_model_inference(prop)
213
+ model_plugin.infer_types(prop)
214
+ end
215
+
216
+ def apply_multi_flag(prop, multi_attrs)
217
+ return prop unless multi_attrs.include?(prop.column_name.to_sym)
218
+
219
+ prop.with(multi: true)
220
+ end
181
221
 
182
- model_plugin.infer_types(prop)
222
+ def apply_metadata(prop)
223
+ prop.tap do |p|
224
+ p.comment ||= model_plugin.comment_for(p) if config.comments && p.comment != false
225
+ p.enum ||= model_plugin.enum_for(p) if p.enum != false
183
226
  end
184
227
  end
185
228