typelizer 0.10.0 → 0.11.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc23d62a95119b1ff871bcde1eeb0c3180cc0fe1e38fa1252edad30a454661bc
4
- data.tar.gz: 2658b01eebb6f9317ff4905f6c482cbdfa55afcadd4ba6508e1cd9b5469072da
3
+ metadata.gz: 6dbce7bf8d558500341875958f875183e18668765f812d1ef8110f51c521cc7d
4
+ data.tar.gz: c297302a9425f27f0491307d7597201430ddf94eff38d80cf0b0760f16186296
5
5
  SHA512:
6
- metadata.gz: 237f41ebfc6999a73c30e97a89dab3435638c49bac38ff8072493b62833fd7a6c7d5cd7655868bf07c958b879c906c4c38bd809331cad1827b37529183c1aae3
7
- data.tar.gz: b882bab4aea8a599b69e39c296a8794cf1fa357ec20f092649d5fa7e7d80bf2fb05ffff69b4362f7e0aa0be9b669724933a4167bfe403f9ce094e37254dfc9e8
6
+ metadata.gz: 5509e82f421cc1f52411b073431a246134d5bdcfe53eb5f308554492da3f2dbc78c708cf45c89651a749d5ad2aa1e0a59c119287f0c0ce8af19a10e374d4efbb
7
+ data.tar.gz: 37ba94237e3992f96542f33ef4376db48a1c5397907d01fbd7a619102a412db58418e36822d46f522f2ebeb672922591a46cbb112e0be2103bf7119d241e2bbe
data/CHANGELOG.md CHANGED
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning].
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.11.0] - 2026-03-26
11
+
12
+ ### Added
13
+
14
+ - Per-serializer `output_dir` override via `typelizer_config`. Interfaces are written to their configured directory while the shared `index.ts` barrel generates correct relative import paths. ([@skryukov])
15
+
16
+ - `filename_mapper` configuration to decouple generated file paths from TypeScript type names. Useful for mirroring Ruby module namespaces as nested directories. ([@skryukov], [@rdavid1099])
17
+
18
+ ```ruby
19
+ Typelizer.configure do |config|
20
+ config.filename_mapper = ->(name) { name.gsub("::", "/") }
21
+ end
22
+ # Alba::UserSerializer → types/Alba/User.ts (type name stays AlbaUser)
23
+ ```
24
+
25
+ ### Fixed
26
+
27
+ - Walk over inline association properties to determine imports. ([@skryukov])
28
+ - Fix `config.type_mapping` override not applied to OpenAPI schema generation. ([@skryukov])
29
+ - Fix key transformation for Alba traits. ([@skryukov])
30
+
10
31
  ## [0.10.0] - 2026-03-02
11
32
 
12
33
  ### Changed
@@ -426,20 +447,22 @@ and this project adheres to [Semantic Versioning].
426
447
 
427
448
  [@davidrunger]: https://github.com/davidrunger
428
449
  [@Envek]: https://github.com/Envek
429
- [@jonmarkgo]: https://github.com/jonmarkgo
430
450
  [@hkamberovic]: https://github.com/hkamberovic
451
+ [@jonmarkgo]: https://github.com/jonmarkgo
431
452
  [@kristinemcbride]: https://github.com/kristinemcbride
432
453
  [@nkriege]: https://github.com/nkriege
433
454
  [@NOX73]: https://github.com/NOX73
434
455
  [@okuramasafumi]: https://github.com/okuramasafumi
435
456
  [@patvice]: https://github.com/patvice
436
457
  [@pgiblock]: https://github.com/pgiblock
437
- [@prog-supdex]: https://github.com/prog-supdex
438
458
  [@PedroAugustoRamalhoDuarte]: https://github.com/PedroAugustoRamalhoDuarte
459
+ [@prog-supdex]: https://github.com/prog-supdex
460
+ [@rdavid1099]: https://github.com/rdavid1099
439
461
  [@skryukov]: https://github.com/skryukov
440
462
  [@ventsislaf]: https://github.com/ventsislaf
441
463
 
442
- [Unreleased]: https://github.com/skryukov/typelizer/compare/v0.10.0...HEAD
464
+ [Unreleased]: https://github.com/skryukov/typelizer/compare/v0.11.0...HEAD
465
+ [0.11.0]: https://github.com/skryukov/typelizer/compare/v0.10.0...v0.11.0
443
466
  [0.10.0]: https://github.com/skryukov/typelizer/compare/v0.9.3...v0.10.0
444
467
  [0.9.3]: https://github.com/skryukov/typelizer/compare/v0.9.2...v0.9.3
445
468
  [0.9.2]: https://github.com/skryukov/typelizer/compare/v0.9.1...v0.9.2
data/README.md CHANGED
@@ -628,6 +628,16 @@ class PostResource < ApplicationResource
628
628
  end
629
629
  ```
630
630
 
631
+ You can also override `output_dir` per serializer to place its generated file in a different directory:
632
+
633
+ ```ruby
634
+ class Admin::UserResource < ApplicationResource
635
+ typelizer_config do |c|
636
+ c.output_dir = Rails.root.join("app/javascript/types/admin")
637
+ end
638
+ end
639
+ ```
640
+
631
641
  ### Option reference
632
642
 
633
643
  ```ruby
@@ -635,6 +645,13 @@ Typelizer.configure do |config|
635
645
  # Name to type mapping for serializer classes
636
646
  config.serializer_name_mapper = ->(serializer) { ... }
637
647
 
648
+ # Custom file path mapping (decouples filename from type name)
649
+ # Receives the mapped name (output of serializer_name_mapper) and returns a file path.
650
+ # When nil (default), filename is derived from the type name.
651
+ # Example: ->(name) { name.gsub("::", "/") }
652
+ # Alba::UserSerializer → types/Alba/User.ts (type name stays AlbaUser)
653
+ config.filename_mapper = nil
654
+
638
655
  # Maps serializers to their corresponding model classes
639
656
  config.serializer_model_mapper = ->(serializer) { ... }
640
657
 
@@ -41,6 +41,7 @@ module Typelizer
41
41
  # Config keys that don't affect file content (runtime behavior, or effects captured via properties).
