typespec_from_serializers 0.3.1 → 0.4.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: 5a1d95c0934cefa4c37d29af028dbd3677a3d07b5a6f8af804c206400091472d
4
+ data.tar.gz: 8ea38365134c4d22d0e60fb19886d6803e6ee9ad82b97a52c8e21decb4ee916c
5
5
  SHA512:
6
- metadata.gz: dbb851405183db2dd6f633f3ae24a64cf3e3ce164b70ebca97b4af60ba26c4383633c2ddbd46dc381eb0cee5cdb49f66657015857ea6cf90600ff2fe52ba3278
7
- data.tar.gz: 27db65226501347667a097c87e944122072f6c95f3402990f8ebc078116bdf43252b84aaed61c642c85359a20d1a80e15230c6e7c5a58c44f81b424107a73d30
6
+ metadata.gz: 2cfdedc3b5e45b9b9fac2e6abbfd215495787f9eb150d639091f3e5df9eaa4b4d7f84b5ba5fab3ede09876df871a08b11a3165eb358cc2788bac3be8ebbe41ea
7
+ data.tar.gz: 745708a0339c1cb4f7f45773eff7a2751826d734a9f6a252a6f275c050ad194b4b9c9464cd6f81e885575befda88345681db27389a980b9ae36ab64eadd51b27
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # TypeSpec From Serializers Changelog
2
2
 
3
+ ## [0.4.0] - 2025-12-22
4
+
5
+ ### Added
6
+ - Linting system with 5 configurable rules to catch common issues during generation
7
+
8
+ ### Fixed
9
+ - Add `implicitOptionality: true` to PATCH decorators to suppress TypeSpec 1.0+ warnings
10
+
3
11
  ## [0.3.1] - 2025-12-22
4
12
 
5
13
  ### Fixed
@@ -26,6 +26,72 @@ 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
+ # Internal: Lints for missing parameter types
34
+ def missing_param_types(controller, route, path_param_names, param_types, config)
35
+ return unless enabled?(config.linting, :missing_param_types)
36
+
37
+ missing_path_params = path_param_names.select { |name| !param_types.key?(name) }
38
+
39
+ unless missing_path_params.empty?
40
+ location = "#{controller.camelize}#{config.controller_suffix}##{route[:action]}"
41
+ warn "TypeSpec Lint: Missing type for path parameter(s) #{missing_path_params.join(', ')} in #{location} (#{route[:method]} #{route[:path]}). Defaulting to 'string'."
42
+ end
43
+ end
44
+
45
+ # Internal: Lints for unknown response types
46
+ def unknown_response_type(controller, route, response_type, config)
47
+ return unless enabled?(config.linting, :unknown_response_types)
48
+
49
+ if response_type == "unknown"
50
+ location = "#{controller.camelize}#{config.controller_suffix}##{route[:action]}"
51
+ warn "TypeSpec Lint: Unknown response type for #{location} (#{route[:method]} #{route[:path]}). Consider adding a serializer or explicit type annotation."
52
+ end
53
+ end
54
+
55
+ # Internal: Lints for missing documentation
56
+ def missing_documentation(controller, route, doc, config)
57
+ return unless enabled?(config.linting, :missing_documentation) && config.extract_docs
58
+
59
+ if doc.nil?
60
+ location = "#{controller.camelize}#{config.controller_suffix}##{route[:action]}"
61
+ warn "TypeSpec Lint: Missing documentation for #{location} (#{route[:method]} #{route[:path]}). Consider adding an RDoc comment."
62
+ end
63
+ end
64
+
65
+ # Internal: Lints for duplicate action names
66
+ def ambiguous_operations(name_counts, operations, config)
67
+ return unless enabled?(config.linting, :ambiguous_operations)
68
+
69
+ duplicates = name_counts.select { |_, count| count > 1 }
70
+ duplicates.each do |action, count|
71
+ ops = operations.select { |op| op.action == action }
72
+ methods = ops.map { |op| op.method }.join(', ')
73
+ warn "TypeSpec Lint: Action '#{action}' used for multiple routes (#{methods}). Using method suffix to differentiate."
74
+ end
75
+ end
76
+
77
+ # Internal: Lints for type inference failures
78
+ def type_inference_failure(property, explicit_type, serializer_name, config)
79
+ return unless enabled?(config.linting, :type_inference_failures)
80
+
81
+ if property.type.nil? && explicit_type.nil?
82
+ warn "TypeSpec Lint: Could not infer type for '#{property.name}' in #{serializer_name}. Consider adding explicit type annotation."
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ # Internal: Checks if a linting rule is enabled
89
+ def enabled?(linting_config, rule)
90
+ return false if linting_config == false
91
+ linting_config.is_a?(Hash) && linting_config[rule]
92
+ end
93
+ end
94
+
29
95
  # Internal: Extensions that simplify the implementation of the generator.
