From vovk
Generates typed Rust crates from Vovk APIs using vovk-rust: async reqwest/serde calls, JSON Lines futures streams, client validation, crates.io publishing. For Rust SDKs.
npx claudepluginhub finom/vovkThis skill uses the workspace's default tool permissions.
`vovk-rust` generates typed Rust crate from same `.vovk-schema/` artifacts driving TS client. Async by default via `reqwest` + `tokio`; streaming via `futures`.
Generates typed Python SDKs from Vovk APIs using vovk-python, with TypedDict shapes, JSON Lines streaming, client validation, and PyPI publishing workflows.
Provides expert guidance on Rust 1.75+ for building services, libraries, systems tooling with async patterns (Tokio/axum), advanced types, ownership, lifetimes, and performance optimization.
Provides expertise in Rust 1.75+ for async programming with Tokio and axum, advanced type system, ownership management, and performance-optimized systems programming.
Share bugs, ideas, or general feedback.
vovk-rust generates typed Rust crate from same .vovk-schema/ artifacts driving TS client. Async by default via reqwest + tokio; streaming via futures.
Experimental — generated API may shift between Vovk versions. Pin crate version on consume.
Covers:
vovk-rust + generate Rust crate.rs (standalone crate) vs rsSrc (source files to embed).vovk.config.mjs._:: type convention.futures::Stream.Out of scope:
procedure skill.vovk bundle general (TS, Python) → bundle skill.jsonlines skill.npm i -D vovk-rust
Canonical output dir ./dist_rust (hello-world convention):
npx vovk generate --from rs --out ./dist_rust
Output:
dist_rust/
src/http_request.rs # HTTP handling
src/lib.rs # RPC functions + types
src/read_full_schema.rs # Schema utilities
src/schema.json # Vovk schema (read at runtime via CARGO_MANIFEST_DIR/src/schema.json)
Cargo.toml # edition = "2021"
README.md
npx vovk generate --from rsSrc --out ./rust_src
// vovk.config.mjs
const config = {
composedClient: {
fromTemplates: ['js', 'rs'],
},
};
export default config;
Bake prod API URL via clientTemplateDefs.rs.outputConfig.origin — generated crate uses this by default. Pattern from hello-world:
// vovk.config.js
const PROD_ORIGIN = 'https://hello-world.vovk.dev';
const config = {
composedClient: { fromTemplates: ['js', 'rs'] },
clientTemplateDefs: {
rs: {
extends: 'rs',
outputConfig: { origin: PROD_ORIGIN },
// composedClient: { outDir: './my_other_dir' }, // optional — defaults to ./dist_rust
},
},
};
Consumers can still override per call via api_root arg.
Every RPC function async, takes positional args: body, query, params, headers?, api_root?, disable_client_validation.
pub async fn update_user(
body: update_user_::body,
query: update_user_::query,
params: update_user_::params,
headers: Option<&HashMap<String, String>>,
api_root: Option<&str>,
disable_client_validation: bool,
) -> Result<update_user_::output, HttpException>
Per-endpoint type variation (verified in packages/vovk-rust/client-templates/rsSrc/lib.rs.ejs:60-67): each of query, params typed <handler_name>_::query / _::params only when procedure declares validation for that key; else slot is Rust unit type () → pass () at call site. body has three cases: multipart/form-data content-type → reqwest::multipart::Form; declared body validation (JSON / text / binary) → <handler>_::body; no body → (). So endpoint with no body/query/params signature:
pub async fn ping(
body: (), query: (), params: (),
headers: Option<&HashMap<String, String>>,
api_root: Option<&str>,
disable_client_validation: bool,
) -> Result<serde_json::Value, HttpException>
Method names lodash snakeCase(handlerName) — getUser → get_user, findPetsByStatus → find_pets_by_status, UserRPC (module) → user_rpc.
Nested types use _:: module syntax. body.profile typed update_user_::body_::profile. How generator flattens deep JSON Schema into Rust modules.
use my_api_client::user_rpc;
pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
use user_rpc::update_user_::{
body as Body,
body_::profile as Profile,
query as Query,
query_::notify as Notify,
params as Params,
};
let response = user_rpc::update_user(
Body {
email: String::from("john@example.com"),
profile: Profile {
name: String::from("John Doe"),
age: 25,
},
},
Query { notify: Notify::email },
Params {
id: String::from("123e4567-e89b-12d3-a456-426614174000"),
},
None, // headers
None, // api_root — overrides baked-in URL
false, // disable_client_validation
)
.await?;
Ok(())
}
Method names Rust-side snake_case; user_rpc module reflects UserRPC controller. Module path (my_api_client above) depends on crate name set at generation time.
Streaming endpoints return pinned boxed Stream:
pub async fn stream_tokens(
body: (), query: (), params: (),
headers: Option<&HashMap<String, String>>,
api_root: Option<&str>,
disable_client_validation: bool,
) -> Result<
Pin<Box<dyn Stream<Item = Result<stream_tokens_::iteration, HttpException>> + Send>>,
HttpException,
>
Consume with futures::StreamExt:
use futures::StreamExt;
use my_api_client::stream_rpc;
pub async fn consume_stream() -> Result<(), Box<dyn std::error::Error>> {
let mut stream = stream_rpc::stream_tokens((), (), (), None, None, false).await?;
while let Some(item) = stream.next().await {
let val = item.expect("stream item should be Ok");
println!("{}", val.message);
}
Ok(())
}
See jsonlines skill for server side.
Generated Cargo.toml brings (per hello-world):
[dependencies]
serde_json = "1.0"
futures-util = "0.3"
jsonschema = "0.17"
urlencoding = "2.1"
once_cell = "1.17"
[dependencies.serde]
version = "1.0"
features = ["derive"]
[dependencies.reqwest]
version = "0.12"
features = ["json", "multipart", "stream"]
[dependencies.tokio]
version = "1"
features = ["macros", "rt-multi-thread", "io-util"]
[dependencies.tokio-util]
version = "0.7"
features = ["codec"]
reqwest 0.12 — async HTTP, with multipart and stream features for file uploads + JSON Lines.tokio 1 — async runtime. Generated crate assumes multi-thread runtime available; consumers pull transitively but typically add directly with #[tokio::main] for binaries.tokio-util (codec) — line-delimited framing for JSON Lines decoding.serde (derive) + serde_json — (de)serialization.futures-util — streaming combinators (StreamExt::next etc).jsonschema 0.17 — client-side validation against schema.json.urlencoding + once_cell — internal utilities.Pass api_root per call to override baked-in URL. Pass &HashMap<String, String> of headers for auth (Bearer token, API key, whatever upstream needs):
let mut headers = HashMap::new();
headers.insert("Authorization".into(), format!("Bearer {token}"));
let resp = user_rpc::get_user(
(), (), Params { id },
Some(&headers),
Some("https://api.example.com"),
false,
).await?;
Don't bake secrets into crate. Consumers supply auth at call time.
Runs by default. Last positional arg disables:
user_rpc::update_user(body, query, params, None, None, true /* disable */).await?;
Skip for hot paths; keep on for safety in app code.
Canonical script from hello-world — note --allow-dirty, needed since generated crate is uncommitted build artifact:
cargo publish --manifest-path dist_rust/Cargo.toml --allow-dirty
Wire into release flow alongside npm bundle + Python package (hello-world chains all three under postversion):
"scripts": {
"publish:node": "npm publish ./dist",
"publish:rust": "cargo publish --manifest-path dist_rust/Cargo.toml --allow-dirty",
"publish:python": "python3 -m build ./dist_python --wheel --sdist && python3 -m twine upload ./dist_python/dist/*",
"postversion": "vovk generate && vovk bundle && npm run publish:node && npm run publish:rust && npm run publish:python"
}
Crate name / version flow from root package.json (generator copies fields into generated Cargo.toml). Hello-world example crate vovk_hello_world (underscored, since hyphens crates.io-discouraged).
npm i -D vovk-rust.'rs' to composedClient.fromTemplates.npx vovk generate.Cargo.toml.use my_api::user_rpc; and call.npx vovk generate --from rs --out ./rust_package.info.title + info.version in vovk.config.mjs first.cargo publish --manifest-path rust_package/Cargo.toml.use futures::StreamExt;
let mut stream = chat_rpc::stream_tokens(body, (), (), None, None, false).await?;
while let Some(chunk) = stream.next().await {
print!("{}", chunk?.message);
}
Use rsSrc:
npx vovk generate --from rsSrc --out ./src/api
Add module to src/lib.rs: pub mod api;.
body_::profile_::address_::zip patterns common. use ... as Name; import trick keeps call sites readable.$refs not fully supported yet — if mixin has cycles, generator may fail. Roadmap.text/plain and application/octet-stream body params not fully supported — JSON is solid path today.components/schemas don't yet produce importable shared types — roadmap. Every call site gets own scoped types.futures::StreamExt — forget import → .next() won't compile.