using GitConverter.Lib.Converters; using GitConverter.Lib.Logging; using System; using System.Collections.Generic; using System.Linq; namespace GitConverter.Lib.Factories { /// /// Registry-based, non-static converter factory. /// - Normalizes CLI input keys (flexible matching) /// - Provides TryCreate to avoid throwing from CLI flows /// - Offers suggestions for close matches /// /// /// /// ConverterFactory maps human-friendly display keys (for example "Shapefile" or "GeoJson") /// to factory delegates that produce concrete instances. It is intended /// to be a lightweight, testable component used by the ConsoleApp and higher-level services to /// resolve converters based on user-supplied option strings. /// /// /// Key normalization /// - Input keys are normalized using so callers /// can supply options in a forgiving manner (different case, extra punctuation, spacing or aliases). /// - Registration keys may include comma-separated aliases (for example "Kml,Kmz") when constructed /// via the registrations dictionary overload. /// /// /// Registration model & lifetime /// - Registrations are applied in the constructor and stored in an internal, case-insensitive /// dictionary keyed by the normalized key. After construction the instance is effectively /// read-only and safe for concurrent readers. Do not attempt to mutate the internal registry /// after construction; for dynamic scenarios create a new factory instance. /// - The default constructor registers the built-in converters used by the project. For unit /// tests prefer the overload that accepts an explicit registrations dictionary so tests can /// register small, deterministic factories (e.g., fakes) and avoid instantiating heavy /// dependencies like Aspose drivers. /// /// /// Thread-safety /// - The factory is safe for concurrent Create/TryCreate calls. Registration operations occur /// only during construction. The internal logger is expected to be thread-safe. /// /// /// Error handling and API choices /// - is a non-throwing operation intended for /// CLI flows and tests where callers prefer to handle unsupported options gracefully. /// - is a convenience that throws a /// when the option is unsupported. When a close match is available the exception message /// includes a suggestion to improve UX. /// - Instantiation exceptions thrown by factory delegates are caught in /// and logged at Warn level; the method returns false so callers can decide how to surface /// the error to users. /// /// /// Logging /// - The factory accepts an optional . When none is provided it falls /// back to the static . Logging is best-effort — diagnostic calls /// should not affect control flow. /// - The constructor logs each registration at Debug level and emits an Info-level summary of /// registered options. Lookup misses, instantiation failures and warnings are logged at /// Debug/Warn levels to aid troubleshooting in tests and production. /// /// /// Testing guidance /// - Provide a small registrations dictionary and a test logger to isolate unit tests from /// heavy converter implementations. Use to assert the /// available display keys when verifying help text. /// - Tests that exercise suggestion logic can assert the message produced by /// when a close match exists. /// /// /// Example usage /// /// var factory = new ConverterFactory(); /// if (factory.TryCreate("Shapefile", out var converter)) /// { /// var result = converter.Convert(...); /// } /// else /// { /// // Present usage / suggestion to user /// } /// /// /// public class ConverterFactory : IConverterFactory { private readonly IDictionary> _registry; private readonly IReadOnlyCollection _displayKeys; private readonly IAppLogger _logger; /// /// Default ctor registers built-in converters. /// For testability you can use . /// /// /// - Registers the library's built-in converters (EsriJson, GeoJson, Kml, etc.). /// - Uses the default logger () when no logger is supplied. /// - For unit tests prefer passing a custom registrations dictionary to avoid creating real converters. /// public ConverterFactory() : this(null, null) { } /// /// Construct factory with optional external registrations (displayKey -> factory) and optional logger. /// Keys will be normalized; a key may contain comma-separated aliases. /// /// /// Optional custom registrations. The dictionary key may contain a single display key or comma-separated aliases. /// Values are factory delegates that create an instance. /// /// Optional logger; when null the static is used. /// /// - Validation: blank display keys or null factories will throw during construction (ArgumentException/ArgumentNullException). /// - Duplicate normalized keys cause an ArgumentException after logging the duplicate attempt. /// - Useful for tests: pass a small registrations map and a TestLogger to avoid instantiating built-in converters. /// public ConverterFactory(IDictionary> registrations, IAppLogger logger = null) { // Use provided logger or fall back to static Log.Current so behaviour remains compatible. _logger = logger ?? Log.Current; _registry = new Dictionary>(StringComparer.OrdinalIgnoreCase); var displayList = new List(); void AddSingle(string displayKey, Func factory) { if (string.IsNullOrWhiteSpace(displayKey)) throw new ArgumentException("displayKey is required.", nameof(displayKey)); if (factory == null) throw new ArgumentNullException(nameof(factory)); var key = FactoryHelpers.NormalizeKey(displayKey); if (_registry.ContainsKey(key)) { // Log duplicate registration attempt before throwing _logger.Error($"ConverterFactory duplicate registration attempt. NormalizedKey='{key}', displayKey='{displayKey}'"); throw new ArgumentException($"Duplicate registration for normalized key '{key}' (display: '{displayKey}')."); } _registry[key] = factory; displayList.Add(displayKey); // Log each registration at Debug level (helpful during startup) _logger.Debug($"ConverterFactory registered '{displayKey}' (normalized='{key}')."); } void AddAliases(IEnumerable displayKeys, Func factory) { foreach (var dk in displayKeys) AddSingle(dk, factory); } // Log purpose of following built-in registrations (helps startup diagnostics). _logger.Info("ConverterFactory: registering built-in converters (display keys) - these map user-facing option names to concrete converter implementations."); // Built-in registrations (lightweight stubs or real implementations) AddSingle("EsriJson", () => new UniversalGisConverter()); AddSingle("GeoJson", () => new UniversalGisConverter()); AddSingle("GeoJsonSeq", () => new UniversalGisConverter()); AddSingle("Kml", () => new UniversalGisConverter()); AddSingle("Kmz", () => new UniversalGisConverter()); AddSingle("Shapefile", () => new UniversalGisConverter()); AddSingle("Osm", () => new UniversalGisConverter()); AddSingle("Gpx", () => new UniversalGisConverter()); AddSingle("Gml", () => new UniversalGisConverter()); AddSingle("Gdb", () => new UniversalGisConverter()); AddSingle("TopoJson", () => new UniversalGisConverter()); AddSingle("MapInfoInterchange", () => new UniversalGisConverter()); AddSingle("MapInfoTab", () => new UniversalGisConverter()); AddSingle("Csv", () => new UniversalGisConverter()); AddSingle("GeoPackage", () => new UniversalGisConverter()); // Apply external registrations (if provided). if (registrations != null) { foreach (var kv in registrations) { if (string.IsNullOrWhiteSpace(kv.Key)) continue; var aliases = kv.Key.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x)); if (!aliases.Any()) AddSingle(kv.Key, kv.Value); else AddAliases(aliases, kv.Value); } } _displayKeys = displayList.OrderBy(s => s, StringComparer.OrdinalIgnoreCase).ToList().AsReadOnly(); // Log final registration summary at Info level (one-time) _logger.Info($"ConverterFactory initialized with options: {string.Join(", ", _displayKeys)}"); _logger.Info(Environment.NewLine); } /// /// Create a converter for the given format option or throw if unsupported. /// /// User-provided option (case/format tolerant). /// An instance. /// /// - Preferred when callers expect a converter or want an exception on unsupported options. /// - When an unsupported option is provided the method will throw . /// If a close suggestion exists the exception message will include it to improve UX. /// - Logs useful diagnostics at Warn/Debug levels to assist troubleshooting. /// public IConverter Create(string formatOption) { if (string.IsNullOrWhiteSpace(formatOption)) throw new ArgumentException("formatOption is required.", nameof(formatOption)); if (!TryCreate(formatOption, out var converter)) { var suggestion = FactoryHelpers.SuggestClosest(formatOption, _displayKeys); if (suggestion != null) { // Log suggestion before throwing to aid diagnostics _logger.Warn($"Unsupported format option '{formatOption}'. Suggestion: '{suggestion}'"); throw new KeyNotFoundException($"Unsupported format option '{formatOption}'. Did you mean '{suggestion}'?"); } // Log unsupported option and available options _logger.Warn($"Unsupported format option '{formatOption}'. Supported: {string.Join(", ", _displayKeys)}"); throw new KeyNotFoundException($"Unsupported conversion option '{formatOption}'. Supported: {string.Join(", ", _displayKeys)}"); } // Successful creation - brief debug log _logger.Debug($"ConverterFactory.Create resolved '{formatOption}' to '{converter.GetType().FullName}'"); return converter; } /// /// Try to create a converter for the given option. Returns false if unsupported. /// /// User-provided option string. /// When true returned, the resolved converter instance. /// True when a converter was created; false otherwise. /// /// - Safe, non-throwing alternative to suitable for CLI flows and tests. /// - Performs normalization via and looks up the registered factory. /// - Instantiation is delegated to the stored factory delegate; any exception during instantiation is caught, /// logged at Warn level and the method returns false. /// - Use this method when callers want to handle unsupported options gracefully. /// public bool TryCreate(string formatOption, out IConverter converter) { converter = null; if (string.IsNullOrWhiteSpace(formatOption)) return false; var key = FactoryHelpers.NormalizeKey(formatOption); if (!_registry.TryGetValue(key, out var factory)) { // Debug-level lookup miss (useful to diagnose why inputs don't match) _logger.Debug($"ConverterFactory lookup miss for input='{formatOption}', normalized='{key}'"); return false; } try { converter = factory(); // Debug-level instantiation success _logger.Debug(converter != null ? $"ConverterFactory instantiated converter for '{formatOption}' ({converter.GetType().FullName})" : $"ConverterFactory factory returned null for '{formatOption}'"); return converter != null; } catch (Exception ex) { // Defensive: log and return false so caller can handle gracefully. _logger.Warn($"ConverterFactory failed to instantiate converter for '{formatOption}': {ex.Message}"); converter = null; return false; } } /// /// Returns the supported option keys (human-friendly). /// /// /// - The returned collection is ordered and read-only; suitable for displaying help text to users. /// public IReadOnlyCollection GetSupportedOptions() => _displayKeys; } }