Skip to content

Commit

Permalink
Implement connection string
Browse files Browse the repository at this point in the history
  • Loading branch information
shaan1337 committed Jul 1, 2020
1 parent 23ad2ae commit ea3cda7
Show file tree
Hide file tree
Showing 4 changed files with 555 additions and 1 deletion.
37 changes: 37 additions & 0 deletions src/EventStore.Client/ConnectionStringParseException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;

namespace EventStore.Client {
public class ConnectionStringParseException : Exception {
public ConnectionStringParseException(string message) : base(message) { }
}

public class NoSchemeException : ConnectionStringParseException {
public NoSchemeException()
: base("Could not parse scheme from connection string") { }
}

public class InvalidSchemeException : ConnectionStringParseException {
public InvalidSchemeException(string scheme, string[] supportedSchemes)
: base($"Invalid scheme: '{scheme}'. Supported values are: {string.Join(",", supportedSchemes)}") { }
}

public class InvalidUserCredentialsException : ConnectionStringParseException {
public InvalidUserCredentialsException(string userInfo)
: base($"Invalid user credentials: '{userInfo}'. Username & password must be delimited by a colon") { }
}

public class InvalidHostException : ConnectionStringParseException {
public InvalidHostException(string host)
: base($"Invalid host: '{host}'") { }
}

public class InvalidKeyValuePairException : ConnectionStringParseException {
public InvalidKeyValuePairException(string keyValuePair)
: base($"Invalid key/value pair: '{keyValuePair}'") { }
}

public class InvalidSettingException : ConnectionStringParseException {
public InvalidSettingException(string message) : base(message) { }
}

}
233 changes: 233 additions & 0 deletions src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;

namespace EventStore.Client {
public partial class EventStoreClientSettings {
public static EventStoreClientSettings Create(string connectionString) {
return ConnectionStringParser.Parse(connectionString);
}

private static class ConnectionStringParser {
private const string SchemeSeparator = "://";
private const string UserInfoSeparator = "@";
private const string Colon = ":";
private const string Slash = "/";
private const string Comma = ",";
private const string Ampersand = "&";
private const string Equal = "=";
private const string QuestionMark = "?";
private static readonly string[] Schemes = { "esdb" };
private static readonly int DefaultPort = EventStoreClientConnectivitySettings.Default.Address.Port;
private static readonly bool DefaultUseTls = true;

private static readonly Dictionary<string, Type> SettingsType = new Dictionary<string, Type> (StringComparer.InvariantCultureIgnoreCase) {
{"ConnectionName", typeof(string)},
{"MaxDiscoverAttempts", typeof(int)},
{"DiscoveryInterval", typeof(int)},
{"GossipTimeout", typeof(int)},
{"NodePreference", typeof(string)},
{"Tls", typeof(bool)},
{"TlsVerifyCert", typeof(bool)},
{"OperationTimeout", typeof(int)},
{"ThrowOnAppendFailure", typeof(bool)}
};

public static EventStoreClientSettings Parse(string connectionString) {
var currentIndex = 0;
var schemeIndex = connectionString.IndexOf(SchemeSeparator, currentIndex, StringComparison.Ordinal);
if (schemeIndex == -1)
throw new NoSchemeException();
var scheme = ParseScheme(connectionString.Substring(0, schemeIndex));

currentIndex = schemeIndex + SchemeSeparator.Length;
var userInfoIndex = connectionString.IndexOf(UserInfoSeparator, currentIndex, StringComparison.Ordinal);
(string user, string pass) userInfo = (null, null);
if (userInfoIndex != -1) {
userInfo = ParseUserInfo(connectionString.Substring(currentIndex, userInfoIndex - currentIndex));
currentIndex = userInfoIndex + UserInfoSeparator.Length;
}

var slashIndex = connectionString.IndexOf(Slash, currentIndex, StringComparison.Ordinal);
if (slashIndex == -1)
throw new ConnectionStringParseException("The connection string must contain a / (forward slash) after specifying the hosts");

var hosts = ParseHosts(connectionString.Substring(currentIndex, slashIndex - currentIndex));
currentIndex = slashIndex + Slash.Length;

var questionMarkIndex = connectionString.IndexOf(QuestionMark, currentIndex);
var options = new Dictionary<string, string>();
if (questionMarkIndex != -1) {
currentIndex = questionMarkIndex + QuestionMark.Length;
options = ParseKeyValuePairs(connectionString.Substring(currentIndex));
}

return CreateSettings(scheme, userInfo, hosts, options);
}

private static EventStoreClientSettings CreateSettings(string scheme, (string user, string pass) userInfo, EndPoint[] hosts, Dictionary<string, string> options) {
var settings = new EventStoreClientSettings {
ConnectivitySettings = EventStoreClientConnectivitySettings.Default,
OperationOptions = EventStoreClientOperationOptions.Default
};

if (userInfo != (null, null))
settings.DefaultCredentials = new UserCredentials(userInfo.user, userInfo.pass);

var typedOptions = new Dictionary<string, object>(StringComparer.InvariantCultureIgnoreCase);
foreach (var option in options) {
if (!SettingsType.TryGetValue(option.Key, out var type)) throw new InvalidSettingException($"Unknown option: {option.Key}");
if(type == typeof(int)){
if (!int.TryParse(option.Value, out var intValue))
throw new InvalidSettingException($"{option.Key} must be an integer value");
typedOptions.Add(option.Key, intValue);
} else if (type == typeof(bool)) {
if (!bool.TryParse(option.Value, out var boolValue))
throw new InvalidSettingException($"{option.Key} must be either true or false");
typedOptions.Add(option.Key, boolValue);
} else if (type == typeof(string)) {
typedOptions.Add(option.Key, option.Value);
}
}

if (typedOptions.TryGetValue("ConnectionName", out object connectionName))
settings.ConnectionName = (string) connectionName;

var connSettings = settings.ConnectivitySettings;

if (typedOptions.TryGetValue("MaxDiscoverAttempts", out object maxDiscoverAttempts))
connSettings.MaxDiscoverAttempts = (int)maxDiscoverAttempts;

if (typedOptions.TryGetValue("DiscoveryInterval", out object discoveryInterval))
connSettings.DiscoveryInterval = TimeSpan.FromSeconds((int)discoveryInterval);

if (typedOptions.TryGetValue("GossipTimeout", out object gossipTimeout))
connSettings.GossipTimeout = TimeSpan.FromSeconds((int)gossipTimeout);

if (typedOptions.TryGetValue("NodePreference", out object nodePreference)) {
var nodePreferenceLowerCase = ((string)nodePreference).ToLowerInvariant();
switch (nodePreferenceLowerCase) {
case "leader":
connSettings.NodePreference = NodePreference.Leader;
break;
case "follower":
connSettings.NodePreference = NodePreference.Follower;
break;
case "random":
connSettings.NodePreference = NodePreference.Random;
break;
case "readonlyreplica":
connSettings.NodePreference = NodePreference.ReadOnlyReplica;
break;
default:
throw new InvalidSettingException($"Invalid NodePreference: {nodePreference}");
}
}

var useTls = DefaultUseTls;
if(typedOptions.TryGetValue("Tls", out object tls)) {
useTls = (bool)tls;
}

if (typedOptions.TryGetValue("TlsVerifyCert", out object tlsVerifyCert)) {
if (!(bool)tlsVerifyCert) {
settings.CreateHttpMessageHandler = () => new SocketsHttpHandler {
SslOptions = {
RemoteCertificateValidationCallback = delegate { return true; }
}
};
}
}

if(typedOptions.TryGetValue("OperationTimeout", out object operationTimeout))
settings.OperationOptions.TimeoutAfter = TimeSpan.FromSeconds((int) operationTimeout);

if(typedOptions.TryGetValue("ThrowOnAppendFailure", out object throwOnAppendFailure))
settings.OperationOptions.ThrowOnAppendFailure = (bool) throwOnAppendFailure;

if (hosts.Length == 1) {
connSettings.Address = new Uri(hosts[0].ToHttpUrl(useTls?Uri.UriSchemeHttps:Uri.UriSchemeHttp));
} else {
if (hosts.Any(x => x is DnsEndPoint))
connSettings.DnsGossipSeeds = hosts.Select(x => new DnsEndPoint(x.GetHost(), x.GetPort())).ToArray();
else
connSettings.IpGossipSeeds = hosts.Select(x => x as IPEndPoint).ToArray();

connSettings.GossipOverHttps = useTls;
}

return settings;
}

private static string ParseScheme(string s) {
if (!Schemes.Contains(s)) throw new InvalidSchemeException(s, Schemes);
return s;
}

private static (string,string) ParseUserInfo(string s) {
var tokens = s.Split(Colon);
if (tokens.Length != 2) throw new InvalidUserCredentialsException(s);
return (tokens[0], tokens[1]);
}

private static EndPoint[] ParseHosts(string s) {
var hostsTokens = s.Split(Comma);
var hosts = new List<EndPoint>();
foreach (var hostToken in hostsTokens) {
var hostPortToken = hostToken.Split(Colon);
string host;
int port;
switch (hostPortToken.Length)
{
case 1:
host = hostPortToken[0];
port = DefaultPort;
break;
case 2:
{
host = hostPortToken[0];
if (!int.TryParse(hostPortToken[1], out port))
throw new InvalidHostException(hostToken);
break;
}
default:
throw new InvalidHostException(hostToken);
}

if (host.Length == 0) {
throw new InvalidHostException(hostToken);
}

if (IPAddress.TryParse(host, out IPAddress ip)) {
hosts.Add(new IPEndPoint(ip, port));
} else {
hosts.Add(new DnsEndPoint(host, port));
}
}

return hosts.ToArray();
}

private static Dictionary<string, string> ParseKeyValuePairs(string s) {
var options = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
var optionsTokens = s.Split(Ampersand);
foreach (var optionToken in optionsTokens) {
var (key, val) = ParseKeyValuePair(optionToken);
options[key] = val;
}
return options;
}

private static (string,string) ParseKeyValuePair(string s) {
var keyValueToken = s.Split(Equal);
if (keyValueToken.Length != 2) {
throw new InvalidKeyValuePairException(s);
}

return (keyValueToken[0], keyValueToken[1]);
}
}
}
}
2 changes: 1 addition & 1 deletion src/EventStore.Client/EventStoreClientSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

#nullable enable
namespace EventStore.Client {
public class EventStoreClientSettings {
public partial class EventStoreClientSettings {
public IEnumerable<Interceptor>? Interceptors { get; set; }
public string? ConnectionName { get; set; }
public Func<HttpMessageHandler>? CreateHttpMessageHandler { get; set; }
Expand Down
Loading

0 comments on commit ea3cda7

Please sign in to comment.