{"id":21659598,"url":"https://github.com/oxidecomputer/progenitor","last_synced_at":"2025-05-13T20:14:40.448Z","repository":{"id":37966310,"uuid":"378353157","full_name":"oxidecomputer/progenitor","owner":"oxidecomputer","description":"An OpenAPI client generator","archived":false,"fork":false,"pushed_at":"2025-05-05T16:41:45.000Z","size":6448,"stargazers_count":646,"open_issues_count":87,"forks_count":83,"subscribers_count":28,"default_branch":"main","last_synced_at":"2025-05-05T17:58:15.390Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/oxidecomputer.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.adoc","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2021-06-19T07:37:00.000Z","updated_at":"2025-05-05T17:17:46.000Z","dependencies_parsed_at":"2024-02-04T20:51:14.104Z","dependency_job_id":"1a768bf2-d0aa-45a7-93e6-6846cc064b1f","html_url":"https://github.com/oxidecomputer/progenitor","commit_stats":{"total_commits":389,"total_committers":16,"mean_commits":24.3125,"dds":"0.40616966580976865","last_synced_commit":"7da8db8544fbb59c91dacb807fe5a7494471181a"},"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oxidecomputer%2Fprogenitor","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oxidecomputer%2Fprogenitor/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oxidecomputer%2Fprogenitor/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oxidecomputer%2Fprogenitor/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/oxidecomputer","download_url":"https://codeload.github.com/oxidecomputer/progenitor/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254020633,"owners_count":22000755,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":[],"created_at":"2024-11-25T09:31:17.212Z","updated_at":"2025-05-13T20:14:40.405Z","avatar_url":"https://github.com/oxidecomputer.png","language":"Rust","funding_links":[],"categories":["Rust"],"sub_categories":[],"readme":"# Progenitor\n\nProgenitor is a Rust crate for generating opinionated clients from API\ndescriptions in the OpenAPI 3.0.x specification. It makes use of Rust\nfutures for `async` API calls and `Streams` for paginated interfaces.\n\nIt generates a type called `Client` with methods that correspond to the\noperations specified in the OpenAPI document.\n\nProgenitor can also generate a CLI to interact with an OpenAPI service\ninstance, and [`httpmock`](https://crates.io/crates/httpmock) helpers to\ncreate a strongly typed mock of the OpenAPI service.\n\nThe primary target is OpenAPI documents emitted by\n[Dropshot](https://github.com/oxidecomputer/dropshot)-generated APIs, but it\ncan be used for many OpenAPI documents. As OpenAPI covers a wide range of APIs,\nProgenitor may fail for some OpenAPI documents. If you encounter a problem, you\ncan help the project by filing an issue that includes the OpenAPI document that\nproduced the problem.\n\n## Using Progenitor\n\nThere are three different ways of using the `progenitor` crate. The one you\nchoose will depend on your use case and preferences.\n\n### Macro\n\nThe simplest way to use Progenitor is via its `generate_api!` macro.\n\nIn a source file (often `main.rs`, `lib.rs`, or `mod.rs`) simply invoke the\nmacro:\n\n```rust\ngenerate_api!(\"path/to/openapi_document.json\");\n```\n\nYou'll need to add the following to `Cargo.toml`:\n\n```toml\n[dependencies]\nfutures = \"0.3\"\nprogenitor = { git = \"https://github.com/oxidecomputer/progenitor\" }\nreqwest = { version = \"0.12\", features = [\"json\", \"stream\"] }\nserde = { version = \"1.0\", features = [\"derive\"] }\n```\n\nIn addition, if the OpenAPI document contains string types with the `format`\nfield set to `date` or `date-time`, include\n\n```toml\n[dependencies]\nchrono = { version = \"0.4\", features = [\"serde\"] }\n```\n\nSimilarly, if there is a `format` field set to `uuid`:\n\n```toml\n[dependencies]\nuuid = { version = \"1.0.0\", features = [\"serde\", \"v4\"] }\n```\n\nAnd if there are any websocket channel endpoints:\n\n```toml\n[dependencies]\nbase64 = \"0.21\"\nrand = \"0.8\"\n```\n\nIf types include regular expression validation:\n\n```toml\n[dependencies]\nregress = \"0.4.1\"\n```\n\nThe macro has some additional fancy options to control the generated code:\n\n```rust\ngenerate_api!(\n    spec = \"path/to/openapi_document.json\",      // The OpenAPI document\n    interface = Builder,                         // Choose positional (default) or builder style\n    tags = Separate,                             // Tags may be Merged or Separate (default)\n    inner_type = my_client::InnerType,           // Client inner type available to pre and post hooks\n    pre_hook = closure::or::path::to::function,  // Hook invoked before issuing the HTTP request\n    post_hook = closure::or::path::to::function, // Hook invoked prior to receiving the HTTP response\n    derives = [ schemars::JsonSchema ],          // Additional derive macros applied to generated types\n);\n```\n\nNote that the macro will be re-evaluated when the `spec` OpenAPI document\nchanges (when its mtime is updated).\n\n### `build.rs`\n\nProgenitor includes an interface appropriate for use in a\n[`build.rs`](https://doc.rust-lang.org/cargo/reference/build-scripts.html)\nfile. While slightly more onerous than the macro, a builder has the advantage of making the generated code visible.\nThe capability of generating a CLI and `httpmock` helpers is only available using `build.rs`\nand the `Generator` functions `cli` and `httpmock` respectively.\n\nThe `build.rs` file should look something like this:\n\n```rust\nfn main() {\n    let src = \"../sample_openapi/keeper.json\";\n    println!(\"cargo:rerun-if-changed={}\", src);\n    let file = std::fs::File::open(src).unwrap();\n    let spec = serde_json::from_reader(file).unwrap();\n    let mut generator = progenitor::Generator::default();\n\n    let tokens = generator.generate_tokens(\u0026spec).unwrap();\n    let ast = syn::parse2(tokens).unwrap();\n    let content = prettyplease::unparse(\u0026ast);\n\n    let mut out_file = std::path::Path::new(\u0026std::env::var(\"OUT_DIR\").unwrap()).to_path_buf();\n    out_file.push(\"codegen.rs\");\n\n    std::fs::write(out_file, content).unwrap();\n}\n```\n\nIn a source file (often `main.rs`, `lib.rs`, or `mod.rs`) include the generated\ncode:\n\n```rust\ninclude!(concat!(env!(\"OUT_DIR\"), \"/codegen.rs\"));\n```\n\nYou'll need to add the following to `Cargo.toml`:\n\n```toml\n[dependencies]\nfutures = \"0.3\"\nprogenitor-client = { git = \"https://github.com/oxidecomputer/progenitor\" }\nreqwest = { version = \"0.12\", features = [\"json\", \"stream\"] }\nserde = { version = \"1.0\", features = [\"derive\"] }\nserde_json = \"1.0\"\n\n[build-dependencies]\nprettyplease = \"0.2.22\"\nprogenitor = { git = \"https://github.com/oxidecomputer/progenitor\" }\nserde_json = \"1.0\"\nsyn = \"2.0\"\n```\n\n(`chrono`, `uuid`, `base64`, and `rand` as above)\n\nNote that `progenitor` is used by `build.rs`, but the generated code required\n`progenitor-client`.\n\n### Static Crate\n\nProgenitor can be run to emit a stand-alone crate for the generated client.\nThis ensures no unexpected changes (e.g. from updates to progenitor). It is\nhowever, the most manual way to use Progenitor.\n\nUsage:\n\n```\ncargo progenitor\n\nOptions:\n    -i INPUT            OpenAPI definition document (JSON or YAML)\n    -o OUTPUT           Generated Rust crate directory\n    -n CRATE            Target Rust crate name\n    -v VERSION          Target Rust crate version\n```\n\nFor example:\n\n```\ncargo install cargo-progenitor\ncargo progenitor -i sample_openapi/keeper.json -o keeper -n keeper -v 0.1.0\n```\n\n... or within the repo:\n```\ncargo run --bin cargo-progenitor -- progenitor -i sample_openapi/keeper.json -o keeper -n keeper -v 0.1.0\n```\n\nThis will produce a package in the specified directory.\n\nOptions `--license` and `--registry-name` may also be used to improve metadata\nbefore publishing the static crate.\n\nThe output will use the published `progenitor-client` crate by default\nif progenitor is built in release mode. When built in debug mode, the\n`progenitor-client` will be inlined into the generated crate by default. The\ncommand line flag `--include-client true|false` can be used to override the\ndefault behavior. A value of `true` copies in the client code; a value of\n`false` includes a dependency on `progenitor-client` in the generated\n`Cargo.toml` file.\n\nHere is an excerpt from the emitted `Cargo.toml`:\n\n```toml\n[dependencies]\nbytes = \"1.9\"\nchrono = { version = \"0.4\", default-features=false, features = [\"serde\"] }\nfutures-core = \"0.3\"\nprogenitor-client = \"0.9.1\"\nreqwest = { version = \"0.12\", default-features=false, features = [\"json\", \"stream\"] }\nserde = { version = \"1.0\", features = [\"derive\"] }\nserde_urlencoded = \"0.7\"\n```\n\nHere's another example of dependencies with `--include-client true`:\n\n```toml\n[dependencies]\nbytes = \"1.9\"\nchrono = { version = \"0.4\", default-features=false, features = [\"serde\"] }\nfutures-core = \"0.3\"\npercent-encoding = \"2.3\"\nreqwest = { version = \"0.12\", default-features=false, features = [\"json\", \"stream\"] }\nserde = { version = \"1.0\", features = [\"derive\"] }\nserde_json = \"1.0\"\nserde_urlencoded = \"0.7\"\n```\n\n## Generation Styles\n\nProgenitor can generate two distinct interface styles: positional and builder\n(described below). The choice is simply a matter of preference that many vary\nby API and taste.\n\n### Positional (current default)\n\nThe \"positional\" style generates `Client` methods that accept parameters in\norder, for example:\n\n```rust\nimpl Client {\n    pub async fn instance_create\u003c'a\u003e(\n        \u0026'a self,\n        organization_name: \u0026'a types::Name,\n        project_name: \u0026'a types::Name,\n        body: \u0026'a types::InstanceCreate,\n    ) -\u003e Result\u003cResponseValue\u003ctypes::Instance\u003e, Error\u003ctypes::Error\u003e\u003e {\n        // ...\n    }\n}\n```\n\nA caller invokes this interface by specifying parameters by position:\n\n```rust\nlet result = client.instance_create(org, proj, body).await?;\n```\n\nNote that the type of each parameter must match precisely--no conversion is\ndone implicitly.\n\n### Builder\n\nThe \"builder\" style generates `Client` methods that produce a builder struct.\nAPI parameters are applied to that builder, and then the builder is executed\n(via a `send` method). The code is more extensive and more complex to enable\nsimpler and more legible consumers:\n\n```rust\nimpl Client\n    pub fn instance_create(\u0026self) -\u003e builder::InstanceCreate {\n        builder::InstanceCreate::new(self)\n    }\n}\n\nmod builder {\n    pub struct InstanceCreate\u003c'a\u003e {\n        client: \u0026'a super::Client,\n        organization_name: Result\u003ctypes::Name, String\u003e,\n        project_name: Result\u003ctypes::Name, String\u003e,\n        body: Result\u003ctypes::InstanceCreate, String\u003e,\n    }\n\n    impl\u003c'a\u003e InstanceCreate\u003c'a\u003e {\n        pub fn new(client: \u0026'a super::Client) -\u003e Self {\n            // ...\n        }\n\n        pub fn organization_name\u003cV\u003e(mut self, value: V) -\u003e Self\n        where\n            V: TryInto\u003ctypes::Name\u003e,\n        {\n            // ...\n        }\n\n        pub fn project_name\u003cV\u003e(mut self, value: V) -\u003e Self\n        where\n            V: TryInto\u003ctypes::Name\u003e,\n        {\n            // ...\n        }\n\n        pub fn body\u003cV\u003e(mut self, value: V) -\u003e Self\n        where\n            V: TryInto\u003ctypes::InstanceCreate\u003e,\n        {\n            // ...\n        }\n\n        pub async fn send(self) -\u003e\n            Result\u003cResponseValue\u003ctypes::Instance\u003e, Error\u003ctypes::Error\u003e\u003e\n        {\n            // ...\n        }\n    }\n}\n```\n\nNote that, unlike positional generation, consumers can supply compatible\n(rather than invariant) parameters:\n\n```rust\nlet result = client\n    .instance_create()\n    .organization_name(\"org\")\n    .project_name(\"proj\")\n    .body(body)\n    .send()\n    .await?;\n```\n\nThe string parameters will implicitly have `TryFrom::try_from()` invoked on\nthem. Failed conversions or missing required parameters will result in an\n`Error` result from the `send()` call.\n\nGenerated `struct` types also have builders so that the `body` parameter can be\nconstructed inline:\n\n```rust\nlet result = client\n    .instance_create()\n    .organization_name(\"org\")\n    .project_name(\"proj\")\n    .body(types::InstanceCreate::builder()\n        .name(\"...\")\n        .description(\"...\")\n        .hostname(\"...\")\n        .ncpus(types::InstanceCpuCount(4))\n        .memory(types::ByteCount(1024 * 1024 * 1024)),\n    )\n    .send()\n    .await?;\n```\n\nConsumers do not need to specify parameters and struct properties that are not\nrequired or for which the API specifies defaults. Neat!\n\n#### Enabling the builder style in build.rs\n\nTo enable the builder style, the `build.rs` file should look something like this:\n\n```rust\nfn main() {\n    let src = \"../sample_openapi/keeper.json\";\n    println!(\"cargo:rerun-if-changed={}\", src);\n    let file = std::fs::File::open(src).unwrap();\n    let spec = serde_json::from_reader(file).unwrap();\n    let mut binding = GenerationSettings::default();\n    let settings = binding.with_interface(InterfaceStyle::Builder);\n    let mut generator = progenitor::Generator::new(\u0026settings);\n    let tokens = generator.generate_tokens(\u0026spec).unwrap();\n    let ast = syn::parse2(tokens).unwrap();\n    let content = prettyplease::unparse(\u0026ast);\n\n    let mut out_file = std::path::Path::new(\u0026std::env::var(\"OUT_DIR\").unwrap()).to_path_buf();\n    out_file.push(\"codegen.rs\");\n\n    std::fs::write(out_file, content).unwrap();\n}\n```\n\n## Changing default client settings\n\nCurrently, the generated code doesn't deal with request headers. To add default headers to all requests, you can use the default_headers method when constructing the Client.\n\n```rust\n    let baseurl = std::env::var(\"API_URL\").expect(\"$API_URL not set\");\n    \n    let access_token = std::env::var(\"API_ACCESS_TOKEN\").expect(\"$API_ACCESS_TOKEN not set);\n    let authorization_header = format!(\"Bearer {}\", access_token);\n\n    let mut headers = reqwest::header::HeaderMap::new();\n    headers.insert(\n        reqwest::header::AUTHORIZATION,\n        authorization_header.parse().unwrap(),\n    );\n\n    let client_with_custom_defaults = reqwest::ClientBuilder::new()\n        .connect_timeout(Duration::from_secs(15))\n        .timeout(Duration::from_secs(15))\n        .default_headers(headers)\n        .build()\n        .unwrap();\n\n    let client = Client::new_with_client(baseurl, client_with_custom_defaults);\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foxidecomputer%2Fprogenitor","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Foxidecomputer%2Fprogenitor","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foxidecomputer%2Fprogenitor/lists"}