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 /// /// /// Design goals /// - Lightweight, testable factory that maps human-friendly display keys (e.g. "EsriJson") to /// converter factory delegates (Func). /// - Normalization uses so callers may provide /// keys with varying case, punctuation or spacing. /// /// Thread-safety and lifetime /// - Instances are effectively read-only after construction: registrations are applied in the /// constructor and stored in an internal dictionary. Concurrent readers (Create/TryCreate) /// are safe for typical usage. Do not modify the factory's internal state after construction. /// /// Logging /// - The factory accepts an optional . When none is provided the static /// is used by default. Logging calls are best-effort and should not /// change application control-flow. /// /// Error handling /// - Use when calling code prefers not to throw; /// throws on unsupported options /// and includes a friendly suggestion when available. /// /// Testability /// - For unit tests provide a custom registrations dictionary via the constructor to avoid depending /// on built-in converters. Prefer injecting an implementation (TestLogger) /// so tests can assert emitted diagnostics without touching global logging. /// 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. /// 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 EsriJsonConverter()); AddSingle("GeoJson", () => new GeoJsonConverter()); AddSingle("GeoJsonSeq", () => new GeoJsonSeqConverter()); AddSingle("Kml", () => new KmlConverter()); AddSingle("Kmz", () => new KmzConverter()); AddSingle("Shapefile", () => new ShapefileConverter()); AddSingle("Osm", () => new OsmConverter()); AddSingle("Gpx", () => new GpxConverter()); AddSingle("Gml", () => new GmlConverter()); AddSingle("Gdb", () => new GbdConverter()); AddSingle("TopoJson", () => new TopoJsonConverter()); AddSingle("MapInfoInterchange", () => new MapInfoInterchangeConverter()); AddSingle("MapInfoTab", () => new MapInfoTabConverter()); AddSingle("Csv", () => new CsvConverter()); AddSingle("GeoPackage", () => new GeoPackageConverter()); // 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)}"); } /// /// 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; } }