Skip to main content

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.

## 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" => []}]
)
```

## 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

| Ash Type | JSON Schema | Format |
|----------|-------------|--------|
| `:string`, `:ci_string`, `:atom`, `:module` | `string` | - |
| `:integer` | `integer` | - |
| `:float` | `number` | `float` |
| `:decimal` | `number` | `double` |
| `:boolean` | `boolean` | - |
| `:date` | `string` | `date` |
| `:time`, `:time_usec` | `string` | `time` |
| `:datetime`, `:utc_datetime`, `:utc_datetime_usec`, `:naive_datetime` | `string` | `date-time` |
| `: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) | - |

## Constraint Mapping

| Ash | JSON Schema |
|-----|-------------|
| `:min_length` | `minLength` |
| `:max_length` | `maxLength` |
| `:min` | `minimum` |
| `:max` | `maximum` |
| `:match` | `pattern` |
| `:one_of` | `enum` |

## 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
```