typespec_from_serializers 0.3.1 → 0.5.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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 58c9803235729fabc83ae742e83822906d3ed88746e2c40f7d04d7368409a5cb
|
|
4
|
+
data.tar.gz: de245ba8d20d6b695af332f886c8b0b83a491cd563364a2c06f000c90726739e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: efc632aa512a2fec97c35bde59de42c1516ec23d4231be04cbc3238ca5a1bc7756c495cb9b7efcbb5a0209aade99bd6096b04229b0b6bbc4b1afae58a5b4b62b
|
|
7
|
+
data.tar.gz: 2a2fb5ca8cf367d6a22649538af11e2ee690256522caca1b63de0639d8e5b64fb8d6d5af1453ac7604a2632c439b3132652a4b906d16b0cac76c0ac4c87150f3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# TypeSpec From Serializers Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0] - 2025-12-22
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Route parameter typing with `type:` now works correctly without interfering with Rails routing
|
|
7
|
+
|
|
8
|
+
## [0.4.0] - 2025-12-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Linting system with 5 configurable rules to catch common issues during generation
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- Add `implicitOptionality: true` to PATCH decorators to suppress TypeSpec 1.0+ warnings
|
|
15
|
+
|
|
3
16
|
## [0.3.1] - 2025-12-22
|
|
4
17
|
|
|
5
18
|
### Fixed
|
|
@@ -26,6 +26,92 @@ module TypeSpecFromSerializers
|
|
|
26
26
|
MEMBER_ACTIONS = %w[show update destroy].freeze
|
|
27
27
|
SPECIAL_ACTIONS = %w[new edit].freeze
|
|
28
28
|
|
|
29
|
+
# Internal: Linting methods for TypeSpec generation
|
|
30
|
+
module Linting
|
|
31
|
+
extend self
|
|
32
|
+
|
|
33
|
+
@warning_count = 0
|
|
34
|
+
|
|
35
|
+
def reset_count
|
|
36
|
+
@warning_count = 0
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def warning_count
|
|
40
|
+
@warning_count
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def print_summary
|
|
44
|
+
return if @warning_count == 0
|
|
45
|
+
puts "\nTypeSpec generation completed with #{@warning_count} lint warning#{'s' unless @warning_count == 1}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Internal: Lints for missing parameter types
|
|
49
|
+
def missing_param_types(controller, route, path_param_names, param_types, config)
|
|
50
|
+
return unless enabled?(config.linting, :missing_param_types)
|
|
51
|
+
|
|
52
|
+
missing_path_params = path_param_names.select { |name| !param_types.key?(name) }
|
|
53
|
+
|
|
54
|
+
unless missing_path_params.empty?
|
|
55
|
+
@warning_count += 1
|
|
56
|
+
location = "#{controller.camelize}#{config.controller_suffix}##{route[:action]}"
|
|
57
|
+
warn "TypeSpec Lint: Missing type for path parameter(s) #{missing_path_params.join(', ')} in #{location} (#{route[:method]} #{route[:path]}). Defaulting to 'string'."
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Internal: Lints for unknown response types
|
|
62
|
+
def unknown_response_type(controller, route, response_type, config)
|
|
63
|
+
return unless enabled?(config.linting, :unknown_response_types)
|
|
64
|
+
|
|
65
|
+
if response_type == "unknown"
|
|
66
|
+
@warning_count += 1
|
|
67
|
+
location = "#{controller.camelize}#{config.controller_suffix}##{route[:action]}"
|
|
68
|
+
warn "TypeSpec Lint: Unknown response type for #{location} (#{route[:method]} #{route[:path]}). Consider adding a serializer or explicit type annotation."
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Internal: Lints for missing documentation
|
|
73
|
+
def missing_documentation(controller, route, doc, config)
|
|
74
|
+
return unless enabled?(config.linting, :missing_documentation) && config.extract_docs
|
|
75
|
+
|
|
76
|
+
if doc.nil?
|
|
77
|
+
@warning_count += 1
|
|
78
|
+
location = "#{controller.camelize}#{config.controller_suffix}##{route[:action]}"
|
|
79
|
+
warn "TypeSpec Lint: Missing documentation for #{location} (#{route[:method]} #{route[:path]}). Consider adding an RDoc comment."
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Internal: Lints for duplicate action names
|
|
84
|
+
def ambiguous_operations(name_counts, operations, config)
|
|
85
|
+
return unless enabled?(config.linting, :ambiguous_operations)
|
|
86
|
+
|
|
87
|
+
duplicates = name_counts.select { |_, count| count > 1 }
|
|
88
|
+
duplicates.each do |action, count|
|
|
89
|
+
@warning_count += 1
|
|
90
|
+
ops = operations.select { |op| op.action == action }
|
|
91
|
+
methods = ops.map { |op| op.method }.join(', ')
|
|
92
|
+
warn "TypeSpec Lint: Action '#{action}' used for multiple routes (#{methods}). Using method suffix to differentiate."
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Internal: Lints for type inference failures
|
|
97
|
+
def type_inference_failure(property, explicit_type, serializer_name, config)
|
|
98
|
+
return unless enabled?(config.linting, :type_inference_failures)
|
|
99
|
+
|
|
100
|
+
if property.type.nil? && explicit_type.nil?
|
|
101
|
+
@warning_count += 1
|
|
102
|
+
warn "TypeSpec Lint: Could not infer type for '#{property.name}' in #{serializer_name}. Consider adding explicit type annotation."
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
# Internal: Checks if a linting rule is enabled
|
|
109
|
+
def enabled?(linting_config, rule)
|
|
110
|
+
return false if linting_config == false
|
|
111
|
+
linting_config.is_a?(Hash) && linting_config[rule]
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
29
115
|
# Internal: Extensions that simplify the implementation of the generator.
|
|
30
116
|
module SerializerRefinements
|
|
31
117
|
refine String do
|
|
@@ -96,7 +182,11 @@ module TypeSpecFromSerializers
|
|
|
96
182
|
column_name: options.fetch(:value_from),
|
|
97
183
|
doc: TypeSpecFromSerializers.config.extract_docs ? RDoc.method_doc(self, options.fetch(:value_from)) : nil,
|
|
98
184
|
).tap do |property|
|
|
185
|
+
explicit_type = property.type
|
|
99
186
|
property.infer_typespec_from(model_columns, model_enums, typespec_from, self, model_class)
|
|
187
|
+
|
|
188
|
+
# Linting
|
|
189
|
+
TypeSpecFromSerializers::Linting.type_inference_failure(property, explicit_type, self.name, TypeSpecFromSerializers.config)
|
|
100
190
|
end
|
|
101
191
|
end
|
|
102
192
|
}
|
|
@@ -138,6 +228,7 @@ module TypeSpecFromSerializers
|
|
|
138
228
|
:openapi_path,
|
|
139
229
|
:max_line_length,
|
|
140
230
|
:extract_docs,
|
|
231
|
+
:linting,
|
|
141
232
|
:root,
|
|
142
233
|
keyword_init: true,
|
|
143
234
|
) do
|
|
@@ -416,14 +507,21 @@ module TypeSpecFromSerializers
|
|
|
416
507
|
def build_single_line(tsp_method, operation_name)
|
|
417
508
|
params = params_typespec
|
|
418
509
|
params_str = params.empty? ? "()" : "(#{params})"
|
|
419
|
-
|
|
510
|
+
method_decorator = format_method_decorator(tsp_method)
|
|
511
|
+
"#{method_decorator} #{operation_name}#{params_str}: #{response_type.delete(":")};"
|
|
420
512
|
end
|
|
421
513
|
|
|
422
514
|
def multiline_format(route_line, tsp_method, operation_name)
|
|
423
515
|
params_indented = all_params.map { |p| "\n #{p}," }.join
|
|
424
516
|
return_type = response_type.delete(":")
|
|
517
|
+
method_decorator = format_method_decorator(tsp_method)
|
|
518
|
+
|
|
519
|
+
"#{route_line}#{method_decorator} #{operation_name}(#{params_indented}\n ): #{return_type};"
|
|
520
|
+
end
|
|
425
521
|
|
|
426
|
-
|
|
522
|
+
def format_method_decorator(tsp_method)
|
|
523
|
+
# PATCH operations need implicitOptionality flag in TypeSpec 1.0+
|
|
524
|
+
tsp_method == "patch" ? "@patch(\#{implicitOptionality: true})" : "@#{tsp_method}"
|
|
427
525
|
end
|
|
428
526
|
|
|
429
527
|
def operation_route_decorator(resource_path)
|
|
@@ -520,6 +618,8 @@ module TypeSpecFromSerializers
|
|
|
520
618
|
|
|
521
619
|
# Public: Generates code for all serializers in the app.
|
|
522
620
|
def generate(force: ENV["SERIALIZER_TYPESPEC_FORCE"])
|
|
621
|
+
Linting.reset_count
|
|
622
|
+
|
|
523
623
|
@force_generation = force
|
|
524
624
|
clean_output_dir if force && config.output_dir.exist?
|
|
525
625
|
|
|
@@ -536,6 +636,8 @@ module TypeSpecFromSerializers
|
|
|
536
636
|
generate_model_for(serializer)
|
|
537
637
|
end
|
|
538
638
|
|
|
639
|
+
Linting.print_summary
|
|
640
|
+
|
|
539
641
|
{serializers: serializers, controllers: controllers}
|
|
540
642
|
end
|
|
541
643
|
|
|
@@ -685,7 +787,7 @@ module TypeSpecFromSerializers
|
|
|
685
787
|
path: path,
|
|
686
788
|
response_type: response_type,
|
|
687
789
|
route_name: route_name,
|
|
688
|
-
param_types: route.defaults[:
|
|
790
|
+
param_types: route.defaults[:__typespec_types] || {},
|
|
689
791
|
}
|
|
690
792
|
end
|
|
691
793
|
|
|
@@ -723,14 +825,23 @@ module TypeSpecFromSerializers
|
|
|
723
825
|
param_types = extract_all_param_types(controller, route)
|
|
724
826
|
controller_class = "#{controller.camelize}#{config.controller_suffix}".safe_constantize
|
|
725
827
|
|
|
828
|
+
# Linting
|
|
829
|
+
Linting.missing_param_types(controller, route, path_param_names, param_types, config)
|
|
830
|
+
|
|
831
|
+
response_type = infer_operation_response_type(route)
|
|
832
|
+
doc = config.extract_docs && controller_class ? RDoc.method_doc(controller_class, route[:action]) : nil
|
|
833
|
+
|
|
834
|
+
Linting.unknown_response_type(controller, route, response_type, config)
|
|
835
|
+
Linting.missing_documentation(controller, route, doc, config)
|
|
836
|
+
|
|
726
837
|
Operation.new(
|
|
727
838
|
method: route[:method],
|
|
728
839
|
action: simplify_operation_name(route),
|
|
729
840
|
path: route[:path],
|
|
730
841
|
path_params: build_path_params(path_param_names, param_types),
|
|
731
842
|
body_params: build_body_params(route[:method], path_param_names, param_types),
|
|
732
|
-
response_type:
|
|
733
|
-
doc:
|
|
843
|
+
response_type: response_type,
|
|
844
|
+
doc: doc,
|
|
734
845
|
)
|
|
735
846
|
end
|
|
736
847
|
|
|
@@ -786,6 +897,9 @@ module TypeSpecFromSerializers
|
|
|
786
897
|
name_counts = operations.group_by(&:action).transform_values(&:count)
|
|
787
898
|
name_usage = Hash.new(0)
|
|
788
899
|
|
|
900
|
+
# Linting
|
|
901
|
+
Linting.ambiguous_operations(name_counts, operations, config)
|
|
902
|
+
|
|
789
903
|
operations.map do |op|
|
|
790
904
|
next op unless name_counts[op.action] > 1
|
|
791
905
|
|
|
@@ -1295,6 +1409,15 @@ module TypeSpecFromSerializers
|
|
|
1295
1409
|
# Extract documentation from RDoc comments
|
|
1296
1410
|
extract_docs: true,
|
|
1297
1411
|
|
|
1412
|
+
# Linting configuration
|
|
1413
|
+
linting: {
|
|
1414
|
+
missing_param_types: true,
|
|
1415
|
+
unknown_response_types: true,
|
|
1416
|
+
missing_documentation: true,
|
|
1417
|
+
ambiguous_operations: true,
|
|
1418
|
+
type_inference_failures: true,
|
|
1419
|
+
},
|
|
1420
|
+
|
|
1298
1421
|
# Project root directory
|
|
1299
1422
|
root: root,
|
|
1300
1423
|
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_dispatch/routing/mapper"
|
|
4
|
+
require "action_dispatch/journey/route"
|
|
5
|
+
|
|
6
|
+
# Internal: Minimal surgical patches to ActionDispatch routing for TypeSpec metadata support.
|
|
7
|
+
#
|
|
8
|
+
# This module patches Rails routing to support the `type:` parameter for specifying
|
|
9
|
+
# path parameter types without interfering with Rails' route matching or URL generation.
|
|
10
|
+
#
|
|
11
|
+
# The type metadata is stored in route defaults under the :__typespec_types key and
|
|
12
|
+
# explicitly excluded from route requirements to prevent it from affecting routing behavior.
|
|
13
|
+
#
|
|
14
|
+
# Examples
|
|
15
|
+
#
|
|
16
|
+
# # In routes.rb
|
|
17
|
+
# resources :users, type: { id: Integer }
|
|
18
|
+
# get '/posts/:id', to: 'posts#show', type: { id: Integer }
|
|
19
|
+
#
|
|
20
|
+
# The patches work at three interception points:
|
|
21
|
+
# 1. Mapper::Base#match - for GET/POST/PATCH/etc methods
|
|
22
|
+
# 2. Mapper::Resources#resources - for resources/resource methods
|
|
23
|
+
# 3. Journey::Route#requirements - filters metadata from route requirements
|
|
24
|
+
module TypeSpecFromSerializers
|
|
25
|
+
# Internal: Shared logic for moving type metadata from options to defaults.
|
|
26
|
+
module RoutingPatchHelpers
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# Internal: Moves type: parameter from route options to defaults[:__typespec_types].
|
|
30
|
+
#
|
|
31
|
+
# This ensures type metadata is stored but doesn't interfere with routing.
|
|
32
|
+
#
|
|
33
|
+
# options - Hash of route options (will be modified in place)
|
|
34
|
+
#
|
|
35
|
+
# Returns the modified options Hash
|
|
36
|
+
def move_type_to_defaults!(options)
|
|
37
|
+
return options unless options.key?(:type)
|
|
38
|
+
|
|
39
|
+
types = options.delete(:type)
|
|
40
|
+
options[:defaults] = (options[:defaults] || {}).merge(__typespec_types: types)
|
|
41
|
+
options
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Internal: Patches ActionDispatch::Routing::Mapper::Base to intercept match method.
|
|
46
|
+
#
|
|
47
|
+
# Intercepts the low-level match method used by get, post, patch, delete, etc.
|
|
48
|
+
module MapperPatch
|
|
49
|
+
def match(path, *rest, &block)
|
|
50
|
+
options = rest.last.is_a?(Hash) ? rest.pop.dup : {}
|
|
51
|
+
RoutingPatchHelpers.move_type_to_defaults!(options)
|
|
52
|
+
|
|
53
|
+
rest.push(options) unless options.empty?
|
|
54
|
+
super(path, *rest, &block)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Internal: Patches ActionDispatch::Routing::Mapper::Resources to intercept resources method.
|
|
59
|
+
#
|
|
60
|
+
# Intercepts the resources/resource DSL methods to support type: parameter.
|
|
61
|
+
module ResourcesPatch
|
|
62
|
+
def resources(*resources, &block)
|
|
63
|
+
options = resources.extract_options!
|
|
64
|
+
RoutingPatchHelpers.move_type_to_defaults!(options)
|
|
65
|
+
|
|
66
|
+
resources.push(options) unless options.empty?
|
|
67
|
+
super(*resources, &block)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Internal: Patches ActionDispatch::Journey::Route to filter type metadata from requirements.
|
|
72
|
+
#
|
|
73
|
+
# The requirements method is used for route matching and URL generation.
|
|
74
|
+
# We exclude :__typespec_types to ensure it doesn't affect routing behavior.
|
|
75
|
+
module RoutePatch
|
|
76
|
+
def requirements
|
|
77
|
+
super.except(:__typespec_types)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Apply patches to ActionDispatch
|
|
83
|
+
ActionDispatch::Routing::Mapper::Base.prepend(TypeSpecFromSerializers::MapperPatch)
|
|
84
|
+
ActionDispatch::Routing::Mapper::Resources.prepend(TypeSpecFromSerializers::ResourcesPatch)
|
|
85
|
+
ActionDispatch::Journey::Route.prepend(TypeSpecFromSerializers::RoutePatch)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Danila Poyarkov
|
|
@@ -320,6 +320,7 @@ files:
|
|
|
320
320
|
- lib/typespec_from_serializers/railtie.rb
|
|
321
321
|
- lib/typespec_from_serializers/rbi.rb
|
|
322
322
|
- lib/typespec_from_serializers/rdoc.rb
|
|
323
|
+
- lib/typespec_from_serializers/routing_patch.rb
|
|
323
324
|
- lib/typespec_from_serializers/runner.rb
|
|
324
325
|
- lib/typespec_from_serializers/sorbet.rb
|
|
325
326
|
- lib/typespec_from_serializers/version.rb
|