apiwork 0.0.0.pre → 0.1.2

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 +638 -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,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Representation
5
+ class Deserializer
6
+ class << self
7
+ def deserialize(representation_class, payload)
8
+ new(representation_class).deserialize(payload)
9
+ end
10
+ end
11
+
12
+ def initialize(representation_class)
13
+ @representation_class = representation_class
14
+ end
15
+
16
+ def deserialize(payload)
17
+ if payload.is_a?(Array)
18
+ payload.map { |hash| deserialize_hash(hash) }
19
+ else
20
+ deserialize_hash(payload)
21
+ end
22
+ end
23
+
24
+ def deserialize_hash(hash)
25
+ return hash unless hash.is_a?(Hash)
26
+
27
+ result = hash.dup
28
+
29
+ transform_type_columns(result)
30
+
31
+ @representation_class.attributes.each do |name, attribute|
32
+ next unless result.key?(name)
33
+
34
+ result[name] = attribute.decode(result[name])
35
+ end
36
+
37
+ @representation_class.associations.each do |name, association|
38
+ next unless result.key?(name)
39
+
40
+ nested_representation_class = association.representation_class
41
+ next unless nested_representation_class
42
+
43
+ value = result[name]
44
+ result[name] = if association.collection? && value.is_a?(Array)
45
+ value.map { |item| nested_representation_class.deserialize(item) }
46
+ elsif value.is_a?(Hash)
47
+ nested_representation_class.deserialize(value)
48
+ else
49
+ value
50
+ end
51
+ end
52
+
53
+ result
54
+ end
55
+
56
+ private
57
+
58
+ def transform_type_columns(hash)
59
+ transform_sti_type(hash)
60
+ transform_polymorphic_types(hash)
61
+ end
62
+
63
+ def transform_sti_type(hash)
64
+ inheritance_config = @representation_class.subclass? ? @representation_class.superclass.inheritance : @representation_class.inheritance
65
+ return unless inheritance_config&.transform?
66
+
67
+ column = inheritance_config.column
68
+ return unless hash.key?(column)
69
+
70
+ api_value = hash[column]
71
+ db_value = inheritance_config.mapping[api_value]
72
+ hash[column] = db_value if db_value
73
+ end
74
+
75
+ def transform_polymorphic_types(hash)
76
+ @representation_class.attributes.each_key do |name|
77
+ next unless hash.key?(name)
78
+
79
+ association = @representation_class.polymorphic_association_for_type_column(name)
80
+ next unless association
81
+
82
+ api_value = hash[name]
83
+ next unless api_value.is_a?(String)
84
+
85
+ polymorphic_representation = association.polymorphic.find do |representation|
86
+ representation.polymorphic_name == api_value
87
+ end
88
+ next unless polymorphic_representation
89
+
90
+ hash[name] = polymorphic_representation.model_class.polymorphic_name
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Representation
5
+ # @api public
6
+ # Block context for defining JSON blob structure in representation attributes.
7
+ #
8
+ # Used inside attribute blocks to define the shape of JSON/JSONB columns,
9
+ # Rails store attributes, or any serialized data structure.
10
+ #
11
+ # Only complex types are allowed at the top level:
12
+ # - {#object} for key-value structures
13
+ # - {#array} for ordered collections
14
+ # - {#union} for polymorphic structures
15
+ #
16
+ # Inside these blocks, the full type DSL is available including
17
+ # nested objects, arrays, primitives, and unions.
18
+ #
19
+ # @see API::Element Block context for array elements
20
+ # @see API::Object Block context for object fields
21
+ # @see API::Union Block context for union variants
22
+ #
23
+ # @example Object structure
24
+ # attribute :settings do
25
+ # object do
26
+ # string :theme
27
+ # boolean :notifications
28
+ # integer :max_items, min: 1, max: 100
29
+ # end
30
+ # end
31
+ #
32
+ # @example Array of objects
33
+ # attribute :addresses do
34
+ # array do
35
+ # object do
36
+ # string :street
37
+ # string :city
38
+ # string :zip
39
+ # boolean :primary
40
+ # end
41
+ # end
42
+ # end
43
+ #
44
+ # @example Nested structures
45
+ # attribute :config do
46
+ # object do
47
+ # string :name
48
+ # array :tags do
49
+ # string
50
+ # end
51
+ # object :metadata do
52
+ # datetime :created_at
53
+ # datetime :updated_at
54
+ # end
55
+ # end
56
+ # end
57
+ #
58
+ # @example Union for polymorphic data
59
+ # attribute :payment_details do
60
+ # union discriminator: :type do
61
+ # variant tag: 'card' do
62
+ # object do
63
+ # string :last_four
64
+ # string :brand
65
+ # end
66
+ # end
67
+ # variant tag: 'bank' do
68
+ # object do
69
+ # string :account_number
70
+ # string :routing_number
71
+ # end
72
+ # end
73
+ # end
74
+ # end
75
+ class Element < Apiwork::Element
76
+ def validate!
77
+ raise ConfigurationError, 'must define exactly one type (object, array, or union)' unless @defined
78
+ end
79
+
80
+ # @api public
81
+ # Defines the element type.
82
+ #
83
+ # Only complex types (:object, :array, :union) are allowed.
84
+ #
85
+ # @param type [Symbol] [:array, :object, :union]
86
+ # The element type.
87
+ # @param discriminator [Symbol, nil] (nil)
88
+ # The discriminator field name. Unions only.
89
+ # @yield block for defining nested structure (instance_eval style)
90
+ # @yieldparam shape [API::Object, API::Union, API::Element]
91
+ # @return [void]
92
+ # @raise [ArgumentError] if object, array, or union type is missing block
93
+ def of(type, discriminator: nil, &block)
94
+ case type
95
+ when :object
96
+ raise ConfigurationError, 'object requires a block' unless block
97
+
98
+ builder = API::Object.new
99
+ block.arity.positive? ? yield(builder) : builder.instance_eval(&block)
100
+ @type = :object
101
+ @shape = builder
102
+ @defined = true
103
+ when :array
104
+ raise ConfigurationError, 'array requires a block' unless block
105
+
106
+ inner = API::Element.new
107
+ block.arity.positive? ? yield(inner) : inner.instance_eval(&block)
108
+ inner.validate!
109
+ @type = :array
110
+ @inner = inner
111
+ @shape = inner.shape
112
+ @defined = true
113
+ when :union
114
+ raise ConfigurationError, 'union requires a block' unless block
115
+
116
+ builder = API::Union.new(discriminator:)
117
+ block.arity.positive? ? yield(builder) : builder.instance_eval(&block)
118
+ @type = :union
119
+ @shape = builder
120
+ @discriminator = discriminator
121
+ @defined = true
122
+ else
123
+ raise ConfigurationError, "Representation::Element only supports :object, :array, :union - got #{type.inspect}"
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Representation
5
+ # @api public
6
+ # Tracks STI subclass representations for a base representation.
7
+ #
8
+ # Created automatically when a representation's model uses STI.
9
+ # Provides resolution of records to their correct subclass representation.
10
+ #
11
+ # @example
12
+ # ClientRepresentation.inheritance.column # => :type
13
+ # ClientRepresentation.inheritance.subclasses # => [PersonClientRepresentation, ...]
14
+ # ClientRepresentation.inheritance.resolve(record) # => PersonClientRepresentation
15
+ class Inheritance
16
+ # @!attribute [r] base_class
17
+ # @api public
18
+ # The base class for this inheritance.
19
+ #
20
+ # @return [Class<Representation::Base>]
21
+ # @!attribute [r] subclasses
22
+ # @api public
23
+ # The subclasses for this inheritance.
24
+ #
25
+ # @return [Array<Class<Representation::Base>>]
26
+ attr_reader :base_class,
27
+ :subclasses
28
+
29
+ def initialize(base_class)
30
+ @base_class = base_class
31
+ @subclasses = []
32
+ end
33
+
34
+ # @api public
35
+ # The column for this inheritance.
36
+ #
37
+ # @return [Symbol]
38
+ def column
39
+ @base_class.model_class.inheritance_column.to_sym
40
+ end
41
+
42
+ # @api public
43
+ # Resolves a record to its subclass representation.
44
+ #
45
+ # @param record [ActiveRecord::Base]
46
+ # The record to resolve.
47
+ # @return [Class<Representation::Base>, nil]
48
+ def resolve(record)
49
+ type_value = record.public_send(column)
50
+ @subclasses.find { |klass| klass.model_class.sti_name == type_value }
51
+ end
52
+
53
+ # @api public
54
+ # Whether this inheritance requires type transformation.
55
+ #
56
+ # @return [Boolean]
57
+ def transform?
58
+ @subclasses.any? { |klass| klass.sti_name != klass.model_class.sti_name }
59
+ end
60
+
61
+ # @api public
62
+ # Mapping of API names to database type values.
63
+ #
64
+ # @return [Hash{String => String}]
65
+ def mapping
66
+ @subclasses.to_h { |klass| [klass.sti_name, klass.model_class.sti_name] }
67
+ end
68
+
69
+ def register(representation_class)
70
+ @subclasses << representation_class
71
+ end
72
+
73
+ def subclass?(representation_class)
74
+ @subclasses.include?(representation_class)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Representation
5
+ class ModelDetector
6
+ def initialize(representation_class)
7
+ @representation_class = representation_class
8
+ end
9
+
10
+ def detect
11
+ return nil if @representation_class.abstract?
12
+
13
+ full_name = @representation_class.name
14
+ return nil unless full_name
15
+
16
+ representation_name = full_name.demodulize
17
+ model_name = representation_name.delete_suffix('Representation')
18
+ return nil if model_name.blank?
19
+
20
+ resolve_model_class(full_name, model_name)
21
+ end
22
+
23
+ def sti_base?(model_class)
24
+ return false if model_class.abstract_class?
25
+
26
+ column = model_class.inheritance_column
27
+ return false unless column
28
+
29
+ begin
30
+ return false unless model_class.column_names.include?(column.to_s)
31
+ rescue ActiveRecord::StatementInvalid, ActiveRecord::NoDatabaseError, ActiveRecord::ConnectionNotEstablished
32
+ return false
33
+ end
34
+
35
+ model_class == model_class.base_class
36
+ end
37
+
38
+ def sti_subclass?(model_class)
39
+ return false if model_class.abstract_class?
40
+
41
+ model_class != model_class.base_class
42
+ end
43
+
44
+ def superclass_is_sti_base?(model_class)
45
+ parent_model = @representation_class.superclass.model_class
46
+ return false unless parent_model
47
+
48
+ parent_model == model_class.base_class
49
+ end
50
+
51
+ private
52
+
53
+ def resolve_model_class(full_name, model_name)
54
+ namespace = full_name.deconstantize
55
+ model_class = if namespace.present?
56
+ "#{namespace}::#{model_name}".safe_constantize || model_name.safe_constantize
57
+ else
58
+ model_name.safe_constantize
59
+ end
60
+
61
+ if model_class.is_a?(Class) && model_class < ActiveRecord::Base
62
+ model_class
63
+ else
64
+ raise ConfigurationError.new(
65
+ code: :model_not_found,
66
+ detail: "Could not find model '#{model_name}' for #{full_name}. " \
67
+ "Either create the model, declare it explicitly with 'model YourModel', " \
68
+ "or mark this representation as abstract with 'abstract!'",
69
+ path: [],
70
+ )
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Representation
5
+ # @api public
6
+ # Represents the JSON root key for a representation.
7
+ #
8
+ # Root keys wrap response data in a named container.
9
+ # Used by adapters to structure JSON responses.
10
+ #
11
+ # @example
12
+ # root_key = InvoiceRepresentation.root_key
13
+ # root_key.singular # => "invoice"
14
+ # root_key.plural # => "invoices"
15
+ class RootKey
16
+ # @!attribute [r] plural
17
+ # @api public
18
+ # The plural root key.
19
+ #
20
+ # @return [String]
21
+ # @!attribute [r] singular
22
+ # @api public
23
+ # The singular root key.
24
+ #
25
+ # @return [String]
26
+ attr_reader :plural,
27
+ :singular
28
+
29
+ def initialize(singular, plural = singular.pluralize)
30
+ @singular = singular
31
+ @plural = plural
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Representation
5
+ class Serializer
6
+ class << self
7
+ def serialize(representation, includes)
8
+ new(representation, includes).serialize
9
+ end
10
+ end
11
+
12
+ def initialize(representation, includes)
13
+ @representation = representation
14
+ @representation_class = representation.class
15
+ @includes = includes
16
+ end
17
+
18
+ def serialize
19
+ fields = {}
20
+
21
+ add_discriminator_field(fields) if @representation_class.subclass?
22
+
23
+ @representation_class.attributes.each do |name, attribute|
24
+ value = @representation.respond_to?(name) ? @representation.public_send(name) : @representation.record.public_send(name)
25
+ value = map_type_column_output(name, value)
26
+ value = attribute.encode(value)
27
+ fields[name] = value
28
+ end
29
+
30
+ @representation_class.associations.each do |name, association|
31
+ next unless include_association?(name, association)
32
+
33
+ fields[name] = serialize_association(name, association)
34
+ end
35
+
36
+ fields
37
+ end
38
+
39
+ private
40
+
41
+ def add_discriminator_field(fields)
42
+ parent_representation = @representation_class.superclass
43
+ return unless parent_representation.inheritance
44
+
45
+ fields[parent_representation.inheritance.column] = @representation_class.sti_name
46
+ end
47
+
48
+ def map_type_column_output(attribute_name, value)
49
+ return value if value.nil?
50
+
51
+ association = @representation_class.polymorphic_association_for_type_column(attribute_name)
52
+ if association
53
+ found_class = association.find_representation_for_type(value)
54
+ return found_class.polymorphic_name if found_class
55
+ end
56
+
57
+ inheritance = @representation_class.inheritance_for_column(attribute_name)
58
+ if inheritance
59
+ klass = inheritance.subclasses.find { |subclass| subclass.model_class.sti_name == value }
60
+ return klass.sti_name if klass
61
+ end
62
+
63
+ value
64
+ end
65
+
66
+ def serialize_association(name, association)
67
+ target = @representation.respond_to?(name) ? @representation.public_send(name) : @representation.record.public_send(name)
68
+ return nil if target.nil?
69
+
70
+ target_representation_class = association.representation_class
71
+ return nil unless target_representation_class
72
+
73
+ nested_includes = @includes[name] || @includes[name.to_s] || @includes[name.to_sym] if @includes.is_a?(Hash)
74
+
75
+ if association.collection?
76
+ target.map { |record| serialize_variant_aware(record, target_representation_class, nested_includes) }
77
+ else
78
+ serialize_variant_aware(target, target_representation_class, nested_includes)
79
+ end
80
+ end
81
+
82
+ def serialize_variant_aware(record, target_representation_class, nested_includes)
83
+ if target_representation_class.inheritance&.subclasses&.any?
84
+ subclass_representation_class = target_representation_class.inheritance.resolve(record)
85
+ end
86
+ representation_class = subclass_representation_class || target_representation_class
87
+
88
+ representation_class.new(record, context: @representation.context, include: nested_includes).as_json
89
+ end
90
+
91
+ def include_association?(name, association)
92
+ return explicitly_included?(name) unless association.include == :always
93
+ return true unless circular_reference?(association)
94
+
95
+ false
96
+ end
97
+
98
+ def circular_reference?(association)
99
+ return false unless association.representation_class
100
+
101
+ association.representation_class.associations.values.any? do |nested_association|
102
+ nested_association.include == :always && nested_association.representation_class == @representation_class
103
+ end
104
+ end
105
+
106
+ def explicitly_included?(name)
107
+ return false if @includes.nil?
108
+
109
+ case @includes
110
+ when Symbol, String
111
+ @includes.to_sym == name
112
+ when Array
113
+ include_symbols.include?(name)
114
+ when Hash
115
+ name = name.to_sym
116
+ @includes.key?(name) || @includes.key?(name.to_s)
117
+ else
118
+ false
119
+ end
120
+ end
121
+
122
+ def include_symbols
123
+ @include_symbols ||= @includes.map(&:to_sym)
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ # @api public
5
+ # Immutable value object representing a request.
6
+ #
7
+ # Encapsulates query and body parameters. Transformations return
8
+ # new instances, preserving immutability.
9
+ #
10
+ # @example Creating a request
11
+ # request = Request.new(query: { page: 1 }, body: { title: "Hello" })
12
+ # request.query # => { page: 1 }
13
+ # request.body # => { title: "Hello" }
14
+ #
15
+ # @example Transforming keys
16
+ # request.transform { |data| normalize(data) }
17
+ class Request
18
+ # @!attribute [r] query
19
+ # @api public
20
+ # The query for this request.
21
+ #
22
+ # @return [Hash]
23
+ # @!attribute [r] body
24
+ # @api public
25
+ # The body for this request.
26
+ #
27
+ # @return [Hash]
28
+ attr_reader :body,
29
+ :query
30
+
31
+ # @api public
32
+ # Creates a new request context.
33
+ #
34
+ # @param body [Hash]
35
+ # The body parameters.
36
+ # @param query [Hash]
37
+ # The query parameters.
38
+ def initialize(body:, query:)
39
+ @query = query
40
+ @body = body
41
+ end
42
+
43
+ # @api public
44
+ # Transforms both query and body with the same block.
45
+ #
46
+ # @yield [Hash] each field (query, then body)
47
+ # @return [Request]
48
+ #
49
+ # @example
50
+ # request.transform { |data| normalize(data) }
51
+ def transform
52
+ self.class.new(body: yield(body), query: yield(query))
53
+ end
54
+
55
+ # @api public
56
+ # Transforms only the query.
57
+ #
58
+ # @yield [Hash] the query parameters
59
+ # @return [Request]
60
+ #
61
+ # @example
62
+ # request.transform_query { |query| normalize(query) }
63
+ def transform_query
64
+ self.class.new(body: body, query: yield(query))
65
+ end
66
+
67
+ # @api public
68
+ # Transforms only the body.
69
+ #
70
+ # @yield [Hash] the body parameters
71
+ # @return [Request]
72
+ #
73
+ # @example
74
+ # request.transform_body { |body| prepare(body) }
75
+ def transform_body
76
+ self.class.new(body: yield(body), query: query)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ # @api public
5
+ # Immutable value object representing a response.
6
+ #
7
+ # Encapsulates body parameters. Transformations return new instances,
8
+ # preserving immutability.
9
+ #
10
+ # @example Creating a response
11
+ # response = Response.new(body: { id: 1, title: "Hello" })
12
+ # response.body # => { id: 1, title: "Hello" }
13
+ #
14
+ # @example Transforming keys
15
+ # response.transform { |data| camelize(data) }
16
+ class Response
17
+ # @api public
18
+ # The body for this response.
19
+ #
20
+ # @return [Hash]
21
+ attr_reader :body
22
+
23
+ # @api public
24
+ # Creates a new response context.
25
+ #
26
+ # @param body [Hash]
27
+ # The body parameters.
28
+ def initialize(body:)
29
+ @body = body
30
+ end
31
+
32
+ # @api public
33
+ # Transforms the body parameters.
34
+ #
35
+ # @yield [Hash] the body parameters
36
+ # @return [Response]
37
+ #
38
+ # @example
39
+ # response.transform { |data| camelize(data) }
40
+ def transform
41
+ self.class.new(body: yield(body))
42
+ end
43
+
44
+ # @api public
45
+ # Transforms the body parameters.
46
+ #
47
+ # @yield [Hash] the body parameters
48
+ # @return [Response]
49
+ #
50
+ # @example
51
+ # response.transform_body { |data| camelize(data) }
52
+ def transform_body
53
+ self.class.new(body: yield(body))
54
+ end
55
+ end
56
+ end