diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0d59874..dce363f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -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 diff --git a/.gitignore b/.gitignore index 9b91904..94649b9 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/.golangci.yaml b/.golangci.yaml index e727854..0c01033 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -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] diff --git a/README.md b/README.md index e3a6ecb..18f19f3 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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, @@ -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 @@ -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. diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 72113f2..ec07319 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -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" ) @@ -29,6 +30,8 @@ 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": @@ -36,7 +39,7 @@ func Run(args []string) error { 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) } } @@ -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 --mode --lang --out ") + return Options{}, fmt.Errorf("usage: oasmith --openapi --mode --lang --out ") } if opts.OpenAPI == "" || opts.Mode == "" || opts.Lang == "" || opts.Out == "" { - return Options{}, fmt.Errorf("usage: oasmith --openapi --mode --lang --out ") + return Options{}, fmt.Errorf("usage: oasmith --openapi --mode --lang --out ") } - 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) } diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 430d5a3..91fd6e0 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -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()) } } diff --git a/internal/openapi/openapi.go b/internal/openapi/openapi.go index cbc8b72..60a0d25 100644 --- a/internal/openapi/openapi.go +++ b/internal/openapi/openapi.go @@ -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. diff --git a/internal/rustemit/rustemit.go b/internal/rustemit/rustemit.go new file mode 100644 index 0000000..5e3e351 --- /dev/null +++ b/internal/rustemit/rustemit.go @@ -0,0 +1,342 @@ +// Package rustemit emits serde models and reqwest clients from OpenAPI contracts. +package rustemit + +import ( + "fmt" + "os" + "path/filepath" + "slices" + "sort" + "strconv" + "strings" + "unicode" + + "github.com/responsibleapi/oasmith/internal/clientgen" + "github.com/responsibleapi/oasmith/internal/openapi" +) + +// Options configures Rust source emission. +type Options struct{ OutDir string } + +type emitter struct { + schemas map[string]*openapi.Schema + definitions map[string]string +} + +// Emit writes Rust serde models, optionally including a reqwest client. +func Emit(doc *openapi.Document, opts Options, client bool) error { + e := &emitter{schemas: doc.Components.Schemas, definitions: map[string]string{}} + for _, name := range doc.SchemaNames() { + e.define(typeName(name), doc.Components.Schemas[name]) + } + var methods strings.Builder + if client { + operations, err := clientgen.Analyze(doc) + if err != nil { + return fmt.Errorf("analyze Rust client: %w", err) + } + methods.WriteString(clientPrelude) + for _, operation := range operations { + if operation.RequestBody.Multipart != nil { + return fmt.Errorf("rust operation %s: sequential multipart is unsupported", operation.Route.Operation.OperationID) + } + methods.WriteString(e.operation(operation)) + } + methods.WriteString("}\n") + } + var out strings.Builder + out.WriteString("// Code generated by oasmith. DO NOT EDIT.\n#![allow(dead_code, clippy::large_enum_variant, clippy::enum_variant_names)]\nuse serde::{Deserialize, Serialize};\n\n") + names := make([]string, 0, len(e.definitions)) + for name := range e.definitions { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + out.WriteString(e.definitions[name]) + out.WriteString("\n") + } + out.WriteString(methods.String()) + if err := os.MkdirAll(opts.OutDir, 0o755); err != nil { + return fmt.Errorf("create Rust output: %w", err) + } + if err := os.WriteFile(filepath.Join(opts.OutDir, "mod.rs"), []byte(out.String()), 0o644); err != nil { + return fmt.Errorf("write Rust output: %w", err) + } + return nil +} + +func typeName(value string) string { return openapi.ExportName(value) } + +func snake(value string) string { + var out []rune + runes := []rune(value) + for index, r := range runes { + if !unicode.IsLetter(r) && !unicode.IsDigit(r) { + if len(out) > 0 && out[len(out)-1] != '_' { + out = append(out, '_') + } + continue + } + if unicode.IsUpper(r) && index > 0 && len(out) > 0 && out[len(out)-1] != '_' && + (unicode.IsLower(runes[index-1]) || unicode.IsDigit(runes[index-1]) || (index+1 < len(runes) && unicode.IsLower(runes[index+1]))) { + out = append(out, '_') + } + out = append(out, unicode.ToLower(r)) + } + result := strings.Trim(string(out), "_") + if result == "self" || result == "super" || result == "crate" { + return result + "_value" + } + if result == "" { + result = "value" + } + if unicode.IsDigit([]rune(result)[0]) { + result = "x_" + result + } + if slices.Contains([]string{"type", "ref", "enum", "struct", "fn", "mod", "self", "super", "crate", "match", "loop", "in", "as", "move", "where", "async", "await", "use", "pub", "const", "static", "impl", "trait", "dyn", "let", "for", "while", "return", "true", "false", "box", "gen"}, result) { + return "r#" + result + } + return result +} + +func (e *emitter) rustType(schema *openapi.Schema, name string) string { + if schema == nil { + return "serde_json::Value" + } + if schema.Ref != "" { + return typeName(schema.Ref[strings.LastIndex(schema.Ref, "/")+1:]) + } + if schema.Type.Has("null") && len(schema.Type) > 1 { + copySchema := *schema + copySchema.Type = nil + for _, typ := range schema.Type { + if typ != "null" { + copySchema.Type = append(copySchema.Type, typ) + } + } + return "std::option::Option<" + e.rustType(©Schema, name) + ">" + } + if len(schema.Properties) > 0 || len(schema.OneOf) > 0 || len(schema.Enum) > 0 || schema.Const != nil { + e.define(name, schema) + return name + } + switch schema.Type.First() { + case "string": + return "std::string::String" + case "integer": + return "i64" + case "number": + return "f64" + case "boolean": + return "bool" + case "array": + return "std::vec::Vec<" + e.rustType(schema.Items, name+"Item") + ">" + case "null": + return "()" + default: + return "serde_json::Value" + } +} + +func (e *emitter) define(name string, schema *openapi.Schema) { + if _, exists := e.definitions[name]; exists { + return + } + e.definitions[name] = "" + var out strings.Builder + switch { + case schema != nil && (len(schema.Enum) > 0 || schema.Const != nil): + values := schema.Enum + if value, ok := schema.Const.(string); ok { + values = []string{value} + } + if len(values) == 0 { + e.definitions[name] = "pub type " + name + " = serde_json::Value;\n" + return + } + out.WriteString("#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]\npub enum " + name + " {\n") + used := map[string]bool{} + for index, value := range values { + variant := typeName(value) + if value == strings.ToUpper(value) { + variant = typeName(strings.ToLower(value)) + } + if variant == "" || used[variant] { + variant = fmt.Sprintf("Value%d", index) + } + used[variant] = true + fmt.Fprintf(&out, " #[serde(rename = %s)]\n %s,\n", strconv.Quote(value), variant) + } + out.WriteString("}\n") + case schema != nil && len(schema.OneOf) > 0: + out.WriteString("#[derive(Clone, Debug, Serialize, Deserialize)]\n#[serde(untagged)]\npub enum " + name + " {\n") + for index, item := range schema.OneOf { + typ := e.rustType(item, fmt.Sprintf("%sVariant%d", name, index)) + fmt.Fprintf(&out, " Variant%d(%s),\n", index, typ) + } + out.WriteString("}\n") + case schema != nil && len(schema.Properties) > 0: + e.object(&out, name, schema) + default: + fmt.Fprintf(&out, "pub type %s = %s;\n", name, e.rustType(schema, name+"Value")) + } + e.definitions[name] = out.String() +} + +func (e *emitter) object(out *strings.Builder, name string, schema *openapi.Schema) { + out.WriteString("#[derive(Clone, Debug, Serialize, Deserialize)]\n") + if allowed, ok := schema.AdditionalProperties.(bool); ok && !allowed { + out.WriteString("#[serde(deny_unknown_fields)]\n") + } + out.WriteString("pub struct " + name + " {\n") + keys := make([]string, 0, len(schema.Properties)) + for key := range schema.Properties { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + typ := e.rustType(schema.Properties[key], name+typeName(key)) + fmt.Fprintf(out, " #[serde(rename = %s", strconv.Quote(key)) + if !slices.Contains(schema.Required, key) { + out.WriteString(", default, skip_serializing_if = \"Option::is_none\"") + typ = "std::option::Option<" + typ + ">" + } + fmt.Fprintf(out, ")]\n pub %s: %s,\n", snake(key), typ) + } + out.WriteString("}\n") +} + +func (e *emitter) operation(op clientgen.Operation) string { + name := typeName(op.Route.Operation.OperationID) + var params, body strings.Builder + params.WriteString("#[derive(Clone, Debug)]\npub struct " + name + "Params {\n") + fmt.Fprintf(&body, " pub fn %s(&self", snake(op.Route.Operation.OperationID)) + hasParams := len(op.Route.Operation.Parameters) > 0 || op.RequestBody.JSON != nil || op.RequestBody.Raw != nil + if hasParams { + fmt.Fprintf(&body, ", params: %sParams", name) + } + body.WriteString(") -> reqwest::RequestBuilder {\n") + fmt.Fprintf(&body, " let path = %s.to_owned();\n", strconv.Quote(op.Route.Path)) + for _, param := range op.Route.Operation.Parameters { + typ := e.rustType(param.Schema, name+typeName(param.Name)) + if !param.Required { + typ = "std::option::Option<" + typ + ">" + } + fmt.Fprintf(¶ms, " pub %s: %s,\n", snake(param.Name), typ) + if param.In == "path" { + fmt.Fprintf(&body, " let path = path.replace(%s, &encode_path(¶ms.%s.to_string()));\n", strconv.Quote("{"+param.Name+"}"), snake(param.Name)) + } + } + fmt.Fprintf(&body, " let request = self.http.request(reqwest::Method::%s, format!(\"{}{}\", self.base_url, path)).header(\"Accept\", %s);\n", op.Route.Method, strconv.Quote(op.Accept)) + body.WriteString(" let request = match &self.bearer_token { Some(token) => request.bearer_auth(token), None => request };\n") + e.parameters(&body, op) + if op.RequestBody.JSON != nil { + typ := e.rustType(op.RequestBody.JSON.Schema, name+"Body") + if op.RequestBody.JSON.Required { + fmt.Fprintf(¶ms, " pub body: %s,\n", typ) + body.WriteString(" let request = request.json(¶ms.body);\n") + } else { + fmt.Fprintf(¶ms, " pub body: Option<%s>,\n", typ) + body.WriteString(" let request = match params.body { Some(body) => request.json(&body), None => request };\n") + } + } + if op.RequestBody.Raw != nil { + if op.RequestBody.Raw.Required { + params.WriteString(" pub body: std::vec::Vec,\n") + fmt.Fprintf(&body, " let request = request.header(\"Content-Type\", %s).body(params.body);\n", strconv.Quote(op.RequestBody.Raw.MediaType)) + } else { + params.WriteString(" pub body: std::option::Option>,\n") + fmt.Fprintf(&body, " let request = match params.body { Some(body) => request.header(\"Content-Type\", %s).body(body), None => request };\n", strconv.Quote(op.RequestBody.Raw.MediaType)) + } + } + + params.WriteString("}\n") + if hasParams { + e.definitions[name+"Params"] = params.String() + } + e.response(op, name) + source := body.String() + last := strings.LastIndex(source, " let request = ") + return source[:last] + " " + strings.TrimSuffix(source[last+len(" let request = "):], ";\n") + "\n }\n" +} + +func (e *emitter) parameters(body *strings.Builder, op clientgen.Operation) { + for _, param := range op.Route.Operation.Parameters { + field := snake(param.Name) + switch param.In { + case "query": + switch { + case param.Schema.Type.Has("array"): + query := fmt.Sprintf("request.query(&value.iter().map(|item| (%s, item)).collect::>())", strconv.Quote(param.Name)) + if param.Required { + fmt.Fprintf(body, " let value = ¶ms.%s;\n let request = %s;\n", field, query) + } else { + fmt.Fprintf(body, " let request = match ¶ms.%s { Some(value) => %s, None => request };\n", field, query) + } + case param.Required: + fmt.Fprintf(body, " let request = request.query(&[(%s, ¶ms.%s)]);\n", strconv.Quote(param.Name), field) + default: + fmt.Fprintf(body, " let request = match params.%s { Some(value) => request.query(&[(%s, value)]), None => request };\n", field, strconv.Quote(param.Name)) + } + case "header": + if param.Required { + fmt.Fprintf(body, " let request = request.header(%s, params.%s.to_string());\n", strconv.Quote(param.Name), field) + } else { + fmt.Fprintf(body, " let request = match params.%s { Some(value) => request.header(%s, value.to_string()), None => request };\n", field, strconv.Quote(param.Name)) + } + } + } +} + +func (e *emitter) response(op clientgen.Operation, name string) { + var response strings.Builder + fmt.Fprintf(&response, "#[derive(Debug)]\npub enum %sResponse {\n", name) + for _, res := range op.Responses { + typ := e.rustType(res.Schema, fmt.Sprintf("%sStatus%d", name, res.Status)) + switch { + case res.SSE: + typ = "reqwest::Response" + case !res.HasBody(): + typ = "()" + case !res.JSON(): + typ = "Vec" + } + fmt.Fprintf(&response, " Status%d(%s),\n", res.Status, typ) + } + response.WriteString(" Unexpected(reqwest::Response),\n}\n") + fmt.Fprintf(&response, "impl %sResponse {\n pub async fn decode(response: reqwest::Response) -> Result {\n Ok(match response.status().as_u16() {\n", name) + for _, res := range op.Responses { + value := "response.json().await?" + switch { + case res.SSE: + value = "response" + case !res.HasBody(): + value = "()" + case !res.JSON(): + value = "response.bytes().await?.to_vec()" + } + fmt.Fprintf(&response, " %d => Self::Status%d(%s),\n", res.Status, res.Status, value) + } + response.WriteString(" _ => Self::Unexpected(response),\n })\n }\n}\n") + e.definitions[name+"Response"] = response.String() +} + +const clientPrelude = ` +#[derive(Clone, Debug)] +pub struct Client { + http: reqwest::Client, + base_url: String, + bearer_token: Option, +} +fn encode_path(value: &str) -> String { + value.bytes().map(|byte| { + if byte.is_ascii_alphanumeric() || b"-._~".contains(&byte) { + char::from(byte).to_string() + } else { format!("%{byte:02X}") } + }).collect() +} +impl Client { + pub fn new(http: reqwest::Client, base_url: String, bearer_token: Option) -> Self { + Self { http, base_url: base_url.trim_end_matches('/').to_owned(), bearer_token } + } +` diff --git a/moon.yml b/moon.yml index ad9eb4d..3998b95 100644 --- a/moon.yml +++ b/moon.yml @@ -56,6 +56,31 @@ tasks: deps: - ~:lint - ~:test + - ~:test-rust inputs: [] options: runInCI: false + generate-rust-fixtures: + deps: [~:mod-download] + inputs: + - "@group(sources)" + - testdata/fixtures/*.yaml + script: |- + set -euo pipefail + go run ./cmd/oasmith --openapi testdata/fixtures/public-client.yaml --mode client --lang rust --out testdata/rust-client/src/public + go run ./cmd/oasmith --openapi testdata/fixtures/rust-models.yaml --mode types --lang rust --out testdata/rust-client/src/models + outputs: + - testdata/rust-client/src/public/mod.rs + - testdata/rust-client/src/models/mod.rs + options: + cache: true + test-rust: + deps: [~:generate-rust-fixtures] + inputs: + - "@group(sources)" + - testdata/fixtures/*.yaml + - testdata/rust-client/Cargo.* + - testdata/rust-client/src/lib.rs + command: cargo test --locked --manifest-path testdata/rust-client/Cargo.toml + options: + cache: true diff --git a/testdata/fixtures/rust-models.yaml b/testdata/fixtures/rust-models.yaml new file mode 100644 index 0000000..022651e --- /dev/null +++ b/testdata/fixtures/rust-models.yaml @@ -0,0 +1,14 @@ +openapi: 3.2.0 +info: {title: Rust type names, version: "1"} +paths: {} +components: + schemas: + String: {type: string} + Record: + type: object + required: [type, nullable, self] + properties: + type: {type: string} + self: {type: string} + nullable: {type: [string, "null"]} + optional: {type: string} diff --git a/testdata/rust-client/Cargo.lock b/testdata/rust-client/Cargo.lock new file mode 100644 index 0000000..ed2f137 --- /dev/null +++ b/testdata/rust-client/Cargo.lock @@ -0,0 +1,863 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "oasmith-rust-contract-tests" +version = "0.0.0" +dependencies = [ + "http", + "reqwest", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/testdata/rust-client/Cargo.toml b/testdata/rust-client/Cargo.toml new file mode 100644 index 0000000..9e4da3a --- /dev/null +++ b/testdata/rust-client/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "oasmith-rust-contract-tests" +version = "0.0.0" +edition = "2024" +publish = false +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +reqwest = { version = "0.12", default-features = false, features = ["json"] } +tokio = { version = "1", features = ["macros", "rt"] } +http = "1" diff --git a/testdata/rust-client/src/lib.rs b/testdata/rust-client/src/lib.rs new file mode 100644 index 0000000..4494e20 --- /dev/null +++ b/testdata/rust-client/src/lib.rs @@ -0,0 +1,71 @@ +mod public; +mod models; + +#[cfg(test)] +mod tests { + use super::{public::*, models}; + fn client() -> Client { + Client::new(reqwest::Client::new(), "http://localhost:1234/".into(), Some("test-key".into())) + } + #[test] + fn encodes_path_repeated_query_headers_and_json() { + let request = client().create_thing(CreateThingParams { + thing_id: "a/b ?#%".into(), tag: Some("x & y".into()), notify: false, + label: Some(vec!["one".into(), "two".into()]), x_request_id: "req-1".into(), + body: CreateThing { name: "Podcast".into() }, + }).build().unwrap(); + assert_eq!(request.url().path(), "/things/a%2Fb%20%3F%23%25"); + assert_eq!(request.url().query_pairs().collect::>(), vec![ + ("tag".into(), "x & y".into()), ("notify".into(), "false".into()), + ("label".into(), "one".into()), ("label".into(), "two".into()), + ]); + assert_eq!(request.headers()["authorization"], "Bearer test-key"); + assert_eq!(request.headers()["x-request-id"], "req-1"); + assert_eq!(request.headers()["content-type"], "application/json"); + let body: serde_json::Value = serde_json::from_slice(request.body().unwrap().as_bytes().unwrap()).unwrap(); + assert_eq!(body, serde_json::json!({"name":"Podcast"})); + } + #[test] + fn optional_bodies_are_absent_and_raw_bytes_are_unchanged() { + for request in [client().patch_thing(PatchThingParams {body: None}), client().upload_optional_media(UploadOptionalMediaParams {body: None})] { + let request = request.build().unwrap(); + assert!(request.body().is_none()); + assert!(!request.headers().contains_key("content-type")); + } + let request = client().upload_media(UploadMediaParams {owner: "owner".into(), upload_type: "media".into(), body: vec![0, 255, 10]}).build().unwrap(); + assert_eq!(request.headers()["content-type"], "application/octet-stream"); + assert_eq!(request.body().unwrap().as_bytes().unwrap(), &[0, 255, 10]); + } + #[tokio::test] + async fn decodes_statuses_and_leaves_event_stream_unconsumed() { + let response = reqwest::Response::from(http_response(201, r#"{"id":"a","name":"Podcast"}"#)); + let CreateThingResponse::Status201(thing) = CreateThingResponse::decode(response).await.unwrap() else { panic!("wrong success variant") }; + assert_eq!(thing.name, "Podcast"); + let response = reqwest::Response::from(http_response(400, r#"{"message":"invalid"}"#)); + assert!(matches!(CreateThingResponse::decode(response).await.unwrap(), CreateThingResponse::Status400(_))); + let response = reqwest::Response::from(http_response(204, "")); + assert!(matches!(PatchThingResponse::decode(response).await.unwrap(), PatchThingResponse::Status204(()))); + let response = reqwest::Response::from(http_response(502, "unavailable")); + assert!(matches!(CreateThingResponse::decode(response).await.unwrap(), CreateThingResponse::Unexpected(_))); + let request = client().watch_events().build().unwrap(); + assert_eq!(request.headers()["accept"], "text/event-stream"); + let event = "event: progress\ndata: {}\n\n"; + let response = reqwest::Response::from(http_response(200, event)); + let WatchEventsResponse::Status200(stream) = WatchEventsResponse::decode(response).await.unwrap() else { panic!("wrong event variant") }; + assert_eq!(stream.text().await.unwrap(), event); + } + fn http_response(status: u16, body: &str) -> http::Response { + http::Response::builder().status(status).body(body.into()).unwrap() + } + #[test] + fn models_preserve_wire_names_nullability_and_discriminators() { + let value: models::Record = serde_json::from_value(serde_json::json!({"type":"podcast", "nullable":null, "self":"me"})).unwrap(); + assert_eq!(value.r#type, "podcast"); + assert!(value.nullable.is_none()); + assert_eq!(value.self_value, "me"); + assert!(value.optional.is_none()); + let _: models::String = "string alias".into(); + assert!(serde_json::from_value::(serde_json::json!({"status":"completed","thing":{"id":"a","name":"b"}})).is_ok()); + assert!(serde_json::from_value::(serde_json::json!({"status":"unknown","message":"no"})).is_err()); + } +}