From 800f0a61d7b16bf224f8737d755242b2586ddba3 Mon Sep 17 00:00:00 2001 From: Robin <39103327+RobinRuf@users.noreply.github.com> Date: Wed, 18 Oct 2023 20:39:10 +0200 Subject: [PATCH] Pushing StorageLib to the repo, to fulfill the functionality. --- .github/workflows/publish.yml | 2 +- PrimitiveChatBot/VERSION | 2 +- StorageLib/Diagram.cd | 33 ++++ .../Exceptions/ImportFormatException.cs | 21 +++ StorageLib/Message.cs | 138 ++++++++++++++ StorageLib/Storage.cs | 168 ++++++++++++++++++ StorageLib/StorageLib.csproj | 13 ++ StorageLibTests/GlobalUsings.cs | 1 + StorageLibTests/Message.cs | 112 ++++++++++++ StorageLibTests/Storage.cs | 97 ++++++++++ StorageLibTests/StorageLib.Tests.csproj | 29 +++ 11 files changed, 614 insertions(+), 2 deletions(-) create mode 100644 StorageLib/Diagram.cd create mode 100644 StorageLib/Exceptions/ImportFormatException.cs create mode 100644 StorageLib/Message.cs create mode 100644 StorageLib/Storage.cs create mode 100644 StorageLib/StorageLib.csproj create mode 100644 StorageLibTests/GlobalUsings.cs create mode 100644 StorageLibTests/Message.cs create mode 100644 StorageLibTests/Storage.cs create mode 100644 StorageLibTests/StorageLib.Tests.csproj diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 238116f..0ef0d72 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,7 +3,7 @@ name: Publish Application on: push: branches: - - advanced-conversations + - main paths: - "PrimitiveChatBot/VERSION" diff --git a/PrimitiveChatBot/VERSION b/PrimitiveChatBot/VERSION index 5055a64..caf4428 100644 --- a/PrimitiveChatBot/VERSION +++ b/PrimitiveChatBot/VERSION @@ -1 +1 @@ -0.0.1.0 +1.0.0.0 \ No newline at end of file diff --git a/StorageLib/Diagram.cd b/StorageLib/Diagram.cd new file mode 100644 index 0000000..6625781 --- /dev/null +++ b/StorageLib/Diagram.cd @@ -0,0 +1,33 @@ + + + + + + AAAAAAAABACAAAAAAAAEAIAAAAAAAQAAAQAAAgQAABA= + Message.cs + + + + + + ACAAAAAAAAAAAAAAIAAAAAQAgAAAAAAAEACAAwAAAAA= + Storage.cs + + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACAAAAAA= + Exceptions\ImportFormatException.cs + + + + + + AAAAAAAAAEAAAAAAAIAAAAAAAAAEAAgAAAAAAAAAAAA= + Message.cs + + + + \ No newline at end of file diff --git a/StorageLib/Exceptions/ImportFormatException.cs b/StorageLib/Exceptions/ImportFormatException.cs new file mode 100644 index 0000000..2139389 --- /dev/null +++ b/StorageLib/Exceptions/ImportFormatException.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StorageLib.Exceptions +{ + public class ImportFormatException : Exception + { + public string Caption { get; } = "JSON Ungültig Formatiert!"; + public string Text { get; } = "Jede nachricht muss ein Schlüsselwort und eine Antwort enthalten."; + + public ImportFormatException(): base() { } + + public ImportFormatException(string text): base() + { + Text = text; + } + } +} diff --git a/StorageLib/Message.cs b/StorageLib/Message.cs new file mode 100644 index 0000000..69ccbca --- /dev/null +++ b/StorageLib/Message.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StorageLib +{ + /// + /// Possible Detection Types for a Message + /// + public enum KeywordDetection + { + MatchFull, + MatchPartial, + MatchFullCaseSensitive, + MatchPartialCaseSensitive, + } + + /// + /// Represents a keyword-awnser pair + /// + public class Message + { + /// + /// A unique identifier for the message + /// Allows for conversation hopping + /// + public string Slug { get; set; } + + /// + /// The keyword to be contained/matched to display the matching anwser + /// + public string Keyword { get; set; } + + /// + /// The anwser for the keyword + /// + public string Answer { get; set; } + + /// + /// The way the keyword should be matched + /// + public KeywordDetection Type { get; set; } = KeywordDetection.MatchPartial; + + /// + /// Childrens for this conversation + /// + public List Children { get; set; } = new List(); + + /// + /// Basic constructor for a Message + /// + /// The keyword for a message to get triggered + /// The anwser the bot should give + public Message(string keyword, string anwser) + { + Keyword = keyword; + Answer = anwser; + Slug = $"{keyword}_tree"; + } + + /// + /// Overflow constructor for a Message + /// + /// The keyword to match for the message to get triggered + /// The anwser the bot should give + /// The type of detection to use when trying to match a messge + public Message(string keyword, string anwser, KeywordDetection type) : this(keyword, anwser) + { + Type = type; + } + + /// + /// Basic empty constructor for JSON Seria + /// + public Message() : this(string.Empty, string.Empty) { } + + /// + /// Find a message by its slug (will check all children as well) + /// + /// The slug to search for + /// The message to return + public Message? FindMessageBySlug(string slug) + { + if (Slug == slug) return this; + + foreach (var child in Children) + { + var found = child.FindMessageBySlug(slug); + if (found != null) return found; + } + + return null; + } + + /// + /// Check if the conversation tree has messages left + /// + /// + public bool HasNextKeywords() + { + return Children.Count > 0; + } + + /// + /// Get the next possible Keywords + /// + /// + public string[] GetNextKeywords() + { + return Children.Select(w => w.Keyword).ToArray(); + } + + /// + /// Validate the message to be correct and usable by the program + /// + /// True if the message is valid + public bool Validate() + { + if (string.IsNullOrWhiteSpace(Keyword) || string.IsNullOrWhiteSpace(Answer)) + { + return false; + } + if (Children.Count > 0) + { + foreach (var child in Children) + { + if (!child.Validate()) + { + return false; + } + } + } + return true; + } + } +} diff --git a/StorageLib/Storage.cs b/StorageLib/Storage.cs new file mode 100644 index 0000000..22ece14 --- /dev/null +++ b/StorageLib/Storage.cs @@ -0,0 +1,168 @@ +using Microsoft.Win32; +using Newtonsoft.Json; +using StorageLib.Exceptions; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Input; +using System.Xml.Serialization; + +namespace StorageLib +{ + /// + /// Storage handling for keyword-awnser pairs + /// + public class Storage : INotifyPropertyChanged + { + + /// + /// Keyword - Awnser pairs + /// + public List Messages + { + get => _messages; + set + { + _messages = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(Count)); + } + } + private List _messages = new List(); + + /// + /// Count accessor + /// + public int Count => _count(Messages); + + public Storage() { } + + public event PropertyChangedEventHandler? PropertyChanged; + + /// + /// Simplify the usage of OnPropertyChanged + /// + /// + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + /// + /// Receive a message by keyword + /// + /// The keyword identifying a message + /// The default message for non-found keywords + /// The found Message + public Message? GetMessage(string keyword, Message? tree = null) + { + // Filter the array for the keyword + // Will use the Observable collection if no Message passed, the tree else. + Message[] messages = (tree?.Children.ToImmutableArray() ?? Messages.ToImmutableArray()).Where(m => + { + switch (m.Type) + { + case KeywordDetection.MatchFullCaseSensitive: + return m.Keyword.Trim() == keyword.Trim(); + case KeywordDetection.MatchPartialCaseSensitive: + return keyword.Trim().Contains(m.Keyword.Trim()); + case KeywordDetection.MatchFull: + return m.Keyword.ToLower().Trim() == keyword.ToLower().Trim(); + // MatchPartial is the default + default: + return keyword.ToLower().Trim().Contains(m.Keyword.ToLower().Trim()); + } + }).ToArray(); + // Return the default since no match for the keyword was found + if (messages.Length == 0) return null; + return messages.First(); + } + + /// + /// Get the top level keywords + /// + /// The keywords existing + public string[] GetTopLevelKeywords() + { + return Messages.Select(m => m.Keyword).ToArray(); + } + + /// + /// Allow a file path to be prodived for Import, it will be converted to a stream and use the Stream Import + /// + /// Path to the file to Import + public void Import(string filePath) + { + using (Stream stream = File.OpenRead(filePath)) + { + Import(stream); + } + } + + /// + /// Import from a stream to allow compatibility with embedded ressources + /// + /// The Stream to read + /// Thrown on invalid Data + public void Import(Stream stream) + { + using (StreamReader fileStream = new StreamReader(stream)) + using (JsonTextReader reader = new JsonTextReader(fileStream)) + { + JsonSerializer serializer = new JsonSerializer(); + try + { + List? importedMessages = serializer.Deserialize>(reader); + if (importedMessages == null || !importedMessages.Any()) + { + throw new ImportFormatException(); + } + + foreach (var message in importedMessages) + { + if (!message.Validate()) + { + throw new ImportFormatException( + $"Jede nachricht muss ein Schlüsselwort und eine Antwort enthalten.\n\nFehlerhalft:" + + $"\nSchlüsselwort: {(message.Keyword.Length > 0 ? message.Keyword : "????????????????")}" + + $"\nAntwort: {(message.Answer.Length > 0 ? message.Answer : "????????????????")}"); + } + } + + Messages = importedMessages; + } + catch (Exception ex) when (ex is JsonReaderException || ex is JsonSerializationException) + { + throw new InvalidDataException("Provided file does not contain valid JSON."); + } + } + } + + /// + /// Count the current Keywords in the Storage + /// + /// The message object to count the Keywords for + /// The count of all keywords + private int _count(List messages) + { + int count = 0; + foreach (var message in messages) + { + count++; + if (message.Children != null) + { + count += _count(message.Children); + } + } + return count; + } + } +} \ No newline at end of file diff --git a/StorageLib/StorageLib.csproj b/StorageLib/StorageLib.csproj new file mode 100644 index 0000000..0467fad --- /dev/null +++ b/StorageLib/StorageLib.csproj @@ -0,0 +1,13 @@ + + + + net7.0 + enable + enable + + + + + + + diff --git a/StorageLibTests/GlobalUsings.cs b/StorageLibTests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/StorageLibTests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/StorageLibTests/Message.cs b/StorageLibTests/Message.cs new file mode 100644 index 0000000..267e5a5 --- /dev/null +++ b/StorageLibTests/Message.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System; +using Xunit; +using StorageLib; + +namespace StorageLib.Tests +{ + public class MessageTests + { + [Fact] + public void Constructor_SetsExpectedProperties() + { + var message = new Message("hello", "hi"); + + Assert.Equal("hello", message.Keyword); + Assert.Equal("hi", message.Answer); + Assert.Equal("hello_tree", message.Slug); + Assert.Equal(KeywordDetection.MatchPartial, message.Type); + } + + [Fact] + public void ConstructorWithDetectionType_SetsExpectedProperties() + { + var message = new Message("hello", "hi", KeywordDetection.MatchFull); + + Assert.Equal("hello", message.Keyword); + Assert.Equal("hi", message.Answer); + Assert.Equal("hello_tree", message.Slug); + Assert.Equal(KeywordDetection.MatchFull, message.Type); + } + + [Fact] + public void FindMessageBySlug_FindsCorrectMessage() + { + var parent = new Message("parent", "parent_answer"); + var child = new Message("child", "child_answer"); + parent.Children.Add(child); + + Assert.Equal(child, parent.FindMessageBySlug("child_tree")); + } + + [Fact] + public void FindMessageBySlug_ReturnsNullForMissingSlug() + { + var parent = new Message("parent", "parent_answer"); + + Assert.Null(parent.FindMessageBySlug("missing_tree")); + } + + [Fact] + public void HasNextKeywords_ReturnsTrueWithChildren() + { + var parent = new Message("parent", "parent_answer"); + var child = new Message("child", "child_answer"); + parent.Children.Add(child); + + Assert.True(parent.HasNextKeywords()); + } + + [Fact] + public void HasNextKeywords_ReturnsFalseWithoutChildren() + { + var parent = new Message("parent", "parent_answer"); + + Assert.False(parent.HasNextKeywords()); + } + + [Fact] + public void GetNextKeywords_ReturnsCorrectKeywords() + { + var parent = new Message("parent", "parent_answer"); + var child1 = new Message("child1", "child1_answer"); + var child2 = new Message("child2", "child2_answer"); + parent.Children.Add(child1); + parent.Children.Add(child2); + + var expected = new string[] { "child1", "child2" }; + Assert.Equal(expected, parent.GetNextKeywords()); + } + + [Fact] + public void Validate_ReturnsFalseForEmptyFields() + { + var message = new Message("", ""); + + Assert.False(message.Validate()); + } + + [Fact] + public void Validate_ReturnsTrueForValidMessage() + { + var message = new Message("hello", "hi"); + + Assert.True(message.Validate()); + } + + [Fact] + public void Validate_ReturnsFalseForInvalidChild() + { + var parent = new Message("parent", "parent_answer"); + var child = new Message("", ""); + parent.Children.Add(child); + + Assert.False(parent.Validate()); + } + } +} + diff --git a/StorageLibTests/Storage.cs b/StorageLibTests/Storage.cs new file mode 100644 index 0000000..bfad6f4 --- /dev/null +++ b/StorageLibTests/Storage.cs @@ -0,0 +1,97 @@ +namespace StorageLib.Tests +{ + public class StorageTests + { + [Fact] + public void Constructor_InitializesCorrectly() + { + var storage = new Storage(); + + Assert.NotNull(storage.Messages); + Assert.Equal(0, storage.Count); + } + + [Fact] + public void PropertyChangedEvent_RaisedWhenMessagesChanged() + { + var storage = new Storage(); + bool eventRaised = false; + storage.PropertyChanged += (sender, args) => + { + if (args.PropertyName == "Messages") + { + eventRaised = true; + } + }; + + storage.Messages = new List(); + + Assert.True(eventRaised); + } + + [Theory] + [InlineData("key1", "answer1")] + [InlineData("key2", "answer2")] + public void GetMessage_ReturnsCorrectMessage(string keyword, string expectedAnswer) + { + var storage = new Storage + { + Messages = new List + { + new Message { Keyword = "key1", Answer = "answer1", Type = KeywordDetection.MatchFull }, + new Message { Keyword = "key2", Answer = "answer2", Type = KeywordDetection.MatchFull } + } + }; + + var message = storage.GetMessage(keyword); + + Assert.Equal(expectedAnswer, message?.Answer); + } + + [Fact] + public void Count_ReturnsCorrectCount() + { + var storage = new Storage + { + Messages = new List + { + new Message { Keyword = "key1", Answer = "answer1", Children = new List { new Message { Keyword = "childKey1", Answer = "childAnswer1" } } }, + new Message { Keyword = "key2", Answer = "answer2" } + } + }; + + Assert.Equal(3, storage.Count); + } + + [Fact] + public void GetTopLevelKeywords_ReturnsCorrectKeywords() + { + var storage = new Storage + { + Messages = new List + { + new Message { Keyword = "key1", Answer = "answer1" }, + new Message { Keyword = "key2", Answer = "answer2" } + } + }; + + var keywords = storage.GetTopLevelKeywords(); + + Assert.Equal(2, keywords.Length); + Assert.Contains("key1", keywords); + Assert.Contains("key2", keywords); + } + + [Fact] + public void Import_ThrowsExceptionForInvalidData() + { + var storage = new Storage(); + var invalidJson = "{ invalidJson: true }"; + + using (var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(invalidJson))) + { + Assert.Throws(() => storage.Import(stream)); + } + } + } +} diff --git a/StorageLibTests/StorageLib.Tests.csproj b/StorageLibTests/StorageLib.Tests.csproj new file mode 100644 index 0000000..992fcc3 --- /dev/null +++ b/StorageLibTests/StorageLib.Tests.csproj @@ -0,0 +1,29 @@ + + + + net7.0-windows + enable + enable + + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + +