pgLabII/pgLabII.PgUtils/ConnectionStrings/ConnectionStringService.cs

91 lines
3.5 KiB
C#
Raw Normal View History

2025-08-30 20:38:59 +02:00
using System;
using System.Collections.Generic;
using System.Linq;
using FluentResults;
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>
/// Creates a service pre-configured with built-in codecs (Libpq, Npgsql, Url).
/// </summary>
public static ConnectionStringService CreateDefault()
=> new(new IConnectionStringCodec[] { new LibpqCodec(), new NpgsqlCodec(), new UrlCodec() });
public Result<ConnStringFormat> DetectFormat(string input)
{
if (string.IsNullOrWhiteSpace(input))
return Result.Fail<ConnStringFormat>("Empty input");
// URL: postgresql:// or postgres://
var trimmed = input.TrimStart();
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);
}
}