Skip to content

Instantly share code, notes, and snippets.

@msutkowski
Created September 8, 2024 22:06
Show Gist options
  • Save msutkowski/4ee1c9709dc5676d5ee4a0fa84ee4c05 to your computer and use it in GitHub Desktop.
Save msutkowski/4ee1c9709dc5676d5ee4a0fa84ee4c05 to your computer and use it in GitHub Desktop.
defmodule MyAppWeb.ValidateOpenAPIOperation do
@moduledoc """
Provides a compile-time mechanism for validating controller actions against the OpenAPI specification. Utilizes a macro `validate_operation/2` to link Phoenix controller actions with OpenAPI operation IDs, enforcing validation automatically.
Supports an option `raise_on_error?` to determine behavior upon validation failure: when `true`, it raises an error, closely mirroring the validation enforcement seen in OpenApiSpex's `operation` macro.
## Example Usage
defmodule MyAppWeb.SomeController do
use MyAppWeb, :controller
use FacilityActivityServiceWeb.ValidateOpenAPIOperation
plug MyAppWeb.ValidateWithOpenAPISpec, module: __MODULE__, raise_on_error?: true
validate_operation "public_api_list_outbounds", raise_on_error?: false
def list_outbounds(conn, _params) do
# Action code...
end
# This inherits from the plug, and will potentially raise an error when casting and validating the request.
validate_operation "public_api_list_outbounds"
def list_inbounds(conn, _params) do
# Action code...
end
end
This setup ensures the `list_outbounds` action is validated against the OpenAPI operation "public_api_list_outbounds", with strict adherence enforced by `raise_on_error: true`.
## Dependencies
Ensure the `MyAppWeb.ValidateWithOpenAPISpec` plug is configured for accessing and validating against the OpenAPI specification.
## Notes
- Operation IDs must match those in your OpenAPI specification exactly.
- Validation is applied at the controller action level to confirm request parameters match the specified operation.
- Use `OpenApiSpex.TestAssertions.assert_operation_response/2` for validating responses in tests.
"""
defmacro __using__(_opts \\ []) do
quote do
import FacilityActivityServiceWeb.ValidateOpenAPIOperation
Module.register_attribute(__MODULE__, :validate_operations, accumulate: true)
Module.register_attribute(__MODULE__, :current_operation, accumulate: false)
@before_compile FacilityActivityServiceWeb.ValidateOpenAPIOperation
@on_definition FacilityActivityServiceWeb.ValidateOpenAPIOperation
end
end
@doc """
Validates a Phoenix controller action against a specified OpenAPI operation at compile time.
This macro associates a controller action with an OpenAPI operation ID and optional configuration. It primarily supports the `raise_on_error?` option, which dictates the error handling behavior when the incoming request does not comply with the OpenAPI specification.
## Parameters
- `operation_id` : String.t() - The OpenAPI operation ID to validate against. This ID must exactly match one defined in your OpenAPI specification.
- `opts` : Keyword.t() - Optional configuration for validation. Supported options include:
- `:raise_on_error` : boolean() (default: false) - When set to `true`, raises an error if the request does not match the OpenAPI specification. When `false`, the request processing continues without raising an error, allowing for custom handling or logging.
## Examples
validate_operation "listPets", raise_on_error?: true
Associates the controller action with the "listPets" OpenAPI operation. If a request does not conform to the operation's specification, an error will be raised.
## Usage
This macro is intended to be used within a Phoenix controller AND requires the `ValidateWithOpenAPISpec` plug, where each action can be associated with an OpenAPI operation for automatic request validation.
"""
defmacro validate_operation(operation_id, opts \\ []) do
quote do
@current_operation %{operation_id: unquote(operation_id), opts: unquote(opts)}
end
end
def __on_definition__(%{module: module}, _kind, name, _args, _guards, _body) do
operation = Module.get_attribute(module, :current_operation)
if operation do
Module.put_attribute(module, :validate_operations, {name, operation})
end
Module.delete_attribute(module, :current_operation)
end
defmacro __before_compile__(env) do
validate_operations = Module.get_attribute(env.module, :validate_operations) || []
validate_operations =
validate_operations
|> Map.new()
|> Macro.escape()
quote generated: true do
def validate_operations, do: unquote(validate_operations)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment