refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
using System.Text.Json;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.InboundApi;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
|
||||
|
||||
/// <summary>
|
||||
/// WP-2: Validates and deserializes JSON request body against method parameter definitions.
|
||||
/// Extended type system: Boolean, Integer, Float, String, Object, List.
|
||||
/// </summary>
|
||||
public static class ParameterValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates the request body against the method's parameter definitions.
|
||||
/// Returns deserialized parameters or an error message.
|
||||
/// </summary>
|
||||
/// <param name="body">The parsed JSON request body; null or undefined if no body was supplied.</param>
|
||||
/// <param name="parameterDefinitions">JSON-serialized list of <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.InboundApi.ParameterDefinition"/>; null or empty means no parameters are defined.</param>
|
||||
public static ParameterValidationResult Validate(
|
||||
JsonElement? body,
|
||||
string? parameterDefinitions)
|
||||
{
|
||||
if (string.IsNullOrEmpty(parameterDefinitions))
|
||||
{
|
||||
// No parameters defined — body should be empty or null
|
||||
return ParameterValidationResult.Valid(new Dictionary<string, object?>());
|
||||
}
|
||||
|
||||
List<ParameterDefinition> definitions;
|
||||
try
|
||||
{
|
||||
definitions = JsonSerializer.Deserialize<List<ParameterDefinition>>(
|
||||
parameterDefinitions,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
|
||||
?? [];
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return ParameterValidationResult.Invalid("Invalid parameter definitions in method configuration");
|
||||
}
|
||||
|
||||
if (definitions.Count == 0)
|
||||
{
|
||||
return ParameterValidationResult.Valid(new Dictionary<string, object?>());
|
||||
}
|
||||
|
||||
if (body == null || body.Value.ValueKind == JsonValueKind.Null || body.Value.ValueKind == JsonValueKind.Undefined)
|
||||
{
|
||||
// Check if all parameters are optional
|
||||
var required = definitions.Where(d => d.Required).ToList();
|
||||
if (required.Count > 0)
|
||||
{
|
||||
return ParameterValidationResult.Invalid(
|
||||
$"Missing required parameters: {string.Join(", ", required.Select(r => r.Name))}");
|
||||
}
|
||||
|
||||
return ParameterValidationResult.Valid(new Dictionary<string, object?>());
|
||||
}
|
||||
|
||||
if (body.Value.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return ParameterValidationResult.Invalid("Request body must be a JSON object");
|
||||
}
|
||||
|
||||
var result = new Dictionary<string, object?>();
|
||||
var errors = new List<string>();
|
||||
|
||||
// InboundAPI-010: report top-level body fields that do not match any defined
|
||||
// parameter, so a caller learns about a typo'd parameter name instead of
|
||||
// having the field silently ignored.
|
||||
var defined = new HashSet<string>(definitions.Select(d => d.Name), StringComparer.Ordinal);
|
||||
var unexpected = body.Value.EnumerateObject()
|
||||
.Select(p => p.Name)
|
||||
.Where(name => !defined.Contains(name))
|
||||
.ToList();
|
||||
if (unexpected.Count > 0)
|
||||
{
|
||||
errors.Add($"Unexpected parameter(s): {string.Join(", ", unexpected)}");
|
||||
}
|
||||
|
||||
foreach (var def in definitions)
|
||||
{
|
||||
if (body.Value.TryGetProperty(def.Name, out var prop))
|
||||
{
|
||||
var (value, error) = CoerceValue(prop, def.Type, def.Name);
|
||||
if (error != null)
|
||||
{
|
||||
errors.Add(error);
|
||||
}
|
||||
else
|
||||
{
|
||||
result[def.Name] = value;
|
||||
}
|
||||
}
|
||||
else if (def.Required)
|
||||
{
|
||||
errors.Add($"Missing required parameter: {def.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
return ParameterValidationResult.Invalid(string.Join("; ", errors));
|
||||
}
|
||||
|
||||
return ParameterValidationResult.Valid(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Coerces a JSON element to the declared parameter type. InboundAPI-010: the
|
||||
/// <c>Object</c> and <c>List</c> extended types are validated for JSON <em>shape</em>
|
||||
/// only (object vs. array) — there is no field-level or element-level type
|
||||
/// validation. A method script that needs a specific nested structure must
|
||||
/// validate it itself; invalid nested data surfaces as a runtime script error.
|
||||
/// </summary>
|
||||
private static (object? value, string? error) CoerceValue(JsonElement element, string expectedType, string paramName)
|
||||
{
|
||||
return expectedType.ToLowerInvariant() switch
|
||||
{
|
||||
"boolean" => element.ValueKind == JsonValueKind.True || element.ValueKind == JsonValueKind.False
|
||||
? (element.GetBoolean(), null)
|
||||
: (null, $"Parameter '{paramName}' must be a Boolean"),
|
||||
|
||||
"integer" => element.ValueKind == JsonValueKind.Number && element.TryGetInt64(out var intVal)
|
||||
? (intVal, null)
|
||||
: (null, $"Parameter '{paramName}' must be an Integer"),
|
||||
|
||||
"float" => element.ValueKind == JsonValueKind.Number
|
||||
? (element.GetDouble(), null)
|
||||
: (null, $"Parameter '{paramName}' must be a Float"),
|
||||
|
||||
"string" => element.ValueKind == JsonValueKind.String
|
||||
? (element.GetString(), null)
|
||||
: (null, $"Parameter '{paramName}' must be a String"),
|
||||
|
||||
"object" => element.ValueKind == JsonValueKind.Object
|
||||
? (JsonSerializer.Deserialize<Dictionary<string, object?>>(element.GetRawText()), null)
|
||||
: (null, $"Parameter '{paramName}' must be an Object"),
|
||||
|
||||
"list" => element.ValueKind == JsonValueKind.Array
|
||||
? (JsonSerializer.Deserialize<List<object?>>(element.GetRawText()), null)
|
||||
: (null, $"Parameter '{paramName}' must be a List"),
|
||||
|
||||
_ => (null, $"Unknown parameter type '{expectedType}' for parameter '{paramName}'")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of parameter validation.
|
||||
/// </summary>
|
||||
public class ParameterValidationResult
|
||||
{
|
||||
/// <summary>Gets a value indicating whether validation succeeded.</summary>
|
||||
public bool IsValid { get; private init; }
|
||||
/// <summary>Gets the error message when <see cref="IsValid"/> is false; null on success.</summary>
|
||||
public string? ErrorMessage { get; private init; }
|
||||
/// <summary>Gets the validated and type-coerced parameter values keyed by parameter name.</summary>
|
||||
public IReadOnlyDictionary<string, object?> Parameters { get; private init; } = new Dictionary<string, object?>();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a successful validation result with the given parameters.
|
||||
/// </summary>
|
||||
/// <param name="parameters">The validated and coerced parameter values.</param>
|
||||
public static ParameterValidationResult Valid(Dictionary<string, object?> parameters) =>
|
||||
new() { IsValid = true, Parameters = parameters };
|
||||
|
||||
/// <summary>
|
||||
/// Creates a failed validation result with the given error message.
|
||||
/// </summary>
|
||||
/// <param name="message">Description of the validation failure.</param>
|
||||
public static ParameterValidationResult Invalid(string message) =>
|
||||
new() { IsValid = false, ErrorMessage = message };
|
||||
}
|
||||
Reference in New Issue
Block a user