4b6187c853
Object/List parameters and return values were shape-validated only (object vs
array), with no field-level/nested type checks — type-wrong nested data passed
inbound validation and failed only at script runtime. Add recursive type
validation (declared Object field types, List element type, scalars at any depth)
with path-qualified errors, symmetric across ParameterValidator and ReturnValueValidator.
Both validators now parse the canonical JSON Schema definition format (the
Central UI / MigrateParametersToJsonSchema output) via a shared recursive engine,
Commons.Types.InboundApi.InboundApiSchema, instead of the legacy flat
[{name,type}] array which they could not even deserialize from migrated rows.
The legacy flat-array form is still accepted on read for transition safety.
Undeclared fields are rejected at every level (consistent with the existing
top-level unexpected-parameter rejection); a present-but-null value satisfies
any type, only absence of a required field is an error.
151 lines
6.5 KiB
C#
151 lines
6.5 KiB
C#
using System.Text.Json;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.InboundApi;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
|
|
|
|
/// <summary>
|
|
/// WP-2: Validates and deserializes a JSON request body against a method's
|
|
/// parameter definitions. Extended type system: Boolean, Integer, Float,
|
|
/// String, Object, List.
|
|
///
|
|
/// <para>
|
|
/// InboundAPI-M2.6: validation is now RECURSIVE and type-aware for the
|
|
/// extended <c>Object</c> / <c>List</c> 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. <c>order.items[2].quantity</c>). 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
|
|
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.InboundApi.InboundApiSchema"/>
|
|
/// for the shared recursive engine that <see cref="ReturnValueValidator"/>
|
|
/// also uses.
|
|
/// </para>
|
|
/// </summary>
|
|
public static class ParameterValidator
|
|
{
|
|
/// <summary>
|
|
/// Validates the request body against the method's parameter definitions.
|
|
/// Returns deserialized parameters or an error message.
|
|
/// </summary>
|
|
/// <param name="body">The parsed JSON request body; null or undefined if no body was supplied.</param>
|
|
/// <param name="parameterDefinitions">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.</param>
|
|
/// <returns>A <see cref="ParameterValidationResult"/> with coerced parameter values on success, or an error message on failure.</returns>
|
|
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<string, object?>());
|
|
}
|
|
|
|
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<string, object?>());
|
|
}
|
|
|
|
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<string>();
|
|
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<string, object?>();
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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 <see cref="Dictionary{TKey,TValue}"/>,
|
|
/// arrays to <see cref="List{T}"/>.
|
|
/// </summary>
|
|
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<Dictionary<string, object?>>(element.GetRawText()),
|
|
"array" => JsonSerializer.Deserialize<List<object?>>(element.GetRawText()),
|
|
_ => JsonSerializer.Deserialize<object?>(element.GetRawText()),
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result of parameter validation.
|
|
/// </summary>
|
|
public class ParameterValidationResult
|
|
{
|
|
/// <summary>Gets a value indicating whether validation succeeded.</summary>
|
|
public bool IsValid { get; private init; }
|
|
/// <summary>Gets the error message when <see cref="IsValid"/> is false; null on success.</summary>
|
|
public string? ErrorMessage { get; private init; }
|
|
/// <summary>Gets the validated and type-coerced parameter values keyed by parameter name.</summary>
|
|
public IReadOnlyDictionary<string, object?> Parameters { get; private init; } = new Dictionary<string, object?>();
|
|
|
|
/// <summary>
|
|
/// Creates a successful validation result with the given parameters.
|
|
/// </summary>
|
|
/// <param name="parameters">The validated and coerced parameter values.</param>
|
|
/// <returns>A <see cref="ParameterValidationResult"/> with <see cref="IsValid"/> set to <c>true</c>.</returns>
|
|
public static ParameterValidationResult Valid(Dictionary<string, object?> parameters) =>
|
|
new() { IsValid = true, Parameters = parameters };
|
|
|
|
/// <summary>
|
|
/// Creates a failed validation result with the given error message.
|
|
/// </summary>
|
|
/// <param name="message">Description of the validation failure.</param>
|
|
/// <returns>A <see cref="ParameterValidationResult"/> with <see cref="IsValid"/> set to <c>false</c>.</returns>
|
|
public static ParameterValidationResult Invalid(string message) =>
|
|
new() { IsValid = false, ErrorMessage = message };
|
|
}
|