using System.Text.Json; using ScadaLink.Commons.Types.InboundApi; namespace ScadaLink.InboundAPI; /// /// WP-2: Validates and deserializes JSON request body against method parameter definitions. /// Extended type system: Boolean, Integer, Float, String, Object, List. /// public static class ParameterValidator { /// /// Validates the request body against the method's parameter definitions. /// Returns deserialized parameters or an error message. /// 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()); } List definitions; try { definitions = JsonSerializer.Deserialize>( 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()); } 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()); } if (body.Value.ValueKind != JsonValueKind.Object) { return ParameterValidationResult.Invalid("Request body must be a JSON object"); } var result = new Dictionary(); var errors = new List(); // 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(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); } /// /// Coerces a JSON element to the declared parameter type. InboundAPI-010: the /// Object and List extended types are validated for JSON shape /// 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. /// 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>(element.GetRawText()), null) : (null, $"Parameter '{paramName}' must be an Object"), "list" => element.ValueKind == JsonValueKind.Array ? (JsonSerializer.Deserialize>(element.GetRawText()), null) : (null, $"Parameter '{paramName}' must be a List"), _ => (null, $"Unknown parameter type '{expectedType}' for parameter '{paramName}'") }; } } /// /// Result of parameter validation. /// public class ParameterValidationResult { public bool IsValid { get; private init; } public string? ErrorMessage { get; private init; } public IReadOnlyDictionary Parameters { get; private init; } = new Dictionary(); public static ParameterValidationResult Valid(Dictionary parameters) => new() { IsValid = true, Parameters = parameters }; public static ParameterValidationResult Invalid(string message) => new() { IsValid = false, ErrorMessage = message }; }