static_assert( __cplusplus > 2020'99 ); #pragma once #include #include #include #include #include #include #include /*! * @file * @todo validate levels for externals sources like vector constructor, operator / * @todo better exceptions * @todo switch both isValid to use regex if appropriate * @todo factory method for Level from a string, then possibly use it in UA_Key constructor */ namespace Alepha::Hydrogen ::detail:: UA_Key_m { inline namespace exports { using namespace std::literals::string_literals; /*! @brief Used as a compound key to address an element in a (possibly) multi level Universal Aggregate) * @detail Format is that it must start with a name (string) and then is followed by a series of names or vector indices. * Names are separated with a period if consecutive and indices are contained in brackets. * Names are alphanumeric starting with an alpha. Indices are non negative ints. * A Name is used a key for a map in the UniversalAggregate * Correct examples: key * key[1] * key.key2 * key[3][4]key2[2] * key.key.key * Bad examples: 3key starts with a period * key.[2] period not supported between key and bracket * key[2].key period not supported between key and bracket */ class UA_Key { private: /*! @brief Determines if a stringview represents a valid name according to our definition * @details Currently required to start with an alpha and contain only alphanumerics. */ bool isValidAsName( const std::string_view sv ) const { return true and ( not sv.empty() ) and ( std::isalpha( sv.at( 0 ) ) ) and ( std::find_if_not( sv.cbegin() + 1, sv.cend(), []( const unsigned char c ){ return std::isalnum( c ); } ) == sv.end() ); } /*! @brief Determines if a stringview represents a valid index (i.e., valid size_t representation, no check is done against an actual vector) * @details Currently required to represent a non negative integer. */ bool isValidAsIndex( const std::string_view sv ) const { return true and ( not sv.empty() ) and ( std::find_if_not( sv.cbegin(), sv.cend(), []( const unsigned char c ){ return std::isdigit( c ); } ) == sv.end() ); } public: /*! @brief Type for a UA key for a map */ using NameType= std::string; /*! @brief Type for a UA index for a vector */ using IndexType= size_t; /*! @brief A representation of either a name for a UA map lookup or an index for a vector */ class Level: public std::variant< size_t, std::string > { friend std::ostream & operator << ( std::ostream &os, const Level &lv ) { std::visit( [&]( const auto &val ) { os << val; }, lv ); return os; } }; /*! @brief Stores the parsed levels */ std::vector< Level > levels; /*! @brief The default empty constructor is fine */ UA_Key()= default; /*!@brief construct from a vector of Level */ explicit UA_Key( const std::vector< Level > &lev ) : levels( lev ) { } /*! @brief construct from another UA_Key with option to skip some of the levels at the front source UA_Key */ explicit UA_Key( const UA_Key &ua, const IndexType skip ) : levels( ua.levels.cbegin() + std::min( skip, ua.levels.size() ), ua.levels.cend() ) { } /*!@brief construct from a stringview * @details see class documentation for format explanation\n */ explicit UA_Key( const std::string_view sv ) { size_t levelStart= 0; // index to the first char of the level being parsed bool inIndex= false; // set to true when parsing of this level is after an open bracket bool postIndex= false; // set to true when parsing of this level is after a closed bracket - next char must be a period or open bracket while( levelStart < sv.size() ) { char term; // which terminating char is encountered auto pos= sv.find_first_of( "[].", levelStart ); if( pos == std::string::npos ) { if( inIndex ) { throw std::runtime_error( "Unable to parse UA_Key(1) from " + std::string( sv ) ); } else { term= '.'; pos= sv.size(); } } else { term= sv.at( pos ); } const std::string_view candidate= sv.substr( levelStart, pos - levelStart ); levelStart= pos + 1; if( postIndex ) { if( candidate.size() != 0 or term == ']' ) { throw std::runtime_error( "Unable to parse UA_Key(2) from " + std::string( sv ) ); } postIndex= false; inIndex= ( term == '[' ); } else if( ( term == '.' or term == '[' ) and not inIndex and isValidAsName( candidate ) ) { levels.push_back( Level( std::string( candidate ) ) ); inIndex= ( term == '[' ); } else if( term == ']' and inIndex and isValidAsIndex( candidate ) ) { levels.push_back( Level( ::boost::lexical_cast< size_t >( candidate ) ) ); inIndex= false; postIndex= true; } else { throw std::runtime_error( "Unable to parse UA_Key(3) from " + std::string( sv ) ); } } if( inIndex ) { throw std::runtime_error( "Unable to parse UA_Key(4) from " + std::string( sv ) ); } } /*! @brief default == and != are fine. Other comparisons are not useful for now. * @details Perhaps an imposed ordering could be used if sorting were to be needed. */ bool operator == ( const UA_Key & ) const= default; /*! @brief returns true if the lowest level is a vector index. * @note, false will be returned if there are no levels */ bool isVector() const { return true and ( not levels.empty() ) and ( std::holds_alternative< size_t >( levels.back() ) ) ; } /*! @brief returns a UA_Key formed by removing the last level from a copy of this UA_Key * @note, current behavior mimics that of vector.pop_back() in that if the this UA_Key has no levels, the behavior is undefined */ UA_Key parent() const { UA_Key ret= *this; ret.levels.pop_back(); return ret; } /*! @brief returns a reference to the last level * @note, current behavior mimics that of vector.back() in that if the this UA_Key has no levels, the behavior is undefined */ Level & back() { if( levels.empty() ) { throw std::runtime_error( "back attempted on UA_Key with 0 Levels " ); } return levels.back(); } /*! @brief returns a const reference to the last level * @note, current behavior mimics that of vector.back() in that if the this UA_Key has no levels, the behavior is undefined */ const Level & back() const { return levels.back(); } /*! @brief appends a level to a copy of this UA_Key * */ UA_Key operator / ( const Level &lv ) { UA_Key ret= *this; ret.levels.push_back( lv ); return ret; } /*! @brief outputs a character representation of the levels * @note, as long as the current restriction against whitespace on a string parses into a UA_Key remains, the operator << will produce an output identical to the input */ friend std::ostream & operator << ( std::ostream &os, const UA_Key &uak ) { bool first= true; for( const auto lv: uak.levels ) { std::visit ( [&]< typename T >( const T &t ) { if constexpr( std::is_same_v< std::string, T > ) { if( not first ) { os << '.'; } os << std::get< std::string >( lv ); } else if constexpr( std::is_same_v< size_t, T > ) { os << '[' << std::get< size_t >( lv ) << ']'; } }, lv ); first= false; } return os; } /*! @brief istream operator. Reads a string * @details result is the same as if the istream is read into a string and the string were used to construct a UA_Key and then copied ot uak uak. */ friend std::istream & operator >> ( std::istream &is, UA_Key &uak ) { std::string input_string; is >> input_string; uak= UA_Key( input_string ); return is; } }; } } namespace Alepha::Hydrogen::inline exports::inline UA_Key_m { using namespace detail::UA_Key_m::exports; }