feat(m9/T32b): JSON Schema $ref resolver (lib seam, cycle/depth-guarded) + deploy-time dangling-ref block
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.InboundApi;
|
||||
using ZB.MOM.WW.ScadaBridge.ScriptAnalysis;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation;
|
||||
@@ -83,13 +84,25 @@ public class ValidationService
|
||||
/// connection is checked against this set so a binding to a phantom/stale connection
|
||||
/// is caught. <c>null</c> skips the "exists at site" half (it stays inert).
|
||||
/// </param>
|
||||
/// <param name="resolveSchemaRef">
|
||||
/// M9-T32b: optional JSON-Schema <c>$ref</c> resolution seam mapping a
|
||||
/// <c>lib:Name</c> reference target's name to the referenced schema JSON (or
|
||||
/// <c>null</c> when the library entry does not exist). Supplied on the deploy path
|
||||
/// (backed by <c>ISharedSchemaRepository</c>) so a dangling/cyclic/over-depth
|
||||
/// <c>$ref</c> in any validated script parameter/return schema becomes a
|
||||
/// deploy-blocking <see cref="ValidationCategory.SchemaReference"/> error naming the
|
||||
/// missing reference. When <c>null</c> the seam is absent: schemas with no
|
||||
/// <c>$ref</c> are unaffected (behavior unchanged), and a <c>$ref</c> with no
|
||||
/// resolver is treated as dangling (the safe option).
|
||||
/// </param>
|
||||
/// <returns>A merged <see cref="ValidationResult"/> aggregating all pipeline stage outcomes.</returns>
|
||||
public ValidationResult Validate(
|
||||
FlattenedConfiguration configuration,
|
||||
IReadOnlyList<ResolvedScript>? sharedScripts = null,
|
||||
IReadOnlySet<string>? alarmCapableConnectionNames = null,
|
||||
bool enforceConnectionBindings = false,
|
||||
IReadOnlySet<string>? siteConnectionNames = null)
|
||||
IReadOnlySet<string>? siteConnectionNames = null,
|
||||
Func<string, string?>? resolveSchemaRef = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
|
||||
@@ -102,12 +115,86 @@ public class ValidationService
|
||||
ValidateScriptTriggerReferences(configuration),
|
||||
ValidateExpressionTriggers(configuration),
|
||||
ValidateConnectionBindingCompleteness(configuration, enforceConnectionBindings, siteConnectionNames),
|
||||
ValidateSchemaReferences(configuration, sharedScripts, resolveSchemaRef),
|
||||
_semanticValidator.Validate(configuration, sharedScripts, alarmCapableConnectionNames)
|
||||
};
|
||||
|
||||
return ValidationResult.Merge(results.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M9-T32b — JSON-Schema <c>$ref</c> resolution check. Parses every script
|
||||
/// parameter/return schema (instance scripts and the supplied shared scripts)
|
||||
/// through <see cref="InboundApiSchema.ParseWithRefs"/>, resolving any
|
||||
/// <c>{"$ref":"lib:Name"}</c> reference via <paramref name="resolveSchemaRef"/>.
|
||||
/// A reference that cannot be resolved — dangling (the seam returns <c>null</c> or
|
||||
/// none is supplied), cyclic, or over-depth — is a deploy-blocking
|
||||
/// <see cref="ValidationCategory.SchemaReference"/> error naming the missing
|
||||
/// reference and the owning script.
|
||||
///
|
||||
/// <para>
|
||||
/// The check is INERT for schemas that contain no <c>$ref</c>:
|
||||
/// <see cref="InboundApiSchema.ParseWithRefs"/> never consults the seam for them and
|
||||
/// reports no unresolved refs, so the pre-existing validation behavior is unchanged
|
||||
/// (this is the only edit to the schema validation path for non-<c>$ref</c> schemas).
|
||||
/// A malformed (non-JSON) schema is left to the existing script-compilation /
|
||||
/// semantic checks — this check swallows the parse exception and reports nothing,
|
||||
/// so it never double-reports a structural problem as a missing reference.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="configuration">The flattened configuration whose scripts' schemas are checked.</param>
|
||||
/// <param name="sharedScripts">Optional shared scripts whose schemas are also checked.</param>
|
||||
/// <param name="resolveSchemaRef">The <c>$ref</c> resolution seam, or <c>null</c> (no resolver → refs dangle).</param>
|
||||
/// <returns>A <see cref="ValidationResult"/> with one error per unresolved reference, or success.</returns>
|
||||
internal static ValidationResult ValidateSchemaReferences(
|
||||
FlattenedConfiguration configuration,
|
||||
IReadOnlyList<ResolvedScript>? sharedScripts,
|
||||
Func<string, string?>? resolveSchemaRef)
|
||||
{
|
||||
var errors = new List<ValidationEntry>();
|
||||
|
||||
foreach (var script in configuration.Scripts)
|
||||
{
|
||||
CheckSchema(script.CanonicalName, "parameter definitions", script.ParameterDefinitions);
|
||||
CheckSchema(script.CanonicalName, "return definition", script.ReturnDefinition);
|
||||
}
|
||||
|
||||
foreach (var shared in sharedScripts ?? [])
|
||||
{
|
||||
CheckSchema(shared.CanonicalName, "parameter definitions", shared.ParameterDefinitions);
|
||||
CheckSchema(shared.CanonicalName, "return definition", shared.ReturnDefinition);
|
||||
}
|
||||
|
||||
return errors.Count > 0
|
||||
? new ValidationResult { Errors = errors }
|
||||
: ValidationResult.Success();
|
||||
|
||||
void CheckSchema(string scriptName, string schemaLabel, string? schemaJson)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(schemaJson))
|
||||
return;
|
||||
|
||||
SchemaParseResult parsed;
|
||||
try
|
||||
{
|
||||
parsed = InboundApiSchema.ParseWithRefs(schemaJson, resolveSchemaRef);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Malformed schema JSON / over-depth nesting is surfaced by the other
|
||||
// validation stages — not a missing-reference concern. Don't double-report.
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var missing in parsed.UnresolvedRefs)
|
||||
{
|
||||
errors.Add(ValidationEntry.Error(ValidationCategory.SchemaReference,
|
||||
$"Script '{scriptName}' {schemaLabel} references schema 'lib:{missing}' which could not be resolved.",
|
||||
scriptName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that flattening produced a non-empty configuration.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user