diff options
author | Marcel Müller <m.mueller@ifm.com> | 2022-05-03 18:10:42 +0200 |
---|---|---|
committer | Marcel Müller <m.mueller@ifm.com> | 2022-05-05 15:07:19 +0200 |
commit | ec704c858e39d17ea25d5f5931a68ad044819e84 (patch) | |
tree | a9f74e7305d4c4d045e1256c798878fae700ec4e /crates | |
parent | 8536c84d6e16c3520014e709ca57608bf41b934b (diff) |
Add config printing
Signed-off-by: Marcel Müller <m.mueller@ifm.com>
Diffstat (limited to 'crates')
-rw-r--r-- | crates/core/tedge_api/Cargo.toml | 4 | ||||
-rw-r--r-- | crates/core/tedge_api/src/config.rs | 191 |
2 files changed, 171 insertions, 24 deletions
diff --git a/crates/core/tedge_api/Cargo.toml b/crates/core/tedge_api/Cargo.toml index 78c1d1e5..7fdf5508 100644 --- a/crates/core/tedge_api/Cargo.toml +++ b/crates/core/tedge_api/Cargo.toml @@ -15,6 +15,10 @@ tokio = { version = "1.16.1", features = ["sync", "time"] } tokio-util = "0.7.0" toml = "0.5.8" serde = { version = "1.0.136", features = ["derive"] } +pretty = { version = "0.11.3", features = ["termcolor"] } +termcolor = "1.1.3" +termimad = "0.20.1" +nu-ansi-term = "0.45.1" [dev-dependencies] static_assertions = "1.1.0" diff --git a/crates/core/tedge_api/src/config.rs b/crates/core/tedge_api/src/config.rs index 21c24305..9ae1d532 100644 --- a/crates/core/tedge_api/src/config.rs +++ b/crates/core/tedge_api/src/config.rs @@ -1,35 +1,31 @@ use std::collections::HashMap; +use nu_ansi_term::Color; +use pretty::{Arena, Doc, DocAllocator, Pretty, RefDoc}; use serde::Serialize; +use termimad::MadSkin; use crate::message::MessageType; /// Generic config that represents what kind of config a plugin wishes to accept #[derive(Debug, Serialize)] pub struct Config { + name: String, kind: ConfigKind, - doc: Option<String>, + doc: Option<&'static str>, } impl Config { /// Construct a new generic config explanation #[must_use] - pub fn new(kind: ConfigKind, doc: Option<String>) -> Self { - Self { kind, doc } - } - - /// Construct a new generic config explanation from a single kind - /// - /// This leaves the documentation set to [`None`] - #[must_use] - pub fn from_kind(kind: ConfigKind) -> Self { - Self { kind, doc: None } + pub fn new(name: String, kind: ConfigKind, doc: Option<&'static str>) -> Self { + Self { name, kind, doc } } /// Get a reference to the config's documentation. #[must_use] - pub fn doc(&self) -> Option<&str> { - self.doc.as_deref() + pub fn doc(&self) -> Option<&'static str> { + self.doc } /// Get a reference to the config's kind. @@ -40,10 +36,16 @@ impl Config { /// Set or replace the documentation of this [`Config`] #[must_use] - pub fn with_doc(mut self, doc: Option<String>) -> Self { + pub fn with_doc(mut self, doc: Option<&'static str>) -> Self { self.doc = doc; self } + + /// Get the config's name. + #[must_use] + pub fn name(&self) -> &str { + &self.name + } } /// The specific kind a [`Config`] represents @@ -94,37 +96,104 @@ pub trait AsConfig { impl<T: AsConfig> AsConfig for Vec<T> { fn as_config() -> Config { - Config::from_kind(ConfigKind::Array(Box::new(T::as_config()))) + Config::new( + format!("Array of '{}'s", T::as_config().name()), + ConfigKind::Array(Box::new(T::as_config())), + None, + ) } } impl<V: AsConfig> AsConfig for HashMap<String, V> { fn as_config() -> Config { - Config::from_kind(ConfigKind::HashMap(Box::new(V::as_config()))) + Config::new( + format!("Table of '{}'s", V::as_config().name()), + ConfigKind::HashMap(Box::new(V::as_config())), + None, + ) } } macro_rules! impl_config_kind { - ($kind:expr => $($name:ty),+) => { + ($kind:expr; $name:expr; $doc:expr => $($typ:ty),+) => { $( - impl AsConfig for $name { + impl AsConfig for $typ { fn as_config() -> Config { - Config::from_kind($kind) + Config::new({$name}.into(), $kind, Some($doc)) } } )+ }; } -impl_config_kind!(ConfigKind::Integer => u64, i64); -impl_config_kind!(ConfigKind::Float => f64); -impl_config_kind!(ConfigKind::Bool => bool); -impl_config_kind!(ConfigKind::String => String); +impl_config_kind!(ConfigKind::Integer; "Integer"; "A signed integer with 64 bits" => u64, i64); +impl_config_kind!(ConfigKind::Float; "Float"; "A floating point value with 64 bits" => f64); +impl_config_kind!(ConfigKind::Bool; "Boolean"; "A boolean representing either true or false" => bool); +impl_config_kind!(ConfigKind::String; "String"; "An UTF-8 encoded string of characters" => String); + +/******Pretty Printing of Configs******/ + +impl Config { + /// Get a [`RcDoc`](pretty::RcDoc) which can be used to write the documentation of this + pub fn as_terminal_doc<'a>(&'a self, arena: &'a Arena<'a>) -> RefDoc<'a> { + let mut doc = arena + .nil() + .append(Color::LightBlue.bold().paint(self.name()).to_string()) + .append(arena.hardline()); + + if let Some(conf_doc) = self.doc() { + let skin = MadSkin::default_dark(); + let rendered = skin.text(&conf_doc, None).to_string(); + doc = doc.append(arena.intersperse( + rendered.split("\n").map(|t| { + arena.intersperse( + t.split(char::is_whitespace).map(|t| t.to_string()), + arena.softline(), + ) + }), + arena.hardline(), + )); + } + + match self.kind() { + ConfigKind::Array(conf) => { + doc = doc.append(Pretty::pretty(conf.as_terminal_doc(arena), arena)) + } + ConfigKind::Struct(stc) => { + doc = doc + .append(arena.hardline()) + .append(Color::Blue.paint("[Members]").to_string()) + .append(arena.hardline()) + .append(arena.intersperse( + stc.iter().map(|(member_name, member_conf)| { + arena + .text(Color::Blue.bold().paint(member_name).to_string()) + .append(": ") + .append( + Pretty::pretty(member_conf.as_terminal_doc(arena), arena) + .nest(4), + ) + }), + Doc::hardline(), + )) + } + ConfigKind::HashMap(conf) => { + doc = doc.append(Pretty::pretty(conf.as_terminal_doc(arena), arena)) + } + _ => (), + }; + + doc.into_doc() + } +} #[cfg(test)] mod tests { use std::collections::HashMap; + use nu_ansi_term::Color; + use pretty::Arena; + use crate::config::{AsConfig, Config, ConfigKind}; #[test] @@ -133,7 +202,8 @@ mod tests { Vec::<f64>::as_config(), Config { doc: None, - kind: ConfigKind::Array(x) + kind: ConfigKind::Array(x), + .. } if matches!(x.kind(), ConfigKind::Float) )); @@ -144,4 +214,77 @@ mod tests { matches!(complex_config.kind(), ConfigKind::HashMap(map) if matches!(map.kind(), ConfigKind::Array(arr) if matches!(arr.kind(), ConfigKind::HashMap(inner_map) if matches!(inner_map.kind(), ConfigKind::String)))) ); } + + struct Port(u64); + + impl AsConfig for Port { + fn as_config() -> Config { + Config::new( + String::from("Integer"), + ConfigKind::Integer, + Some("A TCP port number is an integer between 0 and 65535"), + ) + } + } + + struct VHost; + + impl AsConfig for VHost { + fn as_config() -> Config { + Config::new( + String::from("VHost"), + ConfigKind::Struct(HashMap::from([(String::from("name"), String::as_config())])), + Some("A virtual host definition"), + ) + } + } + + #[test] + fn check_terminal_output() { + let arena = Arena::new(); + + let doc = Vec::<String>::as_config(); + let rendered_doc = doc.as_terminal_doc(&arena); + + let mut output = String::new(); + + rendered_doc.render_fmt(80, &mut output).unwrap(); + + println!("{}", output); + println!("-------"); + + let arena = Arena::new(); + + let doc = Config::new( + String::from("ServerConfig"), + ConfigKind::Struct(HashMap::from([ + (String::from("port"), Port::as_config()), + (String::from("interface"), String::as_config()), + (String::from("virtual_hosts"), Vec::<VHost>::as_config()), + (String::from("headers"), HashMap::<String, String>::as_config()), + ])), + Some("Specify how the server should be started\n\n## Note\n\nThis is a reallly really loooooooooooooooooong loooooooooooooooooooong new *line*."), + ); + let rendered_doc = doc.as_terminal_doc(&arena); + + let mut output = String::new(); + + rendered_doc.render_fmt(80, &mut output).unwrap(); + + println!( + "Configuration for {} plugin kinds", + Color::White.bold().paint(doc.name()) + ); + println!( + "{}", + Color::White.dimmed().bold().paint(format!( + "=================={}=============", + std::iter::repeat('=') + .take(doc.name().len()) + .collect::<String>() + )) + ); + println!("{}", output); + println!("-------"); + } } |