typespec_from_serializers 0.2.1 → 0.3.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: 86e41505961e13ef72df62124721ae6d1c929104726378b893c2c785dcec7da0
4
- data.tar.gz: 985c5514cdc77c02ee25a4257c8c3fc7d3c8adb7d7ea7585d1d64c3f3af1f732
3
+ metadata.gz: 0fcb3ccc36429f3897ca0241c3546ae767871bcd3dc7acd51ed396ad9242baf4
4
+ data.tar.gz: c005d31f939a5d18cc85c7dc4444e73407c587575646debf673bf272abe8d507
5
5
  SHA512:
6
- metadata.gz: d51adab2df0d34f587eb375950cafd6231fd5e94b8e1359086d8b7fd5745c6c200900217c7e95edab0a855a07029aaf5147fde3f89639c9843770298d03ab031
7
- data.tar.gz: 06fd0b0629657ae60d101bca4ed879cb5134801ce91097e87acc8e14825f7f2a290211dd282801bcd8154b4d7f1514ab2f24e83cfd5fd4f22be18db711bb6399
6
+ metadata.gz: b35e53c47b2e075eb57e26436ac037d18fe4b2cb2154ed58877e55bc206fe67ee17a253cc78f61d0c8007f7487c8429b6d899332c8b1e7f5686afbd026b080c8
7
+ data.tar.gz: ec7c3fa57c006157fefa598c3c129091edd3ac5f35484f46602392a7fbd2b00dc9df2343e776f19cdbc2a98eec64c49d94b6af89e252423c8d401d5bae3a695c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # TypeSpec From Serializers Changelog
2
2
 
3
+ ## [0.3.0] - 2025-12-20
4
+
5
+ ### Features
6
+ - Extract RDoc comments from serializers and controllers to generate `@doc` decorators
7
+ - New `extract_docs` config option (enabled by default) to control documentation extraction
8
+ - Requires RDoc 7.0+ for Prism-based parser support
9
+
3
10
  ## [0.2.1] - 2025-12-19
4
11
 
5
12
  ### Fixed
@@ -3,6 +3,7 @@
3
3
  require "digest"
4
4
  require "fileutils"
5
5
  require "pathname"
6
+ require "typespec_from_serializers/rdoc"
6
7
 
7
8
  # Public: Automatically generates TypeSpec descriptions for Ruby serializers and Rails routes.
8
9
  module TypeSpecFromSerializers
@@ -93,6 +94,7 @@ module TypeSpecFromSerializers
93
94
  optional: options[:optional] || options.key?(:if),
94
95
  multi: options[:association] == :many,
95
96
  column_name: options.fetch(:value_from),
97
+ doc: TypeSpecFromSerializers.config.extract_docs ? RDoc.method_doc(self, options.fetch(:value_from)) : nil,
96
98
  ).tap do |property|
97
99
  property.infer_typespec_from(model_columns, model_enums, typespec_from, self, model_class)
98
100
  end
@@ -107,6 +109,7 @@ module TypeSpecFromSerializers
107
109
  name: tsp_name,
108
110
  filename: tsp_filename,
109
111
  properties: tsp_properties,
112
+ doc: TypeSpecFromSerializers.config.extract_docs ? RDoc.class_doc(self) : nil,
110
113
  )
111
114
  end
112
115
  end
@@ -134,6 +137,7 @@ module TypeSpecFromSerializers
134
137
  :package_manager,
135
138
  :openapi_path,
136
139
  :max_line_length,
140
+ :extract_docs,
137
141
  :root,
138
142
  keyword_init: true,
139
143
  ) do
@@ -151,6 +155,7 @@ module TypeSpecFromSerializers
151
155
  :name,
152
156
  :filename,
153
157
  :properties,
158
+ :doc,
154
159
  keyword_init: true,
155
160
  ) do
156
161
  using SerializerRefinements
@@ -209,13 +214,20 @@ module TypeSpecFromSerializers
209
214
 
210
215
  def as_typespec
211
216
  indent = TypeSpecFromSerializers.config.namespace ? 2 : 1
217
+ doc_decorator = doc ? "@doc(\"#{escape_doc(doc)}\")\n#{" " * (indent - 1)}" : ""
212
218
  <<~TSP.gsub(/\n$/, "")
213
- model #{name} {
219
+ #{doc_decorator}model #{name} {
214
220
  #{" " * indent}#{properties.index_by(&:name).values.map(&:as_typespec).join("\n#{" " * indent}")}
215
221
  #{" " * (indent - 1)}}
216
222
  TSP
217
223
  end
218
224
 
225
+ private
226
+
227
+ def escape_doc(str)
228
+ str.gsub('\\', '\\\\').gsub('"', '\\"').gsub("\n", "\\n")
229
+ end
230
+
219
231
  protected
220
232
 
221
233
  def pathname
