Module: Apiwork::Controller

Extended by:
ActiveSupport::Concern
Included in:
ErrorsController
Defined in:
lib/apiwork/controller.rb

Overview

Mixin for API controllers that provides request validation and response helpers.

Include in controllers to access #contract, #expose, and #expose_error. Automatically validates requests against the contract before actions run.

Examples:

Basic controller

class InvoicesController < ApplicationController
  include Apiwork::Controller

  def index
    expose Invoice.all
  end

  def show
    invoice = Invoice.find(params[:id])
    expose invoice
  end

  def create
    invoice = Invoice.create(contract.body[:invoice])
    expose invoice
  end
end

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.skip_contract_validation!(only: nil, except: nil) ⇒ Object

Skips contract validation for specified actions.

Examples:

Skip for specific actions

skip_contract_validation! only: [:ping, :status]

Skip for all actions

skip_contract_validation!

Parameters:

  • except (Array<Symbol>) (defaults to: nil)

    Skip for all except these actions.

  • only (Array<Symbol>) (defaults to: nil)

    Skip only for these actions.



55
56
57
58
59
# File 'lib/apiwork/controller.rb', line 55

class_methods do
  def skip_contract_validation!(except: nil, only: nil)
    skip_before_action :validate_contract, except:, only:
  end
end

Instance Method Details

#contextHash

The context for this controller.

Passed to representations during serialization. Override to provide current user, permissions, locale, or feature flags.

Examples:

Provide current user context

def context
  { current_user: current_user }
end

Access context in representation

class InvoiceRepresentation < Apiwork::Representation::Base
  attribute :editable, type: :boolean

  def editable
    context[:current_user].admin?
  end
end

Returns:

  • (Hash)


220
221
222
# File 'lib/apiwork/controller.rb', line 220

def context
  {}
end

#contractContract::Base

The contract for this controller.

Contains parsed query parameters and request body with type coercion applied. Access parameters via Apiwork::Contract::Base#query and Apiwork::Contract::Base#body.

Examples:

Access parsed parameters

def create
  invoice = Invoice.new(contract.body)
  # contract.body contains validated, coerced params
end

Check for specific parameters

def index
  if contract.query[:include]
    # handle include parameter
  end
end

Returns:

See Also:



82
83
84
85
86
87
88
89
90
# File 'lib/apiwork/controller.rb', line 82

def contract
  @contract ||= begin
    api_request = Request.new(
      body: request.request_parameters,
      query: request.query_parameters,
    ).transform(&:deep_symbolize_keys)
    contract_class.new(action_name, api_request, coerce: true)
  end
end

#expose(data, meta: {}, status: nil) ⇒ Object

Exposes data as an API response.

When a representation is linked via Apiwork::Contract::Base.representation, data is serialized through the representation. Otherwise, data is rendered as-is. Key transformation is applied according to the API’s API::Base.key_format.

Examples:

Expose a single record

def show
  invoice = Invoice.find(params[:id])
  expose invoice
end

Expose a collection with metadata

def index
  invoices = Invoice.all
  expose invoices, meta: { total: invoices.count }
end

Custom status

def create
  invoice = Invoice.create(contract.body[:invoice])
  expose invoice, status: :created
end

Parameters:

  • data (Object, Array)

    The record(s) to expose.

  • meta (Hash) (defaults to: {})

    ({}) The metadata to include in response (pagination, etc.).

  • status (Symbol, Integer, nil) (defaults to: nil)

    (nil) The HTTP status (:ok, or :created for create action).

See Also:



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/apiwork/controller.rb', line 124

def expose(data, meta: {}, status: nil)
  if contract_class.actions[action_name.to_sym]&.response&.no_content?
    head :no_content
    return
  end

  representation_class = contract_class.representation_class

  body = if representation_class
           action = resource.actions[action_name.to_sym]
           if action.collection?
             adapter.process_collection(data, representation_class, contract.request, context:, meta:)
           else
             adapter.process_member(data, representation_class, contract.request, context:, meta:)
           end
         else
           data[:meta] = meta if meta.present?
           data.deep_symbolize_keys
         end

  response = Response.new(body: deep_as_json(body))

  if Rails.env.development?
    result = contract_class.parse_response(response, action_name)
    result.issues.each { |issue| Rails.logger.warn(issue.to_s) }
  end

  response = api_class.prepare_response(response)

  render json: response.body, status: status || (action_name.to_sym == :create ? :created : :ok)
end

#expose_error(code_key, detail: nil, path: nil, meta: {}) ⇒ Object

Exposes an error response using a registered error code.

Defaults to I18n lookup when detail is not provided.

Examples:

Not found error

def show
  invoice = Invoice.find_by(id: params[:id])
  return expose_error :not_found unless invoice
  expose invoice
end

With custom message

expose_error :forbidden, detail: 'You cannot access this invoice'

Parameters:

  • code_key (Symbol)

    The registered error code (:not_found, :unauthorized, etc.).

  • detail (String, nil) (defaults to: nil)

    (nil) The custom error message (uses I18n lookup if nil).

  • meta (Hash) (defaults to: {})

    ({}) The additional metadata to include.

  • path (Array<String, Symbol>, nil) (defaults to: nil)

    (nil) The JSON path to the error.

See Also:



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/apiwork/controller.rb', line 181

def expose_error(
  code_key,
  detail: nil,
  path: nil,
  meta: {}
)
  error_code = ErrorCode.find!(code_key)

  issue = Issue.new(
    error_code.key,
    detail || error_code.description(locale_key: api_class.locale_key),
    meta:,
    path: path || (error_code.attach_path? ? relative_path.split('/').reject(&:blank?) : []),
  )

  render_error HttpError.new([issue], error_code)
end