145 lines
5.4 KiB
C#
145 lines
5.4 KiB
C#
using System.Text.Json;
|
|
|
|
namespace ScadaLink.InboundAPI;
|
|
|
|
/// <summary>
|
|
/// InboundAPI-014: validates a method script's return value against the method's
|
|
/// declared <c>ReturnDefinition</c>. <c>Component-InboundAPI.md</c> ("Return Value
|
|
/// Definition" / "Response Format") states the success body has "fields matching
|
|
/// the return value definition"; this is the response-side mirror of
|
|
/// <see cref="ParameterValidator"/>.
|
|
///
|
|
/// <para>
|
|
/// The return definition is a JSON array of <see cref="ReturnFieldDefinition"/>
|
|
/// (the same <c>{name,type}</c> shape as a parameter definition). A method whose
|
|
/// <c>ReturnDefinition</c> 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 <c>Object</c>/<c>List</c> types
|
|
/// are shape-checked only (object vs. array), consistent with how
|
|
/// <see cref="ParameterValidator"/> treats inbound extended types.
|
|
/// </para>
|
|
/// </summary>
|
|
public static class ReturnValueValidator
|
|
{
|
|
/// <summary>
|
|
/// Validates the serialized script result JSON against the method's return
|
|
/// definition. Returns <see cref="ReturnValidationResult.Valid"/> when no
|
|
/// definition is configured or the result conforms to it.
|
|
/// </summary>
|
|
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<ReturnFieldDefinition> fields;
|
|
try
|
|
{
|
|
fields = JsonSerializer.Deserialize<List<ReturnFieldDefinition>>(
|
|
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<string>();
|
|
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}";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// InboundAPI-014: one field of a method's declared return structure — the
|
|
/// deserialized form of an entry in <c>ApiMethod.ReturnDefinition</c>. Defined in
|
|
/// this module (not Commons) because the inbound API is currently its only consumer.
|
|
/// </summary>
|
|
public sealed class ReturnFieldDefinition
|
|
{
|
|
public string Name { get; set; } = string.Empty;
|
|
public string Type { get; set; } = "String";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result of validating a script return value against a method's return definition.
|
|
/// </summary>
|
|
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 };
|
|
}
|