typelizer 0.5.3 → 0.5.5

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: 7b353423e2bc10fd89e3d944692c04b41b69f6b75567821060b5b9e84b5009a0
4
+ data.tar.gz: 6abb793e276d1a6627284ac7e3e5a37888e9d48850f0f74c3475ee5034c06d11
5
5
  SHA512:
6
- metadata.gz: 67fcb4958821e25180c5a8493ca1d16b2e9f590959d2ef97f347018e692b3d94b9f084222355db547b1de27b0a9758049e3da391c640553a81aa33a4ab821bda
7
- data.tar.gz: e1fba11ea9af7e654da8dde504582748a2d1046d15cb52a343895c9453d5e779215bc15f9827fa8604a822b04e2e2acef72f7176f77bd52783df2094e8b68d37
6
+ metadata.gz: 92ebe7361f52e8644576b8eabffcf0602f24369e9f6a9bc82837550b123440438befef9fb0d730c8c96cab63e111858aa05088b65aa46ab2a1c6b2c58868fcff
7
+ data.tar.gz: 9ee89e0e1761257e51a97ba4666c3cecc4c02581f1726117f67f5328be510b4fd05653f46effad3d33fb119dcee01c9734160be3b2893d3cf566a2d5079c05f5
data/CHANGELOG.md CHANGED
@@ -7,7 +7,110 @@ and this project adheres to [Semantic Versioning].
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [0.5.2] - 2025-11-25
10
+ ## [0.5.5] - 2025-12-24
11
+
12
+ ### Added
13
+
14
+ - New `properties_sort_order` configuration option for consistent property ordering in generated TypeScript interfaces. ([@skryukov])
15
+
16
+ ```ruby
17
+ Typelizer.configure do |config|
18
+ # Sort properties alphabetically with 'id' first
19
+ config.properties_sort_order = :id_first_alphabetical
20
+ end
21
+ ```
22
+
23
+ Available options:
24
+ - `:none` (default) - preserve serializer definition order
25
+ - `:alphabetical` - sort properties A-Z (case-insensitive)
26
+ - `:id_first_alphabetical` - place `id` first, then sort remaining A-Z
27
+ - `Proc` - custom sorting logic
28
+
29
+ ```ruby
30
+ # Custom sorting example
31
+ config.properties_sort_order = ->(props) {
32
+ priority = %w[id uuid type]
33
+ props.sort_by { |p| [priority.index(p.name) || 999, p.name] }
34
+ }
35
+ ```
36
+
37
+ ### Fixed
38
+
39
+ - Fix self-import issue when using custom `typelize` types for self-referential associations in namespaced serializers. ([@skryukov])
40
+
41
+ ## [0.5.4] - 2025-12-08
42
+
43
+ ### Added
44
+
45
+ - Type shortcuts for `typelize` method. ([@skryukov])
46
+
47
+ Use `?` suffix for optional and `[]` suffix for arrays:
48
+
49
+ ```ruby
50
+ typelize "string?" # optional: true
51
+ typelize "number[]" # multi: true
52
+ typelize "string?[]" # optional: true, multi: true
53
+
54
+ # With hash syntax
55
+ typelize name: "string?", tags: "string[]"
56
+
57
+ # Combined with explicit options
58
+ typelize status: ["string?", nullable: true]
59
+ ```
60
+
61
+ Generates:
62
+
63
+ ```typescript
64
+ name?: string;
65
+ tags: Array<string>;
66
+ roles?: Array<string>;
67
+ status?: string | null;
68
+ ```
69
+
70
+ - Alba: support for traits. ([@skryukov])
71
+
72
+ Typelizer now generates TypeScript types for Alba traits and supports `with_traits` in associations:
73
+
74
+ ```ruby
75
+ class UserResource < ApplicationResource
76
+ attributes :id, :name
77
+
78
+ trait :detailed do
79
+ attributes :email, :created_at
80
+ end
81
+
82
+ trait :with_posts do
83
+ has_many :posts, resource: PostResource, with_traits: [:summary]
84
+ end
85
+ end
86
+ ```
87
+
88
+ Generates:
89
+
90
+ ```typescript
91
+ type User = {
92
+ id: number;
93
+ name: string;
94
+ }
95
+
96
+ type UserDetailedTrait = {
97
+ email: string;
98
+ created_at: string;
99
+ }
100
+
101
+ type UserWithPostsTrait = {
102
+ posts: Array<Post & PostSummaryTrait>;
103
+ }
104
+ ```
105
+
106
+ When using `with_traits` in associations, Typelizer generates intersection types:
107
+
108
+ ```ruby
109
+ has_one :author, resource: UserResource, with_traits: [:detailed]
110
+ # Generates: author: User & UserDetailedTrait
111
+ ```
112
+
113
+ ## [0.5.3] - 2025-11-25
11
114
 
12
115
  ## Fixed
13
116
 
@@ -207,7 +310,9 @@ and this project adheres to [Semantic Versioning].
207
310
  [@prog-supdex]: https://github.com/prog-supdex
208
311
  [@ventsislaf]: https://github.com/ventsislaf
209
312
 
210
- [Unreleased]: https://github.com/skryukov/typelizer/compare/v0.5.3...HEAD
313
+ [Unreleased]: https://github.com/skryukov/typelizer/compare/v0.5.5...HEAD
314
+ [0.5.5]: https://github.com/skryukov/typelizer/compare/v0.5.4...v0.5.5
315
+ [0.5.4]: https://github.com/skryukov/typelizer/compare/v0.5.3...v0.5.4
211
316
  [0.5.3]: https://github.com/skryukov/typelizer/compare/v0.5.2...v0.5.3
212
317
  [0.5.2]: https://github.com/skryukov/typelizer/compare/v0.5.1...v0.5.2
213
318
  [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:
@@ -387,6 +486,13 @@ Typelizer.configure do |config|
387
486
  # Custom transformation for generated properties
388
487
  config.properties_transformer = ->(properties) { ... }
389
488
 
489
+ # Strategy for ordering properties in generated TypeScript interfaces
490
+ # :none - preserve serializer definition order (default)
491
+ # :alphabetical - sort properties A-Z (case-insensitive)
492
+ # :id_first_alphabetical - place 'id' first, then sort remaining A-Z
493
+ # Proc - custom sorting function receiving array of Property objects
494
+ config.properties_sort_order = :none
495
+
390
496
  # Plugin for model type inference (default: ModelPlugins::Auto)
391
497
  config.model_plugin = Typelizer::ModelPlugins::Auto
392
498
 
@@ -23,6 +23,7 @@ module Typelizer
23
23
  :serializer_name_mapper,
24
24
  :serializer_model_mapper,
25
25
  :properties_transformer,
26
+ :properties_sort_order,
26
27
  :model_plugin,
27
28
  :serializer_plugin,
28
29
  :plugin_configs,
@@ -77,6 +78,7 @@ module Typelizer
77
78
  types_import_path: "@/types",
78
79
  types_global: DEFAULT_TYPES_GLOBAL,
79
80
  properties_transformer: nil,
81
+ properties_sort_order: :none,
80
82
  verbatim_module_syntax: false
81
83
  }
82
84
  end
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
@@ -48,16 +48,22 @@ module Typelizer
48
48
  props = serializer_plugin.meta_fields || []
49
49
  props = infer_types(props, :_typelizer_meta_attributes)
50
50
  props = config.properties_transformer.call(props) if config.properties_transformer
51
- props
51
+ PropertySorter.sort(props, config.properties_sort_order)
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
58
64
  props = infer_types(props)
59
65
  props = config.properties_transformer.call(props) if config.properties_transformer
60
- props
66
+ PropertySorter.sort(props, config.properties_sort_order)
61
67
  end
62
68
  end
63
69
 
