Current section

Files

Jump to
ash_oaskit usage-rules.md
Raw

usage-rules.md

# AshOaskit Usage Rules

OpenAPI specification generator for Ash Framework domains. Supports OpenAPI 3.0 and 3.1.

## Spec Modules (preferred)

```elixir
# Define a cached, oaskit-native spec module
defmodule MyAppWeb.ApiSpec do
  use AshOaskit,
    domains: [MyApp.Blog],
    title: "My API",
    api_version: "1.0.0"
end

# Customize the generated spec (result is cached)
defmodule MyAppWeb.ApiSpec do
  use AshOaskit, domains: [MyApp.Blog]

  @impl AshOaskit.Spec
  def modify_spec(spec) do
    put_in(spec, ["components", "securitySchemes"], %{
      "bearerAuth" => %{"type" => "http", "scheme" => "bearer"}
    })
  end
end

# Serve it (Phoenix or Plug.Router) with optional Redoc UI
use AshOaskit.Router,
  spec: MyAppWeb.ApiSpec,
  open_api: "/openapi",
  redoc: "/redoc"

# Dual version: two spec modules
use AshOaskit.Router,
  spec: [{"3.1", MyAppWeb.ApiSpecV31}, {"3.0", MyAppWeb.ApiSpecV30}],
  open_api: "/openapi"

# Export the exact served spec
# mix openapi.dump MyAppWeb.ApiSpec --pretty -o openapi.json
```

Rules:
- The spec is cached in `:persistent_term`; disable in dev with `config :ash_oaskit, cache_specs: false` (or per module with `cache: false`).
- One spec module = one OpenAPI version; define two modules for dual-version output.
- Only `public? true` attributes/calculations/aggregates/relationships appear in specs.
- Request body schemas follow the routed action's `accept` list plus its public arguments.
- Use `resource_scope: :routed` when a listed domain contains internal resources that should not seed public schemas/tags.

## Programmatic API

```elixir
# Generate spec (defaults to 3.1)
AshOaskit.spec(domains: [MyApp.Blog])
AshOaskit.spec_30(domains: [MyApp.Blog])  # Force 3.0
AshOaskit.spec_31(domains: [MyApp.Blog])  # Force 3.1

# Full options
AshOaskit.spec(
  domains: [MyApp.Blog, MyApp.Accounts],
  version: "3.1",
  title: "My API",
  api_version: "2.0.0",
  description: "API description",
  servers: [%{"url" => "https://api.example.com"}],
  contact: %{"name" => "Support", "email" => "api@example.com"},
  license: %{"name" => "MIT"},
  security: [%{"bearerAuth" => []}],
  resource_scope: :routed
)
```

## CLI

```bash
# Preferred once a spec module exists:
mix openapi.dump MyAppWeb.ApiSpec --pretty -o openapi.json

mix ash_oaskit.generate -d MyApp.Blog -o openapi.json
mix ash_oaskit.generate -d MyApp.Blog,MyApp.Accounts -v 3.0 -o openapi.yaml -f yaml
mix ash_oaskit.generate --domains MyApp.Blog --title "My API" --api-version 1.0.0
```

## Router Macro

```elixir
# Preferred: spec module mode (cached, supports redoc:)
use AshOaskit.Router,
  spec: MyAppWeb.ApiSpec,
  open_api: "/openapi",
  redoc: "/redoc"

# Plug.Router — same options, place before catch-all `match _`

# DEPRECATED (regenerates spec per request, warns at compile time):
use AshOaskit.Router,
  domains: [MyApp.Blog],
  open_api: "/openapi",
  title: "My API"
```

## Domain Setup

```elixir
defmodule MyApp.Blog do
  use Ash.Domain, extensions: [AshJsonApi.Domain]

  resources do
    resource MyApp.Blog.Post
  end

  json_api do
    routes do
      base_route "/posts", MyApp.Blog.Post do
        get :read
        index :read
        post :create
        patch :update
        delete :destroy
      end
    end
  end
end
```

## Resource Setup

```elixir
defmodule MyApp.Blog.Post do
  use Ash.Resource, domain: MyApp.Blog, extensions: [AshJsonApi.Resource]

  json_api do
    type "post"
  end

  attributes do
    uuid_primary_key :id

    # public? true is REQUIRED for fields to appear in the spec
    attribute :title, :string,
      public?: true,
      allow_nil?: false,
      constraints: [min_length: 1, max_length: 255]

    attribute :body, :string, public?: true, description: "Post content"
    attribute :status, :atom, public?: true, constraints: [one_of: [:draft, :published]], default: :draft
  end

  actions do
    defaults [:read, :destroy]
    create :create, accept: [:title, :body, :status]
    update :update, accept: [:title, :body, :status]
  end
end
```

## Type Mapping

Literal defaults are included only for non-sensitive fields and action
arguments. Values marked `sensitive? true` are omitted from defaults in both
OpenAPI versions, while their types and constraints remain documented. Runtime
default callbacks are never executed during generation.

| Ash Type | JSON Schema | Format |
|----------|-------------|--------|
| `:string`, `:ci_string`, `:atom`, `:module` | `string` | - |
| `:integer` | `integer` | - |
| `:float` | `number` | `float` |
| `:decimal` | `string` | exact decimal pattern |
| `:boolean` | `boolean` | - |
| `:date` | `string` | `date` |
| `:time`, `:time_usec` | `string` | local ISO 8601 pattern |
| `:datetime`, `:utc_datetime`, `:utc_datetime_usec` | `string` | `date-time` |
| `:naive_datetime` | `string` | local ISO 8601 pattern |
| `:duration` | `string` | `duration` |
| `:uuid`, `:uuid_v7` | `string` | `uuid` |
| `:binary` | `string` | `binary` |
| `:url_encoded_binary`, `Ash.Type.File` | `string` | `byte` |
| `:map`, `:keyword`, `:tuple` | `object` | - |
| `:vector` | `array` of `number` | - |
| `:term`, `:function` | `{}` (any) | - |
| `{:array, type}` | `array` | - |
| `Ash.Type.Enum` implementors | `string` + `enum` | - |
| `Ash.Type.NewType` wrappers | (subtype schema) | - |

