From 50abe7a88b4cbaae3273d6a2f855266c55b28ab1 Mon Sep 17 00:00:00 2001 From: Booklordofthedings Date: Thu, 27 Mar 2025 17:46:02 +0100 Subject: [PATCH] Implement Logger, Caching, Args (#2) This adds basic testing, the logger, caching and an commandline argument parser Reviewed-on: https://code.booklordofthe.dev/Booklordofthedings/Common/pulls/2 closes #1 --- BeefProj.toml | 6 ++ BeefSpace.toml | 5 ++ src/Args.bf | 204 ++++++++++++++++++++++++++++++++++++++++++++++ src/Caching.bf | 87 ++++++++++++++++++++ src/Logging.bf | 153 ++++++++++++++++++++++++++++++++++ src/Tests/Args.bf | 66 +++++++++++++++ 6 files changed, 521 insertions(+) create mode 100644 BeefProj.toml create mode 100644 BeefSpace.toml create mode 100644 src/Args.bf create mode 100644 src/Caching.bf create mode 100644 src/Logging.bf create mode 100644 src/Tests/Args.bf diff --git a/BeefProj.toml b/BeefProj.toml new file mode 100644 index 0000000..aadb9ff --- /dev/null +++ b/BeefProj.toml @@ -0,0 +1,6 @@ +FileVersion = 1 + +[Project] +Name = "Common" +TargetType = "BeefLib" +StartupObject = "Common.Program" diff --git a/BeefSpace.toml b/BeefSpace.toml new file mode 100644 index 0000000..fef990f --- /dev/null +++ b/BeefSpace.toml @@ -0,0 +1,5 @@ +FileVersion = 1 +Projects = {Common = {Path = "."}} + +[Workspace] +StartupProject = "Common" diff --git a/src/Args.bf b/src/Args.bf new file mode 100644 index 0000000..3b80ffe --- /dev/null +++ b/src/Args.bf @@ -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 _args = new .(10) ~ DeleteContainerAndItems!(_); + private List _verbs = new .(10) ~ delete _; + private List _flags = new .(10) ~ delete _; + private Dictionary> _values = new .(10) ~ DeleteDictionaryAndValues!(_); + + public this(T args) where T : IEnumerable, 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 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 GetVerb(uint32 index = 0) + { + if (index >= _verbs.Count) + return .Err; + return _verbs[index]; + } + + /// Retrieve all of the verbs + public void GetVerbs(List verbs) + { + for (var v in _verbs) + verbs.Add(v); + } + + /// Retrieve all of the flags + public void GetFlags(List flags) + { + for (var f in _flags) + flags.Add(f); + } + + /// Check wether the arguments have a certain sets of flags + public bool HasFlag(params Span 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 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 values, params Span 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 GetValue(params Span 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 values) + { + for (var flag in values) + if (_values.ContainsKey(flag)) + return _values[flag].Front; + + return dfault; + } +} + + diff --git a/src/Caching.bf b/src/Caching.bf new file mode 100644 index 0000000..784e7c2 --- /dev/null +++ b/src/Caching.bf @@ -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 GetCachedValue(StringView key); + + public bool HasCachedValue(StringView key); +} + +interface ICacheProvider +{ + public void Cache(T ownedValue, StringView key, DateTime timeout = DateTime.UtcNow.AddYears(2)); + + public void ClearCache(); + + public Result GetCachedValue(StringView key); + + public bool HasCachedValue(StringView key); +} + +class CommonCacheProvider : ICacheProvider, ICacheProvider +{ + private int32 _nextIndex = 0; + private List<(Variant, DateTime)> _items ~ delete _; + private Dictionary _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 GetCachedValue(StringView key) + { + if(!_indexes.ContainsKeyAlt(key)) + return .Err; + + return .Ok(_items[_indexes.GetValue(scope .(key))].0); + } + + public bool HasCachedValue(StringView key) + { + if(_indexes.ContainsKeyAlt(key)) + return true; + return false; + } +} \ No newline at end of file diff --git a/src/Logging.bf b/src/Logging.bf new file mode 100644 index 0000000..d80e15b --- /dev/null +++ b/src/Logging.bf @@ -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 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 GetSettingValue(LogSetting setting, String value); + public Result 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 SetLogLevel(LogLevel level) + { + _logLevel = level; + return .Ok; + } + + public Result GetSettingValue(LogSetting setting, String value) + { + return .Err; + } + + public Result SetSettingValue(LogSetting setting, StringView value) + { + return .Err; + } + +} \ No newline at end of file diff --git a/src/Tests/Args.bf b/src/Tests/Args.bf new file mode 100644 index 0000000..f83982f --- /dev/null +++ b/src/Tests/Args.bf @@ -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 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); + } +} \ No newline at end of file