typespec_from_serializers 0.5.3 → 0.6.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: 1bc90948b5d93848663bab0d1162a1f26d49d832a05ef4c57eb5cb1e90f74ff3
4
- data.tar.gz: '008ddb8319166e72e2e75a30d840ad387a082df6e4d0846698e87f9b15ac1c05'
3
+ metadata.gz: 80df92c4cd5814bc352130e1d4eb56d648c7efbb46158a99bf98a100f55cc186
4
+ data.tar.gz: f1d3027b282117a49be85a929cf85183ce22a08e39a6a8390d7f3be1363c85cb
5
5
  SHA512:
6
- metadata.gz: 950c7c34c0d3766854fd6eb4b3aa958b7ed8a20040ad8a43c5573d52741b156441b43e0fbded753536624d368ce353959f5f9e13d311eddea05ba75b8a4e85df
7
- data.tar.gz: 6695700e9a0f73b03e97107d1844e2ef878dcf50217b45826418488827643923260f9b2465bd20fc4d03ef298ea2028f87132fa496abbb67f43ad635fda47562
6
+ metadata.gz: 99f22f4f43af8b642d4e69e81b089f472e252b9075df4975a34e392226fea6c5b9e01b39bd8cb555a3f03c1bac0cb8b113a04ea2daa68e0ea87a4b40c6f0a5d7
7
+ data.tar.gz: 38fc2e2f89bc1e8933de619f8573806bb562e907fc71a46c66c58d4411f25299c6f96d6507440542c8b1ebe841bfe3d2538c1eaa2a0ceaea9c556a1edeb771ef
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # TypeSpec From Serializers Changelog
2
2
 
3
+ ## [0.6.0] - 2026-03-03
4
+
5
+ ### Added
6
+ - Automatic route namespace/interface disambiguation: names that collide with model names are suffixed (e.g., `namespace Task` → `namespace TaskRoutes` when `TaskSerializer` exists)
7
+ - New `route_namespace_suffix` config option (default: `"Routes"`) to control the disambiguation suffix
8
+ - Content-based cache keys: files are only rewritten when generated output actually changes, eliminating unnecessary diffs from internal representation changes
9
+
10
+ ## [0.5.4] - 2025-12-23
11
+
12
+ ### Fixed
13
+ - Transitive params extraction now follows method calls through intermediate methods
14
+ - Route-level `type:` declarations now only apply to actual path params (not inherited by collection routes)
15
+
3
16
  ## [0.5.3] - 2025-12-23
4
17
 
5
18
  ### Fixed
@@ -213,6 +213,7 @@ module TypeSpecFromSerializers
213
213
  :custom_typespec_dir,
214
214
  :name_from_serializer,
215
215
  :controller_suffix,
216
+ :route_namespace_suffix,
216
217
  :param_method_suffix,
217
218
  :global_types,
218
219
  :sort_properties_by,
@@ -653,9 +654,10 @@ module TypeSpecFromSerializers
653
654
  # Internal: Defines a TypeSpec model for the serializer.
654
655
  def generate_model_for(serializer)
655
656
  model = serializer.tsp_model
657
+ content = serializer_model_content(model)
656
658
 
