typelizer 0.5.6 → 0.7.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 +41 -1
- data/README.md +6 -0
- data/lib/typelizer/config.rb +36 -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 +7 -53
- data/lib/typelizer/import_sorter.rb +22 -0
- data/lib/typelizer/interface.rb +57 -18
- data/lib/typelizer/model_plugins/active_record.rb +2 -0
- data/lib/typelizer/property.rb +54 -9
- data/lib/typelizer/serializer_plugins/alba/trait_interface.rb +1 -1
- 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 +3 -0
- data/lib/typelizer/templates/index.ts.erb +7 -2
- 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 +27 -4
- data/lib/typelizer.rb +2 -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: 819c25a9bfc5dd74c3b7c77045bb5f5ef968f518f8a8d228502badb6936ea50d
|
|
4
|
+
data.tar.gz: c8d8d9affec2d6eb07cf1e58942fe46009fad2782c57689218b8357a85c227f0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b4b39192ff398bb25c6b537fcb5c73893de40d82f14a09a254e4ef8317a08232819c3285b7e6806938f1c7058f895d0100b03884574158bbaa2e329982518683
|
|
7
|
+
data.tar.gz: 8971c5a2cc78992fd38dc101e7b2fc1eaefe82ad756f828bca4416a4a6ed54d1ff25eace61fd80cd293cca3dc66b6b6c24706a0fa45156fb275eb9bfe4976c46
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning].
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.0] - 2026-01-15
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Use DSL hooks instead of TracePoint for `typelize` method. ([@skryukov])
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Apply sorting and quote style configs consistently to all generated files. ([@jonmarkgo], [@skryukov])
|
|
19
|
+
- Fix fingerprint calculations to include all config options. ([@skryukov])
|
|
20
|
+
|
|
21
|
+
## [0.6.0] - 2026-01-14
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- New `imports_sort_order` configuration option for consistent import ordering in generated TypeScript interfaces. ([@jonmarkgo])
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
Typelizer.configure do |config|
|
|
29
|
+
# Sort imports alphabetically
|
|
30
|
+
config.imports_sort_order = :alphabetical
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Available options:
|
|
35
|
+
- `:none` (default) - preserve original order
|
|
36
|
+
- `:alphabetical` - sort imports A-Z (case-insensitive)
|
|
37
|
+
- `Proc` - custom sorting logic
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- Rails enum attributes now generate named types (e.g., `PostCategory`) in a separate `Enums.ts` file instead of inline unions. ([@skryukov])
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- Fix `index.ts` not being regenerated when traits are added or removed. ([@skryukov])
|
|
46
|
+
|
|
10
47
|
## [0.5.6] - 2026-01-12
|
|
11
48
|
|
|
12
49
|
### Added
|
|
@@ -309,6 +346,7 @@ and this project adheres to [Semantic Versioning].
|
|
|
309
346
|
|
|
310
347
|
[@davidrunger]: https://github.com/davidrunger
|
|
311
348
|
[@Envek]: https://github.com/Envek
|
|
349
|
+
[@jonmarkgo]: https://github.com/jonmarkgo
|
|
312
350
|
[@hkamberovic]: https://github.com/hkamberovic
|
|
313
351
|
[@kristinemcbride]: https://github.com/kristinemcbride
|
|
314
352
|
[@nkriege]: https://github.com/nkriege
|
|
@@ -320,7 +358,9 @@ and this project adheres to [Semantic Versioning].
|
|
|
320
358
|
[@prog-supdex]: https://github.com/prog-supdex
|
|
321
359
|
[@ventsislaf]: https://github.com/ventsislaf
|
|
322
360
|
|
|
323
|
-
[Unreleased]: https://github.com/skryukov/typelizer/compare/v0.
|
|
361
|
+
[Unreleased]: https://github.com/skryukov/typelizer/compare/v0.7.0...HEAD
|
|
362
|
+
[0.7.0]: https://github.com/skryukov/typelizer/compare/v0.6.0...v0.7.0
|
|
363
|
+
[0.6.0]: https://github.com/skryukov/typelizer/compare/v0.5.6...v0.6.0
|
|
324
364
|
[0.5.6]: https://github.com/skryukov/typelizer/compare/v0.5.5...v0.5.6
|
|
325
365
|
[0.5.5]: https://github.com/skryukov/typelizer/compare/v0.5.4...v0.5.5
|
|
326
366
|
[0.5.4]: https://github.com/skryukov/typelizer/compare/v0.5.3...v0.5.4
|
data/README.md
CHANGED
|
@@ -493,6 +493,12 @@ Typelizer.configure do |config|
|
|
|
493
493
|
# Proc - custom sorting function receiving array of Property objects
|
|
494
494
|
config.properties_sort_order = :none
|
|
495
495
|
|
|
496
|
+
# Strategy for ordering imports in generated TypeScript interfaces
|
|
497
|
+
# :none - preserve original order (default)
|
|
498
|
+
# :alphabetical - sort imports A-Z (case-insensitive)
|
|
499
|
+
# Proc - custom sorting function receiving array of import strings
|
|
500
|
+
config.imports_sort_order = :none
|
|
501
|
+
|
|
496
502
|
# Plugin for model type inference (default: ModelPlugins::Auto)
|
|
497
503
|
config.model_plugin = Typelizer::ModelPlugins::Auto
|
|
498
504
|
|
data/lib/typelizer/config.rb
CHANGED
|
@@ -19,11 +19,46 @@ 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
|
+
].freeze
|
|
55
|
+
|
|
22
56
|
Config = Struct.new(
|
|
23
57
|
:serializer_name_mapper,
|
|
24
58
|
:serializer_model_mapper,
|
|
25
59
|
:properties_transformer,
|
|
26
60
|
:properties_sort_order,
|
|
61
|
+
:imports_sort_order,
|
|
27
62
|
:model_plugin,
|
|
28
63
|
:serializer_plugin,
|
|
29
64
|
:plugin_configs,
|
|
@@ -79,6 +114,7 @@ module Typelizer
|
|
|
79
114
|
types_global: DEFAULT_TYPES_GLOBAL,
|
|
80
115
|
properties_transformer: nil,
|
|
81
116
|
properties_sort_order: :none,
|
|
117
|
+
imports_sort_order: :none,
|
|
82
118
|
verbatim_module_syntax: false
|
|
83
119
|
}
|
|
84
120
|
end
|
|
@@ -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,15 +9,9 @@ module Typelizer
|
|
|
9
9
|
def call(force: false)
|
|
10
10
|
return [] unless Typelizer.enabled?
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
@scan_plugin_cache = {}
|
|
14
|
-
|
|
15
|
-
read_serializers
|
|
12
|
+
load_serializers
|
|
16
13
|
serializers = target_serializers
|
|
17
14
|
|
|
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
15
|
Typelizer.configuration.writers.each do |writer_name, writer_config|
|
|
22
16
|
context = WriterContext.new(writer_name: writer_name)
|
|
23
17
|
interfaces = serializers.map { |klass| context.interface_for(klass) }
|
|
@@ -30,6 +24,12 @@ module Typelizer
|
|
|
30
24
|
|
|
31
25
|
private
|
|
32
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
33
|
def target_serializers
|
|
34
34
|
base_classes = Typelizer.base_classes.filter_map do |base_class|
|
|
35
35
|
Object.const_get(base_class) if Object.const_defined?(base_class)
|
|
@@ -41,51 +41,5 @@ module Typelizer
|
|
|
41
41
|
.reject { |serializer| Typelizer.reject_class.call(serializer: serializer) }
|
|
42
42
|
.sort_by(&:name)
|
|
43
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
|
-
end
|
|
90
44
|
end
|
|
91
45
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typelizer
|
|
4
|
+
module ImportSorter
|
|
5
|
+
def self.sort(imports, sort_order)
|
|
6
|
+
case sort_order
|
|
7
|
+
when :none, nil
|
|
8
|
+
imports
|
|
9
|
+
when :alphabetical
|
|
10
|
+
imports.sort_by { |i| i.to_s.downcase }
|
|
11
|
+
when Proc
|
|
12
|
+
result = sort_order.call(imports)
|
|
13
|
+
result.is_a?(Array) ? result : imports
|
|
14
|
+
else
|
|
15
|
+
imports
|
|
16
|
+
end
|
|
17
|
+
rescue => e
|
|
18
|
+
Typelizer.logger.warn("ImportSorter error: #{e.message}, preserving original order")
|
|
19
|
+
imports
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
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
|
|
@@ -58,6 +58,15 @@ module Typelizer
|
|
|
58
58
|
@trait_interfaces ||= serializer_plugin.trait_interfaces
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
def enum_types
|
|
62
|
+
@enum_types ||= begin
|
|
63
|
+
all_properties = properties + trait_interfaces.flat_map(&:properties)
|
|
64
|
+
all_properties
|
|
65
|
+
.select(&:enum_definition)
|
|
66
|
+
.uniq(&:enum_type_name)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
61
70
|
def properties
|
|
62
71
|
@properties ||= begin
|
|
63
72
|
props = serializer_plugin.properties
|
|
@@ -119,7 +128,11 @@ module Typelizer
|
|
|
119
128
|
prop.with_traits.map { |t| "#{prop.type.name}#{t.to_s.camelize}Trait" }
|
|
120
129
|
end
|
|
121
130
|
|
|
122
|
-
|
|
131
|
+
# Collect enum type names from properties
|
|
132
|
+
enum_imports = all_properties.filter_map(&:enum_type_name)
|
|
133
|
+
|
|
134
|
+
result = (custom_type_imports + serializer_types + trait_imports + enum_imports + Array(parent_interface&.name)).uniq - [self_type_name, name]
|
|
135
|
+
ImportSorter.sort(result, config.imports_sort_order)
|
|
123
136
|
end
|
|
124
137
|
end
|
|
125
138
|
|
|
@@ -128,12 +141,15 @@ module Typelizer
|
|
|
128
141
|
end
|
|
129
142
|
|
|
130
143
|
def fingerprint
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
137
153
|
end
|
|
138
154
|
|
|
139
155
|
def quote(str)
|
|
@@ -155,18 +171,41 @@ module Typelizer
|
|
|
155
171
|
end
|
|
156
172
|
|
|
157
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
|
+
|
|
158
177
|
props.map do |prop|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
178
|
+
has_dsl = dsl_attrs[prop.column_name.to_sym]&.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 apply_dsl_type(prop, dsl_attrs)
|
|
189
|
+
dsl_type = dsl_attrs[prop.column_name.to_sym]
|
|
190
|
+
return prop unless dsl_type&.any?
|
|
191
|
+
|
|
192
|
+
prop.with(**dsl_type)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def apply_model_inference(prop)
|
|
196
|
+
model_plugin.infer_types(prop)
|
|
197
|
+
end
|
|
198
|
+
|
|
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
|
|
168
204
|
|
|
169
|
-
|
|
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
|
|
170
209
|
end
|
|
171
210
|
end
|
|
172
211
|
|
|
@@ -28,6 +28,8 @@ module Typelizer
|
|
|
28
28
|
return unless model_class&.defined_enums&.key?(prop.column_name.to_s)
|
|
29
29
|
|
|
30
30
|
prop.enum = model_class.defined_enums[prop.column_name.to_s].keys
|
|
31
|
+
prop.enum_type_name = "#{model_class.name.demodulize}#{prop.column_name.to_s.camelize}"
|
|
32
|
+
prop.enum
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
private
|
data/lib/typelizer/property.rb
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
module Typelizer
|
|
2
2
|
Property = Struct.new(
|
|
3
3
|
:name, :type, :optional, :nullable,
|
|
4
|
-
:multi, :column_name, :comment, :enum, :deprecated,
|
|
4
|
+
:multi, :column_name, :comment, :enum, :enum_type_name, :deprecated,
|
|
5
5
|
:with_traits,
|
|
6
6
|
keyword_init: true
|
|
7
7
|
) do
|
|
8
|
+
def with(**attrs)
|
|
9
|
+
dup.tap { |p| attrs.each { |k, v| p[k] = v } }
|
|
10
|
+
end
|
|
11
|
+
|
|
8
12
|
def inspect
|
|
9
13
|
props = to_h.merge(type: type_name).map { |k, v| "#{k}=#{v.inspect}" }.join(" ")
|
|
10
14
|
"<#{self.class.name} #{props}>"
|
|
@@ -16,8 +20,17 @@ module Typelizer
|
|
|
16
20
|
fingerprint == other.fingerprint
|
|
17
21
|
end
|
|
18
22
|
|
|
23
|
+
# Default to_s for backward compatibility (no sorting)
|
|
19
24
|
def to_s
|
|
20
|
-
|
|
25
|
+
render(sort_order: :none)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Renders the property as a TypeScript property string
|
|
29
|
+
# @param sort_order [Symbol, Proc, nil] Sort order for union types (:none, :alphabetical, or Proc)
|
|
30
|
+
# @param prefer_double_quotes [Boolean] Whether to use double quotes for string values
|
|
31
|
+
# @return [String] The property string like "name?: Type1 | Type2"
|
|
32
|
+
def render(sort_order: :none, prefer_double_quotes: false)
|
|
33
|
+
type_str = type_name(sort_order: sort_order, prefer_double_quotes: prefer_double_quotes)
|
|
21
34
|
|
|
22
35
|
# Handle intersection types for traits
|
|
23
36
|
if with_traits&.any? && type.respond_to?(:name)
|
|
@@ -26,23 +39,55 @@ module Typelizer
|
|
|
26
39
|
end
|
|
27
40
|
|
|
28
41
|
type_str = "Array<#{type_str}>" if multi
|
|
42
|
+
|
|
43
|
+
# Apply union sorting to the final type string (handles Array<...> unions too)
|
|
44
|
+
type_str = UnionTypeSorter.sort(type_str, sort_order)
|
|
45
|
+
|
|
46
|
+
# Add nullable at the end (null should always be last in sorted output)
|
|
29
47
|
type_str = "#{type_str} | null" if nullable
|
|
30
48
|
|
|
31
49
|
"#{name}#{"?" if optional}: #{type_str}"
|
|
32
50
|
end
|
|
33
51
|
|
|
34
52
|
def fingerprint
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
53
|
+
# Use array format for consistent output across Ruby versions
|
|
54
|
+
# (Hash#inspect format changed in Ruby 3.4)
|
|
55
|
+
to_h.merge(type: UnionTypeSorter.sort(type_name(sort_order: :alphabetical), :alphabetical))
|
|
56
|
+
.to_a.inspect
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Generates a TypeScript type definition for named enums
|
|
60
|
+
# @param sort_order [Symbol, Proc, nil] Sort order for enum values (:none, :alphabetical, or Proc)
|
|
61
|
+
# @param prefer_double_quotes [Boolean] Whether to use double quotes for string values
|
|
62
|
+
# @return [String, nil] The type definition like "type UserRole = 'admin' | 'user'"
|
|
63
|
+
def enum_definition(sort_order: :none, prefer_double_quotes: false)
|
|
64
|
+
return unless enum && enum_type_name
|
|
65
|
+
|
|
66
|
+
values = enum.map { |v| quote_string(v.to_s, prefer_double_quotes) }
|
|
67
|
+
values = values.sort_by(&:downcase) if sort_order == :alphabetical
|
|
68
|
+
"type #{enum_type_name} = #{values.join(" | ")}"
|
|
40
69
|
end
|
|
41
70
|
|
|
42
71
|
private
|
|
43
72
|
|
|
44
|
-
def
|
|
45
|
-
|
|
73
|
+
def quote_string(str, prefer_double_quotes)
|
|
74
|
+
prefer_double_quotes ? "\"#{str}\"" : "'#{str}'"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Returns the type name, optionally sorting union members
|
|
78
|
+
# @param sort_order [Symbol, Proc, nil] Sort order for union types
|
|
79
|
+
# @param prefer_double_quotes [Boolean] Whether to use double quotes for string values
|
|
80
|
+
# @return [String] The type name
|
|
81
|
+
def type_name(sort_order: :none, prefer_double_quotes: false)
|
|
82
|
+
# If enum_type_name is set, use it (named enum type)
|
|
83
|
+
return enum_type_name if enum_type_name
|
|
84
|
+
|
|
85
|
+
if enum
|
|
86
|
+
# Sort enum values if alphabetical sorting is requested
|
|
87
|
+
enum_values = enum.map { |v| quote_string(v.to_s, prefer_double_quotes) }
|
|
88
|
+
enum_values = enum_values.sort_by(&:downcase) if sort_order == :alphabetical
|
|
89
|
+
return enum_values.join(" | ")
|
|
90
|
+
end
|
|
46
91
|
|
|
47
92
|
type.respond_to?(:name) ? type.name : type || "unknown"
|
|
48
93
|
end
|
|
@@ -36,7 +36,7 @@ module Typelizer
|
|
|
36
36
|
# First check for typelize DSL in the trait
|
|
37
37
|
dsl_type = typelizes[prop.column_name.to_sym]
|
|
38
38
|
if dsl_type&.any?
|
|
39
|
-
next
|
|
39
|
+
next prop.with(**dsl_type).tap do |property|
|
|
40
40
|
property.comment ||= model_plugin.comment_for(property) if config.comments && property.comment != false
|
|
41
41
|
property.enum ||= model_plugin.enum_for(property) if property.enum != false
|
|
42
42
|
end
|
|
@@ -17,29 +17,6 @@ module Typelizer
|
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def methods_to_typelize
|
|
21
|
-
[
|
|
22
|
-
:association, :one, :has_one,
|
|
23
|
-
:many, :has_many,
|
|
24
|
-
:attributes, :attribute,
|
|
25
|
-
:method_added,
|
|
26
|
-
:nested_attribute, :nested,
|
|
27
|
-
:meta
|
|
28
|
-
]
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def typelize_method_transform(method:, name:, binding:, type:, attrs:)
|
|
32
|
-
if method == :method_added && binding.local_variable_defined?(:method_name)
|
|
33
|
-
name = binding.local_variable_get(:method_name)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
if [:many, :has_many].include?(method)
|
|
37
|
-
return {name => [type, attrs.merge(multi: true)]}
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
super
|
|
41
|
-
end
|
|
42
|
-
|
|
43
20
|
def root_key
|
|
44
21
|
root = serializer.new({}).send(:_key)
|
|
45
22
|
if !root.nil? && has_transform_key?(serializer) && should_transform_root_key?(serializer)
|
|
@@ -3,19 +3,6 @@ require_relative "base"
|
|
|
3
3
|
module Typelizer
|
|
4
4
|
module SerializerPlugins
|
|
5
5
|
class AMS < Base
|
|
6
|
-
def methods_to_typelize
|
|
7
|
-
[
|
|
8
|
-
:has_many, :has_one, :belongs_to,
|
|
9
|
-
:attribute, :attributes
|
|
10
|
-
]
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def typelize_method_transform(method:, name:, binding:, type:, attrs:)
|
|
14
|
-
return {binding.local_variable_get(:attr) => [type, attrs]} if method == :attribute
|
|
15
|
-
|
|
16
|
-
super
|
|
17
|
-
end
|
|
18
|
-
|
|
19
6
|
def properties
|
|
20
7
|
serializer._attributes_data.merge(serializer._reflections).flat_map do |key, association|
|
|
21
8
|
type = association.options[:serializer] ? context.interface_for(association.options[:serializer]) : nil
|
|
@@ -3,13 +3,6 @@ require_relative "base"
|
|
|
3
3
|
module Typelizer
|
|
4
4
|
module SerializerPlugins
|
|
5
5
|
class OjSerializers < Base
|
|
6
|
-
def methods_to_typelize
|
|
7
|
-
[
|
|
8
|
-
:has_many, :has_one, :belongs_to,
|
|
9
|
-
:flat_one, :attribute, :attributes
|
|
10
|
-
]
|
|
11
|
-
end
|
|
12
|
-
|
|
13
6
|
def properties
|
|
14
7
|
transform_keys = serializer.try(:_transform_keys)
|
|
15
8
|
attributes = serializer._attributes
|
|
@@ -3,10 +3,6 @@ require_relative "base"
|
|
|
3
3
|
module Typelizer
|
|
4
4
|
module SerializerPlugins
|
|
5
5
|
class Panko < Base
|
|
6
|
-
def methods_to_typelize
|
|
7
|
-
[:has_many, :has_one, :attributes, :method_added]
|
|
8
|
-
end
|
|
9
|
-
|
|
10
6
|
def properties
|
|
11
7
|
descriptor = serializer.new.instance_variable_get(:@descriptor)
|
|
12
8
|
attributes = descriptor.attributes
|
|
@@ -25,14 +21,6 @@ module Typelizer
|
|
|
25
21
|
end
|
|
26
22
|
end
|
|
27
23
|
|
|
28
|
-
def typelize_method_transform(method:, name:, binding:, type:, attrs:)
|
|
29
|
-
if method == :method_added && binding.local_variable_defined?(:method)
|
|
30
|
-
name = binding.local_variable_get(:method)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
super
|
|
34
|
-
end
|
|
35
|
-
|
|
36
24
|
private
|
|
37
25
|
|
|
38
26
|
def attribute_property(att)
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
<%- if enums.any? -%>
|
|
2
|
+
<%- enums_path = prefer_double_quotes ? '"./Enums"' : "'./Enums'" -%>
|
|
3
|
+
export type { <%= ImportSorter.sort(enums.map(&:enum_type_name), imports_sort_order).join(", ") %> } from <%= enums_path %>
|
|
4
|
+
<%- end -%>
|
|
1
5
|
<%- interfaces.each do |interface| -%>
|
|
6
|
+
<%- sorted_traits = ImportSorter.sort(interface.trait_interfaces.map(&:name), interface.config.imports_sort_order) -%>
|
|
2
7
|
<%- if interface.config.verbatim_module_syntax -%>
|
|
3
|
-
export type { <%= interface.name %><%= ", " +
|
|
8
|
+
export type { <%= interface.name %><%= ", " + sorted_traits.join(", ") if sorted_traits.any? %> } from <%= interface.quote('./' + interface.filename) %>
|
|
4
9
|
<%- else -%>
|
|
5
|
-
export type { default as <%= interface.name %><%= ", " +
|
|
10
|
+
export type { default as <%= interface.name %><%= ", " + sorted_traits.join(", ") if sorted_traits.any? %> } from <%= interface.quote('./' + interface.filename) %>
|
|
6
11
|
<%- end -%>
|
|
7
12
|
<%- end -%>
|
|
@@ -1 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
<%
|
|
2
|
+
omit_props = interface.overwritten_properties.map { |pr| interface.quote(pr.name) }
|
|
3
|
+
omit_props = omit_props.sort_by(&:downcase) if interface.config.properties_sort_order == :alphabetical
|
|
4
|
+
-%>
|
|
5
|
+
<%= interface.overwritten_properties.any? ? "Omit<" : "" %><%= interface.parent_interface.name %><%= "[" + interface.quote(interface.parent_interface.root_key) + "]" if interface.parent_interface.root_key %><%= interface.overwritten_properties.any? ? ", " + omit_props.join(' | ') + ">" : ""%>
|
|
@@ -9,14 +9,14 @@ render("inheritance.ts.erb", interface: interface).strip if interface.parent_int
|
|
|
9
9
|
<%= " & " if interface.parent_interface %>{
|
|
10
10
|
<% interface.properties_to_print.each do |property| -%>
|
|
11
11
|
<%= render("comment.ts.erb", interface: interface, property: property) -%>
|
|
12
|
-
<%= indent(property) %>;
|
|
12
|
+
<%= indent(property.render(sort_order: interface.config.properties_sort_order, prefer_double_quotes: interface.config.prefer_double_quotes)) %>;
|
|
13
13
|
<% end -%>
|
|
14
14
|
}
|
|
15
15
|
<% end %><% if interface.root_key %>
|
|
16
16
|
type <%= interface.name %> = {
|
|
17
17
|
<%= indent(interface.root_key) %>: <%= interface.name %>Data;
|
|
18
18
|
<% interface.meta_fields&.each do |property| -%>
|
|
19
|
-
<%= indent(property) %>;
|
|
19
|
+
<%= indent(property.render(sort_order: interface.config.properties_sort_order, prefer_double_quotes: interface.config.prefer_double_quotes)) %>;
|
|
20
20
|
<% end -%>
|
|
21
21
|
}
|
|
22
22
|
<% end -%>
|
|
@@ -24,16 +24,17 @@ type <%= interface.name %> = {
|
|
|
24
24
|
|
|
25
25
|
type <%= trait.name %> = {
|
|
26
26
|
<% trait.properties.each do |property| -%>
|
|
27
|
-
<%= indent(property) %>;
|
|
27
|
+
<%= indent(property.render(sort_order: interface.config.properties_sort_order, prefer_double_quotes: interface.config.prefer_double_quotes)) %>;
|
|
28
28
|
<% end -%>
|
|
29
29
|
}
|
|
30
30
|
<% end -%>
|
|
31
31
|
|
|
32
|
+
<% sorted_trait_names = ImportSorter.sort(interface.trait_interfaces.map(&:name), interface.config.imports_sort_order) -%>
|
|
32
33
|
<% if interface.config.verbatim_module_syntax -%>
|
|
33
|
-
export type { <%= interface.name %><%= ", " +
|
|
34
|
+
export type { <%= interface.name %><%= ", " + sorted_trait_names.join(", ") if sorted_trait_names.any? %> };
|
|
34
35
|
<% else -%>
|
|
35
36
|
export default <%= interface.name %>;
|
|
36
|
-
<% if
|
|
37
|
-
export type { <%=
|
|
37
|
+
<% if sorted_trait_names.any? -%>
|
|
38
|
+
export type { <%= sorted_trait_names.join(", ") %> };
|
|
38
39
|
<% end -%>
|
|
39
40
|
<% end -%>
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typelizer
|
|
4
|
+
# Sorts union type members within TypeScript type strings.
|
|
5
|
+
# Handles types like "Type3 | Type1 | Type2" -> "Type1 | Type2 | Type3"
|
|
6
|
+
# Also handles complex nested types like "Array<Type3 | Type1>" -> "Array<Type1 | Type3>"
|
|
7
|
+
module UnionTypeSorter
|
|
8
|
+
# Sorts union type members in a type string
|
|
9
|
+
# @param type_str [String] The type string potentially containing unions
|
|
10
|
+
# @param sort_order [Symbol, Proc, nil] The sort order (:none, :alphabetical, or Proc)
|
|
11
|
+
# @return [String] The type string with sorted union members
|
|
12
|
+
def self.sort(type_str, sort_order)
|
|
13
|
+
return type_str if type_str.nil? || type_str.empty?
|
|
14
|
+
|
|
15
|
+
case sort_order
|
|
16
|
+
when :none, nil
|
|
17
|
+
type_str
|
|
18
|
+
when :alphabetical
|
|
19
|
+
sort_unions_alphabetically(type_str)
|
|
20
|
+
when Proc
|
|
21
|
+
result = sort_order.call(type_str)
|
|
22
|
+
result.is_a?(String) ? result : type_str
|
|
23
|
+
else
|
|
24
|
+
type_str
|
|
25
|
+
end
|
|
26
|
+
rescue => e
|
|
27
|
+
Typelizer.logger.warn("UnionTypeSorter error: #{e.message}, preserving original order")
|
|
28
|
+
type_str
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Sorts union members alphabetically while preserving structure
|
|
32
|
+
# @param type_str [String] The type string to sort
|
|
33
|
+
# @return [String] The sorted type string
|
|
34
|
+
def self.sort_unions_alphabetically(type_str)
|
|
35
|
+
# Handle the string by sorting unions at each level
|
|
36
|
+
# We need to be careful with nested generics like Array<A | B | C>
|
|
37
|
+
|
|
38
|
+
result = type_str.dup
|
|
39
|
+
|
|
40
|
+
# First, handle unions inside angle brackets (generics)
|
|
41
|
+
# Match content inside < > and sort unions within
|
|
42
|
+
result = result.gsub(/<([^<>]+)>/) do |match|
|
|
43
|
+
inner = Regexp.last_match(1)
|
|
44
|
+
sorted_inner = sort_simple_union(inner)
|
|
45
|
+
"<#{sorted_inner}>"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Then handle any remaining top-level unions
|
|
49
|
+
# But avoid sorting if the string has unbalanced brackets
|
|
50
|
+
if balanced_brackets?(result)
|
|
51
|
+
result = sort_top_level_union(result)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
result
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Sorts a simple union string (no nested generics)
|
|
58
|
+
# @param union_str [String] String like "Type3 | Type1 | Type2"
|
|
59
|
+
# @return [String] Sorted string like "Type1 | Type2 | Type3"
|
|
60
|
+
def self.sort_simple_union(union_str)
|
|
61
|
+
return union_str unless union_str.include?("|")
|
|
62
|
+
|
|
63
|
+
parts = split_union_members(union_str)
|
|
64
|
+
return union_str if parts.size <= 1
|
|
65
|
+
|
|
66
|
+
# Sort while preserving special cases:
|
|
67
|
+
# - 'null' should typically stay at the end
|
|
68
|
+
# - Keep the relative order of complex nested types
|
|
69
|
+
regular_parts, null_parts = parts.partition { |p| p.strip.downcase != "null" }
|
|
70
|
+
|
|
71
|
+
sorted_regular = regular_parts.sort_by { |p| p.strip.downcase }
|
|
72
|
+
(sorted_regular + null_parts).join(" | ")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Sorts top-level union (handles cases where unions aren't inside generics)
|
|
76
|
+
# @param type_str [String] The type string
|
|
77
|
+
# @return [String] The sorted type string
|
|
78
|
+
def self.sort_top_level_union(type_str)
|
|
79
|
+
return type_str unless type_str.include?("|")
|
|
80
|
+
|
|
81
|
+
parts = split_union_members(type_str)
|
|
82
|
+
return type_str if parts.size <= 1
|
|
83
|
+
|
|
84
|
+
# Separate null from other types
|
|
85
|
+
regular_parts, null_parts = parts.partition { |p| p.strip.downcase != "null" }
|
|
86
|
+
|
|
87
|
+
sorted_regular = regular_parts.sort_by { |p| p.strip.downcase }
|
|
88
|
+
(sorted_regular + null_parts).join(" | ")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Splits union members while respecting nested brackets
|
|
92
|
+
# @param str [String] The string to split
|
|
93
|
+
# @return [Array<String>] Array of union members
|
|
94
|
+
def self.split_union_members(str)
|
|
95
|
+
members = []
|
|
96
|
+
current = +""
|
|
97
|
+
depth = 0
|
|
98
|
+
|
|
99
|
+
str.each_char do |char|
|
|
100
|
+
case char
|
|
101
|
+
when "<", "("
|
|
102
|
+
depth += 1
|
|
103
|
+
current << char
|
|
104
|
+
when ">", ")"
|
|
105
|
+
depth -= 1
|
|
106
|
+
current << char
|
|
107
|
+
when "|"
|
|
108
|
+
if depth == 0
|
|
109
|
+
members << current.strip unless current.strip.empty?
|
|
110
|
+
current = +""
|
|
111
|
+
else
|
|
112
|
+
current << char
|
|
113
|
+
end
|
|
114
|
+
else
|
|
115
|
+
current << char
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
members << current.strip unless current.strip.empty?
|
|
120
|
+
members
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Checks if brackets are balanced in the string
|
|
124
|
+
# @param str [String] The string to check
|
|
125
|
+
# @return [Boolean] True if brackets are balanced
|
|
126
|
+
def self.balanced_brackets?(str)
|
|
127
|
+
angle_depth = 0
|
|
128
|
+
paren_depth = 0
|
|
129
|
+
|
|
130
|
+
str.each_char do |char|
|
|
131
|
+
case char
|
|
132
|
+
when "<"
|
|
133
|
+
angle_depth += 1
|
|
134
|
+
when ">"
|
|
135
|
+
angle_depth -= 1
|
|
136
|
+
return false if angle_depth < 0
|
|
137
|
+
when "("
|
|
138
|
+
paren_depth += 1
|
|
139
|
+
when ")"
|
|
140
|
+
paren_depth -= 1
|
|
141
|
+
return false if paren_depth < 0
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
angle_depth == 0 && paren_depth == 0
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
data/lib/typelizer/version.rb
CHANGED
data/lib/typelizer/writer.rb
CHANGED
|
@@ -22,7 +22,10 @@ module Typelizer
|
|
|
22
22
|
begin
|
|
23
23
|
written_files.concat(valid_interfaces.map { |interface| write_interface(interface) })
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
enums = collect_enums(valid_interfaces)
|
|
26
|
+
written_files << write_enums(enums) if enums.any?
|
|
27
|
+
|
|
28
|
+
written_files << write_index(valid_interfaces, enums: enums)
|
|
26
29
|
|
|
27
30
|
cleanup_stale_files(written_files) unless force
|
|
28
31
|
|
|
@@ -49,9 +52,29 @@ module Typelizer
|
|
|
49
52
|
File.delete(*stale_files) unless stale_files.empty?
|
|
50
53
|
end
|
|
51
54
|
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
def collect_enums(interfaces)
|
|
56
|
+
interfaces
|
|
57
|
+
.flat_map(&:enum_types)
|
|
58
|
+
.uniq(&:enum_type_name)
|
|
59
|
+
.sort_by(&:enum_type_name)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def write_enums(enums)
|
|
63
|
+
fingerprint = [enums.map(&:fingerprint), config.properties_sort_order, config.prefer_double_quotes].inspect
|
|
64
|
+
write_file("Enums.ts", fingerprint) do
|
|
65
|
+
render_template("enums.ts.erb", enums: enums, sort_order: config.properties_sort_order, prefer_double_quotes: config.prefer_double_quotes)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def write_index(interfaces, enums: [])
|
|
70
|
+
fingerprint = [
|
|
71
|
+
enums.map(&:enum_type_name),
|
|
72
|
+
interfaces.map { |i|
|
|
73
|
+
[i.name, i.filename, i.trait_interfaces.map(&:name), CONFIGS_AFFECTING_INDEX_OUTPUT.map { |key| i.config.public_send(key) }]
|
|
74
|
+
}
|
|
75
|
+
].inspect
|
|
76
|
+
write_file("index.ts", fingerprint) do
|
|
77
|
+
render_template("index.ts.erb", interfaces: interfaces, enums: enums, imports_sort_order: config.imports_sort_order, prefer_double_quotes: config.prefer_double_quotes)
|
|
55
78
|
end
|
|
56
79
|
end
|
|
57
80
|
|
data/lib/typelizer.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "typelizer/version"
|
|
4
|
+
require_relative "typelizer/union_type_sorter"
|
|
4
5
|
require_relative "typelizer/property"
|
|
5
6
|
require_relative "typelizer/model_plugins/auto"
|
|
6
7
|
require_relative "typelizer/serializer_plugins/auto"
|
|
@@ -10,8 +11,8 @@ require_relative "typelizer/configuration"
|
|
|
10
11
|
require_relative "typelizer/serializer_config_layer"
|
|
11
12
|
|
|
12
13
|
require_relative "typelizer/contexts/writer_context"
|
|
13
|
-
require_relative "typelizer/contexts/scan_context"
|
|
14
14
|
require_relative "typelizer/property_sorter"
|
|
15
|
+
require_relative "typelizer/import_sorter"
|
|
15
16
|
require_relative "typelizer/interface"
|
|
16
17
|
require_relative "typelizer/renderer"
|
|
17
18
|
require_relative "typelizer/writer"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: typelizer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Svyatoslav Kryukov
|
|
@@ -50,10 +50,15 @@ files:
|
|
|
50
50
|
- lib/typelizer.rb
|
|
51
51
|
- lib/typelizer/config.rb
|
|
52
52
|
- lib/typelizer/configuration.rb
|
|
53
|
-
- lib/typelizer/contexts/scan_context.rb
|
|
54
53
|
- lib/typelizer/contexts/writer_context.rb
|
|
55
54
|
- lib/typelizer/dsl.rb
|
|
55
|
+
- lib/typelizer/dsl/hooks.rb
|
|
56
|
+
- lib/typelizer/dsl/hooks/alba.rb
|
|
57
|
+
- lib/typelizer/dsl/hooks/ams.rb
|
|
58
|
+
- lib/typelizer/dsl/hooks/oj_serializers.rb
|
|
59
|
+
- lib/typelizer/dsl/hooks/panko.rb
|
|
56
60
|
- lib/typelizer/generator.rb
|
|
61
|
+
- lib/typelizer/import_sorter.rb
|
|
57
62
|
- lib/typelizer/interface.rb
|
|
58
63
|
- lib/typelizer/listen.rb
|
|
59
64
|
- lib/typelizer/model_plugins/active_record.rb
|
|
@@ -73,12 +78,14 @@ files:
|
|
|
73
78
|
- lib/typelizer/serializer_plugins/oj_serializers.rb
|
|
74
79
|
- lib/typelizer/serializer_plugins/panko.rb
|
|
75
80
|
- lib/typelizer/templates/comment.ts.erb
|
|
81
|
+
- lib/typelizer/templates/enums.ts.erb
|
|
76
82
|
- lib/typelizer/templates/fingerprint.ts.erb
|
|
77
83
|
- lib/typelizer/templates/index.ts.erb
|
|
78
84
|
- lib/typelizer/templates/inheritance.ts.erb
|
|
79
85
|
- lib/typelizer/templates/inline_type.ts.erb
|
|
80
86
|
- lib/typelizer/templates/interface.ts.erb
|
|
81
87
|
- lib/typelizer/type_parser.rb
|
|
88
|
+
- lib/typelizer/union_type_sorter.rb
|
|
82
89
|
- lib/typelizer/version.rb
|
|
83
90
|
- lib/typelizer/writer.rb
|
|
84
91
|
homepage: https://github.com/skryukov/typelizer
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Typelizer
|
|
4
|
-
# Builds a minimal plugin used during scan time
|
|
5
|
-
class ScanContext
|
|
6
|
-
class InvalidOperationError < StandardError; end
|
|
7
|
-
|
|
8
|
-
# Interface creation is not available during DSL scanning phase (TracePoint)
|
|
9
|
-
def self.interface_for(serializer_class)
|
|
10
|
-
class_name = serializer_class&.name || "unknown class"
|
|
11
|
-
raise InvalidOperationError,
|
|
12
|
-
"Interface creation is not allowed during DSL scan (#{class_name})"
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# just in case, if we call ScanContext like an object
|
|
16
|
-
def interface_for(serializer_class)
|
|
17
|
-
self.class.interface_for(serializer_class)
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|