Rust RPC Client (Experimental)
The Rust client can be generated with vovk generate using the rs or rsSrc template.
Generate a Rust package with the CLI:
npx vovk generate --from rs --out ./rust_packageThis produces the following structure:
- http_request.rs
- lib.rs
- read_full_schema.rs
- schema.json
- Cargo.toml
- README.md
Publish to crates.io with:
cargo publish --manifest-path rust_package/Cargo.tomlIf you prefer generating source files to embed in another Rust project, use the rsSrc template:
npx vovk generate --from rsSrc --out ./rust_srcThis generates:
- http_request.rs
- lib.rs
- read_full_schema.rs
- schema.json
Configuring the Rust Client
You can configure generation so the client is produced automatically by the default generate command (no flags) and during vovk dev, which performs “hot generation” whenever the schema changes. Add the rs template to the composed client config:
/** @type {import('vovk').VovkConfig} */
const config = {
composedClient: {
fromTemplates: ['cjs', 'mjs', 'rs'], // keep the default "cjs" and "mjs" templates
},
};
export default config;The rs template (and others) has a default outDir (./dist_rust) for composed clients. Override it via template definitions:
/** @type {import('vovk').VovkConfig} */
const config = {
// ...
clientTemplateDefs: {
rs: {
extends: 'rs', // extends the built-in "rs" template
composedClient: {
outDir: './my_dist_rust', // custom output directory for the composed client
},
},
},
};
export default config;Generated Rust Client Example
JSON Endpoints
The snippets below are adapted from a real example described on the Hello World page.
A controller like this:
…emits a Vovk.ts schema, which is then used to generate the Rust client following Rust conventions, adding comments from the schema description, and choosing suitable types. For example, age is generated as u8 due to min/max constraints.
mod http_request;
mod read_full_schema;
pub use crate::http_request::HttpException;
pub mod user_rpc {
#[allow(unused_imports)]
use crate::http_request::{HttpException, http_request, http_request_stream};
use std::collections::HashMap;
// UserRPC.update_user POST `http://localhost:3000/api/users/{id}`
pub mod update_user_ {
use serde::{Serialize, Deserialize};
/// User data object
#[derive(Debug, Serialize, Deserialize, Clone)]
#[allow(non_snake_case, non_camel_case_types)]
pub struct body {
/// User email
pub email: String,
/// User profile object
pub profile: body_::profile,
}
#[allow(non_snake_case)]
pub mod body_ {
use serde::{Serialize, Deserialize};
/// User profile object
#[derive(Debug, Serialize, Deserialize, Clone)]
#[allow(non_snake_case, non_camel_case_types)]
pub struct profile {
/// User full name
pub name: String,
/// User age
pub age: u8,
}
}
/// Query parameters
#[derive(Debug, Serialize, Deserialize, Clone)]
#[allow(non_snake_case, non_camel_case_types)]
pub struct query {
/// Notification type
pub notify: query_::notify,
}
#[allow(non_snake_case)]
pub mod query_ {
use serde::{Serialize, Deserialize};
/// Notification type
#[derive(Debug, Serialize, Deserialize, Clone)]
#[allow(non_camel_case_types)]
pub enum notify {
#[serde(rename = "email")]
email,
#[serde(rename = "push")]
push,
#[serde(rename = "none")]
none,
}
}
/// Path parameters
#[derive(Debug, Serialize, Deserialize, Clone)]
#[allow(non_snake_case, non_camel_case_types)]
pub struct params {
/// User ID
pub id: String,
}
/// Response object
#[derive(Debug, Serialize, Deserialize, Clone)]
#[allow(non_snake_case, non_camel_case_types)]
pub struct output {
/// Success status
pub success: bool,
}
}
/// Summary: Update user
/// Description: Update user by ID
/// Params: Path parameters
/// Body: User data object
/// Query: Query parameters
/// Returns: Response object
pub 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>{
let result = http_request::<
update_user_::output,
update_user_::body,
update_user_::query,
update_user_::params
>(
"http://localhost:3000/api",
"",
"UserRPC",
"updateUser",
Some(&body),
None,
Some(&query),
Some(¶ms),
headers,
api_root,
disable_client_validation,
);
result
}
}All RPC modules are generated in lib.rs, which contains RPC functions and the associated types. Nested structures are emitted as nested modules with corresponding struct definitions or types.
Access nested structures via _::. For example, body.profile is update_user_::body_::profile.
Use use to import and rename structs to follow PascalCase:
use std::io::Write;
use vovk_hello_world::user_rpc;
pub fn main() {
use user_rpc::update_user_::{
body as Body,
body_::profile as Profile,
query as Query,
query_::notify as Notify,
params as Params,
};
let update_user_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 (hashmap)
None, // API root
false, // Disable client validation
);
match update_user_response {
Ok(response) => println!("user_rpc.update_user: {:?}", response),
Err(e) => println!("update_user error: {:?}", e),
}
}Under the hood, it uses reqwest for HTTP, jsonschema for client-side validation, and other common crates.
JSON Lines Endpoints
For continuous streaming with JSON Lines endpoints, the client implements the Iterator trait to return an async-capable iterator for streamed data.
A controller like this:
Compiles to:
pub mod stream_rpc {
#[allow(unused_imports)]
use crate::http_request::{HttpException, http_request, http_request_stream};
use std::collections::HashMap;
// StreamRPC.stream_tokens GET `http://localhost:3000/api/streams/tokens`
pub mod stream_tokens_ {
use serde::{Serialize, Deserialize};
/// Streamed token object
#[derive(Debug, Serialize, Deserialize, Clone)]
#[allow(non_snake_case, non_camel_case_types)]
pub struct iteration {
/// Message from the token
pub message: String,
}
}
/// Summary: Stream tokens
/// Description: Stream tokens to the client
pub fn stream_tokens(
body: (),
query: (),
params: (),
headers: Option<&HashMap<String, String>>,
api_root: Option<&str>,
disable_client_validation: bool,
) -> Result<Box<dyn Iterator<Item = stream_tokens_::iteration>>, HttpException>{
let result = http_request_stream::<
stream_tokens_::iteration,
(),
(),
()
>(
"http://localhost:3000/api",
"",
"StreamRPC",
"streamTokens",
Some(&body),
None,
Some(&query),
Some(¶ms),
headers,
api_root,
disable_client_validation,
);
result
}
}Usage:
use std::io::Write;
use vovk_hello_world_local::open_api_rpc::stream_rpc;
pub fn main() {
let stream_response = stream_rpc::stream_tokens(
(), // no body, query, or params
(),
(),
None,
None,
false,
);
match stream_response {
Ok(stream) => {
print!("streamTokens:\n");
for (_i, item) in stream.enumerate() {
print!("{}", item.message.as_str());
std::io::stdout().flush().unwrap();
}
println!();
},
Err(e) => println!("Error initiating stream: {:?}", e),
}
}Roadmap
- 🧩 Use async (.await) instead of blocking calls.
- 🐞 Allow circular $refs with OpenAPI Mixins.
- ✨ Generate importable types for named schemas defined in
components/schemas.