Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
radicle-schemars: A crate for extracting JSON schemas
Merged lorenz opened 11 months ago

JSON Schema for the Control Socket

JSON Schema extraction via schemars is provided for configurations. There are other interfaces that use JSON for (de-)serialization, such as the communication with radicle-node via the control socket.

To ease implementation of tools that want to communicate via the control socket, we add the respective schemars annotations.

Extracting of JSON Schemas

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.

schemars as workspace dependency

The schemars crate is a dependency of multiple workspace crates in the same version. Its version number is repeated multiple times in the respective */Cargo.toml files. This requires more maintenance effort and risks versions drifting.

As long as all crates depend on the same version, it makes more sense to have schemars as a workspace dependency.

See: https://doc.rust-lang.org/cargo/reference/workspaces.html#the-dependencies-table

13 files changed +314 -12 9dae540c 4cd0782f
modified Cargo.lock
@@ -2822,6 +2822,16 @@ dependencies = [
]

[[package]]
+
name = "radicle-schemars"
+
version = "0.1.0"
+
dependencies = [
+
 "radicle",
+
 "schemars",
+
 "serde",
+
 "serde_json",
+
]
+

+
[[package]]
name = "radicle-signals"
version = "0.11.0"
dependencies = [
modified Cargo.toml
@@ -12,6 +12,7 @@ members = [
  "radicle-remote-helper",
  "radicle-ssh",
  "radicle-tools",
+
  "radicle-schemars",
  "radicle-signals",
  "radicle-systemd",
]
@@ -42,6 +43,9 @@ version = "0.9.0"
# for the day it makes a difference…
rust-version = "1.81.0"

+
[workspace.dependencies]
+
schemars = { version = "1.0.0-alpha.17" }
+

[workspace.lints]
clippy.type_complexity = "allow"
clippy.enum_variant_names = "allow"
modified radicle-cli/Cargo.toml
@@ -46,7 +46,7 @@ tree-sitter-bash = { version = "0.23.3" }
tree-sitter-go = { version = "0.23.4" }
tree-sitter-md = { version = "0.3.2" }
zeroize = { version = "1.1" }
-
schemars = { version = "1.0.0-alpha.17" }
+
schemars = { workspace = true }

[dependencies.radicle]
version = "0.15"
added radicle-schemars/Cargo.toml
@@ -0,0 +1,23 @@
+
[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]
+
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
added radicle-schemars/src/main.rs
@@ -0,0 +1,90 @@
+
use std::io;
+
use std::net;
+

+
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];
+

+
#[inline]
+
fn unknown_schema(schema: Option<String>) -> io::Result<()> {
+
    let schema = match schema {
+
        Some(schema) => format!("Unexpected schema name \"{schema}\" given."),
+
        None => "No schema name given.".into(),
+
    };
+
    let schemas = SCHEMAS.to_vec().join("\", \"");
+
    Err(io::Error::new(
+
        io::ErrorKind::InvalidInput,
+
        format!("{schema} Expected exactly one of the following schema names: [\"{schemas}\"]."),
+
    ))
+
}
+

+
fn main() {
+
    if let Err(e) = print_schema() {
+
        eprintln!("{}", e);
+
        std::process::exit(1);
+
    }
+
}
+

+
fn print_schema() -> io::Result<()> {
+
    let mut args = std::env::args();
+

+
    let Some(name) = args.nth(1) else {
+
        return unknown_schema(None);
+
    };
+

+
    if !SCHEMAS.contains(&&name.as_str()) {
+
        return unknown_schema(Some(name));
+
    }
+

+
    let schema = match name.as_str() {
+
        SCHEMA_COMMAND => {
+
            let settings = SchemaSettings::default().for_serialize();
+
            let generator = settings.into_generator();
+

+
            generator.into_root_schema_for::<radicle::node::Command>()
+
        }
+
        SCHEMA_COMMAND_RESULT => {
+
            #[derive(JsonSchema)]
+
            #[allow(dead_code)]
+
            struct ListenAddrs(Vec<net::SocketAddr>);
+

+
            #[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<radicle::node::Session>),
+
                Session(Option<radicle::node::Session>),
+
                Error(Error),
+
            }
+
            schema_for!(CommandResult)
+
        }
+
        SCHEMA_PROFILE_CONFIG => schemars::schema_for!(radicle::profile::Config),
+
        _ => {
+
            return unknown_schema(Some(name));
+
        }
+
    };
+

+
    serde_json::to_writer_pretty(std::io::stdout(), &schema)?;
+
    Ok(())
