mix pd.scaffold (PropertyDamage v0.2.0)
View SourceGenerate a complete PropertyDamage test suite from an OpenAPI specification.
This dramatically reduces setup time for testing REST APIs by automatically generating:
- Command modules with generators
- Event structs from response schemas
- HTTP adapter with execute clauses
- Model module with command weights
- Authentication support
Usage
# Generate everything from an OpenAPI spec
mix pd.scaffold --from openapi.json --output lib/my_app_test/
# From a URL
mix pd.scaffold --from https://api.example.com/openapi.json --output lib/
# Only specific operations
mix pd.scaffold --from openapi.json --operations createUser,updateUser
# Generate only commands (skip adapter/model)
mix pd.scaffold --from openapi.json --commands-only
# Preview without writing files
mix pd.scaffold --from openapi.json --dry-runOptions
--from- Path or URL to OpenAPI spec (JSON or YAML)--output- Output directory for generated files (default: lib/generated/)--operations- Comma-separated list of operationIds to generate--namespace- Module namespace prefix (e.g., MyAppTest)--commands-only- Only generate command modules--dry-run- Print what would be generated without writing files--base-url- Base URL for the API (overrides spec's servers)
What Gets Generated
Commands (one per operation)
defmodule MyAppTest.Commands.CreateUser do
@behaviour PropertyDamage.Command
import PropertyDamage.Generator, only: [merge_overrides: 2]
defstruct [:name, :email, :role]
@impl true
def generator(overrides \\ %{}) do
%{
name: StreamData.string(:alphanumeric, min_length: 5, max_length: 20),
email: StreamData.map(StreamData.positive_integer(), &"test_email_#{&1}@example.com"),
role: StreamData.member_of(["admin", "user", "guest"])
}
|> merge_overrides(overrides)
|> StreamData.fixed_map()
end
# Fill in to map an HTTP response to events (status-aware):
def events(_command, 201, body), do: [%MyAppTest.Events.UserCreated{id: body["id"]}]
def events(_command, _status, _body), do: []
endEvents (from response schemas)
defmodule MyAppTest.Events.UserCreated do
import PropertyDamage, only: [external: 0]
defstruct [id: external(), name: nil, email: nil, role: nil, created_at: nil]
endAdapter
defmodule MyAppTest.Adapter do
use PropertyDamage.Adapter
@impl true
def execute(%Commands.CreateUser{} = cmd, ctx) do
# ... build url/body, then map the response to events via the command:
case http_request(:post, url, body, []) do
{:ok, status, response} -> {:ok, cmd.__struct__.events(cmd, status, response)}
{:error, reason} -> {:error, reason}
end
end
# ...
endModel
defmodule MyAppTest.Model do
@behaviour PropertyDamage.Model
def commands do
[
{Commands.CreateUser, weight: 5},
{Commands.GetUser, weight: 3},
# ...
]
end
def command_sequence_projection, do: MyAppTest.Projections.State
def assertion_projections, do: [MyAppTest.Projections.UniqueUsers]
endYAML Support
YAML files (.yaml, .yml) are supported if yaml_elixir is installed:
{:yaml_elixir, "~> 2.9"}After Generation
- Review and customize generators in command
generator/1callbacks - Define events/3 (command, status, response) to map responses to your event structs
- Add preconditions (when:) and overrides (with:) in the Model's commands()
- Implement simulate/2 in the Model for expected events
- Configure authentication in the adapter
- Add invariants/projections to the model
Summary
Types
Extracted metadata about the API.
A normalized security scheme extracted from the spec.
A normalized event derived from a response schema.
An inferred field type from a JSON schema.
A normalized operation extracted from the spec.
A parsed OpenAPI specification document.
Types
@type api_info() :: %{ title: String.t(), version: String.t(), description: String.t(), base_url: String.t() }
Extracted metadata about the API.
@type auth_scheme() :: %{ name: String.t(), type: String.t() | nil, scheme: String.t() | nil, bearer_format: String.t() | nil, in: String.t() | nil, param_name: String.t() | nil, description: String.t() }
A normalized security scheme extracted from the spec.
@type event() :: %{ name: String.t(), fields: [map()], description: String.t(), operation: String.t() | nil }
A normalized event derived from a response schema.
@type field_type() :: :uuid | :datetime | :date | :email | :uri | :string | :integer | :number | :boolean | :map | :any | {:enum, [term()]} | {:string, integer(), integer()} | {:integer, integer(), integer()} | {:number, number(), number()} | {:array, field_type()} | {:pattern, String.t()}
An inferred field type from a JSON schema.
@type operation() :: %{ operation_id: String.t() | nil, module_name: String.t(), method: String.t(), method_atom: atom(), path: String.t(), summary: String.t(), description: String.t(), parameters: [map()], request_body: map() | nil, responses: %{optional(String.t()) => map()}, tags: [String.t()], security: [map()] }
A normalized operation extracted from the spec.
@type spec() :: map()
A parsed OpenAPI specification document.