apiwork 0.6.1 → 0.7.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/lib/apiwork/adapter/serializer/error/default/api_builder.rb +6 -1
- data/lib/apiwork/adapter/standard/capability/writing/contract_builder.rb +1 -0
- data/lib/apiwork/api/base.rb +31 -0
- data/lib/apiwork/api/object.rb +12 -12
- data/lib/apiwork/contract/object.rb +74 -74
- data/lib/apiwork/controller.rb +6 -2
- data/lib/apiwork/export/apiwork.rb +20 -0
- data/lib/apiwork/export/apiwork_mapper.rb +232 -0
- data/lib/apiwork/export/open_api.rb +2 -0
- data/lib/apiwork/export/registry.rb +1 -1
- data/lib/apiwork/export/sorbus.rb +1 -5
- data/lib/apiwork/export/type_script.rb +1 -4
- data/lib/apiwork/export/type_script_mapper.rb +12 -4
- data/lib/apiwork/export/zod.rb +0 -9
- data/lib/apiwork/export/zod_mapper.rb +22 -1
- data/lib/apiwork/export.rb +1 -0
- data/lib/apiwork/introspection/api/resource.rb +9 -0
- data/lib/apiwork/introspection/dump/param.rb +9 -10
- data/lib/apiwork/introspection/dump/resource.rb +3 -1
- data/lib/apiwork/introspection/dump/type.rb +15 -4
- data/lib/apiwork/introspection/enum.rb +9 -0
- data/lib/apiwork/introspection/param/array.rb +3 -0
- data/lib/apiwork/introspection/param/base.rb +11 -0
- data/lib/apiwork/introspection/param/binary.rb +3 -0
- data/lib/apiwork/introspection/param/boolean.rb +3 -0
- data/lib/apiwork/introspection/param/date.rb +3 -0
- data/lib/apiwork/introspection/param/date_time.rb +3 -0
- data/lib/apiwork/introspection/param/decimal.rb +3 -0
- data/lib/apiwork/introspection/param/integer.rb +3 -0
- data/lib/apiwork/introspection/param/number.rb +3 -0
- data/lib/apiwork/introspection/param/record.rb +3 -0
- data/lib/apiwork/introspection/param/string.rb +3 -0
- data/lib/apiwork/introspection/param/time.rb +3 -0
- data/lib/apiwork/introspection/param/uuid.rb +3 -0
- data/lib/apiwork/introspection/type.rb +9 -0
- data/lib/apiwork/issue.rb +1 -1
- data/lib/apiwork/object.rb +99 -99
- data/lib/apiwork/reference_generator.rb +43 -0
- data/lib/apiwork/representation/attribute.rb +86 -1
- data/lib/apiwork/representation/base.rb +4 -0
- data/lib/apiwork/version.rb +1 -1
- data/lib/apiwork.rb +4 -0
- metadata +4 -3
- data/lib/apiwork/export/builder_mapper.rb +0 -184
|
@@ -21,6 +21,14 @@ module Apiwork
|
|
|
21
21
|
string: %i[date datetime email hostname ipv4 ipv6 password text url uuid],
|
|
22
22
|
}.freeze
|
|
23
23
|
|
|
24
|
+
# @!attribute [r] default
|
|
25
|
+
# @api public
|
|
26
|
+
# The default for this attribute.
|
|
27
|
+
#
|
|
28
|
+
# Returns `nil` for both "no default" and "default is explicitly `nil`".
|
|
29
|
+
# Use {#default?} to distinguish these cases.
|
|
30
|
+
#
|
|
31
|
+
# @return [Object, nil]
|
|
24
32
|
# @!attribute [r] description
|
|
25
33
|
# @api public
|
|
26
34
|
# The description for this attribute.
|
|
@@ -71,7 +79,8 @@ module Apiwork
|
|
|
71
79
|
# The type for this attribute.
|
|
72
80
|
#
|
|
73
81
|
# @return [Symbol]
|
|
74
|
-
attr_reader :
|
|
82
|
+
attr_reader :default,
|
|
83
|
+
:description,
|
|
75
84
|
:element,
|
|
76
85
|
:empty,
|
|
77
86
|
:enum,
|
|
@@ -90,6 +99,7 @@ module Apiwork
|
|
|
90
99
|
name,
|
|
91
100
|
owner_representation_class,
|
|
92
101
|
decode: nil,
|
|
102
|
+
default: UNSET,
|
|
93
103
|
deprecated: false,
|
|
94
104
|
description: nil,
|
|
95
105
|
empty: false,
|
|
@@ -134,6 +144,24 @@ module Apiwork
|
|
|
134
144
|
type = :string if detected_enum && type == :integer
|
|
135
145
|
optional = detect_optional(name) if optional.nil?
|
|
136
146
|
nullable = detect_nullable(name) if nullable.nil?
|
|
147
|
+
default = detect_default(name, empty:, nullable:, optional:) if UNSET.equal?(default)
|
|
148
|
+
|
|
149
|
+
if @db_column && type == :string
|
|
150
|
+
detected_max = detect_string_max_length(name)
|
|
151
|
+
max = [max, detected_max].compact.min
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
if @db_column && type == :decimal
|
|
155
|
+
detected_min, detected_max = detect_decimal_bounds(name)
|
|
156
|
+
min = [min, detected_min].compact.max
|
|
157
|
+
max = [max, detected_max].compact.min
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
if @db_column && type == :integer
|
|
161
|
+
detected_min, detected_max = detect_integer_bounds(name)
|
|
162
|
+
min = [min, detected_min].compact.max
|
|
163
|
+
max = [max, detected_max].compact.min
|
|
164
|
+
end
|
|
137
165
|
rescue ActiveRecord::StatementInvalid, ActiveRecord::NoDatabaseError, ActiveRecord::ConnectionNotEstablished
|
|
138
166
|
@db_column = false
|
|
139
167
|
end
|
|
@@ -141,6 +169,7 @@ module Apiwork
|
|
|
141
169
|
|
|
142
170
|
optional = false if optional.nil?
|
|
143
171
|
nullable = false if nullable.nil?
|
|
172
|
+
default = '' if UNSET.equal?(default) && empty
|
|
144
173
|
|
|
145
174
|
@filterable = filterable
|
|
146
175
|
@preload = preload
|
|
@@ -160,12 +189,25 @@ module Apiwork
|
|
|
160
189
|
@format = format
|
|
161
190
|
@deprecated = deprecated
|
|
162
191
|
@write_only = write_only
|
|
192
|
+
@default_set = !UNSET.equal?(default)
|
|
193
|
+
@default = @default_set ? default : nil
|
|
163
194
|
|
|
164
195
|
validate_min_max_range!
|
|
165
196
|
validate_format!
|
|
166
197
|
validate_empty!
|
|
167
198
|
end
|
|
168
199
|
|
|
200
|
+
# @api public
|
|
201
|
+
# Whether this attribute has a default value.
|
|
202
|
+
#
|
|
203
|
+
# Use this to distinguish "no default" from "default is explicitly `nil`".
|
|
204
|
+
# The {#default} accessor returns `nil` in both cases.
|
|
205
|
+
#
|
|
206
|
+
# @return [Boolean]
|
|
207
|
+
def default?
|
|
208
|
+
@default_set
|
|
209
|
+
end
|
|
210
|
+
|
|
169
211
|
# @api public
|
|
170
212
|
# Whether this attribute is deprecated.
|
|
171
213
|
#
|
|
@@ -276,6 +318,32 @@ module Apiwork
|
|
|
276
318
|
end
|
|
277
319
|
end
|
|
278
320
|
|
|
321
|
+
def detect_string_max_length(name)
|
|
322
|
+
column = column_for(name)
|
|
323
|
+
return nil unless column
|
|
324
|
+
|
|
325
|
+
column.limit
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def detect_decimal_bounds(name)
|
|
329
|
+
column = column_for(name)
|
|
330
|
+
return [nil, nil] unless column
|
|
331
|
+
return [nil, nil] unless column.precision
|
|
332
|
+
|
|
333
|
+
scale = column.scale || 0
|
|
334
|
+
max = (10**(column.precision - scale) - 10.0**(-scale)).to_f
|
|
335
|
+
[-max, max]
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def detect_integer_bounds(name)
|
|
339
|
+
column = column_for(name)
|
|
340
|
+
return [nil, nil] unless column
|
|
341
|
+
|
|
342
|
+
limit = column.limit || 4
|
|
343
|
+
max = 2**(8 * limit - 1) - 1
|
|
344
|
+
[-max - 1, max]
|
|
345
|
+
end
|
|
346
|
+
|
|
279
347
|
def detect_optional(name)
|
|
280
348
|
return false unless @model_class
|
|
281
349
|
return false unless db_column?
|
|
@@ -288,6 +356,23 @@ module Apiwork
|
|
|
288
356
|
column.null
|
|
289
357
|
end
|
|
290
358
|
|
|
359
|
+
def detect_default(name, empty:, nullable:, optional:)
|
|
360
|
+
return UNSET unless @model_class
|
|
361
|
+
return UNSET unless db_column?
|
|
362
|
+
|
|
363
|
+
column = column_for(name)
|
|
364
|
+
return UNSET unless column
|
|
365
|
+
return UNSET if column.default_function
|
|
366
|
+
|
|
367
|
+
default = @model_class.column_defaults[name.to_s]
|
|
368
|
+
return default unless default.nil?
|
|
369
|
+
|
|
370
|
+
return '' if empty && nullable && optional
|
|
371
|
+
return nil if nullable && optional
|
|
372
|
+
|
|
373
|
+
UNSET
|
|
374
|
+
end
|
|
375
|
+
|
|
291
376
|
def detect_nullable(name)
|
|
292
377
|
return false unless @model_class
|
|
293
378
|
return false unless db_column?
|
|
@@ -184,6 +184,8 @@ module Apiwork
|
|
|
184
184
|
# The attribute name.
|
|
185
185
|
# @param decode [Proc, nil] (nil)
|
|
186
186
|
# Transform for request input (API to database). Must preserve the attribute type.
|
|
187
|
+
# @param default [Object] (UNSET)
|
|
188
|
+
# The default value. Omit to declare no default. Pass `nil` for an explicit null default. If omitted and name maps to a database column, auto-detected from the column's static default.
|
|
187
189
|
# @param deprecated [Boolean] (false)
|
|
188
190
|
# Whether deprecated. Metadata included in exports.
|
|
189
191
|
# @param description [String, nil] (nil)
|
|
@@ -273,6 +275,7 @@ module Apiwork
|
|
|
273
275
|
def attribute(
|
|
274
276
|
name,
|
|
275
277
|
decode: nil,
|
|
278
|
+
default: UNSET,
|
|
276
279
|
deprecated: false,
|
|
277
280
|
description: nil,
|
|
278
281
|
empty: nil,
|
|
@@ -297,6 +300,7 @@ module Apiwork
|
|
|
297
300
|
name,
|
|
298
301
|
self,
|
|
299
302
|
decode:,
|
|
303
|
+
default:,
|
|
300
304
|
deprecated:,
|
|
301
305
|
description:,
|
|
302
306
|
empty:,
|
data/lib/apiwork/version.rb
CHANGED
data/lib/apiwork.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.7.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-04-
|
|
11
|
+
date: 2026-04-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -260,8 +260,9 @@ files:
|
|
|
260
260
|
- lib/apiwork/error_code/definition.rb
|
|
261
261
|
- lib/apiwork/error_code/registry.rb
|
|
262
262
|
- lib/apiwork/export.rb
|
|
263
|
+
- lib/apiwork/export/apiwork.rb
|
|
264
|
+
- lib/apiwork/export/apiwork_mapper.rb
|
|
263
265
|
- lib/apiwork/export/base.rb
|
|
264
|
-
- lib/apiwork/export/builder_mapper.rb
|
|
265
266
|
- lib/apiwork/export/open_api.rb
|
|
266
267
|
- lib/apiwork/export/pipeline.rb
|
|
267
268
|
- lib/apiwork/export/pipeline/writer.rb
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Apiwork
|
|
4
|
-
module Export
|
|
5
|
-
class BuilderMapper
|
|
6
|
-
class << self
|
|
7
|
-
def map(export, surface)
|
|
8
|
-
new(export).map(surface)
|
|
9
|
-
end
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def initialize(export)
|
|
13
|
-
@export = export
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def map(surface)
|
|
17
|
-
build_builders(surface.types)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def build_builders(types)
|
|
21
|
-
builders = types.sort_by { |name, _type| name.to_s }.flat_map do |name, type|
|
|
22
|
-
if type.union?
|
|
23
|
-
build_union_builders(name, type, types)
|
|
24
|
-
elsif type.object?
|
|
25
|
-
[build_object_builder(name, type)]
|
|
26
|
-
end
|
|
27
|
-
end.compact
|
|
28
|
-
|
|
29
|
-
builders.join("\n\n")
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
private
|
|
33
|
-
|
|
34
|
-
def build_object_builder(name, type)
|
|
35
|
-
type_name = pascal_case(name)
|
|
36
|
-
defaulted, required = classify_fields(type.shape)
|
|
37
|
-
|
|
38
|
-
if defaulted.empty?
|
|
39
|
-
build_passthrough_builder(type_name)
|
|
40
|
-
elsif required.empty?
|
|
41
|
-
build_all_defaulted_builder(type_name, defaulted)
|
|
42
|
-
else
|
|
43
|
-
build_mixed_builder(type_name, defaulted, required)
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def build_passthrough_builder(type_name)
|
|
48
|
-
"export function build#{type_name}(fields: #{type_name}): #{type_name} {\n" \
|
|
49
|
-
" return fields;\n" \
|
|
50
|
-
'}'
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def build_all_defaulted_builder(type_name, defaulted)
|
|
54
|
-
defaults = build_defaults_body(defaulted)
|
|
55
|
-
|
|
56
|
-
"export function build#{type_name}(fields?: Partial<#{type_name}>): #{type_name} {\n" \
|
|
57
|
-
" return {\n" \
|
|
58
|
-
"#{defaults}\n" \
|
|
59
|
-
" ...fields,\n" \
|
|
60
|
-
" };\n" \
|
|
61
|
-
'}'
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def build_mixed_builder(type_name, defaulted, required)
|
|
65
|
-
required_keys = required.keys.map { |name| "'#{@export.transform_key(name)}'" }.sort.join(' | ')
|
|
66
|
-
fields_type = "Pick<#{type_name}, #{required_keys}> & Partial<#{type_name}>"
|
|
67
|
-
defaults = build_defaults_body(defaulted)
|
|
68
|
-
|
|
69
|
-
"export function build#{type_name}(fields: #{fields_type}): #{type_name} {\n" \
|
|
70
|
-
" return {\n" \
|
|
71
|
-
"#{defaults}\n" \
|
|
72
|
-
" ...fields,\n" \
|
|
73
|
-
" };\n" \
|
|
74
|
-
'}'
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def build_defaults_body(defaulted)
|
|
78
|
-
defaulted.sort_by { |name, _param| name.to_s }.map do |name, param|
|
|
79
|
-
" #{@export.transform_key(name)}: #{serialize_default(param)},"
|
|
80
|
-
end.join("\n")
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def build_union_builders(name, type, types)
|
|
84
|
-
type_name = pascal_case(name)
|
|
85
|
-
builders = [build_passthrough_builder(type_name)]
|
|
86
|
-
|
|
87
|
-
if type.discriminator
|
|
88
|
-
type.variants.each do |variant|
|
|
89
|
-
next unless variant.tag
|
|
90
|
-
next if variant.reference? && types.key?(variant.reference)
|
|
91
|
-
|
|
92
|
-
builders << build_variant_builder(type_name, type.discriminator, variant)
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
builders
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def build_variant_builder(type_name, discriminator, variant)
|
|
100
|
-
discriminator_key = @export.transform_key(discriminator)
|
|
101
|
-
builder_name = variant.reference? ? "build#{pascal_case(variant.reference)}" : "build#{type_name}#{pascal_case(variant.tag)}"
|
|
102
|
-
extract_type = "Extract<#{type_name}, { #{discriminator_key}: '#{variant.tag}' }>"
|
|
103
|
-
|
|
104
|
-
variant_defaults = {}
|
|
105
|
-
variant_required = {}
|
|
106
|
-
|
|
107
|
-
variant_defaults, variant_required = classify_fields(variant.shape) if variant.object? && variant.shape.any?
|
|
108
|
-
|
|
109
|
-
fields_type = build_variant_fields_type(extract_type, discriminator_key, variant_defaults, variant_required)
|
|
110
|
-
|
|
111
|
-
body_lines = [" #{discriminator_key}: '#{variant.tag}',"]
|
|
112
|
-
variant_defaults.sort_by { |name, _param| name.to_s }.each do |name, param|
|
|
113
|
-
body_lines << " #{@export.transform_key(name)}: #{serialize_default(param)},"
|
|
114
|
-
end
|
|
115
|
-
body_lines << ' ...fields,'
|
|
116
|
-
|
|
117
|
-
"export function #{builder_name}(fields: #{fields_type}): #{type_name} {\n" \
|
|
118
|
-
" return {\n" \
|
|
119
|
-
"#{body_lines.join("\n")}\n" \
|
|
120
|
-
" };\n" \
|
|
121
|
-
'}'
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def build_variant_fields_type(extract_type, discriminator_key, defaulted, required)
|
|
125
|
-
omitted_type = "Omit<#{extract_type}, '#{discriminator_key}'>"
|
|
126
|
-
|
|
127
|
-
if required.empty? && defaulted.empty?
|
|
128
|
-
omitted_type
|
|
129
|
-
elsif required.empty?
|
|
130
|
-
"Partial<#{omitted_type}>"
|
|
131
|
-
else
|
|
132
|
-
required_keys = required.keys.map { |name| "'#{@export.transform_key(name)}'" }.sort.join(' | ')
|
|
133
|
-
"Pick<#{omitted_type}, #{required_keys}> & Partial<#{omitted_type}>"
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def classify_fields(shape)
|
|
138
|
-
defaulted = {}
|
|
139
|
-
required = {}
|
|
140
|
-
|
|
141
|
-
shape.sort_by { |name, _param| name.to_s }.each do |name, param|
|
|
142
|
-
if defaulted_field?(param)
|
|
143
|
-
defaulted[name] = param
|
|
144
|
-
else
|
|
145
|
-
required[name] = param
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
[defaulted, required]
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def defaulted_field?(param)
|
|
153
|
-
return true if param.nullable?
|
|
154
|
-
return true if param.respond_to?(:default) && !param.default.nil?
|
|
155
|
-
|
|
156
|
-
false
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def serialize_default(param)
|
|
160
|
-
if param.respond_to?(:default) && !param.default.nil?
|
|
161
|
-
serialize_value(param.default)
|
|
162
|
-
elsif param.nullable?
|
|
163
|
-
'null'
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def serialize_value(value)
|
|
168
|
-
case value
|
|
169
|
-
when String then "'#{value.gsub("'", "\\\\'")}'"
|
|
170
|
-
when Integer, Float then value.to_s
|
|
171
|
-
when BigDecimal then value.to_s('F')
|
|
172
|
-
when TrueClass, FalseClass then value.to_s
|
|
173
|
-
when Array then '[]'
|
|
174
|
-
when Hash then '{}'
|
|
175
|
-
else value.to_s
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
def pascal_case(name)
|
|
180
|
-
name.to_s.camelize(:upper)
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
end
|
|
184
|
-
end
|