diff --git a/src/config_spec.rs b/src/config_spec.rs index 6487df6..43e7e7f 100644 --- a/src/config_spec.rs +++ b/src/config_spec.rs @@ -1,3 +1,4 @@ +use crate::is_dry_run; use crate::key_value::KeyValueStore as KV; use std::error::Error; use std::io::{self, Write}; @@ -13,6 +14,127 @@ const ANSI_FIELD: &str = "\x1b[1;33m"; const ANSI_NOTICE: &str = "\x1b[33m"; const ANSI_RESET: &str = "\x1b[0m"; +/* ***** config structure definition ***** */ + +/// Describes a field in the configuration. +pub enum CfgField<'a> { + /// Silently writes a default to the DB if absent, never prompts for changes. + Silent { key: &'a str, default: &'a str }, + /// Writes a default, asks for changes. + Default { key: &'a str, default: &'a str, description: &'a str }, + /// No default, asks for value, but can stay empty. + Optional { key: &'a str, description: &'a str }, + /// Empty by default, required, will prompt without echo. + Password { key: &'a str, description: &'a str }, + /// Empty by default, can be user-provided, or will be generated randomly. + RandomId { + key: &'a str, + generator: fn(config: &KV, is_dry_run: bool) -> Result>, + generator_description: &'a str, + description: &'a str, + }, +} + +/// A group of related config fields. The final key of an inner value will be +/// `{group.name}-{key}`. +pub struct CfgGroup<'a> { + pub name: &'a str, + pub description: &'a str, + pub fields: &'a [CfgField<'a>], +} + +/// A description of the entire config, as a collection of named groups. +/// +/// Used both to populate defaults and to interactively adjust the configuration. +pub struct CfgSpec<'a> { + pub groups: &'a [CfgGroup<'a>], +} + +impl<'a> CfgField<'a> { + /// Silent fields don't get prompted or shown. + fn is_silent(&self) -> bool { + matches!(self, CfgField::Silent { .. }) + } + + /// Password fields will be censored when displayed. + fn is_password(&self) -> bool { + matches!(self, CfgField::Password { .. }) + } + + /// Optional fields are allowed to be (and stay) empty. + fn is_optional(&self) -> bool { + matches!(self, CfgField::Optional { .. }) + } + + /// Reports if the field needs changing or is ok as-is. + /// + /// (Because of the default population at boot, Silent and Default must + /// *necessarily* always have a value. Optional is optional. Only + /// Password and RandomId might need actions if they are missing.) + fn is_action_required(&self, config: &KV, grpname: &str) -> bool { + if matches!( + self, + CfgField::Silent { .. } | CfgField::Default { .. } | CfgField::Optional { .. } + ) { + false + } else { + config.get(self.full_key(grpname).as_str()).ok().is_none() + } + } + + /// Gets `key`, which is the local part of the field name. (`.full_key` is used for the DB.) + fn key(&self) -> String { + match self { + CfgField::Silent { key, .. } => key, + CfgField::Default { key, .. } => key, + CfgField::Optional { key, .. } => key, + CfgField::Password { key, .. } => key, + CfgField::RandomId { key, .. } => key, + } + .to_string() + } + + /// Full field name / key used in the DB (currently `{grpname}-{fieldname}`.) + fn full_key(&self, grpname: &str) -> String { + format!("{}-{}", grpname, self.key()) + } + + /// Gets `description` or "(none)". + fn description(&self) -> String { + match self { + CfgField::Silent { .. } => "(none)", + CfgField::Default { description, .. } => description, + CfgField::Optional { description, .. } => description, + CfgField::Password { description, .. } => description, + CfgField::RandomId { description, .. } => description, + } + .to_string() + } + + /// Gets a description of the default value if one exists. + /// (Either the value itself, or a description of the generator making a default value.) + fn default_description(&self) -> Option { + match self { + CfgField::Silent { default, .. } => Some(default.to_string()), + CfgField::Default { default, .. } => Some(default.to_string()), + CfgField::RandomId { generator_description, .. } => { + Some(format!("{}{}{}", '(', generator_description, ')')) + }, + _ => None, + } + } + + /// Gets the actual default value, which may involve running a generator. + fn default_value(&self, config: &KV) -> Result, Box> { + match self { + CfgField::Silent { default, .. } => Ok(Some(default.to_string())), + CfgField::Default { default, .. } => Ok(Some(default.to_string())), + CfgField::RandomId { generator, .. } => generator(config, is_dry_run()).map(Some), + _ => Ok(None), + } + } +} + /// Ensure all fields with known default values exist. pub fn populate_defaults(spec: &CfgSpec, config: &KV) { for group in spec.groups { @@ -119,7 +241,7 @@ fn check_field( } }, 'R' => { - match field.default_value()? { + match field.default_value(&config)? { Some(value) => { config.set(&key, &value)?; }, @@ -246,124 +368,3 @@ fn prompt_password() -> String { println!(""); pass } - -/* ***** config structure definition ***** */ - -/// Describes a field in the configuration. -pub enum CfgField<'a> { - /// Silently writes a default to the DB if absent, never prompts for changes. - Silent { key: &'a str, default: &'a str }, - /// Writes a default, asks for changes. - Default { key: &'a str, default: &'a str, description: &'a str }, - /// No default, asks for value, but can stay empty. - Optional { key: &'a str, description: &'a str }, - /// Empty by default, required, will prompt without echo. - Password { key: &'a str, description: &'a str }, - /// Empty by default, can be user-provided, or will be generated randomly. - RandomId { - key: &'a str, - generator: fn() -> Result>, - generator_description: &'a str, - description: &'a str, - }, -} - -/// A group of related config fields. The final key of an inner value will be -/// `{group.name}-{key}`. -pub struct CfgGroup<'a> { - pub name: &'a str, - pub description: &'a str, - pub fields: &'a [CfgField<'a>], -} - -/// A description of the entire config, as a collection of named groups. -/// -/// Used both to populate defaults and to interactively adjust the configuration. -pub struct CfgSpec<'a> { - pub groups: &'a [CfgGroup<'a>], -} - -impl<'a> CfgField<'a> { - /// Silent fields don't get prompted or shown. - fn is_silent(&self) -> bool { - matches!(self, CfgField::Silent { .. }) - } - - /// Password fields will be censored when displayed. - fn is_password(&self) -> bool { - matches!(self, CfgField::Password { .. }) - } - - /// Optional fields are allowed to be (and stay) empty. - fn is_optional(&self) -> bool { - matches!(self, CfgField::Optional { .. }) - } - - /// Reports if the field needs changing or is ok as-is. - /// - /// (Because of the default population at boot, Silent and Default must - /// *necessarily* always have a value. Optional is optional. Only - /// Password and RandomId might need actions if they are missing.) - fn is_action_required(&self, config: &KV, grpname: &str) -> bool { - if matches!( - self, - CfgField::Silent { .. } | CfgField::Default { .. } | CfgField::Optional { .. } - ) { - false - } else { - config.get(self.full_key(grpname).as_str()).ok().is_none() - } - } - - /// Gets `key`, which is the local part of the field name. (`.full_key` is used for the DB.) - fn key(&self) -> String { - match self { - CfgField::Silent { key, .. } => key, - CfgField::Default { key, .. } => key, - CfgField::Optional { key, .. } => key, - CfgField::Password { key, .. } => key, - CfgField::RandomId { key, .. } => key, - } - .to_string() - } - - /// Full field name / key used in the DB (currently `{grpname}-{fieldname}`.) - fn full_key(&self, grpname: &str) -> String { - format!("{}-{}", grpname, self.key()) - } - - /// Gets `description` or "(none)". - fn description(&self) -> String { - match self { - CfgField::Silent { .. } => "(none)", - CfgField::Default { description, .. } => description, - CfgField::Optional { description, .. } => description, - CfgField::Password { description, .. } => description, - CfgField::RandomId { description, .. } => description, - } - .to_string() - } - - /// Gets a description of the default value if one exists. - /// (Either the value itself, or a description of the generator making a default value.) - fn default_description(&self) -> Option { - match self { - CfgField::Silent { default, .. } => Some(default.to_string()), - CfgField::Default { default, .. } => Some(default.to_string()), - CfgField::RandomId { generator_description, .. } => { - Some(format!("{}{}{}", '(', generator_description, ')')) - }, - _ => None, - } - } - - /// Gets the actual default value, which may involve running a generator. - fn default_value(&self) -> Result, Box> { - match self { - CfgField::Silent { default, .. } => Ok(Some(default.to_string())), - CfgField::Default { default, .. } => Ok(Some(default.to_string())), - CfgField::RandomId { generator, .. } => generator().map(Some), - _ => Ok(None), - } - } -} diff --git a/src/hedgedoc.rs b/src/hedgedoc.rs index 1ff94aa..32e4089 100644 --- a/src/hedgedoc.rs +++ b/src/hedgedoc.rs @@ -17,9 +17,18 @@ pub const CONFIG: CfgGroup<'static> = CfgGroup { default: "plenum-template", description: "Name of the pad containing the template to use.", }, - // TODO: make these generators? - CfgField::Optional { key: "last-id", description: "ID of last plenum's pad." }, - CfgField::Optional { key: "next-id", description: "ID of next plenum's pad." }, + CfgField::RandomId { + key: "last-id", + generator: make_pad_id, + generator_description: "Makes a new pad that's completely empty.", + description: "ID of last plenum's pad.", + }, + CfgField::RandomId { + key: "next-id", + generator: make_pad_id, + generator_description: "Makes a new pad that's completely empty.", + description: "ID of next plenum's pad.", + }, ], }; @@ -55,7 +64,9 @@ impl HedgeDoc { } pub fn create_pad(&self) -> Result> { - if self.is_dry_run { todo!("NYI: sane dry-run behavior") } + if self.is_dry_run { + todo!("NYI: sane dry-run behavior") + } let res = self.do_request(&format!("{}/new", self.server_url)).unwrap(); if res.status().is_success() { Ok(self.get_id_from_response(res)) @@ -65,7 +76,9 @@ impl HedgeDoc { } pub fn import_note(&self, id: Option<&str>, content: String) -> Result> { - if self.is_dry_run { todo!("NYI: sane dry-run behavior") } + if self.is_dry_run { + todo!("NYI: sane dry-run behavior") + } let url = match id { Some(id) => self.format_url(&format!("new/{id}")), None => self.format_url("new"), @@ -81,3 +94,8 @@ impl HedgeDoc { } } } + +/// For the config, make a new pad ID (by actually making a pad.) +fn make_pad_id(config: &crate::KV, is_dry_run: bool) -> Result> { + HedgeDoc::new(&config.get("hedgedoc-server-url").unwrap(), is_dry_run).create_pad() +}