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 +4 -4
- data/CHANGELOG.md +13 -0
- data/lib/typespec_from_serializers/generator.rb +76 -34
- data/lib/typespec_from_serializers/version.rb +1 -1
- metadata +3 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 80df92c4cd5814bc352130e1d4eb56d648c7efbb46158a99bf98a100f55cc186
|
|
4
|
+
data.tar.gz: f1d3027b282117a49be85a929cf85183ce22a08e39a6a8390d7f3be1363c85cb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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:
|
|
658
|
-
|
|
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
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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
|
-
|
|
681
|
-
write_if_changed(filename: "routes", cache_key:
|
|
682
|
-
|
|
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:
|
|
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" → "
|
|
916
|
-
# "/tasks/{task_id}/comments" → "
|
|
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
|
-
|
|
1143
|
-
|
|
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
|
-
|
|
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:
|
|
1170
|
-
def
|
|
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
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
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",
|
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.
|
|
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:
|
|
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.
|
|
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: []
|