using System.Text.Json; namespace Microsoft.SelfService.Portal.Core.API.JsonDocuments { public static class ConfigurationDocumentValidator { public const string CurrentSchemaVersion = "1.0"; private static readonly HashSet SupportedSchemaVersions = new(StringComparer.OrdinalIgnoreCase) { CurrentSchemaVersion }; private static readonly HashSet SupportedTemplateTypes = new(StringComparer.OrdinalIgnoreCase) { "Environment", "Domain", "Landscape", "Service", "Stage", "Deployment" }; public static ConfigurationDocumentValidationResult NormalizeAndValidate( string? jsonData, ConfigurationDocumentKind kind, string? expectedSchemaVersion = null) { var source = string.IsNullOrWhiteSpace(jsonData) ? "{}" : jsonData; using var document = Parse(source); var root = document.RootElement; if (root.ValueKind != JsonValueKind.Object) { throw new ConfigurationDocumentValidationException("Configuration data must be a JSON object."); } return kind switch { ConfigurationDocumentKind.Template => ValidateTemplate(root, expectedSchemaVersion), ConfigurationDocumentKind.DeploymentOverride => ValidateDeploymentOverride(root), ConfigurationDocumentKind.Metadata => ValidateMetadata(root), _ => throw new ConfigurationDocumentValidationException($"Unsupported configuration document kind [{kind}].") }; } public static string NormalizeObject(string? jsonData) { return NormalizeAndValidate(jsonData, ConfigurationDocumentKind.Metadata).JsonData; } private static ConfigurationDocumentValidationResult ValidateTemplate(JsonElement root, string? expectedSchemaVersion) { var schemaVersion = GetRequiredString(root, "schemaVersion"); if (!SupportedSchemaVersions.Contains(schemaVersion)) { throw new ConfigurationDocumentValidationException($"Unsupported schemaVersion [{schemaVersion}]."); } if (!string.IsNullOrWhiteSpace(expectedSchemaVersion) && !string.Equals(schemaVersion, expectedSchemaVersion, StringComparison.OrdinalIgnoreCase)) { throw new ConfigurationDocumentValidationException($"Document schemaVersion [{schemaVersion}] does not match requested schemaVersion [{expectedSchemaVersion}]."); } var templateType = GetRequiredString(root, "templateType"); if (!SupportedTemplateTypes.Contains(templateType)) { throw new ConfigurationDocumentValidationException($"Unsupported templateType [{templateType}]."); } RequireObject(root, "parameters"); RequireObject(root, "variables"); RequireObject(root, "resources"); return new ConfigurationDocumentValidationResult(root.GetRawText(), schemaVersion); } private static ConfigurationDocumentValidationResult ValidateDeploymentOverride(JsonElement root) { var schemaVersion = TryGetString(root, "schemaVersion") ?? CurrentSchemaVersion; if (!SupportedSchemaVersions.Contains(schemaVersion)) { throw new ConfigurationDocumentValidationException($"Unsupported deployment override schemaVersion [{schemaVersion}]."); } return new ConfigurationDocumentValidationResult(root.GetRawText(), schemaVersion); } private static ConfigurationDocumentValidationResult ValidateMetadata(JsonElement root) { return new ConfigurationDocumentValidationResult(root.GetRawText(), TryGetString(root, "schemaVersion") ?? CurrentSchemaVersion); } private static JsonDocument Parse(string jsonData) { try { return JsonDocument.Parse(jsonData); } catch (JsonException ex) { throw new ConfigurationDocumentValidationException($"Invalid JSON document: {ex.Message}"); } } private static string GetRequiredString(JsonElement root, string propertyName) { var value = TryGetString(root, propertyName); if (string.IsNullOrWhiteSpace(value)) { throw new ConfigurationDocumentValidationException($"Configuration data requires string property [{propertyName}]."); } return value; } private static string? TryGetString(JsonElement root, string propertyName) { if (!root.TryGetProperty(propertyName, out var property)) { return null; } return property.ValueKind == JsonValueKind.String ? property.GetString() : null; } private static void RequireObject(JsonElement root, string propertyName) { if (!root.TryGetProperty(propertyName, out var property) || property.ValueKind != JsonValueKind.Object) { throw new ConfigurationDocumentValidationException($"Configuration data requires object property [{propertyName}]."); } } } }