861 lines
27 KiB
Markdown
861 lines
27 KiB
Markdown
# 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<T> (Field-Based Validation)
|
|
|
|
The validation system is built around `ValidationBuilder<T>` which requires a field name for context:
|
|
|
|
```csharp
|
|
// Create a validation builder for a specific field
|
|
var builder = new ValidationBuilder<string>("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<ValidationError> Errors { get; set; } = new List<ValidationError>();
|
|
public List<ValidationWarning> Warnings { get; set; } = new List<ValidationWarning>();
|
|
|
|
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<T>**: Validates non-null/non-empty values with whitespace control
|
|
- **WhitelistRule<T>**: Validates values against allowed lists (includes null validation)
|
|
- **MinValueRule<T>**: Validates minimum values for comparable types
|
|
- **EnumValidationRule<TEnum>**: Validates enum values are defined
|
|
- **CustomRule<T>**: 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<T>**: Validates cache key existence with null handling and exception safety
|
|
- **CacheTypeValidationRule<TExpected>**: 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<SplineyBuilderBase>();
|
|
|
|
// 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<GameObject> 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<IBuilderTransaction, GameObject> 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<TData>(JObject data) where TData : IValidatable
|
|
{
|
|
var deserializedData = data.ToObject<TData>();
|
|
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<TComponent, TSerialized>(string id, JObject data)
|
|
where TComponent : Component
|
|
where TSerialized : ICreatableComponent<TComponent>, IValidatable
|
|
{
|
|
var serialized = DeserializeAndValidate<TSerialized>(data);
|
|
Logger.Information("Creating {ObjectType} {Id} using serialized component", typeof(TComponent).Name, id);
|
|
return serialized.Create(id).gameObject;
|
|
}
|
|
```
|
|
|
|
## Serialized Component Framework
|
|
|
|
### SerializedComponentBase<T>
|
|
|
|
Serialized components can inherit from `SerializedComponentBase<T>` for standardized validation functionality:
|
|
|
|
```csharp
|
|
public abstract class SerializedComponentBase<T> : ISerializedPatchableComponent<T>, IValidatable
|
|
where T : Component
|
|
{
|
|
private readonly Dictionary<string, Func<ValidationResult>> _validators = new Dictionary<string, Func<ValidationResult>>();
|
|
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<TValue> RuleFor<TValue>(Expression<Func<TValue>> propertyExpression)
|
|
{
|
|
// Creates a ValidationBuilder for the specified property
|
|
var propertyName = GetPropertyName(propertyExpression);
|
|
var builder = new ValidationBuilder<TValue>(propertyName);
|
|
|
|
// Store validator function for later execution
|
|
_validators[propertyName] = () => {
|
|
var propertyValue = GetPropertyValue<TValue>(propertyExpression);
|
|
var context = new ValidationContext { Owner = this };
|
|
return builder.Validate(propertyValue, context);
|
|
};
|
|
|
|
return builder;
|
|
}
|
|
```
|
|
|
|
### Example Implementation
|
|
|
|
```csharp
|
|
public class SerializedLoader : SerializedComponentBase<LoaderInstance>
|
|
{
|
|
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<LoaderInstance>();
|
|
|
|
// 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<T>`:
|
|
|
|
```csharp
|
|
var builder = new ValidationBuilder<string>("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<MyEnum>(); // 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<MyCustomComponent, SerializedMyCustomComponent>(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<MyCustomComponent>
|
|
{
|
|
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<MyCustomComponent>(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<TComponent>(GameObject gameObject) where TComponent : Component
|
|
{
|
|
return gameObject.GetComponent<TComponent>() ?? gameObject.AddComponent<TComponent>();
|
|
}
|
|
|
|
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<SerializedComponent>();
|
|
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<MyComponent, SerializedComponent>(id, data);
|
|
}
|
|
|
|
public IEditableObject CreateObject(PatchEditor editor, string id) =>
|
|
new SerializedComponent().Create(id);
|
|
}
|
|
```
|
|
|
|
### Updating Serialized Components
|
|
|
|
1. **Inherit from SerializedComponentBase<T>**: 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<MyComponent>
|
|
{
|
|
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<TComponent>(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<TComponent>(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<string>("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<int>("Subdivisions");
|
|
numberBuilder.GreaterThan(2);
|
|
|
|
var numberResult = numberBuilder.Validate(subdivisions);
|
|
```
|
|
|
|
#### Creating Validation Rules
|
|
|
|
```csharp
|
|
// Create a custom validation rule
|
|
public class PositiveNumberRule : ValidationRule<int>
|
|
{
|
|
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<int>("MyNumber");
|
|
builder.AddRule(new PositiveNumberRule());
|
|
```
|
|
|
|
#### Working with ValidationContext
|
|
|
|
```csharp
|
|
var context = new ValidationContext { FieldName = "PrefabField" };
|
|
var rule = new RequiredRule<string>();
|
|
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<string>("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<string>("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 |