Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ jobs:
- uses: moonrepo/setup-toolchain@v0
with:
moon-version: "2.5.5"
- run: rustup toolchain install 1.98.1 --profile minimal
- run: rustup default 1.98.1
- run: mkdir -p "$HOME/.cache/go-build"
- name: Check affected tasks
id: ci
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ test-results/
*.pprof
dump-*.json
**/.moon/cache/

/testdata/rust-client/target/
/testdata/rust-client/src/public/
/testdata/rust-client/src/models/
3 changes: 3 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ linters:
- path: ^internal/tsemit/tsemit\.go$
linters: [gosec]
text: G301|G306
- path: ^internal/rustemit/rustemit\.go$
linters: [gosec]
text: G301|G306
# CLI input paths are intentionally supplied by the caller.
- path: ^internal/openapi/openapi\.go$
linters: [gosec]
Expand Down
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# OASmith

OASmith generates focused Go and TypeScript code from OpenAPI YAML or JSON
OASmith generates focused Go, TypeScript, and Rust code from OpenAPI YAML or JSON
documents.
It supports focused generation modes without the runtime and configuration
surface of a general-purpose OpenAPI generator.
Expand All @@ -12,6 +12,8 @@ surface of a general-purpose OpenAPI generator.
| `types` | `go` | Go models |
| `client` | `go` | Go models and HTTP client |
| `client` | `typescript` | TypeScript types and HTTP client |
| `types` | `rust` | Serde models in `mod.rs` |
| `client` | `rust` | Serde models and a Reqwest client in `mod.rs` |

OASmith handles the OpenAPI schema and operation subset covered by its fixture
suite, including objects, arrays, enums, `oneOf` discriminators, parameters,
Expand Down Expand Up @@ -39,7 +41,7 @@ Every invocation requires:

- `--openapi`: input OpenAPI YAML or JSON document;
- `--mode`: `types` or `client`;
- `--lang`: `go` or `typescript`, subject to the supported pairs above;
- `--lang`: `go`, `typescript`, or `rust`, subject to the supported pairs above;
- `--out`: generated output directory.

JSON input is supported alongside YAML. The document syntax is accepted
Expand Down Expand Up @@ -162,3 +164,22 @@ reports are attached to the workflow, including on failure.
## License

[MIT](LICENSE)

## Rust clients

Use `--mode client --lang rust --out src/api`, then `mod api;`. Add `serde` 1
(with `derive`), `serde_json` 1, and `reqwest` 0.12 (with `json`) to Cargo dependencies.
Choose the Reqwest TLS features appropriate to your application.

Construct `api::Client::new(http, base_url, bearer_token)` with your configured
Reqwest client. Operation methods return request builders, so callers own timeouts,
cancellation, tracing, and bounded body reads. Models and operation-specific
`Response::decode` enums preserve declared HTTP statuses; undeclared statuses
retain their original response. SSE responses remain streaming Reqwest responses,
leaving event framing and cancellation to the caller.

Rust supports JSON and raw request bodies, optional bodies, scalar and repeated
query parameters, headers, escaped path parameters, enums, nullable values and
untagged `oneOf` models. Sequential multipart requests currently fail generation
with an explicit unsupported-body error. Run `moon run oasmith:test-rust` to generate,
compile, and execute the Rust contract fixtures.
14 changes: 9 additions & 5 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/responsibleapi/oasmith/internal/goemit"
"github.com/responsibleapi/oasmith/internal/openapi"
"github.com/responsibleapi/oasmith/internal/rustemit"
"github.com/responsibleapi/oasmith/internal/tsemit"
)

Expand All @@ -29,14 +30,16 @@ func Run(args []string) error {
return err
}
switch {
case opts.Lang == "rust":
return rustemit.Emit(doc, rustemit.Options{OutDir: opts.Out}, opts.Mode == "client")
case opts.Mode == "types" && opts.Lang == "go":
return goemit.Emit(doc, goemit.Options{OutDir: opts.Out, SourcePath: opts.OpenAPI})
case opts.Mode == "client" && opts.Lang == "go":
return goemit.EmitClient(doc, goemit.Options{OutDir: opts.Out, SourcePath: opts.OpenAPI})
case opts.Mode == "client" && opts.Lang == "typescript":
return tsemit.Emit(doc, tsemit.Options{OutDir: opts.Out})
default:
return fmt.Errorf("unsupported --mode/--lang pair %q/%q; valid pairs are types/go, client/go, and client/typescript", opts.Mode, opts.Lang)
return fmt.Errorf("unsupported --mode/--lang pair %q/%q; valid pairs are types/go, client/go, client/typescript, types/rust, and client/rust", opts.Mode, opts.Lang)
}
}

