Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

automatic generation of json schema for plugin config #537

Merged
merged 7 commits into from
Aug 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
528 changes: 517 additions & 11 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ home = "0.5.4"
http-types = "2.12.0"
humantime = "2.1.0"
json5 = "0.4.1"
jsonschema = "0.17.1"
keyed-set = "0.4.4"
lazy_static = "1.4.0"
libc = "0.2.139"
Expand Down Expand Up @@ -124,6 +125,7 @@ rustc_version = "0.4.0"
rustls = { version = "0.21.5", features = ["dangerous_configuration"] }
rustls-native-certs = "0.6.2"
rustls-pemfile = "1.0.2"
schemars = "0.8.12"
serde = { version = "1.0.154", default-features = false, features = [
"derive",
] } # Default features are disabled due to usage in no_std crates
Expand Down
3 changes: 2 additions & 1 deletion commons/zenoh-keyexpr/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ description = "Internal crate for zenoh."

[features]
default = ["std"]
std = ["zenoh-result/std"]
std = ["zenoh-result/std", "dep:schemars"]

[dependencies]
keyed-set = { workspace = true }
rand = { workspace = true, features = ["alloc", "getrandom"] }
schemars = { workspace = true, optional = true }
serde = { workspace = true, features = ["alloc"] }
token-cell = { workspace = true }
zenoh-result = { workspace = true }
Expand Down
1 change: 1 addition & 0 deletions commons/zenoh-keyexpr/src/key_expr/owned.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use core::{
///
/// See [`keyexpr`](super::borrowed::keyexpr).
#[derive(Clone, PartialEq, Eq, Hash, serde::Deserialize)]
#[cfg_attr(feature = "std", derive(schemars::JsonSchema))]
#[serde(try_from = "String")]
pub struct OwnedKeyExpr(pub(crate) Arc<str>);
impl serde::Serialize for OwnedKeyExpr {
Expand Down
1 change: 1 addition & 0 deletions plugins/zenoh-backend-traits/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ serde_json = { workspace = true }
zenoh = { workspace = true }
zenoh-result = { workspace = true }
zenoh-util = { workspace = true }
schemars = { workspace = true }
13 changes: 8 additions & 5 deletions plugins/zenoh-backend-traits/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,27 @@
// ZettaScale Zenoh Team, <[email protected]>
//
use derive_more::{AsMut, AsRef};
use schemars::JsonSchema;
use serde_json::{Map, Value};
use std::convert::TryFrom;
use std::time::Duration;
use zenoh::{key_expr::keyexpr, prelude::OwnedKeyExpr, Result as ZResult};
use zenoh_result::{bail, zerror, Error};

#[derive(Debug, Clone, AsMut, AsRef)]
#[derive(JsonSchema, Debug, Clone, AsMut, AsRef)]
pub struct PluginConfig {
pub name: String,
pub required: bool,
pub backend_search_dirs: Option<Vec<String>>,
#[schemars(with = "Map<String, Value>")]
pub volumes: Vec<VolumeConfig>,
#[schemars(with = "Map<String, Value>")]
pub storages: Vec<StorageConfig>,
#[as_ref]
#[as_mut]
pub rest: Map<String, Value>,
}
#[derive(Debug, Clone, AsMut, AsRef)]
#[derive(JsonSchema, Debug, Clone, AsMut, AsRef)]
pub struct VolumeConfig {
pub name: String,
pub backend: Option<String>,
Expand All @@ -39,7 +42,7 @@ pub struct VolumeConfig {
#[as_mut]
pub rest: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(JsonSchema, Debug, Clone, PartialEq, Eq)]
pub struct StorageConfig {
pub name: String,
pub key_expr: OwnedKeyExpr,
Expand All @@ -52,7 +55,7 @@ pub struct StorageConfig {
pub replica_config: Option<ReplicaConfig>,
}
// Note: All parameters should be same for replicas, else will result on huge overhead
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(JsonSchema, Debug, Clone, PartialEq, Eq)]
pub struct ReplicaConfig {
pub publication_interval: Duration,
pub propagation_delay: Duration,
Expand All @@ -78,7 +81,7 @@ impl Default for ReplicaConfig {
}

// The configuration for periodic garbage collection of metadata in storage manager
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(JsonSchema, Debug, Clone, PartialEq, Eq)]
pub struct GarbageCollectionConfig {
// The duration between two garbage collection events
// The garbage collection will be scheduled as a periodic event with this period
Expand Down
5 changes: 5 additions & 0 deletions plugins/zenoh-plugin-rest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ git-version = { workspace = true }
http-types = { workspace = true }
lazy_static = { workspace = true }
log = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true, features = ["default"] }
serde_json = { workspace = true }
tide = { workspace = true }
Expand All @@ -54,6 +55,10 @@ zenoh-util = { workspace = true }

[build-dependencies]
rustc_version = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true, features = ["default"] }
serde_json = { workspace = true }
jsonschema = { workspace = true }

[[example]]
name = "z_serve_sse"
Expand Down
24 changes: 24 additions & 0 deletions plugins/zenoh-plugin-rest/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,35 @@
// Contributors:
// ZettaScale Zenoh Team, <[email protected]>
//
use schemars::schema_for;

use crate::config::Config;

#[path = "src/config.rs"]
mod config;

fn main() {
// Add rustc version to zenohd
let version_meta = rustc_version::version_meta().unwrap();
println!(
"cargo:rustc-env=RUSTC_VERSION={}",
version_meta.short_version_string
);
// Generate config schema
let schema = schema_for!(Config);
std::fs::write(
"config_schema.json5",
serde_json::to_string_pretty(&schema).unwrap(),
)
.unwrap();
// Check that the example config matches the schema
let schema = std::fs::read_to_string("config_schema.json5").unwrap();
let schema: serde_json::Value = serde_json::from_str(&schema).unwrap();
let schema = jsonschema::JSONSchema::compile(&schema).unwrap();
let config = std::fs::read_to_string("config.json5").unwrap();
let config: serde_json::Value = serde_json::from_str(&config).unwrap();
if let Err(es) = schema.validate(&config) {
let es = es.map(|e| format!("{}", e)).collect::<Vec<_>>().join("\n");
panic!("config.json5 schema validation error: {}", es);
};
}
3 changes: 3 additions & 0 deletions plugins/zenoh-plugin-rest/config.json5
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"http_port": "8080"
}
26 changes: 26 additions & 0 deletions plugins/zenoh-plugin-rest/config_schema.json5
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Config",
"type": "object",
"required": [
"http_port"
],
"properties": {
"__path__": {
"type": [
"string",
"null"
]
},
"__required__": {
"type": [
"boolean",
"null"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have expected {"type": "boolean", "required": false}. Is this equivalent, or is the _required_ key now mandatory, but nullable?

Copy link
Contributor Author

@milyin milyin Aug 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to check json schema standard, but at least example file without __required__ field passes the check.
I've added additional verification of sample config file against generated schema. Seems that serde is much less strict than formal schema validation. Sometimes it seems completely wrong: schema requires volumes and storages to be vectors, but our config sample shows them as objects:

pub struct PluginConfig {
    pub name: String,
    pub required: bool,
    pub backend_search_dirs: Option<Vec<String>>,
    pub volumes: Vec<VolumeConfig>,
    pub storages: Vec<StorageConfig>,
...

]
},
"http_port": {
"type": "string"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated schema here is incomplete: a number would also be accepted. Not sure it that's an issue for our purpose though

Copy link
Contributor Author

@milyin milyin Aug 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This goes directly from definition of port field in zenoh-plugin-rest/config.rs. If we want to correct it, we should fix this type

pub struct Config {
    #[serde(deserialize_with = "deserialize_http_port")]
    pub http_port: String,
    __path__: Option<String>,
    __required__: Option<bool>,
}

}
},
"additionalProperties": false
}
3 changes: 2 additions & 1 deletion plugins/zenoh-plugin-rest/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@
// Contributors:
// ZettaScale Zenoh Team, <[email protected]>
//
use schemars::JsonSchema;
use serde::de::{Unexpected, Visitor};
use serde::{de, Deserialize, Deserializer};
use std::fmt;

const DEFAULT_HTTP_INTERFACE: &str = "[::]";

#[derive(Deserialize, serde::Serialize, Clone, Debug)]
#[derive(JsonSchema, Deserialize, serde::Serialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
pub struct Config {
#[serde(deserialize_with = "deserialize_http_port")]
Expand Down
5 changes: 5 additions & 0 deletions plugins/zenoh-plugin-storage-manager/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ zenoh_backend_traits = { workspace = true }

[build-dependencies]
rustc_version = { workspace = true }
zenoh_backend_traits = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true, features = ["default"] }
serde_json = { workspace = true }
jsonschema = { workspace = true }

[dev-dependencies]
async-global-executor = { workspace = true }
Expand Down
20 changes: 20 additions & 0 deletions plugins/zenoh-plugin-storage-manager/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,31 @@
// Contributors:
// ZettaScale Zenoh Team, <[email protected]>
//
use schemars::schema_for;
use zenoh_backend_traits::config::PluginConfig;

fn main() {
// Add rustc version to zenohd
let version_meta = rustc_version::version_meta().unwrap();
println!(
"cargo:rustc-env=RUSTC_VERSION={}",
version_meta.short_version_string
);
// Generate default config schema
let schema = schema_for!(PluginConfig);
std::fs::write(
"config_schema.json5",
serde_json::to_string_pretty(&schema).unwrap(),
)
.unwrap();
// Check that the example config matches the schema
let schema = std::fs::read_to_string("config_schema.json5").unwrap();
let schema: serde_json::Value = serde_json::from_str(&schema).unwrap();
let schema = jsonschema::JSONSchema::compile(&schema).unwrap();
let config = std::fs::read_to_string("config.json5").unwrap();
let config: serde_json::Value = serde_json::from_str(&config).unwrap();
if let Err(es) = schema.validate(&config) {
let es = es.map(|e| format!("{}", e)).collect::<Vec<_>>().join("\n");
panic!("config.json5 schema validation error: {}", es);
};
}
7 changes: 7 additions & 0 deletions plugins/zenoh-plugin-storage-manager/config.json5
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"volumes": {},
"storages": {},
"name" : "test",
"required" : true,
"rest": {}
}
41 changes: 41 additions & 0 deletions plugins/zenoh-plugin-storage-manager/config_schema.json5
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "PluginConfig",
"type": "object",
"required": [
"name",
"required",
"rest",
"storages",
"volumes"
],
"properties": {
"backend_search_dirs": {
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
},
"name": {
"type": "string"
},
"required": {
"type": "boolean"
},
"rest": {
"type": "object",
"additionalProperties": true
},
"storages": {
"type": "object",
"additionalProperties": true
},
"volumes": {
"type": "object",
"additionalProperties": true
}
}
}