apiwork 0.0.0.pre → 0.1.1

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 +622 -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,421 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Export
5
+ class ZodMapper
6
+ TYPE_MAP = {
7
+ binary: 'z.string()',
8
+ boolean: 'z.boolean()',
9
+ date: 'z.iso.date()',
10
+ datetime: 'z.iso.datetime()',
11
+ decimal: 'z.number()',
12
+ integer: 'z.number().int()',
13
+ number: 'z.number()',
14
+ string: 'z.string()',
15
+ time: 'z.iso.time()',
16
+ unknown: 'z.unknown()',
17
+ uuid: 'z.uuid()',
18
+ }.freeze
19
+
20
+ class << self
21
+ def map(export, surface)
22
+ new(export).map(surface)
23
+ end
24
+ end
25
+
26
+ def initialize(export)
27
+ @export = export
28
+ end
29
+
30
+ def map(surface)
31
+ @surface = surface
32
+ parts = []
33
+
34
+ enum_schemas = build_enum_schemas
35
+ parts << enum_schemas if enum_schemas.present?
36
+
37
+ type_schemas = build_type_schemas
38
+ parts << type_schemas if type_schemas.present?
39
+
40
+ action_schemas = build_action_schemas
41
+ parts << action_schemas if action_schemas.present?
42
+
43
+ parts.join("\n\n")
44
+ end
45
+
46
+ def build_object_schema(type_name, type, recursive: false)
47
+ schema_name = pascal_case(type_name)
48
+
49
+ properties = type.shape.sort_by { |name, _param| name.to_s }.map do |name, param|
50
+ key = @export.transform_key(name)
51
+ zod_type = map_field(param)
52
+ " #{key}: #{zod_type}"
53
+ end.join(",\n")
54
+
55
+ type_annotation = recursive ? ": z.ZodType<#{schema_name}>" : ''
56
+
57
+ if recursive
58
+ "export const #{schema_name}Schema#{type_annotation} = z.lazy(() => z.object({\n#{properties}\n}));"
59
+ elsif type.extends?
60
+ build_object_schema_code(schema_name, properties, type.extends, type_annotation:)
61
+ else
62
+ "export const #{schema_name}Schema#{type_annotation} = z.object({\n#{properties}\n});"
63
+ end
64
+ end
65
+
66
+ def build_object_schema_code(schema_name, properties, extends, type_annotation: '')
67
+ base_schemas = extends.map { |type| "#{pascal_case(type)}Schema" }
68
+
69
+ base_chain = if base_schemas.size == 1
70
+ base_schemas.first
71
+ else
72
+ first, *rest = base_schemas
73
+ rest.reduce(first) { |acc, schema| "#{acc}.merge(#{schema})" }
74
+ end
75
+
76
+ if properties.empty?
77
+ "export const #{schema_name}Schema#{type_annotation} = #{base_chain};"
78
+ else
79
+ "export const #{schema_name}Schema#{type_annotation} = #{base_chain}.extend({\n#{properties}\n});"
80
+ end
81
+ end
82
+
83
+ def build_union_schema(type_name, type, recursive: false)
84
+ schema_name = pascal_case(type_name)
85
+
86
+ variant_schemas = type.variants.map do |variant|
87
+ base_schema = map_param(variant)
88
+
89
+ if type.discriminator && variant.tag && !reference_contains_discriminator?(variant, type.discriminator)
90
+ "#{base_schema}.extend({ #{@export.transform_key(type.discriminator)}: z.literal('#{variant.tag}') })"
91
+ else
92
+ base_schema
93
+ end
94
+ end
95
+
96
+ union_body = variant_schemas.map { |schema| " #{schema}" }.join(",\n")
97
+
98
+ type_annotation = recursive ? ": z.ZodType<#{schema_name}>" : ''
99
+
100
+ union_code = if type.discriminator
101
+ "z.discriminatedUnion('#{@export.transform_key(type.discriminator)}', [\n#{union_body}\n])"
102
+ else
103
+ "z.union([\n#{union_body}\n])"
104
+ end
105
+
106
+ if recursive
107
+ "export const #{schema_name}Schema#{type_annotation} = z.lazy(() => #{union_code});"
108
+ else
109
+ "export const #{schema_name}Schema#{type_annotation} = #{union_code};"
110
+ end
111
+ end
112
+
113
+ def build_action_request_query_schema(resource_name, action_name, query_params, parent_identifiers: [])
114
+ properties = query_params.sort_by { |name, _param| name.to_s }.map do |param_name, param|
115
+ key = @export.transform_key(param_name)
116
+ zod_type = map_field(param)
117
+ " #{key}: #{zod_type}"
118
+ end.join(",\n")
119
+
120
+ "export const #{action_type_name(resource_name, action_name, 'RequestQuery', parent_identifiers:)}Schema = z.object({\n#{properties}\n});"
121
+ end
122
+
123
+ def build_action_request_body_schema(resource_name, action_name, body_params, parent_identifiers: [])
124
+ properties = body_params.sort_by { |name, _param| name.to_s }.map do |param_name, param|
125
+ key = @export.transform_key(param_name)
126
+ zod_type = map_field(param)
127
+ " #{key}: #{zod_type}"
128
+ end.join(",\n")
129
+
130
+ "export const #{action_type_name(resource_name, action_name, 'RequestBody', parent_identifiers:)}Schema = z.object({\n#{properties}\n});"
131
+ end
132
+
133
+ def build_action_request_schema(resource_name, action_name, request, parent_identifiers: [])
134
+ nested_properties = []
135
+
136
+ if request[:query].any?
137
+ nested_properties << " query: #{action_type_name(resource_name, action_name, 'RequestQuery', parent_identifiers:)}Schema"
138
+ end
139
+
140
+ if request[:body].any?
141
+ nested_properties << " body: #{action_type_name(resource_name, action_name, 'RequestBody', parent_identifiers:)}Schema"
142
+ end
143
+
144
+ "export const #{action_type_name(
145
+ resource_name,
146
+ action_name,
147
+ 'Request',
148
+ parent_identifiers:,
149
+ )}Schema = z.object({\n#{nested_properties.join(",\n")}\n});"
150
+ end
151
+
152
+ def build_action_response_body_schema(resource_name, action_name, response_body, parent_identifiers: [])
153
+ "export const #{action_type_name(resource_name, action_name, 'ResponseBody', parent_identifiers:)}Schema = #{map_param(response_body)};"
154
+ end
155
+
156
+ def build_action_response_schema(resource_name, action_name, response, parent_identifiers: [])
157
+ "export const #{action_type_name(
158
+ resource_name,
159
+ action_name,
160
+ 'Response',
161
+ parent_identifiers:,
162
+ )}Schema = z.object({\n body: #{action_type_name(
163
+ resource_name,
164
+ action_name,
165
+ 'ResponseBody',
166
+ parent_identifiers:,
167
+ )}Schema\n});"
168
+ end
169
+
170
+ def action_type_name(resource_name, action_name, suffix, parent_identifiers: [])
171
+ "#{pascal_case((parent_identifiers + [resource_name.to_s, action_name.to_s]).join('_'))}#{suffix.split(/(?=[A-Z])/).map(&:capitalize).join}"
172
+ end
173
+
174
+ def map_field(param, force_optional: nil)
175
+ if param.reference? && type_or_enum_reference?(param.reference)
176
+ schema_name = pascal_case(param.reference)
177
+ type = "#{schema_name}Schema"
178
+ return apply_modifiers(type, param, force_optional:)
179
+ end
180
+
181
+ type = map_param(param)
182
+ type = resolve_enum_schema(param) || type
183
+
184
+ apply_modifiers(type, param, force_optional:)
185
+ end
186
+
187
+ def map_param(param)
188
+ if param.object?
189
+ map_object_type(param)
190
+ elsif param.array?
191
+ map_array_type(param)
192
+ elsif param.union?
193
+ map_union_type(param)
194
+ elsif param.literal?
195
+ map_literal_type(param)
196
+ elsif param.reference? && type_or_enum_reference?(param.reference)
197
+ resolve_enum_schema(param) || schema_reference(param.reference)
198
+ else
199
+ resolve_enum_schema(param) || map_primitive(param)
200
+ end
201
+ end
202
+
203
+ def map_object_type(param)
204
+ return 'z.record(z.string(), z.unknown())' if param.shape.empty?
205
+
206
+ partial = param.partial?
207
+
208
+ properties = param.shape.sort_by { |name, _field| name.to_s }.map do |name, field|
209
+ key = @export.transform_key(name)
210
+ zod_type = if partial
211
+ map_field(field, force_optional: false)
212
+ else
213
+ map_field(field)
214
+ end
215
+ "#{key}: #{zod_type}"
216
+ end.join(', ')
217
+
218
+ base_object = "z.object({ #{properties} })"
219
+ partial ? "#{base_object}.partial()" : base_object
220
+ end
221
+
222
+ def map_array_type(param)
223
+ items_type = param.of
224
+
225
+ if items_type.nil? && param.shape.any?
226
+ items_schema = map_object_type(param)
227
+ base = "z.array(#{items_schema})"
228
+ elsif items_type
229
+ items_schema = map_param(items_type)
230
+ base = "z.array(#{items_schema})"
231
+ else
232
+ base = 'z.array(z.unknown())'
233
+ end
234
+
235
+ base += ".min(#{param.min})" unless param.min.nil?
236
+ base += ".max(#{param.max})" unless param.max.nil?
237
+ base
238
+ end
239
+
240
+ def map_union_type(param)
241
+ if param.discriminator
242
+ map_discriminated_union(param)
243
+ else
244
+ variants = param.variants.map { |variant| map_param(variant) }
245
+ "z.union([#{variants.join(', ')}])"
246
+ end
247
+ end
248
+
249
+ def map_discriminated_union(param)
250
+ discriminator_field = @export.transform_key(param.discriminator)
251
+
252
+ variant_schemas = param.variants.map { |variant| map_param(variant) }
253
+
254
+ "z.discriminatedUnion('#{discriminator_field}', [#{variant_schemas.join(', ')}])"
255
+ end
256
+
257
+ def map_literal_type(param)
258
+ case param.value
259
+ when nil then 'z.null()'
260
+ when String then "z.literal('#{param.value}')"
261
+ when Numeric, TrueClass, FalseClass then "z.literal(#{param.value})"
262
+ else "z.literal('#{param.value}')"
263
+ end
264
+ end
265
+
266
+ def map_primitive(param)
267
+ return 'z.unknown()' if param.unknown?
268
+
269
+ format = param.format&.to_sym if param.formattable?
270
+
271
+ base_type = if format
272
+ map_format_to_zod(format)
273
+ else
274
+ TYPE_MAP[param.type.to_sym] || 'z.unknown()'
275
+ end
276
+
277
+ if param.boundable?
278
+ base_type += ".min(#{param.min})" unless param.min.nil?
279
+ base_type += ".max(#{param.max})" unless param.max.nil?
280
+ end
281
+
282
+ base_type
283
+ end
284
+
285
+ def map_format_to_zod(format)
286
+ case format
287
+ when :email then 'z.email()'
288
+ when :uuid then 'z.uuid()'
289
+ when :url then 'z.url()'
290
+ when :ipv4 then 'z.ipv4()'
291
+ when :ipv6 then 'z.ipv6()'
292
+ when :date then 'z.iso.date()'
293
+ when :datetime then 'z.iso.datetime()'
294
+ when :password, :hostname then 'z.string()'
295
+ when :int32, :int64 then 'z.number().int()'
296
+ when :float, :double then 'z.number()'
297
+ else 'z.string()'
298
+ end
299
+ end
300
+
301
+ def schema_reference(symbol)
302
+ "#{pascal_case(symbol)}Schema"
303
+ end
304
+
305
+ def pascal_case(name)
306
+ name.to_s.camelize(:upper)
307
+ end
308
+
309
+ private
310
+
311
+ def build_enum_schemas
312
+ return '' if @surface.enums.empty?
313
+
314
+ @surface.enums.map do |name, enum|
315
+ "export const #{pascal_case(name)}Schema = z.enum([#{enum.values.sort.map { |value| "'#{value}'" }.join(', ')}]);"
316
+ end.join("\n\n")
317
+ end
318
+
319
+ def build_type_schemas
320
+ types_hash = @surface.types.transform_values(&:to_h)
321
+ lazy_types = TypeAnalysis.cycle_breaking_types(types_hash)
322
+
323
+ TypeAnalysis.topological_sort_types(types_hash).map(&:first).map do |type_name|
324
+ type = @surface.types[type_name]
325
+ recursive = lazy_types.include?(type_name)
326
+
327
+ if type.union?
328
+ build_union_schema(type_name, type, recursive:)
329
+ else
330
+ build_object_schema(type_name, type, recursive:)
331
+ end
332
+ end.join("\n\n")
333
+ end
334
+
335
+ def build_action_schemas
336
+ schemas = []
337
+
338
+ traverse_resources do |resource|
339
+ resource_name = resource.identifier.to_sym
340
+ parent_identifiers = resource.parent_identifiers
341
+
342
+ resource.actions.each do |action_name, action|
343
+ request = action.request
344
+ if request && (request.query? || request.body?)
345
+ if request.query?
346
+ schemas << build_action_request_query_schema(
347
+ resource_name,
348
+ action_name,
349
+ request.query,
350
+ parent_identifiers:,
351
+ )
352
+ end
353
+ if request.body?
354
+ schemas << build_action_request_body_schema(
355
+ resource_name,
356
+ action_name,
357
+ request.body,
358
+ parent_identifiers:,
359
+ )
360
+ end
361
+ schemas << build_action_request_schema(
362
+ resource_name,
363
+ action_name,
364
+ { body: request.body, query: request.query },
365
+ parent_identifiers:,
366
+ )
367
+ end
368
+
369
+ response = action.response
370
+ if response.no_content?
371
+ schemas << "export const #{action_type_name(resource_name, action_name, 'Response', parent_identifiers:)}Schema = z.never();"
372
+ elsif response.body?
373
+ schemas << build_action_response_body_schema(resource_name, action_name, response.body, parent_identifiers:)
374
+ schemas << build_action_response_schema(resource_name, action_name, { body: response.body }, parent_identifiers:)
375
+ end
376
+ end
377
+ end
378
+
379
+ schemas.join("\n\n")
380
+ end
381
+
382
+ def traverse_resources(resources: @export.api.resources, &block)
383
+ resources.each_value do |resource|
384
+ yield(resource)
385
+ traverse_resources(resources: resource.resources, &block) if resource.resources.any?
386
+ end
387
+ end
388
+
389
+ def type_or_enum_reference?(symbol)
390
+ @export.api.types.key?(symbol) || @export.api.enums.key?(symbol)
391
+ end
392
+
393
+ def reference_contains_discriminator?(variant, discriminator)
394
+ return false unless variant.reference?
395
+
396
+ referenced_type = @export.api.types[variant.reference]
397
+ return false unless referenced_type
398
+
399
+ referenced_type.shape.key?(discriminator)
400
+ end
401
+
402
+ def resolve_enum_schema(param)
403
+ return nil unless param.scalar? && param.enum?
404
+
405
+ if param.enum_reference? && @export.api.enums.key?(param.enum)
406
+ "#{pascal_case(param.enum)}Schema"
407
+ else
408
+ enum_literal = param.enum.map { |value| "'#{value}'" }.join(', ')
409
+ "z.enum([#{enum_literal}])"
410
+ end
411
+ end
412
+
413
+ def apply_modifiers(type, param, force_optional: nil)
414
+ type += '.nullable()' if param.nullable?
415
+ optional = force_optional.nil? ? param.optional? : force_optional
416
+ type += '.optional()' if optional
417
+ type
418
+ end
419
+ end
420
+ end
421
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ # @api public
5
+ module Export
6
+ class << self
7
+ # @!method find(name)
8
+ # @api public
9
+ # Finds an export by name.
10
+ # @param name [Symbol]
11
+ # The export name.
12
+ # @return [Class<Export::Base>, nil]
13
+ # @see .find!
14
+ # @example
15
+ # Apiwork::Export.find(:openapi)
16
+ #
17
+ # @!method find!(name)
18
+ # @api public
19
+ # Finds an export by name.
20
+ # @param name [Symbol]
21
+ # The export name.
22
+ # @return [Class<Export::Base>]
23
+ # @raise [KeyError] if the export is not found
24
+ # @see .find
25
+ # @example
26
+ # Apiwork::Export.find!(:openapi)
27
+ #
28
+ # @!method register(klass)
29
+ # @api public
30
+ # Registers an export.
31
+ # @param klass [Class<Export::Base>]
32
+ # The export class with export_name set.
33
+ # @see Export::Base
34
+ # @example
35
+ # Apiwork::Export.register(JSONSchemaExport)
36
+ delegate :clear!,
37
+ :exists?,
38
+ :find,
39
+ :find!,
40
+ :keys,
41
+ :register,
42
+ :values,
43
+ to: Registry
44
+
45
+ # @api public
46
+ # Generates an export for an API.
47
+ #
48
+ # @param export_name [Symbol]
49
+ # The registered export name. Built-in: `:openapi`, `:typescript`, `:zod`.
50
+ # @param api_base_path [String]
51
+ # The API base path.
52
+ # @param format [Symbol, nil] (nil) [:json, :yaml]
53
+ # The output format. Hash exports only.
54
+ # @param key_format [Symbol, nil] (nil) [:camel, :kebab, :keep, :pascal, :underscore]
55
+ # The key format.
56
+ # @param locale [Symbol, nil] (nil)
57
+ # The locale for translations.
58
+ # @param options
59
+ # Export-specific keyword arguments.
60
+ # @return [String]
61
+ # @raise [ConfigurationError] if export is not declared for the API
62
+ # @see Export::Base
63
+ #
64
+ # @example
65
+ # Apiwork::Export.generate(:openapi, '/api/v1')
66
+ # Apiwork::Export.generate(:openapi, '/api/v1', format: :yaml)
67
+ # Apiwork::Export.generate(:typescript, '/api/v1', key_format: :camel)
68
+ def generate(export_name, api_base_path, format: nil, key_format: nil, locale: nil, **options)
69
+ export_class = find!(export_name)
70
+ export_class.generate(api_base_path, format:, key_format:, locale:, **options)
71
+ end
72
+
73
+ def register_defaults!
74
+ register(OpenAPI)
75
+ register(TypeScript)
76
+ register(Zod)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ class HttpError < ConstraintError
5
+ attr_reader :error_code
6
+
7
+ def initialize(issues, error_code)
8
+ @error_code = error_code
9
+ super(issues)
10
+ end
11
+
12
+ def layer
13
+ :http
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Introspection
5
+ class Action
6
+ # @api public
7
+ # Wraps action request definitions.
8
+ #
9
+ # Contains query parameters and/or body parameters.
10
+ #
11
+ # @example
12
+ # request = action.request
13
+ # request.query? # => true
14
+ # request.body? # => false
15
+ # request.query[:page] # => Param for page param
16
+ class Request
17
+ def initialize(dump)
18
+ @dump = dump
19
+ end
20
+
21
+ # @api public
22
+ # The query for this request.
23
+ #
24
+ # @return [Hash{Symbol => Param}]
25
+ def query
26
+ @query ||= @dump[:query].transform_values { |dump| Param.build(dump) }
27
+ end
28
+
29
+ # @api public
30
+ # The body for this request.
31
+ #
32
+ # @return [Hash{Symbol => Param}]
33
+ def body
34
+ @body ||= @dump[:body].transform_values { |dump| Param.build(dump) }
35
+ end
36
+
37
+ # @api public
38
+ # Whether this request has query parameters.
39
+ #
40
+ # @return [Boolean]
41
+ def query?
42
+ query.any?
43
+ end
44
+
45
+ # @api public
46
+ # Whether this request has a body.
47
+ #
48
+ # @return [Boolean]
49
+ def body?
50
+ body.any?
51
+ end
52
+
53
+ # @api public
54
+ # Converts this request to a hash.
55
+ #
56
+ # @return [Hash]
57
+ def to_h
58
+ {
59
+ body: body.transform_values(&:to_h),
60
+ query: query.transform_values(&:to_h),
61
+ }
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Introspection
5
+ class Action
6
+ # @api public
7
+ # Wraps action response definitions.
8
+ #
9
+ # @example Response with body
10
+ # response = action.response
11
+ # response.body? # => true
12
+ # response.no_content? # => false
13
+ # response.body # => Param for response body
14
+ #
15
+ # @example No content response
16
+ # response.no_content? # => true
17
+ # response.body? # => false
18
+ class Response
19
+ def initialize(dump)
20
+ @dump = dump
21
+ end
22
+
23
+ # @api public
24
+ # The body for this response.
25
+ #
26
+ # @return [Param, nil]
27
+ def body
28
+ @body ||= @dump[:body] ? Param.build(@dump[:body]) : nil
29
+ end
30
+
31
+ # @api public
32
+ # Whether this response has no content.
33
+ #
34
+ # @return [Boolean]
35
+ def no_content?
36
+ @dump[:no_content]
37
+ end
38
+
39
+ # @api public
40
+ # Whether this response has a body.
41
+ #
42
+ # @return [Boolean]
43
+ def body?
44
+ body.present?
45
+ end
46
+
47
+ # @api public
48
+ # Converts this response to a hash.
49
+ #
50
+ # @return [Hash]
51
+ def to_h
52
+ { body: body&.to_h, no_content: no_content? }
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end