Even more features
This adds basic testing, the logger, caching and an commandline argument parser
This commit is contained in:
parent
78183fcc9e
commit
fd7ef34199
6 changed files with 521 additions and 0 deletions
6
BeefProj.toml
Normal file
6
BeefProj.toml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
FileVersion = 1
|
||||||
|
|
||||||
|
[Project]
|
||||||
|
Name = "Common"
|
||||||
|
TargetType = "BeefLib"
|
||||||
|
StartupObject = "Common.Program"
|
5
BeefSpace.toml
Normal file
5
BeefSpace.toml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
FileVersion = 1
|
||||||
|
Projects = {Common = {Path = "."}}
|
||||||
|
|
||||||
|
[Workspace]
|
||||||
|
StartupProject = "Common"
|
204
src/Args.bf
Normal file
204
src/Args.bf
Normal 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
87
src/Caching.bf
Normal 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
153
src/Logging.bf
Normal 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
66
src/Tests/Args.bf
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue