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 };
}