using System.Text.Json; using ZB.MOM.WW.ScadaBridge.Commons.Types.InboundApi; namespace ZB.MOM.WW.ScadaBridge.InboundAPI; /// /// InboundAPI-014: validates a method script's return value against the method's /// declared ReturnDefinition. Component-InboundAPI.md ("Return Value /// Definition" / "Response Format") states the success body has "fields matching /// the return value definition"; this is the response-side mirror of /// . /// /// /// The return definition is JSON Schema (the canonical persisted format; the /// legacy flat [{name,type}] array is still accepted for transition /// safety). A method whose ReturnDefinition is null/empty is /// unconstrained — its return value is serialized as-is (backward compatible). /// /// /// /// InboundAPI-M2.6: validation is RECURSIVE and type-aware — declared object /// fields are validated against their declared (nested) types, list elements /// against the declared element type, and scalars at any depth — with /// path-qualified errors. The recursion is shared with /// via /// , /// so the inbound and outbound type checks cannot drift apart. /// /// public static class ReturnValueValidator { /// /// Validates the serialized script result JSON against the method's return /// definition. Returns when no /// definition is configured or the result conforms to it. /// /// The JSON-serialized script return value to validate. /// JSON Schema describing the method's return value, or null/empty to skip validation. The legacy flat-array form is also accepted. /// A indicating success or describing the validation failures. public static ReturnValidationResult Validate(string? resultJson, string? returnDefinition) { if (string.IsNullOrWhiteSpace(returnDefinition)) { // No declared return shape — the script's return value is unconstrained. return ReturnValidationResult.Valid(); } InboundApiSchema? schema; try { schema = InboundApiSchema.Parse(returnDefinition); } catch (JsonException) { return ReturnValidationResult.Invalid( "Invalid return definition in method configuration"); } // A schema that declares no constraints (e.g. an object schema with no // fields) leaves the return value unconstrained. if (schema is null || (schema.Type == "object" && schema.Fields.Count == 0)) { return ReturnValidationResult.Valid(); } // INTENTIONAL asymmetry with ParameterValidator: // // ParameterValidator has an early-return guard for "schema.Type != object" // because method parameters are ALWAYS a top-level JSON object (flat map of // name→value); a non-object parameter schema is treated as unconstrained. // // ReturnValueValidator does NOT guard on schema.Type here. A method may // declare a scalar return type (e.g. {"type":"string"} or {"type":"integer"}) // and the script is expected to return exactly that scalar JSON value. // Guarding on type == "object" would silently bypass validation for scalar // and array return schemas — do NOT add that guard here. if (string.IsNullOrWhiteSpace(resultJson)) { return ReturnValidationResult.Invalid( "Method declares a return structure but the script returned no value"); } JsonElement root; JsonDocument doc; try { doc = JsonDocument.Parse(resultJson); } catch (JsonException) { return ReturnValidationResult.Invalid("Script return value is not valid JSON"); } using (doc) { root = doc.RootElement; // A JSON null result against a declared structure is treated as // "no value returned" (preserves the prior contract). if (root.ValueKind == JsonValueKind.Null) { return ReturnValidationResult.Invalid( "Method declares a return structure but the script returned no value"); } var errors = new List(); schema.Validate(root, string.Empty, errors); return errors.Count > 0 ? ReturnValidationResult.Invalid( $"Return value does not match the declared return definition: {string.Join("; ", errors)}") : ReturnValidationResult.Valid(); } } } /// /// Result of validating a script return value against a method's return definition. /// public sealed class ReturnValidationResult { /// True when the return value conforms to the declared return definition. public bool IsValid { get; private init; } /// Human-readable error message when is false; empty otherwise. public string ErrorMessage { get; private init; } = string.Empty; /// Returns a successful validation result. /// A with set to true. public static ReturnValidationResult Valid() => new() { IsValid = true }; /// Returns a failed validation result with the specified error message. /// Human-readable description of the validation failure. /// A with set to false and the given error message. public static ReturnValidationResult Invalid(string message) => new() { IsValid = false, ErrorMessage = message }; }