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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3deaa0f184a8406a1bcca5994b75565a861c32e4afd2af6e7c48643422127f6
4
- data.tar.gz: d270f2596b33c68d7cd426866618d573c9798648376e66b8e1e0bf76d3967c28
3
+ metadata.gz: '0528e1e626a19920401677115cc49d13d3c2a730b8f332e285dcdaada8699a62'
4
+ data.tar.gz: 171dde8c90cc46ee6eb5cf434f2c7cd83f15098a233e9e2a5dbb2b5f36ba470c
5
5
  SHA512:
6
- metadata.gz: 67fcb4958821e25180c5a8493ca1d16b2e9f590959d2ef97f347018e692b3d94b9f084222355db547b1de27b0a9758049e3da391c640553a81aa33a4ab821bda
7
- data.tar.gz: e1fba11ea9af7e654da8dde504582748a2d1046d15cb52a343895c9453d5e779215bc15f9827fa8604a822b04e2e2acef72f7176f77bd52783df2094e8b68d37
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.2] - 2025-11-25
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.3...HEAD
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 specify more complex type definitions using a lower-level API:
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
- @keyless_type = [type, type_params.merge(attributes)]
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
- options[:type] = attrs.join(" | ") if attrs.any?
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
@@ -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
- association_serializers, attribute_types = properties_to_print.filter_map(&:type)
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
- (custom_type_imports + serializer_types + Array(parent_interface&.name)).uniq - Array(self_type_name)
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
- "<#{self.class.name} #{name} properties=[#{properties_to_print.map(&:fingerprint).join(", ")}]>"
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)
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Typelizer
4
- VERSION = "0.5.3"
4
+ VERSION = "0.5.4"
5
5
  end
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.3
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: 3.6.9
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: []