using System.Text.Json;
namespace ScadaLink.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 a JSON array of
/// (the same {name,type} shape as a parameter definition). A method whose
/// ReturnDefinition is null/empty is unconstrained — its return value is
/// serialized as-is (backward compatible). Primitive fields (Boolean / Integer /
/// Float / String) are type-checked; the extended Object/List types
/// are shape-checked only (object vs. array), consistent with how
/// treats inbound extended types.
///
///
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.
///
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();
}
List fields;
try
{
fields = JsonSerializer.Deserialize>(
returnDefinition,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
?? [];
}
catch (JsonException)
{
return ReturnValidationResult.Invalid(
"Invalid return definition in method configuration");
}
if (fields.Count == 0)
{
return ReturnValidationResult.Valid();
}
if (string.IsNullOrWhiteSpace(resultJson))
{
return ReturnValidationResult.Invalid(
"Method declares a return structure but the script returned no value");
}
JsonElement root;
try
{
using var doc = JsonDocument.Parse(resultJson);
root = doc.RootElement.Clone();
}
catch (JsonException)
{
return ReturnValidationResult.Invalid("Script return value is not valid JSON");
}
if (root.ValueKind != JsonValueKind.Object)
{
return ReturnValidationResult.Invalid(
"Method declares a return structure but the script did not return an object");
}
var errors = new List();
foreach (var field in fields)
{
if (!root.TryGetProperty(field.Name, out var value))
{
errors.Add($"missing return field '{field.Name}'");
continue;
}
var typeError = CheckFieldType(value, field.Type, field.Name);
if (typeError != null)
errors.Add(typeError);
}
return errors.Count > 0
? ReturnValidationResult.Invalid(
$"Return value does not match the declared return definition: {string.Join("; ", errors)}")
: ReturnValidationResult.Valid();
}
private static string? CheckFieldType(JsonElement value, string declaredType, string fieldName)
{
// A null value satisfies any field type — the script may legitimately omit
// optional data; only a missing field (handled by the caller) is an error.
if (value.ValueKind == JsonValueKind.Null)
return null;
var ok = declaredType.ToLowerInvariant() switch
{
"boolean" => value.ValueKind is JsonValueKind.True or JsonValueKind.False,
"integer" => value.ValueKind == JsonValueKind.Number && value.TryGetInt64(out _),
"float" => value.ValueKind == JsonValueKind.Number,
"string" => value.ValueKind == JsonValueKind.String,
"object" => value.ValueKind == JsonValueKind.Object,
"list" => value.ValueKind == JsonValueKind.Array,
_ => true, // unknown declared type — do not block the response
};
return ok ? null : $"return field '{fieldName}' must be {declaredType}";
}
}
///
/// InboundAPI-014: one field of a method's declared return structure — the
/// deserialized form of an entry in ApiMethod.ReturnDefinition. Defined in
/// this module (not Commons) because the inbound API is currently its only consumer.
///
public sealed class ReturnFieldDefinition
{
public string Name { get; set; } = string.Empty;
public string Type { get; set; } = "String";
}
///
/// Result of validating a script return value against a method's return definition.
///
public sealed class ReturnValidationResult
{
public bool IsValid { get; private init; }
public string ErrorMessage { get; private init; } = string.Empty;
public static ReturnValidationResult Valid() => new() { IsValid = true };
public static ReturnValidationResult Invalid(string message) =>
new() { IsValid = false, ErrorMessage = message };
}