///|
pub(all) enum Pattern {
Ubiquitous
StateDriven
EventDriven
OptionalFeature
UnwantedBehaviour
Complex
} derive(Eq, Debug)
///|
/// The trigger of a requirement. Only the parser makes triggers, so the
/// condition is never empty and its white space is folded.
pub enum Trigger {
/// A `When` clause.
Event(String)
/// An `If ... then` clause.
Unwanted(String)
} derive(Eq, Debug)
///|
/// A clause keyword of EARS.
pub(all) enum Keyword {
Where
While
When
If
} derive(Eq, Hash, Debug)
///|
/// Every clause keyword, in the order that a sentence uses them. The parser
/// and the Markdown extraction use this one table.
pub let keywords : ReadOnlyArray[Keyword] = [Where, While, When, If]
///|
/// The keyword in lower case, as the parser matches it without letter case.
pub fn Keyword::word(self : Keyword) -> String {
match self {
Where => "where"
While => "while"
When => "when"
If => "if"
}
}
///|
/// The keyword in canonical case: `Where`, `While`, `When` or `If`.
pub fn Keyword::to_string(self : Keyword) -> String {
match self {
Where => "Where"
While => "While"
When => "When"
If => "If"
}
}
///|
pub impl Show for Keyword with fn output(self, logger) {
logger.write_string(self.to_string())
}
///|
/// Raised when an offset is not a position in a sentence.
pub(all) suberror PositionError {
/// The offset is negative.
NegativeOffset(Int32)
} derive(Eq, Debug)
///|
/// A place in the normalized sentence: the UTF-16 offset of a character,
/// counted from 0, or the length of the sentence for its end, such as the
/// place after a final `shall`.
struct SentencePosition {
offset : Int32
} derive(Eq, Compare, Hash, Debug)
///|
pub fn SentencePosition::SentencePosition(
offset : Int32,
) -> SentencePosition raise PositionError {
if offset < 0 {
raise NegativeOffset(offset)
}
{ offset, }
}
///|
pub fn SentencePosition::offset(self : SentencePosition) -> Int32 {
self.offset
}
///|
pub impl Show for SentencePosition with fn output(self, logger) {
logger.write_object(self.offset)
}
///|
/// The modal verb of a requirement, such as `shall` or `must`, and whether
/// `not` follows it.
pub struct Modal {
verb : Word
negated : Bool
} derive(Eq, Debug)
///|
/// The subject of a requirement.
pub enum Subject {
/// A determiner, such as `the` or `each`, and the system after it.
Determined(opener~ : Word, system~ : String)
/// A system noun of a dialect, such as a product name.
Noun(String)
/// A pronoun, such as `it`. Block parsing resolves it to the system of an
/// earlier sentence.
Pronoun(opener~ : Word, resolved~ : String?)
} derive(Eq, Debug)
///|
/// A validated EARS sentence. Only the parser can construct a requirement.
/// Free-text clauses retain their case and punctuation, with whitespace folded.
struct Requirement {
lead_in : String?
feature : String?
preconditions : ReadOnlyArray[String]
trigger : Trigger?
subject : Subject
modal : Modal
response : String
} derive(Eq, Debug)
///|
/// The lead-in phrase before the clauses, such as `Otherwise` or
/// `For each document`, as written.
pub fn Requirement::lead_in(self : Requirement) -> String? {
self.lead_in
}
///|
pub fn Requirement::feature(self : Requirement) -> String? {
self.feature
}
///|
pub fn Requirement::preconditions(self : Requirement) -> ReadOnlyArray[String] {
self.preconditions
}
///|
pub fn Requirement::trigger(self : Requirement) -> Trigger? {
self.trigger
}
///|
pub fn Requirement::subject(self : Requirement) -> Subject {
self.subject
}
///|
/// The subject opener as written: the determiner or the pronoun. A system
/// noun has none.
pub fn Requirement::opener(self : Requirement) -> Word? {
match self.subject {
Determined(opener~, ..) | Pronoun(opener~, ..) => Some(opener)
Noun(_) => None
}
}
///|
/// The system: the words after the determiner, the system noun, or the
/// system that a pronoun was resolved to. An unresolved pronoun has none.
pub fn Requirement::system(self : Requirement) -> String? {
match self.subject {
Determined(system~, ..) => Some(system)
Noun(system) => Some(system)
Pronoun(resolved~, ..) => resolved
}
}
///|
/// The system that block parsing resolved a pronoun subject to.
pub fn Requirement::resolved_system(self : Requirement) -> String? {
match self.subject {
Pronoun(resolved~, ..) => resolved
Determined(..) | Noun(_) => None
}
}
///|
pub fn Requirement::modal(self : Requirement) -> Modal {
self.modal
}
///|
pub fn Requirement::response(self : Requirement) -> String {
self.response
}
///|
pub fn Requirement::pattern(self : Requirement) -> Pattern {
let count = (if self.feature is Some(_) { 1 } else { 0 }) +
self.preconditions.length() +
(if self.trigger is Some(_) { 1 } else { 0 })
if count > 1 {
return Complex
}
if self.feature is Some(_) {
return OptionalFeature
}
if !self.preconditions.is_empty() {
return StateDriven
}
match self.trigger {
Some(Event(_)) => EventDriven
Some(Unwanted(_)) => UnwantedBehaviour
None => Ubiquitous
}
}
///|
/// A word in lower case, or with a capital first letter at the start of a
/// sentence.
fn canonical(word : String, at_start : Bool) -> String {
let lower = word.to_lower()
if at_start && lower.length() > 0 {
lower[0:1].to_owned().to_upper() + lower[1:].to_owned()
} else {
lower
}
}
///|
/// Render canonical keyword case and spacing: the lead-in and the clauses as
/// written, then the subject opener and the modal verb in lower case, with a
/// capital first letter at the start. Response punctuation is retained.
/// Parsing the text with the same dialects gives an equal requirement.
pub fn Requirement::to_string(self : Requirement) -> String {
let clauses = []
match self.lead_in {
Some(lead_in) => clauses.push(lead_in)
None => ()
}
if self.feature is Some(feature) {
clauses.push("\{Where} " + feature)
}
for state in self.preconditions {
clauses.push("\{While} " + state)
}
match self.trigger {
Some(Event(event)) => clauses.push("\{When} " + event)
Some(Unwanted(event)) => clauses.push("\{If} " + event)
None => ()
}
let unwanted = self.trigger is Some(Unwanted(_))
let at_start = clauses.is_empty()
let subject = match self.subject {
Determined(opener~, system~) =>
canonical(opener.to_string(), at_start && !unwanted) + " " + system
Noun(system) => system
Pronoun(opener~, ..) => canonical(opener.to_string(), at_start && !unwanted)
}
let modal = self.modal.verb.lower() +
(if self.modal.negated { " not" } else { "" })
let head = if unwanted { "then " } else { "" }
clauses.push(head + subject + " " + modal + " " + self.response)
clauses.join(", ")
}
///|
/// Why a sentence is not an EARS requirement. Each case except
/// `EmptyRequirement` has the position in the normalized sentence where the
/// parse failed.
pub(all) suberror ParseError {
/// The sentence is empty or white space.
EmptyRequirement
/// The system clause, `the shall`, is missing at the position.
ExpectedSystem(at~ : SentencePosition)
/// The system clause from the position has no `shall`.
MissingShall(at~ : SentencePosition)
/// Nothing follows `shall` at the position.
MissingResponse(at~ : SentencePosition)
/// The clause of the keyword at the position is empty.
EmptyClause(keyword~ : Keyword, at~ : SentencePosition)
/// The clause at the position has no comma before the next clause.
MissingSeparator(at~ : SentencePosition)
/// The `If` clause at the position has no `then`.
MissingThen(at~ : SentencePosition)
/// The clause at the position is out of order.
InvalidClauseOrder(at~ : SentencePosition)
/// In a requirement block, the pronoun subject at the position has no
/// earlier sentence with a system to refer to.
UnresolvedPronoun(pronoun~ : Word, at~ : SentencePosition)
} derive(Eq, Debug)
///|
/// The position where the parse failed, or `None` for an empty sentence.
pub fn ParseError::position(self : ParseError) -> SentencePosition? {
match self {
EmptyRequirement => None
ExpectedSystem(at~)
| MissingShall(at~)
| MissingResponse(at~)
| EmptyClause(at~, ..)
| MissingSeparator(at~)
| MissingThen(at~)
| InvalidClauseOrder(at~)
| UnresolvedPronoun(at~, ..) => Some(at)
}
}
///|
pub extend Pattern with Eq::{equal, not_equal}
///|
pub extend Pattern with @debug.Debug::{to_repr}
///|
pub extend Trigger with Eq::{equal, not_equal}
///|
pub extend Trigger with @debug.Debug::{to_repr}
///|
pub extend Requirement with Eq::{equal, not_equal}
///|
pub extend Requirement with @debug.Debug::{to_repr}
///|
pub extend ParseError with Eq::{equal, not_equal}
///|
pub extend ParseError with @debug.Debug::{to_repr}
///|
pub extend Keyword with Eq::{not_equal, equal}
///|
pub extend Keyword with Hash::{hash, hash_combine}
///|
pub extend Keyword with @debug.Debug::{to_repr}
///|
pub extend Keyword with Show::{output}
///|
pub extend PositionError with Eq::{not_equal, equal}
///|
pub extend PositionError with @debug.Debug::{to_repr}
///|
pub extend SentencePosition with Eq::{not_equal, equal}
///|
pub extend SentencePosition with Compare::{op_lt, op_le, op_ge, compare, op_gt}
///|
pub extend SentencePosition with Hash::{hash, hash_combine}
///|
pub extend SentencePosition with @debug.Debug::{to_repr}
///|
pub extend SentencePosition with Show::{to_string, output}
///|
pub extend Modal with Eq::{equal, not_equal}
///|
pub extend Modal with @debug.Debug::{to_repr}
///|
pub extend Subject with Eq::{equal, not_equal}
///|
pub extend Subject with @debug.Debug::{to_repr}