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.
- checksums.yaml +4 -4
- data/LICENSE.txt +2 -2
- data/README.md +117 -1
- data/Rakefile +5 -3
- data/app/controllers/apiwork/errors_controller.rb +13 -0
- data/app/controllers/apiwork/exports_controller.rb +22 -0
- data/lib/apiwork/abstractable.rb +26 -0
- data/lib/apiwork/adapter/base.rb +369 -0
- data/lib/apiwork/adapter/builder/api/base.rb +66 -0
- data/lib/apiwork/adapter/builder/contract/base.rb +86 -0
- data/lib/apiwork/adapter/capability/api/base.rb +51 -0
- data/lib/apiwork/adapter/capability/api/scope.rb +64 -0
- data/lib/apiwork/adapter/capability/base.rb +291 -0
- data/lib/apiwork/adapter/capability/contract/base.rb +37 -0
- data/lib/apiwork/adapter/capability/contract/scope.rb +110 -0
- data/lib/apiwork/adapter/capability/operation/base.rb +172 -0
- data/lib/apiwork/adapter/capability/operation/metadata_shape.rb +165 -0
- data/lib/apiwork/adapter/capability/result.rb +21 -0
- data/lib/apiwork/adapter/capability/runner.rb +56 -0
- data/lib/apiwork/adapter/capability/transformer/request/base.rb +72 -0
- data/lib/apiwork/adapter/capability/transformer/response/base.rb +45 -0
- data/lib/apiwork/adapter/registry.rb +16 -0
- data/lib/apiwork/adapter/serializer/error/base.rb +72 -0
- data/lib/apiwork/adapter/serializer/error/default/api_builder.rb +32 -0
- data/lib/apiwork/adapter/serializer/error/default.rb +37 -0
- data/lib/apiwork/adapter/serializer/resource/base.rb +84 -0
- data/lib/apiwork/adapter/serializer/resource/default/contract_builder.rb +209 -0
- data/lib/apiwork/adapter/serializer/resource/default.rb +39 -0
- data/lib/apiwork/adapter/standard/capability/filtering/api_builder.rb +75 -0
- data/lib/apiwork/adapter/standard/capability/filtering/constants.rb +37 -0
- data/lib/apiwork/adapter/standard/capability/filtering/contract_builder.rb +193 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation/filter/builder.rb +47 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation/filter/operator_builder.rb +36 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation/filter.rb +462 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation.rb +22 -0
- data/lib/apiwork/adapter/standard/capability/filtering/request_transformer.rb +47 -0
- data/lib/apiwork/adapter/standard/capability/filtering.rb +18 -0
- data/lib/apiwork/adapter/standard/capability/including/contract_builder.rb +169 -0
- data/lib/apiwork/adapter/standard/capability/including/operation.rb +20 -0
- data/lib/apiwork/adapter/standard/capability/including.rb +16 -0
- data/lib/apiwork/adapter/standard/capability/pagination/api_builder.rb +34 -0
- data/lib/apiwork/adapter/standard/capability/pagination/contract_builder.rb +35 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate/cursor.rb +84 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate/offset.rb +66 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate.rb +24 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation.rb +24 -0
- data/lib/apiwork/adapter/standard/capability/pagination.rb +21 -0
- data/lib/apiwork/adapter/standard/capability/sorting/api_builder.rb +19 -0
- data/lib/apiwork/adapter/standard/capability/sorting/contract_builder.rb +84 -0
- data/lib/apiwork/adapter/standard/capability/sorting/operation/sort.rb +83 -0
- data/lib/apiwork/adapter/standard/capability/sorting/operation.rb +22 -0
- data/lib/apiwork/adapter/standard/capability/sorting.rb +17 -0
- data/lib/apiwork/adapter/standard/capability/writing/constants.rb +15 -0
- data/lib/apiwork/adapter/standard/capability/writing/contract_builder.rb +253 -0
- data/lib/apiwork/adapter/standard/capability/writing/operation/issue_mapper.rb +210 -0
- data/lib/apiwork/adapter/standard/capability/writing/operation.rb +32 -0
- data/lib/apiwork/adapter/standard/capability/writing/request_transformer.rb +37 -0
- data/lib/apiwork/adapter/standard/capability/writing.rb +17 -0
- data/lib/apiwork/adapter/standard/includes_resolver.rb +106 -0
- data/lib/apiwork/adapter/standard.rb +22 -0
- data/lib/apiwork/adapter/wrapper/base.rb +70 -0
- data/lib/apiwork/adapter/wrapper/collection/base.rb +60 -0
- data/lib/apiwork/adapter/wrapper/collection/default.rb +47 -0
- data/lib/apiwork/adapter/wrapper/error/base.rb +30 -0
- data/lib/apiwork/adapter/wrapper/error/default.rb +34 -0
- data/lib/apiwork/adapter/wrapper/member/base.rb +58 -0
- data/lib/apiwork/adapter/wrapper/member/default.rb +40 -0
- data/lib/apiwork/adapter/wrapper/shape.rb +203 -0
- data/lib/apiwork/adapter.rb +50 -0
- data/lib/apiwork/api/base.rb +802 -0
- data/lib/apiwork/api/element.rb +110 -0
- data/lib/apiwork/api/enum_registry/definition.rb +51 -0
- data/lib/apiwork/api/enum_registry.rb +98 -0
- data/lib/apiwork/api/info/contact.rb +67 -0
- data/lib/apiwork/api/info/license.rb +50 -0
- data/lib/apiwork/api/info/server.rb +50 -0
- data/lib/apiwork/api/info.rb +221 -0
- data/lib/apiwork/api/object.rb +235 -0
- data/lib/apiwork/api/registry.rb +33 -0
- data/lib/apiwork/api/representation_registry.rb +76 -0
- data/lib/apiwork/api/resource/action.rb +41 -0
- data/lib/apiwork/api/resource.rb +648 -0
- data/lib/apiwork/api/router.rb +104 -0
- data/lib/apiwork/api/type_registry/definition.rb +117 -0
- data/lib/apiwork/api/type_registry.rb +99 -0
- data/lib/apiwork/api/union.rb +49 -0
- data/lib/apiwork/api.rb +85 -0
- data/lib/apiwork/configurable.rb +71 -0
- data/lib/apiwork/configuration/option.rb +125 -0
- data/lib/apiwork/configuration/validatable.rb +25 -0
- data/lib/apiwork/configuration.rb +95 -0
- data/lib/apiwork/configuration_error.rb +6 -0
- data/lib/apiwork/constraint_error.rb +20 -0
- data/lib/apiwork/contract/action/request.rb +79 -0
- data/lib/apiwork/contract/action/response.rb +87 -0
- data/lib/apiwork/contract/action.rb +258 -0
- data/lib/apiwork/contract/base.rb +714 -0
- data/lib/apiwork/contract/element.rb +130 -0
- data/lib/apiwork/contract/object/coercer.rb +194 -0
- data/lib/apiwork/contract/object/deserializer.rb +101 -0
- data/lib/apiwork/contract/object/transformer.rb +95 -0
- data/lib/apiwork/contract/object/validator/result.rb +27 -0
- data/lib/apiwork/contract/object/validator.rb +734 -0
- data/lib/apiwork/contract/object.rb +566 -0
- data/lib/apiwork/contract/request_parser/result.rb +25 -0
- data/lib/apiwork/contract/request_parser.rb +72 -0
- data/lib/apiwork/contract/response_parser/result.rb +25 -0
- data/lib/apiwork/contract/response_parser.rb +35 -0
- data/lib/apiwork/contract/union.rb +56 -0
- data/lib/apiwork/contract_error.rb +9 -0
- data/lib/apiwork/controller.rb +300 -0
- data/lib/apiwork/domain_error.rb +13 -0
- data/lib/apiwork/element.rb +386 -0
- data/lib/apiwork/engine.rb +20 -0
- data/lib/apiwork/error.rb +6 -0
- data/lib/apiwork/error_code/definition.rb +63 -0
- data/lib/apiwork/error_code/registry.rb +18 -0
- data/lib/apiwork/error_code.rb +132 -0
- data/lib/apiwork/export/base.rb +291 -0
- data/lib/apiwork/export/open_api.rb +600 -0
- data/lib/apiwork/export/pipeline/writer.rb +66 -0
- data/lib/apiwork/export/pipeline.rb +84 -0
- data/lib/apiwork/export/registry.rb +16 -0
- data/lib/apiwork/export/surface_resolver.rb +189 -0
- data/lib/apiwork/export/type_analysis.rb +170 -0
- data/lib/apiwork/export/type_script.rb +23 -0
- data/lib/apiwork/export/type_script_mapper.rb +349 -0
- data/lib/apiwork/export/zod.rb +39 -0
- data/lib/apiwork/export/zod_mapper.rb +421 -0
- data/lib/apiwork/export.rb +80 -0
- data/lib/apiwork/http_error.rb +16 -0
- data/lib/apiwork/introspection/action/request.rb +66 -0
- data/lib/apiwork/introspection/action/response.rb +57 -0
- data/lib/apiwork/introspection/action.rb +124 -0
- data/lib/apiwork/introspection/api/info/contact.rb +59 -0
- data/lib/apiwork/introspection/api/info/license.rb +49 -0
- data/lib/apiwork/introspection/api/info/server.rb +50 -0
- data/lib/apiwork/introspection/api/info.rb +107 -0
- data/lib/apiwork/introspection/api/resource.rb +83 -0
- data/lib/apiwork/introspection/api.rb +92 -0
- data/lib/apiwork/introspection/contract.rb +63 -0
- data/lib/apiwork/introspection/dump/action.rb +101 -0
- data/lib/apiwork/introspection/dump/api.rb +119 -0
- data/lib/apiwork/introspection/dump/contract.rb +129 -0
- data/lib/apiwork/introspection/dump/param.rb +486 -0
- data/lib/apiwork/introspection/dump/resource.rb +112 -0
- data/lib/apiwork/introspection/dump/type.rb +339 -0
- data/lib/apiwork/introspection/dump.rb +17 -0
- data/lib/apiwork/introspection/enum.rb +63 -0
- data/lib/apiwork/introspection/error_code.rb +44 -0
- data/lib/apiwork/introspection/param/array.rb +88 -0
- data/lib/apiwork/introspection/param/base.rb +285 -0
- data/lib/apiwork/introspection/param/binary.rb +73 -0
- data/lib/apiwork/introspection/param/boolean.rb +73 -0
- data/lib/apiwork/introspection/param/date.rb +73 -0
- data/lib/apiwork/introspection/param/date_time.rb +73 -0
- data/lib/apiwork/introspection/param/decimal.rb +121 -0
- data/lib/apiwork/introspection/param/integer.rb +131 -0
- data/lib/apiwork/introspection/param/literal.rb +45 -0
- data/lib/apiwork/introspection/param/number.rb +121 -0
- data/lib/apiwork/introspection/param/object.rb +59 -0
- data/lib/apiwork/introspection/param/reference.rb +45 -0
- data/lib/apiwork/introspection/param/string.rb +122 -0
- data/lib/apiwork/introspection/param/time.rb +73 -0
- data/lib/apiwork/introspection/param/union.rb +57 -0
- data/lib/apiwork/introspection/param/unknown.rb +26 -0
- data/lib/apiwork/introspection/param/uuid.rb +73 -0
- data/lib/apiwork/introspection/param.rb +31 -0
- data/lib/apiwork/introspection/type.rb +129 -0
- data/lib/apiwork/introspection.rb +28 -0
- data/lib/apiwork/issue.rb +80 -0
- data/lib/apiwork/json_pointer.rb +21 -0
- data/lib/apiwork/object.rb +1618 -0
- data/lib/apiwork/reference_generator.rb +638 -0
- data/lib/apiwork/registry.rb +56 -0
- data/lib/apiwork/representation/association.rb +391 -0
- data/lib/apiwork/representation/attribute.rb +335 -0
- data/lib/apiwork/representation/base.rb +819 -0
- data/lib/apiwork/representation/deserializer.rb +95 -0
- data/lib/apiwork/representation/element.rb +128 -0
- data/lib/apiwork/representation/inheritance.rb +78 -0
- data/lib/apiwork/representation/model_detector.rb +75 -0
- data/lib/apiwork/representation/root_key.rb +35 -0
- data/lib/apiwork/representation/serializer.rb +127 -0
- data/lib/apiwork/request.rb +79 -0
- data/lib/apiwork/response.rb +56 -0
- data/lib/apiwork/union.rb +102 -0
- data/lib/apiwork/version.rb +2 -2
- data/lib/apiwork.rb +61 -3
- data/lib/generators/apiwork/api_generator.rb +38 -0
- data/lib/generators/apiwork/contract_generator.rb +25 -0
- data/lib/generators/apiwork/install_generator.rb +27 -0
- data/lib/generators/apiwork/representation_generator.rb +25 -0
- data/lib/generators/apiwork/templates/api/api.rb.tt +4 -0
- data/lib/generators/apiwork/templates/contract/contract.rb.tt +6 -0
- data/lib/generators/apiwork/templates/install/application_contract.rb.tt +5 -0
- data/lib/generators/apiwork/templates/install/application_representation.rb.tt +5 -0
- data/lib/generators/apiwork/templates/representation/representation.rb.tt +6 -0
- data/lib/tasks/apiwork.rake +102 -0
- metadata +319 -19
- data/.rubocop.yml +0 -8
- data/sig/apiwork.rbs +0 -4
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Contract
|
|
5
|
+
class ResponseParser
|
|
6
|
+
class << self
|
|
7
|
+
def parse(contract_class, action_name, response)
|
|
8
|
+
new(contract_class, action_name).parse(response)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(contract_class, action_name)
|
|
13
|
+
@contract_class = contract_class
|
|
14
|
+
@action_name = action_name.to_sym
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def parse(response)
|
|
18
|
+
return Result.new(response:) unless action
|
|
19
|
+
|
|
20
|
+
shape = action.response.body
|
|
21
|
+
return Result.new(response:) unless shape
|
|
22
|
+
return Result.new(response:) unless shape.params.any?
|
|
23
|
+
|
|
24
|
+
validated = shape.validate(response.body)
|
|
25
|
+
Result.new(issues: validated.issues, response: Response.new(body: validated.params))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def action
|
|
31
|
+
@action ||= @contract_class.action_for(@action_name)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Contract
|
|
5
|
+
# @api public
|
|
6
|
+
# Block context for defining inline union types.
|
|
7
|
+
#
|
|
8
|
+
# Accessed via `union :name, discriminator: do` inside contract actions.
|
|
9
|
+
# Use {#variant} to define possible types.
|
|
10
|
+
#
|
|
11
|
+
# @see API::Union Block context for reusable unions
|
|
12
|
+
# @see Contract::Element Block context for variant types
|
|
13
|
+
#
|
|
14
|
+
# @example instance_eval style
|
|
15
|
+
# union :payment_method, discriminator: :type do
|
|
16
|
+
# variant tag: 'card' do
|
|
17
|
+
# object do
|
|
18
|
+
# string :last_four
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
# variant tag: 'bank' do
|
|
22
|
+
# object do
|
|
23
|
+
# string :account_number
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# @example yield style
|
|
29
|
+
# union :payment_method, discriminator: :type do |union|
|
|
30
|
+
# union.variant tag: 'card' do |variant|
|
|
31
|
+
# variant.object do |object|
|
|
32
|
+
# object.string :last_four
|
|
33
|
+
# end
|
|
34
|
+
# end
|
|
35
|
+
# union.variant tag: 'bank' do |variant|
|
|
36
|
+
# variant.object do |object|
|
|
37
|
+
# object.string :account_number
|
|
38
|
+
# end
|
|
39
|
+
# end
|
|
40
|
+
# end
|
|
41
|
+
class Union < Apiwork::Union
|
|
42
|
+
attr_reader :contract_class
|
|
43
|
+
|
|
44
|
+
def initialize(contract_class, discriminator: nil)
|
|
45
|
+
super(discriminator:)
|
|
46
|
+
@contract_class = contract_class
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def build_element
|
|
52
|
+
Element.new(@contract_class)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
# @api public
|
|
5
|
+
# Mixin for API controllers that provides request validation and response helpers.
|
|
6
|
+
#
|
|
7
|
+
# Include in controllers to access {#contract}, {#expose}, and {#expose_error}.
|
|
8
|
+
# Automatically validates requests against the contract before actions run.
|
|
9
|
+
#
|
|
10
|
+
# @example Basic controller
|
|
11
|
+
# class InvoicesController < ApplicationController
|
|
12
|
+
# include Apiwork::Controller
|
|
13
|
+
#
|
|
14
|
+
# def index
|
|
15
|
+
# expose Invoice.all
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# def show
|
|
19
|
+
# invoice = Invoice.find(params[:id])
|
|
20
|
+
# expose invoice
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# def create
|
|
24
|
+
# invoice = Invoice.create(contract.body[:invoice])
|
|
25
|
+
# expose invoice
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
module Controller
|
|
29
|
+
extend ActiveSupport::Concern
|
|
30
|
+
|
|
31
|
+
included do
|
|
32
|
+
wrap_parameters false
|
|
33
|
+
|
|
34
|
+
before_action :validate_contract
|
|
35
|
+
|
|
36
|
+
rescue_from ConstraintError do |error|
|
|
37
|
+
render_error error
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @!method self.skip_contract_validation!(only: nil, except: nil)
|
|
42
|
+
# @api public
|
|
43
|
+
# Skips contract validation for specified actions.
|
|
44
|
+
#
|
|
45
|
+
# @param except [Array<Symbol>]
|
|
46
|
+
# Skip for all except these actions.
|
|
47
|
+
# @param only [Array<Symbol>]
|
|
48
|
+
# Skip only for these actions.
|
|
49
|
+
#
|
|
50
|
+
# @example Skip for specific actions
|
|
51
|
+
# skip_contract_validation! only: [:ping, :status]
|
|
52
|
+
#
|
|
53
|
+
# @example Skip for all actions
|
|
54
|
+
# skip_contract_validation!
|
|
55
|
+
class_methods do
|
|
56
|
+
def skip_contract_validation!(except: nil, only: nil)
|
|
57
|
+
skip_before_action :validate_contract, except:, only:
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @api public
|
|
62
|
+
# The contract for this controller.
|
|
63
|
+
#
|
|
64
|
+
# Contains parsed query parameters and request body with type coercion applied.
|
|
65
|
+
# Access parameters via {Contract::Base#query} and {Contract::Base#body}.
|
|
66
|
+
#
|
|
67
|
+
# @return [Contract::Base]
|
|
68
|
+
# @see Contract::Base
|
|
69
|
+
#
|
|
70
|
+
# @example Access parsed parameters
|
|
71
|
+
# def create
|
|
72
|
+
# invoice = Invoice.new(contract.body)
|
|
73
|
+
# # contract.body contains validated, coerced params
|
|
74
|
+
# end
|
|
75
|
+
#
|
|
76
|
+
# @example Check for specific parameters
|
|
77
|
+
# def index
|
|
78
|
+
# if contract.query[:include]
|
|
79
|
+
# # handle include parameter
|
|
80
|
+
# end
|
|
81
|
+
# end
|
|
82
|
+
def contract
|
|
83
|
+
@contract ||= begin
|
|
84
|
+
api_request = Request.new(
|
|
85
|
+
body: request.request_parameters,
|
|
86
|
+
query: request.query_parameters,
|
|
87
|
+
).transform(&:deep_symbolize_keys)
|
|
88
|
+
contract_class.new(action_name, api_request, coerce: true)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @api public
|
|
93
|
+
# Exposes data as an API response.
|
|
94
|
+
#
|
|
95
|
+
# When a representation is linked via {Contract::Base.representation}, data is serialized
|
|
96
|
+
# through the representation. Otherwise, data is rendered as-is. Key transformation
|
|
97
|
+
# is applied according to the API's {API::Base.key_format}.
|
|
98
|
+
#
|
|
99
|
+
# @param data [Object, Array]
|
|
100
|
+
# The record(s) to expose.
|
|
101
|
+
# @param meta [Hash] ({})
|
|
102
|
+
# The metadata to include in response (pagination, etc.).
|
|
103
|
+
# @param status [Symbol, Integer, nil] (nil)
|
|
104
|
+
# The HTTP status (:ok, or :created for create action).
|
|
105
|
+
# @see Representation::Base
|
|
106
|
+
#
|
|
107
|
+
# @example Expose a single record
|
|
108
|
+
# def show
|
|
109
|
+
# invoice = Invoice.find(params[:id])
|
|
110
|
+
# expose invoice
|
|
111
|
+
# end
|
|
112
|
+
#
|
|
113
|
+
# @example Expose a collection with metadata
|
|
114
|
+
# def index
|
|
115
|
+
# invoices = Invoice.all
|
|
116
|
+
# expose invoices, meta: { total: invoices.count }
|
|
117
|
+
# end
|
|
118
|
+
#
|
|
119
|
+
# @example Custom status
|
|
120
|
+
# def create
|
|
121
|
+
# invoice = Invoice.create(contract.body[:invoice])
|
|
122
|
+
# expose invoice, status: :created
|
|
123
|
+
# end
|
|
124
|
+
def expose(data, meta: {}, status: nil)
|
|
125
|
+
if contract_class.actions[action_name.to_sym]&.response&.no_content?
|
|
126
|
+
head :no_content
|
|
127
|
+
return
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
representation_class = contract_class.representation_class
|
|
131
|
+
|
|
132
|
+
body = if representation_class
|
|
133
|
+
action = resource.actions[action_name.to_sym]
|
|
134
|
+
if action.collection?
|
|
135
|
+
adapter.process_collection(data, representation_class, contract.request, context:, meta:)
|
|
136
|
+
else
|
|
137
|
+
adapter.process_member(data, representation_class, contract.request, context:, meta:)
|
|
138
|
+
end
|
|
139
|
+
else
|
|
140
|
+
data[:meta] = meta if meta.present?
|
|
141
|
+
data
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
response = Response.new(body:)
|
|
145
|
+
|
|
146
|
+
if Rails.env.development?
|
|
147
|
+
result = contract_class.parse_response(response, action_name)
|
|
148
|
+
result.issues.each { |issue| Rails.logger.warn(issue.to_s) }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
response = api_class.prepare_response(response)
|
|
152
|
+
|
|
153
|
+
render json: response.body, status: status || (action_name.to_sym == :create ? :created : :ok)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# @api public
|
|
157
|
+
# Exposes an error response using a registered error code.
|
|
158
|
+
#
|
|
159
|
+
# Defaults to I18n lookup when detail is not provided.
|
|
160
|
+
#
|
|
161
|
+
# @param code_key [Symbol]
|
|
162
|
+
# The registered error code (:not_found, :unauthorized, etc.).
|
|
163
|
+
# @param detail [String, nil] (nil)
|
|
164
|
+
# The custom error message (uses I18n lookup if nil).
|
|
165
|
+
# @param meta [Hash] ({})
|
|
166
|
+
# The additional metadata to include.
|
|
167
|
+
# @param path [Array<String, Symbol>, nil] (nil)
|
|
168
|
+
# The JSON path to the error.
|
|
169
|
+
# @see ErrorCode
|
|
170
|
+
# @see Issue
|
|
171
|
+
#
|
|
172
|
+
# @example Not found error
|
|
173
|
+
# def show
|
|
174
|
+
# invoice = Invoice.find_by(id: params[:id])
|
|
175
|
+
# return expose_error :not_found unless invoice
|
|
176
|
+
# expose invoice
|
|
177
|
+
# end
|
|
178
|
+
#
|
|
179
|
+
# @example With custom message
|
|
180
|
+
# expose_error :forbidden, detail: 'You cannot access this invoice'
|
|
181
|
+
def expose_error(
|
|
182
|
+
code_key,
|
|
183
|
+
detail: nil,
|
|
184
|
+
path: nil,
|
|
185
|
+
meta: {}
|
|
186
|
+
)
|
|
187
|
+
error_code = ErrorCode.find!(code_key)
|
|
188
|
+
|
|
189
|
+
issue = Issue.new(
|
|
190
|
+
error_code.key,
|
|
191
|
+
detail || error_code.description(locale_key: api_class.locale_key),
|
|
192
|
+
meta:,
|
|
193
|
+
path: path || (error_code.attach_path? ? relative_path.split('/').reject(&:blank?) : []),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
render_error HttpError.new([issue], error_code)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# @api public
|
|
200
|
+
# The context for this controller.
|
|
201
|
+
#
|
|
202
|
+
# Passed to representations during serialization. Override to provide
|
|
203
|
+
# current user, permissions, locale, or feature flags.
|
|
204
|
+
#
|
|
205
|
+
# @return [Hash]
|
|
206
|
+
#
|
|
207
|
+
# @example Provide current user context
|
|
208
|
+
# def context
|
|
209
|
+
# { current_user: current_user }
|
|
210
|
+
# end
|
|
211
|
+
#
|
|
212
|
+
# @example Access context in representation
|
|
213
|
+
# class InvoiceRepresentation < Apiwork::Representation::Base
|
|
214
|
+
# attribute :editable, type: :boolean
|
|
215
|
+
#
|
|
216
|
+
# def editable
|
|
217
|
+
# context[:current_user].admin?
|
|
218
|
+
# end
|
|
219
|
+
# end
|
|
220
|
+
def context
|
|
221
|
+
{}
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
private
|
|
225
|
+
|
|
226
|
+
def validate_contract
|
|
227
|
+
return unless resource
|
|
228
|
+
return if contract.valid?
|
|
229
|
+
|
|
230
|
+
raise ContractError, contract.issues
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def render_error(error)
|
|
234
|
+
representation_class = resource ? contract_class.representation_class : nil
|
|
235
|
+
json = adapter.process_error(error, representation_class, context:)
|
|
236
|
+
render json:, status: error.status
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def contract_class
|
|
240
|
+
@contract_class ||= begin
|
|
241
|
+
klass = resource&.resolve_contract_class
|
|
242
|
+
klass || raise_contract_not_found_error
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def api_class
|
|
247
|
+
@api_class ||= find_api_class || raise_api_not_found_error
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def adapter
|
|
251
|
+
api_class.adapter
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def resource
|
|
255
|
+
@resource ||= api_class.root_resource.find_resource_for_path(relative_path)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def relative_path
|
|
259
|
+
@relative_path ||= request.path.delete_prefix(api_class.base_path)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def raise_api_not_found_error
|
|
263
|
+
path = path_parts.empty? ? '/' : "/#{path_parts[0..1].join('/')}"
|
|
264
|
+
api_file = "config/apis/#{path.split('/').reject(&:blank?).join('_')}.rb"
|
|
265
|
+
|
|
266
|
+
raise ConfigurationError,
|
|
267
|
+
"No API found for #{self.class.name}. " \
|
|
268
|
+
"Create the API: #{api_file} (Apiwork::API.define '#{path}')"
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def raise_contract_not_found_error
|
|
272
|
+
resource_base = resource.name.to_s.singularize
|
|
273
|
+
namespaces = api_class.namespaces
|
|
274
|
+
|
|
275
|
+
contract_name = [*namespaces.map { |namespace| namespace.to_s.camelize }, "#{resource_base.camelize}Contract"].join('::')
|
|
276
|
+
contract_path = ['app/contracts', *namespaces, "#{resource_base}_contract.rb"].join('/')
|
|
277
|
+
|
|
278
|
+
raise ConfigurationError,
|
|
279
|
+
"No contract found for #{self.class.name}. " \
|
|
280
|
+
"Create the contract: #{contract_path} (#{contract_name})"
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def find_api_class
|
|
284
|
+
parts = path_parts
|
|
285
|
+
return API.find('/') if parts.empty?
|
|
286
|
+
|
|
287
|
+
(parts.length - 1).downto(1) do |index|
|
|
288
|
+
base_path = "/#{parts[0...index].join('/')}"
|
|
289
|
+
api_class = API.find(base_path)
|
|
290
|
+
return api_class if api_class
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
nil
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def path_parts
|
|
297
|
+
@path_parts ||= request.path.split('/').reject(&:blank?)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|