cli: `rad config schema` emits JSON Schema

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

The output of `rad config schema` can be used by editors to provide a
smoother editing experience for the configuration file. Discovery
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
This commit is contained in:
n4ch7 2025-04-28 18:35:45 +02:00 committed by Fintan Halpenny
parent 5a2f26eaa7
commit b608a78806
14 changed files with 1066 additions and 63 deletions

857
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -46,11 +46,12 @@ tree-sitter-bash = { version = "0.23.3" }
tree-sitter-go = { version = "0.23.4" } tree-sitter-go = { version = "0.23.4" }
tree-sitter-md = { version = "0.3.2" } tree-sitter-md = { version = "0.3.2" }
zeroize = { version = "1.1" } zeroize = { version = "1.1" }
schemars = { version = "1.0.0-alpha.17" }
[dependencies.radicle] [dependencies.radicle]
version = "0" version = "0"
path = "../radicle" path = "../radicle"
features = ["logger"] features = ["logger", "schemars"]
[dependencies.radicle-cli-test] [dependencies.radicle-cli-test]
version = "0" version = "0"

View File

@ -23,6 +23,7 @@ Usage
rad config init --alias <alias> [<option>...] rad config init --alias <alias> [<option>...]
rad config edit [<option>...] rad config edit [<option>...]
rad config get <key> [<option>...] rad config get <key> [<option>...]
rad config schema [<option>...]
rad config set <key> <value> [<option>...] rad config set <key> <value> [<option>...]
rad config unset <key> [<option>...] rad config unset <key> [<option>...]
rad config push <key> <value> [<option>...] rad config push <key> <value> [<option>...]
@ -43,6 +44,7 @@ enum Operation {
#[default] #[default]
Show, Show,
Get(String), Get(String),
Schema,
Set(String, String), Set(String, String),
Push(String, String), Push(String, String),
Remove(String, String), Remove(String, String),
@ -79,6 +81,7 @@ impl Args for Options {
} }
Value(val) if op.is_none() => match val.to_string_lossy().as_ref() { Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
"show" => op = Some(Operation::Show), "show" => op = Some(Operation::Show),
"schema" => op = Some(Operation::Schema),
"edit" => op = Some(Operation::Edit), "edit" => op = Some(Operation::Edit),
"init" => op = Some(Operation::Init), "init" => op = Some(Operation::Init),
"get" => { "get" => {
@ -140,6 +143,9 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?; let profile = ctx.profile()?;
term::json::to_pretty(&profile.config, path.as_path())?.print(); term::json::to_pretty(&profile.config, path.as_path())?.print();
} }
Operation::Schema => {
term::json::to_pretty(&schemars::schema_for!(Config), path.as_path())?.print();
}
Operation::Get(key) => { Operation::Get(key) => {
let mut temp_config = RawConfig::from_file(&path)?; let mut temp_config = RawConfig::from_file(&path)?;
let key: ConfigPath = key.into(); let key: ConfigPath = key.into();

View File

@ -35,6 +35,7 @@ sqlite = { version = "0.32.0", features = ["bundled"] }
tempfile = { version = "3.3.0" } tempfile = { version = "3.3.0" }
thiserror = { version = "1" } thiserror = { version = "1" }
unicode-normalization = { version = "0.1" } unicode-normalization = { version = "0.1" }
schemars = { version = "1.0.0-alpha.17", optional = true }
[dependencies.chrono] [dependencies.chrono]
version = "0.4.0" version = "0.4.0"
@ -75,6 +76,7 @@ emojis = { version = "0.6" }
pretty_assertions = { version = "1.3.0" } pretty_assertions = { version = "1.3.0" }
qcheck-macros = { version = "1", default-features = false } qcheck-macros = { version = "1", default-features = false }
qcheck = { version = "1", default-features = false } qcheck = { version = "1", default-features = false }
jsonschema = { version = "0.30" }
[dev-dependencies.radicle-crypto] [dev-dependencies.radicle-crypto]
path = "../radicle-crypto" path = "../radicle-crypto"

View File

@ -1,6 +1,11 @@
/// CLI configuration. /// CLI configuration.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[cfg_attr(
feature = "schemars",
derive(schemars::JsonSchema),
schemars(rename = "CliConfig")
)]
pub struct Config { pub struct Config {
/// Whether to show hints or not in the CLI. /// Whether to show hints or not in the CLI.
#[serde(default)] #[serde(default)]

View File

@ -81,8 +81,9 @@ impl std::fmt::Display for ExplorerUrl {
} }
/// A public explorer, eg. `https://app.radicle.xyz`. /// A public explorer, eg. `https://app.radicle.xyz`.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(transparent)] #[serde(transparent)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Explorer(String); pub struct Explorer(String);
impl Default for Explorer { impl Default for Explorer {

View File

@ -20,7 +20,17 @@ pub enum IdError {
/// A repository identifier. /// A repository identifier.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RepoId(git::Oid); #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RepoId(
#[cfg_attr(feature = "schemars", schemars(
with = "String",
description = "A repository identifier. Starts with \"rad:\", followed by a multibase Base58 encoded Git object identifier.",
regex(pattern = r"rad:z[1-9a-km-zA-HJ-NP-Z]+"),
length(min = 5),
example = &"rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5",
))]
git::Oid,
);
impl fmt::Display for RepoId { impl fmt::Display for RepoId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {

View File

@ -23,6 +23,8 @@ pub mod logger;
pub mod node; pub mod node;
pub mod profile; pub mod profile;
pub mod rad; pub mod rad;
#[cfg(feature = "schemars")]
pub(crate) mod schemars_ext;
pub mod serde_ext; pub mod serde_ext;
pub mod sql; pub mod sql;
pub mod storage; pub mod storage;

View File

@ -181,7 +181,7 @@ impl Penalty {
} }
/// Repository sync status for our own refs. /// Repository sync status for our own refs.
#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(tag = "status")] #[serde(tag = "status")]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub enum SyncStatus { pub enum SyncStatus {
@ -221,7 +221,7 @@ impl PartialOrd for SyncStatus {
} }
/// Node user agent. /// Node user agent.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize)]
pub struct UserAgent(String); pub struct UserAgent(String);
impl UserAgent { impl UserAgent {
@ -291,10 +291,25 @@ impl AsRef<str> for UserAgent {
} }
} }
/// Node alias. /// Node alias, i.e. a short and memorable name for it.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")] #[serde(try_from = "String", into = "String")]
pub struct Alias(String); #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Alias(
// To exclude control characters, one might be inclined to use the character
// class `[[:cntrl:]]` which is understood by the `regex` crate.
// However, the patterns in JSON schema must conform to ECMA-262, which does
// not specify the character class.
// Thus, we unfold its definition from <https://www.unicode.org/reports/tr18/#cntrl>,
// which refers to the "general category" named "Cc",
// see <https://unicode.org/reports/tr44/#General_Category_Values>.
// We obtain the two ranges below from <https://www.unicode.org/notes/tn36/Categories.txt>.
#[cfg_attr(
feature = "schemars",
schemars(regex(pattern = r"^[^\x00-\x1F\x7F-\x9F\s]{0,32}$"), length(max = 32))
)]
String,
);
impl Alias { impl Alias {
/// Create a new alias from a string. Panics if the string is not a valid alias. /// Create a new alias from a string. Panics if the string is not a valid alias.
@ -411,7 +426,7 @@ impl TryFrom<&sqlite::Value> for Alias {
} }
/// Options passed to the "connect" node command. /// Options passed to the "connect" node command.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectOptions { pub struct ConnectOptions {
/// Establish a persistent connection. /// Establish a persistent connection.
pub persistent: bool, pub persistent: bool,
@ -500,10 +515,30 @@ impl<T: Serialize> CommandResult<T> {
} }
/// Peer public protocol address. /// Peer public protocol address.
#[derive(Wrapper, WrapperMut, Clone, Eq, PartialEq, Debug, Hash, From, Serialize, Deserialize)] #[derive(Clone, Eq, PartialEq, Debug, Hash, From, Wrapper, WrapperMut, Serialize, Deserialize)]
#[wrapper(Deref, Display, FromStr)] #[wrapper(Deref, Display, FromStr)]
#[wrapper_mut(DerefMut)] #[wrapper_mut(DerefMut)]
pub struct Address(#[serde(with = "crate::serde_ext::string")] NetAddr<HostName>); #[cfg_attr(
feature = "schemars",
derive(schemars::JsonSchema),
schemars(description = "\
An IP address, or a DNS name, or a Tor onion name, followed by the symbol ':', \
followed by a TCP port number.\
")
)]
pub struct Address(
#[serde(with = "crate::serde_ext::string")]
#[cfg_attr(feature = "schemars", schemars(
with = "String",
regex(pattern = r"^.+:((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$"),
extend("examples" = [
"xmrhfasfg5suueegrnc4gsgyi2tyclcy5oz7f5drnrodmdtob6t2ioyd.onion:8776",
"seed.example.com:8776",
"192.0.2.0:31337",
]),
))]
NetAddr<HostName>,
);
impl Address { impl Address {
/// Check whether this address is from the local network. /// Check whether this address is from the local network.
@ -667,7 +702,7 @@ impl Session {
} }
/// A seed for some repository, with metadata about its status. /// A seed for some repository, with metadata about its status.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Seed { pub struct Seed {
/// The Node ID. /// The Node ID.
@ -708,7 +743,7 @@ impl Seed {
} }
} }
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
/// Represents a set of seeds with associated metadata. Uses an RNG /// Represents a set of seeds with associated metadata. Uses an RNG
/// underneath, so every iteration returns a different ordering. /// underneath, so every iteration returns a different ordering.
#[serde(into = "Vec<Seed>", from = "Vec<Seed>")] #[serde(into = "Vec<Seed>", from = "Vec<Seed>")]

