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: 59daa7ce75b192fd0318e7360f7fa9f8f6a91a1b4c2af9de4c7e8f6ccb281cec
4
- data.tar.gz: e4fc7a830df38e1b2a846acf3ad3f8b233e2d77e339479944c5228901942408c
3
+ metadata.gz: 58c9803235729fabc83ae742e83822906d3ed88746e2c40f7d04d7368409a5cb
4
+ data.tar.gz: de245ba8d20d6b695af332f886c8b0b83a491cd563364a2c06f000c90726739e
5
5
  SHA512:
6
- metadata.gz: dbb851405183db2dd6f633f3ae24a64cf3e3ce164b70ebca97b4af60ba26c4383633c2ddbd46dc381eb0cee5cdb49f66657015857ea6cf90600ff2fe52ba3278
7
- data.tar.gz: 27db65226501347667a097c87e944122072f6c95f3402990f8ebc078116bdf43252b84aaed61c642c85359a20d1a80e15230c6e7c5a58c44f81b424107a73d30
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
- "@#{tsp_method} #{operation_name}#{params_str}: #{response_type.delete(":")};"
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
- "#{route_line}@#{tsp_method} #{operation_name}(#{params_indented}\n ): #{return_type};"
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[:type] || {},
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: infer_operation_response_type(route),
733
- doc: config.extract_docs && controller_class ? RDoc.method_doc(controller_class, route[:action]) : nil,
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)
@@ -2,5 +2,5 @@
2
2
 
3
3
  module TypeSpecFromSerializers
4
4
  # Public: This library adheres to semantic versioning.
5
- VERSION = "0.3.1"
5
+ VERSION = "0.5.0"
6
6
  end
@@ -5,3 +5,4 @@ require_relative "typespec_from_serializers/dsl"
5
5
  require_relative "typespec_from_serializers/sorbet"
6
6
  require_relative "typespec_from_serializers/rbi"
7
7
  require_relative "typespec_from_serializers/railtie"
8
+ require_relative "typespec_from_serializers/routing_patch"
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.3.1
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