42
42
  CONFIGS_NOT_AFFECTING_OUTPUT = %i[
43
43
  serializer_name_mapper
44
+ filename_mapper
44
45
  serializer_model_mapper
45
46
  properties_transformer
46
47
  model_plugin
@@ -56,6 +57,7 @@ module Typelizer
56
57
 
57
58
  Config = Struct.new(
58
59
  :serializer_name_mapper,
60
+ :filename_mapper,
59
61
  :serializer_model_mapper,
60
62
  :properties_transformer,
61
63
  :properties_sort_order,
@@ -95,6 +97,8 @@ module Typelizer
95
97
  name.sub(/(Serializer|Resource)\z/, "")
96
98
  end,
97
99
 
100
+ filename_mapper: nil,
101
+
98
102
  serializer_model_mapper: lambda do |serializer|
99
103
  base_class = serializer_name_mapper.call(serializer)
100
104
  Object.const_get(base_class) if Object.const_defined?(base_class)
@@ -36,7 +36,20 @@ module Typelizer
36
36
  end
37
37
 
38
38
  def filename
39
- name.gsub("::", "/")
39
+ if config.filename_mapper
40
+ config.filename_mapper.call(config.serializer_name_mapper.call(serializer))
41
+ else
42
+ name.gsub("::", "/")
43
+ end
44
+ end
45
+
46
+ def index_path(index_dir)
47
+ iface_dir = config.output_dir.to_s
48
+ if iface_dir != index_dir
49
+ Pathname.new(File.join(iface_dir, filename)).relative_path_from(Pathname.new(index_dir)).to_s
50
+ else
51
+ "./#{filename}"
52
+ end
40
53
  end
41
54
 
42
55
  def root_key
@@ -166,6 +179,8 @@ module Typelizer
166
179
  props.flat_map do |prop|
167
180
  if prop.nested_properties&.any?
168
181
  [prop] + collect_all_properties(prop.nested_properties)
182
+ elsif prop.type.is_a?(Interface) && prop.type.inline?
183
+ [prop] + collect_all_properties(prop.type.properties)
169
184
  else
170
185
  [prop]
171
186
  end
@@ -31,21 +31,22 @@ module Typelizer
31
31
  def schema_for(interface, openapi_version: "3.0")
32
32
  validate_version!(openapi_version)
33
33
 
34
+ type_mapping = interface.respond_to?(:config) ? interface.config.type_mapping : Typelizer.configuration.type_mapping
34
35
  required_props = interface.properties.reject(&:optional).map(&:name)
35
36
  schema = {
36
37
  type: :object,
37
- properties: interface.properties.to_h { |prop| [prop.name, property_schema(prop, openapi_version: openapi_version)] }
38
+ properties: interface.properties.to_h { |prop| [prop.name, property_schema(prop, openapi_version: openapi_version, type_mapping: type_mapping)] }
38
39
  }
39
40
  schema[:required] = required_props if required_props.any?
40
41
  schema
41
42
  end
42
43
 
43
- def property_schema(property, openapi_version: "3.0")
44
+ def property_schema(property, openapi_version: "3.0", type_mapping: Typelizer.configuration.type_mapping)
44
45
  if property.type.is_a?(Array)
45
46
  return union_schema(property, openapi_version: openapi_version)
46
47
  end
47
48
 
48
- definition = base_type(property, openapi_version: openapi_version)
49
+ definition = base_type(property, openapi_version: openapi_version, type_mapping: type_mapping)
49
50
  ref = definition.delete("$ref")
50
51
 
51
52
  definition = if ref
@@ -171,7 +172,7 @@ module Typelizer
171
172
  definition
172
173
  end
173
174
 
174
- def base_type(property, openapi_version:)
175
+ def base_type(property, openapi_version:, type_mapping:)
175
176
  if property.type.respond_to?(:properties)
176
177
  if property.type.respond_to?(:inline?) && property.type.inline?
177
178
  schema_for(property.type, openapi_version: openapi_version)
@@ -179,8 +180,9 @@ module Typelizer
179
180
  {"$ref" => "#/components/schemas/#{property.type.name}"}
180
181
  end
181
182
  elsif property.type.nil? && property.respond_to?(:nested_properties) && property.nested_properties&.any?
182
- nested_schema(property, openapi_version: openapi_version)
183
- elsif property.column_type && COLUMN_TYPE_MAP.key?(property.column_type)
183
+ nested_schema(property, openapi_version: openapi_version, type_mapping: type_mapping)
184
+ elsif property.column_type && COLUMN_TYPE_MAP.key?(property.column_type) &&
185
+ !type_mapping_overridden?(property, type_mapping)
184
186
  result = COLUMN_TYPE_MAP[property.column_type].dup
185
187
  result[:type] = :string if property.enum
186
188
  result
@@ -194,16 +196,20 @@ module Typelizer
194
196
  end
195
197
  end
196
198
 
197
- def nested_schema(property, openapi_version:)
199
+ def nested_schema(property, openapi_version:, type_mapping:)
198
200
  required = property.nested_properties.reject(&:optional).map(&:name)
199
201
  schema = {
200
202
  type: :object,
201
- properties: property.nested_properties.to_h { |p| [p.name, property_schema(p, openapi_version: openapi_version)] }
203
+ properties: property.nested_properties.to_h { |p| [p.name, property_schema(p, openapi_version: openapi_version, type_mapping: type_mapping)] }
202
204
  }
203
205
  schema[:required] = required if required.any?
204
206
  schema
205
207
  end
206
208
 
209
+ def type_mapping_overridden?(property, type_mapping)
210
+ type_mapping[property.column_type] != TYPE_MAPPING[property.column_type]
211
+ end
212
+
207
213
  def v31?(openapi_version)
208
214
  openapi_version.to_s == "3.1"
209
215
  end
@@ -61,11 +61,12 @@ module Typelizer
61
61
  def build_collected_property(name, attr)
62
62
  case attr
63
63
  when BlockAttributeCollector::BlockAssociation
64
+ prop_name = has_transform_key?(serializer) ? fetch_key(serializer, name) : name
64
65
  with_traits = Array(attr.with_traits) if attr.with_traits
65
66
  resource = attr.resource || infer_resource_from_name(name)
66
67
 
67
68
  Property.new(
68
- name: name,
69
+ name: prop_name,
69
70
  type: resource ? context.interface_for(resource) : nil,
70
71
  optional: false,
71
72
  nullable: false,
@@ -5,8 +5,8 @@ export type { <%= ImportSorter.sort(enums.map(&:enum_type_name), imports_sort_or
5
5
  <%- interfaces.each do |interface| -%>
6
6
  <%- sorted_traits = ImportSorter.sort(interface.trait_interfaces.map(&:name), interface.config.imports_sort_order) -%>
7
7
  <%- if interface.config.verbatim_module_syntax -%>
8
- export type { <%= interface.name %><%= ", " + sorted_traits.join(", ") if sorted_traits.any? %> } from <%= interface.quote('./' + interface.filename) %>
8
+ export type { <%= interface.name %><%= ", " + sorted_traits.join(", ") if sorted_traits.any? %> } from <%= interface.quote(interface.index_path(index_dir)) %>
9
9
  <%- else -%>
10
- export type { default as <%= interface.name %><%= ", " + sorted_traits.join(", ") if sorted_traits.any? %> } from <%= interface.quote('./' + interface.filename) %>
10
+ export type { default as <%= interface.name %><%= ", " + sorted_traits.join(", ") if sorted_traits.any? %> } from <%= interface.quote(interface.index_path(index_dir)) %>
11
11
  <%- end -%>
12
12
  <%- end -%>
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Typelizer
4
- VERSION = "0.10.0"
4
+ VERSION = "0.11.0"
5
5
  end
@@ -12,7 +12,7 @@ module Typelizer
12
12
  end
13
13
 
14
14
  def call(interfaces, force:)
15
- cleanup_output_dir if force
15
+ cleanup_output_dir(interfaces) if force
16
16
 
17
17
  valid_interfaces = interfaces.reject(&:empty?)
18
18
  return [] if valid_interfaces.empty?
@@ -27,7 +27,7 @@ module Typelizer
27
27
 
28
28
  written_files << write_index(valid_interfaces, enums: enums)
29
29
 
30
- cleanup_stale_files(written_files) unless force
30
+ cleanup_stale_files(written_files, valid_interfaces) unless force
31
31
 
32
32
  Typelizer.logger.debug("Generated #{written_files.size} TypeScript files in #{config.output_dir}")
33
33
 
@@ -43,10 +43,10 @@ module Typelizer
43
43
 
44
44
  attr_reader :config, :template_cache
45
45
 
46
- def cleanup_stale_files(written_files)
47
- return unless File.directory?(config.output_dir)
46
+ def cleanup_stale_files(written_files, interfaces)
47
+ output_dirs = output_dirs_for(interfaces)
48
48
 
49
- existing_files = Dir[File.join(config.output_dir, "**/*.ts")]
49
+ existing_files = output_dirs.flat_map { |dir| Dir[File.join(dir, "**/*.ts")] }
50
50
  stale_files = existing_files - written_files
51
51
 
52
52
  File.delete(*stale_files) unless stale_files.empty?
@@ -70,22 +70,23 @@ module Typelizer
70
70
  fingerprint = [
71
71
  enums.map(&:enum_type_name),
72
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) }]
73
+ [i.name, i.filename, i.index_path(config.output_dir.to_s), i.trait_interfaces.map(&:name), CONFIGS_AFFECTING_INDEX_OUTPUT.map { |key| i.config.public_send(key) }]
74
74
  }
75
75
  ].inspect
76
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)
77
+ render_template("index.ts.erb", interfaces: interfaces, enums: enums, index_dir: config.output_dir.to_s, imports_sort_order: config.imports_sort_order, prefer_double_quotes: config.prefer_double_quotes)
78
78
  end
