Implement Logger, Caching, Args (#2)

This adds basic testing, the logger, caching and an commandline argument
parser

Reviewed-on: #2

closes #1
This commit is contained in:
Booklordofthedings 2025-03-27 17:46:02 +01:00
parent 78183fcc9e
commit 50abe7a88b
6 changed files with 521 additions and 0 deletions

6
BeefProj.toml Normal file
View file

@ -0,0 +1,6 @@
FileVersion = 1
[Project]
Name = "Common"
TargetType = "BeefLib"
StartupObject = "Common.Program"

5
BeefSpace.toml Normal file
View file

@ -0,0 +1,5 @@
FileVersion = 1
Projects = {Common = {Path = "."}}
[Workspace]
StartupProject = "Common"

204
src/Args.bf Normal file
View file

@ -0,0 +1,204 @@
/*
Common - Booklordofthedings - 2025 - Args
A parser for commandline arguments or things similar to that.
There is one specific syntax that is used and cannot be changed,
this syntax is similar to most other programs but slightly more limited.
verb verb --Name Value -flag -flag -NotAFlag value
Every item that doesnt start with a '-' before the first '-' is considered the verb.
clone/pull etc...
A single command might have multiple verbs
'-' '--' can be used interchangeably and either indicate a flag or a value
This depends on wether the next entry also starts with a '-'
If it doesnt, then its considered a value pair
Otherwise its just a flag thats set to true
--value key1 key2 --flag
One value might have multiple keys
*/
namespace Common;
using System;
using System.Collections;
class Args
{
private List<String> _args = new .(10) ~ DeleteContainerAndItems!(_);
private List<StringView> _verbs = new .(10) ~ delete _;
private List<StringView> _flags = new .(10) ~ delete _;
private Dictionary<StringView, List<StringView>> _values = new .(10) ~ DeleteDictionaryAndValues!(_);
public this<T>(T args) where T : IEnumerable<String>, concrete
{
for (var i in args)
_args.Add(new .(i));
_ParseArguments();
}
public this(StringView args)
{
loop:for (var part in args.Split(' '))
{
if (part.IsEmpty)
continue;
/// "content" <- cut out the quotes and use it
if (part.StartsWith('"') && part.EndsWith('"') && !part.EndsWith("\\\""))
{
_args.Add(new .(part.Substring(1, part.Length - 2)));
continue loop;
}
/// "sadas dsfsdfsd gfdfgdfgdfg dasdas" <- Find the end and use it
if (part.StartsWith('"'))
{
int startIndex = @part.Pos;
int endIndex = -1;
for (var p in @part)
{
if (p.EndsWith('"') && !p.EndsWith("\\\""))
endIndex = @p.Pos + p.Length;
}
if (endIndex < 0)
endIndex = args.Length + 1; //There is no " so we need to move one further
_args.Add(new .(args.Substring(startIndex + 1, endIndex - 2 - startIndex)));
continue loop;
}
_args.Add(new .(part));
}
_ParseArguments();
}
/// Parse the list of arguments thats stored in _args to make further calculations faster
private void _ParseArguments()
{
bool verbSearch = true;
StringView currentValue = "";
for (var i < _args.Count)
{
if (verbSearch)
{
if (_args[i].StartsWith('-'))
{
verbSearch = false;
}
else
{
_verbs.Add(_args[i]);
continue;
}
}
if (!_args[i].StartsWith('-'))
{
_values[currentValue].Add(_args[i]);
continue;
}
if ( //If there are more entries but they start with - or if there are none, we handle this as a flag
(i + 1 < _args.Count && _args[i + 1].StartsWith('-'))
|| !(i + 1 < _args.Count))
{
_flags.Add(_args[i].StartsWith("--") ? _args[i].Substring(2) : _args[i].Substring(1));
continue;
}
//Otherwise its a value
currentValue = _args[i].StartsWith("--") ? _args[i].Substring(2) : _args[i].Substring(1);
if (_values.ContainsKey(currentValue))
delete _values.GetAndRemove(currentValue).Value.value;
_values.Add(currentValue, new .(10));
}
}
/// Retrieve the argument at the given index or return an error if it doesnt exist
/// @param index The n'th argument
public Result<StringView> GetArgument(uint32 index)
{
if (index < _args.Count)
return .Ok(_args[index]);
return .Err;
}
/// Retrieve nth verb or return an error if it doesnt exist
/// @param index The n'th argument
public Result<StringView> GetVerb(uint32 index = 0)
{
if (index >= _verbs.Count)
return .Err;
return _verbs[index];
}
/// Retrieve all of the verbs
public void GetVerbs(List<StringView> verbs)
{
for (var v in _verbs)
verbs.Add(v);
}
/// Retrieve all of the flags
public void GetFlags(List<StringView> flags)
{
for (var f in _flags)
flags.Add(f);
}
/// Check wether the arguments have a certain sets of flags
public bool HasFlag(params Span<StringView> name)
{
for (var f in _flags)
for (var check in name)
if (f == check)
return true;
return false;
}
/// Check wether the argument list has contains a value of any amount of given names
public bool HasValue(params Span<StringView> flags)
{
for (var flag in flags)
if (_values.ContainsKey(flag))
return true;
return false;
}
/// Adds all values from the name parameters into the value list
public void GetValues(List<StringView> values, params Span<StringView> name)
{
for (var flag in name)
if (_values.ContainsKey(flag))
for (var value in _values[flag])
values.Add(value);
}
/// Returns the first entry of the given value or errors
public Result<StringView> GetValue(params Span<StringView> name)
{
for (var flag in name)
if (_values.ContainsKey(flag))
return .Ok(_values[flag].Front);
return .Err;
}
/// This works like GetValue but can remove a conditional statement from the calling code
public StringView GetValueOrDefault(StringView dfault, params Span<StringView> values)
{
for (var flag in values)
if (_values.ContainsKey(flag))
return _values[flag].Front;
return dfault;
}
}

87
src/Caching.bf Normal file
View file

@ -0,0 +1,87 @@
/*
Common - Booklordofthedings - 2025 - Caching
Caching is used to avoid recalculating large slow operations.
A key for an operation is generated and the result is saved under that key
if a future key matches an existing result that result will be returned
instead of a recalculation.
*/
namespace Common.Caching;
using System;
using System.Collections;
interface ICacheProvider
{
public void Cache(Variant ownedValue, StringView key, DateTime timeout = DateTime.UtcNow.AddYears(2));
public void ClearCache();
public Result<Variant> GetCachedValue(StringView key);
public bool HasCachedValue(StringView key);
}
interface ICacheProvider<T>
{
public void Cache(T ownedValue, StringView key, DateTime timeout = DateTime.UtcNow.AddYears(2));
public void ClearCache();
public Result<T> GetCachedValue(StringView key);
public bool HasCachedValue(StringView key);
}
class CommonCacheProvider : ICacheProvider, ICacheProvider<Variant>
{
private int32 _nextIndex = 0;
private List<(Variant, DateTime)> _items ~ delete _;
private Dictionary<String, int64> _indexes = new .() ~ DeleteDictionaryAndKeys!(_);
public ~this()
{
for(var i in _items)
i.0.Dispose();
}
public void Cache(Variant ownedValue, StringView key, DateTime timeout = DateTime.UtcNow.AddYears(2))
{
if(_indexes.GetValue(scope .(key)) case .Ok(let index))
{
_items[index].0.Dispose();
_items[index].0 = ownedValue;
_items[index].1 = timeout;
}
else
{
_items.Add((ownedValue, timeout));
_indexes.Add(new .(key), _items.Count-1);
}
}
public void ClearCache()
{
for(var i in _indexes)
delete i.key;
for(var i in _items)
i.0.Dispose();
}
public Result<Variant> GetCachedValue(StringView key)
{
if(!_indexes.ContainsKeyAlt<StringView>(key))
return .Err;
return .Ok(_items[_indexes.GetValue(scope .(key))].0);
}
public bool HasCachedValue(StringView key)
{
if(_indexes.ContainsKeyAlt<StringView>(key))
return true;
return false;
}
}

153
src/Logging.bf Normal file
View file

@ -0,0 +1,153 @@
/*
Common - Booklordofthedings - 2025 - Logging
ILogger aims to provide a generic logger implementation, that allows library creators to use logging as normal,
without a hard dependency to a specific logging library.
Both of the enums (LogLevel, LogSetting) can be extended by libraries and logger implementations.
!IMPORTANT!: Never use a switch without a default clause for these enums, since that breaks functionality
if they are extended.
*/
namespace Common.Logging;
using System;
interface ILogger
{
public void Log(LogLevel level, StringView message); //Log a message at the specified log level
/*
These are all shorthands for logging with a specific LogLevel.
These should not be extended because it might break stuff.
They are here because they are slightly faster to use than a normal log
*/
public void Trace(StringView message);
public void Debug(StringView message);
public void Info(StringView message);
public void Warn(StringView message);
public void Error(StringView message);
public void Fatal(StringView message);
//Self explanatory
public LogLevel GetLogLevel();
public Result<void> SetLogLevel(LogLevel level);
//I use string as a value here, because it limits error potential to parsing
//while having multiple types as inputs here could potentially create more issues
//Also code libraries should preferably not touch the settings, these are for endusers
public Result<void> GetSettingValue(LogSetting setting, String value);
public Result<void> SetSettingValue(LogSetting setting, StringView value);
}
enum LogLevel
{
/*
LogLevel exists to distinguish log events by their severity and
to enable users to block certain logs from occuring.
Debug and Trace should not output any log events in release mode.
*/
Trace = 0,
Debug = 10,
Info = 20,
Warn = 30,
Error = 40,
Fatal = 50
}
enum LogSetting
{
DoConsoleLog,
DoFileLog
}
class CommonLogger : ILogger
{
private LogLevel _logLevel = .Debug;
public void Log(LogLevel level, StringView message)
{
if (!(_logLevel.Underlying <= level.Underlying))
return;
switch (level)
{
case .Trace:
Console.WriteLine(scope $"[trc]:{message}");
case .Debug:
Console.WriteLine(scope $"[dbg]:{message}");
case .Info:
Console.WriteLine(scope $"[inf]:{message}");
case .Warn:
Console.WriteLine(scope $"[wrn]:{message}");
case .Error:
Console.WriteLine(scope $"[err]:{message}");
case .Fatal:
Console.WriteLine(scope $"[ftl]:{message}");
default:
Console.WriteLine($"[{level}]:{message}");
}
}
#if !(DEBUG || TEST)
[SkipCall]
#endif
public void Trace(StringView message)
{
if (_logLevel.Underlying >= LogLevel.Trace.Underlying)
Console.WriteLine(scope $"[trc]:{message}");
}
#if !(DEBUG || TEST)
[SkipCall]
#endif
public void Debug(StringView message)
{
if (_logLevel.Underlying <= LogLevel.Debug.Underlying)
Console.WriteLine(scope $"[dbg]:{message}");
}
public void Info(StringView message)
{
if (_logLevel.Underlying <= LogLevel.Info.Underlying)
Console.WriteLine(scope $"[ifo]:{message}");
}
public void Warn(StringView message)
{
if (_logLevel.Underlying <= LogLevel.Warn.Underlying)
Console.WriteLine(scope $"[wrn]:{message}");
}
public void Error(StringView message)
{
if (_logLevel.Underlying <= LogLevel.Error.Underlying)
Console.WriteLine(scope $"[err]:{message}");
}
public void Fatal(StringView message)
{
if (_logLevel.Underlying <= LogLevel.Fatal.Underlying)
Console.WriteLine(scope $"[ftl]:{message}");
}
public LogLevel GetLogLevel()
{
return _logLevel;
}
public Result<void> SetLogLevel(LogLevel level)
{
_logLevel = level;
return .Ok;
}
public Result<void> GetSettingValue(LogSetting setting, String value)
{
return .Err;
}
public Result<void> SetSettingValue(LogSetting setting, StringView value)
{
return .Err;
}
}

66
src/Tests/Args.bf Normal file
View file

@ -0,0 +1,66 @@
namespace Common.Tests;
using System;
static
{
[Test(Name = "Common.Args.SingleStringParsing")]
public static void StringParsing()
{
//Testing the functionality of the single string parsing
TestStringParsing("");
TestStringParsing(
"some default testing",
"some", "default", "testing"
);
TestStringParsing(
"\"Single argument in quotes \" ",
"Single argument in quotes "
);
TestStringParsing(
"\"SomeArgument\"",
"SomeArgument"
);
TestStringParsing(
"\"it even works when its not closed",
"it even works when its not closed"
);
TestStringParsing(
"mixed things \"Also work\"",
"mixed", "things", "Also work"
);
}
private static void TestStringParsing(StringView input, params Span<StringView> outputs)
{
Args a = scope .(input);
Test.Assert(a.[Friend]_args.Count == outputs.Length);
for (int i < outputs.Length)
Test.Assert(a.[Friend]_args[i] == outputs[i]);
}
[Test(Name = "Common.Args.Categorization")]
public static void ArgumentCategorization()
{
//Here we test for wether any given input is actually processed into the correct thing
Args a = scope .(
scope String[](
"verb", "verb_2", "verb_3", "-flag", "--another_flag", "-even_more_flags", "-value", "value1", "value2", "--next_value", "okey"
));
Test.Assert(a.[Friend]_args.Count == 11);
Test.Assert(a.[Friend]_verbs.Count == 3);
Test.Assert(a.[Friend]_values.Count == 2);
Test.Assert(a.[Friend]_flags.Count == 3);
Test.Assert(a.[Friend]_values["value"].Count == 2);
}
}