apiwork 0.4.0 → 0.6.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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/lib/apiwork/adapter/serializer/error/default/api_builder.rb +4 -4
  3. data/lib/apiwork/adapter/serializer/resource/base.rb +15 -0
  4. data/lib/apiwork/adapter/serializer/resource/default/contract_builder.rb +3 -2
  5. data/lib/apiwork/adapter/standard/capability/writing/contract_builder.rb +2 -2
  6. data/lib/apiwork/api/base.rb +67 -17
  7. data/lib/apiwork/api/element.rb +33 -2
  8. data/lib/apiwork/api/object.rb +70 -5
  9. data/lib/apiwork/api/router.rb +16 -0
  10. data/lib/apiwork/configuration/validatable.rb +1 -0
  11. data/lib/apiwork/configuration.rb +2 -0
  12. data/lib/apiwork/contract/element.rb +17 -2
  13. data/lib/apiwork/contract/object/coercer.rb +24 -2
  14. data/lib/apiwork/contract/object/deserializer.rb +5 -1
  15. data/lib/apiwork/contract/object/transformer.rb +15 -2
  16. data/lib/apiwork/contract/object/validator.rb +46 -3
  17. data/lib/apiwork/contract/object.rb +85 -7
  18. data/lib/apiwork/controller.rb +15 -2
  19. data/lib/apiwork/element.rb +33 -0
  20. data/lib/apiwork/export/base.rb +1 -4
  21. data/lib/apiwork/export/builder_mapper.rb +184 -0
  22. data/lib/apiwork/export/open_api.rb +9 -2
  23. data/lib/apiwork/export/sorbus.rb +5 -1
  24. data/lib/apiwork/export/sorbus_mapper.rb +3 -7
  25. data/lib/apiwork/export/type_analysis.rb +20 -6
  26. data/lib/apiwork/export/type_script.rb +4 -1
  27. data/lib/apiwork/export/type_script_mapper.rb +25 -2
  28. data/lib/apiwork/export/zod.rb +9 -0
  29. data/lib/apiwork/export/zod_mapper.rb +22 -1
  30. data/lib/apiwork/introspection/dump/action.rb +1 -1
  31. data/lib/apiwork/introspection/dump/param.rb +36 -20
  32. data/lib/apiwork/introspection/dump/type.rb +31 -25
  33. data/lib/apiwork/introspection/param/array.rb +26 -0
  34. data/lib/apiwork/introspection/param/base.rb +16 -18
  35. data/lib/apiwork/introspection/param/binary.rb +36 -0
  36. data/lib/apiwork/introspection/param/boolean.rb +36 -0
  37. data/lib/apiwork/introspection/param/date.rb +36 -0
  38. data/lib/apiwork/introspection/param/date_time.rb +36 -0
  39. data/lib/apiwork/introspection/param/decimal.rb +26 -0
  40. data/lib/apiwork/introspection/param/integer.rb +26 -0
  41. data/lib/apiwork/introspection/param/number.rb +26 -0
  42. data/lib/apiwork/introspection/param/record.rb +71 -0
  43. data/lib/apiwork/introspection/param/string.rb +26 -0
  44. data/lib/apiwork/introspection/param/time.rb +36 -0
  45. data/lib/apiwork/introspection/param/uuid.rb +36 -0
  46. data/lib/apiwork/introspection/param.rb +1 -0
  47. data/lib/apiwork/object.rb +252 -2
  48. data/lib/apiwork/representation/attribute.rb +1 -1
  49. data/lib/apiwork/representation/base.rb +105 -0
  50. data/lib/apiwork/representation/element.rb +15 -5
  51. data/lib/apiwork/version.rb +1 -1
  52. metadata +4 -2
@@ -163,7 +163,7 @@ module Apiwork
163
163
  def validate_enum_value(name, value, enum, field_path)
164
164
  enum_values = resolve_enum(enum)
165
165
  return nil unless enum_values
166
- return nil if enum_values.include?(value.to_s) || enum_values.include?(value)
166
+ return nil if enum_values.any? { |enum_value| enum_value.to_s == value.to_s }
167
167
 
168
168
  Issue.new(
169
169
  :value_invalid,
@@ -201,6 +201,8 @@ module Apiwork
201
201
  validate_shape_object(value, param_options[:shape], field_path, max_depth, current_depth)
202
202
  elsif param_options[:type] == :array && value.is_a?(Array)
203
203
  validate_array_param(value, param_options, field_path, max_depth, current_depth)
204
+ elsif param_options[:type] == :record && value.is_a?(Hash)
205
+ validate_record_param(value, param_options, field_path, max_depth, current_depth)
204
206
  else
205
207
  [[], value]
206
208
  end
@@ -230,6 +232,41 @@ module Apiwork
230
232
  array_issues.empty? ? [[], array_values] : [array_issues, NOT_SET]
231
233
  end
232
234
 
235
+ def validate_record_param(value, param_options, field_path, max_depth, current_depth)
236
+ issues = []
237
+ validated = {}
238
+
239
+ of = param_options[:of]
240
+ of_type = of&.type
241
+ of_shape = of&.shape
242
+
243
+ value.each do |key, item|
244
+ item_path = field_path + [key]
245
+
246
+ if of_shape
247
+ validator = Validator.new(normalize_shape(of_shape))
248
+ shape_result = validator.validate(
249
+ item,
250
+ max_depth:,
251
+ current_depth: current_depth + 1,
252
+ path: item_path,
253
+ )
254
+ if shape_result.invalid?
255
+ issues.concat(shape_result.issues)
256
+ else
257
+ validated[key] = shape_result.params
258
+ end
259
+ elsif of_type
260
+ type_error = validate_type(key, item, of_type, item_path)
261
+ type_error ? issues << type_error : validated[key] = item
262
+ else
263
+ validated[key] = item
264
+ end
265
+ end
266
+
267
+ issues.empty? ? [[], validated] : [issues, NOT_SET]
268
+ end
269
+
233
270
  def check_unknown_params(data, path)
234
271
  extra_keys = data.keys - @shape.params.keys
235
272
  extra_keys.map do |key|
@@ -280,7 +317,10 @@ module Apiwork
280
317
  array.each_with_index do |item, index|
281
318
  item_path = field_path + [index]
282
319
 
283
- if of_shape
320
+ if of&.type == :union && of_shape.is_a?(Apiwork::Union)
321
+ error, validated = validate_union(nil, item, of_shape, item_path, current_depth:, max_depth:)
322
+ error ? issues << error : values << validated
323
+ elsif of_shape
284
324
  validator = Validator.new(normalize_shape(of_shape))
285
325
  shape_result = validator.validate(
286
326
  item,
@@ -369,6 +409,7 @@ module Apiwork
369
409
  when :uuid then value.is_a?(String) && value.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i)
370
410
  when :object then value.is_a?(Hash)
371
411
  when :array then value.is_a?(Array)
412
+ when :record then value.is_a?(Hash)
372
413
  when :decimal, :number then value.is_a?(Numeric)
373
414
  else true
374
415
  end
@@ -600,7 +641,9 @@ module Apiwork
600
641
  end
601
642
 
602
643
  def validate_with_type_definition(type_definition, value, path, current_depth:, exclude_param: nil, max_depth:)
603
- type_shape = Object.new(@shape.contract_class, action_name: @shape.action_name)
644
+ scope = type_definition.scope || @shape.contract_class
645
+
646
+ type_shape = Object.new(scope, action_name: @shape.action_name)
604
647
  type_shape.copy_type_definition_params(type_definition, type_shape)
605
648
  type_shape.params.delete(exclude_param) if exclude_param
606
649
 
@@ -56,7 +56,7 @@ module Apiwork
56
56
  #
57
57
  # @param name [Symbol]
58
58
  # The param name.
59
- # @param type [Symbol, nil] (nil) [:array, :binary, :boolean, :date, :datetime, :decimal, :integer, :literal, :number, :object, :string, :time, :union, :uuid]
59
+ # @param type [Symbol, nil] (nil) [:array, :binary, :boolean, :date, :datetime, :decimal, :integer, :literal, :number, :object, :record, :string, :time, :union, :unknown, :uuid]
60
60
  # The param type.
61
61
  # @param as [Symbol, nil] (nil)
62
62
  # The target attribute name.
@@ -83,7 +83,7 @@ module Apiwork
83
83
  # @param nullable [Boolean] (false)
84
84
  # Whether the value can be `null`.
85
85
  # @param of [Symbol, Hash, nil] (nil)
86
- # The element type. Arrays only.
86
+ # The element or value type. Arrays and records only.
87
87
  # @param optional [Boolean] (false)
88
88
  # Whether the param is optional.
89
89
  # @param required [Boolean] (false)
@@ -109,6 +109,7 @@ module Apiwork
109
109
  name,
110
110
  type: nil,
111
111
  as: nil,
112
+ custom_type: nil,
112
113
  default: nil,
113
114
  deprecated: false,
114
115
  description: nil,
@@ -188,6 +189,10 @@ module Apiwork
188
189
  # Whether deprecated. Metadata included in exports.
189
190
  # @param description [String, nil] (nil)
190
191
  # The description. Metadata included in exports.
192
+ # @param max [Integer, nil] (nil)
193
+ # The maximum number of elements.
194
+ # @param min [Integer, nil] (nil)
195
+ # The minimum number of elements.
191
196
  # @param nullable [Boolean] (false)
192
197
  # Whether the value can be `null`.
193
198
  # @param optional [Boolean] (false)
@@ -213,6 +218,8 @@ module Apiwork
213
218
  default: nil,
214
219
  deprecated: false,
215
220
  description: nil,
221
+ max: nil,
222
+ min: nil,
216
223
  nullable: false,
217
224
  optional: false,
218
225
  required: false,
@@ -230,6 +237,8 @@ module Apiwork
230
237
  default:,
231
238
  deprecated:,
232
239
  description:,
240
+ max:,
241
+ min:,
233
242
  nullable:,
234
243
  optional:,
235
244
  required:,
@@ -238,6 +247,69 @@ module Apiwork
238
247
  )
239
248
  end
240
249
 
250
+ # @api public
251
+ # Defines a record param with value type.
252
+ #
253
+ # @param name [Symbol]
254
+ # The param name.
255
+ # @param as [Symbol, nil] (nil)
256
+ # The target attribute name.
257
+ # @param default [Object, nil] (nil)
258
+ # The default value.
259
+ # @param deprecated [Boolean] (false)
260
+ # Whether deprecated. Metadata included in exports.
261
+ # @param description [String, nil] (nil)
262
+ # The description. Metadata included in exports.
263
+ # @param nullable [Boolean] (false)
264
+ # Whether the value can be `null`.
265
+ # @param optional [Boolean] (false)
266
+ # Whether the param is optional.
267
+ # @param required [Boolean] (false)
268
+ # Whether the param is required.
269
+ # @yield block for defining value type
270
+ # @yieldparam element [Contract::Element]
271
+ # @return [void]
272
+ #
273
+ # @example instance_eval style
274
+ # record :scores do
275
+ # integer
276
+ # end
277
+ #
278
+ # @example yield style
279
+ # record :scores do |element|
280
+ # element.integer
281
+ # end
282
+ def record(
283
+ name,
284
+ as: nil,
285
+ default: nil,
286
+ deprecated: false,
287
+ description: nil,
288
+ nullable: false,
289
+ optional: false,
290
+ required: false,
291
+ &block
292
+ )
293
+ raise ConfigurationError, 'record requires a block' unless block
294
+
295
+ element = Element.new(@contract_class)
296
+ block.arity.positive? ? yield(element) : element.instance_eval(&block)
297
+ element.validate!
298
+
299
+ param(
300
+ name,
301
+ as:,
302
+ default:,
303
+ deprecated:,
304
+ description:,
305
+ nullable:,
306
+ optional:,
307
+ required:,
308
+ of: element,
309
+ type: :record,
310
+ )
311
+ end
312
+
241
313
  def wrapped?
242
314
  @wrapped
243
315
  end
@@ -264,8 +336,10 @@ module Apiwork
264
336
  type_definition.params.each do |param_name, param_options|
265
337
  nested_shape = param_options[:shape]
266
338
 
267
- if param_options[:type] == :array && nested_shape.is_a?(Apiwork::API::Object)
339
+ if param_options[:type] == :array && nested_shape.is_a?(API::Object)
268
340
  target_param.param(param_name, **param_options.except(:name))
341
+ elsif param_options[:type] == :array
342
+ target_param.param(param_name, **param_options.except(:name, :shape, :discriminator))
269
343
  elsif nested_shape.is_a?(API::Object)
270
344
  copy_nested_object_param(target_param, param_name, param_options, nested_shape)
271
345
  elsif nested_shape.is_a?(API::Union)
@@ -416,7 +490,9 @@ module Apiwork
416
490
  as:,
417
491
  options:
418
492
  )
419
- union = Union.new(@contract_class, discriminator: type_definition.discriminator)
493
+ scope = type_definition.scope || @contract_class
494
+
495
+ union = Union.new(scope, discriminator: type_definition.discriminator)
420
496
 
421
497
  type_definition.variants.each do |variant|
422
498
  if variant[:shape].is_a?(API::Object)
@@ -467,8 +543,10 @@ module Apiwork
467
543
  options:,
468
544
  &block
469
545
  )
546
+ scope = type_definition.scope || @contract_class
547
+
470
548
  shape = Object.new(
471
- @contract_class,
549
+ scope,
472
550
  action_name: @action_name,
473
551
  visited_types: visited_types.dup.add([@contract_class.object_id, type]),
474
552
  )
@@ -516,7 +594,7 @@ module Apiwork
516
594
  end
517
595
 
518
596
  def resolve_of(of, type, &block)
519
- return nil unless type == :array
597
+ return nil unless [:array, :record].include?(type)
520
598
 
521
599
  if block_given?
522
600
  element = Element.new(@contract_class)
@@ -531,7 +609,7 @@ module Apiwork
531
609
  end
532
610
 
533
611
  def resolve_shape(shape, type, &block)
534
- return nil if type == :array
612
+ return nil if [:array, :record].include?(type)
535
613
 
536
614
  if shape
537
615
  shape
@@ -138,10 +138,10 @@ module Apiwork
138
138
  end
139
139
  else
140
140
  data[:meta] = meta if meta.present?
141
- data
141
+ data.deep_symbolize_keys
142
142
  end
143
143
 
144
- response = Response.new(body:)
144
+ response = Response.new(body: deep_as_json(body))
145
145
 
146
146
  if Rails.env.development?
147
147
  result = contract_class.parse_response(response, action_name)
@@ -223,6 +223,19 @@ module Apiwork
223
223
 
224
224
  private
225
225
 
226
+ def deep_as_json(value)
227
+ case value
228
+ when Hash
229
+ value.transform_values { |nested_value| deep_as_json(nested_value) }
230
+ when Array
231
+ value.map { |element| deep_as_json(element) }
232
+ when String, Integer, Float, TrueClass, FalseClass, NilClass, Symbol, Time, Date, BigDecimal
233
+ value
234
+ else
235
+ value.as_json
236
+ end
237
+ end
238
+
226
239
  def validate_contract
227
240
  return unless resource
228
241
  return if contract.valid?
@@ -250,6 +250,19 @@ module Apiwork
250
250
  of(:binary)
251
251
  end
252
252
 
253
+ # @api public
254
+ # Defines an unknown.
255
+ #
256
+ # @return [void]
257
+ #
258
+ # @example
259
+ # array :values do
260
+ # unknown
261
+ # end
262
+ def unknown
263
+ of(:unknown)
264
+ end
265
+
253
266
  # @api public
254
267
  # Defines an object.
255
268
  #
@@ -300,6 +313,26 @@ module Apiwork
300
313
  of(:array, &block)
301
314
  end
302
315
 
316
+ # @api public
317
+ # Defines a record.
318
+ #
319
+ # @yield block defining value type
320
+ # @yieldparam element [Element]
321
+ # @return [void]
322
+ #
323
+ # @example instance_eval style
324
+ # record :scores do
325
+ # integer
326
+ # end
327
+ #
328
+ # @example yield style
329
+ # record :scores do |element|
330
+ # element.integer
331
+ # end
332
+ def record(&block)
333
+ of(:record, &block)
334
+ end
335
+
303
336
  # @api public
304
337
  # Defines a union.
305
338
  #
@@ -263,10 +263,7 @@ module Apiwork
263
263
  end
264
264
 
265
265
  def extract_options_from_config(config)
266
- self.class.options.keys.each_with_object({}) do |key, hash|
267
- value = config.public_send(key)
268
- hash[key] = value unless value.nil?
269
- end
266
+ config.to_h.slice(*self.class.options.keys).compact
270
267
  end
271
268
 
272
269
  def validate_options!
@@ -0,0 +1,184 @@
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
@@ -319,7 +319,7 @@ module Apiwork
319
319
  schema = map_param(param)
320
320
 
321
321
  schema[:description] = param.description if param.description
322
- schema[:example] = param.example if param.example
322
+ schema[:example] = param.example if param.concrete? && param.example
323
323
  schema[:deprecated] = true if param.deprecated?
324
324
 
325
325
  schema[:format] = param.format.to_s if param.formattable? && param.format
@@ -332,6 +332,8 @@ module Apiwork
332
332
  map_object(param)
333
333
  elsif param.array?
334
334
  map_array(param)
335
+ elsif param.record?
336
+ map_record(param)
335
337
  elsif param.union?
336
338
  map_union(param)
337
339
  elsif param.literal?
@@ -353,7 +355,7 @@ module Apiwork
353
355
  }
354
356
 
355
357
  result[:description] = param.description if param.description
356
- result[:example] = param.example if param.example
358
+ result[:example] = param.example if param.respond_to?(:example) && param.example
357
359
 
