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 +4 -4
- data/CHANGELOG.md +7 -0
- data/lib/typespec_from_serializers/generator.rb +33 -5
- data/lib/typespec_from_serializers/rdoc.rb +124 -0
- data/lib/typespec_from_serializers/version.rb +1 -1
- metadata +17 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0fcb3ccc36429f3897ca0241c3546ae767871bcd3dc7acd51ed396ad9242baf4
|
|
4
|
+
data.tar.gz: c005d31f939a5d18cc85c7dc4444e73407c587575646debf673bf272abe8d507
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
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.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-
|
|
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
|