apiwork 0.0.0.pre → 0.1.2

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.
Files changed (202) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +2 -2
  3. data/README.md +117 -1
  4. data/Rakefile +5 -3
  5. data/app/controllers/apiwork/errors_controller.rb +13 -0
  6. data/app/controllers/apiwork/exports_controller.rb +22 -0
  7. data/lib/apiwork/abstractable.rb +26 -0
  8. data/lib/apiwork/adapter/base.rb +369 -0
  9. data/lib/apiwork/adapter/builder/api/base.rb +66 -0
  10. data/lib/apiwork/adapter/builder/contract/base.rb +86 -0
  11. data/lib/apiwork/adapter/capability/api/base.rb +51 -0
  12. data/lib/apiwork/adapter/capability/api/scope.rb +64 -0
  13. data/lib/apiwork/adapter/capability/base.rb +291 -0
  14. data/lib/apiwork/adapter/capability/contract/base.rb +37 -0
  15. data/lib/apiwork/adapter/capability/contract/scope.rb +110 -0
  16. data/lib/apiwork/adapter/capability/operation/base.rb +172 -0
  17. data/lib/apiwork/adapter/capability/operation/metadata_shape.rb +165 -0
  18. data/lib/apiwork/adapter/capability/result.rb +21 -0
  19. data/lib/apiwork/adapter/capability/runner.rb +56 -0
  20. data/lib/apiwork/adapter/capability/transformer/request/base.rb +72 -0
  21. data/lib/apiwork/adapter/capability/transformer/response/base.rb +45 -0
  22. data/lib/apiwork/adapter/registry.rb +16 -0
  23. data/lib/apiwork/adapter/serializer/error/base.rb +72 -0
  24. data/lib/apiwork/adapter/serializer/error/default/api_builder.rb +32 -0
  25. data/lib/apiwork/adapter/serializer/error/default.rb +37 -0
  26. data/lib/apiwork/adapter/serializer/resource/base.rb +84 -0
  27. data/lib/apiwork/adapter/serializer/resource/default/contract_builder.rb +209 -0
  28. data/lib/apiwork/adapter/serializer/resource/default.rb +39 -0
  29. data/lib/apiwork/adapter/standard/capability/filtering/api_builder.rb +75 -0
  30. data/lib/apiwork/adapter/standard/capability/filtering/constants.rb +37 -0
  31. data/lib/apiwork/adapter/standard/capability/filtering/contract_builder.rb +193 -0
  32. data/lib/apiwork/adapter/standard/capability/filtering/operation/filter/builder.rb +47 -0
  33. data/lib/apiwork/adapter/standard/capability/filtering/operation/filter/operator_builder.rb +36 -0
  34. data/lib/apiwork/adapter/standard/capability/filtering/operation/filter.rb +462 -0
  35. data/lib/apiwork/adapter/standard/capability/filtering/operation.rb +22 -0
  36. data/lib/apiwork/adapter/standard/capability/filtering/request_transformer.rb +47 -0
  37. data/lib/apiwork/adapter/standard/capability/filtering.rb +18 -0
  38. data/lib/apiwork/adapter/standard/capability/including/contract_builder.rb +169 -0
  39. data/lib/apiwork/adapter/standard/capability/including/operation.rb +20 -0
  40. data/lib/apiwork/adapter/standard/capability/including.rb +16 -0
  41. data/lib/apiwork/adapter/standard/capability/pagination/api_builder.rb +34 -0
  42. data/lib/apiwork/adapter/standard/capability/pagination/contract_builder.rb +35 -0
  43. data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate/cursor.rb +84 -0
  44. data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate/offset.rb +66 -0
  45. data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate.rb +24 -0
  46. data/lib/apiwork/adapter/standard/capability/pagination/operation.rb +24 -0
  47. data/lib/apiwork/adapter/standard/capability/pagination.rb +21 -0
  48. data/lib/apiwork/adapter/standard/capability/sorting/api_builder.rb +19 -0
  49. data/lib/apiwork/adapter/standard/capability/sorting/contract_builder.rb +84 -0
  50. data/lib/apiwork/adapter/standard/capability/sorting/operation/sort.rb +83 -0
  51. data/lib/apiwork/adapter/standard/capability/sorting/operation.rb +22 -0
  52. data/lib/apiwork/adapter/standard/capability/sorting.rb +17 -0
  53. data/lib/apiwork/adapter/standard/capability/writing/constants.rb +15 -0
  54. data/lib/apiwork/adapter/standard/capability/writing/contract_builder.rb +253 -0
  55. data/lib/apiwork/adapter/standard/capability/writing/operation/issue_mapper.rb +210 -0
  56. data/lib/apiwork/adapter/standard/capability/writing/operation.rb +32 -0
  57. data/lib/apiwork/adapter/standard/capability/writing/request_transformer.rb +37 -0
  58. data/lib/apiwork/adapter/standard/capability/writing.rb +17 -0
  59. data/lib/apiwork/adapter/standard/includes_resolver.rb +106 -0
  60. data/lib/apiwork/adapter/standard.rb +22 -0
  61. data/lib/apiwork/adapter/wrapper/base.rb +70 -0
  62. data/lib/apiwork/adapter/wrapper/collection/base.rb +60 -0
  63. data/lib/apiwork/adapter/wrapper/collection/default.rb +47 -0
  64. data/lib/apiwork/adapter/wrapper/error/base.rb +30 -0
  65. data/lib/apiwork/adapter/wrapper/error/default.rb +34 -0
  66. data/lib/apiwork/adapter/wrapper/member/base.rb +58 -0
  67. data/lib/apiwork/adapter/wrapper/member/default.rb +40 -0
  68. data/lib/apiwork/adapter/wrapper/shape.rb +203 -0
  69. data/lib/apiwork/adapter.rb +50 -0
  70. data/lib/apiwork/api/base.rb +802 -0
  71. data/lib/apiwork/api/element.rb +110 -0
  72. data/lib/apiwork/api/enum_registry/definition.rb +51 -0
  73. data/lib/apiwork/api/enum_registry.rb +98 -0
  74. data/lib/apiwork/api/info/contact.rb +67 -0
  75. data/lib/apiwork/api/info/license.rb +50 -0
  76. data/lib/apiwork/api/info/server.rb +50 -0
  77. data/lib/apiwork/api/info.rb +221 -0
  78. data/lib/apiwork/api/object.rb +235 -0
  79. data/lib/apiwork/api/registry.rb +33 -0
  80. data/lib/apiwork/api/representation_registry.rb +76 -0
  81. data/lib/apiwork/api/resource/action.rb +41 -0
  82. data/lib/apiwork/api/resource.rb +648 -0
  83. data/lib/apiwork/api/router.rb +104 -0
  84. data/lib/apiwork/api/type_registry/definition.rb +117 -0
  85. data/lib/apiwork/api/type_registry.rb +99 -0
  86. data/lib/apiwork/api/union.rb +49 -0
  87. data/lib/apiwork/api.rb +85 -0
  88. data/lib/apiwork/configurable.rb +71 -0
  89. data/lib/apiwork/configuration/option.rb +125 -0
  90. data/lib/apiwork/configuration/validatable.rb +25 -0
  91. data/lib/apiwork/configuration.rb +95 -0
  92. data/lib/apiwork/configuration_error.rb +6 -0
  93. data/lib/apiwork/constraint_error.rb +20 -0
  94. data/lib/apiwork/contract/action/request.rb +79 -0
  95. data/lib/apiwork/contract/action/response.rb +87 -0
  96. data/lib/apiwork/contract/action.rb +258 -0
  97. data/lib/apiwork/contract/base.rb +714 -0
  98. data/lib/apiwork/contract/element.rb +130 -0
  99. data/lib/apiwork/contract/object/coercer.rb +194 -0
  100. data/lib/apiwork/contract/object/deserializer.rb +101 -0
  101. data/lib/apiwork/contract/object/transformer.rb +95 -0
  102. data/lib/apiwork/contract/object/validator/result.rb +27 -0
  103. data/lib/apiwork/contract/object/validator.rb +734 -0
  104. data/lib/apiwork/contract/object.rb +566 -0
  105. data/lib/apiwork/contract/request_parser/result.rb +25 -0
  106. data/lib/apiwork/contract/request_parser.rb +72 -0
  107. data/lib/apiwork/contract/response_parser/result.rb +25 -0
  108. data/lib/apiwork/contract/response_parser.rb +35 -0
  109. data/lib/apiwork/contract/union.rb +56 -0
  110. data/lib/apiwork/contract_error.rb +9 -0
  111. data/lib/apiwork/controller.rb +300 -0
  112. data/lib/apiwork/domain_error.rb +13 -0
  113. data/lib/apiwork/element.rb +386 -0
  114. data/lib/apiwork/engine.rb +20 -0
  115. data/lib/apiwork/error.rb +6 -0
  116. data/lib/apiwork/error_code/definition.rb +63 -0
  117. data/lib/apiwork/error_code/registry.rb +18 -0
  118. data/lib/apiwork/error_code.rb +132 -0
  119. data/lib/apiwork/export/base.rb +291 -0
  120. data/lib/apiwork/export/open_api.rb +600 -0
  121. data/lib/apiwork/export/pipeline/writer.rb +66 -0
  122. data/lib/apiwork/export/pipeline.rb +84 -0
  123. data/lib/apiwork/export/registry.rb +16 -0
  124. data/lib/apiwork/export/surface_resolver.rb +189 -0
  125. data/lib/apiwork/export/type_analysis.rb +170 -0
  126. data/lib/apiwork/export/type_script.rb +23 -0
  127. data/lib/apiwork/export/type_script_mapper.rb +349 -0
  128. data/lib/apiwork/export/zod.rb +39 -0
  129. data/lib/apiwork/export/zod_mapper.rb +421 -0
  130. data/lib/apiwork/export.rb +80 -0
  131. data/lib/apiwork/http_error.rb +16 -0
  132. data/lib/apiwork/introspection/action/request.rb +66 -0
  133. data/lib/apiwork/introspection/action/response.rb +57 -0
  134. data/lib/apiwork/introspection/action.rb +124 -0
  135. data/lib/apiwork/introspection/api/info/contact.rb +59 -0
  136. data/lib/apiwork/introspection/api/info/license.rb +49 -0
  137. data/lib/apiwork/introspection/api/info/server.rb +50 -0
  138. data/lib/apiwork/introspection/api/info.rb +107 -0
  139. data/lib/apiwork/introspection/api/resource.rb +83 -0
  140. data/lib/apiwork/introspection/api.rb +92 -0
  141. data/lib/apiwork/introspection/contract.rb +63 -0
  142. data/lib/apiwork/introspection/dump/action.rb +101 -0
  143. data/lib/apiwork/introspection/dump/api.rb +119 -0
  144. data/lib/apiwork/introspection/dump/contract.rb +129 -0
  145. data/lib/apiwork/introspection/dump/param.rb +486 -0
  146. data/lib/apiwork/introspection/dump/resource.rb +112 -0
  147. data/lib/apiwork/introspection/dump/type.rb +339 -0
  148. data/lib/apiwork/introspection/dump.rb +17 -0
  149. data/lib/apiwork/introspection/enum.rb +63 -0
  150. data/lib/apiwork/introspection/error_code.rb +44 -0
  151. data/lib/apiwork/introspection/param/array.rb +88 -0
  152. data/lib/apiwork/introspection/param/base.rb +285 -0
  153. data/lib/apiwork/introspection/param/binary.rb +73 -0
  154. data/lib/apiwork/introspection/param/boolean.rb +73 -0
  155. data/lib/apiwork/introspection/param/date.rb +73 -0
  156. data/lib/apiwork/introspection/param/date_time.rb +73 -0
  157. data/lib/apiwork/introspection/param/decimal.rb +121 -0
  158. data/lib/apiwork/introspection/param/integer.rb +131 -0
  159. data/lib/apiwork/introspection/param/literal.rb +45 -0
  160. data/lib/apiwork/introspection/param/number.rb +121 -0
  161. data/lib/apiwork/introspection/param/object.rb +59 -0
  162. data/lib/apiwork/introspection/param/reference.rb +45 -0
  163. data/lib/apiwork/introspection/param/string.rb +122 -0
  164. data/lib/apiwork/introspection/param/time.rb +73 -0
  165. data/lib/apiwork/introspection/param/union.rb +57 -0
  166. data/lib/apiwork/introspection/param/unknown.rb +26 -0
  167. data/lib/apiwork/introspection/param/uuid.rb +73 -0
  168. data/lib/apiwork/introspection/param.rb +31 -0
  169. data/lib/apiwork/introspection/type.rb +129 -0
  170. data/lib/apiwork/introspection.rb +28 -0
  171. data/lib/apiwork/issue.rb +80 -0
  172. data/lib/apiwork/json_pointer.rb +21 -0
  173. data/lib/apiwork/object.rb +1618 -0
  174. data/lib/apiwork/reference_generator.rb +638 -0
  175. data/lib/apiwork/registry.rb +56 -0
  176. data/lib/apiwork/representation/association.rb +391 -0
  177. data/lib/apiwork/representation/attribute.rb +335 -0
  178. data/lib/apiwork/representation/base.rb +819 -0
  179. data/lib/apiwork/representation/deserializer.rb +95 -0
  180. data/lib/apiwork/representation/element.rb +128 -0
  181. data/lib/apiwork/representation/inheritance.rb +78 -0
  182. data/lib/apiwork/representation/model_detector.rb +75 -0
  183. data/lib/apiwork/representation/root_key.rb +35 -0
  184. data/lib/apiwork/representation/serializer.rb +127 -0
  185. data/lib/apiwork/request.rb +79 -0
  186. data/lib/apiwork/response.rb +56 -0
  187. data/lib/apiwork/union.rb +102 -0
  188. data/lib/apiwork/version.rb +2 -2
  189. data/lib/apiwork.rb +61 -3
  190. data/lib/generators/apiwork/api_generator.rb +38 -0
  191. data/lib/generators/apiwork/contract_generator.rb +25 -0
  192. data/lib/generators/apiwork/install_generator.rb +27 -0
  193. data/lib/generators/apiwork/representation_generator.rb +25 -0
  194. data/lib/generators/apiwork/templates/api/api.rb.tt +4 -0
  195. data/lib/generators/apiwork/templates/contract/contract.rb.tt +6 -0
  196. data/lib/generators/apiwork/templates/install/application_contract.rb.tt +5 -0
  197. data/lib/generators/apiwork/templates/install/application_representation.rb.tt +5 -0
  198. data/lib/generators/apiwork/templates/representation/representation.rb.tt +6 -0
  199. data/lib/tasks/apiwork.rake +102 -0
  200. metadata +319 -19
  201. data/.rubocop.yml +0 -8
  202. data/sig/apiwork.rbs +0 -4