@@ -242,6 +254,7 @@ module TypeSpecFromSerializers
242
254
  :optional,
243
255
  :multi,
244
256
  :column_name,
257
+ :doc,
245
258
  keyword_init: true,
246
259
  ) do
247
260
  using SerializerRefinements
@@ -314,11 +327,16 @@ module TypeSpecFromSerializers
314
327
  end
315
328
 
316
329
  escaped_name = escape_field_name(name)
317
- "#{escaped_name}#{"?" if optional}: #{type_str}#{"[]" if multi};"
330
+ field_line = "#{escaped_name}#{"?" if optional}: #{type_str}#{"[]" if multi};"
331
+ doc ? "@doc(\"#{escape_doc(doc)}\")\n #{field_line}" : field_line
318
332
  end
319
333
 
320
334
  private
321
335
 
336
+ def escape_doc(str)
337
+ str.gsub('\\', '\\\\').gsub('"', '\\"').gsub("\n", "\\n")
338
+ end
339
+
322
340
  def escape_field_name(field_name)
323
341
  # Escape field names that conflict with TypeSpec keywords using backticks
324
342
  all_keywords = TYPESPEC_LANGUAGE_KEYWORDS +
@@ -361,22 +379,27 @@ module TypeSpecFromSerializers
361
379
  end
362
380
 
363
381
  # Internal: Represents a TypeSpec operation within a resource
364
- Operation = Struct.new(:method, :action, :path, :path_params, :body_params, :response_type, keyword_init: true) do
382
+ Operation = Struct.new(:method, :action, :path, :path_params, :body_params, :response_type, :doc, keyword_init: true) do
365
383
  def as_typespec(resource_path: nil)
366
384
  tsp_method = method.downcase
367
385
  operation_name = TypeSpecFromSerializers.config.action_to_operation_mapping[action] || action
368
386
  route_line = build_route_decorator(resource_path)
387
+ doc_line = doc ? "@doc(\"#{escape_doc(doc)}\")\n " : ""
369
388
 
370
389
  # Check if we need multiline formatting
371
390
  single_line = build_single_line(tsp_method, operation_name)
372
391
 
373
392
  too_long_for_single_line?(single_line) ?
374
- multiline_format(route_line, tsp_method, operation_name) :
375
- "#{route_line}#{single_line}"
393
+ "#{doc_line}#{multiline_format(route_line, tsp_method, operation_name)}" :
394
+ "#{doc_line}#{route_line}#{single_line}"
376
395
  end
377
396
 
378
397
  private
379
398
 
399
+ def escape_doc(str)
400
+ str.gsub('\\', '\\\\').gsub('"', '\\"').gsub("\n", "\\n")
401
+ end
402
+
380
403
  def build_route_decorator(resource_path)
381
404
  decorator = operation_route_decorator(resource_path)
382
405
  decorator ? "#{decorator}\n " : ""
@@ -698,6 +721,7 @@ module TypeSpecFromSerializers
698
721
  def build_operation(controller, route)
699
722
  path_param_names = route[:path].scan(/:([a-zA-Z_][a-zA-Z0-9_]*)/).flatten
700
723
  param_types = extract_all_param_types(controller, route)
724
+ controller_class = "#{controller.camelize}#{config.controller_suffix}".safe_constantize
701
725
 
702
726
  Operation.new(
703
727
  method: route[:method],
@@ -706,6 +730,7 @@ module TypeSpecFromSerializers
706
730
  path_params: build_path_params(path_param_names, param_types),
707
731
  body_params: build_body_params(route[:method], path_param_names, param_types),
708
732
  response_type: infer_operation_response_type(route),
733
+ doc: config.extract_docs && controller_class ? RDoc.method_doc(controller_class, route[:action]) : nil,
709
734
  )
710
735
  end
711
736
 
@@ -1267,6 +1292,9 @@ module TypeSpecFromSerializers
1267
1292
  # Maximum line length before switching to multiline format for operations
1268
1293
  max_line_length: 100,
1269
1294
 
1295
+ # Extract documentation from RDoc comments
1296
+ extract_docs: true,
1297
+
1270
1298
  # Project root directory
1271
1299
  root: root,
