using System.Text.Json; using ZB.MOM.WW.ScadaBridge.Commons.Types.InboundApi; namespace ZB.MOM.WW.ScadaBridge.InboundAPI; /// /// WP-2: Validates and deserializes a JSON request body against a method's /// parameter definitions. Extended type system: Boolean, Integer, Float, /// String, Object, List. /// /// /// InboundAPI-M2.6: validation is now RECURSIVE and type-aware for the /// extended Object / List types. Declared object fields are /// validated against their declared (nested) types, list elements against the /// declared element type, and scalars at any depth against the extended type — /// with path-qualified errors (e.g. order.items[2].quantity). The /// definition is read as JSON Schema (the canonical persisted format produced /// by the Central UI / migration); the legacy flat-array form is still /// accepted for transition safety. See /// /// for the shared recursive engine that /// also uses. /// /// public static class ParameterValidator { /// /// Validates the request body against the method's parameter definitions. /// Returns deserialized parameters or an error message. /// /// The parsed JSON request body; null or undefined if no body was supplied. /// JSON Schema describing the method's parameters (an object schema), or null/empty when no parameters are defined. The legacy flat-array form is also accepted. /// A with coerced parameter values on success, or an error message on failure. public static ParameterValidationResult Validate( JsonElement? body, string? parameterDefinitions) { InboundApiSchema? schema; try { schema = InboundApiSchema.Parse(parameterDefinitions); } catch (JsonException) { return ParameterValidationResult.Invalid("Invalid parameter definitions in method configuration"); } // No parameters defined (or an object schema with no declared fields) — // the body is unconstrained and yields an empty parameter set. if (schema is null || schema.Type != "object" || schema.Fields.Count == 0) { return ParameterValidationResult.Valid(new Dictionary()); } if (body == null || body.Value.ValueKind == JsonValueKind.Null || body.Value.ValueKind == JsonValueKind.Undefined) { var required = schema.Fields.Where(f => f.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"); } // Recursively type-check the whole body against the declared object // schema (nested Object fields, List element types, scalars at any // depth, undeclared-field rejection) with path-qualified errors. var errors = new List(); schema.Validate(body.Value, string.Empty, errors); if (errors.Count > 0) { return ParameterValidationResult.Invalid(string.Join("; ", errors)); } // Materialize the coerced top-level parameter values for the script. var result = new Dictionary(); foreach (var field in schema.Fields) { if (body.Value.TryGetProperty(field.Name, out var prop)) { result[field.Name] = Materialize(prop, field.Schema); } } return ParameterValidationResult.Valid(result); } /// /// Converts a validated JSON element to the CLR value handed to the script. /// Validation has already passed, so this only shapes the value: scalars to /// their primitive type, objects to , /// arrays to . /// private static object? Materialize(JsonElement element, InboundApiSchema schema) { if (element.ValueKind == JsonValueKind.Null) { return null; } return schema.Type switch { "boolean" => element.GetBoolean(), "integer" => element.GetInt64(), "number" => element.GetDouble(), "string" => element.GetString(), "object" => JsonSerializer.Deserialize>(element.GetRawText()), "array" => JsonSerializer.Deserialize>(element.GetRawText()), _ => JsonSerializer.Deserialize(element.GetRawText()), }; } } /// /// Result of parameter validation. /// public class ParameterValidationResult { /// Gets a value indicating whether validation succeeded. public bool IsValid { get; private init; } /// Gets the error message when is false; null on success. public string? ErrorMessage { get; private init; } /// Gets the validated and type-coerced parameter values keyed by parameter name. public IReadOnlyDictionary Parameters { get; private init; } = new Dictionary(); /// /// Creates a successful validation result with the given parameters. /// /// The validated and coerced parameter values. /// A with set to true. public static ParameterValidationResult Valid(Dictionary parameters) => new() { IsValid = true, Parameters = parameters }; /// /// Creates a failed validation result with the given error message. /// /// Description of the validation failure. /// A with set to false. public static ParameterValidationResult Invalid(string message) => new() { IsValid = false, ErrorMessage = message }; }