@@ -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 - [self_type_name, 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,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typelizer
4
+ module PropertySorter
5
+ def self.sort(props, sort_order)
6
+ case sort_order
7
+ when :none, nil
8
+ props
9
+ when :alphabetical
10
+ props.sort_by { |p| p.name.to_s.downcase }
11
+ when :id_first_alphabetical
12
+ props.sort_by { |p| [(p.name.to_s.downcase == "id") ? 0 : 1, p.name.to_s.downcase] }
13
+ when Proc
14
+ result = sort_order.call(props)
15
+ result.is_a?(Array) ? result : props
16
+ else
17
+ props
18
+ end
19
+ rescue => e
20
+ Typelizer.logger.warn("PropertySorter error: #{e.message}, preserving original order")
21
+ props
22
+ end
23
+ end
24
+ end
@@ -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,63 @@
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
+ props = infer_types(props, typelizes)
28
+ PropertySorter.sort(props, config.properties_sort_order)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def infer_types(props, typelizes)
35
+ props.map do |prop|
36
+ # First check for typelize DSL in the trait
37
+ dsl_type = typelizes[prop.column_name.to_sym]
38
+ if dsl_type&.any?
39
+ next Property.new(prop.to_h.merge(dsl_type)).tap do |property|
40
+ property.comment ||= model_plugin.comment_for(property) if config.comments && property.comment != false
41
+ property.enum ||= model_plugin.enum_for(property) if property.enum != false
42
+ end
43
+ end
44
+
45
+ # Fall back to model plugin for type inference
46
+ model_plugin.infer_types(prop)
47
+ end
48
+ end
49
+
50
+ def model_class
51
+ return serializer._typelizer_model_name if serializer.respond_to?(:_typelizer_model_name)
52
+
53
+ config.instance_exec(serializer, &config.serializer_model_mapper)
54
+ rescue NameError
55
+ nil
56
+ end
57
+
58
+ def model_plugin
59
+ @model_plugin ||= config.model_plugin.new(model_class: model_class, config: config)
60
+ end
61
+ end
62
+ end
63
+ 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.5"
5
5
  end
data/lib/typelizer.rb CHANGED
@@ -11,10 +11,12 @@ require_relative "typelizer/serializer_config_layer"
11
11
 
12
12
  require_relative "typelizer/contexts/writer_context"
13
13
  require_relative "typelizer/contexts/scan_context"
14
+ require_relative "typelizer/property_sorter"
14
15
  require_relative "typelizer/interface"
15
16
  require_relative "typelizer/renderer"
16
17
  require_relative "typelizer/writer"
17
18
  require_relative "typelizer/generator"
19
+ require_relative "typelizer/type_parser"
18
20
  require_relative "typelizer/dsl"
19
21
 
20
22
  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.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Svyatoslav Kryukov
@@ -60,10 +60,13 @@ files:
60
60
  - lib/typelizer/model_plugins/auto.rb
61
61
  - lib/typelizer/model_plugins/poro.rb
62
62
  - lib/typelizer/property.rb
63
+ - lib/typelizer/property_sorter.rb
63
64
  - lib/typelizer/railtie.rb
64
65
  - lib/typelizer/renderer.rb
65
66
  - lib/typelizer/serializer_config_layer.rb
66
67
  - lib/typelizer/serializer_plugins/alba.rb
68
+ - lib/typelizer/serializer_plugins/alba/trait_attribute_collector.rb
69
+ - lib/typelizer/serializer_plugins/alba/trait_interface.rb
67
70
  - lib/typelizer/serializer_plugins/ams.rb
68
71
  - lib/typelizer/serializer_plugins/auto.rb
69
72
  - lib/typelizer/serializer_plugins/base.rb
@@ -75,6 +78,7 @@ files:
75
78
  - lib/typelizer/templates/inheritance.ts.erb
76
79
  - lib/typelizer/templates/inline_type.ts.erb
77
80
  - lib/typelizer/templates/interface.ts.erb
81
+ - lib/typelizer/type_parser.rb
78
82
  - lib/typelizer/version.rb
79
83
  - lib/typelizer/writer.rb
80
84
  homepage: https://github.com/skryukov/typelizer