apigenie-generatorOpenAPI serversserver phoenix

server phoenix

Generate a Phoenix 1.7 application from an OpenAPI document.

From an AsyncAPI document, the command generates Phoenix channels and Kafka pipelines instead; see apigenie-generator server phoenix (AsyncAPI).

Usage

apigenie-generator server phoenix [...OPTIONS] INPUT_FILE

Description

The generator writes a Mix project (Elixir 1.18 or later) depending on Phoenix 1.7, Bandit and Jason, without Ecto, HTML, assets or a mailer. Its code passes mix format --check-formatted, mix compile --warnings-as-errors and mix credo --strict, and mix phx.server serves it.

File Contents
lib/<app>_web/router.ex the routes of the operations, under the path of the first server
lib/<app>_web/api/<tag>.ex a behaviour per tag (Default for operations without tags), with a callback per operation
lib/<app>_web/controllers/<tag>_controller.ex an action per operation, calling the implementation of the behaviour
lib/<app>/models/*.ex a module per schema: structs, enums, unions and aliases, with new/1, new!/1 and to_map/1
lib/<app>_web/runtime/*.ex the checks of credentials, the decoding of parameters and bodies, the encoding of responses
lib/<app>_web/authenticator.ex the behaviour checking credentials
lib/<app>_web/endpoint.ex, error_json.ex, lib/<app>/application.ex the endpoint, the errors Phoenix renders, the application
config/*.exs, mix.exs, .formatter.exs, .credo.exs, .gitignore, README.md the project
mix deps.get
PORT=8080 mix phx.server

Routes

Every operation has a route in <App>Web.Router, in the scope of the path of the first server (/v2 for https://api.example.com/v2, or the base_path option). Methods without a Phoenix macro, such as QUERY and the COPY of additionalOperations, are routed with match. Static segments come before parameters (/shipments/search before /shipments/{shipmentId}), as Phoenix tries the routes in order. A segment holding parameters is captured whole and split by the runtime with its template, so /labels/{labelId}.{extension} works.

Behaviours and implementations

The operations of each tag are the callbacks of a behaviour, <App>Web.Api.<Tag>. A callback receives the connection, the parameters and the body:

defmodule MyApp.Shipments do
  @behaviour CarpetExpressWeb.Api.Shipments

  @impl true
  def get_shipment(_conn, %{shipment_id: id}, nil) do
    case MyApp.Store.fetch(id) do
      {:ok, shipment} -> {200, shipment}
      :error -> {404, %{"type" => "about:blank", "title" => "Not Found", "status" => 404}}
    end
  end
end

and is configured as the implementation:

config :carpet_express, CarpetExpressWeb.Api.Shipments, impl: MyApp.Shipments

Operations without an implementation answer 501 Not Implemented. A callback returns {status, body}, {status, body, headers}, or the connection after sending a response itself (e.g. a chunked stream). The body is encoded in the media type the document gives the status (the exact code, then its range, then default): JSON with the type of the response, so a model struct or a plain map; text and bytes as they are; nothing for responses without content.

Parameters and bodies

The actions check the request before calling the implementation, and answer a problem document (RFC 9457, application/problem+json) when it doesn’t match the description:

  • 401 when the request has none of the credentials of the operation’s security requirements;
  • 400 when a parameter is missing or invalid (the errors list the parameters), or the body is missing, isn’t JSON or doesn’t match its schema;
  • 415 for a media type the operation doesn’t accept.

Parameters are decoded from their styles into a map with atom keys (%{shipment_id: "...", limit: 20}): simple, label and matrix path parameters, form, spaceDelimited, pipeDelimited and deepObject query parameters, headers, cookies, and parameters described with content as JSON. Integers, numbers and booleans are converted from text, models and enums decoded with their schemas.

Media type Body
JSON (application/json, *+json) the model, a list or the value its schema describes
application/x-www-form-urlencoded the map of the fields
multipart/form-data the map of the parts, files as Plug.Upload structs
text/* a string
anything else the bytes

Plug.Parsers reads JSON, form and multipart bodies of up to 20 MB; the runtime parses the JSON and form bodies of methods Plug.Parsers leaves alone, such as QUERY.

Security

The first security requirement of an operation whose credentials the request presents is checked: API keys in headers, the query or cookies, HTTP bearer and basic, and OAuth2 and OpenID Connect tokens as bearer tokens. Without an implementation of <App>Web.Authenticator, every presented credential is accepted; with one,

config :carpet_express, CarpetExpressWeb.Authenticator, impl: MyApp.Authenticator

its authenticate(conn, scheme, credential, scopes) returns {:ok, principal}, assigned to the connection as :principal, or {:error, reason}, answered with 401.

Types

The models are the modules of the Elixir models, shared with client elixir: a struct per object (with allOf flattened), atoms for string enums, a module dispatching on the discriminator for oneOf and anyOf, Date, DateTime, NaiveDateTime and Time for dates and times, binaries for format: byte (decoded from base64) and format: binary, additional_properties maps, and | nil for nullable values.

Extensions

Extension Effect
x-apigenie.codegen.name name of the module of a component schema, unless module_overrides names it
x-apigenie.codegen.action-name name of the action and callback of an operation without an operationId (with the default action_naming, which lists id first)
x-apigenie.codegen.disabled true on an operation or its path item leaves the operation out
x-internal true on an operation or its path item leaves the operation out, unless internal_operations is set
x-defaultMapping defaultMapping of a discriminator in OpenAPI 3.0 and 3.1 documents: the schema of the values whose discriminator value isn’t mapped

The extensions can live in an overlay for this target instead of in the document, e.g. phoenix.overlay.yaml:

overlay: 1.0.0
info:
  title: Elixir names
  version: 1.0.0
actions:
  - target: $.components.schemas.Pet
    update:
      x-apigenie:
        codegen:
          name: Animal
  - target: $.paths['/admin']
    update:
      x-internal: true
apigenie-generator server phoenix openapi.yaml --overlay phoenix.overlay.yaml -o out

Limitations

  • text/event-stream and JSON Lines responses are sent whole unless the implementation sends the response itself.
  • The models don’t validate minimum, maximum, pattern and the other constraints, and ignore prefixItems, patternProperties and conditional schemas.
  • readOnly and writeOnly properties are fields of the same struct.
  • Every operation is served under the base path; operation and path servers are ignored.
  • Callbacks, webhooks and links aren’t generated.
  • The endpoint serves HTTP; mutual TLS needs an https configuration of the endpoint.

Options

-c, –config

Path to the configuration file to use. See the “Configuration” section for more information.

–overlay OVERLAY

Path to an OpenAPI Overlay (1.0 to 1.2) applied to the input before generating, e.g. one setting the extensions of the “Extensions” section. Can be repeated: the overlays apply in order, after those listed under commands.generate.server_phoenix.overlay in the configuration file (with paths relative to it). Warnings and errors about a value an overlay set point to the overlay.

–strict

Makes an overlay action whose target selects nothing an error (exit status 2, nothing is generated) instead of a warning (OVERLAY:LINE:COL: action #N (TARGET) selects no nodes.).

-o, –output

Directory where the project is generated.

–package-name

Name of the Mix application, converted to snake_case (carpet-express gives carpet_express, the root module CarpetExpress and the web modules CarpetExpressWeb). Defaults to the name of the output directory.

–package-version

Version of the Mix project; 0.1.0 by default.

-p, –property NAME=VALUE

Sets a generator option (see below), e.g. -p base_path=/api. Options given on the command line take precedence over the configuration file. Lists are comma-separated (-p include_tags=pets,store) and mappings too (-p module_overrides=pet-owner=Owner).

Configuration

version: '1.0'
commands:
  generate:
    server_phoenix:
      app: carpet_express
      base_path: /v2
      exclude_tags: [internal]
Option Type Default Description
app string name of the output directory Name of the Mix application (snake_case, the directory under lib/); also set by --package-name.
namespace string the application in PascalCase Root module of the generated modules, e.g. CarpetExpress; the web modules are in CarpetExpressWeb.
base_path string the path of the first server Path the routes are mounted at, e.g. /v2; / mounts them at the root.
include_tags list of strings Generates only the operations with one of these tags.
exclude_tags list of strings Skips the operations with one of these tags.
skip_prune boolean false Keeps the component schemas that no generated operation uses.
internal_operations boolean false Also generates the operations marked x-internal: true.
dev_deps boolean true Lists credo as a dependency of mix.exs (only: [:dev, :test]).
action_naming list of id, extension, url id, extension, url Where the names of actions and callbacks come from, in order of preference.
field_prefix string field_ Prefix of struct fields whose property names give no valid name.
module_overrides map of strings Module names of component schemas, e.g. pet-owner: Owner.