pq connection string parsing from other project

This commit is contained in:
eelke 2024-11-24 12:48:12 +01:00
parent 9a5feb9d54
commit d803cd8003
10 changed files with 529 additions and 0 deletions

View file

@ -0,0 +1,41 @@
using pgLabII.PgUtils.ConnectionStrings;
namespace pgLabII.PgUtils.Tests.ConnectionStrings;
public class PqConnectionStringParserTests
{
private readonly UnitTestTokenizer tokenizer = new();
private const string kw = "ab";
private const string val = "cd";
public PqConnectionStringParserTests()
{
tokenizer
.AddString(kw)
.AddEquals()
.AddString(val);
}
[Fact]
public void Success()
{
var parser = new PqConnectionStringParser(tokenizer);
IDictionary<string, string> output = parser.Parse();
Assert.Single(output);
Assert.True(output.TryGetValue(kw, out string? result));
Assert.Equal(val, result);
}
[Fact]
public void StaticParse()
{
var output = PqConnectionStringParser.Parse("foo=bar");
Assert.Single(output);
Assert.True(output.TryGetValue("foo", out string? result));
Assert.Equal("bar", result);
}
// There are few tests here as this is a predictive parser and all error handling is done
// in the tokenizer
}

View file

@ -0,0 +1,85 @@
using pgLabII.PgUtils.ConnectionStrings;
namespace pgLabII.PgUtils.Tests.ConnectionStrings;
public class PqConnectionStringTokenizerTests
{
[Theory]
[InlineData("abc", "abc")]
[InlineData("abc=", "abc")]
[InlineData(" abc =", "abc")]
public void GetKeyword_Success(string input, string expected)
{
PqConnectionStringTokenizer subject = new(input);
Assert.Equal(expected, subject.GetKeyword());
}
[Theory]
[InlineData("=")]
[InlineData("")]
[InlineData(" ")]
public void GetKeyword_Throws(string input)
{
PqConnectionStringTokenizer subject = new(input);
Assert.Throws<PqConnectionStringParserException>(() => subject.GetKeyword());
}
[Theory]
[InlineData("", true)]
[InlineData(" ", true)]
[InlineData(" \t", true)]
[InlineData("d", false)]
[InlineData("=", false)]
[InlineData(".", false)]
public void Eof(string input, bool expected)
{
PqConnectionStringTokenizer subject = new(input);
Assert.Equal(expected, subject.Eof);
}
[Theory]
[InlineData("=")]
[InlineData("=test")]
[InlineData(" = ")]
public void ConsumeEquals_Success(string input)
{
PqConnectionStringTokenizer subject = new(input);
subject.ConsumeEquals();
}
[Theory]
[InlineData("")]
[InlineData("t")]
[InlineData(" test")]
[InlineData(" ")]
public void ConsumeEquals_Throws(string input)
{
PqConnectionStringTokenizer subject = new(input);
Assert.Throws<PqConnectionStringParserException>(() => subject.ConsumeEquals());
}
[Theory]
[InlineData("foo", "foo")]
[InlineData("foo ", "foo")]
[InlineData("foo=", "foo=")]
[InlineData("1.2.3.4", "1.2.3.4")]
[InlineData(@"'foo \'bar' ", "foo 'bar")]
public void GetValue_Success(string input, string expected)
{
PqConnectionStringTokenizer subject = new(input);
Assert.Equal(expected, subject.GetValue());
}
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData("'d")]
[InlineData(@"'\d'")]
public void GetValue_Throws(string input)
{
PqConnectionStringTokenizer subject = new(input);
Assert.Throws<PqConnectionStringParserException>(() => subject.GetValue());
}
}

View file

@ -0,0 +1,9 @@
namespace pgLabII.PgUtils.Tests.ConnectionStrings;
internal enum PqToken
{
String,
Equals,
Eof,
Exception
}

View file