+
}
modified radicle/Cargo.toml
@@ -35,7 +35,7 @@ sqlite = { version = "0.32.0", features = ["bundled"] }
tempfile = { version = "3.3.0" }
thiserror = { version = "1" }
unicode-normalization = { version = "0.1" }
-
schemars = { version = "1.0.0-alpha.17", optional = true }
+
schemars = { workspace = true, optional = true }

[dependencies.chrono]
version = "0.4.0"
modified radicle/src/lib.rs
@@ -24,7 +24,7 @@ pub mod node;
pub mod profile;
pub mod rad;
#[cfg(feature = "schemars")]
-
pub(crate) mod schemars_ext;
+
pub mod schemars_ext;
pub mod serde_ext;
pub mod sql;
pub mod storage;
modified radicle/src/node.rs
@@ -96,6 +96,7 @@ pub enum PingState {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[allow(clippy::large_enum_variant)]
#[serde(rename_all = "camelCase")]
+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum State {
    /// Initial state for outgoing connections.
    Initial,
@@ -106,6 +107,10 @@ pub enum State {
    Connected {
        /// Connected since this time.
        #[serde(with = "crate::serde_ext::localtime::time")]
+
        #[cfg_attr(
+
            feature = "schemars",
+
            schemars(with = "crate::schemars_ext::localtime::LocalDurationInSeconds")
+
        )]
        since: LocalTime,
        /// Ping state.
        #[serde(skip)]
@@ -124,9 +129,17 @@ pub enum State {
    Disconnected {
        /// Since when has this peer been disconnected.
        #[serde(with = "crate::serde_ext::localtime::time")]
+
        #[cfg_attr(
+
            feature = "schemars",
+
            schemars(with = "crate::schemars_ext::localtime::LocalDurationInSeconds")
+
        )]
        since: LocalTime,
        /// When to retry the connection.
        #[serde(with = "crate::serde_ext::localtime::time")]
+
        #[cfg_attr(
+
            feature = "schemars",
+
            schemars(with = "crate::schemars_ext::localtime::LocalDurationInSeconds")
+
        )]
        retry_at: LocalTime,
    },
}
@@ -185,6 +198,7 @@ impl Penalty {
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(tag = "status")]
#[serde(rename_all = "camelCase")]
+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum SyncStatus {
    /// We're in sync.
    #[serde(rename_all = "camelCase")]
@@ -428,6 +442,7 @@ impl TryFrom<&sqlite::Value> for Alias {

/// Options passed to the "connect" node command.
#[derive(Debug, Clone, Serialize, Deserialize)]
+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ConnectOptions {
    /// Establish a persistent connection.
    pub persistent: bool,
@@ -480,6 +495,7 @@ impl From<Event> for CommandResult<Event> {

/// A success response.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Success {
    /// Whether something was updated.
    #[serde(default, skip_serializing_if = "crate::serde_ext::is_default")]
@@ -589,6 +605,7 @@ impl From<Address> for HostName {
/// Command name.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "command")]
+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum Command {
    /// Announce repository references for given repository to peers.
    #[serde(rename_all = "camelCase")]
@@ -616,7 +633,13 @@ pub enum Command {

    /// Disconnect from a node.
    #[serde(rename_all = "camelCase")]
-
    Disconnect { nid: NodeId },
+
    Disconnect {
+
        #[cfg_attr(
+
            feature = "schemars",
+
            schemars(with = "crate::schemars_ext::crypto::PublicKey")
+
        )]
+
        nid: NodeId,
+
    },

    /// Lookup seeds for the given repository in the routing table.
    #[serde(rename_all = "camelCase")]
@@ -626,12 +649,22 @@ pub enum Command {
    Sessions,

    /// Get a specific peer session.
-
    Session { nid: NodeId },
+
    Session {
+
        #[cfg_attr(
+
            feature = "schemars",
+
            schemars(with = "crate::schemars_ext::crypto::PublicKey")
+
        )]
+
        nid: NodeId,
+
    },

    /// Fetch the given repository from the network.
    #[serde(rename_all = "camelCase")]
    Fetch {
        rid: RepoId,
+
        #[cfg_attr(
+
            feature = "schemars",
+
            schemars(with = "crate::schemars_ext::crypto::PublicKey")
+
        )]
        nid: NodeId,
        timeout: time::Duration,
    },
@@ -646,11 +679,24 @@ pub enum Command {

    /// Follow the given node.
    #[serde(rename_all = "camelCase")]
-
    Follow { nid: NodeId, alias: Option<Alias> },
+
    Follow {
+
        #[cfg_attr(
+
            feature = "schemars",
+
            schemars(with = "crate::schemars_ext::crypto::PublicKey")
+
        )]
+
        nid: NodeId,
+
        alias: Option<Alias>,
+
    },

    /// Unfollow the given node.
    #[serde(rename_all = "camelCase")]
-
    Unfollow { nid: NodeId },
+
    Unfollow {
+
        #[cfg_attr(
+
            feature = "schemars",
+
            schemars(with = "crate::schemars_ext::crypto::PublicKey")
+
        )]
+
        nid: NodeId,
+
    },

    /// Get the node's status.
    Status,
@@ -679,6 +725,7 @@ impl Command {
/// Connection link direction.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum Link {
    /// Outgoing connection.
    Outbound,
@@ -688,7 +735,12 @@ pub enum Link {

/// An established network connection with a peer.
#[derive(Debug, Clone, Serialize, Deserialize)]
+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Session {
+
    #[cfg_attr(
+
        feature = "schemars",
+
        schemars(with = "crate::schemars_ext::crypto::PublicKey")
+
    )]
    pub nid: NodeId,
    pub link: Link,
    pub addr: Address,
@@ -705,8 +757,14 @@ impl Session {
/// A seed for some repository, with metadata about its status.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
+

pub struct Seed {
    /// The Node ID.
+
    #[cfg_attr(
+
        feature = "schemars",
+
        schemars(with = "crate::schemars_ext::crypto::PublicKey")
+
    )]
    pub nid: NodeId,
    /// Known addresses for this seed.
    pub addrs: Vec<KnownAddress>,
@@ -748,7 +806,11 @@ impl Seed {
/// Represents a set of seeds with associated metadata. Uses an RNG
/// underneath, so every iteration returns a different ordering.
#[serde(into = "Vec<Seed>", from = "Vec<Seed>")]
-
pub struct Seeds(address::AddressBook<NodeId, Seed>);
+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
+
pub struct Seeds(
+
    #[cfg_attr(feature = "schemars", schemars(with = "Vec<Seed>"))]
+
    address::AddressBook<NodeId, Seed>,
+
);

impl Seeds {
    /// Create a new seeds list from an RNG.
@@ -819,9 +881,14 @@ impl From<Vec<Seed>> for Seeds {

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "camelCase")]
+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum FetchResult {
    Success {
        updated: Vec<RefUpdate>,
+
        #[cfg_attr(
+
            feature = "schemars",
+
            schemars(with = "HashSet<crate::schemars_ext::crypto::PublicKey>")
+
        )]
        namespaces: HashSet<NodeId>,
        clone: bool,
    },
@@ -978,6 +1045,7 @@ impl Error {

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "status")]
+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum ConnectResult {
    Connected,
    Disconnected { reason: String },
modified radicle/src/node/address.rs
@@ -143,6 +143,7 @@ pub struct Node {
/// A known address.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct KnownAddress {
    /// Network address.
    pub addr: Address,
@@ -150,9 +151,17 @@ pub struct KnownAddress {
    pub source: Source,
    /// Last time this address was used to successfully connect to a peer.
    #[serde(with = "crate::serde_ext::localtime::option::time")]
+
    #[cfg_attr(
+
        feature = "schemars",
+
        schemars(with = "Option<crate::schemars_ext::localtime::LocalDurationInSeconds>")
+
    )]
    pub last_success: Option<LocalTime>,
    /// Last time this address was tried.
    #[serde(with = "crate::serde_ext::localtime::option::time")]
+
    #[cfg_attr(
+
        feature = "schemars",
+
        schemars(with = "Option<crate::schemars_ext::localtime::LocalDurationInSeconds>")
+
    )]
    pub last_attempt: Option<LocalTime>,
    /// Whether this address has been banned.
    pub banned: bool,
@@ -174,6 +183,7 @@ impl KnownAddress {
/// Address source. Specifies where an address originated from.
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum Source {
    /// An address that was shared by another peer.
    Peer,
modified radicle/src/node/seed.rs
@@ -11,11 +11,17 @@ use crate::storage::{refs::RefsAt, ReadRepository, RemoteId};
/// Holds an oid and timestamp.
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct SyncedAt {
    /// Head of `rad/sigrefs`.
+
    #[cfg_attr(feature = "schemars", schemars(with = "crate::schemars_ext::git::Oid"))]
    pub oid: git_ext::Oid,
    /// When these refs were synced.
    #[serde(with = "crate::serde_ext::localtime::time")]
+
    #[cfg_attr(
+
        feature = "schemars",
+
        schemars(with = "crate::schemars_ext::localtime::LocalDurationInSeconds")
+
    )]
    pub timestamp: LocalTime,
}

modified radicle/src/schemars_ext.rs
@@ -4,6 +4,29 @@

use schemars::JsonSchema;