View File

@ -5,6 +5,7 @@ use std::{fmt, net};
use cyphernet::addr::PeerAddr; use cyphernet::addr::PeerAddr;
use localtime::LocalDuration; use localtime::LocalDuration;
use serde::{Deserialize, Serialize};
use serde_json as json; use serde_json as json;
use crate::node; use crate::node;
@ -57,8 +58,9 @@ pub mod seeds {
} }
/// Peer-to-peer network. /// Peer-to-peer network.
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum Network { pub enum Network {
#[default] #[default]
Main, Main,
@ -94,16 +96,25 @@ impl Network {
} }
/// Configuration parameters defining attributes of minima and maxima. /// Configuration parameters defining attributes of minima and maxima.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Limits { pub struct Limits {
/// Number of routing table entries before we start pruning. /// Number of routing table entries before we start pruning.
pub routing_max_size: usize, pub routing_max_size: usize,
/// How long to keep a routing table entry before being pruned. /// How long to keep a routing table entry before being pruned.
#[serde(with = "crate::serde_ext::localtime::duration")] #[serde(with = "crate::serde_ext::localtime::duration")]
#[cfg_attr(
feature = "schemars",
schemars(with = "crate::schemars_ext::localtime::LocalDuration")
)]
pub routing_max_age: LocalDuration, pub routing_max_age: LocalDuration,
/// How long to keep a gossip message entry before pruning it. /// How long to keep a gossip message entry before pruning it.
#[serde(with = "crate::serde_ext::localtime::duration")] #[serde(with = "crate::serde_ext::localtime::duration")]
#[cfg_attr(
feature = "schemars",
schemars(with = "crate::schemars_ext::localtime::LocalDuration")
)]
pub gossip_max_age: LocalDuration, pub gossip_max_age: LocalDuration,
/// Maximum number of concurrent fetches per peer connection. /// Maximum number of concurrent fetches per peer connection.
pub fetch_concurrency: usize, pub fetch_concurrency: usize,
@ -138,10 +149,19 @@ impl Default for Limits {
/// Limiter for byte streams. /// Limiter for byte streams.
/// ///
/// Default: 500MiB /// Default: 500MiB
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[serde(into = "String", try_from = "String")] #[serde(into = "String", try_from = "String")]
#[cfg_attr(
feature = "schemars",
derive(schemars::JsonSchema),
schemars(transparent)
)]
pub struct FetchPackSizeLimit { pub struct FetchPackSizeLimit {
#[cfg_attr(
feature = "schemars",
schemars(with = "crate::schemars_ext::bytesize::ByteSize")
)]
limit: bytesize::ByteSize, limit: bytesize::ByteSize,
} }
@ -213,8 +233,9 @@ impl Default for FetchPackSizeLimit {
} }
/// Connection limits. /// Connection limits.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ConnectionLimits { pub struct ConnectionLimits {
/// Max inbound connections. /// Max inbound connections.
pub inbound: usize, pub inbound: usize,
@ -232,16 +253,18 @@ impl Default for ConnectionLimits {
} }
/// Rate limts for a single connection. /// Rate limts for a single connection.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RateLimit { pub struct RateLimit {
pub fill_rate: f64, pub fill_rate: f64,
pub capacity: usize, pub capacity: usize,
} }
/// Rate limits for inbound and outbound connections. /// Rate limits for inbound and outbound connections.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RateLimits { pub struct RateLimits {
pub inbound: RateLimit, pub inbound: RateLimit,
pub outbound: RateLimit, pub outbound: RateLimit,
@ -263,9 +286,30 @@ impl Default for RateLimits {
} }
/// Full address used to connect to a remote node. /// Full address used to connect to a remote node.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)] #[cfg_attr(
pub struct ConnectAddress(#[serde(with = "crate::serde_ext::string")] PeerAddr<NodeId, Address>); feature = "schemars",
derive(schemars::JsonSchema),
schemars(description = "\
A node address to connect to. Format: An Ed25519 public key in multibase encoding, \
followed by the symbol '@', followed by an IP address, or a DNS name, or a Tor onion \
name, followed by the symbol ':', followed by a TCP port number.\
")
)]
pub struct ConnectAddress(
#[serde(with = "crate::serde_ext::string")]
#[schemars(
with = "String",
regex(pattern = r"^.+@.+:((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$"),
extend("examples" = [
"z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7@seed.radicle.garden:8776",
"z6MkvUJtYD9dHDJfpevWRT98mzDDpdAtmUjwyDSkyqksUr7C@xmrhfasfg5suueegrnc4gsgyi2tyclcy5oz7f5drnrodmdtob6t2ioyd.onion:8776",
"z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi@seed.example.com:8776",
"z6MkkfM3tPXNPrPevKr3uSiQtHPuwnNhu2yUVjgd2jXVsVz5@192.0.2.0:31337",
]),
)]
PeerAddr<NodeId, Address>,
);
impl From<PeerAddr<NodeId, Address>> for ConnectAddress { impl From<PeerAddr<NodeId, Address>> for ConnectAddress {
fn from(value: PeerAddr<NodeId, Address>) -> Self { fn from(value: PeerAddr<NodeId, Address>) -> Self {
@ -300,8 +344,9 @@ impl Deref for ConnectAddress {
} }
/// Peer configuration. /// Peer configuration.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "type")] #[serde(rename_all = "camelCase", tag = "type")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum PeerConfig { pub enum PeerConfig {
/// Static peer set. Connect to the configured peers and maintain the connections. /// Static peer set. Connect to the configured peers and maintain the connections.
Static, Static,
@ -316,8 +361,9 @@ impl Default for PeerConfig {
} }
/// Relay configuration. /// Relay configuration.
#[derive(Debug, Copy, Clone, Default, serde::Serialize, serde::Deserialize)] #[derive(Debug, Copy, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum Relay { pub enum Relay {
/// Always relay messages. /// Always relay messages.
Always, Always,
@ -329,8 +375,9 @@ pub enum Relay {
} }
/// Proxy configuration. /// Proxy configuration.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "mode")] #[serde(rename_all = "camelCase", tag = "mode")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum AddressConfig { pub enum AddressConfig {
/// Proxy connections to this address type. /// Proxy connections to this address type.
Proxy { Proxy {
@ -343,8 +390,9 @@ pub enum AddressConfig {
} }
/// Default seeding policy. Applies when no repository policies for the given repo are found. /// 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, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "default")] #[serde(rename_all = "camelCase", tag = "default")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum DefaultSeedingPolicy { pub enum DefaultSeedingPolicy {
/// Allow seeding. /// Allow seeding.
Allow { Allow {
@ -379,13 +427,19 @@ impl From<DefaultSeedingPolicy> for SeedingPolicy {
} }
/// Service configuration. /// Service configuration.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[cfg_attr(
feature = "schemars",
derive(schemars::JsonSchema),
schemars(rename = "NodeConfig")
)]
pub struct Config { pub struct Config {
/// Node alias. /// Node alias.
pub alias: Alias, pub alias: Alias,
/// Address to listen on. /// Socket address (a combination of IPv4 or IPv6 address and TCP port) to listen on.
#[serde(default)] #[serde(default)]
#[cfg_attr(feature = "schemars", schemars(example = &"127.0.0.1:8776"))]
pub listen: Vec<net::SocketAddr>, pub listen: Vec<net::SocketAddr>,
/// Peer configuration. /// Peer configuration.
#[serde(default)] #[serde(default)]
@ -409,6 +463,10 @@ pub struct Config {
/// Log level. /// Log level.
#[serde(default = "defaults::log")] #[serde(default = "defaults::log")]
#[serde(with = "crate::serde_ext::string")] #[serde(with = "crate::serde_ext::string")]
#[cfg_attr(
feature = "schemars",
schemars(with = "crate::schemars_ext::log::Level")
)]
pub log: log::Level, pub log: log::Level,
/// Whether or not our node should relay messages. /// Whether or not our node should relay messages.
#[serde(default, deserialize_with = "crate::serde_ext::ok_or_default")] #[serde(default, deserialize_with = "crate::serde_ext::ok_or_default")]

View File

@ -35,7 +35,7 @@ pub struct FollowPolicy {
} }
/// Seeding policy of a node or repo. /// Seeding policy of a node or repo.
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "policy")] #[serde(rename_all = "camelCase", tag = "policy")]
pub enum SeedingPolicy { pub enum SeedingPolicy {
/// Allow seeding. /// Allow seeding.
@ -159,6 +159,7 @@ impl TryFrom<&sqlite::Value> for Policy {
/// Follow scope of a seeded repository. /// 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)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum Scope { pub enum Scope {
/// Seed remotes that are explicitly followed. /// Seed remotes that are explicitly followed.
Followed, Followed,

View File

@ -25,6 +25,7 @@ pub enum ConfigError {
/// Local radicle configuration. /// Local radicle configuration.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Config { pub struct Config {
/// Public explorer. This is used for generating links. /// Public explorer. This is used for generating links.
#[serde(default)] #[serde(default)]
@ -360,3 +361,19 @@ impl From<String> for ConfigPath {
ConfigPath(parts) ConfigPath(parts)
} }
} }
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
#[test]
fn schema() {
use super::Config;
use crate::prelude::Alias;
use serde_json::to_value;
let schema = to_value(schemars::schema_for!(Config)).unwrap();
let config = to_value(Config::new(Alias::new("schema"))).unwrap();
jsonschema::validate(&schema, &config)
.expect("generated configuration should validate under generated JSON Schema");
}
}

View File

@ -0,0 +1,59 @@
//! This module contains auxiliary definitions for generating JSONSchemas.
//! See <https://graham.cool/schemars/examples/5-remote_derive/>.
#![allow(dead_code)]
use schemars::JsonSchema;
pub(crate) mod log {
use super::*;
/// See [`::log::Level`]
#[derive(JsonSchema)]
#[schemars(
remote = "log::Level",
description = "A log level.",
rename_all = "UPPERCASE"
)]
pub(crate) enum Level {
/// Designates very serious errors.
Error,
/// Designates hazardous situations.
Warn,
/// Designates useful information.
Info,
/// Designates lower priority information.
Debug,
/// Designates very low priority, often extremely verbose, information.
Trace,
}
}
pub(crate) mod bytesize {
use super::*;
/// See [`::bytesize::ByteSize`] as well as [`::bytesize::parse`].
/// Note that the pattern here is a little more restrictive than
/// the actual parsing logic, as it enforces particular casing and whitespace.
/// However, the regular expression is easier to read.
#[derive(JsonSchema)]
#[schemars(
remote = "bytesize::ByteSize",
description = "Byte quantities using unit prefixes according to SI or ISO/IEC 80000-13.",
extend("examples" = ["7 G", "50.3 TiB", "200 B", "4 Ki", "10 MB"]),
)]
pub(crate) struct ByteSize(
#[schemars(regex(pattern = r"^\d+(\.\d+)? ((K|M|G|T|P)i?B?|B)$"))] String,
);
}
pub(crate) mod localtime {
use super::*;
/// See [`::localtime::LocalDuration`]
#[derive(JsonSchema)]
#[schemars(
remote = "localtime::LocalDuration",
description = "A time duration measured locally in milliseconds."
)]
pub(crate) struct LocalDuration(u64);
}

View File

@ -1,29 +1,36 @@
use std::collections::HashSet;
use serde::{Deserialize, Serialize};
use crate::prelude::RepoId; use crate::prelude::RepoId;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
/// Web configuration. /// Web configuration.
#[derive(Clone, Debug, Default, Serialize, Deserialize)] #[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[cfg_attr(
feature = "schemars",
derive(schemars::JsonSchema),
schemars(rename = "WebConfig")
)]
pub struct Config { pub struct Config {
/// Pinned content. /// Pinned content.
pub pinned: Pinned, pub pinned: Pinned,
/// URL pointing to an image used in the header of a node page. /// URL pointing to an image used in the header of a node page.
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(url))]
pub banner_url: Option<String>, pub banner_url: Option<String>,
/// URL pointing to an image used as the node avatar. /// URL pointing to an image used as the node avatar.
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(url))]
pub avatar_url: Option<String>, pub avatar_url: Option<String>,
/// Node description. /// Node description.
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(url))]
pub description: Option<String>, pub description: Option<String>,
} }
/// Pinned content. This can be used to pin certain content when /// Pinned content. This can be used to pin certain content when
/// listing, e.g. pin repositories on a web client. /// 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")] #[serde(rename_all = "camelCase")]
pub struct Pinned { pub struct Pinned {
/// Pinned repositories. /// Pinned repositories.