Custom `json_schema/1` callbacks must return a map and must not raise. Invalid
callbacks fail generation with type and attribute context; there is no silent
string fallback.

Action input objects reject unknown keys. `require_attributes` means a value
must be present and non-null. Embedded inputs reference separate action schemas;
updates permit partial existing embeds, but replacements and arrays without
public IDs retain create requirements. The stored record still determines whether
an embed can be updated. Respect embedded action constraints and explicitly
configured `skip_unknown_inputs`.

Standard POST/PATCH routes support sparse `fields` and configured `include`.
Only collection reads offer derived `filter`, `sort`, and `page` parameters.
On PATCH, path parameters select the record; same-named update arguments remain
in the body. `data` is required, but `data.id` is optional.

Do not mix keys that normalize to the same JSON name (`:name` and `"name"`, or
`200` and `"200"`). Ambiguous keys raise before normalization, including inside
defaults and custom schemas. Keep native JSON values in callbacks.

Give resources distinct JSON:API `type` values whenever their modules share a
final segment. Component-name collisions are rejected, and generated local
`$ref` targets are validated before the spec is returned.
Local pointer checks inspect schema structure, including atom-keyed callback schemas;
`$ref` text inside defaults, examples, enum/constant values, and vendor extensions is
literal data. External references and anchors require the caller's full validator.

## Constraint Mapping

| Ash | JSON Schema |
|-----|-------------|
| `:min_length` | `minLength` for strings; `minItems` for arrays |
| `:max_length` | `maxLength` for strings; `maxItems` for arrays |
| `:min` | `minimum` |
| `:max` | `maximum` |
| `:match` | `pattern` |
| `:one_of` | `enum` |
| array `:items` | Constraints on the nested item schema |
| array `:nil_items?` | Nullable item schema |
| UUIDv7 `:strict?` | Version 7 UUID pattern |

## Version Differences

- **3.0**: `nullable: true` for nullable fields
- **3.1**: `type: ["string", "null"]` for nullable fields (JSON Schema 2020-12)

## Module Structure

```
lib/ash_oaskit.ex                              # Main API (spec, validate)
lib/ash_oaskit/
  open_api.ex                                  # Version routing
  spec.ex                                      # Spec module behaviour (use AshOaskit)
  open_api_controller.ex                       # Controller behaviour
  phoenix_introspection.ex                     # Phoenix router extraction
  router.ex                                    # Router macro
  spec_builder.ex                              # SpecBuilder behaviour
  spec_builder/default.ex                      # Default SpecBuilder
  core/
    config.ex                                  # AshJsonApi DSL reader
    route_gathering.ex                         # Domain + resource route collection
    path_utils.ex                              # Path param conversion
    schema_ref.ex                              # $ref object builder
    spec_modifier.ex                           # Post-generation hooks
    type_mapper.ex                             # Ash → JSON Schema types
  generators/
    generator.ex                               # Main orchestrator
    info_builder.ex                            # Info, servers, tags
    path_builder.ex                            # Paths and operations
    shared.ex                                  # Entry point (both versions)
    v30.ex                                     # OpenAPI 3.0 entry
    v31.ex                                     # OpenAPI 3.1 entry
  parameters/
    filter_builder.ex                          # Filter query params
    query_parameters.ex                        # page, fields, include, sort
    sort_builder.ex                            # Sort param schemas
  resources/
    included_resources.ex                      # Included array schemas
    resource_identifier.ex                     # Type+id linkage
    tag_builder.ex                             # Operation grouping tags
  responses/
    error_schemas.ex                           # JSON:API error responses
    response_links.ex                          # Self, related, pagination links
    response_meta.ex                           # Pagination meta schemas
  routes/
    relationship_routes.ex                     # Relationship endpoints
    route_operations.ex                        # Operation object builder
    route_responses.ex                         # Response schema builder
  schemas/
    embedded_schemas.ex                        # Embedded resource detection
    nullable.ex                                # Version-aware nullable
    property_builders.ex                       # Attrs/calcs/aggregates → schema
    relationship_schemas.ex                    # Relationship linkage schemas
    resource_schemas.ex                        # Resource schema generation
    schema_builder.ex                          # Accumulator + cycle detection
  support/
    controller.ex                              # Phoenix controller
    multipart_support.ex                       # File upload schemas
    security.ex                                # Security schemes
  router/
    plug.ex                                    # Plug for serving specs
mix/tasks/
  ash_oaskit.generate.ex                       # CLI: mix ash_oaskit.generate
  ash_oaskit.install.ex                        # CLI: mix ash_oaskit.install
```

## Configuration

```elixir
config :ash_oaskit,
  version: "3.1",
  title: "My API",
  api_version: "1.0.0"

# Dev only: regenerate the spec on code reload
config :ash_oaskit, cache_specs: false
```

## Testing

```elixir
test "generates valid spec" do
  spec = AshOaskit.spec(domains: [MyApp.Blog])
  assert spec["openapi"] == "3.1.0"
  assert is_map(spec["paths"])
  assert is_map(spec["components"]["schemas"])
end
```

## Development

```bash
mix deps.get && mix test && mix check
```