diff --git a/src/lib.rs b/src/lib.rs index 5bc6657..f5f5131 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,8 @@ //! # Humanize //! +//! Make your user interface more human friendly! +//! //! This library provides functionality for both formatting values //! into human friendly forms as well as parsing human input to get //! back likely values. @@ -21,6 +23,38 @@ //! //! Contributions extending our functionality are welcome, as are //! contributions that add support for additional languages. +//! +//! # Human-friendly Parsing +//! +//! When dealing with humans, you often want them to be able to +//! input values in a flexible manner. For example, you might want +//! to be able to input a `bool` using text like `"on"`, `"off"`, +//! `"yes"`, `"no"` or perhaps even `"nope"`. +//! +//! First, you'll want to construct a parser: +//! +//! ``` +//! use humanize::HumanizedParser; +//! +//! let parser = HumanizedParser::new(); +//! ``` +//! +//! Then, you can use that parser to examine some input. In the typical +//! case, you can invoke a type-specific parse method like `parse_boolean`. +//! You may also limit the matchers run to a specific language. (Here, +//! we don't limit the languages, so we pass `Default::default()`.) +//! +//! ``` +//! # use humanize::HumanizedParser; +//! # +//! # let parser = HumanizedParser::new(); +//! let enabled = parser.parse_boolean("on", Default::default()).unwrap_or(false); +//! assert_eq!(enabled, true); +//! ``` +//! +//! The parser stores no state related to an actual parse operation. It +//! simply stores the matchers which have been registered, so this can +//! and should be cached and used across multiple parse operations. #![warn(missing_docs)] #![deny(trivial_numeric_casts, @@ -30,4 +64,7 @@ #[macro_use] extern crate language_tags; -pub mod parse; +pub mod matchers; +mod parser; + +pub use parser::HumanizedParser; diff --git a/src/parse/english/boolean.rs b/src/matchers/english/boolean.rs similarity index 89% rename from src/parse/english/boolean.rs rename to src/matchers/english/boolean.rs index ef60329..e7537e5 100644 --- a/src/parse/english/boolean.rs +++ b/src/matchers/english/boolean.rs @@ -10,10 +10,11 @@ // This is just a dummy example for now. It should clearly be // much better and actually work correctly. :) -use parse::*; +use matchers::*; +use parser::HumanizedParser; #[allow(missing_docs)] -pub fn register(parser: &mut Parser) { +pub fn register(parser: &mut HumanizedParser) { parser.register_matcher(Matcher { name: "English Booleans", language: langtag!(en), diff --git a/src/parse/english/mod.rs b/src/matchers/english/mod.rs similarity index 85% rename from src/parse/english/mod.rs rename to src/matchers/english/mod.rs index 4fe54d4..1684abc 100644 --- a/src/parse/english/mod.rs +++ b/src/matchers/english/mod.rs @@ -6,12 +6,12 @@ //! English Humanization -use parse::Parser; +use parser::HumanizedParser; pub mod boolean; // pub mod ordinal; /// Register all of the English language matchers. -pub fn register(parser: &mut Parser) { +pub fn register(parser: &mut HumanizedParser) { boolean::register(parser); } diff --git a/src/matchers/mod.rs b/src/matchers/mod.rs new file mode 100644 index 0000000..f961f29 --- /dev/null +++ b/src/matchers/mod.rs @@ -0,0 +1,70 @@ +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! # Register Matchers +//! +//! Matchers can be provided to augment the built-in parsing and +//! recognition capabilities of this library. +//! +//! _We will expand upon this in the future once our own infrastructure +//! for doing matchers well is in place._ +//! +//! ... + +use language_tags::LanguageTag; +use std::time::{Duration, Instant}; + +pub mod english; + +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ValueType { + Boolean, + Duration, + Instant, + Integer, + Ordinal, +} + +#[allow(missing_docs)] +#[derive(Debug, PartialEq)] +pub enum HumanValue { + Boolean(bool), + Duration(Duration), + Instant(Instant), + Integer(i64), + Ordinal(i64), +} + +/// A possible match for a value within some text. +/// +/// A `Match` result is obtained by calling [`parse`] on +/// some input text. They are created by [`Matcher`]s. +/// +/// [`parse`]: ../struct.HumanizedParser.html#method.parse +/// [`Matcher`]: struct.Matcher.html +#[derive(Debug)] +pub struct Match { + /// The value determined for this match. + pub value: HumanValue, + + /// Strength of the match. + /// + /// This is useful when there is more than one possible match. + /// + /// TODO: Should be this be a percentage? What is the range? + /// Should it be an enum with values like 'Likely', 'Unlikely', + /// and 'Certain'? + pub weight: i32, +} + +#[allow(missing_docs)] +pub struct Matcher<'m> { + pub name: &'m str, + pub language: LanguageTag, + pub result_type: ValueType, + pub matcher: Box Option>, +} diff --git a/src/parse/mod.rs b/src/parse/mod.rs deleted file mode 100644 index bb0d646..0000000 --- a/src/parse/mod.rs +++ /dev/null @@ -1,171 +0,0 @@ -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -//! Human-friendly Parsing -//! -//! This module provides support for finding human-friendly values -//! within some source text. This is typically used for parsing -//! input text for prompts that need common data types such as -//! instants in time, durations, booleans, etc. -//! -//! # Upcoming Breaking Changes -//! -//! The system of using `ValueType` and `HumanValue` as seen in -//! the current implementation of this library will be going away -//! in favor of something else (and depending on another crate) -//! in the near future so that we can interoperate with other -//! things that need a richer type system. -//! -//! # Match Input -//! -//! First, you'll want to construct a parser: -//! -//! ``` -//! use humanize::parse::Parser; -//! -//! let parser = Parser::new(); -//! ``` -//! -//! Then, you can use that parser to examine some input. In the typical -//! case, you can invoke a type-specific parse method like `parse_boolean`. -//! You may also limit the matchers run to a specific language. (Here, -//! we don't limit the languages, so we pass `Default::default()`.) -//! -//! ``` -//! # use humanize::parse::Parser; -//! # -//! # let parser = Parser::new(); -//! let maybe_bool = parser.parse_boolean("on", Default::default()); -//! assert_eq!(maybe_bool, Some(true)); -//! ``` -//! -//! # Register Matchers -//! -//! Matchers can be provided to augment the built-in capabilities -//! of this library. -//! -//! _We will expand upon this in the future once our own infrastructure -//! for doing matchers well is in place._ -//! -//! ... - -use language_tags::LanguageTag; -use std::time::{Duration, Instant}; - -pub mod english; - -#[allow(missing_docs)] -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum ValueType { - Boolean, - Duration, - Instant, - Integer, - Ordinal, -} - -#[allow(missing_docs)] -#[derive(Debug, PartialEq)] -pub enum HumanValue { - Boolean(bool), - Duration(Duration), - Instant(Instant), - Integer(i64), - Ordinal(i64), -} - -/// A possible match for a value within some text. -/// -/// A `Match` result is obtained by calling [`parse`] on -/// some input text. -/// -/// [`parse`]: fn.parse.html -#[derive(Debug)] -pub struct Match { - /// The value determined for this match. - pub value: HumanValue, - - /// Strength of the match. - /// - /// This is useful when there is more than one possible match. - /// - /// TODO: Should be this be a percentage? What is the range? - /// Should it be an enum with values like 'Likely', 'Unlikely', - /// and 'Certain'? - pub weight: i32, -} - -#[allow(missing_docs)] -pub struct Matcher<'m> { - pub name: &'m str, - pub language: LanguageTag, - pub result_type: ValueType, - pub matcher: Box Option>, -} - -#[allow(missing_docs)] -pub struct Parser<'p> { - /// The matchers which have been registered with this parser. - /// - /// Use `Parser.register_matcher` to add a new matcher. - matchers: Vec>, -} - -impl<'p> Parser<'p> { - /// Construct a new parser, including the default matchers. - pub fn new() -> Self { - Default::default() - } - - /// Construct a new parser, but without any of the default matchers. - pub fn new_without_default_matchers() -> Self { - Parser { matchers: vec![] } - } - - #[allow(missing_docs)] - pub fn register_matcher(&mut self, matcher: Matcher<'p>) { - self.matchers.push(matcher); - } - - /// Parse `text`, looking for a value of the [desired type], using - /// the optionally provided language. - /// - /// The resulting collection of matches will be ordered by their - /// weight of likelihood with the most likely first. - /// - /// [desired type]: enum.ValueType.html - pub fn parse(&self, text: &str, desired: ValueType, language: LanguageTag) -> Vec { - let mut matches = vec![]; - for matcher in &self.matchers { - if matcher.result_type == desired && language.matches(&matcher.language) { - if let Some(m) = (matcher.matcher)(text) { - matches.push(m); - } - } - } - matches - } - - /// Parse `text`, looking for a `bool` value. - /// - /// If you don't want to limit the matching to a particular language, - /// pass `Default::default()` for the `language`. - pub fn parse_boolean(&self, text: &str, language: LanguageTag) -> Option { - let matches = self.parse(text, ValueType::Boolean, language); - match matches.first().map(|ref m| &m.value) { - Some(&HumanValue::Boolean(val)) => Some(val), - _ => None, - } - } -} - -impl<'p> Default for Parser<'p> { - fn default() -> Self { - let mut p = Parser { matchers: vec![] }; - english::register(&mut p); - p - } -} diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..2bb5a17 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,75 @@ +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use language_tags::LanguageTag; +use matchers::*; + +#[allow(missing_docs)] +pub struct HumanizedParser<'p> { + /// The matchers which have been registered with this parser. + /// + /// Use `HumanizedParser.register_matcher` to add a new matcher. + matchers: Vec>, +} + +impl<'p> HumanizedParser<'p> { + /// Construct a new parser, including the default matchers. + pub fn new() -> Self { + Default::default() + } + + /// Construct a new parser, but without any of the default matchers. + pub fn new_without_default_matchers() -> Self { + HumanizedParser { matchers: vec![] } + } + + /// Parse `text`, looking for a `bool` value. + /// + /// If you don't want to limit the matching to a particular language, + /// pass `Default::default()` for the `language`. + pub fn parse_boolean(&self, text: &str, language: LanguageTag) -> Option { + let matches = self.parse(text, ValueType::Boolean, language); + match matches.first().map(|ref m| &m.value) { + Some(&HumanValue::Boolean(val)) => Some(val), + _ => None, + } + } + + /// Parse `text`, looking for a value of the [desired type], using + /// the optionally provided language. + /// + /// The resulting collection of matches will be ordered by their + /// weight of likelihood with the most likely first. + /// + /// You won't typically invoke this directly, instead preferring to + /// use type-specific methods like `parse_boolean`. + /// + /// [desired type]: matchers/enum.ValueType.html + pub fn parse(&self, text: &str, desired: ValueType, language: LanguageTag) -> Vec { + let mut matches = vec![]; + for matcher in &self.matchers { + if matcher.result_type == desired && language.matches(&matcher.language) { + if let Some(m) = (matcher.matcher)(text) { + matches.push(m); + } + } + } + matches + } + + #[allow(missing_docs)] + pub fn register_matcher(&mut self, matcher: Matcher<'p>) { + self.matchers.push(matcher); + } +} + +impl<'p> Default for HumanizedParser<'p> { + fn default() -> Self { + let mut p = HumanizedParser { matchers: vec![] }; + english::register(&mut p); + p + } +}