Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@

Generates Dyalog APL client code from OpenAPI 3.0 specifications.

[![Release](https://github.com/Dyalog/OpenAPI/actions/workflows/release.yml/badge.svg)](https://github.com/Dyalog/OpenAPI/releases/latest)
[![Release](https://github.com/dyalog-labs/OpenAPI/actions/workflows/release.yml/badge.svg)](https://github.com/dyalog-labs/OpenAPI/releases/latest)

> **This project is in active development and may not yet be fully functional with the entirety of the OpenAPI specification. Please raise a GitHub issue for any discovered bugs or feature requests.**

## Documentation

Full documentation is available at **[dyalog.github.io/OpenAPI](https://dyalog.github.io/OpenAPI)**, including installation, CLI reference, and a guide to the generated client.
Full documentation is available at **[dyalog-labs.github.io/OpenAPI](https://dyalog-labs.github.io/OpenAPI)**, including installation, CLI reference, and a guide to the generated client.

## Quick Start

1. Download the binary for your platform from the [GitHub Releases page](https://github.com/Dyalog/OpenAPI/releases/latest).
1. Download the binary for your platform from the [GitHub Releases page](https://github.com/dyalog-labs/OpenAPI/releases/latest).
2. Run the generator against your spec:

```
openapidyalog path/to/openapi.yaml ./output
```

See [Installation](https://dyalog.github.io/OpenAPI/installation/) for platform-specific setup details.
See [Installation](https://dyalog-labs.github.io/OpenAPI/installation/) for platform-specific setup details.

## Requirements

Expand Down
59 changes: 51 additions & 8 deletions dev-docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,29 +52,32 @@ Task GenerateVersionAsync(OpenApiDocument document, string outputDirectory)
Task CopyHttpCommandAsync(string outputDirectory)
Task CopySpecificationAsync(string specificationPath, string outputDirectory)
Task GenerateClientAsync(OpenApiDocument document, string outputDirectory)
Task GenerateReadmeAsync(OpenApiDocument document, string outputDirectory)
Task GenerateReadmeAsync(OpenApiDocument document, string outputDirectory, IReadOnlyDictionary<string, IOpenApiSchema>? inlineSchemas = null)
Task GenerateTagDocsAsync(OpenApiDocument document, string outputDirectory, IReadOnlyDictionary<string, IOpenApiSchema>? inlineSchemas = null)
```

`GenerateTagDocsAsync` writes `docs/<tag>.md` for each tag, named by the tag's APL name.

## `EndpointGeneratorService`

Generates one `.aplf` file per operation.

```csharp
Task<List<(string Name, OpenApiSchema Schema)>> GenerateEndpointsAsync(
Task<IReadOnlyDictionary<string, IOpenApiSchema>> GenerateEndpointsAsync(
OpenApiDocument document,
string outputDirectory,
string? @namespace)
string? @namespace = null)
```

Returns a list of inline schemas discovered during generation (reserved for future model generation).
Returns the inline request body schemas found during generation, keyed by the name of the model to generate for each.

## `ModelGeneratorService`

Placeholder — not yet implemented. Both methods are intentional no-ops.
Generates one model class per schema, in `APLSource/models/`.

```csharp
Task GenerateComponentModelsAsync(OpenApiDocument document, string outputDirectory)
Task GenerateInlineSchemaModelsAsync(List<(string Name, OpenApiSchema Schema)> schemas, string outputDirectory)
Task GenerateInlineSchemaModelsAsync(IReadOnlyDictionary<string, IOpenApiSchema> inlineSchemas, OpenApiDocument document, string outputDirectory)
```

## `TemplateService`
Expand Down Expand Up @@ -109,7 +112,7 @@ CLI configuration passed through the pipeline.
static string ToValidAplName(string name)
```

Converts an arbitrary string to a valid APL identifier. Invalid characters are replaced with `⍙<UCS code>⍙` escaping, identical to Dyalog's JSON name mangling (`7159⌶`).
Converts an arbitrary string to a valid APL identifier. Invalid characters are replaced with `⍙<UCS code>⍙` escaping, identical to Dyalog's JSON name mangling (`0(7162⌶)`).

```csharp
static string CommentLines(string? text)
Expand All @@ -123,4 +126,44 @@ Prefixes each line of `text` with `⍝ `.
static string ToDyalogPath(string path)
```

Converts an OpenAPI path template (e.g. `/pets/{petId}`) to a Dyalog APL expression (e.g. `'/pets/',(⍕argsNs.petId)`).
Converts an OpenAPI path template (e.g. `/pets/{petId}`) to a Dyalog APL expression (e.g. `'/pets/',(c.∆.HttpCommand.UrlEncode⍕argsNs.petId)`).

## `OperationNaming`

Naming rules shared by code and docs generation.

```csharp
static string TagOf(OpenApiOperation operation)
static string TagName(string tag)
static string FunctionName(string? operationId, string method, string path)
static List<IOpenApiParameter> MergeParameters(IOpenApiPathItem? pathItem, OpenApiOperation operation)
static JsonBody? DescribeJsonBody(IOpenApiSchema schema, string functionName, OpenApiDocument? document)
```

`TagName` gives the Client field, `_tags` directory and docs file name for a tag; it is always a valid APL name, so is safe as a file name. `MergeParameters` overrides path item parameters with operation parameters of the same name and location. `DescribeJsonBody` gives the argument name, model name and array-ness of a JSON request body. A `$ref` is resolved, so a reference to an array of models is an array; a reference to anything else has no model. For an unnamed body that is not a model or an array of models, it returns null (the body is then passed as `body`).

## `SchemaHelpers`

Schema rules shared by model, endpoint and docs generation.

```csharp
static IOpenApiSchema? Resolve(IOpenApiSchema? schema, OpenApiDocument? document)
static bool IsObjectModel(IOpenApiSchema schema)
static bool IsModelReference(IOpenApiSchema? schema, OpenApiDocument? document)
static string ClassNameOf(string schemaName)
static IEnumerable<KeyValuePair<string, IOpenApiSchema>> ModelComponents(OpenApiDocument document)
static List<(string Key, IOpenApiSchema Schema, bool Required)> Properties(IOpenApiSchema schema, OpenApiDocument? document)
static void CheckModelClassNames(OpenApiDocument document)
```

Only object schemas (`ModelComponents`) get a model class. `Properties` includes properties inherited through `allOf` at any depth. `CheckModelClassNames` throws if two schema names map to the same class.

## `DocsBuilder`

```csharp
DocsBuilder(OpenApiDocument document, IReadOnlyDictionary<string, IOpenApiSchema>? inlineSchemas = null)
List<TagDoc> BuildTags()
List<ModelDoc> BuildModels()
```

Builds the documentation model for `README.md` and `docs/<tag>.md`, including a usage example for each operation. `inlineSchemas` is what `EndpointGeneratorService.GenerateEndpointsAsync` returns, so the docs name each inline model as it was generated (an inline model is numbered if its name is taken).
42 changes: 29 additions & 13 deletions dev-docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ OpenAPI spec (JSON/YAML)
│
▼
CodeGeneratorService — orchestrates generation; delegates to three sub-services:
├── ArtifactGeneratorService — utils.apln, Version.aplf, HttpCommand.aplc, Client.aplc, README.md
├── ArtifactGeneratorService — utils.apln, Version.aplf, HttpCommand.aplc, Client.aplc, README.md, docs/<tag>.md
├── EndpointGeneratorService — one .aplf per operation, grouped under _tags/<tag>/
└── ModelGeneratorService — model classes (not yet implemented)
└── ModelGeneratorService — one .aplc model class per schema, under models/
│
▼
TemplateService — renders each Scriban template with its context object
│
▼
Output directory — APLSource/ tree + README.md
Output directory — APLSource/ tree + README.md + docs/
```

## Services
Expand All @@ -38,8 +38,9 @@ Wraps `Microsoft.OpenApi.Reader` to parse and optionally validate the spec. Retu
Thin orchestrator. Calls the three sub-services in order:

1. `ArtifactGeneratorService` — shared/static artifacts
2. `EndpointGeneratorService` — per-operation function files
3. `ModelGeneratorService` — model classes (currently no-ops)
2. `EndpointGeneratorService` — per-operation function files, returning the inline request body schemas it found
3. `ArtifactGeneratorService` — `Client.aplc`, `README.md` and `docs/<tag>.md`
4. `ModelGeneratorService` — model classes for the inline schemas and the component schemas

### `ArtifactGeneratorService`

Expand All @@ -52,46 +53,61 @@ Generates files that are the same regardless of the number of operations:
| `APLSource/HttpCommand.aplc` | Embedded binary (copied directly) |
| `APLSource/Client.aplc` | `Client.aplc.scriban` |
| `README.md` | `README.md.scriban` |
| `docs/<tag>.md` (one per tag) | `docs/tag.md.scriban` |

The original spec file is also copied into the output directory.

The README and tag pages are built from `DocsBuilder`, which produces a usage example for each operation. Their names come from `OperationNaming`, as the endpoint functions' names do, so the docs always match the generated code.

### `EndpointGeneratorService`

Groups all operations by tag, then for each operation:

1. Constructs an `OperationTemplateContext` from the OpenAPI operation object
2. Resolves the request body type and form fields
1. Constructs an `OperationTemplateContext` from the OpenAPI operation object, with the path item's parameters merged in
2. Resolves the request body's argument name, model and form fields
3. Converts path parameter placeholders into Dyalog APL concatenation expressions via `PathConverter`
4. Renders `endpoint.aplf.scriban` and writes to `APLSource/_tags/<tag>/<OperationId>.aplf`

Operations with no tag are placed under the `default` tag.

### `ModelGeneratorService`

Placeholder — both public methods are intentional no-ops. Model generation is planned but not yet implemented.
Renders `models/model.aplc.scriban` once per component object schema, and once per inline request body schema found by `EndpointGeneratorService` (named `<OperationId>Request`). Array and primitive schemas get no class; a `$ref` to one is treated as that schema. `allOf` sub-schemas are flattened into one class, to any depth; an object schema with no properties of its own becomes a map type. A model keeps its property values in a private namespace, `⍙v`, by JSON name, so a property cannot clash with the class's own members; a property whose name is one the template uses (`FormatNS`, `args`, an `Enum…` namespace, …) has `_` appended.

`CodeGeneratorService` first checks that no two schema names map to the same class name (e.g. `pet` and `Pet`), and stops with an error if they do, as one class would overwrite the other.

### `TemplateService`

Loads Scriban templates from embedded assembly resources and renders them with a context object. When building the Scriban script object, all C# property names are converted to `snake_case` so templates use `operation_id` rather than `OperationId`. `CustomProperties` dictionaries on context objects are merged in the same way.

Two custom template functions are registered:
These custom template functions are registered:

- `comment_lines` — prefixes each line of a string with `⍝`
- `get_operations_by_tag` — returns operations grouped by tag, for use in the README template
- `apl_name` — converts a name to a valid APL identifier (`StringHelpers.ToValidAplName`)
- `one_line` — collapses a string onto one line
- `md_cell` — makes a string safe for a Markdown table cell

## Context objects

Each template receives a typed context object. The two main ones are:

**`ApiTemplateContext`** — used for document-level templates (Client, Utils, README, etc.). Exposes the full OpenAPI document, API title/version/description, base URL, all tags, and security scheme metadata.
**`ApiTemplateContext`** — used for document-level templates (Client, Utils, README, etc.). Exposes the full OpenAPI document, API title/version/description, base URL, all tags, and security scheme metadata, plus `TagDocs` and `Models` (built by `DocsBuilder`) for the README and tag pages.

**`ModelTemplateContext`** — used for model templates. Exposes a model's class name, properties and enum values.

**`OperationTemplateContext`** — used for endpoint templates. Exposes a single operation's method, path, parameters, request body (including content type and resolved field names), response codes, and security requirements.

## Key utilities

**`PathConverter`** — converts an OpenAPI path template such as `/pets/{petId}` into a Dyalog APL expression: `'/pets/',(⍕argsNs.petId)`. Segments and parameter references are concatenated with `,`.
**`PathConverter`** — converts an OpenAPI path template such as `/pets/{petId}` into a Dyalog APL expression: `'/pets/',(c.∆.HttpCommand.UrlEncode⍕argsNs.petId)`. Segments and parameter references are concatenated with `,`. Each parameter value is percent-encoded with `HttpCommand.UrlEncode`, so a value containing `/` or a space stays in its own path segment.

**`StringHelpers.ToValidAplName`** — converts an arbitrary string into a valid APL identifier. If the name contains characters outside the APL identifier character set, or begins with a digit, it is prefixed with `⍙` and each invalid character is replaced with `⍙<UCS code>⍙`. This is the same escaping scheme used by Dyalog's JSON name mangling (`0(7162⌶)`), so `⎕JSON` restores the original name. Parameters and model properties whose names are not valid APL are held under these names.

**`OperationNaming`** — the naming rules shared by code and docs generation: a tag's APL name (its Client field, `_tags` directory and docs file name), an operation's function name, the merged parameter list, and the argument name and model for a JSON request body.

**`SchemaHelpers`** — schema rules shared by model, endpoint and docs generation: resolving a `$ref`, whether a schema gets a model class, a schema's properties including those inherited through `allOf`, and the model class name for a schema.

**`StringHelpers.ToValidAplName`** — converts an arbitrary string into a valid APL identifier. If the name contains characters outside the APL identifier character set, or begins with a digit, it is prefixed with `⍙` and each invalid character is replaced with `⍙<UCS code>⍙`. This is the same escaping scheme used by Dyalog's JSON name mangling (`7159⌶`).
**`DocsBuilder`** — builds the documentation model for the README and tag pages, including a usage example for each operation.

## Design notes

Expand Down
2 changes: 1 addition & 1 deletion dev-docs/development-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
## Clone the repository

```bash
git clone https://github.com/Dyalog/OpenAPI.git
git clone https://github.com/dyalog-labs/OpenAPI.git
cd OpenAPI
```

Expand Down
29 changes: 22 additions & 7 deletions dev-docs/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ Templates/
│ ├── HttpCommand.aplc → APLSource/HttpCommand.aplc (copied as-is, not a template)
│ ├── _tags/
│ │ └── endpoint.aplf.scriban → APLSource/_tags/<tag>/<OperationId>.aplf
│ └── models/
│ └── model.aplc.scriban → APLSource/models/<Model>.aplc
├── docs/
│ └── tag.md.scriban → docs/<tag>.md
└── README.md.scriban → README.md
```

Expand Down Expand Up @@ -60,7 +64,7 @@ C# context properties are automatically renamed to `snake_case` before being pas

### Document-level templates

Used by `Client.aplc.scriban`, `utils.apln.scriban`, `Version.aplf.scriban`, `README.md.scriban`.
Used by `Client.aplc.scriban`, `utils.apln.scriban`, `Version.aplf.scriban`, `README.md.scriban`, `docs/tag.md.scriban`.

| Property | Description |
|---|---|
Expand All @@ -71,13 +75,18 @@ Used by `Client.aplc.scriban`, `utils.apln.scriban`, `Version.aplf.scriban`, `RE
| `generated_at` | UTC timestamp of generation |
| `tags` | List of all tag names used in the spec |
| `security_schemes` | Map of security scheme name → scheme info (`type`, `in`, `parameter_name`, `scheme`, `bearer_format`) |
| `schemas` | Map of component schema name → schema info |
| `has_security_schemes` | Whether there are any security schemes (`.size` does not work on maps in Scriban) |
| `schemas` | Map of component schema name → schema |
| `tag_docs` | List of tags, each with `name`, `apl_name`, `display_name`, `description`, `doc_path` and `operations`. Each operation has `function_name`, `method`, `path`, `summary`, `description`, `deprecated`, `parameters`, `request_body`, `responses` and `usage_example` |
| `models` | List of generated models, each with `class_name` and `description` |

Custom properties (set per-template in `ArtifactGeneratorService`):

| Property | Set for | Description |
|---|---|---|
| `class_name` | `Client.aplc` | Generated class name (default: `Client`) |
| `class_name` | `Client.aplc`, `README.md` | Generated class name (default: `Client`) |
| `tags` | `Client.aplc` | APL names of the tags, one per Client field |
| `tag` | `docs/tag.md` | The tag being documented (an item of `tag_docs`) |

### Endpoint template

Expand All @@ -88,29 +97,35 @@ Used by `_tags/endpoint.aplf.scriban`.
| `operation_id` | Sanitised, PascalCase function name |
| `method` | HTTP method (lowercase) |
| `path` | Original path string (e.g. `/pets/{petId}`) |
| `dyalog_path` | Path converted to Dyalog APL expression (e.g. `'/pets/',(⍕argsNs.petId)`) |
| `dyalog_path` | Path converted to Dyalog APL expression (e.g. `'/pets/',(c.∆.HttpCommand.UrlEncode⍕argsNs.petId)`) |
| `summary` | Operation summary |
| `description` | Operation description |
| `parameters` | List of parameters, each with `name`, `in`, `required`, `schema` |
| `request_body` | Request body object, or null |
| `request_content_type` | Content type of the request body |
| `request_json_body_type` | Resolved JSON schema type name, or null |
| `request_json_body_type` | Model class for a JSON body (or its items), or null |
| `request_body_arg_name` | Field on `argsNs` that holds a JSON body |
| `request_body_is_array` | Whether a JSON body is an array |
| `form_fields` | List of form fields for `multipart/form-data` |
| `responses` | Map of status code → response |
| `has_security` | Boolean — whether the operation declares security requirements |
| `security_scheme_names` | List of required security scheme names |

## Custom template functions

Two functions are available in all templates:
These functions are available in all templates:

**`comment_lines <text>`** — prefixes every line of `text` with `⍝ `.

```scriban
{{ comment_lines description }}
```

**`get_operations_by_tag`** — returns operations grouped by tag, used in `README.md.scriban`. Returns a dictionary of tag name → list of operation info objects (each with `operation_id`, `method`, `path`, `summary`, `parameters`, `has_request_body`).
**`apl_name <name>`** — converts `name` to a valid APL identifier, as used for parameters on `argsNs`.

**`one_line <text>`** — collapses `text` onto one line.

**`md_cell <text>`** — makes `text` safe for a Markdown table cell: one line, with `|` escaped.

## Modifying a template

Expand Down
2 changes: 1 addition & 1 deletion docs/_archive/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ dotnet run -- ../pet-store/openapi.json ../pet-store/PetStore-Client
]LINK.Import # pet-store/PetStore-Client/APLSource

⍝ Create a client instance
client←⎕NEW Client (baseURL:'https://petstore3.swagger.io/api/v3')
client←⎕NEW Client (baseUrl:'https://petstore3.swagger.io/api/v3')

⍝ Find available pets
pets←api.pet.findPetsByStatus.sync (client:client ⋄ status:'available')
Expand Down
2 changes: 1 addition & 1 deletion docs/_archive/usage-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ This will create a complete APL client in the `./output` directory.

```apl
⍝ Create a client instance
client ← ⎕NEW Client (baseURL: 'https://api.example.com/v4')
client ← ⎕NEW Client (baseUrl: 'https://api.example.com/v4')

⍝ Call an API method
result ← api.pet.findPetsByStatus.syncDetailed (client:client ⋄ status:'available')
Expand Down
Loading
Loading