Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
cli: `rad config schema` emits JSON Schema
Merged did:key:z6MkjFMA...eQnJ opened 1 year ago

Leverage schemars to generate a JSON Schema from our structs for configurations and those occurring within them.

Regarding dependency on an alpha version of schemars: Reading the road to 1.0 issue I don’t expect that we would be affected by breakage. We use pretty standard/core parts of the API that probably are most stable.

The output of rad config schema can be used by editors to provide a smoother editing experience for the configuration file. Discovery of attributes (both keys and values) is greatly improved with the schema helping in the background.

Refer to:

  • https://json-schema.org
  • https://code.visualstudio.com/docs/languages/json#_json-schemas-and-settings
  • https://schemastore.org

Original description by did:key:z6MkjFMAoA3hUG6tM7Wprn7dh7bquLpmN1f3yECbwkyweQnJ follows:

This will be a great addition especially for LSP users who dont want to read through all the code to know what options can be changed where and how.

How it works: You simply pipe the output into xyz.json and then add to your config: “$schema”: “/path/to/xyz.json” and if you have a working json LSP you’ll get docs, etc. for your config, meaning we’ll have to write a lot less docs in the future :)

13 files changed +132 -29 5a2f26ea b608a788
modified Cargo.lock
@@ -2203,6 +2203,7 @@ dependencies = [
 "radicle-crypto",
 "radicle-git-ext",
 "radicle-ssh",
+
 "schemars",
 "serde",
 "serde_json",
 "siphasher 1.0.1",
@@ -2232,6 +2233,7 @@ dependencies = [
 "radicle-node",
 "radicle-surf",
 "radicle-term",
+
 "schemars",
 "serde",
 "serde_json",
 "shlex",
@@ -2638,6 +2640,30 @@ dependencies = [
]

[[package]]
+
name = "schemars"
+
version = "0.8.22"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
+
dependencies = [
+
 "dyn-clone",
+
 "schemars_derive",
+
 "serde",
+
 "serde_json",
+
]
+

+
[[package]]
+
name = "schemars_derive"
+
version = "0.8.22"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "serde_derive_internals",
+
 "syn 2.0.89",
+
]
+

+
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2701,6 +2727,17 @@ dependencies = [
]

[[package]]
+
name = "serde_derive_internals"
+
version = "0.29.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
+
dependencies = [
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.89",
+
]
+

+
[[package]]
name = "serde_json"
version = "1.0.116"
source = "registry+https://github.com/rust-lang/crates.io-index"
modified radicle-cli/Cargo.toml
@@ -45,6 +45,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 = "0.8.22"

[dependencies.radicle]
version = "0"
modified radicle-cli/src/commands/config.rs
@@ -3,6 +3,8 @@ use std::ffi::OsString;
use std::path::Path;
use std::str::FromStr;

+
use schemars::schema_for;
+

use anyhow::anyhow;
use radicle::node::Alias;
use radicle::profile::{Config, ConfigError, ConfigPath, RawConfig};
@@ -23,6 +25,7 @@ Usage
    rad config init --alias <alias> [<option>...]
    rad config edit [<option>...]
    rad config get <key> [<option>...]
+
    rad config schema [<option>...]
    rad config set <key> <value> [<option>...]
    rad config unset <key> [<option>...]
    rad config push <key> <value> [<option>...]
@@ -43,6 +46,7 @@ enum Operation {
    #[default]
    Show,
    Get(String),
+
    Schema,
    Set(String, String),
    Push(String, String),
    Remove(String, String),
@@ -79,6 +83,7 @@ impl Args for Options {
                }
                Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
                    "show" => op = Some(Operation::Show),
+
                    "schema" => op = Some(Operation::Schema),
                    "edit" => op = Some(Operation::Edit),
                    "init" => op = Some(Operation::Init),
                    "get" => {
@@ -140,6 +145,10 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
            let profile = ctx.profile()?;
            term::json::to_pretty(&profile.config, path.as_path())?.print();
        }
+
        Operation::Schema => {
+
            let profile = ctx.profile()?;
+
            term::json::to_pretty(&schema_for!(Config), path.as_path())?.print();
+
        }
        Operation::Get(key) => {
            let mut temp_config = RawConfig::from_file(&path)?;
            let key: ConfigPath = key.into();
modified radicle/Cargo.toml
@@ -34,6 +34,7 @@ sqlite = { version = "0.32.0", features = ["bundled"] }
tempfile = { version = "3.3.0" }
thiserror = { version = "1" }
unicode-normalization = { version = "0.1" }
+
schemars = { version = "0.8.22", features = ["derive_json_schema", "impl_json_schema"] }

[dependencies.chrono]
version = "0.4.0"
modified radicle/src/cli.rs
@@ -1,5 +1,6 @@
+
use schemars::JsonSchema;
/// CLI configuration.
-
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct Config {
    /// Whether to show hints or not in the CLI.
modified radicle/src/explorer.rs
@@ -1,5 +1,6 @@
use std::str::FromStr;

+
use schemars::JsonSchema;
use thiserror::Error;

use crate::prelude::RepoId;
@@ -81,7 +82,7 @@ impl std::fmt::Display for ExplorerUrl {
}

/// A public explorer, eg. `https://app.radicle.xyz`.
-
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(transparent)]
pub struct Explorer(String);

modified radicle/src/identity/doc/id.rs
@@ -2,6 +2,7 @@ use std::ops::Deref;
use std::{ffi::OsString, fmt, str::FromStr};

use git_ext::ref_format::{Component, RefString};
+
use schemars::JsonSchema;
use thiserror::Error;

use crate::git;
@@ -18,9 +19,13 @@ pub enum IdError {
    Multibase(#[from] multibase::Error),
}

+
/// remote derive JsonSchema
+
/// https://github.com/GREsau/schemars/blob/v0/schemars/examples/remote_derive.rs
+
pub struct OidDef {}
+

/// A repository identifier.
-
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
-
pub struct RepoId(git::Oid);
+
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, JsonSchema)]
+
pub struct RepoId(#[schemars(with = "str")] git::Oid);

impl fmt::Display for RepoId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
modified radicle/src/node.rs
@@ -25,6 +25,7 @@ use std::{fmt, io, net, thread, time};
use amplify::WrapperMut;
use cyphernet::addr::NetAddr;
use localtime::{LocalDuration, LocalTime};
+
use schemars::JsonSchema;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json as json;
@@ -291,7 +292,9 @@ impl AsRef<str> for UserAgent {
}

/// Node alias.
-
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, serde::Serialize, serde::Deserialize)]
+
#[derive(
+
    Debug, PartialEq, Eq, PartialOrd, Ord, Clone, serde::Serialize, serde::Deserialize, JsonSchema,
+
)]
#[serde(try_from = "String", into = "String")]
pub struct Alias(String);

@@ -499,10 +502,16 @@ impl<T: Serialize> CommandResult<T> {
}

/// Peer public protocol address.
-
#[derive(Wrapper, WrapperMut, Clone, Eq, PartialEq, Debug, Hash, From, Serialize, Deserialize)]
+
#[derive(
+
    Wrapper, WrapperMut, Clone, Eq, PartialEq, Debug, Hash, From, Serialize, Deserialize, JsonSchema,
+
)]
#[wrapper(Deref, Display, FromStr)]
#[wrapper_mut(DerefMut)]
-
pub struct Address(#[serde(with = "crate::serde_ext::string")] NetAddr<HostName>);
+
pub struct Address(
+
    #[serde(with = "crate::serde_ext::string")]
+
    #[schemars(schema_with = "crate::serde_ext::string::make_schema")]
+
    NetAddr<HostName>,
+
);

impl Address {
    /// Check whether this address is from the local network.
modified radicle/src/node/config.rs
@@ -3,6 +3,8 @@ use std::ops::Deref;
use std::str::FromStr;
use std::{fmt, net};

+
use schemars::JsonSchema;
+

use cyphernet::addr::PeerAddr;
use localtime::LocalDuration;
use serde_json as json;
@@ -57,7 +59,9 @@ pub mod seeds {
}

/// Peer-to-peer network.
-
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
+
#[derive(
+
    Default, Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, JsonSchema,
+
)]
#[serde(rename_all = "camelCase")]
pub enum Network {
    #[default]
@@ -94,16 +98,18 @@ impl Network {
}

/// Configuration parameters defining attributes of minima and maxima.
-
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct Limits {
    /// Number of routing table entries before we start pruning.
    pub routing_max_size: usize,
    /// How long to keep a routing table entry before being pruned.
    #[serde(with = "crate::serde_ext::localtime::duration")]
+
    #[schemars(schema_with = "crate::serde_ext::localtime::duration::make_schema")]
    pub routing_max_age: LocalDuration,
    /// How long to keep a gossip message entry before pruning it.
    #[serde(with = "crate::serde_ext::localtime::duration")]
+
    #[schemars(schema_with = "crate::serde_ext::localtime::duration::make_schema")]
    pub gossip_max_age: LocalDuration,
    /// Maximum number of concurrent fetches per peer connection.
    pub fetch_concurrency: usize,
@@ -135,13 +141,20 @@ impl Default for Limits {
    }
}

+
/// for JsonSchema
+
/// https://graham.cool/schemars/examples/5-remote_derive/
+
#[derive(JsonSchema)]
+
#[serde(remote = "ByteSize")]
+
pub struct ByteSizeDef(pub u64);
+

/// Limiter for byte streams.
///
/// Default: 500MiB
-
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
+
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[serde(into = "String", try_from = "String")]
pub struct FetchPackSizeLimit {
+
    #[serde(with = "ByteSizeDef")]
    limit: bytesize::ByteSize,
}

@@ -213,7 +226,7 @@ impl Default for FetchPackSizeLimit {
}

/// Connection limits.
-
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ConnectionLimits {
    /// Max inbound connections.
@@ -232,7 +245,7 @@ impl Default for ConnectionLimits {
}

/// Rate limts for a single connection.
-
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct RateLimit {
    pub fill_rate: f64,
@@ -240,7 +253,7 @@ pub struct RateLimit {
}

/// Rate limits for inbound and outbound connections.
-
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct RateLimits {
    pub inbound: RateLimit,
@@ -263,9 +276,13 @@ impl Default for RateLimits {
}

/// Full address used to connect to a remote node.
-
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash)]
+
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, JsonSchema)]
#[serde(transparent)]
-
pub struct ConnectAddress(#[serde(with = "crate::serde_ext::string")] PeerAddr<NodeId, Address>);
+
pub struct ConnectAddress(
+
    #[serde(with = "crate::serde_ext::string")]
+
    #[schemars(schema_with = "crate::serde_ext::string::make_schema")]
+
    PeerAddr<NodeId, Address>,
+
);

impl From<PeerAddr<NodeId, Address>> for ConnectAddress {
    fn from(value: PeerAddr<NodeId, Address>) -> Self {
@@ -300,7 +317,7 @@ impl Deref for ConnectAddress {
}

/// Peer configuration.
-
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum PeerConfig {
    /// Static peer set. Connect to the configured peers and maintain the connections.
@@ -316,7 +333,7 @@ impl Default for PeerConfig {
}

/// Relay configuration.
-
#[derive(Debug, Copy, Clone, Default, serde::Serialize, serde::Deserialize)]
+
#[derive(Debug, Copy, Clone, Default, serde::Serialize, serde::Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub enum Relay {
    /// Always relay messages.
@@ -329,7 +346,7 @@ pub enum Relay {
}

/// Proxy configuration.
-
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", tag = "mode")]
pub enum AddressConfig {
    /// Proxy connections to this address type.
@@ -343,7 +360,9 @@ pub enum AddressConfig {
}

/// Default seeding policy. Applies when no repository policies for the given repo are found.
-
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
+
#[derive(
+
    Debug, Copy, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, JsonSchema,
+
)]
#[serde(rename_all = "camelCase", tag = "default")]
pub enum DefaultSeedingPolicy {
    /// Allow seeding.
@@ -379,7 +398,7 @@ impl From<DefaultSeedingPolicy> for SeedingPolicy {
}

/// Service configuration.
-
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct Config {
    /// Node alias.
@@ -409,6 +428,7 @@ pub struct Config {
    /// Log level.
    #[serde(default = "defaults::log")]
    #[serde(with = "crate::serde_ext::string")]
+
    #[schemars(schema_with = "crate::serde_ext::string::make_schema")]
    pub log: log::Level,
    /// Whether or not our node should relay messages.
    #[serde(default, deserialize_with = "crate::serde_ext::ok_or_default")]
modified radicle/src/node/policy.rs
@@ -1,6 +1,8 @@
pub mod config;
pub mod store;

+
use schemars::JsonSchema;
+

use std::fmt;
use std::str::FromStr;

@@ -157,7 +159,9 @@ impl TryFrom<&sqlite::Value> for Policy {
}

/// Follow scope of a seeded repository.
-
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
+
#[derive(
+
    Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
+
)]
#[serde(rename_all = "camelCase")]
pub enum Scope {
    /// Seed remotes that are explicitly followed.
modified radicle/src/profile/config.rs
@@ -2,6 +2,8 @@ use std::io::Write;
use std::path::Path;
use std::{fmt, fs, io};

+
use schemars::JsonSchema;
+

use serde::Serialize as _;
use serde_json as json;
use thiserror::Error;
@@ -23,7 +25,7 @@ pub enum ConfigError {
}

/// Local radicle configuration.
-
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct Config {
    /// Public explorer. This is used for generating links.
modified radicle/src/serde_ext.rs
@@ -29,6 +29,12 @@ pub mod string {
            .parse()
            .map_err(de::Error::custom)
    }
+
    /// custom de-serialization needs custom JsonSchema generators
+
    /// source: https://graham.cool/schemars/examples/7-custom_serialization/
+
    use schemars::{schema::Schema, JsonSchema, SchemaGenerator};
+
    pub fn make_schema(generator: &mut SchemaGenerator) -> Schema {
+
        return String::json_schema(generator);
+
    }
}

/// Unlike the default `serde` instances from `localtime`, this encodes and decodes using seconds
@@ -102,6 +108,14 @@ pub mod localtime {

            Ok(LocalDuration::from_secs(seconds))
        }
+
        use schemars::schema::{Schema, SchemaObject};
+
        use schemars::{r#gen::SchemaGenerator, schema_for, JsonSchema};
+

+
        pub fn make_schema(generator: &mut SchemaGenerator) -> Schema {
+
            let mut schema: SchemaObject = <String>::json_schema(generator).into();
+
            schema.format = Some("duration".to_owned());
+
            schema.into()
+
        }
    }
}

@@ -125,7 +139,7 @@ where
mod test {
    use super::*;

-
    use ::localtime::LocalTime;
+
    use localtime::LocalTime;

    #[test]
    fn test_localtime() {
modified radicle/src/web.rs
@@ -1,11 +1,10 @@
-
use std::collections::HashSet;
-

-
use serde::{Deserialize, Serialize};
-

use crate::prelude::RepoId;
+
use schemars::JsonSchema;
+
use serde::{Deserialize, Serialize};
+
use std::collections::HashSet;

/// Web configuration.
-
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
+
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct Config {
    /// Pinned content.
@@ -23,7 +22,7 @@ pub struct Config {

/// Pinned content. This can be used to pin certain content when
/// listing, e.g. pin repositories on a web client.
-
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
+
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct Pinned {
    /// Pinned repositories.