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
+
+
+
+
+
+
+
+