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