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.
- checksums.yaml +4 -4
- data/LICENSE.txt +2 -2
- data/README.md +117 -1
- data/Rakefile +5 -3
- data/app/controllers/apiwork/errors_controller.rb +13 -0
- data/app/controllers/apiwork/exports_controller.rb +22 -0
- data/lib/apiwork/abstractable.rb +26 -0
- data/lib/apiwork/adapter/base.rb +369 -0
- data/lib/apiwork/adapter/builder/api/base.rb +66 -0
- data/lib/apiwork/adapter/builder/contract/base.rb +86 -0
- data/lib/apiwork/adapter/capability/api/base.rb +51 -0
- data/lib/apiwork/adapter/capability/api/scope.rb +64 -0
- data/lib/apiwork/adapter/capability/base.rb +291 -0
- data/lib/apiwork/adapter/capability/contract/base.rb +37 -0
- data/lib/apiwork/adapter/capability/contract/scope.rb +110 -0
- data/lib/apiwork/adapter/capability/operation/base.rb +172 -0
- data/lib/apiwork/adapter/capability/operation/metadata_shape.rb +165 -0
- data/lib/apiwork/adapter/capability/result.rb +21 -0
- data/lib/apiwork/adapter/capability/runner.rb +56 -0
- data/lib/apiwork/adapter/capability/transformer/request/base.rb +72 -0
- data/lib/apiwork/adapter/capability/transformer/response/base.rb +45 -0
- data/lib/apiwork/adapter/registry.rb +16 -0
- data/lib/apiwork/adapter/serializer/error/base.rb +72 -0
- data/lib/apiwork/adapter/serializer/error/default/api_builder.rb +32 -0
- data/lib/apiwork/adapter/serializer/error/default.rb +37 -0
- data/lib/apiwork/adapter/serializer/resource/base.rb +84 -0
- data/lib/apiwork/adapter/serializer/resource/default/contract_builder.rb +209 -0
- data/lib/apiwork/adapter/serializer/resource/default.rb +39 -0
- data/lib/apiwork/adapter/standard/capability/filtering/api_builder.rb +75 -0
- data/lib/apiwork/adapter/standard/capability/filtering/constants.rb +37 -0
- data/lib/apiwork/adapter/standard/capability/filtering/contract_builder.rb +193 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation/filter/builder.rb +47 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation/filter/operator_builder.rb +36 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation/filter.rb +462 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation.rb +22 -0
- data/lib/apiwork/adapter/standard/capability/filtering/request_transformer.rb +47 -0
- data/lib/apiwork/adapter/standard/capability/filtering.rb +18 -0
- data/lib/apiwork/adapter/standard/capability/including/contract_builder.rb +169 -0
- data/lib/apiwork/adapter/standard/capability/including/operation.rb +20 -0
- data/lib/apiwork/adapter/standard/capability/including.rb +16 -0
- data/lib/apiwork/adapter/standard/capability/pagination/api_builder.rb +34 -0
- data/lib/apiwork/adapter/standard/capability/pagination/contract_builder.rb +35 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate/cursor.rb +84 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate/offset.rb +66 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate.rb +24 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation.rb +24 -0
- data/lib/apiwork/adapter/standard/capability/pagination.rb +21 -0
- data/lib/apiwork/adapter/standard/capability/sorting/api_builder.rb +19 -0
- data/lib/apiwork/adapter/standard/capability/sorting/contract_builder.rb +84 -0
- data/lib/apiwork/adapter/standard/capability/sorting/operation/sort.rb +83 -0
- data/lib/apiwork/adapter/standard/capability/sorting/operation.rb +22 -0
- data/lib/apiwork/adapter/standard/capability/sorting.rb +17 -0
- data/lib/apiwork/adapter/standard/capability/writing/constants.rb +15 -0
- data/lib/apiwork/adapter/standard/capability/writing/contract_builder.rb +253 -0
- data/lib/apiwork/adapter/standard/capability/writing/operation/issue_mapper.rb +210 -0
- data/lib/apiwork/adapter/standard/capability/writing/operation.rb +32 -0
- data/lib/apiwork/adapter/standard/capability/writing/request_transformer.rb +37 -0
- data/lib/apiwork/adapter/standard/capability/writing.rb +17 -0
- data/lib/apiwork/adapter/standard/includes_resolver.rb +106 -0
- data/lib/apiwork/adapter/standard.rb +22 -0
- data/lib/apiwork/adapter/wrapper/base.rb +70 -0
- data/lib/apiwork/adapter/wrapper/collection/base.rb +60 -0
- data/lib/apiwork/adapter/wrapper/collection/default.rb +47 -0
- data/lib/apiwork/adapter/wrapper/error/base.rb +30 -0
- data/lib/apiwork/adapter/wrapper/error/default.rb +34 -0
- data/lib/apiwork/adapter/wrapper/member/base.rb +58 -0
- data/lib/apiwork/adapter/wrapper/member/default.rb +40 -0
- data/lib/apiwork/adapter/wrapper/shape.rb +203 -0
- data/lib/apiwork/adapter.rb +50 -0
- data/lib/apiwork/api/base.rb +802 -0
- data/lib/apiwork/api/element.rb +110 -0
- data/lib/apiwork/api/enum_registry/definition.rb +51 -0
- data/lib/apiwork/api/enum_registry.rb +98 -0
- data/lib/apiwork/api/info/contact.rb +67 -0
- data/lib/apiwork/api/info/license.rb +50 -0
- data/lib/apiwork/api/info/server.rb +50 -0
- data/lib/apiwork/api/info.rb +221 -0
- data/lib/apiwork/api/object.rb +235 -0
- data/lib/apiwork/api/registry.rb +33 -0
- data/lib/apiwork/api/representation_registry.rb +76 -0
- data/lib/apiwork/api/resource/action.rb +41 -0
- data/lib/apiwork/api/resource.rb +648 -0
- data/lib/apiwork/api/router.rb +104 -0
- data/lib/apiwork/api/type_registry/definition.rb +117 -0
- data/lib/apiwork/api/type_registry.rb +99 -0
- data/lib/apiwork/api/union.rb +49 -0
- data/lib/apiwork/api.rb +85 -0
- data/lib/apiwork/configurable.rb +71 -0
- data/lib/apiwork/configuration/option.rb +125 -0
- data/lib/apiwork/configuration/validatable.rb +25 -0
- data/lib/apiwork/configuration.rb +95 -0
- data/lib/apiwork/configuration_error.rb +6 -0
- data/lib/apiwork/constraint_error.rb +20 -0
- data/lib/apiwork/contract/action/request.rb +79 -0
- data/lib/apiwork/contract/action/response.rb +87 -0
- data/lib/apiwork/contract/action.rb +258 -0
- data/lib/apiwork/contract/base.rb +714 -0
- data/lib/apiwork/contract/element.rb +130 -0
- data/lib/apiwork/contract/object/coercer.rb +194 -0
- data/lib/apiwork/contract/object/deserializer.rb +101 -0
- data/lib/apiwork/contract/object/transformer.rb +95 -0
- data/lib/apiwork/contract/object/validator/result.rb +27 -0
- data/lib/apiwork/contract/object/validator.rb +734 -0
- data/lib/apiwork/contract/object.rb +566 -0
- data/lib/apiwork/contract/request_parser/result.rb +25 -0
- data/lib/apiwork/contract/request_parser.rb +72 -0
- data/lib/apiwork/contract/response_parser/result.rb +25 -0
- data/lib/apiwork/contract/response_parser.rb +35 -0
- data/lib/apiwork/contract/union.rb +56 -0
- data/lib/apiwork/contract_error.rb +9 -0
- data/lib/apiwork/controller.rb +300 -0
- data/lib/apiwork/domain_error.rb +13 -0
- data/lib/apiwork/element.rb +386 -0
- data/lib/apiwork/engine.rb +20 -0
- data/lib/apiwork/error.rb +6 -0
- data/lib/apiwork/error_code/definition.rb +63 -0
- data/lib/apiwork/error_code/registry.rb +18 -0
- data/lib/apiwork/error_code.rb +132 -0
- data/lib/apiwork/export/base.rb +291 -0
- data/lib/apiwork/export/open_api.rb +600 -0
- data/lib/apiwork/export/pipeline/writer.rb +66 -0
- data/lib/apiwork/export/pipeline.rb +84 -0
- data/lib/apiwork/export/registry.rb +16 -0
- data/lib/apiwork/export/surface_resolver.rb +189 -0
- data/lib/apiwork/export/type_analysis.rb +170 -0
- data/lib/apiwork/export/type_script.rb +23 -0
- data/lib/apiwork/export/type_script_mapper.rb +349 -0
- data/lib/apiwork/export/zod.rb +39 -0
- data/lib/apiwork/export/zod_mapper.rb +421 -0
- data/lib/apiwork/export.rb +80 -0
- data/lib/apiwork/http_error.rb +16 -0
- data/lib/apiwork/introspection/action/request.rb +66 -0
- data/lib/apiwork/introspection/action/response.rb +57 -0
- data/lib/apiwork/introspection/action.rb +124 -0
- data/lib/apiwork/introspection/api/info/contact.rb +59 -0
- data/lib/apiwork/introspection/api/info/license.rb +49 -0
- data/lib/apiwork/introspection/api/info/server.rb +50 -0
- data/lib/apiwork/introspection/api/info.rb +107 -0
- data/lib/apiwork/introspection/api/resource.rb +83 -0
- data/lib/apiwork/introspection/api.rb +92 -0
- data/lib/apiwork/introspection/contract.rb +63 -0
- data/lib/apiwork/introspection/dump/action.rb +101 -0
- data/lib/apiwork/introspection/dump/api.rb +119 -0
- data/lib/apiwork/introspection/dump/contract.rb +129 -0
- data/lib/apiwork/introspection/dump/param.rb +486 -0
- data/lib/apiwork/introspection/dump/resource.rb +112 -0
- data/lib/apiwork/introspection/dump/type.rb +339 -0
- data/lib/apiwork/introspection/dump.rb +17 -0
- data/lib/apiwork/introspection/enum.rb +63 -0
- data/lib/apiwork/introspection/error_code.rb +44 -0
- data/lib/apiwork/introspection/param/array.rb +88 -0
- data/lib/apiwork/introspection/param/base.rb +285 -0
- data/lib/apiwork/introspection/param/binary.rb +73 -0
- data/lib/apiwork/introspection/param/boolean.rb +73 -0
- data/lib/apiwork/introspection/param/date.rb +73 -0
- data/lib/apiwork/introspection/param/date_time.rb +73 -0
- data/lib/apiwork/introspection/param/decimal.rb +121 -0
- data/lib/apiwork/introspection/param/integer.rb +131 -0
- data/lib/apiwork/introspection/param/literal.rb +45 -0
- data/lib/apiwork/introspection/param/number.rb +121 -0
- data/lib/apiwork/introspection/param/object.rb +59 -0
- data/lib/apiwork/introspection/param/reference.rb +45 -0
- data/lib/apiwork/introspection/param/string.rb +122 -0
- data/lib/apiwork/introspection/param/time.rb +73 -0
- data/lib/apiwork/introspection/param/union.rb +57 -0
- data/lib/apiwork/introspection/param/unknown.rb +26 -0
- data/lib/apiwork/introspection/param/uuid.rb +73 -0
- data/lib/apiwork/introspection/param.rb +31 -0
- data/lib/apiwork/introspection/type.rb +129 -0
- data/lib/apiwork/introspection.rb +28 -0
- data/lib/apiwork/issue.rb +80 -0
- data/lib/apiwork/json_pointer.rb +21 -0
- data/lib/apiwork/object.rb +1618 -0
- data/lib/apiwork/reference_generator.rb +622 -0
- data/lib/apiwork/registry.rb +56 -0
- data/lib/apiwork/representation/association.rb +391 -0
- data/lib/apiwork/representation/attribute.rb +335 -0
- data/lib/apiwork/representation/base.rb +819 -0
- data/lib/apiwork/representation/deserializer.rb +95 -0
- data/lib/apiwork/representation/element.rb +128 -0
- data/lib/apiwork/representation/inheritance.rb +78 -0
- data/lib/apiwork/representation/model_detector.rb +75 -0
- data/lib/apiwork/representation/root_key.rb +35 -0
- data/lib/apiwork/representation/serializer.rb +127 -0
- data/lib/apiwork/request.rb +79 -0
- data/lib/apiwork/response.rb +56 -0
- data/lib/apiwork/union.rb +102 -0
- data/lib/apiwork/version.rb +2 -2
- data/lib/apiwork.rb +61 -3
- data/lib/generators/apiwork/api_generator.rb +38 -0
- data/lib/generators/apiwork/contract_generator.rb +25 -0
- data/lib/generators/apiwork/install_generator.rb +27 -0
- data/lib/generators/apiwork/representation_generator.rb +25 -0
- data/lib/generators/apiwork/templates/api/api.rb.tt +4 -0
- data/lib/generators/apiwork/templates/contract/contract.rb.tt +6 -0
- data/lib/generators/apiwork/templates/install/application_contract.rb.tt +5 -0
- data/lib/generators/apiwork/templates/install/application_representation.rb.tt +5 -0
- data/lib/generators/apiwork/templates/representation/representation.rb.tt +6 -0
- data/lib/tasks/apiwork.rake +102 -0
- metadata +319 -19
- data/.rubocop.yml +0 -8
- 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,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
|