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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -1
- data/README.md +75 -0
- data/lib/tasks/generate.rake +4 -4
- data/lib/typelizer/config.rb +37 -0
- data/lib/typelizer/configuration.rb +1 -2
- data/lib/typelizer/delegate_tracker.rb +34 -0
- data/lib/typelizer/dsl/hooks/alba.rb +17 -0
- data/lib/typelizer/dsl/hooks/ams.rb +15 -0
- data/lib/typelizer/dsl/hooks/oj_serializers.rb +15 -0
- data/lib/typelizer/dsl/hooks/panko.rb +16 -0
- data/lib/typelizer/dsl/hooks.rb +64 -0
- data/lib/typelizer/dsl.rb +20 -0
- data/lib/typelizer/generator.rb +2 -73
- data/lib/typelizer/interface.rb +60 -17
- data/lib/typelizer/model_plugins/active_record.rb +23 -0
- data/lib/typelizer/openapi.rb +124 -0
- data/lib/typelizer/property.rb +50 -10
- data/lib/typelizer/serializer_plugins/alba/trait_interface.rb +2 -2
- data/lib/typelizer/serializer_plugins/alba.rb +0 -23
- data/lib/typelizer/serializer_plugins/ams.rb +0 -13
- data/lib/typelizer/serializer_plugins/base.rb +0 -8
- data/lib/typelizer/serializer_plugins/oj_serializers.rb +0 -7
- data/lib/typelizer/serializer_plugins/panko.rb +0 -12
- data/lib/typelizer/templates/enums.ts.erb +1 -1
- data/lib/typelizer/templates/index.ts.erb +5 -3
- data/lib/typelizer/templates/inheritance.ts.erb +5 -1
- data/lib/typelizer/templates/inline_type.ts.erb +1 -1
- data/lib/typelizer/templates/interface.ts.erb +7 -6
- data/lib/typelizer/union_type_sorter.rb +148 -0
- data/lib/typelizer/version.rb +1 -1
- data/lib/typelizer/writer.rb +10 -4
- data/lib/typelizer.rb +31 -1
- metadata +9 -2
- data/lib/typelizer/contexts/scan_context.rb +0 -20
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,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.
|
|
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:
|
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
|
@@ -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, :
|
|
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,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
|
data/lib/typelizer/generator.rb
CHANGED
|
@@ -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
|
-
|
|
23
|
-
|
|
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
|
data/lib/typelizer/interface.rb
CHANGED
|
@@ -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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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
|
|