From 4cd0782f2e442c76cc433c292cdfda5523d70bf6 Mon Sep 17 00:00:00 2001 From: Lorenz Leutgeb Date: Sun, 25 May 2025 20:51:40 +0200 Subject: [PATCH] radicle-schemars: Add crate for utility binary Extracting JSON Schemas from the Radicle crate can be done via `radicle-cli` for `radicle::profile::Config` by executing: rad config schema However, for other JSON Schema metadata, this is not possible. To allow other tools to extract JSON schemas as part of their build process, introduce a new crate that only depends on `radicle`, `schemars`, and `serde`, with a tiny binary that will reproduce various schemas. --- Cargo.lock | 11 ++++ Cargo.toml | 1 + radicle-schemars/Cargo.toml | 24 ++++++++ radicle-schemars/src/main.rs | 108 +++++++++++++++++++++++++++++++++++ 4 files changed, 144 insertions(+) create mode 100644 radicle-schemars/Cargo.toml create mode 100644 radicle-schemars/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index c8cdcb11..4fb88e7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2821,6 +2821,17 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "radicle-schemars" +version = "0.1.0" +dependencies = [ + "once_cell", + "radicle", + "schemars", + "serde", + "serde_json", +] + [[package]] name = "radicle-signals" version = "0.11.0" diff --git a/Cargo.toml b/Cargo.toml index d2b90c2c..267bd789 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "radicle-remote-helper", "radicle-ssh", "radicle-tools", + "radicle-schemars", "radicle-signals", "radicle-systemd", ] diff --git a/radicle-schemars/Cargo.toml b/radicle-schemars/Cargo.toml new file mode 100644 index 00000000..3fb30043 --- /dev/null +++ b/radicle-schemars/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "radicle-schemars" +description = "Utility to print JSON Schemas from the `radicle` crate" +homepage = "https://radicle.xyz" +repository = "https://app.radicle.xyz/seeds/seed.radicle.xyz/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5" +license = "MIT OR Apache-2.0" +edition = "2021" +version = "0.1.0" +rust-version.workspace = true + +[[bin]] +name = "radicle-schemars" +path = "src/main.rs" + +[dependencies] +once_cell = { version = "1.13" } +schemars = { workspace = true } +serde = { version = "1.0" } +serde_json = { version = "1" } + +[dependencies.radicle] +version = "0" +path = "../radicle" +features = ["schemars"] \ No newline at end of file diff --git a/radicle-schemars/src/main.rs b/radicle-schemars/src/main.rs new file mode 100644 index 00000000..5a2dcb11 --- /dev/null +++ b/radicle-schemars/src/main.rs @@ -0,0 +1,108 @@ +use std::io; +use std::net; + +use once_cell::sync::Lazy; +use schemars::{generate::*, *}; + +const SCHEMA_COMMAND: &str = "radicle::node::Command"; +const SCHEMA_COMMAND_RESULT: &str = "radicle::node::CommandResult"; +const SCHEMA_PROFILE_CONFIG: &str = "radicle::profile::Config"; + +const SCHEMAS: &[&str] = &[SCHEMA_COMMAND, SCHEMA_COMMAND_RESULT, SCHEMA_PROFILE_CONFIG]; + +pub static ERROR_MSG: Lazy = Lazy::new(|| { + let schemas = SCHEMAS.to_vec().join("\", \""); + format!("Expected exactly one of the following schema names: [\"{schemas}\"].") +}); + +enum Schema { + Command, + CommandResult, + ProfileConfig, +} + +impl std::str::FromStr for Schema { + type Err = io::Error; + + fn from_str(s: &str) -> Result { + match s { + SCHEMA_COMMAND => Ok(Self::Command), + SCHEMA_COMMAND_RESULT => Ok(Self::CommandResult), + SCHEMA_PROFILE_CONFIG => Ok(Self::ProfileConfig), + schema => Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("{schema}: {}", *ERROR_MSG), + )), + } + } +} + +impl Schema { + fn from_args(mut args: std::env::Args) -> io::Result { + let name = args.nth(1).ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!("No schema given. {}", *ERROR_MSG), + ) + })?; + name.parse() + } +} + +fn main() { + if let Err(e) = print_schema() { + eprintln!("{}", e); + std::process::exit(1); + } +} + +fn print_schema() -> io::Result<()> { + let args = std::env::args(); + + let name = Schema::from_args(args)?; + + let schema = match name { + Schema::Command => { + let settings = SchemaSettings::default().for_serialize(); + let generator = settings.into_generator(); + + generator.into_root_schema_for::() + } + Schema::CommandResult => { + #[derive(JsonSchema)] + #[allow(dead_code)] + struct ListenAddrs(Vec); + + #[derive(JsonSchema)] + #[allow(dead_code)] + struct Error { + error: String, + } + + #[derive(JsonSchema)] + #[schemars(untagged)] + #[allow(dead_code)] + enum CommandResult { + Nid( + #[schemars(with = "radicle::schemars_ext::crypto::PublicKey")] + radicle::node::NodeId, + ), + Config(radicle::node::Config), + ListenAddrs(ListenAddrs), + ConnectResult(radicle::node::ConnectResult), + Success(radicle::node::Success), + Seeds(radicle::node::Seeds), + FetchResult(radicle::node::FetchResult), + RefsAt(radicle::storage::refs::RefsAt), + Sessions(Vec), + Session(Option), + Error(Error), + } + schema_for!(CommandResult) + } + Schema::ProfileConfig => schemars::schema_for!(radicle::profile::Config), + }; + + serde_json::to_writer_pretty(std::io::stdout(), &schema)?; + Ok(()) +}