+
pub mod crypto {
+
    use super::*;
+
    /// See [`crate::node::NodeId`]
+
    /// See [`crate::storage::RemoteId`]
+
    /// See [`::crypto::PublicKey`]
+
    ///
+
    /// An Ed25519 public key in multibase encoding.
+
    ///
+
    /// `MULTIBASE(base58-btc, MULTICODEC(public-key-type, raw-public-key-bytes))`
+
    #[derive(JsonSchema)]
+
    #[schemars(
+
    title = "NodeId",
+
    description = "An Ed25519 public key in multibase encoding.",
+
    extend("examples" = [
+
        "z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7",
+
        "z6MkvUJtYD9dHDJfpevWRT98mzDDpdAtmUjwyDSkyqksUr7C",
+
        "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi",
+
        "z6MkkfM3tPXNPrPevKr3uSiQtHPuwnNhu2yUVjgd2jXVsVz5",
+
    ]),
+
)]
+
    pub struct PublicKey(String);
+
}
+

pub(crate) mod log {
    use super::*;

@@ -56,4 +79,34 @@ pub(crate) mod localtime {
        description = "A time duration measured locally in milliseconds."
    )]
    pub(crate) struct LocalDuration(u64);
+

+
    /// See [`crate::serde_ext::localtime::time`]
+
    #[derive(JsonSchema)]
+
    #[schemars(
+
        remote = "localtime::LocalDuration",
+
        description = "A time duration measured locally in seconds."
+
    )]
+
    pub(crate) struct LocalDurationInSeconds(u64);
+
}
+

+
pub(crate) mod git {
+
    use super::*;
+

+
    /// See [`crate::git::Oid`]
+
    /// See [`::git_ext::Oid`]
+
    /// See [`::git2::Oid`]
+
    ///
+
    /// A Git Object Identifier in hexadecimal encoding.
+
    #[derive(JsonSchema)]
+
    #[schemars(
+
        remote = "git2::Oid",
+
        description = "A Git Object Identifier (SHA-1 or SHA-256 hash) in hexadecimal encoding."
+
    )]
+
    pub(crate) struct Oid(
+
        #[schemars(regex(pattern = r"^([0-9a-fA-F]{64}|[0-9a-fA-F]{40})$"))] String,
+
    );
+

+
    /// See [`crate::git::RefString`]
+
    #[derive(JsonSchema)]
+
    pub(crate) struct RefString(String);
}
modified radicle/src/storage.rs
@@ -191,11 +191,43 @@ pub type RemoteId = PublicKey;
/// An update to a reference.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum RefUpdate {
-
    Updated { name: RefString, old: Oid, new: Oid },
-
    Created { name: RefString, oid: Oid },
-
    Deleted { name: RefString, oid: Oid },
-
    Skipped { name: RefString, oid: Oid },
+
    Updated {
+
        #[cfg_attr(
+
            feature = "schemars",
+
            schemars(with = "crate::schemars_ext::git::RefString")
+
        )]
+
        name: RefString,
+
        #[cfg_attr(feature = "schemars", schemars(with = "crate::schemars_ext::git::Oid"))]
+
        old: Oid,
+
        #[cfg_attr(feature = "schemars", schemars(with = "crate::schemars_ext::git::Oid"))]
+
        new: Oid,
+
    },
+
    Created {
+
        #[cfg_attr(
+
            feature = "schemars",
+
            schemars(with = "crate::schemars_ext::git::RefString")
+
        )]
+
        name: RefString,
+
        #[cfg_attr(feature = "schemars", schemars(with = "crate::schemars_ext::git::Oid"))]
+
        oid: Oid,
+
    },
+
    Deleted {
+
        #[cfg_attr(
+
            feature = "schemars",
+
            schemars(with = "crate::schemars_ext::git::RefString")
+
        )]
+
        name: RefString,
+
        #[cfg_attr(feature = "schemars", schemars(with = "crate::schemars_ext::git::Oid"))]
+
        oid: Oid,
+
    },
+
    Skipped {
+
        #[cfg_attr(feature = "schemars", schemars(with = "String"))]
+
        name: RefString,
+
        #[cfg_attr(feature = "schemars", schemars(with = "crate::schemars_ext::git::Oid"))]
+
        oid: Oid,
+
    },
}

impl RefUpdate {
modified radicle/src/storage/refs.rs
@@ -377,10 +377,16 @@ impl<V> Deref for SignedRefs<V> {
/// references to other nodes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RefsAt {
    /// The remote namespace of the `rad/sigrefs`.
+
    #[cfg_attr(
+
        feature = "schemars",
+
        schemars(with = "crate::schemars_ext::crypto::PublicKey")
+
    )]
    pub remote: RemoteId,
    /// The commit SHA that `rad/sigrefs` points to.
+
    #[cfg_attr(feature = "schemars", schemars(with = "crate::schemars_ext::git::Oid"))]
    pub at: Oid,
}