@ -0,0 +1,84 @@
using pgLabII.PgUtils.ConnectionStrings;
namespace pgLabII.PgUtils.Tests.ConnectionStrings;
internal class UnitTestTokenizer : IPqConnectionStringTokenizer
{
private readonly struct Elem(PqToken result, object? output)
{
public readonly PqToken Result = result;
public readonly object? Output = output;
}
private readonly List<Elem> _tokens = [];
private int _position = 0;
public UnitTestTokenizer AddString(string? output)
{
_tokens.Add(new(PqToken.String, output));
return this;
}
public UnitTestTokenizer AddException(Exception? output)
{
_tokens.Add(new(PqToken.Exception, output));
return this;
}
public UnitTestTokenizer AddEquals()
{
_tokens.Add(new(PqToken.Equals, null));
return this;
}
// note we do no whitespace at end tests here
public bool Eof => _position >= _tokens.Count;
public string GetKeyword()
{
EnsureNotEof();
var elem = Consume();
if (elem.Result == PqToken.String)
return (string)elem.Output!;
throw new Exception("Unexpected call to GetKeyword");
}
public void ConsumeEquals()
{
EnsureNotEof();
var elem = Consume();
if (elem.Result == PqToken.Equals)
return;
throw new Exception("Unexpected call to ConsumeEquals");
}
public string GetValue()
{
EnsureNotEof();
var elem = Consume();
if (elem.Result == PqToken.String)
return (string)elem.Output!;
throw new Exception("Unexpected call to GetValue");
}
private Elem Consume()
{
var elem = _tokens[_position++];
if (elem.Result == PqToken.Exception)
{
throw (Exception)elem.Output!;
}
return elem;
}
private void EnsureNotEof()
{
if (Eof)
throw new Exception("unexpected eof in test, wrong parser call?");
}
}

View file

@ -0,0 +1,81 @@
namespace pgLabII.PgUtils.Tests.ConnectionStrings.Util;
public class UnitTestTokenizerTests
{
private readonly UnitTestTokenizer _sut = new();
[Fact]
public void Eof_True()
{
Assert.True(_sut.Eof);
}
[Fact]
public void Eof_False()
{
_sut.AddString("a");
Assert.False(_sut.Eof);
}
[Fact]
public void GetKeyword_Success()
{
_sut.AddString("a");
Assert.Equal("a", _sut.GetKeyword());
}
[Fact]
public void GetKeyword_Unexpected_Throws()
{
_sut.AddEquals();
Assert.Throws<Exception>(() => _sut.GetKeyword());
}
[Fact]
public void GetKeyword_SimulatesException()
{
_sut.AddException(new ArgumentNullException());
Assert.Throws<ArgumentNullException>(() => _sut.GetKeyword());
}
[Fact]
public void GetValue_Success()
{
_sut.AddString("a");
Assert.Equal("a", _sut.GetValue());
}
[Fact]
public void GetValue_Unexpected_Throws()
{
_sut.AddEquals();
Assert.Throws<Exception>(() => _sut.GetValue());
}
[Fact]
public void GetValue_SimulatesException()
{
_sut.AddException(new ArgumentNullException());
Assert.Throws<ArgumentNullException>(() => _sut.GetValue());
}
[Fact]
public void ConsumeEquals_Success()
{
_sut.AddEquals();
_sut.ConsumeEquals();
}
[Fact]
public void ConsumeEquals_Unexpected_Throws1()
{
Assert.Throws<Exception>(() => _sut.ConsumeEquals());
}
[Fact]
public void ConsumeEquals_Unexpected_Throws2()
{
_sut.AddString("t");
Assert.Throws<Exception>(() => _sut.ConsumeEquals());
}
}

View file

@ -0,0 +1,7 @@
namespace pgLabII.PgUtils.ConnectionStrings;
public readonly record struct PgConfigMapping(
string pqKeyword,
string? pqDumpParam,
string? pqEnvironment
);

View file

@ -0,0 +1,10 @@
namespace pgLabII.PgUtils.ConnectionStrings;
public interface IPqConnectionStringTokenizer
{
bool Eof { get; }
//PqToken NextToken(out string? text);
string GetKeyword();
void ConsumeEquals();
string GetValue();
}

View file

@ -0,0 +1,78 @@
using System.Collections.ObjectModel;
namespace pgLabII.PgUtils.ConnectionStrings;
public class PqConnectionStringParser
{
// Note possible keywords
// host
//hostaddr
//port
//dbname
//user
//password
//passfile
//channel_binding
//connect_timeout
//client_encoding
//options
//application_name
//fallback_application_name
//keepalives
//keepalives_idle
//keepalives_interval
//keepalives_count
//tcp_user_timeout
//replication
//gssencmode
//sslmode
//requiresll
//sslcompression
//sslcert
//sslkey
//sslpassword
//sslrootcert
//sslcrl
//sslcrldir
//sslsni
//requirepeer
//ssl_min_protocol_version
//ssl_max_protocol_version
//krbsrvname
//gsslib
//service
//target_session_attrs
public static IDictionary<string, string> Parse(string input)
{
return new PqConnectionStringParser(
new PqConnectionStringTokenizer(input)
).Parse();
}
private readonly IPqConnectionStringTokenizer tokenizer;
private readonly Dictionary<string, string> result = new();
public PqConnectionStringParser(IPqConnectionStringTokenizer tokenizer)
{
this.tokenizer = tokenizer;
}
public IDictionary<string, string> Parse()
{
result.Clear();
while (!tokenizer.Eof)
ParsePair();
return result;
}
private void ParsePair()
{
string kw = tokenizer.GetKeyword();
tokenizer.ConsumeEquals();
string v = tokenizer.GetValue();
result.Add(kw, v);
}
}

View file

@ -0,0 +1,16 @@
namespace pgLabII.PgUtils.ConnectionStrings;
public class PqConnectionStringParserException : Exception
{
public PqConnectionStringParserException()
{
}
public PqConnectionStringParserException(string? message) : base(message)
{
}
public PqConnectionStringParserException(string? message, Exception? innerException) : base(message, innerException)
{
}
}

View file

@ -0,0 +1,118 @@
using System.Text;
using static System.Net.Mime.MediaTypeNames;
namespace pgLabII.PgUtils.ConnectionStrings;
public class PqConnectionStringTokenizer : IPqConnectionStringTokenizer
{
private readonly string input;
private int position = 0;
public bool Eof
{
get
{
ConsumeWhitespace();
return position >= input.Length;
}
}
public PqConnectionStringTokenizer(string input)
{
this.input = input;
position = 0;
}
public string GetKeyword()
{
if (Eof)
throw new PqConnectionStringParserException($"Unexpected end of file was expecting a keyword at position {position}");
return GetString(forKeyword: true);
}
public void ConsumeEquals()
{
ConsumeWhitespace();
if (position < input.Length && input[position] == '=')
{
position++;
}
else
throw new PqConnectionStringParserException($"Was expecting '=' after keyword at position {position}");
}
public string GetValue()
{
if (Eof)
throw new PqConnectionStringParserException($"Unexpected end of file was expecting a keyword at position {position}");
return GetString(forKeyword: false);
}
private string GetString(bool forKeyword)
{
if (forKeyword && input[position] == '=')
throw new PqConnectionStringParserException($"Unexpected '=' was expecting keyword at position {position}");
if (input[position] == '\'')
return ParseQuotedText();
return UnquotedString(forKeyword);
}
private void ConsumeWhitespace()
{
while (position < input.Length && char.IsWhiteSpace(input[position]))
position++;
}
private string UnquotedString(bool forKeyword)
{
int start = position;
while (++position < input.Length && !char.IsWhiteSpace(input[position]) && (!forKeyword || input[position] != '='))
{ }
return input.Substring(start, position - start);
}
private string ParseQuotedText()
{
bool escape = false;
StringBuilder sb = new();
int start = position;
while (++position < input.Length)
{
char c = input[position];
if (escape)
{
switch (c)
{
case '\'':
case '\\':
sb.Append(c);
escape = false;
break;
default:
throw new PqConnectionStringParserException($"Invalid escape sequence at position {position}");
}
}
else
{
if (c == '\'')
{
++position;
return sb.ToString();
}
else if (c == '\\')
{
escape = true;
}
else
{
sb.Append(c);
}
}
}
throw new PqConnectionStringParserException($"Missing end quote on value starting at {start}");
}
}