Expand All @@ -49,14 +52,15 @@ func Parse(args []string) (Options, error) {
fs.StringVar(&opts.Lang, "lang", "", "output language")
fs.StringVar(&opts.Out, "out", "", "output directory")
if err := fs.Parse(args); err != nil {
return Options{}, fmt.Errorf("usage: oasmith --openapi <openapidoc> --mode <types|client> --lang <go|typescript> --out <dir>")
return Options{}, fmt.Errorf("usage: oasmith --openapi <openapidoc> --mode <types|client> --lang <go|typescript|rust> --out <dir>")
}
if opts.OpenAPI == "" || opts.Mode == "" || opts.Lang == "" || opts.Out == "" {
return Options{}, fmt.Errorf("usage: oasmith --openapi <openapidoc> --mode <types|client> --lang <go|typescript> --out <dir>")
return Options{}, fmt.Errorf("usage: oasmith --openapi <openapidoc> --mode <types|client> --lang <go|typescript|rust> --out <dir>")
}
if (opts.Mode == "types" && opts.Lang == "go") ||
if ((opts.Mode == "types" || opts.Mode == "client") && opts.Lang == "rust") ||
(opts.Mode == "types" && opts.Lang == "go") ||
(opts.Mode == "client" && (opts.Lang == "go" || opts.Lang == "typescript")) {
return opts, nil
}
return Options{}, fmt.Errorf("unsupported --mode/--lang pair %q/%q; valid pairs are types/go, client/go, and client/typescript", opts.Mode, opts.Lang)
return Options{}, fmt.Errorf("unsupported --mode/--lang pair %q/%q; valid pairs are types/go, client/go, client/typescript, types/rust, and client/rust", opts.Mode, opts.Lang)
}
2 changes: 1 addition & 1 deletion internal/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func TestParseRejectsInvalidModeLangPair(t *testing.T) {
if err == nil {
t.Fatal("Parse invalid mode/lang succeeded")
}
if !strings.Contains(err.Error(), "valid pairs are types/go, client/go, and client/typescript") {
if !strings.Contains(err.Error(), "valid pairs are types/go, client/go, client/typescript, types/rust, and client/rust") {
t.Fatalf("Parse error = %q, want valid pair message", err.Error())
}
}
Expand Down
35 changes: 18 additions & 17 deletions internal/openapi/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,23 +102,24 @@ type Encoding struct {

// Schema describes the OpenAPI schema subset supported by the generator.
type Schema struct {
Ref string `yaml:"$ref"`
Title string `yaml:"title"`
Type Type `yaml:"type"`
Format string `yaml:"format"`
Description string `yaml:"description"`
Enum []string `yaml:"enum"`
Const any `yaml:"const"`
Properties map[string]*Schema `yaml:"properties"`
Required []string `yaml:"required"`
Items *Schema `yaml:"items"`
PrefixItems []*Schema `yaml:"prefixItems"`
MinItems *int `yaml:"minItems"`
MaxItems *int `yaml:"maxItems"`
OneOf []*Schema `yaml:"oneOf"`
Discriminator *Discriminator `yaml:"discriminator"`
ContentMediaType string `yaml:"contentMediaType"`
ContentSchema *Schema `yaml:"contentSchema"`
AdditionalProperties any `yaml:"additionalProperties"`
Ref string `yaml:"$ref"`
Title string `yaml:"title"`
Type Type `yaml:"type"`
Format string `yaml:"format"`
Description string `yaml:"description"`
Enum []string `yaml:"enum"`
Const any `yaml:"const"`
Properties map[string]*Schema `yaml:"properties"`
Required []string `yaml:"required"`
Items *Schema `yaml:"items"`
PrefixItems []*Schema `yaml:"prefixItems"`
MinItems *int `yaml:"minItems"`
MaxItems *int `yaml:"maxItems"`
OneOf []*Schema `yaml:"oneOf"`
Discriminator *Discriminator `yaml:"discriminator"`
ContentMediaType string `yaml:"contentMediaType"`
ContentSchema *Schema `yaml:"contentSchema"`
}

// Type holds one or more OpenAPI schema type names.
Expand Down
Loading