# Validation Framework Documentation ## Overview The AlinasMapMod validation framework provides a standardized approach to validating spliney builder components and serialized objects. This framework uses a field-based validation system with fluent API extensions and comprehensive error reporting. ## Core Architecture ### ValidationBuilder (Field-Based Validation) The validation system is built around `ValidationBuilder` which requires a field name for context: ```csharp // Create a validation builder for a specific field var builder = new ValidationBuilder("PrefabUri"); // Add validation rules using fluent API builder.AsUri() .AsVanillaPrefab(allowedPrefabs); // Validate a value var result = builder.Validate("vanilla://locomotive_steam_small"); ``` ### ValidationResult System The framework provides detailed validation feedback with manual result creation: ```csharp public class ValidationResult { public bool IsValid { get; set; } = true; public List Errors { get; set; } = new List(); public List Warnings { get; set; } = new List(); public void ThrowIfInvalid(); // Throws ValidationException if invalid // Combines multiple validation results with null handling public static ValidationResult Combine(params ValidationResult[] results); } ``` ### ValidationError and ValidationWarning Detailed error information with context: ```csharp public class ValidationError { public string Field { get; set; } // Field name being validated public string Message { get; set; } // Human-readable error message public string Code { get; set; } // Machine-readable error code public object Value { get; set; } // The invalid value } public class ValidationWarning { public string Field { get; set; } public string Message { get; set; } public object Value { get; set; } } ``` ## Validation Rules ### Core Validation Rules - **RequiredRule**: Validates non-null/non-empty values with whitespace control - **WhitelistRule**: Validates values against allowed lists (includes null validation) - **MinValueRule**: Validates minimum values for comparable types - **EnumValidationRule**: Validates enum values are defined - **CustomRule**: Allows custom validation logic ### URI Validation Rules #### Basic URI Rules - **UriFormatRule**: Validates URI format (contains "://") - **UriSchemeValidationRule**: Validates supported schemes (case-insensitive: path, scenery, vanilla, empty) #### Scheme-Specific Rules (Restrictive) - **VanillaPrefabRule**: Only allows `vanilla://` URIs, validates prefab names with path extraction - **PathUriRule**: Only allows `path://` URIs, validates scene path format - **SceneryUriRule**: Only allows `scenery://` URIs, rejects identifiers with paths - **EmptyUriRule**: Only allows `empty://` URIs or null/empty values #### Comprehensive Rules (Permissive) - **GameObjectUriRule**: Allows multiple URI schemes, validates each according to its rules ### Cache Validation Rules - **CacheValidationRule**: Validates cache key existence with null handling and exception safety - **CacheTypeValidationRule**: Validates cached object types with comprehensive error handling ## Builder Standardization ### SplineyBuilderBase Class All builders inherit from `SplineyBuilderBase` which provides standardized patterns for logging, error handling, validation, and object creation: ```csharp public abstract class SplineyBuilderBase : StrangeCustoms.ISplineyBuilder { protected static readonly Serilog.ILogger Logger = Log.ForContext(); // Main build method with standardized error handling public GameObject BuildSpliney(string id, Transform parentTransform, JObject data) { return SafeBuildSpliney(id, parentTransform, data, () => BuildSplineyInternal(id, parentTransform, data)); } // To be implemented by derived classes protected abstract GameObject BuildSplineyInternal(string id, Transform parentTransform, JObject data); } ``` ### Core Builder Features #### Input Validation ```csharp protected virtual void ValidateInput(string id, JObject data) { if (string.IsNullOrEmpty(id)) throw new ValidationException("ID cannot be null or empty"); if (data == null) throw new ValidationException("Data cannot be null"); } ``` #### GameObject Creation ```csharp protected virtual GameObject CreateGameObject(string id, Transform parentTransform, string objectTypeName = null) { var gameObject = new GameObject(id); if (parentTransform != null) { gameObject.transform.SetParent(parentTransform); } else { var parent = Utils.GetParent(objectTypeName ?? "Unknown"); if (parent != null) gameObject.transform.SetParent(parent.transform); } return gameObject; } ``` #### Standardized Error Handling Safe execution with structured logging and error handling: ```csharp protected GameObject SafeBuildSpliney(string id, Transform parentTransform, JObject data, Func buildAction) { try { ValidateInput(id, data); Logger.Information("Building {BuilderType} with ID {Id}", GetType().Name, id); var result = buildAction(); Logger.Information("Successfully built {BuilderType} with ID {Id}", GetType().Name, id); return result; } catch (ValidationException ex) { Logger.Error(ex, "Validation failed for {BuilderType} {Id}", GetType().Name, id); throw new ValidationException($"Validation failed for {GetType().Name} {id}: {ex.Message}", ex); } catch (Exception ex) { Logger.Error(ex, "Failed to create {BuilderType} {Id}", GetType().Name, id); throw new InvalidOperationException($"Failed to create {GetType().Name} {id}", ex); } } ``` #### Transactional Building with Cleanup For builders that need cleanup on failure: ```csharp protected GameObject SafeBuildSplineyWithCleanup(string id, Transform parentTransform, JObject data, Func buildAction) { var transaction = new BuilderTransaction(); try { ValidateInput(id, data); Logger.Information("Building {BuilderType} with ID {Id}", GetType().Name, id); var result = buildAction(transaction); transaction.Commit(); Logger.Information("Successfully built {BuilderType} with ID {Id}", GetType().Name, id); return result; } catch (Exception ex) { transaction.Rollback(); Logger.Error(ex, "Failed to create {BuilderType} {Id}", GetType().Name, id); throw; } } ``` #### Deserialization and Validation Helper method for deserializing and validating data objects: ```csharp protected TData DeserializeAndValidate(JObject data) where TData : IValidatable { var deserializedData = data.ToObject(); if (deserializedData == null) throw new ValidationException($"Failed to deserialize data as {typeof(TData).Name}"); // Use rich validation for detailed error reporting var validationResult = deserializedData.ValidateWithDetails(); if (!validationResult.IsValid) { foreach (var error in validationResult.Errors) { Logger.Error("Validation error in {Field}: {Message}", error.Field, error.Message); } } // Log warnings but continue foreach (var warning in validationResult.Warnings) { Logger.Warning("Validation warning in {Field}: {Message}", warning.Field, warning.Message); } validationResult.ThrowIfInvalid(); return deserializedData; } ``` #### Component Building Pattern For building from serialized components that implement `ICreatableComponent`: ```csharp protected virtual GameObject BuildFromCreatableComponent(string id, JObject data) where TComponent : Component where TSerialized : ICreatableComponent, IValidatable { var serialized = DeserializeAndValidate(data); Logger.Information("Creating {ObjectType} {Id} using serialized component", typeof(TComponent).Name, id); return serialized.Create(id).gameObject; } ``` ## Serialized Component Framework ### SerializedComponentBase Serialized components can inherit from `SerializedComponentBase` for standardized validation functionality: ```csharp public abstract class SerializedComponentBase : ISerializedPatchableComponent, IValidatable where T : Component { private readonly Dictionary> _validators = new Dictionary>(); private bool _validationConfigured = false; // Override to configure validation rules protected virtual void ConfigureValidation() { } // Abstract methods to be implemented by derived classes public abstract T Create(string id); public abstract void Write(T comp); public abstract void Read(T comp); // IValidatable implementation public virtual void Validate() => ValidateWithDetails().ThrowIfInvalid(); public virtual ValidationResult ValidateWithDetails() { /* implementation */ } } ``` ### Property-Based Validation Rules Use the `RuleFor` method to create validation rules for properties: ```csharp protected ValidationBuilder RuleFor(Expression> propertyExpression) { // Creates a ValidationBuilder for the specified property var propertyName = GetPropertyName(propertyExpression); var builder = new ValidationBuilder(propertyName); // Store validator function for later execution _validators[propertyName] = () => { var propertyValue = GetPropertyValue(propertyExpression); var context = new ValidationContext { Owner = this }; return builder.Validate(propertyValue, context); }; return builder; } ``` ### Example Implementation ```csharp public class SerializedLoader : SerializedComponentBase { public string Prefab { get; set; } public int Capacity { get; set; } public Vector3 Position { get; set; } protected override void ConfigureValidation() { RuleFor(() => Prefab) .Required() .AsUri() .AsVanillaPrefab(VanillaPrefabs.AvailableLoaderPrefabs); RuleFor(() => Capacity) .GreaterThan(0); } public override LoaderInstance Create(string id) { // Create and configure the component var gameObject = new GameObject(id); var loader = gameObject.AddComponent(); // Apply configuration loader.SetPrefab(Prefab); loader.SetCapacity(Capacity); gameObject.transform.position = Position; return loader; } public override void Write(LoaderInstance comp) { Prefab = comp.GetPrefab(); Capacity = comp.GetCapacity(); Position = comp.transform.position; } public override void Read(LoaderInstance comp) { comp.SetPrefab(Prefab); comp.SetCapacity(Capacity); comp.transform.position = Position; } } ``` ### Fluent Validation API The framework provides a fluent API through extension methods on `ValidationBuilder`: ```csharp var builder = new ValidationBuilder("PrefabField"); // Basic validation builder.Required() // Value must not be null/empty .AsUri(); // Must contain "://" // URI scheme validation builder.AsUriScheme() // Must be supported scheme .AsVanillaPrefab(allowedPrefabs) // Specific to vanilla:// URIs .AsPathUri() // Specific to path:// URIs .AsSceneryUri() // Specific to scenery:// URIs .AsEmptyUri() // Specific to empty:// URIs .AsGameObjectUri(allowedPrefabs); // Multiple URI forms allowed // Numeric validation builder.GreaterThan(0) // Value > minimum .GreaterThanOrEqual(0); // Value >= minimum // Enum validation builder.AsValidEnum(); // Must be defined enum value // Custom validation builder.Custom((value, context) => { // Custom validation logic var result = new ValidationResult { IsValid = true }; if (/* custom condition */) { result.IsValid = false; result.Errors.Add(new ValidationError { Field = context.FieldName, Message = "Custom validation failed", Code = "CUSTOM_ERROR", Value = value }); } return result; }); ``` ## Complete Builder Implementation Example ### Creating a New Builder Here's how to implement a complete builder following the standardized patterns: ```csharp public class MyCustomBuilder : SplineyBuilderBase, IObjectFactory { public string Name => "My Custom Component"; public bool Enabled => true; public Type ObjectType => typeof(MyCustomComponent); protected override GameObject BuildSplineyInternal(string id, Transform parentTransform, JObject data) { // Use the standardized pattern for building from serialized components return BuildFromCreatableComponent(id, data); } public IEditableObject CreateObject(PatchEditor editor, string id) { // Create a default instance for the editor return new SerializedMyCustomComponent { Prefab = "vanilla://default_prefab", Scale = 1.0f }.Create(id); } } ``` ### Corresponding Serialized Component ```csharp public class SerializedMyCustomComponent : SerializedComponentBase { public string Prefab { get; set; } public float Scale { get; set; } = 1.0f; public bool IsActive { get; set; } = true; protected override void ConfigureValidation() { RuleFor(() => Prefab) .Required() .AsUri() .AsVanillaPrefab(VanillaPrefabs.AvailableComponentPrefabs); RuleFor(() => Scale) .GreaterThan(0.1f); } public override MyCustomComponent Create(string id) { var gameObject = CreateGameObject(id); var component = GetOrAddComponent(gameObject); ConfigureWithActivation(gameObject, () => { component.SetPrefab(Prefab); component.SetScale(Scale); component.SetActive(IsActive); }); return component; } public override void Write(MyCustomComponent comp) { Prefab = comp.GetPrefab(); Scale = comp.GetScale(); IsActive = comp.IsActive(); } public override void Read(MyCustomComponent comp) { comp.SetPrefab(Prefab); comp.SetScale(Scale); comp.SetActive(IsActive); } private GameObject CreateGameObject(string id) { var gameObject = new GameObject(id); return gameObject; } private TComponent GetOrAddComponent(GameObject gameObject) where TComponent : Component { return gameObject.GetComponent() ?? gameObject.AddComponent(); } private void ConfigureWithActivation(GameObject gameObject, System.Action configureAction) { var wasActive = gameObject.activeSelf; gameObject.SetActive(false); try { configureAction(); } finally { gameObject.SetActive(wasActive); } } } ``` ## Migration Guide ### Updating Existing Builders 1. **Inherit from SplineyBuilderBase**: Replace custom base classes with `SplineyBuilderBase` 2. **Use BuildFromCreatableComponent**: Replace manual deserialization with the standardized pattern 3. **Implement IObjectFactory**: Add `CreateObject` method for editor integration 4. **Remove custom logging**: Use the inherited `Logger` instance **Before:** ```csharp public class OldBuilder : ISplineyBuilder { public GameObject BuildSpliney(string id, Transform parent, JObject data) { try { var serialized = data.ToObject(); serialized.Validate(); // Simple validation return serialized.Create(id).gameObject; } catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); throw; } } } ``` **After:** ```csharp public class NewBuilder : SplineyBuilderBase, IObjectFactory { public string Name => "My Component"; public bool Enabled => true; public Type ObjectType => typeof(MyComponent); protected override GameObject BuildSplineyInternal(string id, Transform parentTransform, JObject data) { return BuildFromCreatableComponent(id, data); } public IEditableObject CreateObject(PatchEditor editor, string id) => new SerializedComponent().Create(id); } ``` ### Updating Serialized Components 1. **Inherit from SerializedComponentBase**: Replace custom validation with the standardized framework 2. **Override ConfigureValidation**: Use `RuleFor` to set up validation rules 3. **Use ValidationBuilder**: Replace manual validation with fluent API **Before:** ```csharp public class OldSerializedComponent : IValidatable { public string Prefab { get; set; } public void Validate() { if (string.IsNullOrEmpty(Prefab)) throw new ValidationException("Prefab is required"); } public ValidationResult ValidateWithDetails() { // Manual validation result creation } } ``` **After:** ```csharp public class NewSerializedComponent : SerializedComponentBase { public string Prefab { get; set; } protected override void ConfigureValidation() { RuleFor(() => Prefab) .Required() .AsUri() .AsVanillaPrefab(allowedPrefabs); } // Implement Create, Write, Read methods } ``` ## Builder Helper Methods The `SplineyBuilderBase` class provides additional helper methods for common builder tasks: ### Finding Existing Components ```csharp // Find existing components by ID using caches protected virtual TComponent FindExistingComponent(string id) where TComponent : Component { // Uses type-specific caches for performance if (typeof(TComponent) == typeof(PaxStationAgent)) { StationAgentCache.Instance.TryGetValue(id, out var stationAgent); return stationAgent as TComponent; } // Fallback to GameObject.FindObjectsOfType return GameObject.FindObjectsOfType(true) .FirstOrDefault(c => GetComponentId(c) == id); } ``` ### Component ID Resolution ```csharp // Gets component ID using common patterns protected virtual string GetComponentId(Component component) { // Try common identifier patterns var identifiableProperty = component.GetType().GetProperty("identifier"); if (identifiableProperty != null) return identifiableProperty.GetValue(component)?.ToString(); var idProperty = component.GetType().GetProperty("Id"); if (idProperty != null) return idProperty.GetValue(component)?.ToString(); return component.name; } ``` ### Safe Component Configuration ```csharp // Safely configure GameObject by disabling during configuration protected virtual void ConfigureWithActivation(GameObject gameObject, System.Action configureAction) { var wasActive = gameObject.activeSelf; gameObject.SetActive(false); try { configureAction(); } finally { gameObject.SetActive(wasActive); } } ``` ### Validation Patterns #### Field-Based Validation ```csharp // Validate a URI field var uriBuilder = new ValidationBuilder("PrefabUri"); uriBuilder.Required() .AsUri() .AsVanillaPrefab(VanillaPrefabs.AvailableLoaderPrefabs); var uriResult = uriBuilder.Validate("vanilla://locomotive_steam_small"); if (!uriResult.IsValid) { // Handle validation errors foreach (var error in uriResult.Errors) { Logger.Error("Validation error in {Field}: {Message}", error.Field, error.Message); } } // Validate numeric values var numberBuilder = new ValidationBuilder("Subdivisions"); numberBuilder.GreaterThan(2); var numberResult = numberBuilder.Validate(subdivisions); ``` #### Creating Validation Rules ```csharp // Create a custom validation rule public class PositiveNumberRule : ValidationRule { public override ValidationResult Validate(int value, ValidationContext context) { var result = new ValidationResult { IsValid = true }; if (value <= 0) { result.IsValid = false; result.Errors.Add(new ValidationError { Field = context.FieldName, Message = $"Value must be positive, got {value}", Code = "MUST_BE_POSITIVE", Value = value }); } return result; } } // Use the custom rule var builder = new ValidationBuilder("MyNumber"); builder.AddRule(new PositiveNumberRule()); ``` #### Working with ValidationContext ```csharp var context = new ValidationContext { FieldName = "PrefabField" }; var rule = new RequiredRule(); var result = rule.Validate(value, context); // The context provides field information for error messages ``` ## URI Validation Architecture ### Two Types of URI Rules The validation framework supports two distinct approaches to URI validation: #### Restrictive Rules (Single Scheme) These rules only accept their specific URI scheme and reject all others: - **VanillaPrefabRule**: Only accepts `vanilla://` URIs - **PathUriRule**: Only accepts `path://` URIs - **SceneryUriRule**: Only accepts `scenery://` URIs - **EmptyUriRule**: Only accepts `empty://` URIs ```csharp // This will ONLY accept vanilla:// URIs var vanillaBuilder = new ValidationBuilder("VanillaPrefab"); vanillaBuilder.AsVanillaPrefab(allowedPrefabs); // VALID: "vanilla://locomotive_steam_small" // INVALID: "path://scene/SomeObject" ``` #### Permissive Rules (Multiple Schemes) These rules accept multiple URI schemes and validate each according to its rules: - **GameObjectUriRule**: Accepts path://, scenery://, vanilla://, empty:// schemes ```csharp // This will accept multiple URI forms var gameObjectBuilder = new ValidationBuilder("GameObjectUri"); gameObjectBuilder.AsGameObjectUri(allowedVanillaPrefabs); // VALID: "vanilla://locomotive_steam_small" // VALID: "path://scene/SomeObject" // VALID: "scenery://tree_oak" // VALID: "empty://" ``` ### URI Parsing Details #### Vanilla Prefab Extraction The `VanillaPrefabRule` extracts prefab names from URIs with paths: ```csharp // Input: "vanilla://Locomotive/part" // Extracted prefab: "Locomotive" // Validation: Check if "Locomotive" is in allowed prefabs list ``` #### Path URI Format Path URIs must follow the format `path://scene/GameObject1/GameObject2/...`: ```csharp // VALID: "path://scene/SomeGameObject" // VALID: "path://scene/Parent/Child" // INVALID: "path://notscene/SomeObject" ``` #### Scenery URI Format Scenery URIs must be simple identifiers without paths: ```csharp // VALID: "scenery://tree_oak" // INVALID: "scenery://category/tree_oak" // Contains path separator ``` ### Case Sensitivity URI scheme validation is case-insensitive: ```csharp // All of these are equivalent: // "PATH://scene/Object" // "path://scene/Object" // "Path://scene/Object" ``` ## Common Error Codes The validation framework uses standardized error codes for consistent error handling: ### Core Validation Errors - `REQUIRED`: Value is required but null/empty - `REQUIRED_NOT_WHITESPACE`: Value cannot be whitespace-only - `INVALID_VALUE`: Value not in allowed whitelist - `MIN_VALUE_VIOLATION`: Value below minimum threshold - `INVALID_ENUM_VALUE`: Enum value not defined ### URI Validation Errors - `INVALID_URI_FORMAT`: URI missing "://" separator - `INVALID_URI_SCHEME`: Unsupported URI scheme - `NON_VANILLA_URI_NOT_ALLOWED`: Non-vanilla URI in vanilla-only context - `NON_PATH_URI_NOT_ALLOWED`: Non-path URI in path-only context - `NON_SCENERY_URI_NOT_ALLOWED`: Non-scenery URI in scenery-only context - `NON_EMPTY_URI_NOT_ALLOWED`: Non-empty URI in empty-only context - `INVALID_VANILLA_PREFAB`: Vanilla prefab not in allowed list - `INVALID_PATH_URI_FORMAT`: Path URI format incorrect - `MISSING_PATH_COMPONENTS`: Path URI missing GameObject path - `MISSING_SCENERY_IDENTIFIER`: Scenery URI missing identifier - `SCENERY_IDENTIFIER_CONTAINS_PATH`: Scenery identifier contains path separators - `VALUE_MUST_BE_EMPTY`: Value must be empty or null ### Cache Validation Errors - `CACHE_KEY_NULL_OR_EMPTY`: Cache key cannot be null or empty - `CACHE_NOT_FOUND`: Cache key not found - `CACHE_ACCESS_ERROR`: Error accessing cache - `CACHE_OBJECT_NULL`: Cached object is null - `INVALID_CACHE_OBJECT_TYPE`: Cached object has wrong type ## Benefits 1. **Field-Based Context**: Validation tied to specific fields provides clear error context 2. **Comprehensive Error Reporting**: Detailed error messages with codes, fields, and values 3. **Flexible Architecture**: Support for both restrictive and permissive validation patterns 4. **URI Scheme Awareness**: Intelligent handling of different URI schemes and formats 5. **Exception Safety**: Proper null handling and exception management 6. **Extensibility**: Easy to add new validation rules through inheritance 7. **Performance**: Efficient validation with early returns and minimal overhead 8. **Testability**: Comprehensive test coverage ensures reliability ## Migration Notes When updating validation code: 1. **ValidationBuilder Constructor**: Always provide a field name 2. **Manual ValidationResult Creation**: No static `Valid`/`Invalid` properties 3. **URI Rule Selection**: Choose restrictive vs permissive rules based on requirements 4. **Null Handling**: Validation rules now properly handle null values 5. **Error Codes**: Use standardized error codes for consistent error handling ## Best Practices 1. **Use Descriptive Field Names**: `"PrefabUri"` instead of `"field1"` 2. **Chain Validation Rules**: Use fluent API for multiple validation rules 3. **Handle ValidationResult**: Always check `IsValid` before proceeding 4. **Log Validation Errors**: Use structured logging with error context 5. **Choose Appropriate URI Rules**: Restrictive for specific schemes, permissive for flexibility 6. **Test Validation Logic**: Write tests for both valid and invalid cases