2025-10-26 14:14:00 +01:00
|
|
|
|
using FluentResults;
|
2025-08-30 20:38:59 +02:00
|
|
|
|
|
|
|
|
|
|
namespace pgLabII.PgUtils.ConnectionStrings;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Composite implementation that composes multiple codecs to detect, parse, format and convert
|
|
|
|
|
|
/// PostgreSQL connection strings between formats.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public sealed class ConnectionStringService : IConnectionStringService
|
|
|
|
|
|
{
|
|
|
|
|
|
private readonly Dictionary<ConnStringFormat, IConnectionStringCodec> _codecs;
|
|
|
|
|
|
private readonly IConnectionStringCodec[] _all;
|
|
|
|
|
|
|
|
|
|
|
|
public ConnectionStringService(IEnumerable<IConnectionStringCodec> codecs)
|
|
|
|
|
|
{
|
|
|
|
|
|
_all = codecs?.ToArray() ?? Array.Empty<IConnectionStringCodec>();
|
|
|
|
|
|
_codecs = _all.GroupBy(c => c.Format).ToDictionary(g => g.Key, g => g.First());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-08-31 10:22:08 +02:00
|
|
|
|
/// Creates a service pre-configured with built-in codecs (Libpq, Npgsql, Url, Jdbc).
|
2025-08-30 20:38:59 +02:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static ConnectionStringService CreateDefault()
|
2025-08-31 10:22:08 +02:00
|
|
|
|
=> new(new IConnectionStringCodec[] { new LibpqCodec(), new NpgsqlCodec(), new UrlCodec(), new JdbcCodec() });
|
2025-08-30 20:38:59 +02:00
|
|
|
|
|
|
|
|
|
|
public Result<ConnStringFormat> DetectFormat(string input)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(input))
|
|
|
|
|
|
return Result.Fail<ConnStringFormat>("Empty input");
|
|
|
|
|
|
|
2025-08-31 10:22:08 +02:00
|
|
|
|
// URL: postgresql:// or postgres:// or JDBC jdbc:postgresql://
|
2025-08-30 20:38:59 +02:00
|
|
|
|
var trimmed = input.TrimStart();
|
2025-08-31 10:22:08 +02:00
|
|
|
|
if (trimmed.StartsWith("jdbc:postgresql://", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
{
|
|
|
|
|
|
return Result.Ok(ConnStringFormat.Jdbc);
|
|
|
|
|
|
}
|
2025-08-30 20:38:59 +02:00
|
|
|
|
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<ConnStringFormat>("Unable to detect format");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public Result<ConnectionDescriptor> ParseToDescriptor(string input)
|
|
|
|
|
|
{
|
|
|
|
|
|
var fmtRes = DetectFormat(input);
|
|
|
|
|
|
if (fmtRes.IsFailed) return Result.Fail<ConnectionDescriptor>(fmtRes.Errors);
|
|
|
|
|
|
var fmt = fmtRes.Value;
|
|
|
|
|
|
if (!_codecs.TryGetValue(fmt, out var codec))
|
|
|
|
|
|
return Result.Fail<ConnectionDescriptor>($"No codec registered for {fmt}");
|
|
|
|
|
|
return codec.TryParse(input);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public Result<string> FormatFromDescriptor(ConnectionDescriptor descriptor, ConnStringFormat targetFormat)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!_codecs.TryGetValue(targetFormat, out var codec))
|
|
|
|
|
|
return Result.Fail<string>($"No codec registered for {targetFormat}");
|
|
|
|
|
|
return codec.TryFormat(descriptor);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public Result<string> Convert(string input, ConnStringFormat targetFormat)
|
|
|
|
|
|
{
|
|
|
|
|
|
var parseRes = ParseToDescriptor(input);
|
|
|
|
|
|
if (parseRes.IsFailed) return Result.Fail<string>(parseRes.Errors);
|
|
|
|
|
|
return FormatFromDescriptor(parseRes.Value, targetFormat);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|