657
- write_if_changed(filename: "models/#{model.filename}", cache_key: model.inspect, extension: "tsp") {
658
- serializer_model_content(model)
659
+ write_if_changed(filename: "models/#{model.filename}", cache_key: content, extension: "tsp") {
660
+ content
659
661
  }
660
662
  rescue => e
661
663
  $stderr.puts "ERROR in generate_model_for(#{serializer.name}): #{e.class}: #{e.message}"
@@ -665,10 +667,10 @@ module TypeSpecFromSerializers
665
667
 
666
668
  # Internal: Allows to import all serializer types from a single file.
667
669
  def generate_index_file
668
- cache_key = all_serializer_files.map { |file| file.delete_prefix(root.to_s) }.join
669
- write_if_changed(filename: "index", cache_key: cache_key) {
670
- load_serializers(all_serializer_files)
671
- serializers_index_content(loaded_serializers)
670
+ load_serializers(all_serializer_files)
671
+ content = serializers_index_content(loaded_serializers)
672
+ write_if_changed(filename: "index", cache_key: content) {
673
+ content
672
674
  }
673
675
  end
674
676
 
@@ -677,9 +679,9 @@ module TypeSpecFromSerializers
677
679
  return [] unless defined?(Rails) && Rails.application
678
680
 
679
681
  routes, controllers = collect_rails_routes
680
- cache_key = routes.map(&:inspect).join
681
- write_if_changed(filename: "routes", cache_key: cache_key) {
682
- routes_content(routes)
682
+ content = routes_content(routes)
683
+ write_if_changed(filename: "routes", cache_key: content) {
684
+ content
683
685
  }
684
686
 
685
687
  # Return list of controller class names
@@ -811,8 +813,10 @@ module TypeSpecFromSerializers
811
813
  operations = ns_routes.map { |route| build_operation(controller, route) }
812
814
  operations = make_operation_names_unique(operations)
813
815
 
816
+ interface_name = disambiguate_route_name(controller.tr("/", "_").camelize)
817
+
814
818
  Resource.new(
815
- name: controller.tr("/", "_").camelize,
819
+ name: interface_name,
816
820
  path: base_path.start_with?("/") ? base_path : "/#{base_path}",
817
821
  operations: operations,
818
822
  parent_namespace: parent_namespace,
@@ -847,7 +851,10 @@ module TypeSpecFromSerializers
847
851
 
848
852
  # Internal: Extracts all parameter types from route metadata and controller DSL
849
853
  def extract_all_param_types(controller, route)
854
+ # Route-level types are for path params only - filter to actual path params
855
+ path_param_names = route[:path].scan(/:([a-zA-Z_][a-zA-Z0-9_]*)/).flatten
850
856
  route_param_types = route[:param_types]
857
+ .select { |k, _| path_param_names.include?(k.to_s) }
851
858
  .transform_keys(&:to_s)
852
859
  .transform_values { |v| map_type_class_to_typespec(v) }
853
860
 
@@ -912,11 +919,21 @@ module TypeSpecFromSerializers
912
919
  end
913
920
 
914
921
  # Internal: Extracts parent namespace from nested route paths
915
- # E.g., "/lands/{land_id}/comments" → "Land"
916
- # "/tasks/{task_id}/comments" → "Task"
922
+ # E.g., "/lands/{land_id}/comments" → "LandRoutes" (suffixed to avoid model collision)
923
+ # "/tasks/{task_id}/comments" → "TaskRoutes"
917
924
  def extract_parent_namespace(path)
918
925
  # Match pattern like /resource/:resource_id/nested (Rails uses : notation)
919
- path[%r{^/([^/]+)/:[^/]+_id/}, 1]&.singularize&.camelize
926
+ name = path[%r{^/([^/]+)/:[^/]+_id/}, 1]&.singularize&.camelize
927
+ return unless name
928
+
929
+ disambiguate_route_name(name)
930
+ end
931
+
932
+ # Internal: Suffixes a route namespace/interface name if it collides with a model name.
933
+ # E.g., "Task" → "TaskRoutes" when TaskSerializer exists.
934
+ def disambiguate_route_name(name)
935
+ model_names = loaded_serializers.map { |s| s.tsp_name }
936
+ model_names.include?(name) ? "#{name}#{config.route_namespace_suffix}" : name
920
937
  end
921
938
 
922
939
  # Internal: Simplifies operation name using REST conventions
@@ -1133,19 +1150,40 @@ module TypeSpecFromSerializers
1133
1150
  {}
1134
1151
  end
1135
1152
 
1136
- # Internal: Finds *_params methods that are called by a specific action method
1153
+ # Internal: Finds *_params methods that are called by a specific action method (transitively)
1137
1154
  def find_params_methods_called_by_action(controller_class, action)
1138
1155
  method_node = find_action_node(controller_class, action)
1139
1156
  return [] unless method_node
1140
1157
 
1158
+ method = controller_class.instance_method(action)
1159
+ file_path, = method.source_location
1141
1160
  suffix = config.param_method_suffix
1142
- find_method_calls(method_node)
1143
- .select { |name| name.end_with?(suffix) }
1144
- .map(&:to_sym)
1161
+
1162
+ find_params_methods_transitively(file_path, method_node, suffix)
1145
1163
  rescue
1146
1164
  []
1147
1165
  end
1148
1166
 
1167
+ # Internal: Recursively finds *_params methods through intermediate method calls
1168
+ def find_params_methods_transitively(file_path, node, suffix, visited = Set.new)
1169
+ calls = find_method_calls(node)
1170
+ params_methods = calls.select { |name| name.end_with?(suffix) }.map(&:to_sym)
1171
+
1172
+ # Follow non-params method calls defined in the same file
1173
+ calls.each do |call_name|
1174
+ next if call_name.end_with?(suffix)
1175
+ next if visited.include?(call_name)
1176
+
1177
+ visited << call_name
1178
+ called_node = find_def_node(file_path) { |n| n.name.to_s == call_name }
1179
+ next unless called_node
1180
+
1181
+ params_methods.concat(find_params_methods_transitively(file_path, called_node, suffix, visited))
1182
+ end
1183
+
1184
+ params_methods.uniq
1185
+ end
1186
+
1149
1187
  # Internal: Finds the AST node for a controller action method.
1150
1188
  def find_action_node(controller_class, action)
1151
1189
  return unless controller_class.method_defined?(action)
@@ -1154,7 +1192,7 @@ module TypeSpecFromSerializers
1154
1192
  file_path, line_number = method.source_location
1155
1193
  return unless file_path && File.exist?(file_path)
1156
1194
 
1157
- find_method_at_line(file_path, line_number)
1195
+ find_def_node(file_path) { |node| node.location.start_line == line_number }
1158
1196
  end
1159
1197
 
1160
1198
  # Internal: Parses a Ruby file and caches the result.
@@ -1166,28 +1204,29 @@ module TypeSpecFromSerializers
1166
1204
  end
1167
1205
  end
1168
1206
 
1169
- # Internal: Finds a DefNode at the exact line number.
1170
- def find_method_at_line(file_path, line_number)
1207
+ # Internal: Traverses all descendant nodes in BFS order.
1208
+ def each_descendant(node)
1209
+ return enum_for(:each_descendant, node) unless block_given?
1210
+ queue = [node]
1211
+ while (current = queue.shift)
1212
+ yield current
1213
+ queue.concat(current.compact_child_nodes)
1214
+ end
1215
+ end
1216
+
1217
+ # Internal: Finds a DefNode matching the given condition.
1218
+ def find_def_node(file_path)
1171
1219
  ast = parse_file_cached(file_path)
1172
1220
  return unless ast
1173
-
1174
- queue = [ast]
1175
- while (node = queue.shift)
1176
- return node if node.is_a?(Prism::DefNode) && node.location.start_line == line_number
1177
- queue.concat(node.compact_child_nodes)
1178
- end
1179
- nil
1221
+ each_descendant(ast).find { |node| node.is_a?(Prism::DefNode) && yield(node) }
1180
1222
  end
1181
1223
 
1182
1224
  # Internal: Finds all unqualified method call names in an AST node.
1183
1225
  def find_method_calls(node)
1184
- calls = []
1185
- queue = [node]
1186
- while (current = queue.shift)
1187
- calls << current.name.to_s if current.is_a?(Prism::CallNode) && current.receiver.nil?
1188
- queue.concat(current.compact_child_nodes)
1189
- end
1190
- calls.uniq
1226
+ each_descendant(node)
1227
+ .select { |n| n.is_a?(Prism::CallNode) && n.receiver.nil? }
1228
+ .map { |n| n.name.to_s }
1229
+ .uniq
1191
1230
  end
1192
1231
 
1193
1232
  # Internal: Extracts key-value types from Sorbet hash type
@@ -1343,6 +1382,9 @@ module TypeSpecFromSerializers
1343
1382
  # Controller suffix for route generation reporting
1344
1383
  controller_suffix: "Controller",
1345
1384
 
1385
+ # Suffix for route namespaces/interfaces that collide with model names
1386
+ route_namespace_suffix: "Routes",
1387
+
1346
1388
  # Types that don't need to be imported in TypeSpec.
1347
1389
  global_types: [
1348
1390
  "Array",
@@ -2,5 +2,5 @@
2
2
 
3
3
  module TypeSpecFromSerializers
4
4
  # Public: This library adheres to semantic versioning.
5
- VERSION = "0.5.3"
5
+ VERSION = "0.6.0"
6
6
  end
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: typespec_from_serializers
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.3
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Danila Poyarkov
8
8
  - Máximo Mussini
9
- autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2025-12-23 00:00:00.000000000 Z
11
+ date: 2026-03-03 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: railties
@@ -328,7 +327,6 @@ homepage: https://github.com/dannote/typespec_from_serializers
328
327
  licenses:
329
328
  - MIT
330
329
  metadata: {}
331
- post_install_message:
332
330
  rdoc_options: []
333
331
  require_paths:
334
332
  - lib
@@ -343,8 +341,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
343
341
  - !ruby/object:Gem::Version
344
342
  version: '0'
345
343
  requirements: []
346
- rubygems_version: 3.5.16
347
- signing_key:
344
+ rubygems_version: 3.6.2
348
345
  specification_version: 4
349
346
  summary: Generate TypeSpec descriptions from your JSON serializers.
350
347
  test_files: []