1
0
forked from Alepha/Alepha
This commit is contained in:
2024-07-08 17:06:31 -04:00
parent 7b449fb3e1
commit 6122f3ba80
5 changed files with 696 additions and 0 deletions

View File

@ -44,6 +44,7 @@ add_subdirectory( assertion.test )
add_subdirectory( Constness.test ) add_subdirectory( Constness.test )
add_subdirectory( Capabilities.test ) add_subdirectory( Capabilities.test )
add_subdirectory( delimited_list.test ) add_subdirectory( delimited_list.test )
add_subdirectory( UniversalAggregate.test )
# Sample applications # Sample applications
add_executable( example example.cc ) add_executable( example example.cc )

282
UA_Key.h Normal file
View File

@ -0,0 +1,282 @@
static_assert( __cplusplus > 2020'99 );
#pragma once
#include <Alepha/Alepha.h>
#include <vector>
#include <string>
#include <string_view>
#include <variant>
#include <stdexcept>
#include <boost/lexical_cast.hpp>
/*!
* @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;
}

207
UniversalAggregate.h Normal file
View File

@ -0,0 +1,207 @@
static_assert( __cplusplus > 2020'99 );
#pragma once
#include <Alepha/Alepha.h>
#include <Alepha/UA_Key.h>
#include <map>
#include <string>
#include <any>
#include <iostream>
#include <vector>
/*!
* @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<UniversalAggregate>(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<UniversalAggregate>(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;
}

View File

@ -0,0 +1,205 @@
static_assert( __cplusplus > 2020'99 );
#include <functional>
#include <Alepha/UniversalAggregate.h>
#include <Alepha/Exception.h>
#include <Alepha/IOStreams/OStreamable.h>
#include <Alepha/Testing/test.h>
#include <Alepha/Testing/TableTest.h>
/*!
* @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<std::string>( 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<std::string>( 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<std::string>( 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<std::string>( 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<std::string>( ua.at( "tony" ) ) == "bonnie";
}
catch( ... )
{
return false;
}
};
};
}

View File

@ -0,0 +1 @@
unit_test( 0 )