MarketAlly.AIPlugin.Extensions/MarketAlly.AIPlugin.Refacto.../Configuration/PluginConfigurationManager.cs

488 lines
20 KiB
C#
Executable File

using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
namespace MarketAlly.AIPlugin.Refactoring.Configuration
{
public interface IPluginConfigurationManager
{
Task<TConfig> LoadConfigurationAsync<TConfig>(
string pluginName,
string? projectPath = null,
CancellationToken cancellationToken = default) where TConfig : class, new();
Task SaveConfigurationAsync<TConfig>(
string pluginName,
TConfig configuration,
string? projectPath = null,
CancellationToken cancellationToken = default) where TConfig : class;
Task<bool> ConfigurationExistsAsync(
string pluginName,
string? projectPath = null,
CancellationToken cancellationToken = default);
void InvalidateCache(string pluginName, string? projectPath = null);
ConfigurationSources GetConfigurationSources(string pluginName, string? projectPath = null);
}
public class ConfigurationSources
{
public string? ProjectConfigPath { get; set; }
public string? UserConfigPath { get; set; }
public string? GlobalConfigPath { get; set; }
public List<string> SearchedPaths { get; set; } = new();
}
public class PluginConfigurationManager : IPluginConfigurationManager
{
private readonly ILogger<PluginConfigurationManager>? _logger;
private readonly Dictionary<string, object> _configCache = new();
private readonly SemaphoreSlim _cacheLock = new(1, 1);
private readonly JsonSerializerOptions _jsonOptions;
public PluginConfigurationManager(ILogger<PluginConfigurationManager>? logger = null)
{
_logger = logger;
_jsonOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters = { new JsonStringEnumConverter() },
AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip
};
}
public async Task<TConfig> LoadConfigurationAsync<TConfig>(
string pluginName,
string? projectPath = null,
CancellationToken cancellationToken = default) where TConfig : class, new()
{
if (string.IsNullOrWhiteSpace(pluginName))
throw new ArgumentException("Plugin name cannot be null or empty", nameof(pluginName));
var cacheKey = GetCacheKey(pluginName, projectPath, typeof(TConfig));
await _cacheLock.WaitAsync(cancellationToken);
try
{
// Check cache first
if (_configCache.TryGetValue(cacheKey, out var cachedConfig))
{
_logger?.LogDebug("Configuration cache hit for {PluginName}", pluginName);
return (TConfig)cachedConfig;
}
// Load from multiple sources
var configuration = await LoadFromMultipleSourcesAsync<TConfig>(pluginName, projectPath, cancellationToken);
// Cache the result
_configCache[cacheKey] = configuration;
_logger?.LogDebug("Configuration loaded and cached for {PluginName}", pluginName);
return configuration;
}
finally
{
_cacheLock.Release();
}
}
public async Task SaveConfigurationAsync<TConfig>(
string pluginName,
TConfig configuration,
string? projectPath = null,
CancellationToken cancellationToken = default) where TConfig : class
{
if (string.IsNullOrWhiteSpace(pluginName))
throw new ArgumentException("Plugin name cannot be null or empty", nameof(pluginName));
if (configuration == null)
throw new ArgumentNullException(nameof(configuration));
var configPath = GetProjectConfigPath(pluginName, projectPath);
var directory = Path.GetDirectoryName(configPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
try
{
var json = JsonSerializer.Serialize(configuration, _jsonOptions);
await File.WriteAllTextAsync(configPath, json, cancellationToken);
// Update cache
var cacheKey = GetCacheKey(pluginName, projectPath, typeof(TConfig));
await _cacheLock.WaitAsync(cancellationToken);
try
{
_configCache[cacheKey] = configuration;
}
finally
{
_cacheLock.Release();
}
_logger?.LogInformation("Configuration saved for {PluginName} to {ConfigPath}", pluginName, configPath);
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to save configuration for {PluginName} to {ConfigPath}", pluginName, configPath);
throw;
}
}
public async Task<bool> ConfigurationExistsAsync(
string pluginName,
string? projectPath = null,
CancellationToken cancellationToken = default)
{
var sources = GetConfigurationSources(pluginName, projectPath);
return (!string.IsNullOrEmpty(sources.ProjectConfigPath) && File.Exists(sources.ProjectConfigPath)) ||
(!string.IsNullOrEmpty(sources.UserConfigPath) && File.Exists(sources.UserConfigPath)) ||
(!string.IsNullOrEmpty(sources.GlobalConfigPath) && File.Exists(sources.GlobalConfigPath));
}
public void InvalidateCache(string pluginName, string? projectPath = null)
{
_cacheLock.Wait();
try
{
var keysToRemove = new List<string>();
var prefix = GetCacheKeyPrefix(pluginName, projectPath);
foreach (var key in _configCache.Keys)
{
if (key.StartsWith(prefix))
{
keysToRemove.Add(key);
}
}
foreach (var key in keysToRemove)
{
_configCache.Remove(key);
}
_logger?.LogDebug("Configuration cache invalidated for {PluginName}", pluginName);
}
finally
{
_cacheLock.Release();
}
}
public ConfigurationSources GetConfigurationSources(string pluginName, string? projectPath = null)
{
var sources = new ConfigurationSources();
// 1. Project-specific configuration
if (!string.IsNullOrEmpty(projectPath))
{
sources.ProjectConfigPath = GetProjectConfigPath(pluginName, projectPath);
sources.SearchedPaths.Add(sources.ProjectConfigPath);
}
// 2. User-specific configuration
sources.UserConfigPath = GetUserConfigPath(pluginName);
sources.SearchedPaths.Add(sources.UserConfigPath);
// 3. Global configuration
sources.GlobalConfigPath = GetGlobalConfigPath(pluginName);
sources.SearchedPaths.Add(sources.GlobalConfigPath);
return sources;
}
private async Task<TConfig> LoadFromMultipleSourcesAsync<TConfig>(
string pluginName,
string? projectPath,
CancellationToken cancellationToken) where TConfig : class, new()
{
var sources = GetConfigurationSources(pluginName, projectPath);
var baseConfig = new TConfig();
// Start with global defaults
if (!string.IsNullOrEmpty(sources.GlobalConfigPath) && File.Exists(sources.GlobalConfigPath))
{
try
{
var globalConfig = await LoadSingleConfigAsync<TConfig>(sources.GlobalConfigPath, cancellationToken);
baseConfig = MergeConfigurations(baseConfig, globalConfig);
_logger?.LogDebug("Loaded global configuration from {Path}", sources.GlobalConfigPath);
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "Failed to load global configuration from {Path}", sources.GlobalConfigPath);
}
}
// Override with user-specific settings
if (!string.IsNullOrEmpty(sources.UserConfigPath) && File.Exists(sources.UserConfigPath))
{
try
{
var userConfig = await LoadSingleConfigAsync<TConfig>(sources.UserConfigPath, cancellationToken);
baseConfig = MergeConfigurations(baseConfig, userConfig);
_logger?.LogDebug("Loaded user configuration from {Path}", sources.UserConfigPath);
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "Failed to load user configuration from {Path}", sources.UserConfigPath);
}
}
// Override with project-specific settings
if (!string.IsNullOrEmpty(sources.ProjectConfigPath) && File.Exists(sources.ProjectConfigPath))
{
try
{
var projectConfig = await LoadSingleConfigAsync<TConfig>(sources.ProjectConfigPath, cancellationToken);
baseConfig = MergeConfigurations(baseConfig, projectConfig);
_logger?.LogDebug("Loaded project configuration from {Path}", sources.ProjectConfigPath);
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "Failed to load project configuration from {Path}", sources.ProjectConfigPath);
}
}
return baseConfig;
}
private async Task<TConfig> LoadSingleConfigAsync<TConfig>(string configPath, CancellationToken cancellationToken) where TConfig : class, new()
{
try
{
var json = await File.ReadAllTextAsync(configPath, cancellationToken);
var config = JsonSerializer.Deserialize<TConfig>(json, _jsonOptions);
return config ?? new TConfig();
}
catch (JsonException ex)
{
_logger?.LogError(ex, "Invalid JSON in configuration file: {ConfigPath}", configPath);
throw new InvalidOperationException($"Invalid JSON in configuration file: {configPath}", ex);
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to load configuration from: {ConfigPath}", configPath);
throw;
}
}
private TConfig MergeConfigurations<TConfig>(TConfig baseConfig, TConfig overrideConfig) where TConfig : class
{
// Simple merge strategy - for complex scenarios, you might want to use a library like AutoMapper
// or implement custom merge logic for specific configuration types
try
{
var baseJson = JsonSerializer.Serialize(baseConfig, _jsonOptions);
var overrideJson = JsonSerializer.Serialize(overrideConfig, _jsonOptions);
// Parse both as JsonDocument for merging
using var baseDoc = JsonDocument.Parse(baseJson);
using var overrideDoc = JsonDocument.Parse(overrideJson);
var merged = MergeJsonObjects(baseDoc.RootElement, overrideDoc.RootElement);
return JsonSerializer.Deserialize<TConfig>(merged, _jsonOptions) ?? baseConfig;
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "Failed to merge configurations, using override configuration");
return overrideConfig;
}
}
private string MergeJsonObjects(JsonElement baseElement, JsonElement overrideElement)
{
var merged = new Dictionary<string, object>();
// Add all properties from base
if (baseElement.ValueKind == JsonValueKind.Object)
{
foreach (var property in baseElement.EnumerateObject())
{
merged[property.Name] = JsonElementToObject(property.Value);
}
}
// Override with properties from override
if (overrideElement.ValueKind == JsonValueKind.Object)
{
foreach (var property in overrideElement.EnumerateObject())
{
merged[property.Name] = JsonElementToObject(property.Value);
}
}
return JsonSerializer.Serialize(merged, _jsonOptions);
}
private object JsonElementToObject(JsonElement element)
{
return element.ValueKind switch
{
JsonValueKind.String => element.GetString()!,
JsonValueKind.Number => element.TryGetInt32(out var i) ? i : element.GetDouble(),
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.Null => null!,
JsonValueKind.Object => element.Deserialize<Dictionary<string, object>>(_jsonOptions)!,
JsonValueKind.Array => element.Deserialize<object[]>(_jsonOptions)!,
_ => element.ToString()
};
}
private string GetProjectConfigPath(string pluginName, string? projectPath)
{
if (string.IsNullOrEmpty(projectPath))
{
// Use current directory if no project path specified
projectPath = Directory.GetCurrentDirectory();
}
// Look for .refactorconfig directory
var configDir = Path.Combine(projectPath, ".refactorconfig");
return Path.Combine(configDir, $"{pluginName}.json");
}
private string GetUserConfigPath(string pluginName)
{
var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var configDir = Path.Combine(userProfile, ".refactorconfig");
return Path.Combine(configDir, $"{pluginName}.json");
}
private string GetGlobalConfigPath(string pluginName)
{
var globalConfigDir = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
var refactorConfigDir = Path.Combine(globalConfigDir, "MarketAlly", "RefactorConfig");
return Path.Combine(refactorConfigDir, $"{pluginName}.json");
}
private string GetCacheKey(string pluginName, string? projectPath, Type configType)
{
return $"{GetCacheKeyPrefix(pluginName, projectPath)}:{configType.Name}";
}
private string GetCacheKeyPrefix(string pluginName, string? projectPath)
{
var normalizedProjectPath = projectPath ?? "global";
return $"{pluginName}:{normalizedProjectPath}";
}
public void Dispose()
{
_cacheLock?.Dispose();
}
}
// Example configuration classes for the refactoring plugins
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum AnalysisDepth
{
Basic,
Detailed,
Comprehensive
}
public class RefactoringConfiguration
{
public CodeAnalysisConfiguration CodeAnalysis { get; set; } = new();
public FormattingConfiguration Formatting { get; set; } = new();
public DocumentationConfiguration Documentation { get; set; } = new();
public NamingConfiguration Naming { get; set; } = new();
public ExclusionsConfiguration Exclusions { get; set; } = new();
public PerformanceConfiguration Performance { get; set; } = new();
}
public class CodeAnalysisConfiguration
{
public int ComplexityThreshold { get; set; } = 10;
public int MaxMethodLength { get; set; } = 50;
public int MaxClassSize { get; set; } = 500;
public AnalysisDepth AnalysisDepth { get; set; } = AnalysisDepth.Detailed;
public List<string> EnabledRules { get; set; } = new() { "long-method", "god-class", "duplicate-code" };
public List<string> DisabledRules { get; set; } = new();
public bool IncludeComplexity { get; set; } = true;
public bool IncludeCodeSmells { get; set; } = true;
public bool IncludeSuggestions { get; set; } = true;
}
public class FormattingConfiguration
{
public string Style { get; set; } = "microsoft";
public int IndentationSize { get; set; } = 4;
public int MaxLineLength { get; set; } = 120;
public bool OrganizeUsings { get; set; } = true;
public bool RemoveUnnecessary { get; set; } = true;
public bool FixIndentation { get; set; } = true;
public bool CreateBackup { get; set; } = true;
}
public class DocumentationConfiguration
{
public string Style { get; set; } = "intelligent";
public bool IncludeExamples { get; set; } = false;
public bool IncludeSeeAlso { get; set; } = false;
public bool ApiDocFormat { get; set; } = false;
public string DocumentationScope { get; set; } = "public";
public bool GenerateFileHeaders { get; set; } = false;
public string FileHeaderTemplate { get; set; } = string.Empty;
}
public class NamingConfiguration
{
public string Convention { get; set; } = "pascal";
public bool CheckMeaningfulness { get; set; } = true;
public bool AISuggestions { get; set; } = true;
public int MinimumNameLength { get; set; } = 3;
public bool CheckAbbreviations { get; set; } = true;
public List<string> ApprovedAbbreviations { get; set; } = new() { "id", "url", "uri", "html", "xml", "json" };
}
public class ExclusionsConfiguration
{
public List<string> Files { get; set; } = new() { "*.generated.cs", "*.designer.cs", "AssemblyInfo.cs" };
public List<string> Directories { get; set; } = new() { "bin/", "obj/", "packages/", ".git/", ".vs/" };
public List<string> Patterns { get; set; } = new() { "*.Test.*", "*.Tests.*" };
public List<string> Namespaces { get; set; } = new();
}
public class PerformanceConfiguration
{
public int MaxConcurrency { get; set; } = 3;
public int MaxFilesPerProject { get; set; } = 100;
public int CacheExpirationMinutes { get; set; } = 30;
public bool EnableMemoryOptimization { get; set; } = true;
public bool EnableProgressReporting { get; set; } = true;
}
// Factory for easy access
public static class ConfigurationManagerFactory
{
private static readonly Lazy<IPluginConfigurationManager> _defaultInstance =
new(() => new PluginConfigurationManager());
public static IPluginConfigurationManager Default => _defaultInstance.Value;
public static IPluginConfigurationManager Create(ILogger<PluginConfigurationManager>? logger = null)
{
return new PluginConfigurationManager(logger);
}
}
}