1272
1300
  )
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rdoc"
4
+
5
+ module TypeSpecFromSerializers
6
+ # Public: RDoc integration for documentation extraction.
7
+ #
8
+ # This module uses RDoc's Prism-based parser to extract documentation
9
+ # comments from Ruby source files. It provides documentation for:
10
+ # 1. Classes (serializers, controllers)
11
+ # 2. Methods (controller actions, serializer attributes)
12
+ #
13
+ # The module caches parsed files to avoid re-parsing the same file
14
+ # multiple times during a single generation run.
15
+ #
16
+ # Requires RDoc 7.0+ for Prism parser support. Gracefully degrades
17
+ # to returning nil for all documentation when unavailable.
18
+ module RDoc
19
+ class << self
20
+ # Public: Check if RDoc Prism parser is available.
21
+ #
22
+ # Returns true if RDoc 7.0+ with Prism parser is available.
23
+ def available?
24
+ return @available if defined?(@available)
25
+
26
+ @available = begin
27
+ require "rdoc/parser/prism_ruby"
28
+ true
29
+ rescue LoadError
30
+ false
31
+ end
32
+ end
33
+
34
+ # Public: Get documentation for a class.
35
+ #
36
+ # klass - The Class to get documentation for
37
+ #
38
+ # Returns String or nil
39
+ def class_doc(klass)
40
+ find_rdoc_class(klass)&.then { extract_comment_text(_1.comment) }
41
+ end
42
+
43
+ # Public: Get documentation for a method.
44
+ #
45
+ # klass - The Class containing the method
46
+ # method_name - Symbol or String name of the method
47
+ #
48
+ # Returns String or nil
49
+ def method_doc(klass, method_name)
50
+ find_rdoc_class(klass)
51
+ &.method_list
52
+ &.find { _1.name == method_name.to_s }
53
+ &.then { extract_comment_text(_1.comment) }
54
+ end
55
+
56
+ # Public: Clear the parse cache.
57
+ #
58
+ # This should be called between generation runs if files may have changed.
59
+ def clear_cache!
60
+ @cache = {}
61
+ end
62
+
63
+ private
64
+
65
+ # Internal: Find the RDoc class object for a Ruby class.
66
+ #
67
+ # klass - The Class to find documentation for
68
+ #
69
+ # Returns RDoc::NormalClass, RDoc::NormalModule, or nil
70
+ def find_rdoc_class(klass)
71
+ return unless available?
72
+
73
+ file_path = Object.const_source_location(klass.name)&.first
74
+ top_level = file_path && parse_file(file_path)
75
+ return unless top_level
76
+
77
+ class_name = klass.name.split("::").last
78
+ top_level.classes.find { _1.name == class_name } ||
79
+ top_level.modules.find { _1.name == class_name }
80
+ rescue
81
+ nil
82
+ end
83
+
84
+ # Internal: Parse a Ruby file and return the RDoc top-level object.
85
+ #
86
+ # file_path - String path to the Ruby file
87
+ #
88
+ # Returns RDoc::TopLevel or nil if parsing fails
89
+ def parse_file(file_path)
90
+ @cache ||= {}
91
+ @cache[file_path] ||= begin
92
+ return unless File.exist?(file_path)
93
+
94
+ options = ::RDoc::Options.new
95
+ store = ::RDoc::Store.new(options)
96
+ top_level = store.add_file(file_path)
97
+ stats = ::RDoc::Stats.new(store, 0, 0)
98
+
99
+ parser = ::RDoc::Parser::PrismRuby.new(top_level, File.read(file_path), options, stats)
100
+ parser.scan
101
+ top_level
102
+ rescue => e
103
+ warn "TypeSpec: Failed to parse #{file_path} for docs: #{e.message}" if ENV["DEBUG"]
104
+ nil
105
+ end
106
+ end
107
+
108
+ # Internal: Extract text from an RDoc::Comment or String.
109
+ #
110
+ # comment - RDoc::Comment object or String
111
+ #
112
+ # Returns String or nil
113
+ def extract_comment_text(comment)
114
+ case comment
115
+ when String then comment.strip.presence
116
+ when nil then nil
117
+ else
118
+ comment.normalize
119
+ comment.text&.strip.presence
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module TypeSpecFromSerializers
4
4
  # Public: This library adheres to semantic versioning.
5
- VERSION = "0.2.1"
5
+ VERSION = "0.3.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.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Danila Poyarkov
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2025-12-19 00:00:00.000000000 Z
12
+ date: 2025-12-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: railties
@@ -87,6 +87,20 @@ dependencies:
87
87
  - - ">="
88
88
  - !ruby/object:Gem::Version
89
89
  version: 1.0.0
90
+ - !ruby/object:Gem::Dependency
91
+ name: rdoc
92
+ requirement: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '7.0'
97
+ type: :runtime
98
+ prerelease: false
99
+ version_requirements: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '7.0'
90
104
  - !ruby/object:Gem::Dependency
91
105
  name: bundler
92
106
  requirement: !ruby/object:Gem::Requirement
@@ -305,6 +319,7 @@ files:
305
319
  - lib/typespec_from_serializers/openapi_compiler.rb
306
320
  - lib/typespec_from_serializers/railtie.rb
307
321
  - lib/typespec_from_serializers/rbi.rb
322
+ - lib/typespec_from_serializers/rdoc.rb
308
323
  - lib/typespec_from_serializers/runner.rb
309
324
  - lib/typespec_from_serializers/sorbet.rb
310
325
  - lib/typespec_from_serializers/version.rb