apiwork 0.0.0.pre → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +2 -2
  3. data/README.md +117 -1
  4. data/Rakefile +5 -3
  5. data/app/controllers/apiwork/errors_controller.rb +13 -0
  6. data/app/controllers/apiwork/exports_controller.rb +22 -0
  7. data/lib/apiwork/abstractable.rb +26 -0
  8. data/lib/apiwork/adapter/base.rb +369 -0
  9. data/lib/apiwork/adapter/builder/api/base.rb +66 -0
  10. data/lib/apiwork/adapter/builder/contract/base.rb +86 -0
  11. data/lib/apiwork/adapter/capability/api/base.rb +51 -0
  12. data/lib/apiwork/adapter/capability/api/scope.rb +64 -0
  13. data/lib/apiwork/adapter/capability/base.rb +291 -0
  14. data/lib/apiwork/adapter/capability/contract/base.rb +37 -0
  15. data/lib/apiwork/adapter/capability/contract/scope.rb +110 -0
  16. data/lib/apiwork/adapter/capability/operation/base.rb +172 -0
  17. data/lib/apiwork/adapter/capability/operation/metadata_shape.rb +165 -0
  18. data/lib/apiwork/adapter/capability/result.rb +21 -0
  19. data/lib/apiwork/adapter/capability/runner.rb +56 -0
  20. data/lib/apiwork/adapter/capability/transformer/request/base.rb +72 -0
  21. data/lib/apiwork/adapter/capability/transformer/response/base.rb +45 -0
  22. data/lib/apiwork/adapter/registry.rb +16 -0
  23. data/lib/apiwork/adapter/serializer/error/base.rb +72 -0
  24. data/lib/apiwork/adapter/serializer/error/default/api_builder.rb +32 -0
  25. data/lib/apiwork/adapter/serializer/error/default.rb +37 -0
  26. data/lib/apiwork/adapter/serializer/resource/base.rb +84 -0
  27. data/lib/apiwork/adapter/serializer/resource/default/contract_builder.rb +209 -0
  28. data/lib/apiwork/adapter/serializer/resource/default.rb +39 -0
  29. data/lib/apiwork/adapter/standard/capability/filtering/api_builder.rb +75 -0
  30. data/lib/apiwork/adapter/standard/capability/filtering/constants.rb +37 -0
  31. data/lib/apiwork/adapter/standard/capability/filtering/contract_builder.rb +193 -0
  32. data/lib/apiwork/adapter/standard/capability/filtering/operation/filter/builder.rb +47 -0
  33. data/lib/apiwork/adapter/standard/capability/filtering/operation/filter/operator_builder.rb +36 -0
  34. data/lib/apiwork/adapter/standard/capability/filtering/operation/filter.rb +462 -0
  35. data/lib/apiwork/adapter/standard/capability/filtering/operation.rb +22 -0
  36. data/lib/apiwork/adapter/standard/capability/filtering/request_transformer.rb +47 -0
  37. data/lib/apiwork/adapter/standard/capability/filtering.rb +18 -0
  38. data/lib/apiwork/adapter/standard/capability/including/contract_builder.rb +169 -0
  39. data/lib/apiwork/adapter/standard/capability/including/operation.rb +20 -0
  40. data/lib/apiwork/adapter/standard/capability/including.rb +16 -0
  41. data/lib/apiwork/adapter/standard/capability/pagination/api_builder.rb +34 -0
  42. data/lib/apiwork/adapter/standard/capability/pagination/contract_builder.rb +35 -0
  43. data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate/cursor.rb +84 -0
  44. data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate/offset.rb +66 -0
  45. data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate.rb +24 -0
  46. data/lib/apiwork/adapter/standard/capability/pagination/operation.rb +24 -0
  47. data/lib/apiwork/adapter/standard/capability/pagination.rb +21 -0
  48. data/lib/apiwork/adapter/standard/capability/sorting/api_builder.rb +19 -0
  49. data/lib/apiwork/adapter/standard/capability/sorting/contract_builder.rb +84 -0
  50. data/lib/apiwork/adapter/standard/capability/sorting/operation/sort.rb +83 -0
  51. data/lib/apiwork/adapter/standard/capability/sorting/operation.rb +22 -0
  52. data/lib/apiwork/adapter/standard/capability/sorting.rb +17 -0
  53. data/lib/apiwork/adapter/standard/capability/writing/constants.rb +15 -0
  54. data/lib/apiwork/adapter/standard/capability/writing/contract_builder.rb +253 -0
  55. data/lib/apiwork/adapter/standard/capability/writing/operation/issue_mapper.rb +210 -0
  56. data/lib/apiwork/adapter/standard/capability/writing/operation.rb +32 -0
  57. data/lib/apiwork/adapter/standard/capability/writing/request_transformer.rb +37 -0
  58. data/lib/apiwork/adapter/standard/capability/writing.rb +17 -0
  59. data/lib/apiwork/adapter/standard/includes_resolver.rb +106 -0
  60. data/lib/apiwork/adapter/standard.rb +22 -0
  61. data/lib/apiwork/adapter/wrapper/base.rb +70 -0
  62. data/lib/apiwork/adapter/wrapper/collection/base.rb +60 -0
  63. data/lib/apiwork/adapter/wrapper/collection/default.rb +47 -0
  64. data/lib/apiwork/adapter/wrapper/error/base.rb +30 -0
  65. data/lib/apiwork/adapter/wrapper/error/default.rb +34 -0
  66. data/lib/apiwork/adapter/wrapper/member/base.rb +58 -0
  67. data/lib/apiwork/adapter/wrapper/member/default.rb +40 -0
  68. data/lib/apiwork/adapter/wrapper/shape.rb +203 -0
  69. data/lib/apiwork/adapter.rb +50 -0
  70. data/lib/apiwork/api/base.rb +802 -0
  71. data/lib/apiwork/api/element.rb +110 -0
  72. data/lib/apiwork/api/enum_registry/definition.rb +51 -0
  73. data/lib/apiwork/api/enum_registry.rb +98 -0
  74. data/lib/apiwork/api/info/contact.rb +67 -0
  75. data/lib/apiwork/api/info/license.rb +50 -0
  76. data/lib/apiwork/api/info/server.rb +50 -0
  77. data/lib/apiwork/api/info.rb +221 -0
  78. data/lib/apiwork/api/object.rb +235 -0
  79. data/lib/apiwork/api/registry.rb +33 -0
  80. data/lib/apiwork/api/representation_registry.rb +76 -0
  81. data/lib/apiwork/api/resource/action.rb +41 -0
  82. data/lib/apiwork/api/resource.rb +648 -0
  83. data/lib/apiwork/api/router.rb +104 -0
  84. data/lib/apiwork/api/type_registry/definition.rb +117 -0
  85. data/lib/apiwork/api/type_registry.rb +99 -0
  86. data/lib/apiwork/api/union.rb +49 -0
  87. data/lib/apiwork/api.rb +85 -0
  88. data/lib/apiwork/configurable.rb +71 -0
  89. data/lib/apiwork/configuration/option.rb +125 -0
  90. data/lib/apiwork/configuration/validatable.rb +25 -0
  91. data/lib/apiwork/configuration.rb +95 -0
  92. data/lib/apiwork/configuration_error.rb +6 -0
  93. data/lib/apiwork/constraint_error.rb +20 -0
  94. data/lib/apiwork/contract/action/request.rb +79 -0
  95. data/lib/apiwork/contract/action/response.rb +87 -0
  96. data/lib/apiwork/contract/action.rb +258 -0
  97. data/lib/apiwork/contract/base.rb +714 -0
  98. data/lib/apiwork/contract/element.rb +130 -0
  99. data/lib/apiwork/contract/object/coercer.rb +194 -0
  100. data/lib/apiwork/contract/object/deserializer.rb +101 -0
  101. data/lib/apiwork/contract/object/transformer.rb +95 -0
  102. data/lib/apiwork/contract/object/validator/result.rb +27 -0
  103. data/lib/apiwork/contract/object/validator.rb +734 -0
  104. data/lib/apiwork/contract/object.rb +566 -0
  105. data/lib/apiwork/contract/request_parser/result.rb +25 -0
  106. data/lib/apiwork/contract/request_parser.rb +72 -0
  107. data/lib/apiwork/contract/response_parser/result.rb +25 -0
  108. data/lib/apiwork/contract/response_parser.rb +35 -0
  109. data/lib/apiwork/contract/union.rb +56 -0
  110. data/lib/apiwork/contract_error.rb +9 -0
  111. data/lib/apiwork/controller.rb +300 -0
  112. data/lib/apiwork/domain_error.rb +13 -0
  113. data/lib/apiwork/element.rb +386 -0
  114. data/lib/apiwork/engine.rb +20 -0
  115. data/lib/apiwork/error.rb +6 -0
  116. data/lib/apiwork/error_code/definition.rb +63 -0
  117. data/lib/apiwork/error_code/registry.rb +18 -0
  118. data/lib/apiwork/error_code.rb +132 -0
  119. data/lib/apiwork/export/base.rb +291 -0
  120. data/lib/apiwork/export/open_api.rb +600 -0
  121. data/lib/apiwork/export/pipeline/writer.rb +66 -0
  122. data/lib/apiwork/export/pipeline.rb +84 -0
  123. data/lib/apiwork/export/registry.rb +16 -0
  124. data/lib/apiwork/export/surface_resolver.rb +189 -0
  125. data/lib/apiwork/export/type_analysis.rb +170 -0
  126. data/lib/apiwork/export/type_script.rb +23 -0
  127. data/lib/apiwork/export/type_script_mapper.rb +349 -0
  128. data/lib/apiwork/export/zod.rb +39 -0
  129. data/lib/apiwork/export/zod_mapper.rb +421 -0
  130. data/lib/apiwork/export.rb +80 -0
  131. data/lib/apiwork/http_error.rb +16 -0
  132. data/lib/apiwork/introspection/action/request.rb +66 -0
  133. data/lib/apiwork/introspection/action/response.rb +57 -0
  134. data/lib/apiwork/introspection/action.rb +124 -0
  135. data/lib/apiwork/introspection/api/info/contact.rb +59 -0
  136. data/lib/apiwork/introspection/api/info/license.rb +49 -0
  137. data/lib/apiwork/introspection/api/info/server.rb +50 -0
  138. data/lib/apiwork/introspection/api/info.rb +107 -0
  139. data/lib/apiwork/introspection/api/resource.rb +83 -0
  140. data/lib/apiwork/introspection/api.rb +92 -0
  141. data/lib/apiwork/introspection/contract.rb +63 -0
  142. data/lib/apiwork/introspection/dump/action.rb +101 -0
  143. data/lib/apiwork/introspection/dump/api.rb +119 -0
  144. data/lib/apiwork/introspection/dump/contract.rb +129 -0
  145. data/lib/apiwork/introspection/dump/param.rb +486 -0
  146. data/lib/apiwork/introspection/dump/resource.rb +112 -0
  147. data/lib/apiwork/introspection/dump/type.rb +339 -0
  148. data/lib/apiwork/introspection/dump.rb +17 -0
  149. data/lib/apiwork/introspection/enum.rb +63 -0
  150. data/lib/apiwork/introspection/error_code.rb +44 -0
  151. data/lib/apiwork/introspection/param/array.rb +88 -0
  152. data/lib/apiwork/introspection/param/base.rb +285 -0
  153. data/lib/apiwork/introspection/param/binary.rb +73 -0
  154. data/lib/apiwork/introspection/param/boolean.rb +73 -0
  155. data/lib/apiwork/introspection/param/date.rb +73 -0
  156. data/lib/apiwork/introspection/param/date_time.rb +73 -0
  157. data/lib/apiwork/introspection/param/decimal.rb +121 -0
  158. data/lib/apiwork/introspection/param/integer.rb +131 -0
  159. data/lib/apiwork/introspection/param/literal.rb +45 -0
  160. data/lib/apiwork/introspection/param/number.rb +121 -0
  161. data/lib/apiwork/introspection/param/object.rb +59 -0
  162. data/lib/apiwork/introspection/param/reference.rb +45 -0
  163. data/lib/apiwork/introspection/param/string.rb +122 -0
  164. data/lib/apiwork/introspection/param/time.rb +73 -0
  165. data/lib/apiwork/introspection/param/union.rb +57 -0
  166. data/lib/apiwork/introspection/param/unknown.rb +26 -0
  167. data/lib/apiwork/introspection/param/uuid.rb +73 -0
  168. data/lib/apiwork/introspection/param.rb +31 -0
  169. data/lib/apiwork/introspection/type.rb +129 -0
  170. data/lib/apiwork/introspection.rb +28 -0
  171. data/lib/apiwork/issue.rb +80 -0
  172. data/lib/apiwork/json_pointer.rb +21 -0
  173. data/lib/apiwork/object.rb +1618 -0
  174. data/lib/apiwork/reference_generator.rb +622 -0
  175. data/lib/apiwork/registry.rb +56 -0
  176. data/lib/apiwork/representation/association.rb +391 -0
  177. data/lib/apiwork/representation/attribute.rb +335 -0
  178. data/lib/apiwork/representation/base.rb +819 -0
  179. data/lib/apiwork/representation/deserializer.rb +95 -0
  180. data/lib/apiwork/representation/element.rb +128 -0
  181. data/lib/apiwork/representation/inheritance.rb +78 -0
  182. data/lib/apiwork/representation/model_detector.rb +75 -0
  183. data/lib/apiwork/representation/root_key.rb +35 -0
  184. data/lib/apiwork/representation/serializer.rb +127 -0
  185. data/lib/apiwork/request.rb +79 -0
  186. data/lib/apiwork/response.rb +56 -0
  187. data/lib/apiwork/union.rb +102 -0
  188. data/lib/apiwork/version.rb +2 -2
  189. data/lib/apiwork.rb +61 -3
  190. data/lib/generators/apiwork/api_generator.rb +38 -0
  191. data/lib/generators/apiwork/contract_generator.rb +25 -0
  192. data/lib/generators/apiwork/install_generator.rb +27 -0
  193. data/lib/generators/apiwork/representation_generator.rb +25 -0
  194. data/lib/generators/apiwork/templates/api/api.rb.tt +4 -0
  195. data/lib/generators/apiwork/templates/contract/contract.rb.tt +6 -0
  196. data/lib/generators/apiwork/templates/install/application_contract.rb.tt +5 -0
  197. data/lib/generators/apiwork/templates/install/application_representation.rb.tt +5 -0
  198. data/lib/generators/apiwork/templates/representation/representation.rb.tt +6 -0
  199. data/lib/tasks/apiwork.rake +102 -0
  200. metadata +319 -19
  201. data/.rubocop.yml +0 -8
  202. data/sig/apiwork.rbs +0 -4
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Contract
5
+ # @api public
6
+ # Block context for defining a single type expression.
7
+ #
8
+ # Used inside `array do` and `variant do` blocks where
9
+ # exactly one element type must be defined.
10
+ #
11
+ # @see Contract::Object Block context for object params
12
+ # @see Contract::Union Block context for union variants
13
+ #
14
+ # @example instance_eval style
15
+ # array :ids do
16
+ # integer
17
+ # end
18
+ #
19
+ # @example yield style
20
+ # array :ids do |element|
21
+ # element.integer
22
+ # end
23
+ #
24
+ # @example Array of references
25
+ # array :items do |element|
26
+ # element.reference :item
27
+ # end
28
+ class Element < Apiwork::Element
29
+ def initialize(contract_class)
30
+ super()
31
+ @contract_class = contract_class
32
+ end
33
+
34
+ # @api public
35
+ # Defines the element type.
36
+ #
37
+ # This is the verbose form. Prefer sugar methods (string, integer, etc.)
38
+ # for static definitions. Use `of` for dynamic element generation.
39
+ #
40
+ # @param type [Symbol] [:array, :binary, :boolean, :date, :datetime, :decimal, :integer, :literal, :number, :object, :string, :time, :union, :uuid]
41
+ # The element type. Custom type references are also allowed.
42
+ # @param discriminator [Symbol, nil] (nil)
43
+ # The discriminator field name. Unions only.
44
+ # @param enum [Array, Symbol, nil] (nil)
45
+ # The allowed values or enum reference. Strings and integers only.
46
+ # @param format [Symbol, nil] (nil) [:date, :datetime, :double, :email, :float, :hostname, :int32, :int64, :ipv4, :ipv6, :password, :url, :uuid]
47
+ # Format hint for exports. Does not change the type, but exports may add validation or documentation based on it.
48
+ # Valid formats by type: `:decimal`/`:number` (`:double`, `:float`), `:integer` (`:int32`, `:int64`),
49
+ # `:string` (`:date`, `:datetime`, `:email`, `:hostname`, `:ipv4`, `:ipv6`, `:password`, `:url`, `:uuid`).
50
+ # @param max [Integer, nil] (nil)
51
+ # The maximum. For `:decimal`, `:integer`, `:number`: value. For `:string`: length.
52
+ # @param min [Integer, nil] (nil)
53
+ # The minimum. For `:decimal`, `:integer`, `:number`: value. For `:string`: length.
54
+ # @param value [Object, nil] (nil)
55
+ # The literal value. Literals only.
56
+ # @yield block for defining nested structure (instance_eval style)
57
+ # @yieldparam shape [Contract::Object, Contract::Union, Contract::Element]
58
+ # @return [void]
59
+ # @raise [ArgumentError] if object, array, or union type is missing block
60
+ #
61
+ # @example Dynamic element type
62
+ # element_type = :string
63
+ # array :values do
64
+ # of element_type
65
+ # end
66
+ #
67
+ # @example Object with block
68
+ # array :tags do
69
+ # of :object do
70
+ # string :name
71
+ # string :color
72
+ # end
73
+ # end
74
+ def of(type, discriminator: nil, enum: nil, format: nil, max: nil, min: nil, value: nil, &block)
75
+ resolved_enum = enum.is_a?(Symbol) ? resolve_enum(enum) : enum
76
+
77
+ case type
78
+ when :string, :integer, :decimal, :boolean, :number, :datetime, :date, :uuid, :time, :binary
79
+ set_type(type, format:, max:, min:, enum: resolved_enum)
80
+ when :literal
81
+ @type = :literal
82
+ @value = value
83
+ @defined = true
84
+ when :object
85
+ @type = :object
86
+ if block
87
+ shape = Object.new(@contract_class)
88
+ block.arity.positive? ? yield(shape) : shape.instance_eval(&block)
89
+ @shape = shape
90
+ end
91
+ @defined = true
92
+ when :array
93
+ raise ConfigurationError, 'array requires a block' unless block
94
+
95
+ inner = Element.new(@contract_class)
96
+ block.arity.positive? ? yield(inner) : inner.instance_eval(&block)
97
+ inner.validate!
98
+ @type = :array
99
+ @inner = inner
100
+ @shape = inner.shape
101
+ @defined = true
102
+ when :union
103
+ raise ConfigurationError, 'union requires a block' unless block
104
+
105
+ shape = Union.new(@contract_class, discriminator:)
106
+ block.arity.positive? ? yield(shape) : shape.instance_eval(&block)
107
+ @type = :union
108
+ @shape = shape
109
+ @discriminator = discriminator
110
+ @defined = true
111
+ else
112
+ @type = type
113
+ @custom_type = type
114
+ @defined = true
115
+ end
116
+ end
117
+
118
+ private
119
+
120
+ def resolve_enum(enum)
121
+ return nil if enum.nil?
122
+ return enum if enum.is_a?(Array)
123
+
124
+ raise ConfigurationError, "Enum :#{enum} not found." unless @contract_class.enum?(enum)
125
+
126
+ enum
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Contract
5
+ class Object
6
+ class Coercer
7
+ PRIMITIVES = {
8
+ boolean: lambda { |value|
9
+ return value if [true, false].include?(value)
10
+ return true if %w[true 1 yes].include?(value.to_s.downcase)
11
+ return false if %w[false 0 no].include?(value.to_s.downcase)
12
+
13
+ nil
14
+ },
15
+ date: lambda { |value|
16
+ return value if value.is_a?(Date)
17
+
18
+ Date.parse(value) if value.is_a?(String)
19
+ },
20
+ datetime: lambda { |value|
21
+ return value if value.is_a?(Time) || value.is_a?(DateTime) || value.is_a?(ActiveSupport::TimeWithZone)
22
+
23
+ Time.zone.parse(value) if value.is_a?(String)
24
+ },
25
+ decimal: lambda { |value|
26
+ return value if value.is_a?(BigDecimal)
27
+
28
+ BigDecimal(value.to_s) if value.is_a?(Numeric) || value.is_a?(String)
29
+ },
30
+ integer: lambda { |value|
31
+ return value if value.is_a?(Integer)
32
+
33
+ Integer(value) if value.is_a?(String) && value.match?(/\A-?\d+\z/)
34
+ },
35
+ number: lambda { |value|
36
+ return value if value.is_a?(Float) || value.is_a?(Integer)
37
+ return nil if value.is_a?(String) && value.blank?
38
+
39
+ Float(value) if value.is_a?(String)
40
+ },
41
+ string: lambda { |value|
42
+ return value if value.is_a?(String)
43
+
44
+ value.to_s
45
+ },
46
+ time: lambda { |value|
47
+ return value if value.is_a?(Time) || value.is_a?(DateTime) || value.is_a?(ActiveSupport::TimeWithZone)
48
+
49
+ Time.zone.parse("2000-01-01T#{value}") if value.is_a?(String) && value.match?(/\A\d{2}:\d{2}(:\d{2})?\z/)
50
+ },
51
+ uuid: lambda { |value|
52
+ return value if 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)
53
+
54
+ nil
55
+ },
56
+ }.freeze
57
+
58
+ class << self
59
+ def coerce(shape, hash)
60
+ new(shape).coerce(hash)
61
+ end
62
+ end
63
+
64
+ def initialize(shape)
65
+ @shape = shape
66
+ @type_cache = {}
67
+ end
68
+
69
+ def coerce(hash)
70
+ coerced = hash.dup
71
+
72
+ @shape.params.each do |name, param_options|
73
+ next unless coerced.key?(name)
74
+
75
+ coerced[name] = coerce_value(coerced[name], param_options)
76
+ end
77
+
78
+ coerced
79
+ end
80
+
81
+ private
82
+
83
+ def coerce_value(value, param_options)
84
+ type = param_options[:type]
85
+
86
+ return coerce_union(value, param_options[:union]) if type == :union
87
+ return coerce_array(value, param_options) if type == :array && value.is_a?(Array)
88
+ return Coercer.coerce(param_options[:shape], value) if param_options[:shape] && value.is_a?(Hash)
89
+
90
+ if value.is_a?(Hash) && type && !PRIMITIVES.key?(type)
91
+ custom_shape = resolve_custom_shape(type)
92
+ return Coercer.coerce(custom_shape, value) if custom_shape
93
+ end
94
+
95
+ coerced = coerce_primitive(value, type)
96
+ coerced.nil? ? value : coerced
97
+ end
98
+
99
+ def coerce_array(array, param_options)
100
+ custom_shape = nil
101
+ of = param_options[:of]
102
+ of_type = of&.type
103
+ of_shape = of&.shape
104
+
105
+ custom_shape = resolve_custom_shape(of_type) if of_type && !PRIMITIVES.key?(of_type)
106
+
107
+ array.map do |item|
108
+ if of_shape && item.is_a?(Hash)
109
+ Coercer.coerce(of_shape, item)
110
+ elsif of_type && PRIMITIVES.key?(of_type)
111
+ coerced = coerce_primitive(item, of_type)
112
+ coerced.nil? ? item : coerced
113
+ elsif of_type == :array && item.is_a?(Array) && of&.inner
114
+ coerce_array(item, { of: of.inner })
115
+ elsif custom_shape && item.is_a?(Hash)
116
+ Coercer.coerce(custom_shape, item)
117
+ else
118
+ item
119
+ end
120
+ end
121
+ end
122
+
123
+ def coerce_union(value, union)
124
+ if union.variants.any? { |variant| variant[:type] == :boolean }
125
+ coerced = coerce_primitive(value, :boolean)
126
+ return coerced unless coerced.nil?
127
+ end
128
+
129
+ discriminator = union.discriminator
130
+
131
+ if discriminator && value.is_a?(Hash)
132
+ discriminator_value = value[discriminator]
133
+ matching_variant = union.variants.find do |variant|
134
+ variant[:tag].to_s == discriminator_value.to_s
135
+ end
136
+
137
+ if matching_variant
138
+ custom_shape = resolve_custom_shape(matching_variant[:type])
139
+ return Coercer.coerce(custom_shape, value) if custom_shape
140
+ end
141
+ end
142
+
143
+ union.variants.each do |variant|
144
+ variant_type = variant[:type]
145
+ variant_of = variant[:of]
146
+ variant_of_type = variant_of&.type
147
+
148
+ if variant_type == :array && value.is_a?(Array) && variant_of_type
149
+ custom_shape = resolve_custom_shape(variant_of_type)
150
+ if custom_shape
151
+ return value.map do |item|
152
+ item.is_a?(Hash) ? Coercer.coerce(custom_shape, item) : item
153
+ end
154
+ end
155
+ end
156
+
157
+ next if discriminator
158
+
159
+ custom_shape = resolve_custom_shape(variant_type)
160
+ next unless custom_shape
161
+
162
+ return Coercer.coerce(custom_shape, value) if value.is_a?(Hash)
163
+ end
164
+
165
+ value
166
+ end
167
+
168
+ def coerce_primitive(value, type)
169
+ return nil if value.nil?
170
+
171
+ coercer = PRIMITIVES[type]
172
+ return nil unless coercer
173
+
174
+ begin
175
+ coercer.call(value)
176
+ rescue ArgumentError, TypeError
177
+ nil
178
+ end
179
+ end
180
+
181
+ def resolve_custom_shape(type_name)
182
+ return @type_cache[type_name] if @type_cache.key?(type_name)
183
+
184
+ type_definition = @shape.contract_class.resolve_custom_type(type_name)
185
+ return @type_cache[type_name] = nil unless type_definition
186
+
187
+ @type_cache[type_name] = Object.new(@shape.contract_class).tap do |type_shape|
188
+ type_shape.copy_type_definition_params(type_definition, type_shape)
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Contract
5
+ class Object
6
+ class Deserializer
7
+ class << self
8
+ def deserialize(shape, hash)
9
+ new(shape).deserialize(hash)
10
+ end
11
+ end
12
+
13
+ def initialize(shape)
14
+ @shape = shape
15
+ end
16
+
17
+ def deserialize(hash)
18
+ deserialized = hash.dup
19
+
20
+ @shape.params.each do |name, param_options|
21
+ next unless deserialized.key?(name)
22
+
23
+ value = deserialized[name]
24
+
25
+ deserialized[name] = deserialize_value(value, param_options)
26
+ end
27
+
28
+ deserialized
29
+ end
30
+
31
+ private
32
+
33
+ def deserialize_value(value, param_options)
34
+ if param_options[:union] && value.is_a?(Hash)
35
+ deserialize_union(value, param_options[:union])
36
+ elsif param_options[:shape] && value.is_a?(Hash)
37
+ deserialize_shape(value, param_options[:shape])
38
+ elsif param_options[:type] == :array && value.is_a?(Array)
39
+ deserialize_array(value, param_options)
40
+ else
41
+ value
42
+ end
43
+ end
44
+
45
+ def deserialize_shape(hash, nested_shape)
46
+ representation_class = nested_shape.contract_class.representation_class
47
+ return representation_class.deserialize(hash) if representation_class && nested_shape.params.none? { |_, options| options[:union] }
48
+
49
+ Deserializer.deserialize(nested_shape, hash)
50
+ end
51
+
52
+ def deserialize_union(hash, union)
53
+ variant = resolve_variant(hash, union)
54
+ return hash unless variant
55
+
56
+ if variant[:shape]
57
+ representation_class = variant[:shape].contract_class.representation_class
58
+ return representation_class.deserialize(hash) if representation_class
59
+
60
+ Deserializer.deserialize(variant[:shape], hash)
61
+ elsif variant[:custom_type]
62
+ deserialize_custom_type(hash, variant[:custom_type])
63
+ else
64
+ hash
65
+ end
66
+ end
67
+
68
+ def resolve_variant(hash, union)
69
+ discriminator = union.discriminator
70
+ return union.variants.first unless discriminator
71
+
72
+ tag = hash[discriminator]
73
+ union.variants.find { |v| v[:tag].to_s == tag.to_s }
74
+ end
75
+
76
+ def deserialize_custom_type(hash, type_name)
77
+ type_definition = @shape.contract_class.resolve_custom_type(type_name)
78
+ return hash unless type_definition
79
+
80
+ representation_class = (type_definition.scope || @shape.contract_class).representation_class
81
+
82
+ return representation_class.deserialize(hash) if representation_class
83
+
84
+ hash
85
+ end
86
+
87
+ def deserialize_array(array, param_options)
88
+ array.map do |item|
89
+ next item unless item.is_a?(Hash)
90
+
91
+ if param_options[:shape]
92
+ Deserializer.deserialize(param_options[:shape], item)
93
+ else
94
+ item
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Contract
5
+ class Object
6
+ class Transformer
7
+ class << self
8
+ def transform(shape, params)
9
+ new(shape).transform(params)
10
+ end
11
+ end
12
+
13
+ def initialize(shape)
14
+ @shape = shape
15
+ end
16
+
17
+ def transform(params)
18
+ return params unless params.is_a?(Hash)
19
+
20
+ transformed = params.dup
21
+
22
+ @shape.params.each do |name, param_options|
23
+ next unless transformed.key?(name)
24
+
25
+ value = transformed[name]
26
+
27
+ if param_options[:as]
28
+ transformed[param_options[:as]] = transformed.delete(name)
29
+ name = param_options[:as]
30
+ value = transformed[name]
31
+ end
32
+
33
+ if param_options[:shape] && value.is_a?(Hash)
34
+ transformed[name] = Transformer.transform(param_options[:shape], value)
35
+ elsif param_options[:type] == :array && value.is_a?(Array)
36
+ of = param_options[:of]
37
+ of_shape = of&.shape
38
+
39
+ if of_shape
40
+ transformed[name] = value.map do |item|
41
+ item.is_a?(Hash) ? Transformer.transform(of_shape, item) : item
42
+ end
43
+ elsif of && (array_result = transform_custom_type_array(value, param_options))
44
+ transformed[name] = array_result
45
+ end
46
+ end
47
+ end
48
+
49
+ transformed
50
+ end
51
+
52
+ private
53
+
54
+ def transform_custom_type_array(value, param_options)
55
+ of = param_options[:of]
56
+ type_name = of&.type
57
+ custom_type_shape = resolve_custom_type_shape(type_name)
58
+ return nil unless custom_type_shape
59
+
60
+ value.map do |item|
61
+ item.is_a?(Hash) ? Transformer.transform(custom_type_shape, item) : item
62
+ end
63
+ end
64
+
65
+ def resolve_custom_type_shape(type_name)
66
+ contract_class = @shape.contract_class
67
+ type_definition = contract_class.resolve_custom_type(type_name)
68
+ return nil unless type_definition
69
+
70
+ scope_contract_class = type_definition.scope || contract_class
71
+
72
+ if type_definition.object?
73
+ build_type_shape(type_definition, scope_contract_class)
74
+ elsif type_definition.union?
75
+ first_variant = type_definition.variants.first
76
+ return nil unless first_variant
77
+
78
+ variant_type_definition = scope_contract_class.resolve_custom_type(first_variant[:type])
79
+ return nil unless variant_type_definition
80
+ return nil unless variant_type_definition.object?
81
+
82
+ variant_contract_class = variant_type_definition.scope || scope_contract_class
83
+ build_type_shape(variant_type_definition, variant_contract_class)
84
+ end
85
+ end
86
+
87
+ def build_type_shape(type_definition, contract_class)
88
+ type_shape = Object.new(contract_class, action_name: @shape.action_name)
89
+ type_shape.copy_type_definition_params(type_definition, type_shape)
90
+ type_shape
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Contract
5
+ class Object
6
+ class Validator
7
+ class Result
8
+ attr_reader :issues,
9
+ :params
10
+
11
+ def initialize(issues: [], params:)
12
+ @issues = issues
13
+ @params = params
14
+ end
15
+
16
+ def valid?
17
+ issues.empty?
18
+ end
19
+
20
+ def invalid?
21
+ issues.any?
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end