diff --git a/Assets/Plugins/Source/Editor/Utility/GUIEditorUtility.cs b/Assets/Plugins/Source/Editor/Utility/GUIEditorUtility.cs index 9032284a6..e681e441c 100644 --- a/Assets/Plugins/Source/Editor/Utility/GUIEditorUtility.cs +++ b/Assets/Plugins/Source/Editor/Utility/GUIEditorUtility.cs @@ -36,6 +36,7 @@ namespace PlayEveryWare.EpicOnlineServices.Editor.Utility using System.Globalization; using System.Linq; using System.Reflection; + using System.Text; using UnityEditor; using UnityEditorInternal; using UnityEngine; @@ -245,7 +246,7 @@ public static void AssigningEnumField(string label, ref T value, float labelW .OrderBy(group => group.Key); } - private static IOrderedEnumerable> GetMembersByGroup() + private static IOrderedEnumerable FieldValidators)>> GetMembersByGroup() { var fields = typeof(T).GetFields(BindingFlags.Public | BindingFlags.Instance); var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance); @@ -254,7 +255,8 @@ public static void AssigningEnumField(string label, ref T value, float labelW return members .Where(member => member.GetCustomAttribute() != null) - .Select(member => (MemberInfo: member, FieldDetails: member.GetCustomAttribute())) + .Select(member => (MemberInfo: member, FieldDetails: member.GetCustomAttribute(), + FieldValidators: member.GetCustomAttributes())) .GroupBy(r => r.FieldDetails.Group) .OrderBy(group => group.Key); } @@ -271,7 +273,6 @@ public static void AssigningEnumField(string label, ref T value, float labelW { typeof(float), (attr, val, width) => RenderInput(attr, (float)val, width) }, { typeof(double), (attr, val, width) => RenderInput(attr, (double)val, width) }, { typeof(bool), (attr, val, width) => RenderInput(attr, (bool)val, width) }, - { typeof(Version), (attr, val, width) => RenderInput(attr, (Version)val, width) }, { typeof(Guid), (attr, val, width) => RenderInput(attr, (Guid)val, width)}, { typeof(List), (attr, val, width) => RenderInput(attr, (List)val, width)}, #if !EOS_DISABLE @@ -500,6 +501,9 @@ public static void RenderInputs(ref T value) { continue; // Skip if MemberInfo is neither FieldInfo nor PropertyInfo } + + // Assign the validators + member.FieldDetails.Validators = member.FieldValidators; // Use the handler from the dictionary if (FieldHandlers.TryGetValue(member.FieldDetails.FieldType, out var handler)) @@ -1126,7 +1130,7 @@ public static WrappedInitializeThreadAffinity RenderInput(ConfigFieldAttribute a #endif private static Guid RenderInput(ConfigFieldAttribute configFieldDetails, Guid value, float labelWidth) { - return InputRendererWrapper(configFieldDetails.Label, configFieldDetails.ToolTip, labelWidth, value, + return InputRendererWrapper(configFieldDetails.Label, configFieldDetails.ToolTip, labelWidth, value, configFieldDetails.Validators, GuidField); } @@ -1212,22 +1216,13 @@ public static Deployment RenderInput(ConfigFieldAttribute configFieldAttribute, public static TEnum RenderEnumInput(ConfigFieldAttribute configFieldAttribute, TEnum value, float labelWidth) where TEnum : Enum { return InputRendererWrapper(configFieldAttribute.Label, configFieldAttribute.ToolTip, labelWidth, value, - EnumFlagsField, configFieldAttribute.HelpURL); + configFieldAttribute.Validators, EnumFlagsField, configFieldAttribute.HelpURL); } private static TEnum EnumFlagsField(GUIContent label, TEnum value, params GUILayoutOption[] options) where TEnum : Enum { return (TEnum)EditorGUILayout.EnumFlagsField(label, value, options); } - private static Version RenderInput(ConfigFieldAttribute configFieldAttribute, Version value, float labelWidth) - { - return RenderInput(value, configFieldAttribute.Label, configFieldAttribute.ToolTip, labelWidth); - } - - public static Version RenderInput(Version value, string label, string tooltip, float labelWidth) - { - return InputRendererWrapper(label, tooltip, labelWidth, value, VersionField); - } public static ProductionEnvironments RenderInput(ConfigFieldAttribute configFieldAttribute, ProductionEnvironments value, float labelWidth) @@ -1306,7 +1301,7 @@ public static string RenderInput(DirectoryPathFieldAttribute configFieldAttribut { EditorGUILayout.BeginHorizontal(); - string filePath = InputRendererWrapper(configFieldAttributeDetails.Label, value, labelWidth, tooltip, EditorGUILayout.TextField, configFieldAttributeDetails.HelpURL); + string filePath = InputRendererWrapper(configFieldAttributeDetails.Label, value, labelWidth, tooltip, configFieldAttributeDetails.Validators, EditorGUILayout.TextField, configFieldAttributeDetails.HelpURL); if (GUILayout.Button("Select", GUILayout.MaxWidth(MAXIMUM_BUTTON_WIDTH))) { @@ -1327,7 +1322,7 @@ public static string RenderInput(FilePathFieldAttribute configFieldAttributeDeta { EditorGUILayout.BeginHorizontal(); - string filePath = InputRendererWrapper(configFieldAttributeDetails.Label, value, labelWidth, tooltip, EditorGUILayout.TextField, configFieldAttributeDetails.HelpURL); + string filePath = InputRendererWrapper(configFieldAttributeDetails.Label, value, labelWidth, tooltip, configFieldAttributeDetails.Validators, EditorGUILayout.TextField, configFieldAttributeDetails.HelpURL); if (GUILayout.Button("Select", GUILayout.MaxWidth(MAXIMUM_BUTTON_WIDTH))) { @@ -1347,17 +1342,17 @@ public static string RenderInput(FilePathFieldAttribute configFieldAttributeDeta public static double RenderInput(ConfigFieldAttribute configFieldDetails, double value, float labelWidth) { - return InputRendererWrapper(configFieldDetails.Label, configFieldDetails.ToolTip, labelWidth, value, EditorGUILayout.DoubleField, configFieldDetails.HelpURL); + return InputRendererWrapper(configFieldDetails.Label, configFieldDetails.ToolTip, labelWidth, value, configFieldDetails.Validators, EditorGUILayout.DoubleField, configFieldDetails.HelpURL); } public static float RenderInput(ConfigFieldAttribute configFieldDetails, float value, float labelWidth) { - return InputRendererWrapper(configFieldDetails.Label, configFieldDetails.ToolTip, labelWidth, value, EditorGUILayout.FloatField, configFieldDetails.HelpURL); + return InputRendererWrapper(configFieldDetails.Label, configFieldDetails.ToolTip, labelWidth, value, configFieldDetails.Validators, EditorGUILayout.FloatField, configFieldDetails.HelpURL); } public static string RenderInput(ConfigFieldAttribute configFieldDetails, string value, float labelWidth) { - return InputRendererWrapper(configFieldDetails.Label, configFieldDetails.ToolTip, labelWidth, value, EditorGUILayout.TextField, configFieldDetails.HelpURL); + return InputRendererWrapper(configFieldDetails.Label, configFieldDetails.ToolTip, labelWidth, value, configFieldDetails.Validators, EditorGUILayout.TextField, configFieldDetails.HelpURL); } public static ulong RenderInput(ConfigFieldAttribute configFieldDetails, ulong value, float labelWidth) @@ -1365,17 +1360,7 @@ public static ulong RenderInput(ConfigFieldAttribute configFieldDetails, ulong v _ = SafeTranslatorUtility.TryConvert(value, out long temp); long longValue = InputRendererWrapper(configFieldDetails.Label, configFieldDetails.ToolTip, labelWidth, - temp, EditorGUILayout.LongField); - - return SafeTranslatorUtility.TryConvert(longValue, out ulong newValue) ? newValue : value; - } - - private static ulong RenderInput(string label, string tooltip, ulong value, float labelWidth) - { - _ = SafeTranslatorUtility.TryConvert(value, out long temp); - - long longValue = InputRendererWrapper(label, tooltip, labelWidth, - temp, EditorGUILayout.LongField); + temp, configFieldDetails.Validators, EditorGUILayout.LongField); return SafeTranslatorUtility.TryConvert(longValue, out ulong newValue) ? newValue : value; } @@ -1384,7 +1369,7 @@ public static uint RenderInput(ConfigFieldAttribute configFieldDetails, uint val { _ = SafeTranslatorUtility.TryConvert(value, out int temp); - int intValue = InputRendererWrapper(configFieldDetails.Label, configFieldDetails.ToolTip, labelWidth, temp, + int intValue = InputRendererWrapper(configFieldDetails.Label, configFieldDetails.ToolTip, labelWidth, temp, configFieldDetails.Validators, EditorGUILayout.IntField); return SafeTranslatorUtility.TryConvert(intValue, out uint newValue) ? newValue : value; @@ -1394,10 +1379,10 @@ public static bool RenderInput(ConfigFieldAttribute configFieldDetails, bool val { return InputRendererWrapper( configFieldDetails.Label, configFieldDetails.ToolTip, labelWidth, - value, EditorGUILayout.Toggle); + value, configFieldDetails.Validators, EditorGUILayout.Toggle); } - public delegate T TestDelegate(GUIContent label, T value, params GUILayoutOption[] options); + public delegate T InputRendererDelegate(GUIContent label, T value, params GUILayoutOption[] options); private static T InputRendererWithAlignedLabel(float labelWidth, Func renderFn) { @@ -1412,10 +1397,50 @@ private static T InputRendererWithAlignedLabel(float labelWidth, Func rend return newValue; } - private static T InputRendererWrapper(string label, string toolTip, float labelWidth, T value, TestDelegate renderFn, string helpURL = null) + private static void RunValidators(IEnumerable validators, object value, out bool isValid) + { + isValid = true; + StringBuilder errorMessageBuilder = new(); + foreach (var validator in validators) + { + // If field is valid then go to the next validator. + if (validator.FieldValueIsValid(value, out string errorMessage)) + { + continue; + } + + // Otherwise append message from the validator. + errorMessageBuilder.AppendLine(errorMessage); + } + + // If there are no error messages, then stop here + if (errorMessageBuilder.Length == 0) + { + return; + } + + isValid = false; + EditorGUILayout.HelpBox(errorMessageBuilder.ToString(), MessageType.Warning); + } + + private static T InputRendererWrapper(string label, string toolTip, float labelWidth, T value, IEnumerable validators, InputRendererDelegate renderFn, string helpURL = null) { return InputRendererWithAlignedLabel(labelWidth, () => { + // Run validators for the config field. + RunValidators(validators, value, out bool isCurrentValueValid); + + // Store the previous background color so that it can be + // restored if need be. + Color previousBackgroundColor = GUI.backgroundColor; + if (!isCurrentValueValid) + { + // This sets the background color for the input field that + // is about to be rendered to red - further highlighting the + // field that has invalid values. + GUI.backgroundColor = Color.red; + } + if (!string.IsNullOrEmpty(helpURL)) { EditorGUILayout.BeginHorizontal(); @@ -1429,6 +1454,10 @@ private static T InputRendererWrapper(string label, string toolTip, float lab EditorGUILayout.EndHorizontal(); } + // Restore the background color that was set before the field + // was rendered. + GUI.backgroundColor = previousBackgroundColor; + return newValue; }); } diff --git a/com.playeveryware.eos/Runtime/Core/Config/Attributes/ConfigFieldAttribute.cs b/com.playeveryware.eos/Runtime/Core/Config/Attributes/ConfigFieldAttribute.cs index 39c082d7c..9592b0676 100644 --- a/com.playeveryware.eos/Runtime/Core/Config/Attributes/ConfigFieldAttribute.cs +++ b/com.playeveryware.eos/Runtime/Core/Config/Attributes/ConfigFieldAttribute.cs @@ -25,6 +25,7 @@ namespace PlayEveryWare.EpicOnlineServices { using System; + using System.Collections.Generic; [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] public class ConfigFieldAttribute : Attribute @@ -58,15 +59,22 @@ public class ConfigFieldAttribute : Attribute /// public PlatformManager.Platform PlatformsEnabledOn { get; } + public IEnumerable Validators { get; set; } + public ConfigFieldAttribute( PlatformManager.Platform enabledOn, string label, ConfigFieldType type, string tooltip = null, int group = -1, - string helpUrl = null) : this(label, type, tooltip, group, helpUrl) + string helpUrl = null) { PlatformsEnabledOn = enabledOn; + Label = label; + FieldType = type; + ToolTip = tooltip; + Group = group; + HelpURL = helpUrl; } public ConfigFieldAttribute( @@ -74,15 +82,8 @@ public ConfigFieldAttribute( ConfigFieldType type, string tooltip = null, int group = -1, - string helpUrl = null) - { - PlatformsEnabledOn = PlatformManager.Platform.Any; - HelpURL = helpUrl; - Label = label; - ToolTip = tooltip; - Group = group; - FieldType = type; - } + string helpUrl = null) : this(PlatformManager.Platform.Any, label, type, tooltip, group, helpUrl) + { } } } diff --git a/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators.meta b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators.meta new file mode 100644 index 000000000..f347ea1d4 --- /dev/null +++ b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6f7d08e961222604598592968435bcd4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.playeveryware.eos/Runtime/Core/Config/Attributes/FieldValidator.cs b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/FieldValidator.cs similarity index 100% rename from com.playeveryware.eos/Runtime/Core/Config/Attributes/FieldValidator.cs rename to com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/FieldValidator.cs diff --git a/com.playeveryware.eos/Runtime/Core/Config/Attributes/FieldValidator.cs.meta b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/FieldValidator.cs.meta similarity index 83% rename from com.playeveryware.eos/Runtime/Core/Config/Attributes/FieldValidator.cs.meta rename to com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/FieldValidator.cs.meta index 73f66de2a..c30de2f5e 100644 --- a/com.playeveryware.eos/Runtime/Core/Config/Attributes/FieldValidator.cs.meta +++ b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/FieldValidator.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 9209686f48242544997a71f68e50fa07 +guid: 10d2cf32aa930674697799f5f5cf5fb9 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/com.playeveryware.eos/Runtime/Core/Config/Attributes/FieldValidatorAttribute.cs b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/FieldValidatorAttribute.cs similarity index 89% rename from com.playeveryware.eos/Runtime/Core/Config/Attributes/FieldValidatorAttribute.cs rename to com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/FieldValidatorAttribute.cs index bab24907f..73dbc6251 100644 --- a/com.playeveryware.eos/Runtime/Core/Config/Attributes/FieldValidatorAttribute.cs +++ b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/FieldValidatorAttribute.cs @@ -25,10 +25,9 @@ namespace PlayEveryWare.EpicOnlineServices using System; using System.Reflection; - [AttributeUsage(AttributeTargets.Field)] - + [AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] public abstract class FieldValidatorAttribute : Attribute { - public abstract bool FieldValueIsValid(object toValidate, out string configurationProblemMessage); + public abstract bool FieldValueIsValid(object value, out string errorMessage); } } \ No newline at end of file diff --git a/com.playeveryware.eos/Runtime/Core/Config/Attributes/FieldValidatorAttribute.cs.meta b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/FieldValidatorAttribute.cs.meta similarity index 83% rename from com.playeveryware.eos/Runtime/Core/Config/Attributes/FieldValidatorAttribute.cs.meta rename to com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/FieldValidatorAttribute.cs.meta index 1806af4aa..f5016e6e0 100644 --- a/com.playeveryware.eos/Runtime/Core/Config/Attributes/FieldValidatorAttribute.cs.meta +++ b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/FieldValidatorAttribute.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 70e4cb59556588240bcc533a7849c5c2 +guid: f07c746cc4a2bef428b1aa020ca61c9f MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/com.playeveryware.eos/Runtime/Core/Config/Attributes/GUIDFieldValidatorAttribute.cs b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/GUIDFieldValidatorAttribute.cs similarity index 100% rename from com.playeveryware.eos/Runtime/Core/Config/Attributes/GUIDFieldValidatorAttribute.cs rename to com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/GUIDFieldValidatorAttribute.cs diff --git a/com.playeveryware.eos/Runtime/Core/Config/Attributes/GUIDFieldValidatorAttribute.cs.meta b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/GUIDFieldValidatorAttribute.cs.meta similarity index 83% rename from com.playeveryware.eos/Runtime/Core/Config/Attributes/GUIDFieldValidatorAttribute.cs.meta rename to com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/GUIDFieldValidatorAttribute.cs.meta index b1be740c8..6e38aa098 100644 --- a/com.playeveryware.eos/Runtime/Core/Config/Attributes/GUIDFieldValidatorAttribute.cs.meta +++ b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/GUIDFieldValidatorAttribute.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: abee8fbb46d0fac49a036589047fcbad +guid: 8cd3e5bb57648514eae4182799ae74f4 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/LengthValidationAttribute.cs b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/LengthValidationAttribute.cs new file mode 100644 index 000000000..3f8dffdd4 --- /dev/null +++ b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/LengthValidationAttribute.cs @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2025 PlayEveryWare + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#if !EOS_DISABLE + +namespace PlayEveryWare.EpicOnlineServices +{ + using System; + + /// + /// Used to describe a validation attribute that validates the length of a + /// string value. + /// TODO: Replace NonEmptyStringValidatorAttribute with this attribute. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] + public class LengthValidationAttribute : StringValidationAttribute + { + private readonly int? _minLength; + private readonly int? _maxLength; + + // Constructor for specifying both min and max length + public LengthValidationAttribute(int minLength, int maxLength) + { + _minLength = minLength; + _maxLength = maxLength; + } + + // Constructor for specifying only min length + public LengthValidationAttribute(int minLength) + { + _minLength = minLength; + _maxLength = null; + } + + // Constructor for specifying only max length + public LengthValidationAttribute(int maxLength, bool isMaxOnly) + { + _minLength = null; + _maxLength = maxLength; + } + + public override bool ValidateStringField(string value, out string errorMessage) + { + // If the minimum length is set and greater than 0, then fail if + // string is either null or is too short. + if ((_minLength is > 0 && value == null) || value.Length < _minLength) + { + errorMessage = $"String must be at least {_minLength.Value} characters long."; + return false; + } + + if (_maxLength.HasValue && value.Length > _maxLength) + { + errorMessage = $"String must be no more than {_maxLength.Value} characters long."; + return false; + } + + // There was no error - so no need for an error message. + errorMessage = string.Empty; + return true; + } + } +} + +#endif \ No newline at end of file diff --git a/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/LengthValidationAttribute.cs.meta b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/LengthValidationAttribute.cs.meta new file mode 100644 index 000000000..10e2bc75c --- /dev/null +++ b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/LengthValidationAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 342ad1355b550334cb3ae439a3040bea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.playeveryware.eos/Runtime/Core/Config/Attributes/NonEmptyStringFieldValidatorAttribute.cs b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/NonEmptyStringFieldValidatorAttribute.cs similarity index 100% rename from com.playeveryware.eos/Runtime/Core/Config/Attributes/NonEmptyStringFieldValidatorAttribute.cs rename to com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/NonEmptyStringFieldValidatorAttribute.cs diff --git a/com.playeveryware.eos/Runtime/Core/Config/Attributes/NonEmptyStringFieldValidatorAttribute.cs.meta b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/NonEmptyStringFieldValidatorAttribute.cs.meta similarity index 83% rename from com.playeveryware.eos/Runtime/Core/Config/Attributes/NonEmptyStringFieldValidatorAttribute.cs.meta rename to com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/NonEmptyStringFieldValidatorAttribute.cs.meta index 7e07cf0cd..8e750c510 100644 --- a/com.playeveryware.eos/Runtime/Core/Config/Attributes/NonEmptyStringFieldValidatorAttribute.cs.meta +++ b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/NonEmptyStringFieldValidatorAttribute.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: c6a317d0691bd974db94adbe0610a397 +guid: 7466b68bc02900741a20a7989003367b MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/README.md b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/README.md new file mode 100644 index 000000000..edc60b02c --- /dev/null +++ b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/README.md @@ -0,0 +1,11 @@ +# Validator Attributes Usage & Application - HON + +This directory contains a variety of attributes that can be applied to classes that derive from `Config`. The attributes are mostly implemented to make effective unit testing of a now-obsolete config class `EOSConfig`, but there are a few additional things to be cognizant of: + +The following attributes were implemented to affect the user experience surrounding the entry of configuration values: + +- `StringValidationAttribute` +- `RegexValidationAttribute` +- `LengthValidationAttribute` + +The attributes above were applied to the `productVersion` field member of the `ProductConfig` class, and enable the user to see immediately when they add an invalid value to the product version field. These attributes and the system can (and likely should) be expanded and be applied to other field members of `ProductConfig`, and the various `PlatformConfig` implementing classes - so as to provide a similar user experience in other fields. \ No newline at end of file diff --git a/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/README.md.meta b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/README.md.meta new file mode 100644 index 000000000..49c6cd0f3 --- /dev/null +++ b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6517396a6d333734297706e2246f03f4 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/RegexValidationAttribute.cs b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/RegexValidationAttribute.cs new file mode 100644 index 000000000..512120b4e --- /dev/null +++ b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/RegexValidationAttribute.cs @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2025 PlayEveryWare + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#if !EOS_DISABLE + +namespace PlayEveryWare.EpicOnlineServices +{ + using System; + using System.Text.RegularExpressions; + + /// + /// ValidationAttribute used to make sure a string field matches a given + /// regex. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] + public class RegexValidationAttribute : StringValidationAttribute + { + /// + /// The pattern that the value must match to pass. + /// + private readonly string _validRegexPattern; + + /// + /// The error message to return if the regex pattern match fails. + /// + private readonly string _customErrorMessage; + + /// + /// Default value of the error message is set to describe the regex. + /// + /// The pattern to match. + public RegexValidationAttribute(string validRegexPattern) : + this(validRegexPattern, $"Value must satisfy the following regex: {validRegexPattern}") + { } + + /// + /// Determine the parameters for validating a string value with a given + /// regex pattern. + /// + /// + /// The pattern to check the value against for validation. + /// + /// + /// The error message should explain in plain language the meaning of + /// the regex pattern being utilized for validation. + /// + public RegexValidationAttribute(string validRegexPattern, string customErrorMessage) + { + _validRegexPattern = validRegexPattern; + _customErrorMessage = customErrorMessage; + } + + public override bool ValidateStringField(string value, out string errorMessage) + { + // Determine whether the value matches the regex pattern. + if (!Regex.IsMatch(value, _validRegexPattern)) + { + errorMessage = _customErrorMessage; + return false; + } + + // There was no error - so no need for an error message. + errorMessage = string.Empty; + return true; + } + } +} + +#endif \ No newline at end of file diff --git a/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/RegexValidationAttribute.cs.meta b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/RegexValidationAttribute.cs.meta new file mode 100644 index 000000000..8b9063140 --- /dev/null +++ b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/RegexValidationAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d1dc0247cd1285341b2fb9e7ad3c09f5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.playeveryware.eos/Runtime/Core/Config/Attributes/SandboxIDFieldValidatorAttribute.cs b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/SandboxIDFieldValidatorAttribute.cs similarity index 100% rename from com.playeveryware.eos/Runtime/Core/Config/Attributes/SandboxIDFieldValidatorAttribute.cs rename to com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/SandboxIDFieldValidatorAttribute.cs diff --git a/com.playeveryware.eos/Runtime/Core/Config/Attributes/SandboxIDFieldValidatorAttribute.cs.meta b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/SandboxIDFieldValidatorAttribute.cs.meta similarity index 83% rename from com.playeveryware.eos/Runtime/Core/Config/Attributes/SandboxIDFieldValidatorAttribute.cs.meta rename to com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/SandboxIDFieldValidatorAttribute.cs.meta index 18acb95e9..d52d5c197 100644 --- a/com.playeveryware.eos/Runtime/Core/Config/Attributes/SandboxIDFieldValidatorAttribute.cs.meta +++ b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/SandboxIDFieldValidatorAttribute.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 2a321a87822af3449974f3b09d869bfb +guid: 1f28de13f7e7273489a24356543c9e0a MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/StringValidationAttribute.cs b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/StringValidationAttribute.cs new file mode 100644 index 000000000..2a7372c1c --- /dev/null +++ b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/StringValidationAttribute.cs @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025 PlayEveryWare + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace PlayEveryWare.EpicOnlineServices +{ + using System; + + /// + /// Used to describe a validation attribute that operates on a string data + /// type. + /// + [AttributeUsage(AttributeTargets.Field)] + public abstract class StringValidationAttribute : FieldValidatorAttribute + { + public override sealed bool FieldValueIsValid( + object value, + out string errorMessage) + { + // Check to make sure the value is actually a string. + if (value is string strValue) + { + return ValidateStringField(strValue, out errorMessage); + } + + // If this point is reached, it is because the attribute was + // improperly applied to a field member that is not of type string, + // therefore an invalid argument exception should be thrown. + throw new ArgumentException( + $"{nameof(StringValidationAttribute)} cannot be " + + $"applied to a field member whose type is not string."); + } + + /// + /// Implement this function in deriving attributes to accomplish the + /// validation of the string value. + /// + /// + /// The string value to be validated. + /// + /// + /// The error message to use if validation fails. + /// + /// + /// True if the string value is valid, false otherwise. + /// + public abstract bool ValidateStringField( + string toValidate, + out string errorMessage); + } +} \ No newline at end of file diff --git a/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/StringValidationAttribute.cs.meta b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/StringValidationAttribute.cs.meta new file mode 100644 index 000000000..dcfab4951 --- /dev/null +++ b/com.playeveryware.eos/Runtime/Core/Config/Attributes/Validators/StringValidationAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fe1862d5892d54845a4752136a43ceee +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.playeveryware.eos/Runtime/Core/Config/ProductConfig.cs b/com.playeveryware.eos/Runtime/Core/Config/ProductConfig.cs index a6042ea96..95b2ead1d 100644 --- a/com.playeveryware.eos/Runtime/Core/Config/ProductConfig.cs +++ b/com.playeveryware.eos/Runtime/Core/Config/ProductConfig.cs @@ -40,6 +40,13 @@ namespace PlayEveryWare.EpicOnlineServices [ConfigGroup("Product Configuration", new[] { "", "Deployment Configuration" }, false)] public class ProductConfig : Config { + /// + /// This is the maximum allowed length for the product version field + /// according to the EOS SDK documentation. The name of this field was + /// selected to mirror the one that exists within the EOS SDK itself. + /// + private const int EOS_INITIALIZEOPTIONS_PRODUCTVERSION_MAX_LENGTH = 64; + /// /// The product ID is a unique GUID labeled "Product ID" in the Epic /// Developer Portal. The name for this value can be set to anything - @@ -63,7 +70,9 @@ public class ProductConfig : Config [ConfigField("Version", ConfigFieldType.Text, "Use this to indicate to the EOS SDK your game version.", - 0)] + 0, "https://dev.epicgames.com/docs/api-ref/structs/eos-initialize-options")] + [RegexValidation("^[A-Za-z0-9._ !?()+:-]+$", "Product version must consist of only the following characters: A-Z, a-z, 0-9, dot, underscore, space, exclamation mark, question mark, sign, hyphen, parenthesis, plus, minus, or colon characters.")] + [LengthValidation(1, EOS_INITIALIZEOPTIONS_PRODUCTVERSION_MAX_LENGTH)] public string ProductVersion; ///