ConnectionStringService
This commit is contained in:
parent
0ad39f6ae0
commit
b7631ecdd0
2 changed files with 163 additions and 0 deletions
|
|
@ -0,0 +1,73 @@
|
|||
using pgLabII.PgUtils.ConnectionStrings;
|
||||
|
||||
namespace pgLabII.PgUtils.Tests.ConnectionStrings;
|
||||
|
||||
public class ConnectionStringServiceTests
|
||||
{
|
||||
private readonly ConnectionStringService svc = ConnectionStringService.CreateDefault();
|
||||
|
||||
[Fact]
|
||||
public void DetectFormat_Url()
|
||||
{
|
||||
var r = svc.DetectFormat("postgresql://user@localhost/db");
|
||||
Assert.True(r.IsSuccess);
|
||||
Assert.Equal(ConnStringFormat.Url, r.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectFormat_Npgsql()
|
||||
{
|
||||
var r = svc.DetectFormat("Host=localhost;Database=db;Username=u");
|
||||
Assert.True(r.IsSuccess);
|
||||
Assert.Equal(ConnStringFormat.Npgsql, r.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectFormat_Libpq()
|
||||
{
|
||||
var r = svc.DetectFormat("host=localhost dbname=db user=u");
|
||||
Assert.True(r.IsSuccess);
|
||||
Assert.Equal(ConnStringFormat.Libpq, r.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseToDescriptor_DispatchesByFormat()
|
||||
{
|
||||
var r1 = svc.ParseToDescriptor("host=localhost dbname=db user=u");
|
||||
Assert.True(r1.IsSuccess);
|
||||
var r2 = svc.ParseToDescriptor("Host=localhost;Database=db;Username=u");
|
||||
Assert.True(r2.IsSuccess);
|
||||
var r3 = svc.ParseToDescriptor("postgresql://u@localhost/db");
|
||||
Assert.True(r3.IsSuccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Convert_Libpq_to_Npgsql()
|
||||
{
|
||||
var input = "host=localhost port=5433 dbname=mydb user=alice password=secret sslmode=require";
|
||||
var r = svc.Convert(input, ConnStringFormat.Npgsql);
|
||||
Assert.True(r.IsSuccess);
|
||||
var s = r.Value;
|
||||
Assert.Contains("Host=localhost", s);
|
||||
Assert.Contains("Port=5433", s);
|
||||
Assert.Contains("Database=mydb", s);
|
||||
Assert.Contains("Username=alice", s);
|
||||
Assert.Contains("Password=secret", s);
|
||||
Assert.Contains("SSL Mode=Require", s);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Convert_Url_to_Libpq()
|
||||
{
|
||||
var input = "postgresql://bob:pwd@host1:5432,host2:5433/db?application_name=cli&sslmode=prefer";
|
||||
var r = svc.Convert(input, ConnStringFormat.Libpq);
|
||||
Assert.True(r.IsSuccess);
|
||||
var s = r.Value;
|
||||
Assert.Contains("host=host1,host2", s);
|
||||
// Ports differ (5432 vs 5433), libpq formatter emits a single port only when all ports are the same
|
||||
Assert.DoesNotContain("port=", s);
|
||||
Assert.Contains("dbname=db", s);
|
||||
Assert.Contains("user=bob", s);
|
||||
Assert.Contains("sslmode=prefer", s);
|
||||
}
|
||||
}
|
||||
90
pgLabII.PgUtils/ConnectionStrings/ConnectionStringService.cs
Normal file
90
pgLabII.PgUtils/ConnectionStrings/ConnectionStringService.cs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue