skir-rust-gen
v0.1.2
Published
[](https://www.npmjs.com/package/skir-rust-gen) [](https://github.com/gepheum/skir-rust-gen/actions)
Readme
Skir's Rust code generator
Official plugin for generating Rust code from .skir files.
Targets Rust edition 2021 and higher.
Set up
In your skir.yml file, add the following snippet under generators:
- mod: skir-rust-gen
outDir: ./src/skirout
config: {}The generated Rust code has a runtime dependency on the skir-client crate. Add it to your Cargo.toml with:
skir-client = "0.1"In your src/lib.rs (or src/main.rs), expose the generated module:
pub mod skirout;Then create src/skirout.rs with the following content (the generator will populate the base subdirectory):
// The skirout module is generated by `npx skir gen`. Do not edit by hand.
pub mod base;For more information, see this Rust project example.
Rust generated code guide
The examples below are for the code generated from this .skir file.
Referring to generated symbols
// Import generated types from the Rust module generated from "user.skir".
use crate::skirout::base::user::{
tarzan_const, SubscriptionStatus, SubscriptionStatus_Trial, User, UserRegistry, User_Pet,
};
// Now you can use: User, SubscriptionStatus, tarzan_const(), etc.Struct types
Skir generates a plain Rust struct for every struct in the .skir file. All
fields are public. Every struct implements Default, Clone, PartialEq,
and Debug.
// Construct a User by filling in each field directly.
let john = User {
user_id: 42,
name: "John Doe".to_string(),
quote: "Coffee is just a socially acceptable form of rage.".to_string(),
pets: vec![User_Pet {
name: "Dumbo".to_string(),
height_in_meters: 1.0,
picture: "🐘".to_string(),
_unrecognized: None,
}],
subscription_status: SubscriptionStatus::Free,
_unrecognized: None, // Present in every struct; always set to None
};
println!("{}", john.name); // John Doe
// User::default() returns a User with every field set to its default value.
println!("{}", User::default().name); // (empty string)
println!("{}", User::default().user_id); // 0
// Struct update syntax: specify some fields and use defaults for the rest.
let jane = User {
user_id: 43,
name: "Jane Doe".to_string(),
..User::default()
};
println!("{}", jane.quote); // (empty string)
println!("{}", jane.pets.len()); // 0Creating modified copies
// Option 1: clone, then mutate.
let mut evil_john = john.clone();
evil_john.name = "Evil John".to_string();
evil_john.quote = "I solemnly swear I am up to no good.".to_string();
println!("{}", evil_john.name); // Evil John
println!("{}", evil_john.user_id); // 42 (copied from john)
println!("{}", john.name); // John Doe (john is unchanged)
// Option 2: use struct update syntax to copy fields from an existing instance.
let evil_john_2 = User {
name: "Evil John".to_string(),
quote: "I solemnly swear I am up to no good.".to_string(),
..john.clone()
};
println!("{}", evil_john_2.name); // Evil John
println!("{}", evil_john_2.user_id); // 42 (copied from john)
println!("{}", john.name); // John Doe (john is unchanged)Enum types
Skir generates a Rust enum for every enum in the .skir file. The Unknown
variant is added automatically and is the default.
The definition of the SubscriptionStatus enum in the .skir file is:
enum SubscriptionStatus {
FREE;
trial: Trial;
PREMIUM;
}Making enum values
let _ = [
// Unknown is the default and is present in all Skir enums.
SubscriptionStatus::Unknown(None),
SubscriptionStatus::default(), // Same as ::Unknown(None)
SubscriptionStatus::Free,
SubscriptionStatus::Premium,
// Wrapper variants carry a value inside a Box.
SubscriptionStatus::Trial(Box::new(SubscriptionStatus_Trial {
start_time: std::time::SystemTime::now(),
_unrecognized: None,
})),
];Conditions on enums
// Direct match on enum variants:
let get_info_text = |status: &SubscriptionStatus| -> String {
match status {
SubscriptionStatus::Free => "Free user".to_string(),
SubscriptionStatus::Premium => "Premium user".to_string(),
SubscriptionStatus::Trial(t) => {
format!("On trial since {:?}", t.start_time)
}
SubscriptionStatus::Unknown(_) => "Unknown subscription status".to_string(),
}
};
println!("{}", get_info_text(&john.subscription_status)); // Free userSerialization
User::serializer() returns a Serializer<User> which can serialise and
deserialise instances of User.
use skir_client::{JsonFlavor, Serializer, UnrecognizedValues};
let serializer = User::serializer();
// Serialize to dense JSON (field-number-based; the default mode).
// Use this when you plan to deserialize the value later. Because field
// names are not included, renaming a field remains backward-compatible.
let john_dense_json = serializer.to_json(&john, JsonFlavor::Dense);
println!("{}", john_dense_json);
// [42,"John Doe",...]
// Serialize to readable (name-based, indented) JSON.
// Use this mainly for debugging.
println!("{}", serializer.to_json(&john, JsonFlavor::Readable));
// {
// "user_id": 42,
// "name": "John Doe",
// "quote": "Coffee is just a socially acceptable form of rage.",
// "pets": [
// {
// "name": "Dumbo",
// "height_in_meters": 1.0,
// "picture": "🐘"
// }
// ],
// "subscription_status": "FREE"
// }
// The dense JSON flavor is the flavor you should pick if you intend to
// deserialize the value in the future. Skir allows fields to be renamed, and
// because fields names are not part of the dense JSON, renaming a field does
// not prevent you from deserializing the value.
// You should pick the readable flavor mostly for debugging purposes.
// Deserialize from JSON (both dense and readable formats are accepted).
let reserialized_john = serializer
.from_json(&john_dense_json, UnrecognizedValues::Drop)
.unwrap();
assert_eq!(reserialized_john, john);
// Serialize to binary format (more compact than JSON; useful when
// performance matters, though the difference is rarely significant).
let john_bytes = serializer.to_bytes(&john);
let from_bytes = serializer
.from_bytes(&john_bytes, UnrecognizedValues::Drop)
.unwrap();
assert_eq!(from_bytes, john);Primitive serializers
println!("{}", Serializer::bool().to_json(&true, JsonFlavor::Dense));
// 1
println!("{}", Serializer::int32().to_json(&3_i32, JsonFlavor::Dense));
// 3
println!(
"{}",
Serializer::int64().to_json(&9_223_372_036_854_775_807_i64, JsonFlavor::Dense)
);
// "9223372036854775807"
println!("{}", Serializer::float32().to_json(&1.5_f32, JsonFlavor::Dense));
// 1.5
println!("{}", Serializer::float64().to_json(&1.5_f64, JsonFlavor::Dense));
// 1.5
println!(
"{}",
Serializer::string().to_json(&"Foo".to_string(), JsonFlavor::Dense)
);
// "Foo"Composite serializers
// Optional serializer:
println!(
"{}",
Serializer::optional(Serializer::string())
.to_json(&Some("foo".to_string()), JsonFlavor::Dense)
);
// "foo"
println!(
"{}",
Serializer::optional(Serializer::string()).to_json(&None::<String>, JsonFlavor::Dense)
);
// null
// Array serializer:
println!(
"{}",
Serializer::array(Serializer::bool()).to_json(&vec![true, false], JsonFlavor::Dense)
);
// [1,0]Constants
// Constants declared with 'const' in the .skir file are available as
// functions in the generated Rust code.
println!(
"{}",
User::serializer().to_json(tarzan_const(), JsonFlavor::Readable)
);
// {
// "user_id": 123,
// "name": "Tarzan",
// "quote": "AAAAaAaAaAyAAAAaAaAaAyAAAAaAaAaA",
// "pets": [
// {
// "name": "Cheeta",
// "height_in_meters": 1.67,
// "picture": "🐒"
// }
// ],
// "subscription_status": {
// "kind": "trial",
// "value": {
// "start_time": {
// "unix_millis": 1743592409000,
// "formatted": "2025-04-02T11:13:29.000Z"
// }
// }
// }
// }Keyed lists
// In the .skir file:
// struct UserRegistry {
// users: [User|user_id];
// }
// The '|user_id' part tells Skir to generate a keyed array with O(1)
// lookup by user_id.
let user_registry = UserRegistry {
users: vec![john.clone(), jane.clone(), evil_john.clone()].into(),
_unrecognized: None,
};
// find_by_key returns the first element whose user_id matches.
// The first call is O(n) to build the index; subsequent calls are O(1).
let found = user_registry.users.find_by_key(43);
println!("{}", found.is_some()); // true
println!("{}", found.unwrap() == &jane); // true
// If multiple elements share the same key, the first one wins.
let found2 = user_registry.users.find_by_key(42);
println!("{}", found2.unwrap() == &john); // true
let not_found = user_registry.users.find_by_key(999_i32);
println!("{}", not_found.is_none()); // true
// find_by_key_or_default() returns a reference to the default (zero) value
// when the key is not present, instead of returning None.
let not_found_or_default = user_registry.users.find_by_key_or_default(999_i32);
println!("{}", not_found_or_default.pets.len()); // 0Skir services
Starting a Skir service on an HTTP server
Full example here.
Sending RPCs to a Skir service
Full example here.
Reflection
Reflection allows you to inspect a Skir type at runtime.
use skir_client::TypeDescriptor;
let td = User::serializer().type_descriptor();
if let TypeDescriptor::Struct(sd) = td {
let names: Vec<&str> = sd.fields().iter().map(|f| f.name()).collect();
println!("{:?}", names);
// ["user_id", "name", "quote", "pets", "subscription_status"]
}
// A TypeDescriptor can be serialized to JSON and deserialized later.
let td2 = TypeDescriptor::parse_from_json(
&User::serializer().type_descriptor().as_json(),
)
.unwrap();
if let TypeDescriptor::Struct(sd2) = td2 {
println!("{}", sd2.fields().len()); // 5
}