A macro based key/value collection class

Radical Edward 0 Tallied Votes 265 Views Share

Edward designed this class as a poor man's named property initializer using syntax similar to C99's designated initializers if they worked in a C++ constructor call:

Person me(
  .FirstName="Radical",
  .LastName="Edward",
  .Age=23
  );
#include <iostream>
#include <string>
#include <sstream>
#include <typeinfo>
#include <map>

namespace TextUtil {
  /// Manages property/value pairs given as macros.
  class MacroCollection {
    typedef std::map<std::string, std::string> ParsedMacroList;

    static const char MacroBegin = '.'; // Marks the beginning of a macro
    static const char MacroEnd = ';';   // Marks the end of a macro
    static const char MacroSep = '=';   // Separates the parts of a macro

    const std::string& _macroList; // The unextracted list of macros
    ParsedMacroList _macros;       // The extracted list of macros
  public:
    MacroCollection(const std::string& macroList);
    template <typename T> T Extract(const std::string& property);
  private:
    void ExtractMacros();
    void ParseMacro(const std::string& macroText);
  };

  /// Create and initialize a MacroCollection object.
  /// @param macroList A collection of macros represented as a string
  /// @throws rumtime_error One or more macros could not be extracted
  MacroCollection::MacroCollection(const std::string& macroList)
    : _macroList(macroList)
  {
    ExtractMacros();
  }

  /// Locate the value for a given property.
  /// @param property The property name to search for
  /// @return The value as a string, without any translation logic
  /// @throws invalid_argument The property was not found
  template <>
  std::string MacroCollection::Extract(const std::string &property)
  {
    ParsedMacroList::const_iterator macro = _macros.find(property);

    if (macro == _macros.end()) {
      std::ostringstream oss;

      // Build an informative error message
      oss << "Unexpected property [\"" + property +
        "\"] for this type. Expected one of: ";

      // Include a comma separated list of expected properties
      for (macro = _macros.begin(); macro != _macros.end(); ++macro) {
        std::string prefix = (macro != _macros.begin()) ? ", " : "(";

        oss << prefix << "\"" + macro->first + "\"";
      }

      oss << ")";

      throw std::invalid_argument(oss.str());
    }

    return macro->second;
  }

  /// Locate the value for a given property.
  /// @param property The property name to search for
  /// @return The value after translation into a specified type
  /// @throws invalid_argument The property was not found
  /// @throws invalid_argument The value could not be translated
  template <typename T>
  T MacroCollection::Extract(const std::string& property)
  {
    std::string value = Extract<std::string>(property);
    std::istringstream iss(value);
    T result;

    // Try to translate the string value as type T
    if (!(iss >> result)) {
      throw std::invalid_argument(
        "Invalid type [" + std::string(typeid(T).name()) +
        "] for the macro value \"" + value + "\"");
    }

    return result;
  }

  /// Parse a list of macros represented as a string.
  /// Given a string, extract individual macros for storage
  /// @throws runtime_error The macro was not formatted correctly
  void MacroCollection::ExtractMacros()
  {
    // The starting index of a macro
    std::string::size_type begin = 0;

    // The number of macros that were extracted
    int extractedMacros = 0;

    // Try to extract a macro based on the initiator character
    while ((begin = _macroList.find(MacroBegin, begin)) != std::string::npos) {
      std::string::size_type end = _macroList.find(MacroEnd, begin);

      // Verify that the macro is delimited correctly
      if (end == std::string::npos) {
        throw std::runtime_error(
          "Invalid macro format found at \"" + _macroList.substr(begin, end) +
          "\": Missing macro terminator character (" + MacroEnd + ")");
      }

      // Separate the macro from the list for parsing
      std::string temp = _macroList.substr(begin, end - begin + 1);

      // Verify that the macro represents a key/value pair
      if (temp.find(MacroSep) == std::string::npos) {
        throw std::runtime_error(
          "Invalid macro format found at \"" + temp +
          "\": Missing macro separator character (" + MacroSep + ")");
      }

#ifdef DEV_DEBUG
      std::cout << "Extracted macro: " << temp << '\n';
#endif

      ParseMacro(temp);
      ++extractedMacros;
      begin = end;
    }

    // No extracted macros means an
    // initiator character was not found
    if (extractedMacros == 0) {
      throw std::runtime_error(
        "Invalid macro format found at \"" + _macroList +
        "\": Missing macro initiator character (" + MacroBegin + ")");
    }
  }

  /// Load the property and value of a macro.
  /// Given a validated macro, extract the property and value for storage
  /// @param macro The valid macro: {begin}{property}{sep}{value}{end}
  /// @throws runtime_error The macro was not formatted properly
  /// @throws runtime_error The property was blank or a duplicate
  void MacroCollection::ParseMacro(const std::string& macro)
  {
    std::istringstream iss(macro);
    std::string property, value;

    // Skip the leading macro initiator character
    if (iss.get() != MacroBegin) {
      std::runtime_error(
        "Invalid macro found at \"" + iss.str() + "\". "
        "Unexpected macro initiator character (" + MacroBegin + ")");
    }

    // Split the macro into component parts for storage
    if (!std::getline(iss, property, MacroSep)) {
      std::runtime_error(
        "Invalid macro found at \"" + iss.str() + "\". "
        "Unable to extract the required items");
    }

    if (!std::getline(iss, value, MacroEnd)) {
      std::runtime_error(
        "Invalid macro found at \"" + iss.str() + "\". "
        "Unable to extract the required items");
    }

#ifdef DEV_DEBUG
    std::cout << "Extracted property: \"" << property << "\"\n"
      << "Extracted value: \"" << value << "\"\n";
#endif

    // Properties correspond to class 
    // data fields and can't be blank
    if (property.length() == 0) {
      throw std::runtime_error(
        "Invalid macro found at \"" + macro +
        "\": Blank properties are not allowed");
    }

    // Duplicate properties are not allowed to avoid 
    // confusion about potentially unexpected values
    if (_macros.find(property) != _macros.end()) {
      throw std::runtime_error(
        "Invalid macro found at \"" + macro + "\": A macro for " +
        "the " + property + " property has already been extracted");
    }

    _macros[property] = value;
  }
}

//
// Sample usage of MacroCollection
//
class Person {
  TextUtil::MacroCollection init;
  std::string _firstName;
  std::string _lastName;
  int _age;
public:
  Person(const std::string& macroInitText);
  friend std::ostream& operator<<(std::ostream& os, const Person& p);
};

Person::Person(const std::string& macroInitText):
  init(macroInitText),
  _firstName(init.Extract<std::string>("FirstName")),
  _lastName(init.Extract<std::string>("LastName")),
  _age(init.Extract<int>("Age"))
{}

std::ostream& operator<<(std::ostream& os, const Person& p)
{
  return os << p._lastName << ", " << p._firstName << ". Age " << p._age;
}

int main() try
{
  Person me(
    ".FirstName=Radical;"
    ".LastName=Edward;"
    ".Age=23;"
    );

  std::cout << me << '\n';
} catch ( const std::exception& ex ) {
  std::cerr << ex.what() << '\n';
}
Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.