79
79
  end
80
80
 
81
81
  def write_interface(interface)
82
- write_file("#{interface.filename}.ts", interface.fingerprint) do
82
+ output_dir = interface.config.output_dir
83
+ write_file("#{interface.filename}.ts", interface.fingerprint, output_dir: output_dir) do
83
84
  render_template("interface.ts.erb", interface: interface)
84
85
  end
85
86
  end
86
87
 
87
- def write_file(filename, fingerprint)
88
- output_file = File.join(config.output_dir, filename)
88
+ def write_file(filename, fingerprint, output_dir: config.output_dir)
89
+ output_file = File.join(output_dir, filename)
89
90
  existing_content = File.exist?(output_file) ? File.read(output_file) : nil
90
91
  digest = render_template("fingerprint.ts.erb", fingerprint: fingerprint)
91
92
 
@@ -104,8 +105,14 @@ module Typelizer
104
105
  template_cache[template].call(**context)
105
106
  end
106
107
 
107
- def cleanup_output_dir
108
- FileUtils.rm_rf(config.output_dir)
108
+ def cleanup_output_dir(interfaces)
109
+ output_dirs_for(interfaces).each { |dir| FileUtils.rm_rf(dir) }
110
+ end
111
+
112
+ def output_dirs_for(interfaces)
113
+ dirs = interfaces.filter_map { |i| i.config.output_dir.to_s unless i.empty? }
114
+ dirs << config.output_dir.to_s
115
+ dirs.uniq
109
116
  end
110
117
 
111
118
  def cleanup_partial_writes(partial_files)
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.10.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Svyatoslav Kryukov