@@ -0,0 +1,638 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'active_support/core_ext/string/inflections'
5
+ require 'active_support/core_ext/object/blank'
6
+
7
+ module Apiwork
8
+ class ReferenceGenerator
9
+ GEM_ROOT = File.expand_path('../..', __dir__)
10
+ OUTPUT_DIR = File.join(GEM_ROOT, 'docs/reference')
11
+ GITHUB_URL = 'https://github.com/skiftle/apiwork/blob/main'
12
+ RUBY_PRIMITIVES = %w[
13
+ String Integer Float Boolean Symbol Hash Array Object
14
+ TrueClass FalseClass NilClass Numeric Proc
15
+ ].to_set.freeze
16
+
17
+ class << self
18
+ def generate
19
+ new.generate
20
+ end
21
+ end
22
+
23
+ def generate
24
+ require 'yard'
25
+
26
+ parse_source
27
+ modules = extract_modules
28
+ write_files(modules)
29
+ end
30
+
31
+ private
32
+
33
+ def parse_source
34
+ YARD::Registry.clear
35
+ YARD.parse(File.join(GEM_ROOT, 'lib/**/*.rb'))
36
+ end
37
+
38
+ def public_api_yard_objects
39
+ @public_api_yard_objects ||= YARD::Registry.all(:class, :module)
40
+ .select { |yard_object| yard_object.path.start_with?('Apiwork') && public_api?(yard_object) }
41
+ end
42
+
43
+ def extract_modules
44
+ public_api_yard_objects
45
+ .map { |yard_object| serialize_module(yard_object) }
46
+ .reject { |mod| mod[:class_methods].empty? && mod[:instance_methods].empty? }
47
+ .sort_by { |mod| mod[:path] }
48
+ end
49
+
50
+ def public_api?(yard_object)
51
+ api_tag = yard_object.docstring.tags(:api).find { |tag| tag.text == 'public' }
52
+
53
+ if yard_object.type == :method
54
+ return false unless api_tag
55
+
56
+ parent = yard_object.namespace
57
+ return true unless parent
58
+
59
+ parent_api_tag = parent.docstring.tags(:api).find { |tag| tag.text == 'public' }
60
+ return api_tag.object_id != parent_api_tag&.object_id
61
+ end
62
+
63
+ has_own_docstring = !yard_object.docstring.to_s.strip.empty?
64
+ return true if has_own_docstring && api_tag
65
+
66
+ return false unless yard_object.file
67
+
68
+ lines = File.readlines(yard_object.file)
69
+ docstring_range = yard_object.docstring.line_range
70
+
71
+ start_line = if docstring_range
72
+ docstring_range.first - 1
73
+ else
74
+ [yard_object.line - 5, 0].max
75
+ end
76
+ end_line = yard_object.line
77
+
78
+ preceding_lines = lines[start_line...end_line].join
79
+ preceding_lines.include?('@api public')
80
+ end
81
+
82
+ def serialize_module(yard_object)
83
+ {
84
+ class_methods: extract_methods(yard_object, :class),
85
+ docstring: yard_object.docstring.to_s,
86
+ examples: extract_examples(yard_object),
87
+ file: relative_path(yard_object.file),
88
+ instance_methods: extract_methods(yard_object, :instance),
89
+ line: yard_object.line,
90
+ name: yard_object.name.to_s,
91
+ path: yard_object.path,
92
+ type: yard_object.type,
93
+ }
94
+ end
95
+
96
+ def relative_path(file)
97
+ return nil unless file
98
+
99
+ file.delete_prefix("#{GEM_ROOT}/")
100
+ end
101
+
102
+ def extract_methods(yard_object, scope)
103
+ methods = yard_object.meths(scope:, visibility: :public)
104
+
105
+ yard_object.mixins(:instance).each do |mixin|
106
+ mixin_yard_object = YARD::Registry.at(mixin.path)
107
+ next unless mixin_yard_object
108
+
109
+ methods += mixin_yard_object.meths(scope:, visibility: :public)
110
+ end
111
+
112
+ methods
113
+ .select { |method| public_api?(method) && documented?(method) }
114
+ .uniq(&:name)
115
+ .sort_by(&:name)
116
+ .map { |method| serialize_method(method) }
117
+ end
118
+
119
+ def documented?(method)
120
+ return true unless method.docstring.to_s.strip.empty?
121
+
122
+ method.docstring.tags.any? do |tag|
123
+ next false if tag.tag_name == 'api'
124
+ next true if tag.tag_name == 'see' && tag.name.present?
125
+ next true if tag.tag_name == 'return' && tag.types.present?
126
+ next true if tag.tag_name == 'param' && tag.types.present?
127
+
128
+ tag.text.to_s.strip.present?
129
+ end
130
+ end
131
+
132
+ def serialize_method(method)
133
+ {
134
+ docstring: method.docstring.to_s,
135
+ examples: extract_examples(method),
136
+ file: relative_path(method.file),
137
+ line: method.line,
138
+ name: method.name.to_s,
139
+ params: extract_params(method),
140
+ returns: extract_return(method),
141
+ see: extract_see(method),
142
+ signature: build_signature(method),
143
+ summary: method.docstring.summary,
144
+ yieldparams: extract_yieldparams(method),
145
+ }
146
+ end
147
+
148
+ def build_signature(method)
149
+ params = method.parameters.reject { |name, _default| name.to_s.start_with?('internal') }
150
+ params = params.map do |name, default|
151
+ if name.to_s.end_with?(':')
152
+ default ? "#{name} #{default}" : name.to_s
153
+ else
154
+ default ? "#{name} = #{default}" : name.to_s
155
+ end
156
+ end
157
+
158
+ name = escape_brackets(method.name.to_s)
159
+ params.any? ? "#{name}(#{params.join(', ')})" : name
160
+ end
161
+
162
+ def escape_brackets(text)
163
+ text.gsub('[', '\\[').gsub(']', '\\]')
164
+ end
165
+
166
+ def extract_params(method)
167
+ method.docstring.tags(:param).map do |tag|
168
+ parsed = parse_param_description(tag.text)
169
+ {
170
+ default: parsed[:default],
171
+ description: parsed[:description],
172
+ name: tag.name,
173
+ types: tag.types || [],
174
+ values: parsed[:values],
175
+ values_type: parsed[:values_type],
176
+ }
177
+ end
178
+ end
179
+
180
+ def format_param_type(param)
181
+ replace_type = param[:values_type] || 'Symbol'
182
+
183
+ types = param[:types].map do |t|
184
+ if param[:values]&.any? && t == replace_type
185
+ values = param[:values].join(', ').gsub('|', '\|')
186
+ "`#{t}<#{values}>`"
187
+ else
188
+ "`#{t}`"
189
+ end
190
+ end
191
+
192
+ types.join(', ')
193
+ end
194
+
195
+ def parse_param_description(text)
196
+ return { default: nil, description: nil, values: nil, values_type: nil } if text.blank?
197
+
198
+ match = text.match(/\A(?:\(([^)]+)\))?\s*(?:\[(?:(\w+):\s*)?([^\]]+)\])?\s*(.*)\z/m)
199
+ {
200
+ default: match[1],
201
+ description: match[4].presence,
202
+ values: match[3]&.split(/,\s*/),
203
+ values_type: match[2],
204
+ }
205
+ end
206
+
207
+ def extract_return(method)
208
+ tag = method.docstring.tag(:return)
209
+ return nil unless tag
210
+
211
+ {
212
+ description: tag.text,
213
+ types: tag.types || [],
214
+ }
215
+ end
216
+
217
+ def extract_examples(method)
218
+ method.docstring.tags(:example).map do |tag|
219
+ {
220
+ code: tag.text,
221
+ title: tag.name,
222
+ }
223
+ end
224
+ end
225
+
226
+ def extract_see(method)
227
+ method.docstring.tags(:see).map(&:name)
228
+ end
229
+
230
+ def extract_yieldparams(method)
231
+ method.docstring.tags(:yieldparam).map do |tag|
232
+ { name: tag.name, types: tag.types || [] }
233
+ end
234
+ end
235
+
236
+ def write_files(modules)
237
+ cleanup_old_files
238
+ @modules = modules
239
+ @modules_with_children = build_modules_with_children(modules)
240
+
241
+ write_root_index
242
+ modules.each.with_index(1) do |mod, order|
243
+ filepath = module_filepath(mod[:path])
244
+ FileUtils.mkdir_p(File.dirname(filepath))
245
+ content = render_module(mod, order)
246
+ File.write(filepath, content)
247
+ end
248
+
249
+ write_namespace_indexes
250
+ end
251
+
252
+ def write_root_index
253
+ children = find_direct_children('Apiwork')
254
+ parts = []
255
+ parts << "---\norder: 2\n---\n"
256
+ parts << "# Reference\n"
257
+ parts << "Complete API reference for Apiwork's public classes.\n"
258
+
259
+ if children.any?
260
+ parts << "## Modules\n"
261
+ render_child_links(parts, 'Apiwork', children)
262
+ end
263
+
264
+ FileUtils.mkdir_p(OUTPUT_DIR)
265
+ File.write(File.join(OUTPUT_DIR, 'index.md'), parts.join("\n"))
266
+ end
267
+
268
+ def write_namespace_indexes
269
+ collect_all_folder_paths.each do |folder_parts|
270
+ folder_path = File.join(OUTPUT_DIR, *folder_parts.map { |p| dasherize(p) })
271
+ index_path = File.join(folder_path, 'index.md')
272
+
273
+ next if File.exist?(index_path)
274
+
275
+ title = folder_parts.last
276
+ parent_path = "Apiwork::#{folder_parts.join('::')}"
277
+ children = find_direct_children(parent_path)
278
+ first_child = @modules.index { |m| m[:path].start_with?("#{parent_path}::") }
279
+ order = first_child + 1
280
+
281
+ content = render_namespace_index(title, parent_path, children, order)
282
+ FileUtils.mkdir_p(folder_path)
283
+ File.write(index_path, content)
284
+ end
285
+ end
286
+
287
+ def collect_all_folder_paths
288
+ paths = Set.new
289
+
290
+ @modules.each do |mod|
291
+ parts = mod[:path].sub('Apiwork::', '').split('::')
292
+
293
+ (1...parts.size).each do |i|
294
+ paths << parts[0...i]
295
+ end
296
+
297
+ paths << parts if @modules_with_children.include?(mod[:path])
298
+ end
299
+
300
+ paths.to_a.sort_by(&:size)
301
+ end
302
+
303
+ def find_direct_children(parent_path)
304
+ @modules
305
+ .select { |m| m[:path].start_with?("#{parent_path}::") }
306
+ .map do |m|
307
+ remaining = m[:path].sub("#{parent_path}::", '')
308
+ remaining.split('::').first
309
+ end
310
+ .uniq
311
+ .sort
312
+ end
313
+
314
+ def render_namespace_index(title, parent_path, children, order)
315
+ parts = []
316
+ parts << "---\norder: #{order}\nprev: false\nnext: false\n---\n"
317
+ parts << "# #{title}\n"
318
+
319
+ if children.any?
320
+ parts << "## Modules\n"
321
+ render_child_links(parts, parent_path, children)
322
+ end
323
+
324
+ parts.join("\n")
325
+ end
326
+
327
+ def render_child_links(parts, parent_path, children)
328
+ children.each do |child|
329
+ child_path = dasherize(child)
330
+ child_full_path = "#{parent_path}::#{child}"
331
+ folder = @modules_with_children.include?(child_full_path) ||
332
+ @modules.none? { |m| m[:path] == child_full_path }
333
+ link = folder ? "./#{child_path}/" : "./#{child_path}"
334
+ parts << "- [#{child}](#{link})"
335
+ end
336
+ parts << ''
337
+ end
338
+
339
+ def render_examples(parts, examples)
340
+ examples.each do |example|
341
+ title_suffix = example[:title].blank? ? '' : ": #{example[:title]}"
342
+ parts << "**Example#{title_suffix}**\n"
343
+ parts << '```ruby'
344
+ parts << example[:code]
345
+ parts << "```\n"
346
+ end
347
+ end
348
+
349
+ def build_modules_with_children(modules)
350
+ paths = modules.map { |m| m[:path] }.to_set
351
+ paths.select do |path|
352
+ paths.any? { |other| other != path && other.start_with?("#{path}::") }
353
+ end.to_set
354
+ end
355
+
356
+ def cleanup_old_files
357
+ Dir.glob(File.join(OUTPUT_DIR, '**/*')).each do |entry|
358
+ FileUtils.rm_rf(entry)
359
+ end
360
+ end
361
+
362
+ def module_filepath(path)
363
+ parts = path.sub('Apiwork::', '').split('::')
364
+ return File.join(OUTPUT_DIR, 'index.md') if parts.empty?
365
+
366
+ file_parts = parts.map { |part| dasherize(part) }
367
+
368
+ if @modules_with_children.include?(path)
369
+ File.join(OUTPUT_DIR, *file_parts, 'index.md')
370
+ else
371
+ folders = file_parts[0..-2]
372
+ filename = "#{file_parts.last}.md"
373
+ File.join(OUTPUT_DIR, *folders, filename)
374
+ end
375
+ end
376
+
377
+ def display_title(path)
378
+ path.sub('Apiwork::', '').split('::').last
379
+ end
380
+
381
+ def dasherize(string)
382
+ string.underscore.dasherize
383
+ end
384
+
385
+ def linkify_yard_refs(text)
386
+ return text if text.blank?
387
+
388
+ result = text.gsub(/\{([^}]+)\}/) do
389
+ ref = ::Regexp.last_match(1)
390
+ link_path = yard_ref_to_path(ref)
391
+ "[#{ref}](#{link_path})"
392
+ end
393
+ escape_html(result)
394
+ end
395
+
396
+ def escape_html(text)
397
+ return text if text.blank?
398
+
399
+ text.gsub('<', '&lt;').gsub('>', '&gt;')
400
+ end
401
+
402
+ def linkify_type(type_str)
403
+ parsed = yard_type_parser.parse(type_str)
404
+ names = extract_type_names(parsed)
405
+ linkable_names = names.select { |name| linkable_type?(name) }
406
+
407
+ if linkable_names.empty?
408
+ "`#{type_str}`"
409
+ else
410
+ result = escape_html(type_str)
411
+ linkable_names.each do |name|
412
+ result.gsub!(/\b#{Regexp.escape(name)}\b/, "[#{name}](#{class_to_filepath(name)})")
413
+ end
414
+ result
415
+ end
416
+ rescue StandardError
417
+ "`#{type_str}`"
418
+ end
419
+
420
+ def yard_type_parser
421
+ YARD::Tags::TypesExplainer::Parser
422
+ end
423
+
424
+ def extract_type_names(types)
425
+ types.flat_map do |type|
426
+ case type
427
+ when YARD::Tags::TypesExplainer::HashCollectionType
428
+ [type.name] + extract_type_names(type.key_types) + extract_type_names(type.value_types)
429
+ when YARD::Tags::TypesExplainer::CollectionType, YARD::Tags::TypesExplainer::FixedCollectionType
430
+ [type.name] + extract_type_names(type.types)
431
+ else
432
+ [type.name]
433
+ end
434
+ end.uniq
435
+ end
436
+
437
+ def linkable_type?(type_name)
438
+ return false if RUBY_PRIMITIVES.include?(type_name)
439
+
440
+ @linkable_types ||= build_linkable_types
441
+ @linkable_types.include?(type_name)
442
+ end
443
+
444
+ def build_linkable_types
445
+ public_api_yard_objects
446
+ .flat_map do |yard_object|
447
+ path = yard_object.path.delete_prefix('Apiwork::')
448
+ parts = path.split('::')
449
+ Array.new(parts.size) { |index| parts[index..].join('::') }
450
+ end
451
+ .reject { |name| RUBY_PRIMITIVES.include?(name) }
452
+ .to_set
453
+ end
454
+
455
+ def type_path_lookup
456
+ @type_path_lookup ||= build_type_path_lookup
457
+ end
458
+
459
+ def build_type_path_lookup
460
+ lookup = {}
461
+ public_api_yard_objects.each do |yard_object|
462
+ full_path = yard_object.path.delete_prefix('Apiwork::')
463
+ parts = full_path.split('::')
464
+
465
+ parts.size.times do |index|
466
+ partial_path = parts[index..].join('::')
467
+ next if RUBY_PRIMITIVES.include?(partial_path)
468
+
469
+ lookup[partial_path] ||= full_path
470
+ end
471
+ end
472
+
473
+ lookup
474
+ end
475
+
476
+ def yard_ref_to_path(ref)
477
+ if ref.start_with?('#', '.')
478
+ "##{ref[1..].dasherize}"
479
+ elsif ref.include?('#')
480
+ class_part, method_part = ref.split('#', 2)
481
+ "#{class_to_filepath(class_part)}##{method_part.dasherize}"
482
+ elsif ref.include?('.')
483
+ class_part, method_part = ref.split('.', 2)
484
+ "#{class_to_filepath(class_part)}##{method_part.dasherize}"
485
+ else
486
+ class_to_filepath(ref)
487
+ end
488
+ end
489
+
490
+ def see_ref_linkable?(ref)
491
+ return true if ref.start_with?('#', '.')
492
+
493
+ class_part = ref.split(/[#.]/, 2).first
494
+ linkable_type?(class_part)
495
+ end
496
+
497
+ def class_to_filepath(class_name)
498
+ resolved = type_path_lookup[class_name] || class_name
499
+ without_apiwork = resolved.delete_prefix('Apiwork::')
500
+ parts = without_apiwork.split('::')
501
+
502
+ return '/reference/' if parts.empty?
503
+
504
+ file_parts = parts.map { |part| dasherize(part) }
505
+ full_path = "Apiwork::#{without_apiwork}"
506
+
507
+ if modules_with_children_for_links.include?(full_path)
508
+ "/reference/#{File.join(*file_parts)}/"
509
+ else
510
+ folders = file_parts[0..-2]
511
+ filename = file_parts.last
512
+ relative_path = folders.any? ? File.join(*folders, filename) : filename
513
+ "/reference/#{relative_path}"
514
+ end
515
+ end
516
+
517
+ def modules_with_children_for_links
518
+ @modules_with_children_for_links ||= begin
519
+ paths = type_path_lookup.values.map { |p| "Apiwork::#{p}" }.to_set
520
+ paths.select do |path|
521
+ paths.any? { |other| other != path && other.start_with?("#{path}::") }
522
+ end.to_set
523
+ end
524
+ end
525
+
526
+ def render_module(mod, order)
527
+ parts = []
528
+
529
+ parts << <<~FRONTMATTER
530
+ ---
531
+ order: #{order}
532
+ prev: false
533
+ next: false
534
+ ---
535
+ FRONTMATTER
536
+
537
+ parts << "# #{display_title(mod[:path])}\n"
538
+
539
+ if mod[:file] && mod[:line]
540
+ github_link = "#{GITHUB_URL}/#{mod[:file]}#L#{mod[:line]}"
541
+ parts << "[GitHub](#{github_link})\n"
542
+ end
543
+
544
+ parts << "#{linkify_yard_refs(mod[:docstring])}\n" if mod[:docstring].present?
545
+
546
+ render_examples(parts, mod[:examples]) if mod[:examples].any?
547
+
548
+ children = find_direct_children(mod[:path])
549
+ if children.any?
550
+ parts << "## Modules\n"
551
+ render_child_links(parts, mod[:path], children)
552
+ end
553
+
554
+ if mod[:class_methods].any?
555
+ parts << "## Class Methods\n"
556
+ mod[:class_methods].each do |method|
557
+ parts << render_method(method, '.')
558
+ end
559
+ end
560
+
561
+ if mod[:instance_methods].any?
562
+ parts << "## Instance Methods\n"
563
+ mod[:instance_methods].each do |method|
564
+ parts << render_method(method, '#')
565
+ end
566
+ end
567
+
568
+ parts.join("\n")
569
+ end
570
+
571
+ def render_method(method, prefix)
572
+ parts = []
573
+
574
+ parts << "### #{prefix}#{escape_brackets(method[:name])}\n"
575
+ parts << "`#{prefix}#{method[:signature]}`\n"
576
+
577
+ if method[:file] && method[:line]
578
+ github_link = "#{GITHUB_URL}/#{method[:file]}#L#{method[:line]}"
579
+ parts << "[GitHub](#{github_link})\n"
580
+ end
581
+
582
+ parts << "#{linkify_yard_refs(method[:docstring])}\n" if method[:docstring].present?
583
+
584
+ if method[:params].any?
585
+ parts << "**Parameters**\n"
586
+
587
+ parts << '<div class="params-table">'
588
+ parts << ''
589
+ parts << '| Name | Type | Default | Description |'
590
+ parts << '|------|------|---------|-------------|'
591
+
592
+ method[:params].each do |param|
593
+ name_cell = param[:default] ? "`#{param[:name]}`" : "**`#{param[:name]}`**"
594
+ row = [
595
+ name_cell,
596
+ format_param_type(param),
597
+ param[:default] ? "`#{param[:default]}`" : '',
598
+ linkify_yard_refs(param[:description])&.gsub(/\s*\n\s*/, ' ') || '',
599
+ ]
600
+ parts << "| #{row.join(' | ')} |"
601
+ end
602
+ parts << ''
603
+ parts << '</div>'
604
+ parts << ''
605
+ end
606
+
607
+ if method[:returns]
608
+ types = method[:returns][:types].map { |type| linkify_type(type) }.join(', ')
609
+ description = linkify_yard_refs(method[:returns][:description])
610
+ parts << "**Returns**\n"
611
+ parts << (description.blank? ? "#{types}\n" : "#{types} — #{description}\n")
612
+ end
613
+
614
+ if method[:yieldparams].any?
615
+ types = method[:yieldparams].flat_map { |p| p[:types] }.uniq
616
+ linked = types.map { |t| linkify_type(t) }.join(', ')
617
+ parts << "**Yields** #{linked}\n"
618
+ end
619
+
620
+ if method[:see].any?
621
+ linkable_sees = method[:see].select { |ref| see_ref_linkable?(ref) }
622
+ if linkable_sees.any?
623
+ parts << "**See also**\n"
624
+ linkable_sees.each do |ref|
625
+ link_path = yard_ref_to_path(ref)
626
+ parts << "- [#{ref}](#{link_path})"
627
+ end
628
+ parts << ''
629
+ end
630
+ end
631
+
632
+ render_examples(parts, method[:examples]) if method[:examples].any?
633
+
634
+ parts << "---\n"
635
+ parts.join("\n")
636
+ end
637
+ end
638
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ class Registry
5
+ class << self
6
+ def store
7
+ @store ||= {}
8
+ end
9
+
10
+ def find(key)
11
+ store[normalize_key(key)]
12
+ end
13
+
14
+ def find!(key)
15
+ normalized_key = normalize_key(key)
16
+ store.fetch(normalized_key) do
17
+ raise KeyError.new(
18
+ "#{registry_name} :#{normalized_key} not found. Available: #{keys.join(', ')}",
19
+ key: normalized_key,
20
+ receiver: store,
21
+ )
22
+ end
23
+ end
24
+
25
+ def exists?(key)
26
+ store.key?(normalize_key(key))
27
+ end
28
+
29
+ def keys
30
+ store.keys
31
+ end
32
+
33
+ def values
34
+ store.values
35
+ end
36
+
37
+ def delete(key)
38
+ store.delete(normalize_key(key))
39
+ end
40
+
41
+ def clear!
42
+ @store = {}
43
+ end
44
+
45
+ private
46
+
47
+ def normalize_key(key)
48
+ key.to_sym
49
+ end
50
+
51
+ def registry_name
52
+ name.demodulize
53
+ end
54
+ end
55
+ end
56
+ end