apiwork 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +8 -0
- data/lib/apiwork/adapter/base.rb +46 -17
- data/lib/apiwork/contract/action/response.rb +0 -3
- data/lib/apiwork/export/open_api.rb +10 -42
- data/lib/apiwork/export/type_script_mapper.rb +31 -22
- data/lib/apiwork/export/zod_mapper.rb +28 -18
- data/lib/apiwork/introspection/dump/action.rb +1 -1
- data/lib/apiwork/introspection/dump/param.rb +1 -36
- data/lib/apiwork/version.rb +1 -1
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ef169b6db5b913f078945dbf9b29939139cd80d5d77e07c0339e9df070dfb259
|
|
4
|
+
data.tar.gz: b979dc9b7b721f5949952a22572fa8ea99593f59f1c5161a91fc8757b28205d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 46548e8a016ade99fecc507ac9cb7d575b21278d4981e2ea5ebed9ddbdb81b2ff270669c4acca0f0cd6ce5e7437c9b14f62d51eff650492160e136f950a040fe
|
|
7
|
+
data.tar.gz: 8aabaa2bd3d750d425429ba4f47d034a07c1562c3ca5f90405d2c1d17e1f159d499e64bd906667bf9f6fe7dd3ee0640863dcb797a264ba985fcee6753f74925b
|
data/README.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Apiwork
|
|
2
2
|
|
|
3
|
+
[](https://rubygems.org/gems/apiwork)
|
|
4
|
+
[](https://github.com/skiftle/apiwork/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE.txt)
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+
|
|
3
11
|
Typed APIs for Rails.
|
|
4
12
|
|
|
5
13
|
Apiwork lets you define your API once and derive validation, serialization, querying, and typed exports from the same definition.
|
data/lib/apiwork/adapter/base.rb
CHANGED
|
@@ -270,15 +270,17 @@ module Apiwork
|
|
|
270
270
|
else
|
|
271
271
|
build_custom_action_response(contract_class, representation_class, action, contract_action)
|
|
272
272
|
end
|
|
273
|
+
|
|
274
|
+
build_request_query_type(contract_class, action.name, contract_action)
|
|
275
|
+
build_request_body_type(contract_class, action.name, contract_action)
|
|
273
276
|
end
|
|
274
277
|
|
|
275
278
|
def build_member_action_response(contract_class, representation_class, action, contract_action)
|
|
276
|
-
|
|
279
|
+
build_response_body_type(contract_class, representation_class, action.name, :member)
|
|
277
280
|
member_shape_class = self.class.member_wrapper.shape_class
|
|
278
281
|
data_type = resolve_resource_data_type(representation_class)
|
|
279
282
|
|
|
280
283
|
contract_action.response do |response|
|
|
281
|
-
response.result_wrapper = result_wrapper
|
|
282
284
|
response.body do |body|
|
|
283
285
|
member_shape_class.apply(body, representation_class.root_key, capabilities, representation_class, :member, data_type:)
|
|
284
286
|
end
|
|
@@ -286,12 +288,11 @@ module Apiwork
|
|
|
286
288
|
end
|
|
287
289
|
|
|
288
290
|
def build_collection_action_response(contract_class, representation_class, action, contract_action)
|
|
289
|
-
|
|
291
|
+
build_response_body_type(contract_class, representation_class, action.name, :collection)
|
|
290
292
|
collection_shape_class = self.class.collection_wrapper.shape_class
|
|
291
293
|
data_type = resolve_resource_data_type(representation_class)
|
|
292
294
|
|
|
293
295
|
contract_action.response do |response|
|
|
294
|
-
response.result_wrapper = result_wrapper
|
|
295
296
|
response.body do |body|
|
|
296
297
|
collection_shape_class.apply(body, representation_class.root_key, capabilities, representation_class, :collection, data_type:)
|
|
297
298
|
end
|
|
@@ -308,23 +309,51 @@ module Apiwork
|
|
|
308
309
|
end
|
|
309
310
|
end
|
|
310
311
|
|
|
311
|
-
def
|
|
312
|
-
|
|
312
|
+
def build_response_body_type(contract_class, representation_class, action_name, response_type)
|
|
313
|
+
type_name = [action_name, 'response_body'].join('_').to_sym
|
|
314
|
+
return if contract_class.type?(type_name)
|
|
313
315
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
data_type = resolve_resource_data_type(representation_class)
|
|
316
|
+
shape_class = if response_type == :collection
|
|
317
|
+
self.class.collection_wrapper.shape_class
|
|
318
|
+
else
|
|
319
|
+
self.class.member_wrapper.shape_class
|
|
320
|
+
end
|
|
321
|
+
data_type = resolve_resource_data_type(representation_class)
|
|
321
322
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
323
|
+
contract_class.object(type_name) do |object|
|
|
324
|
+
shape_class.apply(object, representation_class.root_key, capabilities, representation_class, response_type, data_type:)
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def build_request_query_type(contract_class, action_name, contract_action)
|
|
329
|
+
request = contract_action.request
|
|
330
|
+
return unless request.query.params.any?
|
|
331
|
+
|
|
332
|
+
type_name = [action_name, 'request_query'].join('_').to_sym
|
|
333
|
+
return if contract_class.type?(type_name)
|
|
334
|
+
|
|
335
|
+
contract_class.object(type_name) do |object|
|
|
336
|
+
request.query.params.each { |name, param| object.param(name, **normalize_request_param(param)) }
|
|
325
337
|
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def build_request_body_type(contract_class, action_name, contract_action)
|
|
341
|
+
request = contract_action.request
|
|
342
|
+
return unless request.body.params.any?
|
|
343
|
+
|
|
344
|
+
type_name = [action_name, 'request_body'].join('_').to_sym
|
|
345
|
+
return if contract_class.type?(type_name)
|
|
346
|
+
|
|
347
|
+
contract_class.object(type_name) do |object|
|
|
348
|
+
request.body.params.each { |name, param| object.param(name, **normalize_request_param(param)) }
|
|
349
|
+
end
|
|
350
|
+
end
|
|
326
351
|
|
|
327
|
-
|
|
352
|
+
def normalize_request_param(param)
|
|
353
|
+
options = param.except(:name, :custom_type, :union, :partial)
|
|
354
|
+
options[:type] = param[:custom_type] if param[:custom_type]
|
|
355
|
+
options[:shape] = param[:union] if param[:union]
|
|
356
|
+
options
|
|
328
357
|
end
|
|
329
358
|
|
|
330
359
|
def resolve_resource_data_type(representation_class)
|
|
@@ -11,13 +11,10 @@ module Apiwork
|
|
|
11
11
|
attr_reader :action_name,
|
|
12
12
|
:contract_class
|
|
13
13
|
|
|
14
|
-
attr_accessor :result_wrapper
|
|
15
|
-
|
|
16
14
|
def initialize(contract_class, action_name)
|
|
17
15
|
@contract_class = contract_class
|
|
18
16
|
@action_name = action_name
|
|
19
17
|
@body = nil
|
|
20
|
-
@result_wrapper = nil
|
|
21
18
|
@no_content = false
|
|
22
19
|
end
|
|
23
20
|
|
|
@@ -203,39 +203,18 @@ module Apiwork
|
|
|
203
203
|
if response.no_content?
|
|
204
204
|
responses[:'204'] = { description: 'No content' }
|
|
205
205
|
elsif response.body
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
error_variant = body.variants[1]
|
|
211
|
-
|
|
212
|
-
responses[:'200'] = {
|
|
213
|
-
content: {
|
|
214
|
-
'application/json': {
|
|
215
|
-
schema: map_param(success_variant),
|
|
216
|
-
},
|
|
217
|
-
},
|
|
218
|
-
description: 'Successful response',
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
raises.each do |code|
|
|
222
|
-
error_code = api.error_codes[code]
|
|
223
|
-
responses[error_code.status.to_s.to_sym] = build_union_error_response(error_code.description, error_variant)
|
|
224
|
-
end
|
|
225
|
-
else
|
|
226
|
-
responses[:'200'] = {
|
|
227
|
-
content: {
|
|
228
|
-
'application/json': {
|
|
229
|
-
schema: map_param(body),
|
|
230
|
-
},
|
|
206
|
+
responses[:'200'] = {
|
|
207
|
+
content: {
|
|
208
|
+
'application/json': {
|
|
209
|
+
schema: map_param(response.body),
|
|
231
210
|
},
|
|
232
|
-
|
|
233
|
-
|
|
211
|
+
},
|
|
212
|
+
description: 'Successful response',
|
|
213
|
+
}
|
|
234
214
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
end
|
|
215
|
+
raises.each do |code|
|
|
216
|
+
error_code = api.error_codes[code]
|
|
217
|
+
responses[error_code.status.to_s.to_sym] = build_error_response(error_code.description)
|
|
239
218
|
end
|
|
240
219
|
elsif response
|
|
241
220
|
responses[:'200'] = {
|
|
@@ -280,17 +259,6 @@ module Apiwork
|
|
|
280
259
|
}
|
|
281
260
|
end
|
|
282
261
|
|
|
283
|
-
def build_union_error_response(description, error_variant)
|
|
284
|
-
{
|
|
285
|
-
description:,
|
|
286
|
-
content: {
|
|
287
|
-
'application/json': {
|
|
288
|
-
schema: map_param(error_variant),
|
|
289
|
-
},
|
|
290
|
-
},
|
|
291
|
-
}
|
|
292
|
-
end
|
|
293
|
-
|
|
294
262
|
def surface
|
|
295
263
|
@surface ||= SurfaceResolver.resolve(api)
|
|
296
264
|
end
|
|
@@ -116,18 +116,25 @@ module Apiwork
|
|
|
116
116
|
"export type #{action_type_name(resource_name, action_name, 'ResponseBody', parent_identifiers:)} = #{map_param(response_body_definition)};"
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
-
def build_action_response_type(resource_name, action_name, response, parent_identifiers: [])
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
119
|
+
def build_action_response_type(resource_name, action_name, response, parent_identifiers: [], raises:)
|
|
120
|
+
type_name = action_type_name(resource_name, action_name, 'Response', parent_identifiers:)
|
|
121
|
+
|
|
122
|
+
success_variant = if response.no_content?
|
|
123
|
+
'{ status: 204 }'
|
|
124
|
+
else
|
|
125
|
+
body_ref = action_type_name(resource_name, action_name, 'ResponseBody', parent_identifiers:)
|
|
126
|
+
"{ status: 200; body: #{body_ref} }"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
error_statuses = resolve_error_statuses(raises)
|
|
130
|
+
|
|
131
|
+
if error_statuses.empty?
|
|
132
|
+
"export type #{type_name} = #{success_variant};"
|
|
133
|
+
else
|
|
134
|
+
error_variants = error_statuses.map { |status| "{ status: #{status}; body: #{pascal_case(:error_response_body)} }" }
|
|
135
|
+
all_variants = ([success_variant] + error_variants).map { |variant| " | #{variant}" }.join("\n")
|
|
136
|
+
"export type #{type_name} =\n#{all_variants};"
|
|
137
|
+
end
|
|
131
138
|
end
|
|
132
139
|
|
|
133
140
|
def action_type_name(resource_name, action_name, suffix, parent_identifiers: [])
|
|
@@ -253,6 +260,10 @@ module Apiwork
|
|
|
253
260
|
|
|
254
261
|
private
|
|
255
262
|
|
|
263
|
+
def resolve_error_statuses(raises)
|
|
264
|
+
raises.map { |code| @export.api.error_codes[code].status }.uniq.sort
|
|
265
|
+
end
|
|
266
|
+
|
|
256
267
|
def build_enum_types(surface)
|
|
257
268
|
surface.enums.map do |name, enum|
|
|
258
269
|
{ code: build_enum_type(name, enum), name: pascal_case(name) }
|
|
@@ -276,7 +287,7 @@ module Apiwork
|
|
|
276
287
|
|
|
277
288
|
resource.actions.each do |action_name, action|
|
|
278
289
|
types.concat(build_request_types(resource_name, action_name, action.request, parent_identifiers:))
|
|
279
|
-
types.concat(build_response_types(resource_name, action_name, action
|
|
290
|
+
types.concat(build_response_types(resource_name, action_name, action, parent_identifiers:))
|
|
280
291
|
end
|
|
281
292
|
end
|
|
282
293
|
|
|
@@ -306,22 +317,20 @@ module Apiwork
|
|
|
306
317
|
types
|
|
307
318
|
end
|
|
308
319
|
|
|
309
|
-
def build_response_types(resource_name, action_name,
|
|
320
|
+
def build_response_types(resource_name, action_name, action, parent_identifiers:)
|
|
310
321
|
types = []
|
|
322
|
+
response = action.response
|
|
311
323
|
|
|
312
|
-
if response.
|
|
313
|
-
type_name = action_type_name(resource_name, action_name, 'Response', parent_identifiers:)
|
|
314
|
-
types << { code: "export type #{type_name} = never;", name: type_name }
|
|
315
|
-
elsif response.body?
|
|
324
|
+
if response.body?
|
|
316
325
|
type_name = action_type_name(resource_name, action_name, 'ResponseBody', parent_identifiers:)
|
|
317
326
|
code = build_action_response_body_type(resource_name, action_name, response.body, parent_identifiers:)
|
|
318
327
|
types << { code:, name: type_name }
|
|
319
|
-
|
|
320
|
-
type_name = action_type_name(resource_name, action_name, 'Response', parent_identifiers:)
|
|
321
|
-
code = build_action_response_type(resource_name, action_name, { body: response.body }, parent_identifiers:)
|
|
322
|
-
types << { code:, name: type_name }
|
|
323
328
|
end
|
|
324
329
|
|
|
330
|
+
type_name = action_type_name(resource_name, action_name, 'Response', parent_identifiers:)
|
|
331
|
+
code = build_action_response_type(resource_name, action_name, response, parent_identifiers:, raises: action.raises)
|
|
332
|
+
types << { code:, name: type_name }
|
|
333
|
+
|
|
325
334
|
types
|
|
326
335
|
end
|
|
327
336
|
|
|
@@ -153,18 +153,27 @@ module Apiwork
|
|
|
153
153
|
"export const #{action_type_name(resource_name, action_name, 'ResponseBody', parent_identifiers:)}Schema = #{map_param(response_body)};"
|
|
154
154
|
end
|
|
155
155
|
|
|
156
|
-
def build_action_response_schema(resource_name, action_name, response, parent_identifiers: [])
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
156
|
+
def build_action_response_schema(resource_name, action_name, response, parent_identifiers: [], raises:)
|
|
157
|
+
schema_name = action_type_name(resource_name, action_name, 'Response', parent_identifiers:)
|
|
158
|
+
|
|
159
|
+
success_variant = if response.no_content?
|
|
160
|
+
'z.object({ status: z.literal(204) })'
|
|
161
|
+
else
|
|
162
|
+
body_ref = "#{action_type_name(resource_name, action_name, 'ResponseBody', parent_identifiers:)}Schema"
|
|
163
|
+
"z.object({ status: z.literal(200), body: #{body_ref} })"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
error_statuses = resolve_error_statuses(raises)
|
|
167
|
+
|
|
168
|
+
if error_statuses.empty?
|
|
169
|
+
"export const #{schema_name}Schema = #{success_variant};"
|
|
170
|
+
else
|
|
171
|
+
error_variants = error_statuses.map do |status|
|
|
172
|
+
"z.object({ status: z.literal(#{status}), body: #{pascal_case(:error_response_body)}Schema })"
|
|
173
|
+
end
|
|
174
|
+
all_variants = ([success_variant] + error_variants).map { |variant| " #{variant}" }.join(",\n")
|
|
175
|
+
"export const #{schema_name}Schema = z.discriminatedUnion('status', [\n#{all_variants}\n]);"
|
|
176
|
+
end
|
|
168
177
|
end
|
|
169
178
|
|
|
170
179
|
def action_type_name(resource_name, action_name, suffix, parent_identifiers: [])
|
|
@@ -367,12 +376,9 @@ module Apiwork
|
|
|
367
376
|
end
|
|
368
377
|
|
|
369
378
|
response = action.response
|
|
370
|
-
if response.
|
|
371
|
-
|
|
372
|
-
|
|
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
|
|
379
|
+
schemas << build_action_response_body_schema(resource_name, action_name, response.body, parent_identifiers:) if response.body?
|
|
380
|
+
|
|
381
|
+
schemas << build_action_response_schema(resource_name, action_name, response, parent_identifiers:, raises: action.raises)
|
|
376
382
|
end
|
|
377
383
|
end
|
|
378
384
|
|
|
@@ -399,6 +405,10 @@ module Apiwork
|
|
|
399
405
|
referenced_type.shape.key?(discriminator)
|
|
400
406
|
end
|
|
401
407
|
|
|
408
|
+
def resolve_error_statuses(raises)
|
|
409
|
+
raises.map { |code| @export.api.error_codes[code].status }.uniq.sort
|
|
410
|
+
end
|
|
411
|
+
|
|
402
412
|
def resolve_enum_schema(param)
|
|
403
413
|
return nil unless param.scalar? && param.enum?
|
|
404
414
|
|
|
@@ -56,7 +56,7 @@ module Apiwork
|
|
|
56
56
|
body_shape = response.body
|
|
57
57
|
return { body: {}, no_content: false } unless body_shape
|
|
58
58
|
|
|
59
|
-
{ body: Param.new(body_shape
|
|
59
|
+
{ body: Param.new(body_shape).to_h, no_content: false }
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
def raises
|
|
@@ -4,9 +4,8 @@ module Apiwork
|
|
|
4
4
|
module Introspection
|
|
5
5
|
module Dump
|
|
6
6
|
class Param
|
|
7
|
-
def initialize(contract_param,
|
|
7
|
+
def initialize(contract_param, visited: Set.new)
|
|
8
8
|
@contract_param = contract_param
|
|
9
|
-
@result_wrapper = result_wrapper
|
|
10
9
|
@visited = visited
|
|
11
10
|
@import_prefix_cache = {}
|
|
12
11
|
end
|
|
@@ -14,8 +13,6 @@ module Apiwork
|
|
|
14
13
|
def to_h
|
|
15
14
|
return nil unless @contract_param
|
|
16
15
|
|
|
17
|
-
return build_result_wrapped if @result_wrapper
|
|
18
|
-
|
|
19
16
|
result = {}
|
|
20
17
|
|
|
21
18
|
@contract_param.params.sort_by { |name, _options| name.to_s }.each do |name, param_options|
|
|
@@ -31,38 +28,6 @@ module Apiwork
|
|
|
31
28
|
|
|
32
29
|
private
|
|
33
30
|
|
|
34
|
-
def build_result_wrapped
|
|
35
|
-
success_type = @result_wrapper[:success_type]
|
|
36
|
-
error_type = @result_wrapper[:error_type]
|
|
37
|
-
|
|
38
|
-
success_variant = if success_type
|
|
39
|
-
{ reference: success_type, type: :reference }
|
|
40
|
-
else
|
|
41
|
-
{ reference: nil, shape: build_success_params, type: :object }
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
error_variant = if error_type
|
|
45
|
-
{ reference: error_type, type: :reference }
|
|
46
|
-
else
|
|
47
|
-
{ reference: nil, shape: {}, type: :object }
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
{
|
|
51
|
-
type: :union,
|
|
52
|
-
variants: [success_variant, error_variant],
|
|
53
|
-
}
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def build_success_params
|
|
57
|
-
success_params = {}
|
|
58
|
-
@contract_param.params.sort_by { |name, _options| name.to_s }.each do |name, param_options|
|
|
59
|
-
dumped = build_param(name, param_options)
|
|
60
|
-
dumped[:optional] = true if param_options[:optional]
|
|
61
|
-
success_params[name] = dumped
|
|
62
|
-
end
|
|
63
|
-
success_params
|
|
64
|
-
end
|
|
65
|
-
|
|
66
31
|
def build_param(name, options)
|
|
67
32
|
return build_union_param(options) if options[:type] == :union
|
|
68
33
|
return build_custom_type_param(options) if options[:custom_type]
|
data/lib/apiwork/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: apiwork
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- skiftle
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-02-
|
|
11
|
+
date: 2026-02-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -122,7 +122,7 @@ dependencies:
|
|
|
122
122
|
- - "~>"
|
|
123
123
|
- !ruby/object:Gem::Version
|
|
124
124
|
version: '0.9'
|
|
125
|
-
description:
|
|
125
|
+
description: Define your API once — and generate everything from it
|
|
126
126
|
email:
|
|
127
127
|
executables: []
|
|
128
128
|
extensions: []
|
|
@@ -330,7 +330,9 @@ homepage: https://apiwork.dev
|
|
|
330
330
|
licenses:
|
|
331
331
|
- MIT
|
|
332
332
|
metadata:
|
|
333
|
+
bug_tracker_uri: https://github.com/skiftle/apiwork/issues
|
|
333
334
|
changelog_uri: https://github.com/skiftle/apiwork/blob/main/CHANGELOG.md
|
|
335
|
+
documentation_uri: https://apiwork.dev/guide/introduction
|
|
334
336
|
homepage_uri: https://apiwork.dev
|
|
335
337
|
rubygems_mfa_required: 'true'
|
|
336
338
|
source_code_uri: https://github.com/skiftle/apiwork
|
|
@@ -352,5 +354,5 @@ requirements: []
|
|
|
352
354
|
rubygems_version: 3.4.19
|
|
353
355
|
signing_key:
|
|
354
356
|
specification_version: 4
|
|
355
|
-
summary:
|
|
357
|
+
summary: Typed APIs for Rails
|
|
356
358
|
test_files: []
|