Compare commits
10 commits
876b2e37bd
...
ed6dc601cc
Author | SHA1 | Date | |
---|---|---|---|
ed6dc601cc | |||
9dc5054fc0 | |||
c1e776064d | |||
dd75c91403 | |||
67d28c276b | |||
a2b0361d86 | |||
04487e3a75 | |||
167b8bee8d | |||
405b68b824 | |||
f9c6e9e7cb |
32 changed files with 1862 additions and 2 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -1,4 +1,10 @@
|
|||
# ---> C++
|
||||
# LSP Cache
|
||||
.cache/
|
||||
|
||||
# Build dirs
|
||||
build/
|
||||
|
||||
# Prerequisites
|
||||
*.d
|
||||
|
||||
|
@ -31,4 +37,3 @@
|
|||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
|
||||
|
|
8
CMake/Catch2.cmake
Normal file
8
CMake/Catch2.cmake
Normal file
|
@ -0,0 +1,8 @@
|
|||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
Catch2
|
||||
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
|
||||
GIT_TAG v3.5.0
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(Catch2)
|
28
CMake/sqlite3.cmake
Normal file
28
CMake/sqlite3.cmake
Normal file
|
@ -0,0 +1,28 @@
|
|||
include(FetchContent)
|
||||
|
||||
FetchContent_Declare(
|
||||
sqlite3
|
||||
URL https://www.sqlite.org/2025/sqlite-amalgamation-3490100.zip
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(
|
||||
sqlite3
|
||||
)
|
||||
|
||||
add_library(
|
||||
sqlite3
|
||||
STATIC
|
||||
)
|
||||
|
||||
target_include_directories(
|
||||
sqlite3
|
||||
PUBLIC
|
||||
${sqlite3_SOURCE_DIR}
|
||||
)
|
||||
|
||||
target_sources(
|
||||
sqlite3
|
||||
PRIVATE
|
||||
${sqlite3_SOURCE_DIR}/sqlite3.c
|
||||
)
|
11
CMakeLists.txt
Normal file
11
CMakeLists.txt
Normal file
|
@ -0,0 +1,11 @@
|
|||
cmake_minimum_required(VERSION 3.21)
|
||||
|
||||
project(Garbage
|
||||
LANGUAGES C CXX
|
||||
)
|
||||
|
||||
add_subdirectory(Garbage)
|
||||
|
||||
if(PROJECT_IS_TOP_LEVEL)
|
||||
add_subdirectory(Tests)
|
||||
endif()
|
11
Garbage/CMakeLists.txt
Normal file
11
Garbage/CMakeLists.txt
Normal file
|
@ -0,0 +1,11 @@
|
|||
add_library(Garbage INTERFACE)
|
||||
|
||||
target_compile_features(Garbage
|
||||
INTERFACE
|
||||
cxx_std_20
|
||||
)
|
||||
|
||||
target_include_directories(Garbage
|
||||
INTERFACE
|
||||
"Include"
|
||||
)
|
170
Garbage/Include/Garbage/Base64.hpp
Normal file
170
Garbage/Include/Garbage/Base64.hpp
Normal file
|
@ -0,0 +1,170 @@
|
|||
#ifndef GARBAGE_BASE64_HPP
|
||||
#define GARBAGE_BASE64_HPP
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace Garbage::Base64
|
||||
{
|
||||
namespace Internal
|
||||
{
|
||||
struct SmallBuffer
|
||||
{
|
||||
public:
|
||||
[[nodiscard]] constexpr
|
||||
SmallBuffer()
|
||||
: mStoredBits(0), mData(0)
|
||||
{}
|
||||
|
||||
constexpr
|
||||
void Enqueue(std::uint8_t byte, std::size_t bits = 8)
|
||||
{
|
||||
std::uint8_t mask = (1u << bits) - 1;
|
||||
mData = (mData << bits) | (byte & mask);
|
||||
mStoredBits += bits;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr
|
||||
std::uint8_t Dequeue(std::size_t bits = 8)
|
||||
{
|
||||
std::uint64_t mask = (1u << bits) - 1;
|
||||
mStoredBits -= bits;
|
||||
std::uint64_t out = (mData >> mStoredBits) & mask;
|
||||
return out;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr
|
||||
std::size_t Size()
|
||||
{
|
||||
return mStoredBits;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr
|
||||
std::size_t Capacity()
|
||||
{
|
||||
return 64;
|
||||
}
|
||||
private:
|
||||
std::size_t mStoredBits;
|
||||
std::uint64_t mData;
|
||||
};
|
||||
|
||||
static constexpr auto EncodeLUT = std::array {
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
|
||||
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
|
||||
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
|
||||
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
|
||||
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
|
||||
'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
|
||||
'w', 'x', 'y', 'z', '0', '1', '2', '3',
|
||||
'4', '5', '6', '7', '8', '9', '+', '/',
|
||||
};
|
||||
|
||||
static constexpr std::size_t DecodeOffset = '+';
|
||||
static constexpr auto DecodeLUT = std::array {
|
||||
0b111'110, /* + */ 0b000'000, /*---*/ 0b000'000, /*---*/ 0b000'000, /*---*/
|
||||
0b111'111, /* / */ 0b110'100, /* 0 */ 0b110'101, /* 1 */ 0b110'110, /* 2 */
|
||||
0b110'111, /* 3 */ 0b111'000, /* 4 */ 0b111'001, /* 5 */ 0b111'010, /* 6 */
|
||||
0b111'011, /* 7 */ 0b111'100, /* 8 */ 0b111'101, /* 9 */ 0b000'000, /*---*/
|
||||
0b000'000, /*---*/ 0b000'000, /*---*/ 0b000'000, /*---*/ 0b000'000, /*---*/
|
||||
0b000'000, /*---*/ 0b000'000, /*---*/ 0b000'000, /* A */ 0b000'001, /* B */
|
||||
0b000'010, /* C */ 0b000'011, /* D */ 0b000'100, /* E */ 0b000'101, /* F */
|
||||
0b000'110, /* G */ 0b000'111, /* H */ 0b001'000, /* I */ 0b001'001, /* J */
|
||||
0b001'010, /* K */ 0b001'011, /* L */ 0b001'100, /* M */ 0b001'101, /* N */
|
||||
0b001'110, /* O */ 0b001'111, /* P */ 0b010'000, /* Q */ 0b010'001, /* R */
|
||||
0b010'010, /* S */ 0b010'011, /* T */ 0b010'100, /* U */ 0b010'101, /* V */
|
||||
0b010'110, /* W */ 0b010'111, /* X */ 0b011'000, /* Y */ 0b011'001, /* Z */
|
||||
0b000'000, /*---*/ 0b000'000, /*---*/ 0b000'000, /*---*/ 0b000'000, /*---*/
|
||||
0b000'000, /*---*/ 0b000'000, /*---*/ 0b011'010, /* a */ 0b011'011, /* b */
|
||||
0b011'100, /* c */ 0b011'101, /* d */ 0b011'110, /* e */ 0b011'111, /* f */
|
||||
0b100'000, /* g */ 0b100'001, /* h */ 0b100'010, /* i */ 0b100'011, /* j */
|
||||
0b100'100, /* k */ 0b100'101, /* l */ 0b100'110, /* m */ 0b100'111, /* n */
|
||||
0b101'000, /* o */ 0b101'001, /* p */ 0b101'010, /* q */ 0b101'011, /* r */
|
||||
0b101'100, /* s */ 0b101'101, /* t */ 0b101'110, /* u */ 0b101'111, /* v */
|
||||
0b110'000, /* w */ 0b110'001, /* x */ 0b110'010, /* y */ 0b110'011, /* z */
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr
|
||||
std::string Encode(std::span<std::uint8_t> data) {
|
||||
std::string result;
|
||||
result.reserve(data.size() + data.size() / 2);
|
||||
|
||||
Internal::SmallBuffer buffer;
|
||||
std::size_t i = 0;
|
||||
while (i < data.size()) {
|
||||
while (buffer.Capacity() - buffer.Size() >= 8 && i < data.size()) {
|
||||
buffer.Enqueue(data[i++]);
|
||||
}
|
||||
while (buffer.Size() >= 6) {
|
||||
result.push_back(Internal::EncodeLUT[buffer.Dequeue(6)]);
|
||||
}
|
||||
}
|
||||
if (buffer.Size() > 0) {
|
||||
std::size_t remainingBits = buffer.Size();
|
||||
std::uint8_t data = (buffer.Dequeue(remainingBits) << (6 - remainingBits));
|
||||
result.push_back(Internal::EncodeLUT[data]);
|
||||
}
|
||||
if (data.size() % 3) {
|
||||
result.append(std::string(4 - (result.size() % 4), '='));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr
|
||||
std::vector<std::uint8_t> Decode(std::string_view data) {
|
||||
std::vector<std::uint8_t> result;
|
||||
result.reserve(data.size());
|
||||
|
||||
std::size_t end = data.size();
|
||||
end -= (data.size() > 0 && data[data.size() - 1] == '=');
|
||||
end -= (data.size() > 1 && data[data.size() - 2] == '=');
|
||||
|
||||
Internal::SmallBuffer buffer;
|
||||
std::size_t i = 0;
|
||||
while (i < end) {
|
||||
while (buffer.Capacity() - buffer.Size() >= 6 && i < end) {
|
||||
buffer.Enqueue(Internal::DecodeLUT[data[i++] - Internal::DecodeOffset], 6);
|
||||
}
|
||||
while (buffer.Size() >= 8) {
|
||||
result.push_back(buffer.Dequeue(8));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr
|
||||
bool Validate(std::string_view data)
|
||||
{
|
||||
// Technically superfluous - the decoder can deal with unpadded data.
|
||||
if (data.size() % 4 != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::size_t i = 0;
|
||||
for (char ch : data) {
|
||||
bool condition = (ch == '+') ||
|
||||
(ch >= '/' && ch <= '9') ||
|
||||
(ch >= 'A' && ch <= 'Z') ||
|
||||
(ch >= 'a' && ch <= 'z');
|
||||
if (!condition) {
|
||||
// Allow '=' for the last two character
|
||||
if (i >= data.size() - 2 && ch == '=') {
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
#endif // GARBAGE_BASE64_HPP
|
742
Garbage/Include/Garbage/SQLite.hpp
Normal file
742
Garbage/Include/Garbage/SQLite.hpp
Normal file
|
@ -0,0 +1,742 @@
|
|||
#ifndef GARBAGE_SQLITE_HPP
|
||||
#define GARBAGE_SQLITE_HPP
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
namespace Garbage::SQLite
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// TRAIT SUPPORT
|
||||
|
||||
template <typename T>
|
||||
struct MemberTraits;
|
||||
|
||||
template <typename T, typename U>
|
||||
struct MemberTraits<T U::*>
|
||||
{
|
||||
using Member = T;
|
||||
using Container = U;
|
||||
};
|
||||
|
||||
namespace Implementation
|
||||
{
|
||||
template <typename... Ts>
|
||||
struct First;
|
||||
|
||||
template <typename T, typename... Ts>
|
||||
struct First<T, Ts...>
|
||||
{
|
||||
using Type = T;
|
||||
};
|
||||
}
|
||||
|
||||
template <typename... Ts>
|
||||
using First = Implementation::First<Ts...>::Type;
|
||||
|
||||
namespace Implementation
|
||||
{
|
||||
template <template <typename> typename Template, typename T>
|
||||
struct IsSpecialization
|
||||
{
|
||||
static constexpr bool Value = false;
|
||||
};
|
||||
|
||||
template <template <typename> typename Template, typename... Args>
|
||||
struct IsSpecialization<Template, Template<Args...>>
|
||||
{
|
||||
static constexpr bool Value = true;
|
||||
};
|
||||
|
||||
template <template <auto> typename Template, typename T>
|
||||
struct IsValueSpecialization
|
||||
{
|
||||
static constexpr bool Value = false;
|
||||
};
|
||||
|
||||
template <template <auto> typename Template, auto... Args>
|
||||
struct IsValueSpecialization<Template, Template<Args...>>
|
||||
{
|
||||
static constexpr bool Value = true;
|
||||
};
|
||||
}
|
||||
|
||||
template <template <typename> typename Template, typename T>
|
||||
concept IsSpecialization = Implementation::IsSpecialization<Template, T>::Value;
|
||||
|
||||
template <template <auto> typename Template, typename T>
|
||||
concept IsValueSpecialization = Implementation::IsValueSpecialization<Template, T>::Value;
|
||||
|
||||
template <typename T1, typename T2>
|
||||
concept IsSame = std::is_same_v<T1, T2>;
|
||||
|
||||
template <typename T, typename... Ts>
|
||||
concept IsAnyOf = (IsSame<T, Ts> || ...);
|
||||
|
||||
template <template <typename> typename Wrapper, typename T, typename... Ts>
|
||||
concept IsAnyInstanceOf = IsAnyOf<T, Wrapper<Ts>...>;
|
||||
|
||||
namespace Implementation
|
||||
{
|
||||
template <typename Arg, typename... Ts>
|
||||
struct FirstWithArg
|
||||
{
|
||||
using Type = void;
|
||||
};
|
||||
|
||||
template <typename Arg, template <typename> typename T, typename... Ts, typename... Args>
|
||||
struct FirstWithArg<Arg, T<Args...>, Ts...>
|
||||
{
|
||||
using Type = std::conditional_t<IsSame<Arg, typename First<Args...>::Type>,
|
||||
T<Args...>, typename FirstWithArg<Arg, Ts...>::Type>;
|
||||
};
|
||||
}
|
||||
|
||||
template <typename Arg, typename... Ts>
|
||||
using FirstWithArg = Implementation::FirstWithArg<Arg, Ts...>::Type;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// UTILITY FUNCTIONS
|
||||
|
||||
template <typename DestinationType, typename... TupleTypes>
|
||||
[[nodiscard]] constexpr
|
||||
auto ToArray(std::tuple<TupleTypes...>&& tuple)
|
||||
{
|
||||
constexpr auto makeArray = [] (auto&&... args) {
|
||||
return std::array{ std::forward(args)... };
|
||||
};
|
||||
return std::apply(makeArray, tuple);
|
||||
}
|
||||
|
||||
template <typename T, std::size_t N>
|
||||
[[nodiscard]] constexpr
|
||||
auto ToArray(auto (&&array)[N])
|
||||
{
|
||||
std::array<T, N> result;
|
||||
std::move(std::begin(array), std::end(array), result.begin());
|
||||
return result;
|
||||
}
|
||||
|
||||
// Note(3011): The mutating version is easily addable if need be.
|
||||
template <typename... Ts>
|
||||
constexpr
|
||||
void ForEach(auto&& multifunc, const std::tuple<Ts...>& tuple)
|
||||
{
|
||||
auto conditionalInvoke = [] (auto& multifunc, const auto& item) {
|
||||
if constexpr (std::invocable<decltype(multifunc), decltype(item)>) {
|
||||
multifunc(item);
|
||||
}
|
||||
};
|
||||
|
||||
std::apply([&multifunc, &conditionalInvoke] (const auto&... args) {
|
||||
(conditionalInvoke(multifunc, args), ...);
|
||||
}, tuple);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// ERRORS
|
||||
|
||||
class DatabaseError : public std::runtime_error
|
||||
{
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// BASIC TYPES
|
||||
|
||||
using Integer = std::int64_t;
|
||||
using String = std::string;
|
||||
using Real = double;
|
||||
using Blob = std::vector<std::uint8_t>;
|
||||
|
||||
template <typename T>
|
||||
using Optional = std::optional<T>;
|
||||
|
||||
namespace Implementation
|
||||
{
|
||||
template <typename T>
|
||||
struct TypeString;
|
||||
|
||||
template <> struct TypeString<Integer> { static constexpr std::string_view Value = "INTEGER"; };
|
||||
template <> struct TypeString<String > { static constexpr std::string_view Value = "TEXT" ; };
|
||||
template <> struct TypeString<Real > { static constexpr std::string_view Value = "REAL" ; };
|
||||
template <> struct TypeString<Blob > { static constexpr std::string_view Value = "BLOB" ; };
|
||||
|
||||
template <typename T>
|
||||
struct TypeString<Optional<T>>
|
||||
{
|
||||
static constexpr std::string_view Value = TypeString<T>::Value;
|
||||
};
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static constexpr std::string_view TypeString = Implementation::TypeString<T>::Value;
|
||||
|
||||
template <typename T>
|
||||
concept SupportedType = IsAnyOf<T, Integer, String, Real, Blob>
|
||||
|| IsAnyInstanceOf<Optional, T, Integer, String, Real, Blob>;
|
||||
|
||||
// TODO: These are only for development, remove once prepared statements are
|
||||
// implemented.
|
||||
[[nodiscard]] constexpr
|
||||
std::string ToString(Integer i)
|
||||
{
|
||||
return std::to_string(i);
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr
|
||||
std::string ToString(const String& s)
|
||||
{
|
||||
return "'" + s + "'";
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// DATABASE MODEL
|
||||
|
||||
enum class ColumnFlags : std::uint8_t
|
||||
{
|
||||
Nothing = 0b0000'0000,
|
||||
PrimaryKey = 0b0000'0001,
|
||||
Unique = 0b0000'0010,
|
||||
|
||||
LowerBits = 0b0000'1111,
|
||||
UpperBits = 0b1111'0000,
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr
|
||||
ColumnFlags operator| (ColumnFlags a, ColumnFlags b)
|
||||
{
|
||||
using Type = std::underlying_type_t<ColumnFlags>;
|
||||
return ColumnFlags(static_cast<Type>(a) | static_cast<Type>(b));
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr
|
||||
ColumnFlags operator& (ColumnFlags a, ColumnFlags b)
|
||||
{
|
||||
using Type = std::underlying_type_t<ColumnFlags>;
|
||||
return ColumnFlags(static_cast<Type>(a) & static_cast<Type>(b));
|
||||
}
|
||||
|
||||
template <ColumnFlags Flag>
|
||||
[[nodiscard]] constexpr
|
||||
bool IsSet(ColumnFlags a)
|
||||
{
|
||||
return (a & Flag) != ColumnFlags::Nothing;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires std::is_member_object_pointer_v<T>
|
||||
&& SupportedType<typename MemberTraits<T>::Member>
|
||||
class Column
|
||||
{
|
||||
public:
|
||||
using PtrType = T;
|
||||
using DataType = MemberTraits<T>::Member;
|
||||
using TableType = MemberTraits<T>::Container;
|
||||
|
||||
[[nodiscard]] constexpr
|
||||
Column(DataType TableType::*ptr, std::string_view name, ColumnFlags flags = ColumnFlags::Nothing)
|
||||
: mPtr(ptr), mFlags(flags), mName(name)
|
||||
{}
|
||||
|
||||
std::string_view Name() const
|
||||
{
|
||||
return mName;
|
||||
}
|
||||
|
||||
std::string_view Type() const
|
||||
{
|
||||
return TypeString<DataType>;
|
||||
}
|
||||
|
||||
ColumnFlags Flags() const
|
||||
{
|
||||
return mFlags;
|
||||
}
|
||||
|
||||
bool IsOptional() const
|
||||
{
|
||||
// Note: it doesn't work with the Optional alias
|
||||
return IsSpecialization<std::optional, DataType>;
|
||||
}
|
||||
|
||||
std::string Declaration() const
|
||||
{
|
||||
std::string result;
|
||||
result += mName + " " + std::string(Type());
|
||||
if (!IsOptional()) {
|
||||
result += " NOT NULL";
|
||||
}
|
||||
switch (mFlags & ColumnFlags::LowerBits) {
|
||||
break; case ColumnFlags::Nothing:
|
||||
// noop
|
||||
break; case ColumnFlags::PrimaryKey:
|
||||
result += " PRIMARY KEY";
|
||||
break; case ColumnFlags::Unique:
|
||||
result += " UNIQUE";
|
||||
break; default:
|
||||
throw DatabaseError("Invalid column flag for: " + mName);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
DataType TableType::*Ptr() const
|
||||
{
|
||||
return mPtr;
|
||||
}
|
||||
|
||||
const DataType& Get(const TableType& item) const
|
||||
{
|
||||
return item.*mPtr;
|
||||
}
|
||||
private:
|
||||
DataType TableType::*mPtr;
|
||||
ColumnFlags mFlags;
|
||||
std::string mName;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
Column(T, auto) -> Column<T>;
|
||||
|
||||
template <typename T>
|
||||
Column(T, auto, ColumnFlags) -> Column<T>;
|
||||
|
||||
template <std::size_t N>
|
||||
class ForeignKey
|
||||
{
|
||||
public:
|
||||
template <typename StringLike>
|
||||
requires std::is_convertible_v<StringLike, std::string>
|
||||
[[nodiscard]] constexpr
|
||||
ForeignKey(StringLike (&&from)[N], std::string_view table, StringLike (&&to)[N])
|
||||
: mDestinationTable(table)
|
||||
, mFromColumns(ToArray<std::string>(std::move(from)))
|
||||
, mToColumns(ToArray<std::string>(std::move(to)))
|
||||
{}
|
||||
|
||||
std::string Declaration() const
|
||||
{
|
||||
std::string fromColumns;
|
||||
std::string toColumns;
|
||||
for (std::size_t i = 0; i < mFromColumns.size(); ++i) {
|
||||
fromColumns += mFromColumns[i];
|
||||
toColumns += mToColumns[i];
|
||||
if (i < N - 1) {
|
||||
fromColumns += ", ";
|
||||
toColumns += ", ";
|
||||
}
|
||||
}
|
||||
return "FOREIGN KEY(" + fromColumns + ") REFERENCES " + mDestinationTable + "(" + toColumns + ")";
|
||||
}
|
||||
private:
|
||||
std::string mDestinationTable;
|
||||
std::array<std::string, N> mFromColumns;
|
||||
std::array<std::string, N> mToColumns;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept MappableType = requires {
|
||||
requires std::is_aggregate_v<T>;
|
||||
requires std::is_default_constructible_v<T>;
|
||||
requires !std::is_array_v<T>;
|
||||
};
|
||||
|
||||
template <typename T, typename TableType>
|
||||
concept TableEntity = IsValueSpecialization<ForeignKey, T>
|
||||
|| (IsSpecialization<Column, T> && IsSame<TableType, typename T::TableType>);
|
||||
|
||||
template <MappableType T, TableEntity<T>... Entities>
|
||||
requires (sizeof...(Entities) > 0)
|
||||
class Table
|
||||
{
|
||||
public:
|
||||
[[nodiscard]] constexpr
|
||||
Table(std::string_view name, Entities&&... entities)
|
||||
: mName(name), mEntities(std::move(entities)...)
|
||||
{}
|
||||
|
||||
std::string_view Name() const
|
||||
{
|
||||
return mName;
|
||||
}
|
||||
|
||||
template <typename ColumnPtr>
|
||||
std::string_view Name(ColumnPtr ptr) const
|
||||
{
|
||||
std::string_view name;
|
||||
ForEach([&name, &ptr] (const Column<ColumnPtr>& column) {
|
||||
if (ptr == column.Ptr()) {
|
||||
name = column.Name();
|
||||
}
|
||||
}, mEntities);
|
||||
return name;
|
||||
}
|
||||
|
||||
template <typename ColumnPtr>
|
||||
std::string Identifier(ColumnPtr ptr) const
|
||||
{
|
||||
return "$" + mName + "::" + std::string(Name(ptr));
|
||||
}
|
||||
|
||||
std::string Create() const
|
||||
{
|
||||
std::string result;
|
||||
result += "CREATE TABLE IF NOT EXISTS " + mName + " (\n";
|
||||
ForEach([&result] <typename ColumnType> (const Column<ColumnType>& column) {
|
||||
result += " ";
|
||||
result += column.Declaration();
|
||||
result += ",\n";
|
||||
}, mEntities);
|
||||
ForEach([&result] <std::size_t Size> (const ForeignKey<Size>& key) {
|
||||
result += " ";
|
||||
result += key.Declaration();
|
||||
result += ",\n";
|
||||
}, mEntities);
|
||||
result.erase(result.size() - 2, 1);
|
||||
result.back() = '\n';
|
||||
result += ");\n";
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string Insert() const
|
||||
{
|
||||
std::string columnNames;
|
||||
std::string valueNames;
|
||||
ForEach([this, &columnNames, &valueNames] <typename ColumnType> (const Column<ColumnType>& column) {
|
||||
if (IsSet<ColumnFlags::PrimaryKey>(column.Flags())) {
|
||||
return;
|
||||
}
|
||||
columnNames += column.Name();
|
||||
columnNames += ", ";
|
||||
valueNames += Identifier(column.Ptr());
|
||||
valueNames += ", ";
|
||||
}, mEntities);
|
||||
std::string result;
|
||||
result += "INSERT INTO " + mName + "(";
|
||||
result += columnNames.erase(columnNames.size() - 2);
|
||||
result += ") VALUES (";
|
||||
result += valueNames.erase(valueNames.size() - 2);
|
||||
result += ");\n";
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO: These are only for development, remove once prepared statements are
|
||||
// implemented.
|
||||
std::string Insert(const T& item) const
|
||||
{
|
||||
std::string columnNames;
|
||||
std::string values;
|
||||
ForEach([&item, &columnNames, &values] <typename ColumnType> (const Column<ColumnType>& column) {
|
||||
if (IsSet<ColumnFlags::PrimaryKey>(column.Flags())) {
|
||||
return;
|
||||
}
|
||||
columnNames += column.Name();
|
||||
columnNames += ", ";
|
||||
values += ToString(column.Get(item));
|
||||
values += ", ";
|
||||
}, mEntities);
|
||||
std::string result;
|
||||
result += "INSERT INTO " + mName + "(";
|
||||
result += columnNames.erase(columnNames.size() - 2);
|
||||
result += ") VALUES (";
|
||||
result += values.erase(values.size() - 2);
|
||||
result += ");\n";
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string Update() const
|
||||
{
|
||||
std::string result;
|
||||
result += "UPDATE " + mName + " SET\n";
|
||||
ForEach([this, &result] <typename ColumnType> (const Column<ColumnType>& column) {
|
||||
if (IsSet<ColumnFlags::PrimaryKey>(column.Flags())) {
|
||||
return;
|
||||
}
|
||||
result += " " + std::string(column.Name()) + " = " + Identifier(column.Ptr()) + ",\n";
|
||||
}, mEntities);
|
||||
result.erase(result.size() - 2, 1);
|
||||
ForEach([this, &result] <typename ColumnType> (const Column<ColumnType>& column) {
|
||||
if (IsSet<ColumnFlags::PrimaryKey>(column.Flags())) {
|
||||
result += "WHERE " + std::string(column.Name()) + " = " + Identifier(column.Ptr()) + ";\n";
|
||||
}
|
||||
}, mEntities);
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO: These are only for development, remove once prepared statements are
|
||||
// implemented.
|
||||
std::string Update(const T& item) const
|
||||
{
|
||||
std::string result;
|
||||
result += "UPDATE " + mName + " SET\n";
|
||||
ForEach([this, &result, &item] <typename ColumnType> (const Column<ColumnType>& column) {
|
||||
if (IsSet<ColumnFlags::PrimaryKey>(column.Flags())) {
|
||||
return;
|
||||
}
|
||||
result += " " + std::string(column.Name()) + " = " + ToString(column.Get(item)) + ",\n";
|
||||
}, mEntities);
|
||||
result.erase(result.size() - 2, 1);
|
||||
ForEach([this, &result, &item] <typename ColumnType> (const Column<ColumnType>& column) {
|
||||
if (IsSet<ColumnFlags::PrimaryKey>(column.Flags())) {
|
||||
result += "WHERE " + std::string(column.Name()) + " = " + ToString(column.Get(item)) + ";\n";
|
||||
}
|
||||
}, mEntities);
|
||||
return result;
|
||||
}
|
||||
private:
|
||||
std::string mName;
|
||||
std::tuple<Entities...> mEntities;
|
||||
};
|
||||
|
||||
// Note(3011): This breaks when the first specified entity is not a column.
|
||||
template <typename... Entities>
|
||||
Table(auto, Entities&&...) -> Table<typename MemberTraits<typename First<Entities...>::PtrType>::Container, Entities...>;
|
||||
|
||||
template <typename... Tables>
|
||||
requires (IsSpecialization<Table, Tables> && ...)
|
||||
class Database
|
||||
{
|
||||
public:
|
||||
[[nodiscard]] constexpr
|
||||
Database(std::string_view name, Tables&&... tables)
|
||||
: mName(name), mTables(std::move(tables)...)
|
||||
{}
|
||||
|
||||
template <typename TableType>
|
||||
std::string_view Name() const
|
||||
{
|
||||
using Type = FirstWithArg<TableType, Tables...>;
|
||||
return std::get<Type>(mTables).Name();
|
||||
}
|
||||
|
||||
template <typename ColumnPtr>
|
||||
requires std::is_member_object_pointer_v<ColumnPtr>
|
||||
std::string_view Name(ColumnPtr ptr) const
|
||||
{
|
||||
using TableType = MemberTraits<ColumnPtr>::Container;
|
||||
using Type = FirstWithArg<TableType, Tables...>;
|
||||
return std::get<Type>(mTables).Name(ptr);
|
||||
}
|
||||
|
||||
template <typename ColumnPtr>
|
||||
requires std::is_member_object_pointer_v<ColumnPtr>
|
||||
std::string Identifier(ColumnPtr ptr) const
|
||||
{
|
||||
using TableType = MemberTraits<ColumnPtr>::Container;
|
||||
using Type = FirstWithArg<TableType, Tables...>;
|
||||
return std::get<Type>(mTables).Identifier(ptr);
|
||||
}
|
||||
|
||||
std::string Create() const
|
||||
{
|
||||
std::string result;
|
||||
result += "PRAGMA foreign_keys = ON;\n\n";
|
||||
std::apply([&result](auto&... args) {
|
||||
((result += args.Create(), result += "\n"), ...);
|
||||
}, mTables);
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename TableType>
|
||||
std::string Insert() const
|
||||
{
|
||||
using Type = FirstWithArg<TableType, Tables...>;
|
||||
return std::get<Type>(mTables).Insert();
|
||||
}
|
||||
|
||||
template <typename TableType>
|
||||
std::string Insert(const TableType& item) const
|
||||
{
|
||||
using Type = FirstWithArg<TableType, Tables...>;
|
||||
return std::get<Type>(mTables).Insert(item);
|
||||
}
|
||||
|
||||
template <typename TableType>
|
||||
std::string Update() const
|
||||
{
|
||||
using Type = FirstWithArg<TableType, Tables...>;
|
||||
return std::get<Type>(mTables).Update();
|
||||
}
|
||||
|
||||
template <typename TableType>
|
||||
std::string Update(const TableType& item) const
|
||||
{
|
||||
using Type = FirstWithArg<TableType, Tables...>;
|
||||
return std::get<Type>(mTables).Update(item);
|
||||
}
|
||||
private:
|
||||
std::string mName;
|
||||
std::tuple<Tables...> mTables;
|
||||
};
|
||||
|
||||
template <typename... Tables>
|
||||
Database(auto, Tables...) -> Database<Tables...>;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// DATABASE CONNECTION
|
||||
|
||||
enum class Operator : std::uint8_t
|
||||
{
|
||||
None = 0,
|
||||
Equal = 1,
|
||||
};
|
||||
|
||||
template <typename DatabaseType>
|
||||
requires IsSpecialization<Database, DatabaseType>
|
||||
class Connection;
|
||||
|
||||
template <typename DatabaseType, typename TableType>
|
||||
class Select
|
||||
{
|
||||
private:
|
||||
friend class Connection<DatabaseType>;
|
||||
|
||||
Select(Connection<DatabaseType>& connection)
|
||||
: mConnection(connection)
|
||||
{}
|
||||
public:
|
||||
// Note: Immovable object, please don't use unstoppable force.
|
||||
Select(const Select&) = delete;
|
||||
Select& operator=(const Select&) = delete;
|
||||
Select(Select&&) = delete;
|
||||
Select& operator=(Select&&) = delete;
|
||||
|
||||
template <typename ColumnPtr>
|
||||
requires std::is_member_object_pointer_v<ColumnPtr>
|
||||
Select& Where(ColumnPtr column)
|
||||
{
|
||||
// TODO: Append the condition.
|
||||
return &this;
|
||||
}
|
||||
|
||||
// TODO: Finalizer member function - returns the result.
|
||||
std::optional<TableType> First()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
// TODO: Finalizer member function - returns the result.
|
||||
std::vector<TableType> All()
|
||||
{
|
||||
|
||||
}
|
||||
private:
|
||||
Connection<TableType>& mConnection;
|
||||
std::string mStatement;
|
||||
};
|
||||
|
||||
template <typename DatabaseType>
|
||||
requires IsSpecialization<Database, DatabaseType>
|
||||
class Connection
|
||||
{
|
||||
public:
|
||||
Connection(const DatabaseType& database, std::string path)
|
||||
: mDatabase(&database), mPath(std::move(path)), mConnection(nullptr)
|
||||
{
|
||||
int errc = sqlite3_open(mPath.c_str(), &mConnection);
|
||||
if (errc != SQLITE_OK) {
|
||||
// TODO: Add more information to the error.
|
||||
throw DatabaseError("Could not open a SQLite connection");
|
||||
}
|
||||
}
|
||||
|
||||
Connection(const Connection&) = delete;
|
||||
Connection& operator=(const Connection&) = delete;
|
||||
|
||||
Connection(Connection&& other) noexcept
|
||||
: mDatabase(other.mDatabase)
|
||||
, mPath(std::move(other.mPath))
|
||||
, mConnection(other.mConnection)
|
||||
{
|
||||
other.mConnection = nullptr;
|
||||
}
|
||||
|
||||
Connection& operator= (Connection&& other)
|
||||
{
|
||||
if (&other == this) {
|
||||
return;
|
||||
}
|
||||
|
||||
mDatabase = other.mDatabase;
|
||||
mPath = std::move(other.mPath);
|
||||
mConnection = other.mConnection;
|
||||
|
||||
other.mDatabase = nullptr;
|
||||
other.mConnection = nullptr;
|
||||
}
|
||||
|
||||
~Connection()
|
||||
{
|
||||
// TODO: The current implementation will loop here forever in case
|
||||
// anything isn't finalized - make sure to force a cleanup before
|
||||
// that happens.
|
||||
int errc = sqlite3_close(mConnection);
|
||||
while (errc == SQLITE_BUSY) {
|
||||
errc = sqlite3_close(mConnection);
|
||||
}
|
||||
}
|
||||
|
||||
bool Create()
|
||||
{
|
||||
std::string script = mDatabase->Create();
|
||||
char *errmsg;
|
||||
auto errc = sqlite3_exec(mConnection, script.c_str(), nullptr, nullptr, &errmsg);
|
||||
if (errc != SQLITE_OK) {
|
||||
// TODO: Report the error?
|
||||
sqlite3_free(reinterpret_cast<void *>(errc));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename TableType>
|
||||
bool Insert(TableType& item)
|
||||
{
|
||||
std::string statement = mDatabase->Insert(item);
|
||||
char *errmsg;
|
||||
auto errc = sqlite3_exec(mConnection, statement.c_str(), nullptr, nullptr, &errmsg);
|
||||
if (errc != SQLITE_OK) {
|
||||
// TODO: Report the error?
|
||||
sqlite3_free(reinterpret_cast<void *>(errc));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename TableType>
|
||||
bool Update(TableType& item)
|
||||
{
|
||||
std::string statement = mDatabase->Insert(item);
|
||||
char *errmsg;
|
||||
auto errc = sqlite3_exec(mConnection, statement.c_str(), nullptr, nullptr, &errmsg);
|
||||
if (errc != SQLITE_OK) {
|
||||
// TODO: Report the error?
|
||||
sqlite3_free(reinterpret_cast<void *>(errc));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename TableType>
|
||||
Select<DatabaseType, TableType> Select()
|
||||
{
|
||||
return Select<DatabaseType, TableType>(*this);
|
||||
}
|
||||
private:
|
||||
const DatabaseType *mDatabase;
|
||||
std::string mPath;
|
||||
sqlite3 *mConnection;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // GARBAGE_SQLITE_HPP
|
314
Garbage/Include/Garbage/SimpleConf.hpp
Normal file
314
Garbage/Include/Garbage/SimpleConf.hpp
Normal file
|
@ -0,0 +1,314 @@
|
|||
#ifndef GARBAGE_SIMPLE_CONF_HPP
|
||||
#define GARBAGE_SIMPLE_CONF_HPP
|
||||
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
namespace Garbage
|
||||
{
|
||||
enum class SimpleConfErrorKind
|
||||
{
|
||||
ReadError,
|
||||
ConversionError,
|
||||
ParseError,
|
||||
LookupError,
|
||||
};
|
||||
|
||||
constexpr
|
||||
std::string AsString(SimpleConfErrorKind err)
|
||||
{
|
||||
switch (err) {
|
||||
case SimpleConfErrorKind::ReadError:
|
||||
return "Read Error";
|
||||
case SimpleConfErrorKind::ConversionError:
|
||||
return "Conversion Error";
|
||||
case SimpleConfErrorKind::ParseError:
|
||||
return "Parse Error";
|
||||
case SimpleConfErrorKind::LookupError:
|
||||
return "Lookup Error";
|
||||
default:
|
||||
return "Unknown Error";
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleConfError : public std::runtime_error
|
||||
{
|
||||
public:
|
||||
SimpleConfError(SimpleConfErrorKind kind, std::string_view message)
|
||||
: std::runtime_error(AsString(kind) + ": " + std::string(message))
|
||||
{}
|
||||
};
|
||||
|
||||
namespace SimpleConfImplementation
|
||||
{
|
||||
class ReadError : public SimpleConfError
|
||||
{
|
||||
public:
|
||||
ReadError(std::string_view path, std::string_view message)
|
||||
: SimpleConfError(SimpleConfErrorKind::ReadError,
|
||||
std::format("[Path {}] {}", path, message))
|
||||
{}
|
||||
};
|
||||
|
||||
class ConversionError : public SimpleConfError
|
||||
{
|
||||
public:
|
||||
ConversionError(std::string_view key, std::string_view message)
|
||||
: SimpleConfError(SimpleConfErrorKind::ConversionError,
|
||||
std::format("[Key {}] {}", key, message))
|
||||
{}
|
||||
};
|
||||
|
||||
class ParseError : public SimpleConfError
|
||||
{
|
||||
public:
|
||||
ParseError(std::size_t line, std::string_view message)
|
||||
: SimpleConfError(SimpleConfErrorKind::ParseError,
|
||||
std::format("[Line {}] {}", line, message))
|
||||
{}
|
||||
};
|
||||
|
||||
class LookupError : public SimpleConfError
|
||||
{
|
||||
public:
|
||||
LookupError(std::string_view message)
|
||||
: SimpleConfError(SimpleConfErrorKind::LookupError, message)
|
||||
{}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
constexpr
|
||||
T Convert(std::string_view key, std::string_view rawValue)
|
||||
{
|
||||
if constexpr (std::is_same_v<T, std::string>) {
|
||||
return std::string(rawValue);
|
||||
}
|
||||
|
||||
if constexpr (std::is_integral_v<T> || std::is_floating_point_v<T>) {
|
||||
T result{};
|
||||
auto [ptr, ec] = std::from_chars(rawValue.data(),
|
||||
rawValue.data() + rawValue.size(),
|
||||
result);
|
||||
|
||||
if (ptr != rawValue.data() + rawValue.size()) {
|
||||
throw ConversionError(key, "Could not parse the whole value");
|
||||
}
|
||||
else if (ec == std::errc::invalid_argument) {
|
||||
throw ConversionError(key, "The value is not a valid number");
|
||||
}
|
||||
else if (ec == std::errc::result_out_of_range) {
|
||||
throw ConversionError(key, "The value is out of range of the requested type");
|
||||
}
|
||||
else if (ec != std::errc()) {
|
||||
throw ConversionError(key, "Unknown error");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
throw ConversionError(key, "Can't convert the raw value to the requested type");
|
||||
}
|
||||
|
||||
using PairsType = std::unordered_map<std::string_view, std::string_view>;
|
||||
|
||||
constexpr
|
||||
void Parse(const std::string& rawConf, PairsType& pairs)
|
||||
{
|
||||
std::size_t current = 0;
|
||||
std::size_t currentLine = 1;
|
||||
|
||||
enum class State
|
||||
{
|
||||
Initial,
|
||||
ReadingKey,
|
||||
ReadingValue,
|
||||
CommentLine,
|
||||
} currentState = State::Initial;
|
||||
|
||||
std::size_t keyBegin = 0;
|
||||
std::size_t keyEnd = 0;
|
||||
std::size_t valueBegin = 0;
|
||||
std::size_t valueEnd = 0;
|
||||
|
||||
auto addPair = [&] {
|
||||
if (keyBegin == keyEnd) {
|
||||
throw ParseError(currentLine, "Empty key is not allowed");
|
||||
}
|
||||
|
||||
std::string_view key(rawConf.data() + keyBegin, keyEnd - keyBegin);
|
||||
std::string_view value(rawConf.data() + valueBegin, valueEnd - valueBegin);
|
||||
if (value.size() == 0) {
|
||||
value = std::string_view(nullptr, 0);
|
||||
}
|
||||
pairs[key] = value;
|
||||
|
||||
keyBegin = 0;
|
||||
keyEnd = 0;
|
||||
valueBegin = 0;
|
||||
valueEnd = 0;
|
||||
};
|
||||
|
||||
while (current < rawConf.size()) {
|
||||
switch (currentState) {
|
||||
break; case State::Initial:
|
||||
if (std::isspace(rawConf[current])) {
|
||||
if (rawConf[current] == '\n') {
|
||||
++currentLine;
|
||||
}
|
||||
++current;
|
||||
}
|
||||
else if (rawConf[current] == '#') {
|
||||
currentState = State::CommentLine;
|
||||
}
|
||||
else if (rawConf[current] == '=') {
|
||||
throw ParseError(currentLine, "Empty key is not allowed");
|
||||
}
|
||||
else {
|
||||
keyBegin = current;
|
||||
keyEnd = current + 1; // We know there is at least one char
|
||||
currentState = State::ReadingKey;
|
||||
}
|
||||
break; case State::ReadingKey:
|
||||
if (rawConf[current] == '=') {
|
||||
currentState = State::ReadingValue;
|
||||
}
|
||||
else if (rawConf[current] == '#') {
|
||||
throw ParseError(currentLine, "Unexpected comment start");
|
||||
}
|
||||
else if (!std::isspace(rawConf[current])) {
|
||||
keyEnd = current + 1;
|
||||
}
|
||||
else if (rawConf[current] == '\n') {
|
||||
throw ParseError(currentLine, "End of line while reading key");
|
||||
}
|
||||
++current;
|
||||
break; case State::ReadingValue:
|
||||
if (rawConf[current] == '\n') {
|
||||
addPair();
|
||||
|
||||
++currentLine;
|
||||
currentState = State::Initial;
|
||||
}
|
||||
else if (rawConf[current] == '=') {
|
||||
throw ParseError(currentLine, "Multiple key/value separators are illegal");
|
||||
}
|
||||
else if (rawConf[current] == '#') {
|
||||
addPair();
|
||||
currentState = State::CommentLine;
|
||||
}
|
||||
else {
|
||||
if (!std::isspace(rawConf[current])) {
|
||||
if (valueBegin == 0) {
|
||||
valueBegin = current;
|
||||
valueEnd = current + 1;
|
||||
}
|
||||
else {
|
||||
valueEnd = current + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
++current;
|
||||
break; case State::CommentLine:
|
||||
if (rawConf[current] == '\n') {
|
||||
++currentLine;
|
||||
currentState = State::Initial;
|
||||
}
|
||||
++current;
|
||||
break; default:
|
||||
throw ParseError(currentLine, "Invalid parser state (should be impossible)");
|
||||
}
|
||||
}
|
||||
|
||||
if (currentState == State::ReadingKey) {
|
||||
throw ParseError(currentLine, "End of config while reading the key");
|
||||
} else if (currentState == State::ReadingValue) {
|
||||
addPair();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleConf
|
||||
{
|
||||
public:
|
||||
struct Required {};
|
||||
|
||||
SimpleConf(const std::filesystem::path& path)
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
using namespace SimpleConfImplementation;
|
||||
|
||||
std::error_code ec;
|
||||
if (!fs::exists(path, ec) || ec) {
|
||||
throw ReadError(path.string(), "The specified file does not exist");
|
||||
}
|
||||
if (!fs::is_regular_file(path, ec) || ec) {
|
||||
throw ReadError(path.string(), "The the specified path is not a regular file");
|
||||
}
|
||||
|
||||
std::size_t fileSize = fs::file_size(path, ec);
|
||||
if (ec) {
|
||||
throw ReadError(path.string(), "Could not read the file size");
|
||||
}
|
||||
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) {
|
||||
throw ReadError(path.string(), "Could not open the file for reading");
|
||||
}
|
||||
|
||||
mConfContent = std::string(fileSize, '\0');
|
||||
file.read(mConfContent.data(), fileSize);
|
||||
|
||||
Parse(mConfContent, mPairs);
|
||||
}
|
||||
|
||||
SimpleConf(std::string&& strConfig)
|
||||
: mConfContent(std::move(strConfig))
|
||||
{
|
||||
using namespace SimpleConfImplementation;
|
||||
|
||||
Parse(mConfContent, mPairs);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
std::optional<T> GetOptional(std::string_view key)
|
||||
{
|
||||
using namespace SimpleConfImplementation;
|
||||
|
||||
if (auto it = mPairs.find(key); it != mPairs.end()) {
|
||||
return Convert<T>(key, it->second);
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
template <typename T, typename RequireValue = void>
|
||||
T Get(std::string_view key, const T& defaultValue = {})
|
||||
{
|
||||
using namespace SimpleConfImplementation;
|
||||
|
||||
if (auto it = mPairs.find(key); it != mPairs.end()) {
|
||||
return Convert<T>(key, it->second);
|
||||
}
|
||||
|
||||
if constexpr (std::is_same_v<RequireValue, Required>) {
|
||||
throw LookupError(std::format("Required config key \"{}\" not found", key));
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string mConfContent;
|
||||
SimpleConfImplementation::PairsType mPairs;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // GARBAGE_SIMPLE_CONF_HPP
|
107
README.md
107
README.md
|
@ -1,3 +1,108 @@
|
|||
# Garbage
|
||||
|
||||
A collection of various single-header libraries of questionable quality.
|
||||
A collection of various single-header libraries of questionable quality.
|
||||
|
||||
## Usage
|
||||
|
||||
To use these libraries (please don't), just add them into your project's include path.
|
||||
Ideally, use CMake with FetchContent pinned to a specific commit hash so you don't encounter
|
||||
nasty surprises when the libraries update.
|
||||
|
||||
```cmake
|
||||
include(FetchContent)
|
||||
|
||||
FetchContent_Declare(
|
||||
Garbage
|
||||
GIT_REPOSITORY https://code.3011.io/TennesseeTrash/Garbage
|
||||
GIT_TAG 72f02ff856ddfd1b817d4f95096ec1be0ed11a49
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(
|
||||
Garbage
|
||||
)
|
||||
```
|
||||
|
||||
Once you've added the `Include` directory into your include path, you can use the headers you need,
|
||||
e.g.
|
||||
```cpp
|
||||
#include <Garbage/Base64.hpp>
|
||||
|
||||
// Now you can use
|
||||
Garbage::Base64::Decode(/* some base64 string */);
|
||||
```
|
||||
|
||||
## Base64
|
||||
|
||||
A very simple library composed of 3 functions in the `Garbage::Base64` namespace.
|
||||
|
||||
- `Encode()` takes a `std::span<std::uint8_t>` (i.e. an array of bytes), and produces a
|
||||
padded Base64 encoded string out of it.
|
||||
- `Decode()` takes an encoded `std::string_view`, and converts it into an array of bytes
|
||||
(specifically a `std::vector<std::uint8_t>`).
|
||||
- `Validate()` performs validation of an untrusted `std::string_view`. This is necessary
|
||||
because `Decode()` does not do any checking and running it on unverified data is
|
||||
dangerous. This function is not completely robust, but it is good enough to make sure
|
||||
the `Decode()` call is safe.
|
||||
|
||||
## SimpleConf
|
||||
|
||||
A small config file library. Provides a `Garbage::SimpleConf` class that reads files
|
||||
with key-value pairs. This library makes use of exceptions, it will throw `Garbage::SimpleConfError`
|
||||
when an error is encountered. These exceptions inerit from `std::exception`, so using those to catch
|
||||
them works fine.
|
||||
|
||||
```cpp
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// INIT
|
||||
|
||||
// Read a file from disk
|
||||
Garbage::SimpleConf config(std::filesystem::path(/*path to the config*/));
|
||||
|
||||
// Use an externally provided string
|
||||
std::string rawConfig = /* whatever procedure to obtain a string */;
|
||||
Garbage::SimpleConf config(std::move(rawConfig));
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// GETTING VALUES
|
||||
|
||||
// Optional values
|
||||
|
||||
// Get a std::optional
|
||||
auto value = config.GetOptional<std::string>("SomeKey");
|
||||
// Get a default value
|
||||
int value = config.Get<int>("SomeKey", 0 /* Optional default value */);
|
||||
|
||||
// Required values
|
||||
using Required = Garbage::SimpleConf::Required;
|
||||
int value = config.Get<int, Required>("SomeKey");
|
||||
```
|
||||
|
||||
The config files are structured such that a single line contains a key-value pair separated by a `=`
|
||||
character.
|
||||
Some specifics:
|
||||
- Leading and trailing whitespace is removed from both keys and values.
|
||||
- Whitespace is is allowed inside both keys and values.
|
||||
- Values may be empty.
|
||||
- The `#` character denotes the start of a comment, everything after (and including)
|
||||
this character is ignored until the end of the line.
|
||||
|
||||
Example:
|
||||
```conf
|
||||
# Simplest scenario
|
||||
SomeKey=SomeValue
|
||||
|
||||
# Also works
|
||||
SomeKey = SomeValue
|
||||
|
||||
# Also supported, but note that the key is now "Some Key"
|
||||
Some Key = Some Value
|
||||
|
||||
Comments = After # The pair are also fine
|
||||
|
||||
# Also fine
|
||||
KeyOnly=
|
||||
```
|
||||
|
||||
## SQLite
|
||||
|
||||
WIP
|
||||
|
|
17
Tests/Base64/CMakeLists.txt
Normal file
17
Tests/Base64/CMakeLists.txt
Normal file
|
@ -0,0 +1,17 @@
|
|||
add_executable(TestBase64)
|
||||
|
||||
target_compile_features(TestBase64
|
||||
PRIVATE
|
||||
cxx_std_20
|
||||
)
|
||||
|
||||
target_link_libraries(TestBase64
|
||||
PRIVATE
|
||||
Garbage
|
||||
Catch2::Catch2WithMain
|
||||
)
|
||||
|
||||
target_sources(TestBase64
|
||||
PRIVATE
|
||||
Tests.cpp
|
||||
)
|
81
Tests/Base64/Tests.cpp
Normal file
81
Tests/Base64/Tests.cpp
Normal file
|
@ -0,0 +1,81 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <Garbage/Base64.hpp>
|
||||
|
||||
TEST_CASE("Base64 encoding/decoding tests")
|
||||
{
|
||||
SECTION("Empty span")
|
||||
{
|
||||
std::vector<std::uint8_t> data;
|
||||
|
||||
std::string encoded = Garbage::Base64::Encode(data);
|
||||
REQUIRE(encoded.size() == 0);
|
||||
|
||||
REQUIRE(Garbage::Base64::Validate(encoded));
|
||||
|
||||
std::vector<std::uint8_t> decoded = Garbage::Base64::Decode(encoded);
|
||||
REQUIRE(encoded.size() == 0);
|
||||
}
|
||||
|
||||
SECTION("Encoding short data")
|
||||
{
|
||||
std::string source("Many hands make light work.");
|
||||
std::vector<std::uint8_t> data(source.begin(), source.end());
|
||||
|
||||
std::string encoded = Garbage::Base64::Encode(data);
|
||||
REQUIRE(encoded == "TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcmsu");
|
||||
REQUIRE(Garbage::Base64::Validate(encoded));
|
||||
}
|
||||
|
||||
SECTION("Decoding short data")
|
||||
{
|
||||
std::string data("TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcmsu");
|
||||
|
||||
REQUIRE(Garbage::Base64::Validate(data));
|
||||
|
||||
std::vector<std::uint8_t> decoded = Garbage::Base64::Decode(data);
|
||||
std::string destination(decoded.begin(), decoded.end());
|
||||
REQUIRE(destination == "Many hands make light work.");
|
||||
}
|
||||
|
||||
SECTION("Encoding with a padding char")
|
||||
{
|
||||
std::string source("With a padding");
|
||||
std::vector<std::uint8_t> data(source.begin(), source.end());
|
||||
|
||||
std::string encoded = Garbage::Base64::Encode(data);
|
||||
REQUIRE(encoded == "V2l0aCBhIHBhZGRpbmc=");
|
||||
REQUIRE(Garbage::Base64::Validate(encoded));
|
||||
}
|
||||
|
||||
SECTION("Decoding with a padding char")
|
||||
{
|
||||
std::string data("V2l0aCBhIHBhZGRpbmc=");
|
||||
|
||||
REQUIRE(Garbage::Base64::Validate(data));
|
||||
|
||||
std::vector<std::uint8_t> decoded = Garbage::Base64::Decode(data);
|
||||
std::string destination(decoded.begin(), decoded.end());
|
||||
REQUIRE(destination == "With a padding");
|
||||
}
|
||||
|
||||
SECTION("Encoding with two padding chars")
|
||||
{
|
||||
std::string source("With two padding");
|
||||
std::vector<std::uint8_t> data(source.begin(), source.end());
|
||||
|
||||
std::string encoded = Garbage::Base64::Encode(data);
|
||||
REQUIRE(encoded == "V2l0aCB0d28gcGFkZGluZw==");
|
||||
REQUIRE(Garbage::Base64::Validate(encoded));
|
||||
}
|
||||
|
||||
SECTION("Decoding with two padding chars")
|
||||
{
|
||||
std::string data("V2l0aCB0d28gcGFkZGluZw==");
|
||||
|
||||
REQUIRE(Garbage::Base64::Validate(data));
|
||||
|
||||
std::vector<std::uint8_t> decoded = Garbage::Base64::Decode(data);
|
||||
std::string destination(decoded.begin(), decoded.end());
|
||||
REQUIRE(destination == "With two padding");
|
||||
}
|
||||
}
|
5
Tests/CMakeLists.txt
Normal file
5
Tests/CMakeLists.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
include(${PROJECT_SOURCE_DIR}/CMake/Catch2.cmake)
|
||||
|
||||
add_subdirectory(Base64)
|
||||
add_subdirectory(SQLite)
|
||||
add_subdirectory(SimpleConf)
|
20
Tests/SQLite/CMakeLists.txt
Normal file
20
Tests/SQLite/CMakeLists.txt
Normal file
|
@ -0,0 +1,20 @@
|
|||
include(${PROJECT_SOURCE_DIR}/CMake/sqlite3.cmake)
|
||||
|
||||
add_executable(TestSQLite)
|
||||
|
||||
target_compile_features(TestSQLite
|
||||
PRIVATE
|
||||
cxx_std_20
|
||||
)
|
||||
|
||||
target_link_libraries(TestSQLite
|
||||
PRIVATE
|
||||
Garbage
|
||||
sqlite3
|
||||
Catch2::Catch2WithMain
|
||||
)
|
||||
|
||||
target_sources(TestSQLite
|
||||
PRIVATE
|
||||
Main.cpp
|
||||
)
|
35
Tests/SQLite/Main.cpp
Normal file
35
Tests/SQLite/Main.cpp
Normal file
|
@ -0,0 +1,35 @@
|
|||
#include <Garbage/SQLite.hpp>
|
||||
|
||||
using namespace Garbage::SQLite;
|
||||
|
||||
struct User
|
||||
{
|
||||
std::int64_t Id;
|
||||
std::string Username;
|
||||
std::string Email;
|
||||
std::string PasswordHash;
|
||||
};
|
||||
|
||||
Database db(
|
||||
"UserDb",
|
||||
Table(
|
||||
"Users",
|
||||
Column(&User::Id, "Id", ColumnFlags::PrimaryKey),
|
||||
Column(&User::Username, "Username", ColumnFlags::Unique),
|
||||
Column(&User::Email, "Email", ColumnFlags::Unique),
|
||||
Column(&User::PasswordHash, "PasswordHash")
|
||||
)
|
||||
);
|
||||
|
||||
int main() {
|
||||
User u {
|
||||
.Id = 25,
|
||||
.Username = "noob",
|
||||
.Email = "noob@noob.io",
|
||||
.PasswordHash = "ASDASDASDASDASDA",
|
||||
};
|
||||
|
||||
Connection c(db, "Test.db");
|
||||
c.Create();
|
||||
c.Insert(u);
|
||||
}
|
30
Tests/SimpleConf/CMakeLists.txt
Normal file
30
Tests/SimpleConf/CMakeLists.txt
Normal file
|
@ -0,0 +1,30 @@
|
|||
add_executable(TestSimpleConf)
|
||||
|
||||
target_compile_features(TestSimpleConf
|
||||
PRIVATE
|
||||
cxx_std_20
|
||||
)
|
||||
|
||||
target_link_libraries(TestSimpleConf
|
||||
PRIVATE
|
||||
Garbage
|
||||
Catch2::Catch2WithMain
|
||||
)
|
||||
|
||||
target_sources(TestSimpleConf
|
||||
PRIVATE
|
||||
Comments.cpp
|
||||
Conversions.cpp
|
||||
EmptyConfigs.cpp
|
||||
Files.cpp
|
||||
Optionality.cpp
|
||||
Parsing.cpp
|
||||
WhitespaceBehaviour.cpp
|
||||
)
|
||||
|
||||
add_custom_command(
|
||||
TARGET TestSimpleConf POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/TestConfigs
|
||||
${CMAKE_CURRENT_BINARY_DIR}/TestConfigs
|
||||
)
|
15
Tests/SimpleConf/Comments.cpp
Normal file
15
Tests/SimpleConf/Comments.cpp
Normal file
|
@ -0,0 +1,15 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <Garbage/SimpleConf.hpp>
|
||||
|
||||
TEST_CASE("Comments and empty lines are ignored")
|
||||
{
|
||||
using Required = Garbage::SimpleConf::Required;
|
||||
using LookupError = Garbage::SimpleConfImplementation::LookupError;
|
||||
|
||||
std::filesystem::path path("TestConfigs/Comments.conf");
|
||||
|
||||
Garbage::SimpleConf config(path);
|
||||
|
||||
REQUIRE_THROWS_AS((config.Get<std::string, Required>("this shouldn't")), LookupError);
|
||||
REQUIRE(config.Get<std::string, Required>("while this") == "is accessible");
|
||||
}
|
41
Tests/SimpleConf/Conversions.cpp
Normal file
41
Tests/SimpleConf/Conversions.cpp
Normal file
|
@ -0,0 +1,41 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <Garbage/SimpleConf.hpp>
|
||||
|
||||
TEST_CASE("Check if type conversions work correctly")
|
||||
{
|
||||
using Required = Garbage::SimpleConf::Required;
|
||||
|
||||
std::filesystem::path path("TestConfigs/BasicConversions.conf");
|
||||
|
||||
Garbage::SimpleConf config(path);
|
||||
|
||||
SECTION("Basic stuff that should be correct")
|
||||
{
|
||||
REQUIRE(config.Get<std::string, Required>("an int") == "123456");
|
||||
REQUIRE(config.Get<std::string, Required>("a float") == "3.5");
|
||||
REQUIRE(config.Get<std::string, Required>("a long (8B)") == "121212121212121212");
|
||||
REQUIRE(config.Get<std::string, Required>("a double") == "0.0000128");
|
||||
|
||||
REQUIRE(config.Get<std::int32_t>("an int") == 123456);
|
||||
REQUIRE(config.Get<float>("a float") == 3.5f);
|
||||
REQUIRE(config.Get<std::int64_t>("a long (8B)") == 121212121212121212);
|
||||
REQUIRE(config.Get<double>("a double") == 0.0000128);
|
||||
}
|
||||
|
||||
SECTION("Stuff that should fail to parse")
|
||||
{
|
||||
REQUIRE(config.Get<std::string, Required>("an incorrect char") == "256");
|
||||
REQUIRE(config.Get<std::string, Required>("unsigned") == "-34");
|
||||
REQUIRE(config.Get<std::string, Required>("signed, but large") == "3000000000");
|
||||
REQUIRE(config.Get<std::string, Required>("mangled float") == "3.f4");
|
||||
REQUIRE(config.Get<std::string, Required>("mangled float 2") == "2.4f");
|
||||
|
||||
using ConversionError = Garbage::SimpleConfImplementation::ConversionError;
|
||||
|
||||
REQUIRE_THROWS_AS(config.Get<std::uint8_t>("an incorrect char"), ConversionError);
|
||||
REQUIRE_THROWS_AS(config.Get<std::uint32_t>("unsigned"), ConversionError);
|
||||
REQUIRE_THROWS_AS(config.Get<std::int32_t>("signed, but large"), ConversionError);
|
||||
REQUIRE_THROWS_AS(config.Get<float>("mangled float"), ConversionError);
|
||||
REQUIRE_THROWS_AS(config.Get<float>("mangled float 2"), ConversionError);
|
||||
}
|
||||
}
|
17
Tests/SimpleConf/EmptyConfigs.cpp
Normal file
17
Tests/SimpleConf/EmptyConfigs.cpp
Normal file
|
@ -0,0 +1,17 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <Garbage/SimpleConf.hpp>
|
||||
|
||||
TEST_CASE("Config files with no key value pairs")
|
||||
{
|
||||
SECTION("Empty file")
|
||||
{
|
||||
std::filesystem::path path("TestConfigs/Empty.conf");
|
||||
REQUIRE_NOTHROW(Garbage::SimpleConf(path));
|
||||
}
|
||||
|
||||
SECTION("File with comments")
|
||||
{
|
||||
std::filesystem::path path("TestConfigs/CommentsOnly.conf");
|
||||
REQUIRE_NOTHROW(Garbage::SimpleConf(path));
|
||||
}
|
||||
}
|
23
Tests/SimpleConf/Files.cpp
Normal file
23
Tests/SimpleConf/Files.cpp
Normal file
|
@ -0,0 +1,23 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <Garbage/SimpleConf.hpp>
|
||||
|
||||
TEST_CASE("Interactions with files")
|
||||
{
|
||||
SECTION("Try loading a nonexistent file")
|
||||
{
|
||||
using ReadError = Garbage::SimpleConfImplementation::ReadError;
|
||||
|
||||
std::filesystem::path path("TestConfigs/ThisOneShouldntExist.conf");
|
||||
|
||||
REQUIRE_THROWS_AS(Garbage::SimpleConf(path), ReadError);
|
||||
}
|
||||
|
||||
SECTION("Try loading a directory")
|
||||
{
|
||||
using ReadError = Garbage::SimpleConfImplementation::ReadError;
|
||||
|
||||
std::filesystem::path path("TestConfigs");
|
||||
|
||||
REQUIRE_THROWS_AS(Garbage::SimpleConf(path), ReadError);
|
||||
}
|
||||
}
|
25
Tests/SimpleConf/Optionality.cpp
Normal file
25
Tests/SimpleConf/Optionality.cpp
Normal file
|
@ -0,0 +1,25 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <Garbage/SimpleConf.hpp>
|
||||
|
||||
TEST_CASE("Check handling of optional values")
|
||||
{
|
||||
std::filesystem::path path("TestConfigs/Optionality.conf");
|
||||
|
||||
Garbage::SimpleConf config(path);
|
||||
|
||||
SECTION("GetOptional behaviour")
|
||||
{
|
||||
REQUIRE(!config.GetOptional<std::string>("non-existent key"));
|
||||
REQUIRE(*config.GetOptional<std::string>("existing key") == "existing value");
|
||||
}
|
||||
|
||||
SECTION("Get behaviour")
|
||||
{
|
||||
using Required = Garbage::SimpleConf::Required;
|
||||
using LookupError = Garbage::SimpleConfImplementation::LookupError;
|
||||
|
||||
REQUIRE(config.Get<std::string>("non-existent key", "a default") == "a default");
|
||||
REQUIRE(config.Get<std::string>("existing key") == "existing value");
|
||||
REQUIRE_THROWS_AS((config.Get<std::string, Required>("non-existent key")), LookupError);
|
||||
}
|
||||
}
|
40
Tests/SimpleConf/Parsing.cpp
Normal file
40
Tests/SimpleConf/Parsing.cpp
Normal file
|
@ -0,0 +1,40 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/matchers/catch_matchers_string.hpp>
|
||||
#include <Garbage/SimpleConf.hpp>
|
||||
|
||||
TEST_CASE("Ensure robust parsing")
|
||||
{
|
||||
std::filesystem::path configs("TestConfigs");
|
||||
|
||||
using Catch::Matchers::ContainsSubstring;
|
||||
|
||||
SECTION("Multiple assignments")
|
||||
{
|
||||
REQUIRE_THROWS_WITH(Garbage::SimpleConf(configs / "Parsing-MultipleEquals.conf"),
|
||||
ContainsSubstring("[Line 3]"));
|
||||
}
|
||||
|
||||
SECTION("Hash in key")
|
||||
{
|
||||
REQUIRE_THROWS_WITH(Garbage::SimpleConf(configs / "Parsing-HashInKey.conf"),
|
||||
ContainsSubstring("[Line 6]"));
|
||||
}
|
||||
|
||||
SECTION("No equals")
|
||||
{
|
||||
REQUIRE_THROWS_WITH(Garbage::SimpleConf(configs / "Parsing-OnlyKey.conf"),
|
||||
ContainsSubstring("[Line 7]"));
|
||||
}
|
||||
|
||||
SECTION("Empty key")
|
||||
{
|
||||
REQUIRE_THROWS_WITH(Garbage::SimpleConf(configs / "Parsing-EmptyKey.conf"),
|
||||
ContainsSubstring("[Line 7]"));
|
||||
}
|
||||
|
||||
SECTION("Key at end of file")
|
||||
{
|
||||
REQUIRE_THROWS_WITH(Garbage::SimpleConf(configs / "Parsing-KeyAtEnd.conf"),
|
||||
ContainsSubstring("[Line 10]"));
|
||||
}
|
||||
}
|
12
Tests/SimpleConf/TestConfigs/BasicConversions.conf
Normal file
12
Tests/SimpleConf/TestConfigs/BasicConversions.conf
Normal file
|
@ -0,0 +1,12 @@
|
|||
# First, we wanna test if it can even do the basics
|
||||
an int = 123456
|
||||
a float = 3.5
|
||||
a long (8B) = 121212121212121212
|
||||
a double = 0.0000128
|
||||
|
||||
# Now we want to see if it correctly blows up
|
||||
an incorrect char = 256
|
||||
unsigned = -34
|
||||
signed, but large = 3000000000
|
||||
mangled float = 3.f4
|
||||
mangled float 2 = 2.4f
|
4
Tests/SimpleConf/TestConfigs/Comments.conf
Normal file
4
Tests/SimpleConf/TestConfigs/Comments.conf
Normal file
|
@ -0,0 +1,4 @@
|
|||
# Test whether comments behave correctly
|
||||
|
||||
# this shouldn't = be accessible
|
||||
while this = is accessible # and this comment doesn't interfere
|
3
Tests/SimpleConf/TestConfigs/CommentsOnly.conf
Normal file
3
Tests/SimpleConf/TestConfigs/CommentsOnly.conf
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Another example of an empty config
|
||||
# The config should initialize successfully,
|
||||
# but there should be no kv-pairs inside it
|
0
Tests/SimpleConf/TestConfigs/Empty.conf
Normal file
0
Tests/SimpleConf/TestConfigs/Empty.conf
Normal file
2
Tests/SimpleConf/TestConfigs/Optionality.conf
Normal file
2
Tests/SimpleConf/TestConfigs/Optionality.conf
Normal file
|
@ -0,0 +1,2 @@
|
|||
# This probably doesn't need to exist, but eh
|
||||
existing key = existing value
|
10
Tests/SimpleConf/TestConfigs/Parsing-EmptyKey.conf
Normal file
10
Tests/SimpleConf/TestConfigs/Parsing-EmptyKey.conf
Normal file
|
@ -0,0 +1,10 @@
|
|||
# A simple config file that
|
||||
|
||||
normal_key=normal_value
|
||||
another_key=another_value
|
||||
generic=generic
|
||||
another_one=another_value
|
||||
= oh no, an empty key
|
||||
another_one=...
|
||||
|
||||
# It doesn't really matter what's here, because the parser won't reach it
|
9
Tests/SimpleConf/TestConfigs/Parsing-HashInKey.conf
Normal file
9
Tests/SimpleConf/TestConfigs/Parsing-HashInKey.conf
Normal file
|
@ -0,0 +1,9 @@
|
|||
# A simple config file that contains a # character inside the key
|
||||
|
||||
uhh=
|
||||
normal_key=normal_value
|
||||
another_key=another_value
|
||||
a_very_bas#ic_key=generic_value
|
||||
another_one=...
|
||||
|
||||
# It doesn't really matter what's here, because the parser won't reach it
|
10
Tests/SimpleConf/TestConfigs/Parsing-KeyAtEnd.conf
Normal file
10
Tests/SimpleConf/TestConfigs/Parsing-KeyAtEnd.conf
Normal file
|
@ -0,0 +1,10 @@
|
|||
# A simple config file that
|
||||
|
||||
normal_key=normal_value
|
||||
another_key=another_value
|
||||
generic=generic
|
||||
another_one=another_value
|
||||
another_one=...
|
||||
|
||||
# It doesn't really matter what's here, because the parser won't reach it
|
||||
sneaky_key
|
4
Tests/SimpleConf/TestConfigs/Parsing-MultipleEquals.conf
Normal file
4
Tests/SimpleConf/TestConfigs/Parsing-MultipleEquals.conf
Normal file
|
@ -0,0 +1,4 @@
|
|||
# A simple config
|
||||
that appears = correct
|
||||
but it = has = an invalid line # Should blow up here
|
||||
another=pair
|
10
Tests/SimpleConf/TestConfigs/Parsing-OnlyKey.conf
Normal file
10
Tests/SimpleConf/TestConfigs/Parsing-OnlyKey.conf
Normal file
|
@ -0,0 +1,10 @@
|
|||
# A simple config file that contains only the key on a single line
|
||||
|
||||
normal_key=normal_value
|
||||
another_key=another_value
|
||||
generic=generic
|
||||
another_one=another_value
|
||||
a_very_basic_key
|
||||
another_one=...
|
||||
|
||||
# It doesn't really matter what's here, because the parser won't reach it
|
53
Tests/SimpleConf/WhitespaceBehaviour.cpp
Normal file
53
Tests/SimpleConf/WhitespaceBehaviour.cpp
Normal file
|
@ -0,0 +1,53 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <Garbage/SimpleConf.hpp>
|
||||
|
||||
TEST_CASE("Check behavior when whitespace is involved")
|
||||
{
|
||||
SECTION("Simple multi-line config")
|
||||
{
|
||||
using Required = Garbage::SimpleConf::Required;
|
||||
|
||||
std::string rawConfig =
|
||||
" key with whitespace = and value too \n"
|
||||
"another=one\n"
|
||||
" and yet = another";
|
||||
|
||||
Garbage::SimpleConf config(std::move(rawConfig));
|
||||
|
||||
REQUIRE(config.Get<std::string, Required>("key with whitespace") == "and value too");
|
||||
REQUIRE(config.Get<std::string, Required>("another") == "one");
|
||||
REQUIRE(config.Get<std::string, Required>("and yet") == "another");
|
||||
}
|
||||
|
||||
SECTION("Deal with CRLF correctly")
|
||||
{
|
||||
using Required = Garbage::SimpleConf::Required;
|
||||
|
||||
std::string rawConfig =
|
||||
" simple key=simple value \r\n"
|
||||
" another = one\r\n"
|
||||
" and one = more\r\n";
|
||||
|
||||
Garbage::SimpleConf config(std::move(rawConfig));
|
||||
|
||||
REQUIRE(config.Get<std::string, Required>("simple key") == "simple value");
|
||||
REQUIRE(config.Get<std::string, Required>("another") == "one");
|
||||
REQUIRE(config.Get<std::string, Required>("and one") == "more");
|
||||
}
|
||||
|
||||
SECTION("Weirder whitespace shenanigans")
|
||||
{
|
||||
using Required = Garbage::SimpleConf::Required;
|
||||
|
||||
std::string rawConfig =
|
||||
"let's+start_simple=with some\tbasics\n"
|
||||
"\t\tnow we get \tsome=\t \tweirder\tstuff\r\n"
|
||||
"\t\tinsa+nity\twith\t\t=\t white \t space \t\t";
|
||||
|
||||
Garbage::SimpleConf config(std::move(rawConfig));
|
||||
|
||||
REQUIRE(config.Get<std::string, Required>("let's+start_simple") == "with some\tbasics");
|
||||
REQUIRE(config.Get<std::string, Required>("now we get \tsome") == "weirder\tstuff");
|
||||
REQUIRE(config.Get<std::string, Required>("insa+nity\twith") == "white \t space");
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue