From 6122f3ba8031499cbccccf76a7fd9d17823feb75 Mon Sep 17 00:00:00 2001 From: Tony Date: Mon, 8 Jul 2024 17:06:31 -0400 Subject: [PATCH] UA --- CMakeLists.txt | 1 + UA_Key.h | 282 +++++++++++++++++++++++++ UniversalAggregate.h | 207 ++++++++++++++++++ UniversalAggregate.test/0.cc | 205 ++++++++++++++++++ UniversalAggregate.test/CMakeLists.txt | 1 + 5 files changed, 696 insertions(+) create mode 100644 UA_Key.h create mode 100644 UniversalAggregate.h create mode 100644 UniversalAggregate.test/0.cc create mode 100644 UniversalAggregate.test/CMakeLists.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index b9f4118..890f2ba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,7 @@ add_subdirectory( assertion.test ) add_subdirectory( Constness.test ) add_subdirectory( Capabilities.test ) add_subdirectory( delimited_list.test ) +add_subdirectory( UniversalAggregate.test ) # Sample applications add_executable( example example.cc ) diff --git a/UA_Key.h b/UA_Key.h new file mode 100644 index 0000000..e181f35 --- /dev/null +++ b/UA_Key.h @@ -0,0 +1,282 @@ +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; +} + diff --git a/UniversalAggregate.h b/UniversalAggregate.h new file mode 100644 index 0000000..25385f8 --- /dev/null +++ b/UniversalAggregate.h @@ -0,0 +1,207 @@ +static_assert( __cplusplus > 2020'99 ); + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +/*! + * @file + * @todo consider dropping string arg versions in favor of just using UA_Key versions (after making conversion constructor not explicit) + * @todo consider how operator >> and << should function. JSON? If so, using what? + * @todo consider [] versions of at + * @todo decide exceptions to throw (especially for not found) + * @todo comment at() + * @todo auto creating option for get (and for general use) + * @todo need to constrain type stored in UniversalAggregate and coerce to those when possible +*/ + +namespace Alepha::Hydrogen ::detail:: UniversalAggregate_m +{ + inline namespace exports + { + class UniversalAggregate; + + using MemberType= std::variant< UniversalAggregate, std::string, int, std::vector< UniversalAggregate > >; + + class UniversalAggregate + { + private: + // The value could be a UA, a vector of UAs or anything else. + using CoreMap= std::map< std::string, MemberType >; + CoreMap data; + + UniversalAggregate & + get( UA_Key key, bool create= false ) + { + UniversalAggregate *ret= this; + for( const auto &lv : key.levels ) + { + if( std::holds_alternative< std::string >(lv) ) + { + if( not ret->data.count( std::get< std::string >( lv ) ) ) + { + if( not create ) // throw an error + { + throw std::runtime_error( "desired level not found: " + std::get< std::string >( lv ) ); + } + else // create the missing level + { + ret->data[ std::get< std::string >( lv ) ]= UniversalAggregate{}; + } + } + ret= &(std::get(ret->data.at( std::get< std::string >( lv ) ) ) ); + continue; + } + } + return *ret; + } + + const UniversalAggregate & + get( UA_Key key ) const + { + const UniversalAggregate *ret= this; + for( const auto &lv : key.levels ) + { + if( std::holds_alternative< std::string >(lv) ) + { + if( not ret->data.count( std::get< std::string >( lv ) ) ) + { + throw std::runtime_error( "desired level not found: " + std::get< std::string >( lv ) ); + } + ret= &(std::get(ret->data.at( std::get< std::string >( lv ) ) ) ); + continue; + } + } + return *ret; + } + + public: + UniversalAggregate()= default; + + /*! @brief + * @note direct insert on a vector is not supported. Use at() to obtain a reference to the vector + */ + void + insert( const UA_Key &key, const MemberType &value ) + { + if( key.isVector() ) + { + throw std::runtime_error( "Operation not supported on vector" ); + } + auto& target= get( key.parent(), true ); + target.data[ std::get< std::string >( key.levels.back() ) ]= value; + } + + /*! @brief + * + */ + void + insert( const std::string &key, const MemberType &value ) + { + insert( UA_Key( key ), value ); + } + + /*! @brief + * @note direct erase on a vector is not supported. Use at() to obtain a reference to the vector + */ + void + erase( const UA_Key &key ) + { + if( key.isVector() ) + { + throw std::runtime_error( "Operation not supported on vector" ); + } + auto& target= get( key.parent() ); + target.data.erase( std::get< std::string >( key.levels.back() ) ); + return; + } + + /*! @brief + * + */ + void + erase( const std::string &key ) + { + erase( UA_Key( key ) ); + } + + /*! @brief + * + */ + const MemberType & + at( const std::string &key ) const + { + return at( UA_Key( key ) ); + } + + /*! @brief + * + */ + const MemberType & + at( const UA_Key &key ) const + { + if( key.isVector() ) + { + throw std::runtime_error( "Operation not supported on vector" ); + } + auto& target= get( key.parent() ); + return target.data.at( std::get< std::string >( key.levels.back() ) ); + } + + /*! @brief + * + */ + MemberType & + at( const std::string &key ) + { + return at( UA_Key( key ) ); + } + + /*! @brief + * + */ + MemberType & + at( const UA_Key &key ) + { + if( key.isVector() ) + { + throw std::runtime_error( "Operation not supported on vector" ); + } + auto& target= get( key.parent() ); + return target.data.at( std::get< std::string >( key.levels.back() ) ); + } + + /*! @brief + * + */ + size_t + count( const std::string &key ) + { + return 0; + } + + /*! @brief + * @note direct count on a vector is not supported. Use at() to obtain a reference to the vector + */ + size_t + count( const UA_Key &key ) + { + return 0; + } + }; + } + +} + +namespace Alepha::Hydrogen::inline exports::inline UniversalAggregate_m +{ + using namespace detail::UniversalAggregate_m::exports; +} + diff --git a/UniversalAggregate.test/0.cc b/UniversalAggregate.test/0.cc new file mode 100644 index 0000000..f814b5d --- /dev/null +++ b/UniversalAggregate.test/0.cc @@ -0,0 +1,205 @@ +static_assert( __cplusplus > 2020'99 ); + +#include + +#include +#include +#include + +#include +#include + +/*! + * @file + * @todo test isVector + * @todo test operator/ + * @todo test from vector constructor + * @todo test skipping constructor + * @todo test make skipping constructor default to 0 or add copy constructor + * @todo more UA tests + */ + +namespace +{ + using namespace Alepha::Testing::exports; + + Alepha::UniversalAggregate ua; + + std::string + validUA_Key( std::string s ) + { + Alepha::UA_Key uak( s ); + return Alepha::IOStreams::String{} << uak; + }; + + bool + doesUA_KeyThrow( const std::string &s ) + { + try + { + Alepha::UA_Key uak( s ); + } + catch( ... ) + { + return true; + } + return false; + } + + std::string + parentOfUA_Key( std::string s ) + { + Alepha::UA_Key uak( s ); + return Alepha::IOStreams::String{} << uak.parent(); + }; + + std::string + backOfUA_Key( std::string s ) + { + Alepha::UA_Key uak( s ); + return Alepha::IOStreams::String{} << uak.back(); + }; + + std::string + istreamUA_Key( std::string s ) + { + std::istringstream is(s); + Alepha::UA_Key uak; + is >> uak; + return Alepha::IOStreams::String{} << uak; + }; + + auto UA_Key_tests= Alepha::Utility::enroll <=[] + { + "UA_Key.Valid"_test <=TableTest< validUA_Key >::Cases + { + { "1k", { "tony" }, "tony" }, + { "2k", { "tony.bonnie" }, "tony.bonnie" }, + { "1k1i", { "tony[5]" }, "tony[5]" }, + { "1k2i1k1i", { "tony[5][6].bonnie[7]" }, "tony[5][6].bonnie[7]" }, + }; + + "UA_Key.Exceptions"_test <=TableTest< doesUA_KeyThrow >::Cases + { + { "good", { "tony" }, false }, + { "bad-brackets-1", { "tony[[" }, true }, + { "bad-brackets-2", { "tony[" }, true }, + { "bad-brackets-3", { "tony]" }, true }, + { "bad-leading-dot", { ".tony" }, true }, + { "bad-leading-open", { "[tony" }, true }, + { "bad-leading-close", { "]tony" }, true }, + { "bad-index-empty", { "tony[]" }, true }, + { "bad-index-alpha", { "tony[a]" }, true }, + { "bad-index-containsAlpha", { "tony[1a3]" }, true }, + { "bad-index-containsSpace", { "tony[1 3]" }, true }, + { "bad-name-empty", { "tony..Bonnie" }, true }, + { "bad-name-leadingNumber1", { "2tony.Bonnie" }, true }, + { "bad-name-leadingNumber2", { "tony.2Bonnie" }, true }, + { "bad-name-illegalChar", { "to%ny" }, true }, + { "bad-name-containsSpace", { "to ny" }, true }, + }; + + "UA_Key.Parent"_test <=TableTest< parentOfUA_Key >::Cases + { + { "2lev", { std::string("tony.bonnie") }, "tony" }, + { "2lev[]", { std::string("tony[5]") }, "tony" }, + { "3lev", { std::string("tony[3].bonnie") }, "tony[3]" }, + { "1lev", { std::string("tony") }, "" }, + }; + + "UA_Key.istream"_test <=TableTest< istreamUA_Key >::Cases + { + { "2lev", { std::string("tony.bonnie") }, "tony.bonnie" }, + { "2lev[]", { std::string("tony[5]") }, "tony[5]" }, + { "3lev", { std::string("tony[3].bonnie") }, "tony[3].bonnie" }, + { "1lev", { std::string("tony") }, "tony" }, + }; + + "UA_Key.Back"_test <=TableTest< backOfUA_Key >::Cases + { + { "2lev", { std::string("tony.bonnie") }, "bonnie" }, + { "2lev[]", { std::string("tony[5]") }, "5" }, + { "3lev", { std::string("tony[3].bonnie") }, "bonnie" }, + { "1lev", { std::string("tony") }, "tony" }, + }; + + "UA_KeyDefaultConstructor"_test <=[] () -> bool + { + Alepha::UA_Key uak; + std::ostringstream oss; + oss << uak; + return ( oss.str() == "" ); + }; + }; + + auto UA_test= Alepha::Utility::enroll <=[] + { + "UA_Simple1"_test <=[] () -> bool + { + Alepha::UniversalAggregate ua; + ua.insert( "tony", std::string( "bonnie" ) ); + return std::get( ua.at( "tony" ) ) == "bonnie"; + }; + + "UA_Simple1Const"_test <=[] () -> bool + { + Alepha::UniversalAggregate ua; + ua.insert( "tony", std::string( "bonnie" ) ); + const Alepha::UniversalAggregate ub= ua; + return std::get( ub.at( "tony" ) ) == "bonnie"; + }; + + "UA_Simple2"_test <=[] () -> bool + { + Alepha::UniversalAggregate ua, ub; + ua.insert( "tony", std::string( "bonnie" ) ); + ua.insert( "gina", ub ); + ua.insert( "gina.bobby", std::string("sam") ); + return std::get( ua.at( "gina.bobby" ) ) == "sam"; + }; + + "UA_GetCreate"_test <=[] () -> bool // creates missing level + { + Alepha::UniversalAggregate ua, ub; + ua.insert( "tony", std::string( "bonnie" ) ); + ua.insert( "gina", ub ); + ua.insert( "gina.bobby.bill", std::string("sam") ); + return std::get( ua.at( "gina.bobby.bill" ) ) == "sam"; + }; + + "UA_Erase1"_test <=[] () -> bool + { + Alepha::UniversalAggregate ua, ub; + ua.insert( "tony", std::string( "bonnie" ) ); + ua.insert( "gina", ub ); + ua.erase( "tony" ); + try + { + ua.at( "tony" ); + return false; + } + catch( ... ) + { + return true; + } + }; + + "UA_Erase2"_test <=[] () -> bool + { + Alepha::UniversalAggregate ua, ub; + ua.insert( "tony", std::string( "bonnie" ) ); + ua.insert( "gina", ub ); + ua.erase( "gina" ); + try + { + return std::get( ua.at( "tony" ) ) == "bonnie"; + } + catch( ... ) + { + return false; + } + }; + + + }; +} diff --git a/UniversalAggregate.test/CMakeLists.txt b/UniversalAggregate.test/CMakeLists.txt new file mode 100644 index 0000000..b099603 --- /dev/null +++ b/UniversalAggregate.test/CMakeLists.txt @@ -0,0 +1 @@ +unit_test( 0 )