161 lines
6.2 KiB
C#
161 lines
6.2 KiB
C#
using System.Text.Json;
|
|
using ScadaLink.Commons.Types.InboundApi;
|
|
|
|
namespace ScadaLink.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>
|
|
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
|
|
{
|
|
public bool IsValid { get; private init; }
|
|
public string? ErrorMessage { get; private init; }
|
|
public IReadOnlyDictionary<string, object?> Parameters { get; private init; } = new Dictionary<string, object?>();
|
|
|
|
public static ParameterValidationResult Valid(Dictionary<string, object?> parameters) =>
|
|
new() { IsValid = true, Parameters = parameters };
|
|
|
|
public static ParameterValidationResult Invalid(string message) =>
|
|
new() { IsValid = false, ErrorMessage = message };
|
|
}
|