Current section
Files
Jump to
Current section
Files
ash_oaskit
usage-rules.md
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
```