using FluentResults; namespace pgLabII.PgUtils.ConnectionStrings; /// /// Composite implementation that composes multiple codecs to detect, parse, format and convert /// PostgreSQL connection strings between formats. /// public sealed class ConnectionStringService : IConnectionStringService { private readonly Dictionary _codecs; private readonly IConnectionStringCodec[] _all; public ConnectionStringService(IEnumerable codecs) { _all = codecs?.ToArray() ?? Array.Empty(); _codecs = _all.GroupBy(c => c.Format).ToDictionary(g => g.Key, g => g.First()); } /// /// Creates a service pre-configured with built-in codecs (Libpq, Npgsql, Url, Jdbc). /// public static ConnectionStringService CreateDefault() => new(new IConnectionStringCodec[] { new LibpqCodec(), new NpgsqlCodec(), new UrlCodec(), new JdbcCodec() }); public Result DetectFormat(string input) { if (string.IsNullOrWhiteSpace(input)) return Result.Fail("Empty input"); // URL: postgresql:// or postgres:// or JDBC jdbc:postgresql:// var trimmed = input.TrimStart(); if (trimmed.StartsWith("jdbc:postgresql://", StringComparison.OrdinalIgnoreCase)) { return Result.Ok(ConnStringFormat.Jdbc); } if (trimmed.StartsWith("postgresql://", StringComparison.OrdinalIgnoreCase) || trimmed.StartsWith("postgres://", StringComparison.OrdinalIgnoreCase)) { return Result.Ok(ConnStringFormat.Url); } // Heuristic for Npgsql: presence of ';' separator and typical keys like Host= or Username= // Also many .NET conn strings end with a trailing semicolon but not required. if (trimmed.Contains(';')) { var firstEq = trimmed.IndexOf('='); var firstSemi = trimmed.IndexOf(';'); if (firstEq > 0 && (firstSemi > firstEq || firstSemi == -1)) { // quick key sample var key = trimmed.Substring(0, firstEq).Trim(); if (key.Length > 0) return Result.Ok(ConnStringFormat.Npgsql); } } // Fallback to libpq if it looks like space-separated key=value or single pair // Very loose heuristic: contains '=' and either a space-separated sequence or quotes if (trimmed.Contains('=')) { return Result.Ok(ConnStringFormat.Libpq); } return Result.Fail("Unable to detect format"); } public Result ParseToDescriptor(string input) { var fmtRes = DetectFormat(input); if (fmtRes.IsFailed) return Result.Fail(fmtRes.Errors); var fmt = fmtRes.Value; if (!_codecs.TryGetValue(fmt, out var codec)) return Result.Fail($"No codec registered for {fmt}"); return codec.TryParse(input); } public Result FormatFromDescriptor(ConnectionDescriptor descriptor, ConnStringFormat targetFormat) { if (!_codecs.TryGetValue(targetFormat, out var codec)) return Result.Fail($"No codec registered for {targetFormat}"); return codec.TryFormat(descriptor); } public Result Convert(string input, ConnStringFormat targetFormat) { var parseRes = ParseToDescriptor(input); if (parseRes.IsFailed) return Result.Fail(parseRes.Errors); return FormatFromDescriptor(parseRes.Value, targetFormat); } }