358
360
  param.shape.each do |name, field|
359
361
  result[:properties][transform_key(name)] = map_field(field)
@@ -383,6 +385,11 @@ module Apiwork
383
385
  }
384
386
  end
385
387
 
388
+ def map_record(param)
389
+ value_schema = param.of ? map_param(param.of) : {}
390
+ { additionalProperties: value_schema, type: 'object' }
391
+ end
392
+
386
393
  def map_inline_object(shape)
387
394
  result = { properties: {}, type: 'object' }
388
395
 
@@ -7,8 +7,12 @@ module Apiwork
7
7
  output :string
8
8
  file_extension '.ts'
9
9
 
10
+ option :builders, default: false, type: :boolean
11
+
10
12
  def generate
11
- SorbusMapper.map(self, surface)
13
+ output = SorbusMapper.map(self, surface)
14
+ output += "\n\n#{BuilderMapper.map(self, surface)}" if options[:builders]
15
+ output
12
16
  end
13
17
 
14
18
  private
@@ -37,10 +37,8 @@ module Apiwork
37
37
  end
38
38
 
39
39
  def build_contract
40
- contract = {
41
- endpoints: build_endpoint_tree(@export.api.resources),
42
- error: build_error_schema,
43
- }
40
+ contract = { endpoints: build_endpoint_tree(@export.api.resources) }
41
+ contract[:error] = build_error_schema if @surface.types.key?(:error)
44
42
  "export const contract = #{format_object(contract, indent: 0)} as const;"
45
43
  end
46
44
 
@@ -80,9 +78,7 @@ module Apiwork
80
78
  end
81
79
 
82
80
  def transform_path(path)
83
- path.gsub(%r{(/:?)(\w+)}) do
84
- "#{::Regexp.last_match(1)}#{@export.transform_key(::Regexp.last_match(2))}"
85
- end
81
+ path.to_s.gsub(/:(\w+)/) { ":#{@export.transform_key(::Regexp.last_match(1))}" }
86
82
  end
87
83
 
88
84
  def extract_path_params(path)
@@ -4,9 +4,8 @@ module Apiwork
4
4
  module Export
5
5
  class TypeAnalysis
6
6
  PRIMITIVE_TYPES = %i[
7
- string integer boolean datetime date uuid object array
8
- decimal float literal union enum text binary json number time
9
- unknown
7
+ array binary boolean date datetime decimal enum float integer json
8
+ literal number object record string text time union unknown uuid
10
9
  ].to_set.freeze
11
10
 
12
11
  class << self
@@ -39,11 +38,22 @@ module Apiwork
39
38
 
40
39
  def find_cycle_breaking_types(graph)
41
40
  lazy_types = Set.new
41
+ reduced_graph = graph
42
42
 
43
- find_strongly_connected_components(graph).each do |component|
44
- next if component.size == 1 && !graph[component.first].include?(component.first)
43
+ loop do
44
+ previous_size = lazy_types.size
45
+
46
+ find_strongly_connected_components(reduced_graph).each do |component|
47
+ next if component.size == 1 && !reduced_graph[component.first].include?(component.first)
48
+
49
+ lazy_types.add(select_cycle_breaker(component, reduced_graph))
50
+ end
45
51
 
46
- lazy_types.add(component.min_by(&:to_s))
52
+ break if lazy_types.size == previous_size
53
+
54
+ reduced_graph = reduced_graph
55
+ .reject { |node, _| lazy_types.include?(node) }
56
+ .transform_values { |deps| deps - lazy_types.to_a }
47
57
  end
48
58
 
49
59
  lazy_types
@@ -114,6 +124,10 @@ module Apiwork
114
124
  state[:components] << component
115
125
  end
116
126
 
127
+ def select_cycle_breaker(component, graph)
128
+ component.min_by { |node| [-(graph[node] & component).size, node.to_s] }
129
+ end
130
+
117
131
  def collect_references(node, references, filter)
118
132
  return unless node.is_a?(Hash)
119
133
 
@@ -7,10 +7,13 @@ module Apiwork
7
7
  output :string
8
8
  file_extension '.ts'
9
9
 
10
+ option :builders, default: false, type: :boolean
10
11
  option :version, default: '5', enum: %w[4 5], type: :string
11
12
 
12
13
  def generate
13
- TypeScriptMapper.map(self, surface)
14
+ output = TypeScriptMapper.map(self, surface)
15
+ output += "\n\n#{BuilderMapper.map(self, surface)}" if options[:builders]
16
+ output
14
17
  end
15
18
 
16
19
  private