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

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;
}