{"id":50712109,"url":"https://github.com/zeon256/satay-rs","last_synced_at":"2026-07-05T01:01:12.670Z","repository":{"id":362416773,"uuid":"1238808338","full_name":"zeon256/satay-rs","owner":"zeon256","description":"Sans-IO Rust OpenAPI client generator with typed builders, validation newtypes, and transport-agnostic adapters","archived":false,"fork":false,"pushed_at":"2026-07-04T19:02:26.000Z","size":2039,"stargazers_count":4,"open_issues_count":11,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-04T19:04:02.641Z","etag":null,"topics":["api-client","codegen","http","newtype","openapi","openapi-client","rust","sans-io","validation"],"latest_commit_sha":null,"homepage":"https://satay.inve.rs","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zeon256.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE-APACHE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":"docs/support.md","governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-14T13:26:02.000Z","updated_at":"2026-07-04T19:00:57.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/zeon256/satay-rs","commit_stats":null,"previous_names":["zeon256/satay-rs"],"tags_count":35,"template":false,"template_full_name":null,"purl":"pkg:github/zeon256/satay-rs","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zeon256%2Fsatay-rs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zeon256%2Fsatay-rs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zeon256%2Fsatay-rs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zeon256%2Fsatay-rs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zeon256","download_url":"https://codeload.github.com/zeon256/satay-rs/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zeon256%2Fsatay-rs/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35140189,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-04T02:00:05.987Z","response_time":113,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api-client","codegen","http","newtype","openapi","openapi-client","rust","sans-io","validation"],"created_at":"2026-06-09T16:04:07.291Z","updated_at":"2026-07-05T01:01:12.662Z","avatar_url":"https://github.com/zeon256.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ctable\u003e\n  \u003ctr\u003e\n    \u003ctd width=\"190\" align=\"center\"\u003e\n      \u003cimg src=\"logo.png\" alt=\"satay-rs logo\" width=\"160\"\u003e\n    \u003c/td\u003e\n    \u003ctd\u003e\n      \u003ch1\u003eSatay\u003c/h1\u003e\n      \u003cp\u003e\n        \u003ca href=\"https://crates.io/crates/satay-cli\"\u003e\u003cimg src=\"https://img.shields.io/crates/d/satay-cli\" alt=\"Crates.io Downloads\"\u003e\u003c/a\u003e\n        \u003ca href=\"https://docs.rs/satay-codegen\"\u003e\u003cimg src=\"https://img.shields.io/docsrs/satay-codegen\" alt=\"Docs.rs\"\u003e\u003c/a\u003e\n        \u003ca href=\"#license\"\u003e\u003cimg src=\"https://img.shields.io/badge/license-Apache--2.0%2FMIT-blue\" alt=\"License\"\u003e\u003c/a\u003e\n        \u003ca href=\"https://blog.rust-lang.org/2025/02/20/Rust-1.88.0/\"\u003e\u003cimg src=\"https://img.shields.io/badge/MSRV-1.88.0-orange\" alt=\"MSRV\"\u003e\u003c/a\u003e\n        \u003ca href=\"https://doc.rust-lang.org/edition-guide/rust-2024/\"\u003e\u003cimg src=\"https://img.shields.io/badge/Rust-2024-blue\" alt=\"Rust Edition\"\u003e\u003c/a\u003e\n      \u003c/p\u003e\n      \u003cp\u003e\n        satay is a Rust OpenAPI client generator for \u003ca href=\"https://fasterthanli.me/articles/the-case-for-sans-io\"\u003esans-io\u003c/a\u003e clients. It generates typed request builders, response decoders, and validation \u003ca href=\"https://rust-unofficial.github.io/patterns/patterns/behavioural/newtype.html\"\u003enewtypes\u003c/a\u003e from OpenAPI constraints while leaving HTTP, WebSocket, tests, WASM, and other transports under your application's control.\n      \u003c/p\u003e\n      \u003cp\u003e\n        Named after \u003cstrong\u003esate\u003c/strong\u003e /ˈsɑː.teɪ/, the Malay and Indonesian word for meat threaded on a skewer and grilled.\n      \u003c/p\u003e\n    \u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n\u003e [!WARNING]\n\u003e Satay is work in progress and currently prioritizes generating `lta-rs` clients with good ergonomics and validation. Support for other OpenAPI specs will grow as the supported subset becomes explicit and tested.\n\n## Motivation\n\nI've been bitten by the sans-io bug after I used [rusty-s3](https://github.com/paolobarbolini/rusty-s3) and got so inspired by it that I told myself that all SDK clients that I am gonna make will be sans-io. After maintaining [lta-rs](httpps://github.com/lta-rs/lta-rs) with multiple different implementation for blocking, async, different client backends, sans-io approach is the most reasonable one. This will significantly reduce maintainence time as well as bugs cos we no longer have to depend on those backends! Another reason why I started this project is because there isn't any OpenAPI generator for Rust that generates adequately high quality Rust code that is nice to use from a DX perspective and I want to change that.\n\n## Features\n\n- Generates from OpenAPI 3.1 documents\n- Sans-IO design from the ground up, with optional transport adapters for `reqwest` and `ureq`\n- Validation newtypes for OpenAPI string, number, integer, and array constraints\n- Automatic number type deduction from specified bounds (i.e. if `maximum` is less than `u8::MAX`, the generated type will be a `u8` newtype instead of `u64`)\n\n## Quick Start\n\nInstall the CLI from crates.io:\n\n```bash\ncargo install satay-cli\nsatay --help\n```\n\nGenerate Rust code from an OpenAPI document:\n\n```bash\nsatay generate --input openapi.yaml --output src/generated --rustfmt\n```\n\nBy default Satay writes `mod.rs` at the root of the output directory. Use `--lib` to emit `lib.rs` instead when generating a standalone crate root.\n\nThen use the generated action API with whichever transport you want. With the `satay-reqwest` adapter, the call site can stay compact:\n\n```rust\ninclude!(concat!(env!(\"OUT_DIR\"), \"/satay_generated.rs\"));\n\nuse generated::{Api, GetBusArrivalResponse};\nuse satay_reqwest::{ReqwestActionExt, reqwest};\nuse std::{env, error::Error};\n\nuse crate::generated::BusServiceNumber;\n\n#[tokio::main]\nasync fn main() -\u003e Result\u003c(), Box\u003cdyn Error\u003e\u003e {\n    let api = Api::new().account_key(env::var(\"LTA_ACCOUNT_KEY\")?);\n\n    let client = reqwest::Client::new();\n    let response = api\n        .get_bus_arrival(83139)\n        .service_no(BusServiceNumber::try_new(\"15\")?)\n        .send_with(\u0026client)\n        .await?;\n\n    match response {\n        GetBusArrivalResponse::Ok(arrival) =\u003e {\n            println!(\"{:?}\", arrival);\n        }\n        GetBusArrivalResponse::UnexpectedStatus(status, body) =\u003e {\n            eprintln!(\n                \"unexpected status {status}: {}\",\n                String::from_utf8_lossy(\u0026body)\n            );\n        }\n    }\n\n    Ok(())\n}\n```\n\nThe adapter is optional. Generated actions expose the same IO-free boundary directly:\n\n```rust\nlet request: http::Request\u003cVec\u003cu8\u003e\u003e = action.request()?;\n\n// Send `request` with reqwest, ureq, hyper, tests, WASM, WebSocket, or your own transport.\n\nlet response = satay_runtime::ResponseParts { status, headers, body };\nlet decoded = generated::GetBusArrivalAction::decode(response)?;\n```\n \n## What It Generates\n\n- Rust structs, flattened `allOf` object structs, supported `anyOf`/`oneOf` unions, supported discriminator unions, string enums, primitive aliases, constrained newtypes from `components.schemas`, and named inline structs for supported nested JSON `allOf` object schemas.\n- Operation input builders with required constructor arguments and chainable optional setters.\n- Request builders that return `http::Request\u003cVec\u003cu8\u003e\u003e` without choosing a transport.\n- Response decoders for known JSON responses, preserving unknown statuses as `UnexpectedStatus(http::StatusCode, Vec\u003cu8\u003e)`.\n- Optional `Api` action builders for base URL, API-key auth, request construction, and response decoding.\n- `serde` derives and field renames behind a generated crate `serde` feature.\n- `nutype` validation for OpenAPI string, number, integer, and array bounds.\n- Satay extensions under `x-satay` for typed string parsing, integer primitive overrides, enum variant names, and lossy optional fields.\n\nGenerated code that represents OpenAPI validation constraints uses `nutype` newtypes. Add these dependencies to crates that compile constrained generated clients:\n\n```toml\nnutype = { version = \"0.7\", features = [\"serde\"] }\n```\n\nWhen the OpenAPI spec contains string `pattern` constraints, also add:\n\n```toml\nnutype = { version = \"0.7\", features = [\"serde\", \"regex\"] }\nregex = \"1\"\n```\n\nOpenAPI `pattern` uses ECMA-262 regex syntax, while `nutype` uses Rust's `regex` crate. Common patterns are usually compatible, but JavaScript-only features such as lookahead, lookbehind, and backreferences will not compile in generated Rust code.\n\n## Docs\n\n- [Supported OpenAPI subset](docs/support.md)\n- [Generated validation newtypes](docs/validation.md)\n- [Satay extensions](docs/extensions.md)\n- [Action builders and transport adapters](docs/transports.md)\n\n## Examples\n\n- [examples/reqwest](examples/reqwest): generates from `examples/openapi.yaml` at build time, sends the request with `reqwest`, and decodes with the generated action API.\n- [examples/reqwest-blocking](examples/reqwest-blocking): uses the same generated action API with `reqwest::blocking`.\n- [examples/reqwest-manual](examples/reqwest-manual): sends with `reqwest` directly without using `satay-reqwest`.\n- [examples/tungstenite-ws](examples/tungstenite-ws): sends generated actions over a local WebSocket using `tokio-tungstenite`.\n- [examples/ureq](examples/ureq): sends generated actions with `ureq`.\n\n## Workspace\n\n- [crates/satay-cli](crates/satay-cli): user-facing `satay` executable.\n- [crates/satay-codegen](crates/satay-codegen): OpenAPI parser, normalized IR, and Rust generator.\n- [crates/satay-runtime](crates/satay-runtime): small IO-free support crate for generated code.\n- [crates/satay-reqwest](crates/satay-reqwest): adapter traits for sending generated actions with `reqwest`.\n- [crates/satay-ureq](crates/satay-ureq): adapter traits for sending generated actions with `ureq`.\n\n## Security\n\nPlease see [SECURITY.md](SECURITY.md) for the security policy, reporting guidelines, and hardening techniques used in this project.\n\n## License\n\nLicensed under either of:\n\n- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE))\n- MIT license ([LICENSE-MIT](LICENSE-MIT))\n\nat your option.\n\n\n## Contribution\n\nUnless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzeon256%2Fsatay-rs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzeon256%2Fsatay-rs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzeon256%2Fsatay-rs/lists"}