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,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Export
5
+ class Pipeline
6
+ class << self
7
+ def generate(export_name, api_base_path, format: nil, key_format: nil, locale: nil, version: nil)
8
+ Export.generate(export_name, api_base_path, format:, key_format:, locale:, version:)
9
+ end
10
+
11
+ def write(api_base_path: nil, export_name: nil, format: nil, key_format: nil, locale: nil, output:, version: nil)
12
+ raise ArgumentError, 'output path required' unless output
13
+
14
+ if Writer.file_path?(output) && (api_base_path.nil? || export_name.nil?)
15
+ raise ArgumentError,
16
+ 'api_base_path and export_name required when output is a file'
17
+ end
18
+
19
+ api_classes = api_base_path ? [find_api_class(api_base_path)] : API.values
20
+
21
+ start_time = Time.zone.now
22
+ count = 0
23
+
24
+ Rails.logger.debug 'Generating artifacts...'
25
+
26
+ api_classes.each do |api_class|
27
+ available_exports = export_name ? [export_name.to_sym] : api_class.export_configs.keys
28
+
29
+ available_exports.each do |name|
30
+ count += generate_file(
31
+ api_class:,
32
+ format:,
33
+ key_format:,
34
+ locale:,
35
+ output:,
36
+ version:,
37
+ export_name: name,
38
+ )
39
+ end
40
+ end
41
+
42
+ Rails.logger.debug "\nGenerated #{count} file#{count == 1 ? '' : 's'} in #{(Time.zone.now - start_time).round(2)}s"
43
+
44
+ count
45
+ end
46
+
47
+ def clean(output:)
48
+ raise ArgumentError, 'output path required' unless output
49
+
50
+ Writer.clean(output:)
51
+ end
52
+
53
+ private
54
+
55
+ def find_api_class(api_base_path)
56
+ API.find!(api_base_path)
57
+ end
58
+
59
+ def generate_file(api_class:, export_name:, format:, key_format:, locale:, output:, version:)
60
+ api_base_path = api_class.base_path
61
+
62
+ options = { format:, key_format:, locale:, version: }.compact
63
+ options_label = options.any? ? " (#{options.map { |key, value| "#{key}: #{value}" }.join(', ')})" : ''
64
+ Rails.logger.debug " ✓ #{api_base_path} to #{export_name}#{options_label}"
65
+
66
+ content = generate(export_name, api_base_path, format:, key_format:, locale:, version:)
67
+ export_class = Registry.find!(export_name)
68
+ extension = export_class.file_extension_for(format:)
69
+
70
+ file_path = Writer.write(
71
+ api_base_path:,
72
+ content:,
73
+ export_name:,
74
+ extension:,
75
+ output:,
76
+ )
77
+
78
+ Rails.logger.debug " to #{file_path}"
79
+ 1
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Export
5
+ class Registry < Apiwork::Registry
6
+ class << self
7
+ def register(export_class)
8
+ raise ArgumentError, 'Export must inherit from Apiwork::Export::Base' unless export_class < Base
9
+ raise ArgumentError, "Export #{export_class} must define an export_name" unless export_class.export_name
10
+
11
+ store[export_class.export_name] = export_class
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Export
5
+ class SurfaceResolver
6
+ class << self
7
+ def resolve(api)
8
+ new(api)
9
+ end
10
+ end
11
+
12
+ def initialize(api)
13
+ @api = api
14
+ end
15
+
16
+ def types
17
+ @types ||= compute_reachable_types
18
+ end
19
+
20
+ def enums
21
+ @enums ||= compute_reachable_enums
22
+ end
23
+
24
+ private
25
+
26
+ def compute_reachable_types
27
+ type_names = collect_type_names_from_actions
28
+ expand_transitive_dependencies(type_names)
29
+ @api.types.select { |name, _| type_names.include?(name) }
30
+ end
31
+
32
+ def compute_reachable_enums
33
+ enum_names = collect_enum_names_from_actions
34
+ collect_enum_names_from_types(types, enum_names)
35
+ @api.enums.select { |name, _| enum_names.include?(name) }
36
+ end
37
+
38
+ def collect_type_names_from_actions
39
+ type_names = Set.new
40
+ @api.resources.each_value do |resource|
41
+ collect_types_from_resource(resource, type_names)
42
+ end
43
+ type_names
44
+ end
45
+
46
+ def collect_types_from_resource(resource, type_names)
47
+ resource.actions.each_value do |action|
48
+ collect_types_from_action(action, type_names)
49
+ end
50
+ resource.resources.each_value do |nested|
51
+ collect_types_from_resource(nested, type_names)
52
+ end
53
+ end
54
+
55
+ def collect_types_from_action(action, type_names)
56
+ action.request.query.each_value { |param| collect_types_from_param(param, type_names) }
57
+ action.request.body.each_value { |param| collect_types_from_param(param, type_names) }
58
+ collect_types_from_param(action.response.body, type_names) if action.response.body
59
+ end
60
+
61
+ def collect_types_from_param(param, type_names)
62
+ type_names << param.reference if param.reference?
63
+
64
+ param.shape.each_value { |nested| collect_types_from_param(nested, type_names) } if param.object?
65
+
66
+ if param.array?
67
+ collect_types_from_param(param.of, type_names) if param.of
68
+ param.shape.each_value { |nested| collect_types_from_param(nested, type_names) }
69
+ end
70
+
71
+ return unless param.union?
72
+
73
+ param.variants.each { |variant| collect_types_from_param(variant, type_names) }
74
+ end
75
+
76
+ def expand_transitive_dependencies(type_names)
77
+ added = true
78
+
79
+ while added
80
+ added = false
81
+ type_names.dup.each do |type_name|
82
+ type = @api.types[type_name]
83
+ next unless type
84
+
85
+ collect_reference_names_from_type(type).each do |reference_name|
86
+ next unless @api.types.key?(reference_name)
87
+ next if type_names.include?(reference_name)
88
+
89
+ type_names << reference_name
90
+ added = true
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ def collect_reference_names_from_type(type)
97
+ reference_names = []
98
+
99
+ if type.object?
100
+ reference_names.concat(type.extends) if type.extends?
101
+ type.shape.each_value { |param| collect_reference_names_from_param(param, reference_names) }
102
+ elsif type.union?
103
+ type.variants.each { |param| collect_reference_names_from_param(param, reference_names) }
104
+ end
105
+
106
+ reference_names.uniq
107
+ end
108
+
109
+ def collect_reference_names_from_param(param, reference_names)
110
+ reference_names << param.reference if param.reference?
111
+
112
+ param.shape.each_value { |nested| collect_reference_names_from_param(nested, reference_names) } if param.object?
113
+
114
+ if param.array?
115
+ collect_reference_names_from_param(param.of, reference_names) if param.of
116
+ param.shape.each_value { |nested| collect_reference_names_from_param(nested, reference_names) }
117
+ end
118
+
119
+ param.variants.each { |variant| collect_reference_names_from_param(variant, reference_names) } if param.union?
120
+ end
121
+
122
+ def collect_enum_names_from_actions
123
+ enum_names = Set.new
124
+ @api.resources.each_value do |resource|
125
+ collect_enums_from_resource(resource, enum_names)
126
+ end
127
+ enum_names
128
+ end
129
+
130
+ def collect_enums_from_resource(resource, enum_names)
131
+ resource.actions.each_value do |action|
132
+ collect_enums_from_action(action, enum_names)
133
+ end
134
+ resource.resources.each_value do |nested|
135
+ collect_enums_from_resource(nested, enum_names)
136
+ end
137
+ end
138
+
139
+ def collect_enums_from_action(action, enum_names)
140
+ action.request.query.each_value { |param| collect_enums_from_param(param, enum_names) }
141
+ action.request.body.each_value { |param| collect_enums_from_param(param, enum_names) }
142
+ collect_enums_from_param(action.response.body, enum_names) if action.response.body
143
+ end
144
+
145
+ def collect_enums_from_param(param, enum_names)
146
+ enum_names << param.enum if param.enum_reference?
147
+
148
+ param.shape.each_value { |nested| collect_enums_from_param(nested, enum_names) } if param.object?
149
+
150
+ if param.array?
151
+ collect_enums_from_param(param.of, enum_names) if param.of
152
+ param.shape.each_value { |nested| collect_enums_from_param(nested, enum_names) }
153
+ end
154
+
155
+ return unless param.union?
156
+
157
+ param.variants.each { |variant| collect_enums_from_param(variant, enum_names) }
158
+ end
159
+
160
+ def collect_enum_names_from_types(resolved_types, enum_names)
161
+ resolved_types.each_value do |type|
162
+ collect_enums_from_type(type, enum_names)
163
+ end
164
+ end
165
+
166
+ def collect_enums_from_type(type, enum_names)
167
+ if type.object?
168
+ type.shape.each_value { |param| collect_enums_from_type_param(param, enum_names) }
169
+ elsif type.union?
170
+ type.variants.each { |param| collect_enums_from_type_param(param, enum_names) }
171
+ end
172
+ end
173
+
174
+ def collect_enums_from_type_param(param, enum_names)
175
+ enum_names << param.enum if param.enum_reference?
176
+ enum_names << param.reference if param.reference? && @api.enums.key?(param.reference)
177
+
178
+ param.shape.each_value { |nested| collect_enums_from_type_param(nested, enum_names) } if param.object?
179
+
180
+ if param.array?
181
+ collect_enums_from_type_param(param.of, enum_names) if param.of
182
+ param.shape.each_value { |nested| collect_enums_from_type_param(nested, enum_names) }
183
+ end
184
+
185
+ param.variants.each { |variant| collect_enums_from_type_param(variant, enum_names) } if param.union?
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Export
5
+ class TypeAnalysis
6
+ PRIMITIVE_TYPES = %i[
7
+ string integer boolean datetime date uuid object array
8
+ decimal float literal union enum text binary json number time
9
+ unknown
10
+ ].to_set.freeze
11
+
12
+ class << self
13
+ def topological_sort_types(all_types)
14
+ graph = build_dependency_graph(all_types)
15
+ lazy_types = find_cycle_breaking_types(graph)
16
+ sort_with_lazy_types(all_types, graph, lazy_types)
17
+ end
18
+
19
+ def cycle_breaking_types(all_types)
20
+ find_cycle_breaking_types(build_dependency_graph(all_types))
21
+ end
22
+
23
+ def type_references(definition, filter: :custom_only)
24
+ references = []
25
+ collect_references(definition, references, filter)
26
+ references.uniq
27
+ end
28
+
29
+ def primitive_type?(type)
30
+ PRIMITIVE_TYPES.include?(type)
31
+ end
32
+
33
+ private
34
+
35
+ def build_dependency_graph(all_types)
36
+ type_keys = all_types.keys
37
+ all_types.transform_values { |shape| type_references(shape, filter: type_keys) }
38
+ end
39
+
40
+ def find_cycle_breaking_types(graph)
41
+ lazy_types = Set.new
42
+
43
+ find_strongly_connected_components(graph).each do |component|
44
+ next if component.size == 1 && !graph[component.first].include?(component.first)
45
+
46
+ lazy_types.add(component.min_by(&:to_s))
47
+ end
48
+
49
+ lazy_types
50
+ end
51
+
52
+ def sort_with_lazy_types(all_types, graph, lazy_types)
53
+ dependencies = build_non_lazy_dependencies(all_types, graph, lazy_types)
54
+ sorted = lazy_types.to_a.sort_by(&:to_s)
55
+ remaining = dependencies.keys.sort_by(&:to_s)
56
+
57
+ until remaining.empty?
58
+ ready = remaining.select { |type_name| dependencies[type_name].empty? }
59
+ break if ready.empty?
60
+
61
+ ready.sort_by(&:to_s).each do |type_name|
62
+ sorted << type_name
63
+ remaining.delete(type_name)
64
+ dependencies.each_value { |type_dependencies| type_dependencies.delete(type_name) }
65
+ end
66
+ end
67
+
68
+ sorted.concat(remaining.sort_by(&:to_s))
69
+ sorted.map { |type_name| [type_name, all_types[type_name]] }
70
+ end
71
+
72
+ def build_non_lazy_dependencies(all_types, graph, lazy_types)
73
+ all_types.each_key.with_object({}) do |type_name, dependencies|
74
+ next if lazy_types.include?(type_name)
75
+
76
+ dependencies[type_name] = (graph[type_name] - lazy_types.to_a - [type_name]).to_set
77
+ end
78
+ end
79
+
80
+ def find_strongly_connected_components(graph)
81
+ state = { components: [], index: 0, indices: {}, lowlinks: {}, on_stack: Set.new, stack: [] }
82
+
83
+ graph.each_key do |node|
84
+ tarjan_visit(node, graph, state) unless state[:indices][node]
85
+ end
86
+
87
+ state[:components]
88
+ end
89
+
90
+ def tarjan_visit(node, graph, state)
91
+ state[:indices][node] = state[:lowlinks][node] = state[:index]
92
+ state[:index] += 1
93
+ state[:stack].push(node)
94
+ state[:on_stack].add(node)
95
+
96
+ (graph[node] || []).each do |successor|
97
+ if state[:indices][successor].nil?
98
+ tarjan_visit(successor, graph, state)
99
+ state[:lowlinks][node] = [state[:lowlinks][node], state[:lowlinks][successor]].min
100
+ elsif state[:on_stack].include?(successor)
101
+ state[:lowlinks][node] = [state[:lowlinks][node], state[:indices][successor]].min
102
+ end
103
+ end
104
+
105
+ return unless state[:lowlinks][node] == state[:indices][node]
106
+
107
+ component = []
108
+ loop do
109
+ successor = state[:stack].pop
110
+ state[:on_stack].delete(successor)
111
+ component << successor
112
+ break if successor == node
113
+ end
114
+ state[:components] << component
115
+ end
116
+
117
+ def collect_references(node, references, filter)
118
+ return unless node.is_a?(Hash)
119
+
120
+ extract_type_field(node, references, filter)
121
+ extract_of_field(node[:of], references, filter)
122
+ extract_extends_field(node[:extends], references, filter)
123
+
124
+ node[:variants]&.each { |variant| collect_references(variant, references, filter) }
125
+ node[:shape]&.each_value { |param| collect_references(param, references, filter) }
126
+ end
127
+
128
+ def extract_type_field(node, references, filter)
129
+ type_value = node[:type]
130
+ return unless type_value
131
+
132
+ type_to_add = [:reference, 'reference'].include?(type_value) ? node[:reference] : type_value
133
+ add_if_matches_filter(references, type_to_add, filter)
134
+ end
135
+
136
+ def extract_of_field(of, references, filter)
137
+ return unless of
138
+
139
+ if of.is_a?(Hash)
140
+ collect_references(of, references, filter)
141
+ else
142
+ add_if_matches_filter(references, of, filter)
143
+ end
144
+ end
145
+
146
+ def extract_extends_field(extends, references, filter)
147
+ extends&.each do |extended_type|
148
+ if extended_type.is_a?(Hash)
149
+ collect_references(extended_type, references, filter)
150
+ else
151
+ add_if_matches_filter(references, extended_type, filter)
152
+ end
153
+ end
154
+ end
155
+
156
+ def add_if_matches_filter(references, type_symbol, filter)
157
+ type_symbol = type_symbol.to_sym if type_symbol.is_a?(String)
158
+ return unless type_symbol.is_a?(Symbol)
159
+
160
+ case filter
161
+ when :custom_only
162
+ references << type_symbol unless primitive_type?(type_symbol)
163
+ when Array
164
+ references << type_symbol if filter.include?(type_symbol)
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Export
5
+ class TypeScript < Base
6
+ export_name :typescript
7
+ output :string
8
+ file_extension '.ts'
9
+
10
+ option :version, default: '5', enum: %w[4 5], type: :string
11
+
12
+ def generate
13
+ TypeScriptMapper.map(self, surface)
14
+ end
15
+
16
+ private
17
+
18
+ def surface
19
+ @surface ||= SurfaceResolver.resolve(api)
20
+ end
21
+ end
22
+ end
23
+ end