- Arazzo 1.0 & 1.1
- Swift 6
- Apple platforms & Linux
- MIT
Arazzo workflows,
as Swift values.
Read, build, validate and evaluate OpenAPI Arazzo descriptions with typed Codable models, a runtime-expression parser that follows the specification's grammar, and validation that tells you exactly where a document goes wrong.
import ArazzoKit
import ArazzoKitYAML
let document = try Arazzo.Document(yaml: yamlString)
for issue in document.validate() {
print(issue) // [error] /workflows/0/steps/1/…
}
var context = Arazzo.EvaluationContext()
context.statusCode = 200
context.response = .init(body: ["pets": [["id": 1]]])
try Arazzo.Condition(
"$statusCode == 200 && $response.body#/pets/0/id == 1"
).evaluate(in: context) // true
What is Arazzo
OpenAPI describes endpoints.
Arazzo describes how to use them together.
Arazzo is the OpenAPI Initiative's specification for workflows: sequences of API calls where each step can read the outputs of the steps before it, check success criteria, and retry or jump on failure. Version 1.1 adds AsyncAPI channels. It is how you write down "log in, then find a pet, then order it" in a form tools can check, document and run.
loginUser
Calls loginUser on the pet store's OpenAPI description and keeps the token: $response.body#/token.
findPets
Sends Authorization: {$steps.loginUser.outputs.token} and succeeds when $statusCode == 200.
placeOrder
Orders $steps.findPets.outputs.petId, retries twice on a 503, and returns the order as a workflow output.
swift-arazzo gives each of those pieces a Swift type: the document, its workflows and steps, every runtime expression, every criterion and action, so your tool can work with workflows instead of with YAML.
Features
Everything the specification defines, typed.
In the spirit of OpenAPIKit: a Swift type for every object in the specification, with validation for the rules a type system cannot express.
Arazzo 1.0 and 1.1, completely
Every object, including 1.1's AsyncAPI steps, Selector Objects, Expression Type Objects, querystring parameters and $self. Using 1.1 features in a 1.0 document is flagged.
Validation that points at the problem
18 built-in rules: MUST becomes an error, SHOULD a warning, each reported at a JSON Pointer path. Unknown steps and sources, cycles, forward references, broken expressions. Add your own rules in a few lines.
Runtime expressions as a typed AST
$steps.login.outputs.token#/access parses to a value you can switch over, following the specification's ABNF. Templates know the range of every segment, so editors can highlight and diagnose in place.
Criteria you can evaluate
Simple conditions with the specification's comparison rules, regex criteria, and JSONPath through swift-jsonpath, which passes all 715 cases of the RFC 9535 compliance suite. XPath plugs in through a protocol.
Lossless round trips
Decode and encode JSON or YAML without losing key order, specification extensions or unknown-but-allowed content. Lenient by default so one typo never stops you from reading a document; strict on request.
Linux first-class, Swift 6 ready
Strict concurrency, every type Sendable and Hashable. The core module depends only on swift-jsonpath and uses FoundationEssentials on Linux, with no ICU. YAML support is an optional package trait, and the result-builder DSL a separate product.
Code tour
Short, typed, and checked by the compiler.
These snippets come from the README, which the test suite compiles and runs, so they stay correct.
Decode YAML or JSON, then navigate with enums instead of optional strings: a step's target is one of four cases, never two at once.
let document = try Arazzo.Document(yaml: yamlString) // YAML (or JSON) text
let workflow = document.workflow(id: "loginUserAndRetrievePet")!
let step = workflow.step(id: "getPetStep")!
switch step.target {
case .operationId(let id): print("calls", id)
case .operationPath(let path): print("calls", path)
case .channelPath(let path): print("async channel", path)
case .workflowId(let id): print("runs workflow", id)
}
if case .selector(let selector)? = step.outputs["petId"] {
print(selector.context, selector.selector, selector.type.kind)
}Build a description in code, with string literals that become runtime expressions and criteria, then encode it.
let document = Arazzo.Document(
info: .init(title: "Pets", version: "1.0.0"),
sourceDescriptions: [.init(name: "petstore", url: "https://example.com/openapi.yaml", type: .openapi)],
workflows: [
.init(
workflowId: "adopt",
inputs: .object(properties: ["petId": ["type": "integer"]], required: ["petId"]),
steps: [
.init(
stepId: "getPet",
target: .operationId("getPetById"),
parameters: [.inline(.init(name: "petId", in: .path, value: "$inputs.petId"))],
successCriteria: [.simple("$statusCode == 200")],
onFailure: [.inline(.retry(name: "retry", after: 2, limit: 3))],
outputs: ["name": "$response.body#/name"]
),
],
outputs: ["adoptedName": "$steps.getPet.outputs.name"]
),
]
)
let json = try document.jsonData(prettyPrinted: true)
let yaml = try document.yamlString()Or write the same document with ArazzoKitBuilder's result builders: no wrapping, and if, switch and for in every body. The result is an equal Arazzo.Document.
import ArazzoKitBuilder
let document = Arazzo.Document {
Info(title: "Pets", version: "1.0.0")
Source("petstore", url: "https://example.com/openapi.yaml", type: .openapi)
Workflow("adopt") {
Input("petId", schema: ["type": "integer"], required: true)
Step("getPet", operationId: "getPetById") {
Parameter("petId", in: .path, value: "$inputs.petId")
SuccessCriterion("$statusCode == 200")
OnFailure.retry("retry", after: 2, limit: 3)
Output("name", "$response.body#/name")
}
Output("adoptedName", "$steps.getPet.outputs.name")
}
}Validate against the specification, fail a build on errors (or warnings), and add rules of your own.
for issue in document.validate() {
print(issue) // [error] /workflows/0/steps/1/operationId: operationId references unknown source description 'petstoreDescription'. (step-targets)
}
try document.validateOrThrow() // throws Arazzo.ValidationError on errors
try document.validateOrThrow(failingOn: .warning) // ...or on warnings too
let summaries = Arazzo.ValidationRule("workflow-summary") { document, report in
for (index, workflow) in document.workflows.enumerated() where workflow.summary == nil {
report.warning("/workflows/\(index)", "Every workflow should have a summary.")
}
}
let issues = document.validate(using: .default.adding(summaries).removing("async-steps"))Parse runtime expressions, render templates, and evaluate success criteria against a response, the way a workflow runner does.
let expression = try Arazzo.RuntimeExpression("$steps.login.outputs.token#/access")
expression.kind // .steps(stepId: "login", outputName: "token", pointer: /access)
let template = Arazzo.ExpressionTemplate("https://{$inputs.host}/pets/{$steps.create.outputs.id}")
template.expressions // [$inputs.host, $steps.create.outputs.id]
var context = Arazzo.EvaluationContext()
context.statusCode = 200
context.response = .init(headers: ["Content-Type": "application/json"], body: ["pets": [["id": 1]]])
context.stepOutputs = ["login": ["token": "abc"]]
try Arazzo.Condition("$statusCode == 200 && $response.body#/pets/0/id == 1").evaluate(in: context) // true
step.successCriteria?.areSatisfied(in: context)Resolve reusable components and workflow-level overrides, and get an execution order that honours dependsOn and $steps references.
let parameters = try document.effectiveParameters(for: step, in: workflow) // workflow params + step overrides, reusables resolved
let onSuccess = try document.effectiveSuccessActions(for: step, in: workflow)
let onFailure = try document.effectiveFailureActions(for: step, in: workflow)
let inputs = try document.resolvedInputs(for: workflow) // follows #/components/inputs/…
let order = try workflow.executionOrder(components: document.components) // honours dependsOn + $steps referencesBuilt for
Tools that read, write or run workflows.
Editors and IDE tooling
Lossless round trips keep a user's file as they wrote it, and expression templates carry the range of every segment and error for highlighting and inline diagnostics.
Linters and CI checks
Run validateOrThrow() in a command-line tool or a test, and fail the build with a JSON Pointer path to every problem, on macOS or in a Linux container.
API test runners and workflow engines
Evaluate criteria, render templates, resolve parameters and order steps. Your runner sends the requests; swift-arazzo works out what to send and whether it succeeded.
Agents and code generation
Give an agent a validated, typed plan of API calls instead of free-form text, or generate Arazzo descriptions and check them before anyone runs them.
Quality and speed
Tested against the specification. Measured on every change.
| Benchmark (p50, Apple silicon) | 0.1.0 | 0.1.1 |
|---|---|---|
| Parse 12 runtime expressions | 12.9 µs | 3.1 µs |
| Parse 4 conditions | 18.8 µs | 5.0 µs |
| Validate a large description (~500 KB) | 17.4 ms | 7.8 ms |
| Execution order and effective parameters, same description | 4.9 ms | 2.0 ms |
Benchmarks use package-benchmark and live in the repository: swift package --package-path Benchmarks benchmark.
Install
Add it to your package.
dependencies: [
.package(url: "https://github.com/mackoj/swift-arazzo.git", from: "0.1.3"),
],
targets: [
.target(name: "MyTool", dependencies: [
.product(name: "ArazzoKit", package: "swift-arazzo"),
.product(name: "ArazzoKitYAML", package: "swift-arazzo"), // optional
.product(name: "ArazzoKitBuilder", package: "swift-arazzo"), // optional
]),
]
- Swift 6.0+ on Linux, macOS 13+, iOS 16+, tvOS 16+, watchOS 9+ and visionOS 1+.
ArazzoKit: models, expressions, evaluation and validation. Depends only on swift-jsonpath.ArazzoKitYAML: YAML decoding and encoding on Yams, behind theYAMLpackage trait (on by default). Turn it off withtraits: []to skip building Yams.ArazzoKitBuilder: a result-builder DSL that writes documents in Swift. Depends only onArazzoKit.- API reference on the Swift Package Index; guide and examples in the README.