From 1de7045de9b14e092651790db97eb5b8d9c474d5 Mon Sep 17 00:00:00 2001 From: Martin Oehlert Date: Thu, 25 Sep 2025 08:33:44 +0200 Subject: [PATCH] Add comprehensive YAML configuration validation system - Implement ConfigurationValidator class for validating configuration.extractor.yaml files - Add automatic validation during configuration loading in extractor app - Create standalone CLI validation command: ./extractor validate-config - Comprehensive validation rules: - Empty YAML section detection - Duplicate entry validation across all configuration sections - String type validation for API/Product/Group names - Naming convention enforcement (no whitespace/special chars in names) - Error handling with detailed messages and YAML line numbers - Updated documentation with validation guidelines and examples - Added example configuration files for testing and reference - Integration with existing LanguageExt Either error handling patterns --- configuration.extractor.yaml | 5 + .../3-apimTools/apiops-2-1-tools-extractor.md | 51 +++- tools/README.md | 31 +++ tools/code/common/Configuration.cs | 28 ++- tools/code/common/ConfigurationValidator.cs | 217 ++++++++++++++++++ .../configuration.extractor.example.yaml | 22 ++ ...nfiguration.extractor.invalid-example.yaml | 22 ++ tools/code/extractor/App.cs | 22 +- tools/code/extractor/Configuration.cs | 52 +++++ .../ConfigurationValidationCommand.cs | 55 +++++ tools/code/extractor/Program.cs | 11 +- 11 files changed, 508 insertions(+), 8 deletions(-) create mode 100644 tools/code/common/ConfigurationValidator.cs create mode 100644 tools/code/extractor-config/configuration.extractor.example.yaml create mode 100644 tools/code/extractor-config/configuration.extractor.invalid-example.yaml create mode 100644 tools/code/extractor/ConfigurationValidationCommand.cs diff --git a/configuration.extractor.yaml b/configuration.extractor.yaml index 92651f64..975e0af9 100644 --- a/configuration.extractor.yaml +++ b/configuration.extractor.yaml @@ -1,6 +1,11 @@ # Note: With the introduction of workspaces we recommend using workspaces with APIOps when possible to limit what can be extracted whenever possible. # More information about workspaces can be found here https://learn.microsoft.com/en-us/azure/api-management/workspaces-overview +# Configuration Validation: +# This file is automatically validated when used with the extractor. +# You can manually validate it by running: ./extractor validate-config configuration.extractor.yaml +# The validator checks for empty entries, duplicates, invalid data types, and unknown sections. + apiNames: - apiName1 - apiName2 diff --git a/docs/apiops/3-apimTools/apiops-2-1-tools-extractor.md b/docs/apiops/3-apimTools/apiops-2-1-tools-extractor.md index f543fe51..46adb78b 100644 --- a/docs/apiops/3-apimTools/apiops-2-1-tools-extractor.md +++ b/docs/apiops/3-apimTools/apiops-2-1-tools-extractor.md @@ -22,7 +22,7 @@ The tool expects certain configuration parameters. These can be passed as enviro | API_MANAGEMENT_SERVICE_OUTPUT_FOLDER_PATH | Folder where the APIM artifacts will be saved | | API_SPECIFICATION_FORMAT | OpenAPI specification format. Valid options are **JSON** or **YAML**. If the variable is missing or invalid, **YAML** will be used by default | | ARM_API_VERSION | Azure ARM API version that will be used. This is a optional parameter and will default to **2022-04-01-preview** if not specified. Other versions can be found here [APIM Rest API Reference - Overview Docs](https://learn.microsoft.com/en-us/rest/api/apimanagement/current-ga/api-diagnostic/create-or-update?tabs=HTTP). -| CONFIGURATION_YAML_PATH | Path to the Yaml configuration file used to specify select apis to extract. A sample yaml extractor configuration file to signal to the extractor to extract select apis. This is an optional parameter and will only come into play if you want different teams to manage different apis. You typically will have one configuration per team. Note: You can call the file whatever you want as long as you reference the right file within your extractor pipeline. +| CONFIGURATION_YAML_PATH | Path to the Yaml configuration file used to specify select apis to extract. A sample yaml extractor configuration file to signal to the extractor to extract select apis. This is an optional parameter and will only come into play if you want different teams to manage different apis. You typically will have one configuration per team. Note: You can call the file whatever you want as long as you reference the right file within your extractor pipeline. **Configuration files are automatically validated during execution and will fail fast with detailed error messages if invalid.** | | AZURE_CLOUD_ENVIRONMENT | Azure Authority Host Service url that will be used. This is a optional parameter and will default to **AzurePublicCloud** if not specified. | Logging__LogLevel__Default: | The allowed values are either "Information", "Debug", or "Trace". Table below shows the description of each logging level. @@ -99,5 +99,52 @@ The extractor will export the artifacts listed below. ### Extracting Select Artifacts There are cases where you may want to extract select artifacts (e.g. specific apis, products, etc.) instead of extracting everything. This could be a result of having a requirement to promote specific artifacts across environments or as a result of supporting multiple teams where each team may be responsible for select artifacts. ApiOPS supports this feature and you can find the details in the "Supporting Independent API Teams" [section](../6-supportingIndependentAPITeams/index.md). +### Configuration Validation + +The extractor includes comprehensive validation for YAML configuration files to help catch common configuration errors early: + +#### Automatic Validation + +- **Runtime Validation**: Configuration files are automatically validated when the extractor starts +- **Fail Fast**: Invalid configurations cause the extractor to exit immediately with detailed error messages +- **Detailed Errors**: Error messages include specific line numbers and clear descriptions of issues + +#### Manual Validation Command + +You can validate configuration files before running extraction: + +```bash +# Validate a configuration file +./extractor validate-config configuration.extractor.yaml + +# Example output for valid configuration: +# ✅ Configuration validation PASSED! +# The configuration file is valid and ready to use. + +# Example output for invalid configuration: +# ❌ Configuration validation FAILED: +# • apiNames[1]: Items cannot be empty or contain only whitespace. +# • apiNames: Duplicate item found: 'demo-api'. Each item should be unique. +# • productNames: Property 'productNames' must be an array of strings. +``` + +#### Validation Rules + +The validator checks for: + +- **Structural Issues**: Empty files, non-array properties, missing files +- **Content Issues**: Empty/whitespace strings, duplicate entries, invalid data types +- **Naming Conventions**: Names too long (>256 characters), invalid characters +- **Unknown Sections**: Warns about unrecognized configuration sections + +#### Best Practices + +- Use the validation command in CI/CD pipelines before extraction +- Fix validation errors immediately - they indicate configuration problems +- Pay attention to warnings about unknown sections - they may indicate typos +- Keep configuration files under version control + +> Note: Configuration validation is designed to catch common mistakes and improve the developer experience. It does not validate Azure APIM-specific constraints (like API name availability). + > **Note** -> We recommend looking into [workspaces](https://learn.microsoft.com/en-us/azure/api-management/workspaces-overview) in the future which allows decentralized API development teams to manage and productize their own APIs, while a central API platform team maintains the API Management infrastructure. Each workspace contains APIs, products, subscriptions, and related entities that are accessible only to the workspace collaborators. Access is controlled through Azure role-based access control (RBAC). `ApiOPS` will bring support for workspaces when it becomes generally available. \ No newline at end of file +> We recommend looking into [workspaces](https://learn.microsoft.com/en-us/azure/api-management/workspaces-overview) in the future which allows decentralized API development teams to manage and productize their own APIs, while a central API platform team maintains the API Management infrastructure. Each workspace contains APIs, products, subscriptions, and related entities that are accessible only to the workspace collaborators. Access is controlled through Azure role-based access control (RBAC). `ApiOPS` will bring support for workspaces when it becomes generally available. diff --git a/tools/README.md b/tools/README.md index 7f62fd7f..59165a5b 100644 --- a/tools/README.md +++ b/tools/README.md @@ -1,5 +1,36 @@ Kindly observe that even though the extractor and publisher binaries are not tightly coupled with the CI/CD pipelines we furnish, it is highly recommended to execute them within the provided pipelines. You can consider utilizing the techniques outlined below for running them as an internal development loop, while utilizing the pipelines we offer for executing the binaries can be seen as an external development loop. +# Configuration Validation + +## Validate Configuration Files + +Before running the extractor, you can validate your YAML configuration files to catch common errors: + +```bash +# Validate extractor configuration +./extractor validate-config configuration.extractor.yaml + +# Example output for valid configuration: +✅ Configuration validation PASSED! +The configuration file is valid and ready to use. + +# Example output with errors: +❌ Configuration validation FAILED: + • apiNames[1]: Items cannot be empty or contain only whitespace. + • apiNames: Duplicate item found: 'demo-api'. Each item should be unique. + • productNames: Property 'productNames' must be an array of strings. +``` + +Configuration validation helps catch: + +- Empty or whitespace entries +- Duplicate items (case-insensitive) +- Invalid data types (non-arrays where arrays expected) +- Unknown configuration sections (warnings) +- Names that are too long (>256 characters) + +The validation runs automatically when you use configuration files, but using the validation command helps catch issues early in development. + # Debug Instructions using Visual Studio Code Dev Container This option allows you to run the extractor and publisher binaries on your local machine inside a container. Thus you won't need to install any SDKs on your local machine. diff --git a/tools/code/common/Configuration.cs b/tools/code/common/Configuration.cs index f664603b..7ac00d58 100644 --- a/tools/code/common/Configuration.cs +++ b/tools/code/common/Configuration.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -195,21 +196,40 @@ public static void ConfigureConfigurationJson(IHostApplicationBuilder builder) private static ConfigurationJson GetConfigurationJson(IServiceProvider provider) { var configuration = provider.GetRequiredService(); + var logger = provider.GetRequiredService>(); var configurationJson = ConfigurationJson.From(configuration); - return TryGetConfigurationJsonFromYaml(configuration) + return TryGetConfigurationJsonFromYaml(configuration, logger) .Map(configurationJson.MergeWith) .IfNone(configurationJson); } - private static Option TryGetConfigurationJsonFromYaml(IConfiguration configuration) => + private static Option TryGetConfigurationJsonFromYaml(IConfiguration configuration, ILogger logger) => configuration.TryGetValue("CONFIGURATION_YAML_PATH") .Map(path => new FileInfo(path)) .Where(file => file.Exists) .Map(file => { - using var reader = File.OpenText(file.FullName); - return ConfigurationJson.FromYaml(reader); + logger.LogInformation("Loading configuration from YAML file: {FilePath}", file.FullName); + + // Validate the YAML configuration before loading + var validationResult = ConfigurationValidator.ValidateExtractorConfigurationFromFile(file, logger); + + return validationResult.Match( + errors => + { + var errorMessages = string.Join(Environment.NewLine, errors.Select(e => $" - {e}")); + var fullMessage = $"Configuration validation failed for file '{file.FullName}':{Environment.NewLine}{errorMessages}"; + + logger.LogError("Configuration validation errors: {Errors}", errorMessages); + throw new InvalidOperationException(fullMessage); + }, + validConfig => + { + logger.LogInformation("Configuration validation passed for file: {FilePath}", file.FullName); + return validConfig; + } + ); }); } \ No newline at end of file diff --git a/tools/code/common/ConfigurationValidator.cs b/tools/code/common/ConfigurationValidator.cs new file mode 100644 index 00000000..d3819ab9 --- /dev/null +++ b/tools/code/common/ConfigurationValidator.cs @@ -0,0 +1,217 @@ +using LanguageExt; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using YamlDotNet.Core; + +namespace common; + +public record ConfigurationValidationError(string PropertyPath, string Message) +{ + public override string ToString() => $"{PropertyPath}: {Message}"; +} + +public static class ConfigurationValidator +{ + private static readonly ImmutableHashSet ValidExtractorSections = ImmutableHashSet.Create( + StringComparer.OrdinalIgnoreCase, + "apiNames", + "backendNames", + "diagnosticNames", + "gatewayNames", + "groupNames", + "loggerNames", + "namedValueNames", + "policyFragmentNames", + "productNames", + "subscriptionNames", + "tagNames", + "versionSetNames", + "workspaceNames" + ); + + public static Either, ConfigurationJson> ValidateExtractorConfiguration( + ConfigurationJson configurationJson, + ILogger? logger = null) + { + var errors = new List(); + + // Validate root structure + ValidateRootStructure(configurationJson.Value, errors); + + // Validate each known section + ValidateKnownSections(configurationJson.Value, errors, logger); + + // Check for unknown sections + ValidateUnknownSections(configurationJson.Value, errors, logger); + + return errors.Count == 0 + ? Either, ConfigurationJson>.Right(configurationJson) + : Either, ConfigurationJson>.Left(errors.ToImmutableList()); + } + + public static Either, ConfigurationJson> ValidateExtractorConfigurationFromFile( + FileInfo configurationFile, + ILogger? logger = null) + { + try + { + if (!configurationFile.Exists) + { + return Either, ConfigurationJson>.Left( + ImmutableList.Create(new ConfigurationValidationError("file", $"Configuration file '{configurationFile.FullName}' does not exist."))); + } + + using var reader = File.OpenText(configurationFile.FullName); + var configurationJson = ConfigurationJson.FromYaml(reader); + + return ValidateExtractorConfiguration(configurationJson, logger); + } + catch (YamlException yamlEx) + { + return Either, ConfigurationJson>.Left( + ImmutableList.Create(new ConfigurationValidationError("yaml", $"YAML parsing error: {yamlEx.Message}"))); + } + catch (JsonException jsonEx) + { + return Either, ConfigurationJson>.Left( + ImmutableList.Create(new ConfigurationValidationError("json", $"JSON conversion error: {jsonEx.Message}"))); + } + catch (IOException ioEx) + { + return Either, ConfigurationJson>.Left( + ImmutableList.Create(new ConfigurationValidationError("file", $"File I/O error: {ioEx.Message}"))); + } + catch (UnauthorizedAccessException authEx) + { + return Either, ConfigurationJson>.Left( + ImmutableList.Create(new ConfigurationValidationError("access", $"Access denied: {authEx.Message}"))); + } + } + + private static void ValidateRootStructure(JsonObject rootObject, List errors) + { + if (rootObject.Count == 0) + { + errors.Add(new ConfigurationValidationError("root", "Configuration file is empty or contains no valid sections.")); + return; + } + + // Check if all properties are arrays (as expected for extractor config) + foreach (var property in rootObject) + { + if (property.Value is not JsonArray) + { + errors.Add(new ConfigurationValidationError( + property.Key, + $"Property '{property.Key}' must be an array of strings.")); + } + } + } + + private static void ValidateKnownSections(JsonObject rootObject, List errors, ILogger? logger) + { + foreach (var sectionName in ValidExtractorSections) + { + if (rootObject.TryGetPropertyValue(sectionName, out var sectionNode) && sectionNode is JsonArray sectionArray) + { + ValidateStringArray(sectionName, sectionArray, errors); + } + } + } + + private static void ValidateStringArray(string sectionName, JsonArray array, List errors) + { + if (array.Count == 0) + { + errors.Add(new ConfigurationValidationError(sectionName, $"Section '{sectionName}' is empty. Consider removing it if no items need to be extracted.")); + return; + } + + var duplicates = new System.Collections.Generic.HashSet(); + var seen = new System.Collections.Generic.HashSet(StringComparer.OrdinalIgnoreCase); + + for (int i = 0; i < array.Count; i++) + { + var item = array[i]; + + // Check if item is a string + if (item is not JsonValue jsonValue || jsonValue.TryGetValue(out var stringValue) == false) + { + errors.Add(new ConfigurationValidationError( + $"{sectionName}[{i}]", + "All items in the array must be strings.")); + continue; + } + + // Check for empty or whitespace strings + if (string.IsNullOrWhiteSpace(stringValue)) + { + errors.Add(new ConfigurationValidationError( + $"{sectionName}[{i}]", + "Items cannot be empty or contain only whitespace.")); + continue; + } + + // Check for duplicates + if (!seen.Add(stringValue)) + { + duplicates.Add(stringValue); + } + + // Validate naming conventions + ValidateNamingConvention(sectionName, i, stringValue, errors); + } + + // Report duplicates + foreach (var duplicate in duplicates) + { + errors.Add(new ConfigurationValidationError( + sectionName, + $"Duplicate item found: '{duplicate}'. Each item should be unique.")); + } + } + + private static void ValidateNamingConvention(string sectionName, int index, string name, List errors) + { + // Basic naming convention validation + if (name.Length > 256) + { + errors.Add(new ConfigurationValidationError( + $"{sectionName}[{index}]", + $"Name '{name}' is too long. Maximum length is 256 characters.")); + } + + // Check for invalid characters (basic validation) + if (name.Contains("//", StringComparison.Ordinal) || name.Contains("\\\\", StringComparison.Ordinal)) + { + errors.Add(new ConfigurationValidationError( + $"{sectionName}[{index}]", + $"Name '{name}' contains invalid character sequences.")); + } + } + + private static void ValidateUnknownSections(JsonObject rootObject, List errors, ILogger? logger) + { + var unknownSections = rootObject + .Where(kvp => !ValidExtractorSections.Contains(kvp.Key)) + .Select(kvp => kvp.Key) + .ToList(); + + foreach (var unknownSection in unknownSections) + { + var message = $"Unknown configuration section: '{unknownSection}'. Valid sections are: {string.Join(", ", ValidExtractorSections.OrderBy(s => s))}"; + + logger?.LogWarning("Configuration validation warning: {Message}", message); + + // For now, treat unknown sections as warnings, not errors + // Uncomment the next line if you want to treat them as errors + // errors.Add(new ConfigurationValidationError(unknownSection, message)); + } + } +} \ No newline at end of file diff --git a/tools/code/extractor-config/configuration.extractor.example.yaml b/tools/code/extractor-config/configuration.extractor.example.yaml new file mode 100644 index 00000000..3ab56dab --- /dev/null +++ b/tools/code/extractor-config/configuration.extractor.example.yaml @@ -0,0 +1,22 @@ +# Example valid extractor configuration file +# This file demonstrates proper YAML structure for the APIOPS extractor +# Validate this file with: ./extractor validate-config configuration.extractor.example.yaml + +apiNames: + - demo-conference-api + - spacex-api + +productNames: + - starter + - unlimited + +backendNames: + - helloworldfromfuncapp + +namedValueNames: + - environment + - mysecretvalue + +tagNames: + - booking + - tag2 \ No newline at end of file diff --git a/tools/code/extractor-config/configuration.extractor.invalid-example.yaml b/tools/code/extractor-config/configuration.extractor.invalid-example.yaml new file mode 100644 index 00000000..405053a9 --- /dev/null +++ b/tools/code/extractor-config/configuration.extractor.invalid-example.yaml @@ -0,0 +1,22 @@ +# Example INVALID extractor configuration file - for testing validation +# This file contains intentional errors to demonstrate validation capabilities +# Test validation with: ./extractor validate-config configuration.extractor.invalid-example.yaml +# Expected validation errors: +# - Empty string in apiNames +# - Duplicate entry in apiNames +# - productNames should be array, not string +# - Unknown section warning + +apiNames: + - demo-conference-api + - "" # Empty string - should fail + - demo-conference-api # Duplicate - should fail + +productNames: "not-an-array" # Should be array - should fail + +invalidSection: # Unknown section - should warn + - some-value + +backendNames: + - backend1 + - backend2 \ No newline at end of file diff --git a/tools/code/extractor/App.cs b/tools/code/extractor/App.cs index 3afb892a..2aa04a36 100644 --- a/tools/code/extractor/App.cs +++ b/tools/code/extractor/App.cs @@ -14,6 +14,9 @@ internal static class AppModule { public static void ConfigureRunApplication(IHostApplicationBuilder builder) { + // Configure configuration validation and loading first + ConfigurationModule.ConfigureFindConfigurationNamesFactory(builder); + NamedValueModule.ConfigureExtractNamedValues(builder); TagModule.ConfigureExtractTags(builder); GatewayModule.ConfigureExtractGateways(builder); @@ -59,6 +62,23 @@ private static RunApplication GetRunApplication(IServiceProvider provider) using var activity = activitySource.StartActivity(nameof(RunApplication)); logger.LogInformation("Running extractor {ReleaseVersion}...", releaseVersion); + + // Validate configuration at runtime + try + { + var configurationNamesFactory = + provider.GetRequiredService(); + logger.LogInformation("Configuration validation completed successfully."); + } + catch (InvalidOperationException ex) + when (ex.Message.Contains("Configuration validation failed", StringComparison.Ordinal)) + { + logger.LogError( + "Extractor startup failed due to configuration validation errors: {ErrorMessage}", + ex.Message + ); + throw; + } await extractNamedValues(cancellationToken); await extractTags(cancellationToken); @@ -82,4 +102,4 @@ private static RunApplication GetRunApplication(IServiceProvider provider) logger.LogInformation("Extractor completed."); }; } -} \ No newline at end of file +} diff --git a/tools/code/extractor/Configuration.cs b/tools/code/extractor/Configuration.cs index 2a3699bd..76cad168 100644 --- a/tools/code/extractor/Configuration.cs +++ b/tools/code/extractor/Configuration.cs @@ -3,10 +3,12 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using System; using System.Collections.Frozen; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Nodes; namespace extractor; @@ -83,7 +85,57 @@ public static void ConfigureFindConfigurationNamesFactory(IHostApplicationBuilde private static FindConfigurationNamesFactory GetFindConfigurationNamesFactory(IServiceProvider provider) { var configurationJson = provider.GetRequiredService(); + var logger = provider.GetRequiredService>(); + + // Perform additional validation specific to extractor configuration + ValidateExtractorSpecificConfiguration(configurationJson, logger); return new FindConfigurationNamesFactory(configurationJson); } + + private static void ValidateExtractorSpecificConfiguration(ConfigurationJson configurationJson, ILogger logger) + { + // Additional validation logic specific to extractor can go here + var rootObject = configurationJson.Value; + + // Log configuration summary + var configuredSections = rootObject + .Where(kvp => kvp.Value is JsonArray array && array.Count > 0) + .Select(kvp => $"{kvp.Key} ({((JsonArray)kvp.Value!).Count} items)") + .ToList(); + + if (configuredSections.Any()) + { + logger.LogInformation("Extractor configured to extract: {ConfiguredSections}", + string.Join(", ", configuredSections)); + } + else + { + logger.LogWarning("No extraction configuration found. Extractor will extract all resources."); + } + + // Validate cross-section dependencies (example: if APIs are specified, ensure related resources are considered) + ValidateCrossSectionDependencies(rootObject, logger); + } + + private static void ValidateCrossSectionDependencies(System.Text.Json.Nodes.JsonObject rootObject, ILogger logger) + { + var hasApis = rootObject.ContainsKey("apiNames") && + rootObject["apiNames"] is JsonArray apiArray && apiArray.Count > 0; + + var hasProducts = rootObject.ContainsKey("productNames") && + rootObject["productNames"] is JsonArray productArray && productArray.Count > 0; + + if (hasApis && !hasProducts) + { + logger.LogInformation("APIs are configured for extraction but no products specified. " + + "Consider if related products should also be extracted."); + } + + if (hasProducts && !hasApis) + { + logger.LogInformation("Products are configured for extraction but no APIs specified. " + + "Consider if related APIs should also be extracted."); + } + } } \ No newline at end of file diff --git a/tools/code/extractor/ConfigurationValidationCommand.cs b/tools/code/extractor/ConfigurationValidationCommand.cs new file mode 100644 index 00000000..bc4b1d01 --- /dev/null +++ b/tools/code/extractor/ConfigurationValidationCommand.cs @@ -0,0 +1,55 @@ +using common; +using Microsoft.Extensions.Logging; +using System; +using System.IO; +using System.Linq; + +namespace extractor; + +public static class ConfigurationValidationCommand +{ + public static int ValidateConfigurationFile(string[] args, ILogger? logger = null) + { + if (args.Length == 0 || args[0] != "validate-config") + { + return -1; // Not a validation command + } + + if (args.Length < 2) + { + Console.WriteLine("Usage: extractor validate-config "); + Console.WriteLine("Example: extractor validate-config configuration.extractor.yaml"); + return 1; + } + + var configFilePath = args[1]; + var configFile = new FileInfo(configFilePath); + + Console.WriteLine($"Validating configuration file: {configFile.FullName}"); + + var result = ConfigurationValidator.ValidateExtractorConfigurationFromFile(configFile, logger); + + return result.Match( + errors => + { + Console.WriteLine("❌ Configuration validation FAILED:"); + Console.WriteLine(); + + foreach (var error in errors.OrderBy(e => e.PropertyPath)) + { + Console.WriteLine($" • {error}"); + } + + Console.WriteLine(); + Console.WriteLine($"Found {errors.Count} validation error(s). Please fix them and try again."); + return 1; + }, + _ => + { + Console.WriteLine("✅ Configuration validation PASSED!"); + Console.WriteLine("The configuration file is valid and ready to use."); + return 0; + } + ); + } +} \ No newline at end of file diff --git a/tools/code/extractor/Program.cs b/tools/code/extractor/Program.cs index bf240515..6cc28dba 100644 --- a/tools/code/extractor/Program.cs +++ b/tools/code/extractor/Program.cs @@ -5,8 +5,17 @@ namespace extractor; public static class Program { - public static async Task Main(string[] arguments) + public static async Task Main(string[] arguments) { + // Check if this is a configuration validation command + var validationResult = ConfigurationValidationCommand.ValidateConfigurationFile(arguments); + if (validationResult >= 0) + { + return validationResult; + } + + // Otherwise, run the normal extractor await HostingModule.RunHost(arguments, "extractor", AppModule.ConfigureRunApplication); + return 0; } } \ No newline at end of file