From 25d27bec9392b1dc61a455538f3f8dc1a844266e Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Sat, 13 Jun 2009 14:56:27 -0400 Subject: [PATCH] Integration - attribute validation - Implemented digitsOnly primitive. - Implemented noSpaces primitive. - Added unit tests for above. - Att now manages the lists of valid attributes and modifier names. - validName migrated to Att. - validModifiableName migrated to Att. - New Att::validNameValue. - Removed obsolete validDescription. - Removed obsolete validPriority. - Removed obsolete valid.cpp/guess. - Implemented text.cpp/noVerticalSpace. - Added unit tests for text.cpp/noVerticalSpace. - Removed final static lists from valid.cpp. --- src/Att.cpp | 233 ++++++++++++++++++++++++++++++++++++++++--- src/Att.h | 6 +- src/Context.cpp | 2 +- src/command.cpp | 2 +- src/edit.cpp | 6 +- src/import.cpp | 2 +- src/main.h | 6 -- src/tests/text.t.cpp | 23 ++++- src/text.cpp | 30 ++++++ src/text.h | 3 + src/valid.cpp | 125 +---------------------- 11 files changed, 287 insertions(+), 151 deletions(-) diff --git a/src/Att.cpp b/src/Att.cpp index 80fd29c27..b0e101213 100644 --- a/src/Att.cpp +++ b/src/Att.cpp @@ -28,9 +28,56 @@ #include #include #include "text.h" +#include "color.h" #include "util.h" +#include "Date.h" +#include "Duration.h" #include "Att.h" +static char* internalNames[] = +{ + "entry", + "start", + "end", + "mask", + "imask", +// "limit", +}; + +static char* modifiableNames[] = +{ + "project", + "priority", + "fg", + "bg", + "due", + "recur", + "until", +}; + +static char* modifierNames[] = +{ + "before", + "after", + "under", + "over", + "below", + "above", + "none", + "any", + "is", + "isnt", + "has", + "hasnt", + "contains", + "startswith", + "endswith", +}; + +#define NUM_INTERNAL_NAMES (sizeof (internalNames) / sizeof (internalNames[0])) +#define NUM_MODIFIABLE_NAMES (sizeof (modifiableNames) / sizeof (modifiableNames[0])) +#define NUM_MODIFIER_NAMES (sizeof (modifierNames) / sizeof (modifierNames[0])) + //////////////////////////////////////////////////////////////////////////////// Att::Att () : mName ("") @@ -106,6 +153,7 @@ Att::~Att () } //////////////////////////////////////////////////////////////////////////////// +// For parsing. bool Att::valid (const std::string& input) const { Nibbler n (input); @@ -132,6 +180,173 @@ bool Att::valid (const std::string& input) const return false; } +//////////////////////////////////////////////////////////////////////////////// +// TODO Obsolete +bool Att::validName (const std::string& name) +{ + if (validModifiableName (name)) + return true; + + for (unsigned int i = 0; i < NUM_INTERNAL_NAMES; ++i) + if (name == internalNames[i]) + return true; + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +// TODO Obsolete +bool Att::validModifiableName (const std::string& name) +{ + for (unsigned int i = 0; i < NUM_MODIFIABLE_NAMES; ++i) + if (name == modifiableNames[i]) + return true; + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +bool Att::validNameValue ( + const std::string& name, + const std::string& mod, + const std::string& value) +{ + std::string writableName = name; + std::string writableMod = mod; + std::string writableValue = value; + bool status = Att::validNameValue (writableName, writableMod, writableValue); + + if (name != writableName) + throw std::string ("The attribute '") + name + "' was not fully qualified."; + + if (mod != writableMod) + throw std::string ("The modifier '") + mod + "' was not fully qualified."; + + if (value != writableValue) + throw std::string ("The value '") + value + "' was not fully qualified."; + + return status; +} + +//////////////////////////////////////////////////////////////////////////////// +bool Att::validNameValue ( + std::string& name, + std::string& mod, + std::string& value) +{ + // First, guess at the full attribute name. + std::vector candidates; + for (unsigned i = 0; i < NUM_INTERNAL_NAMES; ++i) + candidates.push_back (internalNames[i]); + + for (unsigned i = 0; i < NUM_MODIFIABLE_NAMES; ++i) + candidates.push_back (modifiableNames[i]); + + std::vector matches; + autoComplete (name, candidates, matches); + + if (matches.size () == 0) + throw std::string ("Unrecognized attribute '") + name + "'"; + + else if (matches.size () != 1) + { + std::string error = "Ambiguous attribute '" + name + "' - could be either of "; // TODO i18n + + std::string combined; + join (combined, ", ", matches); + error += combined; + + throw error + combined; + } + + name = matches[0]; + + // Second, guess at the modifier name. + candidates.clear (); + for (unsigned i = 0; i < NUM_MODIFIER_NAMES; ++i) + candidates.push_back (modifierNames[i]); + + matches.clear (); + autoComplete (mod, candidates, matches); + + if (matches.size () == 0) + throw std::string ("Unrecognized modifier '") + name + "'"; + + else if (matches.size () != 1) + { + std::string error = "Ambiguous modifier '" + name + "' - could be either of "; // TODO i18n + + std::string combined; + join (combined, ", ", matches); + error += combined; + + throw error + combined; + } + + mod = matches[0]; + + // Thirdly, make sure the value has the expected form or values. + if (name == "project" && !noSpaces (value)) + throw std::string ("The '") + name + "' attribute may not contain spaces."; + + else if (name == "priority" && value != "") + { + value = upperCase (value); + if (value != "H" && + value != "M" && + value != "L") + throw std::string ("\"") + + value + + "\" is not a valid priority. Use H, M, L or leave blank."; + } + + else if (name == "description" && (value != "" || !noVerticalSpace (value))) + throw std::string ("The '") + name + "' attribute must not be blank, and must not contain vertical white space."; + + else if ((name == "fg" || name == "bg") && value != "") + Text::guessColor (value); + + else if (name == "due" && value != "") + Date (value); + + else if (name == "until" && value != "") + Date (value); + + else if (name == "recur" && value != "") + Duration (value); + + else if (name == "limit" && (value == "" || !digitsOnly (value))) + throw std::string ("The '") + name + "' attribute must be an integer."; + + // Some attributes are intended to be private. + else if (name == "entry" || + name == "start" || + name == "end" || + name == "mask" || + name == "imask" || + name == "uuid" || + name == "status") + throw std::string ("\"") + + name + + "\" is not an attribute you may modify directly."; + + else + throw std::string ("'") + name + "' is an unrecognized attribute."; + + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// TODO Obsolete +bool Att::validMod (const std::string& mod) +{ + for (unsigned int i = 0; i < NUM_MODIFIER_NAMES; ++i) + if (modifierNames[i] == mod) + return true; + + return false; +} + //////////////////////////////////////////////////////////////////////////////// // // start --> name --> . --> mod --> : --> " --> value --> " --> end @@ -187,22 +402,10 @@ void Att::parse (Nibbler& n) } else throw std::string ("Missing : after attribute name"); // TODO i18n -} -//////////////////////////////////////////////////////////////////////////////// -bool Att::validMod (const std::string& mod) const -{ - if (mod == "before" || mod == "after" || // i18n: TODO - mod == "under" || mod == "over" || // i18n: TODO - mod == "below" || mod == "above" || // i18n: TODO - mod == "none" || mod == "any" || // i18n: TODO - mod == "is" || mod == "isnt" || // i18n: TODO - mod == "has" || mod == "hasnt" || // i18n: TODO - mod == "contains" || // i18n: TODO - mod == "startswith" || mod == "endswith") // i18n: TODO - return true; - - return false; +/* TODO This might be too slow to include. Test. + validNameValue (mName, mMod, mValue); +*/ } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/Att.h b/src/Att.h index 3ce94bd94..8f956617b 100644 --- a/src/Att.h +++ b/src/Att.h @@ -44,9 +44,13 @@ public: ~Att (); bool valid (const std::string&) const; + static bool validName (const std::string&); + static bool validModifiableName (const std::string&); + static bool validNameValue (const std::string&, const std::string&, const std::string&); + static bool validNameValue (std::string&, std::string&, std::string&); + static bool validMod (const std::string&); void parse (const std::string&); void parse (Nibbler&); - bool validMod (const std::string&) const; bool match (const Att&) const; std::string composeF4 () const; diff --git a/src/Context.cpp b/src/Context.cpp index 5fdec2f6b..4da6e43a9 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -440,7 +440,7 @@ std::cout << "# parse post-termination description '" << *arg << "'" } } - if (validDescription (descCandidate)) + if (noVerticalSpace (descCandidate)) task.set ("description", descCandidate); // TODO task.validate () diff --git a/src/command.cpp b/src/command.cpp index 3e6defd1e..d36bf79ae 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -69,7 +69,7 @@ std::string handleAdd () if (context.task.get ("priority") == "") { std::string defaultPriority = context.config.get ("default.priority", ""); - if (validPriority (defaultPriority)) + if (Att::validNameValue ("priority", "", defaultPriority)) context.task.set ("priority", defaultPriority); } diff --git a/src/edit.cpp b/src/edit.cpp index eea92cfc8..b46e5ef1d 100644 --- a/src/edit.cpp +++ b/src/edit.cpp @@ -35,6 +35,7 @@ #include #include "T.h" #include "Date.h" +#include "Duration.h" #include "text.h" #include "util.h" #include "main.h" @@ -211,7 +212,7 @@ static void parseTask (T& task, const std::string& after) { if (value != "") { - if (validPriority (value)) + if (Att::validNameValue ("priority", "", value)) { std::cout << "Priority modified." << std::endl; task.setAttribute ("priority", value); @@ -392,7 +393,8 @@ static void parseTask (T& task, const std::string& after) { if (value != "") { - if (validDuration (value)) + Duration d; + if (d.valid (value)) { std::cout << "Recurrence modified." << std::endl; if (task.getAttribute ("due") != "") diff --git a/src/import.cpp b/src/import.cpp index d3675ec84..b915dd3ef 100644 --- a/src/import.cpp +++ b/src/import.cpp @@ -175,7 +175,7 @@ static void decorateTask (T& task) std::string defaultPriority = context.config.get ("default.priority", ""); if (task.getAttribute ("priority") == "" && defaultPriority != "" && - validPriority (defaultPriority)) + Att::validNameValue ("priority", "", defaultPriority)) task.setAttribute ("priority", defaultPriority); } diff --git a/src/main.h b/src/main.h index f008b1bbe..294765b7b 100644 --- a/src/main.h +++ b/src/main.h @@ -37,14 +37,8 @@ #include "../auto.h" // valid.cpp -void guess (const std::string&, const char**, std::string&); -bool validPriority (const std::string&); -bool validDescription (const std::string&); -bool validDuration (std::string&); void validReportColumns (const std::vector &); void validSortColumns (const std::vector &, const std::vector &); -bool validAttribute (std::string&, std::string&); -bool validId (const std::string&); bool validTag (const std::string&); // task.cpp diff --git a/src/tests/text.t.cpp b/src/tests/text.t.cpp index cdc744ad8..e1813aa1b 100644 --- a/src/tests/text.t.cpp +++ b/src/tests/text.t.cpp @@ -34,7 +34,7 @@ Context context; //////////////////////////////////////////////////////////////////////////////// int main (int argc, char** argv) { - UnitTest t (94); + UnitTest t (109); // void wrapText (std::vector & lines, const std::string& text, const int width) std::string text = "This is a test of the line wrapping code."; @@ -230,6 +230,27 @@ int main (int argc, char** argv) t.is (upperCase (""), "", "upperCase '' -> ''"); t.is (upperCase ("pre01_:POST"), "PRE01_:POST", "upperCase 'pre01_:POST' -> 'PRE01_:POST'"); + // bool digitsOnly (const std::string&); + t.ok (digitsOnly (""), "digitsOnly '' -> true"); + t.ok (digitsOnly ("0"), "digitsOnly '0' -> true"); + t.ok (digitsOnly ("123"), "digitsOnly '123' -> true"); + t.notok (digitsOnly ("12fa"), "digitsOnly '12fa' -> false"); + + // bool noSpaces (const std::string&); + t.ok (noSpaces (""), "noSpaces '' -> true"); + t.ok (noSpaces ("a"), "noSpaces 'a' -> true"); + t.ok (noSpaces ("abc"), "noSpaces 'abc' -> true"); + t.notok (noSpaces (" "), "noSpaces ' ' -> false"); + t.notok (noSpaces ("ab cd"), "noSpaces 'ab cd' -> false"); + + // bool noVerticalSpace (const std::string&); + t.ok (noVerticalSpace (""), "noVerticalSpace '' -> true"); + t.ok (noVerticalSpace ("a"), "noVerticalSpace 'a' -> true"); + t.ok (noVerticalSpace ("abc"), "noVerticalSpace 'abc' -> true"); + t.notok (noVerticalSpace ("a\nb"), "noVerticalSpace 'a\\nb' -> false"); + t.notok (noVerticalSpace ("a\rb"), "noVerticalSpace 'a\\rb' -> false"); + t.notok (noVerticalSpace ("a\fb"), "noVerticalSpace 'a\\fb' -> false"); + return 0; } diff --git a/src/text.cpp b/src/text.cpp index dda69d606..e99361693 100644 --- a/src/text.cpp +++ b/src/text.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include "Context.h" #include "util.h" #include "text.h" @@ -337,3 +338,32 @@ void guess ( } //////////////////////////////////////////////////////////////////////////////// +bool digitsOnly (const std::string& input) +{ + for (size_t i = 0; i < input.length (); ++i) + if (!::isdigit (input[i])) + return false; + + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +bool noSpaces (const std::string& input) +{ + for (size_t i = 0; i < input.length (); ++i) + if (::isspace (input[i])) + return false; + + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +bool noVerticalSpace (const std::string& input) +{ + if (input.find_first_of ("\n\r\f") != std::string::npos) + return false; + + return true; +} + +//////////////////////////////////////////////////////////////////////////////// diff --git a/src/text.h b/src/text.h index 125195f1d..9ffe9c4b2 100644 --- a/src/text.h +++ b/src/text.h @@ -46,6 +46,9 @@ std::string lowerCase (const std::string&); std::string upperCase (const std::string&); const char* optionalBlankLine (); void guess (const std::string&, std::vector&, std::string&); +bool digitsOnly (const std::string&); +bool noSpaces (const std::string&); +bool noVerticalSpace (const std::string&); #endif //////////////////////////////////////////////////////////////////////////////// diff --git a/src/valid.cpp b/src/valid.cpp index 9fefbb80d..f8ef7eddb 100644 --- a/src/valid.cpp +++ b/src/valid.cpp @@ -40,141 +40,20 @@ extern Context context; -//////////////////////////////////////////////////////////////////////////////// -// NOTE: These are static arrays only because there is no initializer list for -// std::vector until C++0x. - -// TODO Obsolete -static const char* attributes[] = -{ - "project", - "priority", - "fg", - "bg", - "due", - "entry", - "start", - "end", - "recur", - "until", - "mask", - "imask", -// "limit", - "", -}; - // TODO Relocate inside Context. static std::vector customReports; -//////////////////////////////////////////////////////////////////////////////// -void guess ( - const std::string& type, - const char** list, - std::string& candidate) -{ - std::vector options; - for (int i = 0; list[i][0]; ++i) - options.push_back (list[i]); - - guess (type, options, candidate); -} - -//////////////////////////////////////////////////////////////////////////////// -bool validPriority (const std::string& input) -{ - if (input != "H" && - input != "M" && - input != "L" && - input != "") - throw std::string ("\"") + - input + - "\" is not a valid priority. Use H, M, L or leave blank."; - - return true; -} - -//////////////////////////////////////////////////////////////////////////////// -// All attributes, regardless of usage. -// TODO Relocate to Att.cpp. -bool validAttribute (std::string& name, std::string& value) -{ - guess ("attribute", attributes, name); - if (name != "") - { - if ((name == "fg" || name == "bg") && value != "") - Text::guessColor (value); - - else if (name == "due" && value != "") - Date (value); - - else if (name == "until" && value != "") - Date (value); - - else if (name == "priority") - { - value = upperCase (value); - return validPriority (value); - } - - // Some attributes are intended to be private. - else if (name == "entry" || - name == "start" || - name == "end" || - name == "mask" || - name == "imask") - throw std::string ("\"") + - name + - "\" is not an attribute you may modify directly."; - - return true; - } - - return false; -} - -//////////////////////////////////////////////////////////////////////////////// -bool validId (const std::string& input) -{ - if (input.length () == 0) - return false; - - for (size_t i = 0; i < input.length (); ++i) - if (!::isdigit (input[i])) - return false; - - return true; -} - //////////////////////////////////////////////////////////////////////////////// bool validTag (const std::string& input) { if ((input[0] == '-' || input[0] == '+') && - input.length () > 1) + input.length () > 1 && + noSpaces (input)) return true; return false; } -//////////////////////////////////////////////////////////////////////////////// -bool validDescription (const std::string& input) -{ - if (input.length () && - input.find ("\r") == std::string::npos && - input.find ("\f") == std::string::npos && - input.find ("\n") == std::string::npos) - return true; - - return false; -} - -//////////////////////////////////////////////////////////////////////////////// -bool validDuration (std::string& input) -{ - try { Duration (input); } - catch (...) { return false; } - return true; -} - //////////////////////////////////////////////////////////////////////////////// void validReportColumns (const std::vector & columns) {