typelizer 0.5.3 → 0.5.4
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 +75 -2
- data/README.md +100 -1
- data/lib/typelizer/dsl.rb +18 -2
- data/lib/typelizer/interface.rb +26 -3
- data/lib/typelizer/property.rb +8 -0
- data/lib/typelizer/serializer_plugins/alba/trait_attribute_collector.rb +113 -0
- data/lib/typelizer/serializer_plugins/alba/trait_interface.rb +62 -0
- data/lib/typelizer/serializer_plugins/alba.rb +75 -0
- data/lib/typelizer/templates/index.ts.erb +2 -2
- data/lib/typelizer/templates/interface.ts.erb +12 -1
- data/lib/typelizer/type_parser.rb +39 -0
- data/lib/typelizer/version.rb +1 -1
- data/lib/typelizer.rb +1 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '0528e1e626a19920401677115cc49d13d3c2a730b8f332e285dcdaada8699a62'
|
|
4
|
+
data.tar.gz: 171dde8c90cc46ee6eb5cf434f2c7cd83f15098a233e9e2a5dbb2b5f36ba470c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b3785036afe6f23d5f5f830271081d20f99810e3582d853b28bd34e5558e20609615b929a870c6ef518b75389b48c6e092a8043d8c24510ad1a01b9d82449021
|
|
7
|
+
data.tar.gz: c0bde62a2756d3b67cba8bf8fa061f898d2837bae191f80fa6960c1fe08c654044807c0c8157cffa6c45edee632716f7c105a54cae4b5f87d3ece6b546c7cda1
|
data/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,79 @@ and this project adheres to [Semantic Versioning].
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
## [0.5.
|
|
10
|
+
## [0.5.4] - 2025-12-08
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Type shortcuts for `typelize` method. ([@skryukov])
|
|
15
|
+
|
|
16
|
+
Use `?` suffix for optional and `[]` suffix for arrays:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
typelize "string?" # optional: true
|
|
20
|
+
typelize "number[]" # multi: true
|
|
21
|
+
typelize "string?[]" # optional: true, multi: true
|
|
22
|
+
|
|
23
|
+
# With hash syntax
|
|
24
|
+
typelize name: "string?", tags: "string[]"
|
|
25
|
+
|
|
26
|
+
# Combined with explicit options
|
|
27
|
+
typelize status: ["string?", nullable: true]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Generates:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
name?: string;
|
|
34
|
+
tags: Array<string>;
|
|
35
|
+
roles?: Array<string>;
|
|
36
|
+
status?: string | null;
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- Alba: support for traits. ([@skryukov])
|
|
40
|
+
|
|
41
|
+
Typelizer now generates TypeScript types for Alba traits and supports `with_traits` in associations:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
class UserResource < ApplicationResource
|
|
45
|
+
attributes :id, :name
|
|
46
|
+
|
|
47
|
+
trait :detailed do
|
|
48
|
+
attributes :email, :created_at
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
trait :with_posts do
|
|
52
|
+
has_many :posts, resource: PostResource, with_traits: [:summary]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Generates:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
type User = {
|
|
61
|
+
id: number;
|
|
62
|
+
name: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type UserDetailedTrait = {
|
|
66
|
+
email: string;
|
|
67
|
+
created_at: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type UserWithPostsTrait = {
|
|
71
|
+
posts: Array<Post & PostSummaryTrait>;
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
When using `with_traits` in associations, Typelizer generates intersection types:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
has_one :author, resource: UserResource, with_traits: [:detailed]
|
|
79
|
+
# Generates: author: User & UserDetailedTrait
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## [0.5.3] - 2025-11-25
|
|
11
83
|
|
|
12
84
|
## Fixed
|
|
13
85
|
|
|
@@ -207,7 +279,8 @@ and this project adheres to [Semantic Versioning].
|
|
|
207
279
|
[@prog-supdex]: https://github.com/prog-supdex
|
|
208
280
|
[@ventsislaf]: https://github.com/ventsislaf
|
|
209
281
|
|
|
210
|
-
[Unreleased]: https://github.com/skryukov/typelizer/compare/v0.5.
|
|
282
|
+
[Unreleased]: https://github.com/skryukov/typelizer/compare/v0.5.4...HEAD
|
|
283
|
+
[0.5.4]: https://github.com/skryukov/typelizer/compare/v0.5.3...v0.5.4
|
|
211
284
|
[0.5.3]: https://github.com/skryukov/typelizer/compare/v0.5.2...v0.5.3
|
|
212
285
|
[0.5.2]: https://github.com/skryukov/typelizer/compare/v0.5.1...v0.5.2
|
|
213
286
|
[0.5.1]: https://github.com/skryukov/typelizer/compare/v0.5.0...v0.5.1
|
data/README.md
CHANGED
|
@@ -11,6 +11,7 @@ Typelizer generates TypeScript types from your Ruby serializers. It supports mul
|
|
|
11
11
|
- [Usage](#usage)
|
|
12
12
|
- [Basic Setup](#basic-setup)
|
|
13
13
|
- [Manual Typing](#manual-typing)
|
|
14
|
+
- [Alba Traits](#alba-traits)
|
|
14
15
|
- [TypeScript Integration](#typescript-integration)
|
|
15
16
|
- [Manual Generation](#manual-generation)
|
|
16
17
|
- [Automatic Generation in Development](#automatic-generation-in-development)
|
|
@@ -108,12 +109,110 @@ class PostResource < ApplicationResource
|
|
|
108
109
|
end
|
|
109
110
|
```
|
|
110
111
|
|
|
111
|
-
You can also
|
|
112
|
+
You can also use shortcut syntax for common type modifiers:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
class PostResource < ApplicationResource
|
|
116
|
+
typelize author_name: "string?" # optional string (name?: string)
|
|
117
|
+
typelize tag_ids: "number[]" # array of numbers (tag_ids: Array<number>)
|
|
118
|
+
typelize categories: "string?[]" # optional array of strings (categories?: Array<string>)
|
|
119
|
+
|
|
120
|
+
# Shortcuts can be combined with explicit options
|
|
121
|
+
typelize status: ["string?", nullable: true] # optional and nullable
|
|
122
|
+
|
|
123
|
+
# Also works with keyless typelize
|
|
124
|
+
typelize "string?"
|
|
125
|
+
attribute :nickname do |user|
|
|
126
|
+
user.nickname
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
For more complex type definitions, use the full API:
|
|
112
132
|
|
|
113
133
|
```ruby
|
|
114
134
|
typelize attribute_name: ["string", "Date", optional: true, nullable: true, multi: true, enum: %w[foo bar], comment: "Attribute description", deprecated: "Use `another_attribute` instead"]
|
|
115
135
|
```
|
|
116
136
|
|
|
137
|
+
### Alba Traits
|
|
138
|
+
|
|
139
|
+
Typelizer supports [Alba traits](https://github.com/okuramasafumi/alba#traits), generating separate TypeScript types for each trait. When using `with_traits` in associations, Typelizer generates intersection types.
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
class UserResource < ApplicationResource
|
|
143
|
+
attributes :id, :name
|
|
144
|
+
|
|
145
|
+
trait :detailed do
|
|
146
|
+
attributes :email, :created_at
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
trait :with_posts do
|
|
150
|
+
has_many :posts, resource: PostResource, with_traits: [:summary]
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
This generates:
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// User.ts
|
|
159
|
+
export type User = {
|
|
160
|
+
id: number;
|
|
161
|
+
name: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
type UserDetailedTrait = {
|
|
165
|
+
email: string;
|
|
166
|
+
created_at: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
type UserWithPostsTrait = {
|
|
170
|
+
posts: Array<Post & PostSummaryTrait>;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export default User;
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
When using `with_traits` in associations, Typelizer generates intersection types combining the base type with trait types:
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
class TeamResource < ApplicationResource
|
|
180
|
+
attributes :id, :name
|
|
181
|
+
has_one :lead, resource: UserResource, with_traits: [:detailed]
|
|
182
|
+
has_many :members, resource: UserResource, with_traits: [:detailed, :with_posts]
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
This generates:
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
// Team.ts
|
|
190
|
+
import type { User, UserDetailedTrait, UserWithPostsTrait } from "@/types";
|
|
191
|
+
|
|
192
|
+
export type Team = {
|
|
193
|
+
id: number;
|
|
194
|
+
name: string;
|
|
195
|
+
lead: User & UserDetailedTrait;
|
|
196
|
+
members: Array<User & UserDetailedTrait & UserWithPostsTrait>;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export default Team;
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The `typelize` method works inside traits for manual type specification:
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
trait :with_stats do
|
|
206
|
+
typelize :number
|
|
207
|
+
attribute :posts_count do |user|
|
|
208
|
+
user.posts.count
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
typelize score: :number
|
|
212
|
+
attributes :score
|
|
213
|
+
end
|
|
214
|
+
```
|
|
215
|
+
|
|
117
216
|
### TypeScript Integration
|
|
118
217
|
|
|
119
218
|
Typelizer generates TypeScript interfaces in the specified output directory:
|
data/lib/typelizer/dsl.rb
CHANGED
|
@@ -37,7 +37,11 @@ module Typelizer
|
|
|
37
37
|
# can be invoked multiple times
|
|
38
38
|
def typelize(type = nil, type_params = {}, **attributes)
|
|
39
39
|
if type
|
|
40
|
-
|
|
40
|
+
# Parse type shortcuts like 'string?', 'string[]'
|
|
41
|
+
parsed = TypeParser.parse(type)
|
|
42
|
+
merged_params = parsed.merge(type_params).merge(attributes)
|
|
43
|
+
actual_type = merged_params.delete(:type)
|
|
44
|
+
@keyless_type = [actual_type, merged_params]
|
|
41
45
|
else
|
|
42
46
|
assign_type_information(:_typelizer_attributes, attributes)
|
|
43
47
|
end
|
|
@@ -79,7 +83,19 @@ module Typelizer
|
|
|
79
83
|
attrs = [attrs] if attrs && !attrs.is_a?(Array)
|
|
80
84
|
options = attrs.last.is_a?(Hash) ? attrs.pop : {}
|
|
81
85
|
|
|
82
|
-
|
|
86
|
+
if attrs.any?
|
|
87
|
+
# Parse type shortcuts and merge options
|
|
88
|
+
parsed_types = attrs.map { |t| TypeParser.parse(t) }
|
|
89
|
+
type_names = parsed_types.map { |p| p[:type] }
|
|
90
|
+
options[:type] = type_names.join(" | ")
|
|
91
|
+
|
|
92
|
+
# Merge modifier flags from all parsed types
|
|
93
|
+
parsed_types.each do |parsed|
|
|
94
|
+
options[:optional] = true if parsed[:optional]
|
|
95
|
+
options[:multi] = true if parsed[:multi]
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
83
99
|
instance_variable_get(instance_variable)[name.to_sym] ||= {}
|
|
84
100
|
instance_variable_get(instance_variable)[name.to_sym].merge!(options)
|
|
85
101
|
end
|
data/lib/typelizer/interface.rb
CHANGED
|
@@ -52,6 +52,12 @@ module Typelizer
|
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
def trait_interfaces
|
|
56
|
+
return [] unless serializer_plugin.respond_to?(:trait_interfaces)
|
|
57
|
+
|
|
58
|
+
@trait_interfaces ||= serializer_plugin.trait_interfaces
|
|
59
|
+
end
|
|
60
|
+
|
|
55
61
|
def properties
|
|
56
62
|
@properties ||= begin
|
|
57
63
|
props = serializer_plugin.properties
|
|
@@ -89,7 +95,10 @@ module Typelizer
|
|
|
89
95
|
|
|
90
96
|
def imports
|
|
91
97
|
@imports ||= begin
|
|
92
|
-
|
|
98
|
+
# Include both main properties and trait properties for import collection
|
|
99
|
+
all_properties = properties_to_print + trait_interfaces.flat_map(&:properties)
|
|
100
|
+
|
|
101
|
+
association_serializers, attribute_types = all_properties.filter_map(&:type)
|
|
93
102
|
.uniq
|
|
94
103
|
.partition { |type| type.is_a?(Interface) }
|
|
95
104
|
|
|
@@ -101,7 +110,16 @@ module Typelizer
|
|
|
101
110
|
.uniq
|
|
102
111
|
.reject { |type| global_type?(type) }
|
|
103
112
|
|
|
104
|
-
|
|
113
|
+
# Collect trait types from properties with with_traits (skip self-references)
|
|
114
|
+
trait_imports = all_properties.flat_map do |prop|
|
|
115
|
+
next [] unless prop.with_traits&.any? && prop.type.is_a?(Interface)
|
|
116
|
+
# Skip if the trait types are from the current interface (same file)
|
|
117
|
+
next [] if prop.type.name == name
|
|
118
|
+
|
|
119
|
+
prop.with_traits.map { |t| "#{prop.type.name}#{t.to_s.camelize}Trait" }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
(custom_type_imports + serializer_types + trait_imports + Array(parent_interface&.name)).uniq - Array(self_type_name)
|
|
105
123
|
end
|
|
106
124
|
end
|
|
107
125
|
|
|
@@ -110,7 +128,12 @@ module Typelizer
|
|
|
110
128
|
end
|
|
111
129
|
|
|
112
130
|
def fingerprint
|
|
113
|
-
|
|
131
|
+
if trait_interfaces.empty?
|
|
132
|
+
"<#{self.class.name} #{name} properties=[#{properties_to_print.map(&:fingerprint).join(", ")}]>"
|
|
133
|
+
else
|
|
134
|
+
traits_fingerprint = trait_interfaces.map { |t| "#{t.name}=[#{t.properties.map(&:fingerprint).join(", ")}]" }.join(", ")
|
|
135
|
+
"<#{self.class.name} #{name} properties=[#{properties_to_print.map(&:fingerprint).join(", ")}] traits=[#{traits_fingerprint}]>"
|
|
136
|
+
end
|
|
114
137
|
end
|
|
115
138
|
|
|
116
139
|
def quote(str)
|
data/lib/typelizer/property.rb
CHANGED
|
@@ -2,6 +2,7 @@ module Typelizer
|
|
|
2
2
|
Property = Struct.new(
|
|
3
3
|
:name, :type, :optional, :nullable,
|
|
4
4
|
:multi, :column_name, :comment, :enum, :deprecated,
|
|
5
|
+
:with_traits,
|
|
5
6
|
keyword_init: true
|
|
6
7
|
) do
|
|
7
8
|
def inspect
|
|
@@ -17,6 +18,13 @@ module Typelizer
|
|
|
17
18
|
|
|
18
19
|
def to_s
|
|
19
20
|
type_str = type_name
|
|
21
|
+
|
|
22
|
+
# Handle intersection types for traits
|
|
23
|
+
if with_traits&.any? && type.respond_to?(:name)
|
|
24
|
+
trait_types = with_traits.map { |t| "#{type.name}#{t.to_s.camelize}Trait" }
|
|
25
|
+
type_str = ([type_str] + trait_types).join(" & ")
|
|
26
|
+
end
|
|
27
|
+
|
|
20
28
|
type_str = "Array<#{type_str}>" if multi
|
|
21
29
|
type_str = "#{type_str} | null" if nullable
|
|
22
30
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typelizer
|
|
4
|
+
module SerializerPlugins
|
|
5
|
+
class Alba::TraitAttributeCollector
|
|
6
|
+
attr_reader :collected_attributes, :collected_typelizes
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@collected_attributes = {}
|
|
10
|
+
@collected_typelizes = {}
|
|
11
|
+
@pending_typelize = nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def attributes(*names, **options)
|
|
15
|
+
names.each do |name|
|
|
16
|
+
@collected_attributes[name] = name
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def attribute(name, **options, &block)
|
|
21
|
+
@collected_attributes[name] = block || name
|
|
22
|
+
# Apply pending typelize to this attribute
|
|
23
|
+
if @pending_typelize
|
|
24
|
+
@collected_typelizes[name] = @pending_typelize
|
|
25
|
+
@pending_typelize = nil
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Capture typelize calls - they apply to the next attribute
|
|
30
|
+
# Handles both:
|
|
31
|
+
# typelize :string, nullable: true (type with options, applies to next attribute)
|
|
32
|
+
# typelize attr_name: [:string, nullable: true] (hash-style, applies to specific attribute)
|
|
33
|
+
def typelize(type_or_hash = nil, **options)
|
|
34
|
+
if type_or_hash.is_a?(Hash)
|
|
35
|
+
# typelize({name: [:string, nullable: true]}) - explicit hash
|
|
36
|
+
type_or_hash.each do |attr_name, type_def|
|
|
37
|
+
@collected_typelizes[attr_name] = normalize_typelize(type_def)
|
|
38
|
+
end
|
|
39
|
+
elsif type_or_hash.nil? && options.any?
|
|
40
|
+
# typelize name: [:string, nullable: true] - Ruby passes as kwargs
|
|
41
|
+
# Check if this looks like attribute definitions (values are arrays or have type-like keys)
|
|
42
|
+
if options.values.first.is_a?(Array) || options.values.first.is_a?(Symbol) || options.values.first.is_a?(String)
|
|
43
|
+
options.each do |attr_name, type_def|
|
|
44
|
+
@collected_typelizes[attr_name] = normalize_typelize(type_def)
|
|
45
|
+
end
|
|
46
|
+
else
|
|
47
|
+
# typelize :string, nullable: true - type with options
|
|
48
|
+
@pending_typelize = normalize_typelize(nil, **options)
|
|
49
|
+
end
|
|
50
|
+
else
|
|
51
|
+
# typelize :string - applies to the next attribute
|
|
52
|
+
@pending_typelize = normalize_typelize(type_or_hash, **options)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Simple struct to hold association info from traits
|
|
57
|
+
TraitAssociation = Struct.new(:name, :resource, :with_traits, :multi, keyword_init: true)
|
|
58
|
+
|
|
59
|
+
# Support association methods that might be used in traits
|
|
60
|
+
def one(name, **options, &block)
|
|
61
|
+
resource = options[:resource] || options[:serializer]
|
|
62
|
+
with_traits = options[:with_traits]
|
|
63
|
+
@collected_attributes[name] = TraitAssociation.new(
|
|
64
|
+
name: name,
|
|
65
|
+
resource: resource,
|
|
66
|
+
with_traits: with_traits,
|
|
67
|
+
multi: false
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
alias_method :has_one, :one
|
|
72
|
+
alias_method :association, :one
|
|
73
|
+
|
|
74
|
+
def many(name, **options, &block)
|
|
75
|
+
resource = options[:resource] || options[:serializer]
|
|
76
|
+
with_traits = options[:with_traits]
|
|
77
|
+
@collected_attributes[name] = TraitAssociation.new(
|
|
78
|
+
name: name,
|
|
79
|
+
resource: resource,
|
|
80
|
+
with_traits: with_traits,
|
|
81
|
+
multi: true
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
alias_method :has_many, :many
|
|
86
|
+
|
|
87
|
+
# Ignore other DSL methods that might be called
|
|
88
|
+
def method_missing(method_name, *args, **kwargs, &block)
|
|
89
|
+
# Silently ignore unknown methods
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
93
|
+
true
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def normalize_typelize(type_def, **options)
|
|
99
|
+
case type_def
|
|
100
|
+
when Array
|
|
101
|
+
# [:string, nullable: true] or ['string?', nullable: true]
|
|
102
|
+
type, *rest = type_def
|
|
103
|
+
opts = rest.first || {}
|
|
104
|
+
TypeParser.parse(type, **opts)
|
|
105
|
+
when Symbol, String
|
|
106
|
+
TypeParser.parse(type_def, **options)
|
|
107
|
+
else
|
|
108
|
+
options
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typelizer
|
|
4
|
+
module SerializerPlugins
|
|
5
|
+
class Alba::TraitInterface
|
|
6
|
+
attr_reader :serializer, :trait_name, :context, :plugin
|
|
7
|
+
|
|
8
|
+
def initialize(serializer:, trait_name:, context:, plugin:)
|
|
9
|
+
@serializer = serializer
|
|
10
|
+
@trait_name = trait_name
|
|
11
|
+
@context = context
|
|
12
|
+
@plugin = plugin
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def config
|
|
16
|
+
context.config_for(serializer)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def name
|
|
20
|
+
base_name = config.serializer_name_mapper.call(serializer).tr_s(":", "")
|
|
21
|
+
"#{base_name}#{trait_name.to_s.camelize}Trait"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def properties
|
|
25
|
+
@properties ||= begin
|
|
26
|
+
props, typelizes = plugin.trait_properties(trait_name)
|
|
27
|
+
infer_types(props, typelizes)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def infer_types(props, typelizes)
|
|
34
|
+
props.map do |prop|
|
|
35
|
+
# First check for typelize DSL in the trait
|
|
36
|
+
dsl_type = typelizes[prop.column_name.to_sym]
|
|
37
|
+
if dsl_type&.any?
|
|
38
|
+
next Property.new(prop.to_h.merge(dsl_type)).tap do |property|
|
|
39
|
+
property.comment ||= model_plugin.comment_for(property) if config.comments && property.comment != false
|
|
40
|
+
property.enum ||= model_plugin.enum_for(property) if property.enum != false
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Fall back to model plugin for type inference
|
|
45
|
+
model_plugin.infer_types(prop)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def model_class
|
|
50
|
+
return serializer._typelizer_model_name if serializer.respond_to?(:_typelizer_model_name)
|
|
51
|
+
|
|
52
|
+
config.instance_exec(serializer, &config.serializer_model_mapper)
|
|
53
|
+
rescue NameError
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def model_plugin
|
|
58
|
+
@model_plugin ||= config.model_plugin.new(model_class: model_class, config: config)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -60,6 +60,74 @@ module Typelizer
|
|
|
60
60
|
]
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
+
def traits
|
|
64
|
+
return {} unless serializer.instance_variable_defined?(:@_traits)
|
|
65
|
+
|
|
66
|
+
serializer.instance_variable_get(:@_traits) || {}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def trait_properties(trait_name)
|
|
70
|
+
trait_block = traits[trait_name]
|
|
71
|
+
return [], {} unless trait_block
|
|
72
|
+
|
|
73
|
+
# Create a collector to capture attributes defined in the trait block
|
|
74
|
+
collector = TraitAttributeCollector.new
|
|
75
|
+
collector.instance_exec(&trait_block)
|
|
76
|
+
|
|
77
|
+
props = collector.collected_attributes.map do |name, attr|
|
|
78
|
+
build_trait_property(name.is_a?(Symbol) ? name.name : name, attr)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
[props, collector.collected_typelizes]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def build_trait_property(name, attr)
|
|
85
|
+
case attr
|
|
86
|
+
when TraitAttributeCollector::TraitAssociation
|
|
87
|
+
with_traits = Array(attr.with_traits) if attr.with_traits
|
|
88
|
+
resource = attr.resource || infer_resource_from_name(name)
|
|
89
|
+
|
|
90
|
+
Property.new(
|
|
91
|
+
name: name,
|
|
92
|
+
type: resource ? context.interface_for(resource) : nil,
|
|
93
|
+
optional: false,
|
|
94
|
+
nullable: false,
|
|
95
|
+
multi: attr.multi,
|
|
96
|
+
column_name: name,
|
|
97
|
+
with_traits: with_traits
|
|
98
|
+
)
|
|
99
|
+
else
|
|
100
|
+
build_property(name, attr)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def infer_resource_from_name(name)
|
|
105
|
+
class_name = name.to_s.classify
|
|
106
|
+
# Try common serializer naming conventions
|
|
107
|
+
["#{class_name}Resource", "#{class_name}Serializer"].each do |resource_name|
|
|
108
|
+
return serializer.const_get(resource_name, false)
|
|
109
|
+
rescue NameError
|
|
110
|
+
# Try in parent namespace
|
|
111
|
+
begin
|
|
112
|
+
return Object.const_get("#{serializer.module_parent}::#{resource_name}")
|
|
113
|
+
rescue NameError
|
|
114
|
+
# Not found in this namespace
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def trait_interfaces
|
|
121
|
+
traits.map do |trait_name, _|
|
|
122
|
+
TraitInterface.new(
|
|
123
|
+
serializer: serializer,
|
|
124
|
+
trait_name: trait_name,
|
|
125
|
+
context: context,
|
|
126
|
+
plugin: self
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
63
131
|
private
|
|
64
132
|
|
|
65
133
|
def build_property(name, attr, **options)
|
|
@@ -92,6 +160,9 @@ module Typelizer
|
|
|
92
160
|
)
|
|
93
161
|
when ::Alba::Association
|
|
94
162
|
resource = attr.instance_variable_get(:@resource)
|
|
163
|
+
# Alba stores with_traits directly in @with_traits, not in @params
|
|
164
|
+
with_traits = attr.instance_variable_get(:@with_traits)
|
|
165
|
+
with_traits = Array(with_traits) if with_traits
|
|
95
166
|
|
|
96
167
|
Property.new(
|
|
97
168
|
name: name,
|
|
@@ -100,6 +171,7 @@ module Typelizer
|
|
|
100
171
|
nullable: false,
|
|
101
172
|
multi: false, # we override this in typelize_method_transform
|
|
102
173
|
column_name: attr.name.is_a?(Symbol) ? attr.name.name : attr.name,
|
|
174
|
+
with_traits: with_traits,
|
|
103
175
|
**options
|
|
104
176
|
)
|
|
105
177
|
when ::Alba::TypedAttribute
|
|
@@ -151,3 +223,6 @@ module Typelizer
|
|
|
151
223
|
end
|
|
152
224
|
end
|
|
153
225
|
end
|
|
226
|
+
|
|
227
|
+
require_relative "alba/trait_attribute_collector"
|
|
228
|
+
require_relative "alba/trait_interface"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<%- interfaces.each do |interface| -%>
|
|
2
2
|
<%- if interface.config.verbatim_module_syntax -%>
|
|
3
|
-
export type { <%= interface.name %> } from <%= interface.quote('./' + interface.filename) %>
|
|
3
|
+
export type { <%= interface.name %><%= ", " + interface.trait_interfaces.map(&:name).join(", ") if interface.trait_interfaces.any? %> } from <%= interface.quote('./' + interface.filename) %>
|
|
4
4
|
<%- else -%>
|
|
5
|
-
export type { default as <%= interface.name %> } from <%= interface.quote('./' + interface.filename) %>
|
|
5
|
+
export type { default as <%= interface.name %><%= ", " + interface.trait_interfaces.map(&:name).join(", ") if interface.trait_interfaces.any? %> } from <%= interface.quote('./' + interface.filename) %>
|
|
6
6
|
<%- end -%>
|
|
7
7
|
<%- end -%>
|
|
@@ -20,9 +20,20 @@ type <%= interface.name %> = {
|
|
|
20
20
|
<% end -%>
|
|
21
21
|
}
|
|
22
22
|
<% end -%>
|
|
23
|
+
<% interface.trait_interfaces.each do |trait| -%>
|
|
24
|
+
|
|
25
|
+
type <%= trait.name %> = {
|
|
26
|
+
<% trait.properties.each do |property| -%>
|
|
27
|
+
<%= indent(property) %>;
|
|
28
|
+
<% end -%>
|
|
29
|
+
}
|
|
30
|
+
<% end -%>
|
|
23
31
|
|
|
24
32
|
<% if interface.config.verbatim_module_syntax -%>
|
|
25
|
-
export type { <%= interface.name %> };
|
|
33
|
+
export type { <%= interface.name %><%= ", " + interface.trait_interfaces.map(&:name).join(", ") if interface.trait_interfaces.any? %> };
|
|
26
34
|
<% else -%>
|
|
27
35
|
export default <%= interface.name %>;
|
|
36
|
+
<% if interface.trait_interfaces.any? -%>
|
|
37
|
+
export type { <%= interface.trait_interfaces.map(&:name).join(", ") %> };
|
|
38
|
+
<% end -%>
|
|
28
39
|
<% end -%>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typelizer
|
|
4
|
+
module TypeParser
|
|
5
|
+
# Regex to match type shortcuts:
|
|
6
|
+
# - Base type (captured)
|
|
7
|
+
# - Optional `?` modifier
|
|
8
|
+
# - Optional `[]` modifier
|
|
9
|
+
# Order of ? and [] can be either way
|
|
10
|
+
TYPE_PATTERN = /\A(.+?)(\?)?(\[\])?(\?)?\z/
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def parse(type_def, **options)
|
|
14
|
+
return options if type_def.nil?
|
|
15
|
+
|
|
16
|
+
type_str = type_def.to_s
|
|
17
|
+
match = TYPE_PATTERN.match(type_str)
|
|
18
|
+
|
|
19
|
+
return {type: type_def}.merge(options) unless match
|
|
20
|
+
|
|
21
|
+
base_type = match[1]
|
|
22
|
+
optional = match[2] == "?" || match[4] == "?"
|
|
23
|
+
multi = match[3] == "[]"
|
|
24
|
+
|
|
25
|
+
result = {type: base_type.to_sym}
|
|
26
|
+
result[:optional] = true if optional
|
|
27
|
+
result[:multi] = true if multi
|
|
28
|
+
result.merge(options)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def shortcut?(type_def)
|
|
32
|
+
return false if type_def.nil?
|
|
33
|
+
|
|
34
|
+
type_str = type_def.to_s
|
|
35
|
+
type_str.end_with?("?", "[]")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/typelizer/version.rb
CHANGED
data/lib/typelizer.rb
CHANGED
|
@@ -15,6 +15,7 @@ require_relative "typelizer/interface"
|
|
|
15
15
|
require_relative "typelizer/renderer"
|
|
16
16
|
require_relative "typelizer/writer"
|
|
17
17
|
require_relative "typelizer/generator"
|
|
18
|
+
require_relative "typelizer/type_parser"
|
|
18
19
|
require_relative "typelizer/dsl"
|
|
19
20
|
|
|
20
21
|
require_relative "typelizer/serializer_plugins/oj_serializers"
|
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.5.
|
|
4
|
+
version: 0.5.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Svyatoslav Kryukov
|
|
@@ -64,6 +64,8 @@ files:
|
|
|
64
64
|
- lib/typelizer/renderer.rb
|
|
65
65
|
- lib/typelizer/serializer_config_layer.rb
|
|
66
66
|
- lib/typelizer/serializer_plugins/alba.rb
|
|
67
|
+
- lib/typelizer/serializer_plugins/alba/trait_attribute_collector.rb
|
|
68
|
+
- lib/typelizer/serializer_plugins/alba/trait_interface.rb
|
|
67
69
|
- lib/typelizer/serializer_plugins/ams.rb
|
|
68
70
|
- lib/typelizer/serializer_plugins/auto.rb
|
|
69
71
|
- lib/typelizer/serializer_plugins/base.rb
|
|
@@ -75,6 +77,7 @@ files:
|
|
|
75
77
|
- lib/typelizer/templates/inheritance.ts.erb
|
|
76
78
|
- lib/typelizer/templates/inline_type.ts.erb
|
|
77
79
|
- lib/typelizer/templates/interface.ts.erb
|
|
80
|
+
- lib/typelizer/type_parser.rb
|
|
78
81
|
- lib/typelizer/version.rb
|
|
79
82
|
- lib/typelizer/writer.rb
|
|
80
83
|
homepage: https://github.com/skryukov/typelizer
|
|
@@ -101,7 +104,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
101
104
|
- !ruby/object:Gem::Version
|
|
102
105
|
version: '0'
|
|
103
106
|
requirements: []
|
|
104
|
-
rubygems_version:
|
|
107
|
+
rubygems_version: 4.0.0
|
|
105
108
|
specification_version: 4
|
|
106
109
|
summary: A TypeScript type generator for Ruby serializers.
|
|
107
110
|
test_files: []
|