30
96
  module SerializerRefinements
31
97
  refine String do
@@ -96,7 +162,11 @@ module TypeSpecFromSerializers
96
162
  column_name: options.fetch(:value_from),
97
163
  doc: TypeSpecFromSerializers.config.extract_docs ? RDoc.method_doc(self, options.fetch(:value_from)) : nil,
98
164
  ).tap do |property|
165
+ explicit_type = property.type
99
166
  property.infer_typespec_from(model_columns, model_enums, typespec_from, self, model_class)
167
+
168
+ # Linting
169
+ TypeSpecFromSerializers::Linting.type_inference_failure(property, explicit_type, self.name, TypeSpecFromSerializers.config)
100
170
  end
101
171
  end
102
172
  }
@@ -138,6 +208,7 @@ module TypeSpecFromSerializers
138
208
  :openapi_path,
139
209
  :max_line_length,
140
210
  :extract_docs,
211
+ :linting,
141
212
  :root,
142
213
  keyword_init: true,
143
214
  ) do
@@ -416,14 +487,21 @@ module TypeSpecFromSerializers
416
487
  def build_single_line(tsp_method, operation_name)
417
488
  params = params_typespec
418
489
  params_str = params.empty? ? "()" : "(#{params})"
419
- "@#{tsp_method} #{operation_name}#{params_str}: #{response_type.delete(":")};"
490
+ method_decorator = format_method_decorator(tsp_method)
491
+ "#{method_decorator} #{operation_name}#{params_str}: #{response_type.delete(":")};"
420
492
  end
421
493
 
422
494
  def multiline_format(route_line, tsp_method, operation_name)
423
495
  params_indented = all_params.map { |p| "\n #{p}," }.join
424
496
  return_type = response_type.delete(":")
497
+ method_decorator = format_method_decorator(tsp_method)
498
+
499
+ "#{route_line}#{method_decorator} #{operation_name}(#{params_indented}\n ): #{return_type};"
500
+ end
425
501
 
426
- "#{route_line}@#{tsp_method} #{operation_name}(#{params_indented}\n ): #{return_type};"
502
+ def format_method_decorator(tsp_method)
503
+ # PATCH operations need implicitOptionality flag in TypeSpec 1.0+
504
+ tsp_method == "patch" ? "@patch(\#{implicitOptionality: true})" : "@#{tsp_method}"
427
505
  end
428
506
 
429
507
  def operation_route_decorator(resource_path)
@@ -723,14 +801,23 @@ module TypeSpecFromSerializers
723
801
  param_types = extract_all_param_types(controller, route)
724
802
  controller_class = "#{controller.camelize}#{config.controller_suffix}".safe_constantize
725
803
 
804
+ # Linting
805
+ Linting.missing_param_types(controller, route, path_param_names, param_types, config)
806
+
807
+ response_type = infer_operation_response_type(route)
808
+ doc = config.extract_docs && controller_class ? RDoc.method_doc(controller_class, route[:action]) : nil
809
+
810
+ Linting.unknown_response_type(controller, route, response_type, config)
811
+ Linting.missing_documentation(controller, route, doc, config)
812
+
726
813
  Operation.new(
727
814
  method: route[:method],
728
815
  action: simplify_operation_name(route),
729
816
  path: route[:path],
730
817
  path_params: build_path_params(path_param_names, param_types),
731
818
  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,
819
+ response_type: response_type,
820
+ doc: doc,
734
821
  )
735
822
  end
736
823
 
@@ -786,6 +873,9 @@ module TypeSpecFromSerializers
786
873
  name_counts = operations.group_by(&:action).transform_values(&:count)
787
874
  name_usage = Hash.new(0)
788
875
 
876
+ # Linting
877
+ Linting.ambiguous_operations(name_counts, operations, config)
878
+
789
879
  operations.map do |op|
790
880
  next op unless name_counts[op.action] > 1
791
881
 
@@ -1295,6 +1385,15 @@ module TypeSpecFromSerializers
1295
1385
  # Extract documentation from RDoc comments
1296
1386
  extract_docs: true,
1297
1387
 
1388
+ # Linting configuration
1389
+ linting: {
1390
+ missing_param_types: true,
1391
+ unknown_response_types: true,
1392
+ missing_documentation: true,
1393
+ ambiguous_operations: true,
1394
+ type_inference_failures: true,
1395
+ },
1396
+
1298
1397
  # Project root directory
1299
1398
  root: root,
1300
1399
  )
@@ -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.4.0"
6
6
  end
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.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Danila Poyarkov