use serde_json::Value;
use std::{env, fs::write};
fn main() {
let json_schema = serde_json::from_str(include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/schema.json"
)))
.expect("Failed to deserialize RPC Schema");
let code = generate(&json_schema).expect("Failed to generate json rpc bindings");
write(
format!("{}/json_rpc_bindings.rs", env::var("OUT_DIR").unwrap()),
code,
)
.expect("Failed to write json_rpc_bindings.rs");
}
const CURLY: [char; 2] = ['{', '}'];
const IDENTATION: &'static str = " ";
const FN_IDENTATION: &'static str = " ";
const DEFAULT_DERIVES: &'static str =
"#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Hash)]";
pub fn generate(schema: &Value) -> Option<String> {
let mut code = String::new();
code.push_str(dependencies());
code.push('\n');
code.push_str(&base_client());
code.push('\n');
println!("schemas");
for (parent_key, schema) in schema.get("components")?.get("schemas")?.as_object()? {
let schema_code = if let Some(c) = EnumData::from_value(&parent_key, &schema) {
c.to_code()
} else if let Some(c) = StructData::from_value(&parent_key, &schema) {
c.to_code()
} else {
return None;
};
code.push_str(&schema_code);
}
code.push_str(&format!("impl Client {}\n", CURLY[0]));
for method in schema.get("methods")?.as_array()? {
code.push_str(&FunctionData::from_value(&method)?.to_code());
}
code.push_str(&format!("\n{}", CURLY[1]));
Some(code)
}
fn dependencies() -> &'static str {
r#"
use std::{result::Result as StdResult, time::Duration};
use serde::{Deserialize, Serialize};
use futures_util::TryStreamExt as _;
use tokio_stream::{Stream, wrappers::{BroadcastStream, errors::BroadcastStreamRecvError}};
pub use pale::{ClientConfig, Result, PaleError, RPCError, StreamExt, WebSocketConfig};"#
}
fn base_client() -> String {
format!(
r#"
#[derive(Debug, Clone)]
pub struct Client(pub(crate) pale::Client);
impl Client {0}
pub async fn new(uri: impl AsRef<str>, config: ClientConfig) -> Result<Self> {0}
Ok(Self(pale::Client::new(uri, config).await?))
{1}
/// Calling [`Self::close`] means:
/// - Closing the underlying connection.
/// - Any and all internal client communication
/// - Closing every subscription stream
///
/// The [`Client`] is not guaranteed to be 100% closed after this function returns.
/// It may take a little while, use [`Self::wait_for_connection`] to make sure before, let's say reconnecting.
pub async fn close(&self) -> Result<()> {0}
self.0.close().await
{1}
/// Returns if the underlying socket is actively connected.
pub async fn is_connected(&self) -> bool {0}
self.0.is_connected().await
{1}
/// Returns when the [`Self::is_connected`] is equal to `state`
///
/// If [`Self::is_connected`] already matches `state`, it instantly returns
///
/// ## Example
/// ```no_run
/// // waits for the underlying connection to be ready & connected
/// client.wait_for_connection(true, Duration::from_secs(5)).await;
///
/// // waits for the connection to disconnect
/// client.wait_for_connection(false, Duration::from_secs(5)).await;
/// ```
pub async fn wait_for_connection(&self, state: bool, timeout_duration: Duration) -> Result<()> {0}
self.0.wait_for_connection(state, timeout_duration).await
{1}
/// Returns a [`Stream`] where a message of type [`Client`] will be sent upon each successful reconnection.
pub fn on_reconnect(&self) -> impl Stream<Item = StdResult<Self, BroadcastStreamRecvError>> {0}
BroadcastStream::new(self.0.on_reconnect()).map_ok(Self)
{1}
/// Returns a [`Stream`] where a message of type [`Client`] will be sent upon disconnect.
pub fn on_disconnect(&self) -> impl Stream<Item = StdResult<Self, BroadcastStreamRecvError>> {0}
BroadcastStream::new(self.0.on_disconnect()).map_ok(Self)
{1}
{1}"#,
CURLY[0], CURLY[1]
)
}
#[derive(Debug, Clone)]
struct RustType(String);
impl RustType {
fn new(type_data: &Value, struct_key: Option<&str>, parent_key: Option<&str>) -> Option<Self> {
let _type = if type_data.get("enum").is_some()
|| type_data.get("type").unwrap_or(&Value::Null).is_array()
{
match (struct_key, parent_key) {
(Some(struct_key), Some(parent_key)) => {
schema_type_to_rust(&format!("{struct_key}_{parent_key}"))
}
_ => return None,
}
} else if let Some(_type) = type_data.get("type") {
let _type = _type.as_str()?;
if _type == "array" {
let items = type_data.get("items")?.as_object()?;
let vec_type = if let Some(item_type) = items.get("type") {
item_type.as_str()?
} else if let Some(item_ref) = items.get("$ref") {
item_ref.as_str()?.split('/').last()?
} else {
return None;
};
format!("Vec<{}>", schema_type_to_rust(vec_type))
} else {
schema_type_to_rust(_type)
}
} else if let Some(_ref) = type_data.get("$ref") {
schema_type_to_rust(_ref.as_str()?.split('/').last()?)
} else {
return None;
};
Some(Self(_type))
}
fn new_empty() -> Self {
Self("()".to_string())
}
fn inner(&self) -> &str {
&self.0
}
}
fn field_case(text: &str) -> (String, bool) {
let chars = text.chars().collect::<Vec<char>>();
let mut new_name = String::new();
let mut is_renamed = false;
for char in chars {
if char.is_ascii_uppercase() {
new_name.push('_');
is_renamed = true;
}
new_name.push(char.to_ascii_lowercase());
}
if new_name == "type" {
new_name = "_type".to_string();
is_renamed = true;
}
(new_name, is_renamed)
}
fn to_pascal_case(text: &str) -> String {
const DELIMITER: char = '_';
text.split(DELIMITER)
.map(|f| {
let mut chars = f.to_lowercase().chars().collect::<Vec<char>>();
chars[0] = chars[0].to_ascii_uppercase();
chars.into_iter().collect::<String>()
})
.collect::<Vec<String>>()
.join("")
}
fn schema_type_to_rust(rust_type: &str) -> String {
match rust_type {
"string" => "String".to_string(),
"integer" => "i32".to_string(),
"boolean" => "bool".to_string(),
other => to_pascal_case(other),
}
}
#[derive(Debug)]
struct StructData {
name: String,
fields: Vec<Field>,
}
impl StructData {
fn from_value(parent_key: &str, data: &Value) -> Option<Self> {
let fields = data
.get("properties")?
.as_object()?
.iter()
.map(|(name, data)| Field::from_value(&parent_key, &name, data))
.collect::<Option<Vec<Field>>>()?;
Some(StructData {
name: parent_key.to_string(),
fields,
})
}
fn to_code(self) -> String {
let mut code = String::new();
code.push_str(&format!("{DEFAULT_DERIVES}\n"));
code.push_str(&format!(
"pub struct {} {}\n",
to_pascal_case(&self.name),
CURLY[0]
));
let enum_arg_code = self.get_arg_enums();
let field_len = self.fields.len();
for (i, field) in self.fields.into_iter().enumerate() {
code.push_str(&field.to_code());
if i != (field_len - 1) {
code.push_str(",\n");
}
}
code.push('\n');
code.push(CURLY[1]);
code.push('\n');
if !enum_arg_code.is_empty() {
code.push_str(&enum_arg_code);
}
code
}
fn get_arg_enums(&self) -> String {
let mut code = String::new();
for field in &self.fields {
if !(field.type_union.is_some() || field.type_enum.is_some()) {
continue;
}
code.push_str(&format!("{DEFAULT_DERIVES}\n"));
if let Some(union) = &field.type_enum {
code.push_str(&format!(
"pub enum {} {}\n",
field.rust_type.inner(),
CURLY[0]
));
for (i, variant) in union.iter().enumerate() {
code.push_str(&format!("{IDENTATION}#[serde(rename = \"{}\")]\n", variant));
code.push_str(IDENTATION);
code.push_str(&to_pascal_case(&variant));
if i != (union.len() - 1) {
code.push_str(",\n");
}
}
} else if let Some(_enum) = &field.type_union {
code.push_str("#[serde(untagged)]\n");
code.push_str(&format!(
"pub enum {} {}\n",
field.rust_type.inner(),
CURLY[0]
));
for (i, variant) in _enum.iter().enumerate() {
code.push_str(&format!(
"{IDENTATION}{}({})",
to_pascal_case(&variant),
schema_type_to_rust(&variant)
));
if i != (_enum.len() - 1) {
code.push_str(",\n");
}
}
}
code.push('\n');
code.push(CURLY[1]);
code.push('\n');
}
code
}
}
#[derive(Debug, Clone)]
struct Field {
name: String,
rust_type: RustType,
attribute: Option<String>,
type_union: Option<Vec<String>>,
type_enum: Option<Vec<String>>,
}
impl Field {
fn from_value(struct_key: &str, parent_key: &str, data: &Value) -> Option<Self> {
Some(Field {
name: parent_key.to_string(),
rust_type: RustType::new(&data, Some(struct_key), Some(parent_key))?,
attribute: None,
type_union: if data.get("type").unwrap_or(&Value::Null).is_array() {
Some(
data.get("type")?
.as_array()?
.iter()
.map(|s| s.as_str().unwrap().to_string())
.collect(),
)
} else {
None
},
type_enum: data.get("enum").map(|e| {
e.as_array()
.unwrap()
.iter()
.map(|s| s.as_str().unwrap().to_string())
.collect()
}),
})
}
fn to_code(self) -> String {
let mut field = String::new();
if let Some(attr) = self.attribute {
field.push_str(&format!("{IDENTATION}{attr}\n"));
}
let (field_name, name_modified) = field_case(&self.name);
if name_modified {
field.push_str(&format!(
"{IDENTATION}#[serde(rename = \"{}\")]\n",
self.name
));
}
field.push_str(&format!(
"{IDENTATION}pub {}: {}",
field_name,
self.rust_type.inner()
));
field
}
}
#[derive(Debug)]
struct EnumData {
name: String,
variants: Vec<String>,
enum_type: String,
attribute: Option<String>,
}
impl EnumData {
fn from_value(parent_key: &str, data: &Value) -> Option<Self> {
let variants = data
.get("enum")?
.as_array()?
.iter()
.map(|v| v.as_str().unwrap().to_string())
.collect::<Vec<String>>();
let enum_type = data.get("type")?.as_str()?.to_string();
Some(EnumData {
name: parent_key.to_string(),
variants,
enum_type,
attribute: None,
})
}
fn to_code(self) -> String {
let mut code = String::new();
if self.enum_type != "string" {
unimplemented!(
"Any other rust type other than string is currently not supported, if this panic was naturally triggered via the autogenerated schema then this will need to be implemented."
);
}
code.push_str(&format!("{DEFAULT_DERIVES}\n"));
if let Some(attr) = self.attribute {
code.push_str(&attr);
code.push('\n');
}
code.push_str(&format!(
"pub enum {} {}\n",
to_pascal_case(&self.name),
CURLY[0]
));
let variant_len = self.variants.len();
for (i, variant) in self.variants.into_iter().enumerate() {
code.push_str(&format!("{IDENTATION}#[serde(rename = \"{variant}\")]\n"));
code.push_str(&format!("{IDENTATION}{}", to_pascal_case(&variant)));
if i != (variant_len - 1) {
code.push_str(",\n");
}
}
code.push('\n');
code.push(CURLY[1]);
code.push('\n');
code
}
}
#[derive(Debug)]
struct FunctionData {
doc: String,
name: String,
endpoint: String,
function_type: FunctionType,
params: Vec<FunctionParam>,
return_type: RustType,
}
#[derive(Debug, Clone)]
struct FunctionParam {
raw_name: String,
name: String,
rust_type: RustType,
}
#[derive(Debug)]
enum FunctionType {
Request,
Notification,
}
impl FunctionData {
fn from_value(data: &Value) -> Option<Self> {
let doc = data.get("description")?.as_str()?.to_string();
let name = data
.get("name")?
.as_str()?
.trim_start_matches("minecraft:")
.replace('/', "_");
let endpoint = data.get("name")?.as_str()?.to_string();
let function_type = if name.starts_with("notification") {
FunctionType::Notification
} else {
FunctionType::Request
};
println!("{doc:?}, {name:?}, {endpoint:?}, {function_type:?}");
let (params, return_type) = match function_type {
FunctionType::Request => {
let params: Vec<FunctionParam> = data
.get("params")?
.as_array()?
.iter()
.map(|p| FunctionParam::from_value(p))
.collect::<Option<Vec<FunctionParam>>>()?;
let return_type = if let Some(return_data) = data.get("result") {
RustType::new(return_data.get("schema")?, None, None)?
} else {
RustType::new_empty()
};
(params, return_type)
}
FunctionType::Notification => {
(
vec![],
if let Some(result) = data.get("params")?.as_array()?.get(0) {
RustType::new(result.as_object()?.get("schema")?, None, None)?
} else {
RustType::new_empty()
},
)
}
};
Some(FunctionData {
doc,
name,
endpoint,
function_type,
params,
return_type,
})
}
fn to_code(self) -> String {
let mut code = String::new();
let mut args = vec!["&self".to_string()];
args.append(
&mut self
.params
.clone()
.into_iter()
.map(|s| s.to_code())
.collect::<Vec<String>>(),
);
code.push_str(&format!("{IDENTATION}/// {}\n", self.doc));
code.push_str(&format!(
"{IDENTATION}pub async fn {}({}) -> Result<",
field_case(&self.name).0,
args.join(", ")
));
match self.function_type {
FunctionType::Notification => {
code.push_str(&format!(
"impl Stream<Item = Option<std::result::Result<Vec<{}>, serde_json::Error>>>",
self.return_type.inner()
));
}
FunctionType::Request => {
code.push_str(self.return_type.inner());
}
}
code.push_str(&format!("> {}\n", CURLY[0]));
match self.function_type {
FunctionType::Notification => {
code.push_str(&format!(
"{FN_IDENTATION}self.0.subscribe(\"{}\").await",
self.endpoint
));
}
FunctionType::Request => {
if self.params.is_empty() {
code.push_str(&format!(
"{FN_IDENTATION}self.0.request(\"{}\", None).await",
self.endpoint
));
} else {
code.push_str(
&format!("{FN_IDENTATION}let mut map: std::collections::HashMap<String, serde_json::Value> = std::collections::HashMap::new();\n"),
);
for param in self.params {
code.push_str(&format!(
"{FN_IDENTATION}map.insert(\"{}\".to_string(), serde_json::to_value(&{})?);\n",
param.raw_name, param.name
));
}
code.push_str(&format!(
"{FN_IDENTATION}self.0.request(\"{}\", Some(map)).await",
self.endpoint
));
}
}
};
code.push_str(&format!("\n{IDENTATION}{}\n", CURLY[1]));
code
}
}
impl FunctionParam {
fn from_value(data: &Value) -> Option<Self> {
let raw_name = data.get("name")?.as_str()?.to_string();
let name = if raw_name.starts_with("type") || raw_name.starts_with("use") {
format!("_{}", raw_name)
} else {
raw_name.to_string()
};
let rust_type = RustType::new(data.get("schema")?, None, None)?;
Some(FunctionParam {
raw_name,
name,
rust_type,
})
}
fn to_code(self) -> String {
format!("{}: {}", self.name, self.rust_type.inner())
}
}