diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs index b48fab7f..1ff6230a 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs @@ -8,320 +8,396 @@ namespace AIStudio.Tools.PluginSystem; /// public abstract class PluginBase { - private readonly string parseError; - + private readonly IReadOnlyCollection baseIssues; protected readonly LuaState state; - protected readonly Guid pluginId; - protected readonly string pluginName; - protected readonly PluginType pluginType; - protected readonly string pluginDescription; - protected readonly PluginVersion pluginVersion; - protected readonly string[] pluginAuthors; - protected readonly string supportContact; - protected readonly string sourceURL; - protected readonly PluginCategory[] pluginCategories; - protected readonly PluginTargetGroup[] pluginTargetGroups; + + protected List pluginIssues = []; - private readonly bool isInitialized; - private bool isValid; + /// + /// The type of this plugin. + /// + public PluginType Type { get; } + + /// + /// The ID of this plugin. + /// + public Guid Id { get; } + + /// + /// The name of this plugin. + /// + public string Name { get; } = string.Empty; + + /// + /// The description of this plugin. + /// + public string Description { get; } = string.Empty; + + /// + /// The version of this plugin. + /// + public PluginVersion Version { get; } + + /// + /// The authors of this plugin. + /// + public string[] Authors { get; } = []; + + /// + /// The support contact for this plugin. + /// + public string SupportContact { get; } = string.Empty; + + /// + /// The source URL of this plugin. + /// + public string SourceURL { get; } = string.Empty; + + /// + /// The categories of this plugin. + /// + public PluginCategory[] Categories { get; } = []; + + /// + /// The target groups of this plugin. + /// + public PluginTargetGroup[] TargetGroups { get; } = []; + + /// + /// The issues that occurred during the initialization of this plugin. + /// + public IEnumerable Issues => this.baseIssues.Concat(this.pluginIssues); + + /// + /// True, when the plugin is valid. + /// + /// + /// False means that there were issues during the initialization of the plugin. + /// Please check the Issues property for more information. + /// + public bool IsValid => this is not NoPlugin && this.baseIssues.Count == 0 && this.pluginIssues.Count == 0; protected PluginBase(LuaState state, PluginType type, string parseError = "") { this.state = state; - this.pluginType = type; - this.pluginId = this.Id(); - this.pluginName = this.Name(); - this.pluginDescription = this.Description(); - this.pluginVersion = this.Version(); - this.pluginAuthors = this.Authors(); - this.supportContact = this.SupportContact(); - this.sourceURL = this.SourceURL(); - this.pluginCategories = this.Categories(); - this.pluginTargetGroups = this.TargetGroups(); - this.parseError = parseError; - + this.Type = type; + // For security reasons, we don't want to allow the plugin to load modules: this.state.ModuleLoader = new NoModuleLoader(); - // - // Check if the plugin is valid: - // - if(!string.IsNullOrWhiteSpace(this.parseError)) - this.isValid = false; + var issues = new List(); + if(!string.IsNullOrWhiteSpace(parseError)) + issues.Add(parseError); - if(this is NoPlugin) - this.isValid = false; + if(this.TryInitId(out var issue, out var id)) + this.Id = id; + else if(this is not NoPlugin) + issues.Add(issue); - this.isInitialized = true; + if(this.TryInitName(out issue, out var name)) + this.Name = name; + else if(this is not NoPlugin) + issues.Add(issue); + + if(this.TryInitDescription(out issue, out var description)) + this.Description = description; + else if(this is not NoPlugin) + issues.Add(issue); + + if(this.TryInitVersion(out issue, out var version)) + this.Version = version; + else if(this is not NoPlugin) + issues.Add(issue); + + if(this.TryInitAuthors(out issue, out var authors)) + this.Authors = authors; + else if(this is not NoPlugin) + issues.Add(issue); + + if(this.TryInitSupportContact(out issue, out var contact)) + this.SupportContact = contact; + else if(this is not NoPlugin) + issues.Add(issue); + + if(this.TryInitSourceURL(out issue, out var url)) + this.SourceURL = url; + else if(this is not NoPlugin) + issues.Add(issue); + + if(this.TryInitCategories(out issue, out var categories)) + this.Categories = categories; + else if(this is not NoPlugin) + issues.Add(issue); + + if(this.TryInitTargetGroups(out issue, out var targetGroups)) + this.TargetGroups = targetGroups; + else if(this is not NoPlugin) + issues.Add(issue); + + this.baseIssues = issues; } + #region Initialization-related methods + /// - /// Checks if the plugin is valid. + /// Tries to read the ID of the plugin. /// - /// The state of the plugin, which may contain an error message. - public PluginState IsValid() + /// The error message, when the ID could not be read. + /// The read ID. + /// True, when the ID could be read successfully. + public bool TryInitId(out string message, out Guid id) { - if(!string.IsNullOrWhiteSpace(this.parseError)) + if (!this.state.Environment["ID"].TryRead(out var idText)) { - this.isValid = false; - return new(false, this.parseError); + message = "The field ID does not exist or is not a valid string."; + id = Guid.Empty; + return false; } - if(this is NoPlugin) + if (!Guid.TryParse(idText, out id)) { - this.isValid = false; - return new(false, "Plugin is not valid."); + message = "The field ID is not a valid GUID / UUID. The ID must be formatted in the 8-4-4-4-12 format (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)."; + id = Guid.Empty; + return false; + } + + if(id == Guid.Empty) + { + message = "The field ID is empty. The ID must be formatted in the 8-4-4-4-12 format (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)."; + return false; } - if(this.Id() == Guid.Empty) - { - this.isValid = false; - return new(false, "The field ID does not exist, is empty, or is not a valid GUID / UUID. The ID must be formatted in the 8-4-4-4-12 format (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)."); - } - - if(string.IsNullOrWhiteSpace(this.Name())) - { - this.isValid = false; - return new(false, "The field NAME does not exist, is empty, or is not a valid string."); - } - - if(string.IsNullOrWhiteSpace(this.Description())) - { - this.isValid = false; - return new(false, "The field DESCRIPTION does not exist, is empty, or is not a valid string."); - } - - if(this.Version() == PluginVersion.NONE) - { - this.isValid = false; - return new(false, "The field VERSION does not exist, is empty, or is not a valid version number. The version number must be formatted as string in the major.minor.patch format (X.X.X)."); - } - - if(this.pluginType == PluginType.NONE) - { - this.isValid = false; - return new(false, $"The field TYPE does not exist, is empty, or is not a valid plugin type. Valid types are: {CommonTools.GetAllEnumValues(PluginType.NONE)}."); - } - - if(this.Authors().Length == 0) - { - this.isValid = false; - return new(false, "The table AUTHORS does not exist, is empty, or is not a valid table of strings."); - } - - if(string.IsNullOrWhiteSpace(this.SupportContact())) - { - this.isValid = false; - return new(false, "The field SUPPORT_CONTACT does not exist, is empty, or is not a valid string."); - } - - if(string.IsNullOrWhiteSpace(this.SourceURL())) - { - this.isValid = false; - return new(false, "The field SOURCE_URL does not exist, is empty, or is not a valid string. Additional, it must start with 'http://' or 'https://'."); - } - - if(this.Categories().Length == 0) - { - this.isValid = false; - return new(false, $"The table CATEGORIES does not exist, is empty, or is not a valid table of strings. Valid categories are: {CommonTools.GetAllEnumValues(PluginCategory.NONE)}."); - } - - if(this.TargetGroups().Length == 0) - { - this.isValid = false; - return new(false, $"The table TARGET_GROUPS does not exist, is empty, or is not a valid table of strings. Valid target groups are: {CommonTools.GetAllEnumValues(PluginTargetGroup.NONE)}."); - } - - this.isValid = true; - return new(true, string.Empty); + message = string.Empty; + return true; } - + /// - /// Returns the intended target groups for the plugin. + /// Tries to read the name of the plugin. /// - /// The target groups. - public PluginTargetGroup[] TargetGroups() + /// The error message, when the name could not be read. + /// The read name. + /// True, when the name could be read successfully. + public bool TryInitName(out string message, out string name) { - if(this.isInitialized) - return this.pluginTargetGroups; + if (!this.state.Environment["NAME"].TryRead(out name)) + { + message = "The field NAME does not exist or is not a valid string."; + name = string.Empty; + return false; + } - if(!this.isValid) - return []; - - if (!this.state.Environment["TARGET_GROUPS"].TryRead(out var targetGroups)) - return []; - - var targetGroupList = new List(); - foreach(var luaTargetGroup in targetGroups.GetArraySpan()) - if(luaTargetGroup.TryRead(out var targetGroupName)) - if(Enum.TryParse(targetGroupName, out var targetGroup) && targetGroup != PluginTargetGroup.NONE) - targetGroupList.Add(targetGroup); - - return targetGroupList.ToArray(); + if(string.IsNullOrWhiteSpace(name)) + { + message = "The field NAME is empty. The name must be a non-empty string."; + return false; + } + + message = string.Empty; + return true; } - + /// - /// Returns the plugin categories. + /// Tries to read the description of the plugin. /// - /// The plugin categories. - public PluginCategory[] Categories() + /// The error message, when the description could not be read. + /// The read description. + /// True, when the description could be read successfully. + public bool TryInitDescription(out string message, out string description) { - if(this.isInitialized) - return this.pluginCategories; + if (!this.state.Environment["DESCRIPTION"].TryRead(out description)) + { + message = "The field DESCRIPTION does not exist or is not a valid string."; + description = string.Empty; + return false; + } - if(!this.isValid) - return []; + if(string.IsNullOrWhiteSpace(description)) + { + message = "The field DESCRIPTION is empty. The description must be a non-empty string."; + return false; + } - if (!this.state.Environment["CATEGORIES"].TryRead(out var categories)) - return []; + message = string.Empty; + return true; + } + + /// + /// Tries to read the version of the plugin. + /// + /// The error message, when the version could not be read. + /// The read version. + /// True, when the version could be read successfully. + public bool TryInitVersion(out string message, out PluginVersion version) + { + if (!this.state.Environment["VERSION"].TryRead(out var versionText)) + { + message = "The field VERSION does not exist or is not a valid string."; + version = PluginVersion.NONE; + return false; + } + + if (!PluginVersion.TryParse(versionText, out version)) + { + message = "The field VERSION is not a valid version number. The version number must be formatted as string in the major.minor.patch format (X.X.X)."; + version = PluginVersion.NONE; + return false; + } + + if(version == PluginVersion.NONE) + { + message = "The field VERSION is empty. The version number must be formatted as string in the major.minor.patch format (X.X.X)."; + return false; + } + + message = string.Empty; + return true; + } + + /// + /// Tries to read the authors of the plugin. + /// + /// The error message, when the authors could not be read. + /// The read authors. + /// True, when the authors could be read successfully. + public bool TryInitAuthors(out string message, out string[] authors) + { + if (!this.state.Environment["AUTHORS"].TryRead(out var authorsTable)) + { + authors = []; + message = "The table AUTHORS does not exist or is using an invalid syntax."; + return false; + } + + var authorList = new List(); + foreach(var author in authorsTable.GetArraySpan()) + if(author.TryRead(out var authorName)) + authorList.Add(authorName); + + authors = authorList.ToArray(); + if(authorList.Count == 0) + { + message = "The table AUTHORS is empty. At least one author must be specified."; + return false; + } + + message = string.Empty; + return true; + } + + /// + /// Tries to read the support contact for the plugin. + /// + /// The error message, when the support contact could not be read. + /// The read support contact. + /// True, when the support contact could be read successfully. + public bool TryInitSupportContact(out string message, out string contact) + { + if (!this.state.Environment["SUPPORT_CONTACT"].TryRead(out contact)) + { + contact = string.Empty; + message = "The field SUPPORT_CONTACT does not exist or is not a valid string."; + return false; + } + + if(string.IsNullOrWhiteSpace(contact)) + { + message = "The field SUPPORT_CONTACT is empty. The support contact must be a non-empty string."; + return false; + } + + message = string.Empty; + return true; + } + + /// + /// Try to read the source URL of the plugin. + /// + /// The error message, when the source URL could not be read. + /// The read source URL. + /// True, when the source URL could be read successfully. + public bool TryInitSourceURL(out string message, out string url) + { + if (!this.state.Environment["SOURCE_URL"].TryRead(out url)) + { + url = string.Empty; + message = "The field SOURCE_URL does not exist or is not a valid string."; + return false; + } + + if (!url.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase) && !url.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase)) + { + url = string.Empty; + message = "The field SOURCE_URL is not a valid URL. The URL must start with 'http://' or 'https://'."; + return false; + } + + message = string.Empty; + return true; + } + + /// + /// Tries to read the categories of the plugin. + /// + /// The error message, when the categories could not be read. + /// The read categories. + /// True, when the categories could be read successfully. + public bool TryInitCategories(out string message, out PluginCategory[] categories) + { + if (!this.state.Environment["CATEGORIES"].TryRead(out var categoriesTable)) + { + categories = []; + message = "The table CATEGORIES does not exist or is using an invalid syntax."; + return false; + } var categoryList = new List(); - foreach(var luaCategory in categories.GetArraySpan()) + foreach(var luaCategory in categoriesTable.GetArraySpan()) if(luaCategory.TryRead(out var categoryName)) if(Enum.TryParse(categoryName, out var category) && category != PluginCategory.NONE) categoryList.Add(category); - return categoryList.ToArray(); - } - - /// - /// Returns the source URL of the plugin. - /// - /// The source URL. - public string SourceURL() - { - if(this.isInitialized) - return this.sourceURL; + categories = categoryList.ToArray(); + if(categoryList.Count == 0) + { + message = $"The table CATEGORIES is empty. At least one category is necessary. Valid categories are: {CommonTools.GetAllEnumValues(PluginCategory.NONE)}."; + return false; + } - if(!this.isValid) - return string.Empty; - - if (!this.state.Environment["SOURCE_URL"].TryRead(out var url)) - return string.Empty; - - if(!url.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase) && !url.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase)) - return string.Empty; - - return url; - } - - /// - /// Returns the support contact of the plugin. - /// - /// The support contact. - public string SupportContact() - { - if(this.isInitialized) - return this.supportContact; - - if(!this.isValid) - return string.Empty; - - if (!this.state.Environment["SUPPORT_CONTACT"].TryRead(out var contact)) - return string.Empty; - - return contact; - } - - /// - /// Returns the ID of the plugin. - /// - /// The plugin ID. - public Guid Id() - { - if(this.isInitialized) - return this.pluginId; - - if(!this.isValid) - return Guid.Empty; - - if (!this.state.Environment["ID"].TryRead(out var idText)) - return Guid.Empty; - - if (!Guid.TryParse(idText, out var id)) - return Guid.Empty; - - return id; + message = string.Empty; + return true; } /// - /// Returns the name of the plugin. + /// Tries to read the intended target groups for the plugin. /// - /// The plugin name. - public string Name() + /// The error message, when the target groups could not be read. + /// The read target groups. + /// True, when the target groups could be read successfully. + private bool TryInitTargetGroups(out string message, out PluginTargetGroup[] targetGroups) { - if(this.isInitialized) - return this.pluginName; - - if(!this.isValid) - return string.Empty; - - if (!this.state.Environment["NAME"].TryRead(out var name)) - return string.Empty; + if (!this.state.Environment["TARGET_GROUPS"].TryRead(out var targetGroupsTable)) + { + targetGroups = []; + message = "The table TARGET_GROUPS does not exist or is using an invalid syntax."; + return false; + } - return name; + var targetGroupList = new List(); + foreach(var luaTargetGroup in targetGroupsTable.GetArraySpan()) + if(luaTargetGroup.TryRead(out var targetGroupName)) + if(Enum.TryParse(targetGroupName, out var targetGroup) && targetGroup != PluginTargetGroup.NONE) + targetGroupList.Add(targetGroup); + + targetGroups = targetGroupList.ToArray(); + if(targetGroups.Length == 0) + { + message = "The table TARGET_GROUPS is empty or is not a valid table of strings. Valid target groups are: {CommonTools.GetAllEnumValues(PluginTargetGroup.NONE)}."; + return false; + } + + message = string.Empty; + return true; } - - /// - /// Returns the description of the plugin. - /// - /// The plugin description. - public string Description() - { - if(this.isInitialized) - return this.pluginDescription; - - if(!this.isValid) - return string.Empty; - - if (!this.state.Environment["DESCRIPTION"].TryRead(out var description)) - return string.Empty; - - return description; - } - - /// - /// Returns the version of the plugin. - /// - /// The plugin version. - public PluginVersion Version() - { - if(this.isInitialized) - return this.pluginVersion; - - if(!this.isValid) - return PluginVersion.NONE; - - if (!this.state.Environment["VERSION"].TryRead(out var versionText)) - return PluginVersion.NONE; - - if (!PluginVersion.TryParse(versionText, out var version)) - return PluginVersion.NONE; - - return version; - } - - /// - /// Returns the authors of the plugin. - /// - /// The plugin authors. - public string[] Authors() - { - if(this.isInitialized) - return this.pluginAuthors; - if (!this.isValid) - return []; - - if (!this.state.Environment["AUTHORS"].TryRead(out var authors)) - return []; - - var authorList = new List(); - foreach(var author in authors.GetArraySpan()) - if(author.TryRead(out var authorName)) - authorList.Add(authorName); - - return authorList.ToArray(); - } + #endregion } \ No newline at end of file