oxedyne/fe2o3/fe2o3_syntax/src/core.rs
8.0 KiB, 37 runs
created by r1870400018:1047, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | use crate::{ |
| 2 | arg::{ |
| 3 | Arg, |
| 4 | ArgConfig, |
| 5 | }, |
| 6 | cmd::Cmd, |
| 7 | help::Topic, |
| 8 | key::Key, |
| 9 | val::Val, |
| 10 | }; |
| 11 | |
| 12 | use oxedyne_fe2o3_core::{ |
| 13 | prelude::*, |
| 14 | map::{ |
| 15 | Recursive, |
| 16 | MapRec, |
| 17 | }, |
| 18 | }; |
| 19 | use oxedyne_fe2o3_jdat::version::SemVer; |
| 20 | |
| 21 | use std::{ |
| 22 | collections::BTreeMap, |
| 23 | fmt, |
| 24 | sync::Arc, |
| 25 | }; |
| 26 | |
| 27 | |
| 28 | #[derive(Clone, Debug, Default)] |
| 29 | pub struct Syntax { |
| 30 | pub cfg: SyntaxConfig, |
| 31 | pub args: BTreeMap<Key, Recursive<Key, Arg>>, |
| 32 | pub cmds: BTreeMap<Key, Recursive<Key, Cmd>>, |
| 33 | // Binary |
| 34 | pub next_arg_id: u16, |
| 35 | pub next_cmd_id: u16, |
| 36 | } |
| 37 | |
| 38 | #[derive(Clone, Debug)] |
| 39 | pub struct SyntaxPrefs { |
| 40 | pub arg_hyph1_pfx: String, |
| 41 | pub arg_hyph2_pfx: String, |
| 42 | } |
| 43 | |
| 44 | impl Default for SyntaxPrefs { |
| 45 | fn default() -> Self { |
| 46 | Self { |
| 47 | arg_hyph1_pfx: fmt!("-"), |
| 48 | arg_hyph2_pfx: fmt!("--"), |
| 49 | } |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | #[derive(Clone, Debug, Default)] |
| 54 | pub struct SyntaxConfig { |
| 55 | pub name: String, |
| 56 | pub ver: SemVer, |
| 57 | pub vals: Vec<Val>, // Expected message values. |
| 58 | pub rargs: Vec<String>, // Required arguments. |
| 59 | pub cmds: BTreeMap<Key, Recursive<Key, Cmd>>, |
| 60 | pub one_cmd:bool, // A message names exactly one command. |
| 61 | // CLI |
| 62 | pub author: Option<String>, |
| 63 | pub about: Option<String>, |
| 64 | pub width: usize, // text width for screen output |
| 65 | pub topics: Vec<Topic>, // Named prose pages for help. |
| 66 | pub footer: Option<String>, // Prose closing the top help page. |
| 67 | // Customisation. |
| 68 | pub prefs: SyntaxPrefs, |
| 69 | } |
| 70 | |
| 71 | impl From<SyntaxConfig> for Syntax { |
| 72 | fn from(cfg: SyntaxConfig) -> Self { |
| 73 | Self { |
| 74 | cfg: cfg, |
| 75 | ..Default::default() |
| 76 | } |
| 77 | } |
| 78 | } |
| 79 | |
| 80 | impl Syntax { |
| 81 | |
| 82 | pub const HELP_COL1: usize = 5; |
| 83 | pub const HELP_COL2: usize = 15; |
| 84 | pub const HELP_COL4: usize = 50; |
| 85 | |
| 86 | /// Its ok for the `Syntax` name to contain separator characters such as spaces. |
| 87 | pub fn new<S: Into<String>>(name: S) -> Self { |
| 88 | let cfg = SyntaxConfig { |
| 89 | name: name.into(), |
| 90 | width: 80, |
| 91 | ..Default::default() |
| 92 | }; |
| 93 | Self { |
| 94 | cfg: cfg, |
| 95 | ..Default::default() |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | pub fn config(&self) -> &SyntaxConfig { &self.cfg } |
| 100 | |
| 101 | pub fn inc_counter(counter: u16, desc: String) -> Outcome<u16> { |
| 102 | match counter.checked_add(1) { |
| 103 | Some(i) => Ok(i), |
| 104 | None => { |
| 105 | return Err(err!( |
| 106 | "The id counter for {} has reached its upper limit of {}", desc, u16::MAX; |
| 107 | Counter, Overflow)); |
| 108 | }, |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | pub fn add_arg(mut self, mut a: Arg) -> Outcome<Self> { |
| 113 | a.id = self.next_arg_id; |
| 114 | self.next_arg_id = res!(Syntax::inc_counter( |
| 115 | self.next_arg_id, |
| 116 | fmt!("syntax '{}' arguments", self), |
| 117 | )); |
| 118 | res!(a.attach_arg( |
| 119 | &mut self.args, |
| 120 | &mut self.cfg.rargs, |
| 121 | )); |
| 122 | Ok(self) |
| 123 | } |
| 124 | |
| 125 | pub fn remove_arg<K: Into<Key>>(mut self, key: K) -> Self { |
| 126 | let key = key.into(); |
| 127 | let mut id_opt = None; |
| 128 | if let Some(Recursive::Key(id)) = self.args.get(&key) { |
| 129 | id_opt = Some(id.clone()); |
| 130 | } |
| 131 | if let Some(id) = id_opt { |
| 132 | self.args.remove(&id.clone()); |
| 133 | } |
| 134 | self.args.remove(&key); |
| 135 | self |
| 136 | } |
| 137 | |
| 138 | /// Add a command to a syntax. |
| 139 | pub fn add_cmd(mut self, mut c: Cmd) -> Outcome<Self> { |
| 140 | let owner = fmt!("command '{}'", c.config().name); |
| 141 | res!(Val::check_shape(&c.config().vals, &owner)); |
| 142 | if let Some(rest) = &c.config().rest { |
| 143 | res!(Val::check_shape(std::slice::from_ref(rest), &owner)); |
| 144 | if let Some(last) = c.config().vals.last() { |
| 145 | if last.verbatim && last.arity.repeats() { |
| 146 | return Err(err!( |
| 147 | "The {} ends on the verbatim value '{}', which takes every word \ |
| 148 | left, so no word could reach its '--' rest.", owner, last.label(); |
| 149 | Input, Invalid)); |
| 150 | } |
| 151 | } |
| 152 | } |
| 153 | c.id = self.next_cmd_id; |
| 154 | self.next_cmd_id = res!(Syntax::inc_counter( |
| 155 | self.next_cmd_id, |
| 156 | fmt!("syntax '{}' commands", self), |
| 157 | )); |
| 158 | self.cmds.insert(Key::Str(c.config().name.clone()), Recursive::Key(Key::Id(c.id))); |
| 159 | self.cmds.insert(Key::Id(c.id), Recursive::Val(c)); |
| 160 | Ok(self) |
| 161 | } |
| 162 | |
| 163 | pub fn remove_cmd<K: Into<Key>>(mut self, key: K) -> Self { |
| 164 | let key = key.into(); |
| 165 | let mut id_opt = None; |
| 166 | if let Some(Recursive::Key(id)) = self.cmds.get(&key) { |
| 167 | id_opt = Some(id.clone()); |
| 168 | } |
| 169 | if let Some(id) = id_opt { |
| 170 | self.cmds.remove(&id); |
| 171 | } |
| 172 | self.cmds.remove(&key); |
| 173 | self |
| 174 | } |
| 175 | |
| 176 | pub fn get_cmd<K: Into<Key>>(&self, key: K) -> Option<&Cmd> { |
| 177 | let key = key.into(); |
| 178 | self.cmds.get_recursive(&key) |
| 179 | } |
| 180 | |
| 181 | pub fn expected_vals<V: Into<Val>>(mut self, vals: Vec<V>) -> Self { |
| 182 | self.cfg.vals = vals.into_iter().map(|v| v.into()).collect(); |
| 183 | self |
| 184 | } |
| 185 | |
| 186 | /// The commands in the order they were added. |
| 187 | pub fn cmds_in_order(&self) -> Vec<&Cmd> { |
| 188 | self.cmds.iter().filter_map(|(k, v)| match (k, v) { |
| 189 | (Key::Id(_), Recursive::Val(cmd)) => Some(cmd), |
| 190 | _ => None, |
| 191 | }).collect() |
| 192 | } |
| 193 | |
| 194 | /// The message arguments in the order they were added. |
| 195 | pub fn args_in_order(&self) -> Vec<&Arg> { |
| 196 | self.args.iter().filter_map(|(k, v)| match (k, v) { |
| 197 | (Key::Id(_), Recursive::Val(arg)) => Some(arg), |
| 198 | _ => None, |
| 199 | }).collect() |
| 200 | } |
| 201 | |
| 202 | pub fn get_topic(&self, name: &str) -> Option<&Topic> { |
| 203 | self.cfg.topics.iter().find(|t| t.name == name) |
| 204 | } |
| 205 | |
| 206 | pub fn add_topic(mut self, topic: Topic) -> Outcome<Self> { |
| 207 | if self.get_topic(&topic.name).is_some() { |
| 208 | return Err(err!( |
| 209 | "The syntax '{}' already has a help topic '{}'.", self, topic.name; |
| 210 | Input, Exists)); |
| 211 | } |
| 212 | if self.get_cmd(topic.name.as_str()).is_some() { |
| 213 | return Err(err!( |
| 214 | "The help topic '{}' has the name of a command in the syntax '{}', so \ |
| 215 | 'help {}' could not tell them apart.", topic.name, self, topic.name; |
| 216 | Input, Exists)); |
| 217 | } |
| 218 | self.cfg.topics.push(topic); |
| 219 | Ok(self) |
| 220 | } |
| 221 | |
| 222 | pub fn one_cmd(mut self, b: bool) -> Self { |
| 223 | self.cfg.one_cmd = b; |
| 224 | self |
| 225 | } |
| 226 | |
| 227 | /// Add help as a command. |
| 228 | pub fn with_default_help_cmd(self) -> Outcome<Self> { |
| 229 | let c = res!(Cmd::new("help")) |
| 230 | .help("Display helpful information"); |
| 231 | self.add_cmd(c) |
| 232 | } |
| 233 | |
| 234 | /// Add help as an argument. |
| 235 | pub fn with_default_help_arg(self) -> Outcome<Self> { |
| 236 | let cfg = ArgConfig { |
| 237 | name: fmt!("help"), |
| 238 | hyph1: Some(fmt!("h")), |
| 239 | hyph2: Some(fmt!("help")), |
| 240 | reqd: false, |
| 241 | help: Some(fmt!("Display helpful information")), |
| 242 | ..Default::default() |
| 243 | }; |
| 244 | self.add_arg(Arg { |
| 245 | cfg: cfg, |
| 246 | ..Default::default() |
| 247 | }) |
| 248 | } |
| 249 | |
| 250 | pub fn ver(mut self, v: SemVer) -> Self { |
| 251 | self.cfg.ver = v; |
| 252 | self |
| 253 | } |
| 254 | |
| 255 | pub fn about<S: Into<String>>(mut self, s: S) -> Self { |
| 256 | self.cfg.about = Some(s.into()); |
| 257 | self |
| 258 | } |
| 259 | |
| 260 | pub fn set_width(mut self, w: usize) { |
| 261 | self.cfg.width = w; |
| 262 | } |
| 263 | |
| 264 | } |
| 265 | |
| 266 | impl fmt::Display for Syntax { |
| 267 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 268 | write!(f, |
| 269 | "{}V{}", |
| 270 | self.config().name, |
| 271 | self.config().ver, |
| 272 | ) |
| 273 | } |
| 274 | } |
| 275 | |
| 276 | new_type!(SyntaxRef, Arc<Syntax>, Clone, Debug, Default); |
| 277 | |
| 278 | /// This newtype makes it easier to give spawned `Msg` and `MsgCmd` objects their own immutable |
| 279 | /// reference to `Syntax`. For a slight performance cost there is no need for lifetime |
| 280 | /// annotation and certain compile time borrowing checks encountered during message decoding are |
| 281 | /// pushed to runtime. |
| 282 | impl SyntaxRef { |
| 283 | |
| 284 | pub fn new(syntax: Syntax) -> Self { |
| 285 | Self(Arc::new(syntax)) |
| 286 | } |
| 287 | } |