Skip to content

Instantly share code, notes, and snippets.

@MichalBrylka
Created November 13, 2020 23:50
Show Gist options
  • Save MichalBrylka/c02ed69d260b1e1b7ce5a11da28e53f3 to your computer and use it in GitHub Desktop.
Save MichalBrylka/c02ed69d260b1e1b7ce5a11da28e53f3 to your computer and use it in GitHub Desktop.
RecordSettings
using System;
namespace RecordSettings
{
/// <summary>
/// Collection parsing settings
/// </summary>
/// <remarks>For performance reasons, all delimiters and escaped characters are single chars. This makes a parsing grammar to conform LL1 rules and is very beneficial to overall parsing performance </remarks>
/// <param name="DefaultCapacity">Capacity used for creating initial collection/list/array. Use no value (null) to calculate capacity each time based on input</param>
public abstract record CollectionSettingsBase(char ListDelimiter, char NullElementMarker, char EscapingSequenceStart, char? Start, char? End, byte? DefaultCapacity) : ISettings
{
public override string ToString() => $"{Start}Item1{ListDelimiter}Item2{ListDelimiter}…{ListDelimiter}ItemN{End} escaped by '{EscapingSequenceStart}', null marked by '{NullElementMarker}'";
public void Validate()
{
if (ListDelimiter == NullElementMarker ||
ListDelimiter == EscapingSequenceStart ||
ListDelimiter == Start ||
ListDelimiter == End ||
NullElementMarker == EscapingSequenceStart ||
NullElementMarker == Start ||
NullElementMarker == End ||
EscapingSequenceStart == Start ||
EscapingSequenceStart == End
)
throw new ArgumentException($@"{nameof(CollectionSettingsBase)} requires unique characters to be used for parsing/formatting purposes.
Start ('{Start}') and end ('{End}') can be equal to each other");
}
public int GetCapacity(in ReadOnlySpan<char> input)
=> DefaultCapacity ?? CountCharacters(input, ListDelimiter) + 1;
private static int CountCharacters(in ReadOnlySpan<char> input, char character)
{
var count = 0;
for (int i = input.Length - 1; i >= 0; i--)
if (input[i] == character) count++;
return count;
}
}
public sealed record CollectionSettings(char ListDelimiter, char NullElementMarker, char EscapingSequenceStart, char? Start, char? End, byte? DefaultCapacity)
: CollectionSettingsBase(ListDelimiter, NullElementMarker, EscapingSequenceStart, Start, End, DefaultCapacity)
{
public static CollectionSettings Default { get; } =
new CollectionSettings('|', '∅', '\\', null, null, null);
public override string ToString() => base.ToString();
}
public sealed record ArraySettings(char ListDelimiter, char NullElementMarker, char EscapingSequenceStart, char? Start, char? End, byte? DefaultCapacity)
: CollectionSettingsBase(ListDelimiter, NullElementMarker, EscapingSequenceStart, Start, End, DefaultCapacity)
{
public static ArraySettings Default { get; } =
new ArraySettings('|', '∅', '\\', null, null, null);
public override string ToString() => base.ToString();
}
/// <summary>
/// Dictionary parsing settings
/// </summary>
/// <remarks>For performance reasons, all delimiters and escaped characters are single chars. This makes a parsing grammar to conform LL1 rules and is very beneficial to overall parsing performance </remarks>
/// <param name="DefaultCapacity">Capacity used for creating initial collection/list/array. Use no value (null) to calculate capacity each time based on input</param>
public sealed record DictionarySettings(char DictionaryPairsDelimiter, char DictionaryKeyValueDelimiter, char NullElementMarker, char EscapingSequenceStart, char? Start,
char? End, DictionaryBehaviour Behaviour, byte? DefaultCapacity) : ISettings
{
public void Validate()
{
if (DictionaryPairsDelimiter == DictionaryKeyValueDelimiter ||
DictionaryPairsDelimiter == NullElementMarker ||
DictionaryPairsDelimiter == EscapingSequenceStart ||
DictionaryPairsDelimiter == Start ||
DictionaryPairsDelimiter == End ||
DictionaryKeyValueDelimiter == NullElementMarker ||
DictionaryKeyValueDelimiter == EscapingSequenceStart ||
DictionaryKeyValueDelimiter == Start ||
DictionaryKeyValueDelimiter == End ||
NullElementMarker == EscapingSequenceStart ||
NullElementMarker == Start ||
NullElementMarker == End ||
EscapingSequenceStart == Start ||
EscapingSequenceStart == End
)
throw new ArgumentException(
$@"{nameof(DictionarySettings)} requires unique characters to be used for parsing/formatting purposes.
Start ('{Start}') and end ('{End}') can be equal to each other");
}
public static DictionarySettings Default { get; } =
new DictionarySettings(';', '=', '∅', '\\', null, null, DictionaryBehaviour.OverrideKeys, null);
public override string ToString() =>
$"{Start}Key1{DictionaryKeyValueDelimiter}Value1{DictionaryPairsDelimiter}…{DictionaryPairsDelimiter}KeyN{DictionaryKeyValueDelimiter}ValueN{End} escaped by '{EscapingSequenceStart}', null marked by '{NullElementMarker}', created by {Behaviour}";
public int GetCapacity(in ReadOnlySpan<char> input)
=> DefaultCapacity ?? CountCharacters(input, DictionaryPairsDelimiter) + 1;
private static int CountCharacters(in ReadOnlySpan<char> input, char character)
{
var count = 0;
for (int i = input.Length - 1; i >= 0; i--)
if (input[i] == character) count++;
return count;
}
}
public enum DictionaryBehaviour : byte
{
OverrideKeys,
DoNotOverrideKeys,
ThrowOnDuplicate
}
}
namespace RecordSettings
{
public sealed record EnumSettings(bool CaseInsensitive, bool AllowParsingNumerics) : ISettings
{
public static EnumSettings Default { get; } = new EnumSettings(true, true);
public override string ToString() => $"Value{(CaseInsensitive ? "≡" : "≠")}vAluE ; Text {(AllowParsingNumerics ? "and" : "but no")} №";
public void Validate() { }
}
}
using System;
namespace RecordSettings
{
public sealed record FactoryMethodSettings(string FactoryMethodName, string EmptyPropertyName, string NullPropertyName) : ISettings
{
public static FactoryMethodSettings Default { get; } =
new FactoryMethodSettings("FromText", "Empty", "Null");
public override string ToString() => $"Parsed by \"{FactoryMethodName}\" Empty: \"{EmptyPropertyName}\" Null: \"{NullPropertyName}\"";
public void Validate()
{
if (string.IsNullOrEmpty(FactoryMethodName))
throw new ArgumentException("FactoryMethodName cannot be null or empty.", nameof(FactoryMethodName));
if (string.IsNullOrEmpty(EmptyPropertyName))
throw new ArgumentException("EmptyPropertyName cannot be null or empty.", nameof(EmptyPropertyName));
if (string.IsNullOrEmpty(NullPropertyName))
throw new ArgumentException("NullPropertyName cannot be null or empty.", nameof(NullPropertyName));
}
}
}
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using Nemesis.Essentials.Runtime;
namespace RecordSettings
{
class Program
{
static void Main()
{
var ss = Sut.Create();
}
}
public interface ISettings
{
void Validate();
}
public sealed class SettingsStoreBuilder
{
private readonly IDictionary<Type, ISettings> _settings;
[SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
public SettingsStoreBuilder(IEnumerable<ISettings> settings = null)
{
if (settings is not null)
foreach (var s in settings) s.Validate();
_settings = settings?.ToDictionary(s => s.GetType()) ?? new Dictionary<Type, ISettings>();
}
public static SettingsStoreBuilder GetDefault(Assembly fromAssembly = null)
{
const BindingFlags PUB_STAT_FLAGS = BindingFlags.Public | BindingFlags.Static;
var types = (fromAssembly ?? Assembly.GetExecutingAssembly())
.GetTypes()
.Where(t => !t.IsAbstract && !t.IsInterface && !t.IsGenericType && !t.IsGenericTypeDefinition &&
typeof(ISettings).IsAssignableFrom(t)
);
var defaultInstances = types
.Select(t => t.GetProperty("Default", PUB_STAT_FLAGS) is { } defaultProperty &&
typeof(ISettings).IsAssignableFrom(defaultProperty.PropertyType)
? (ISettings)defaultProperty.GetValue(null)
: throw new NotSupportedException(
$"Automatic settings store builder supports {nameof(ISettings)} instances with public static property named 'Default' assignable to {nameof(ISettings)}")
);
return new SettingsStoreBuilder(defaultInstances);
}
public TSettings GetSettingsFor<TSettings>() where TSettings : ISettings =>
_settings.TryGetValue(typeof(TSettings), out var s)
? (TSettings)s
: throw new NotSupportedException($"No settings registered for {typeof(TSettings).GetFriendlyName()}");
public SettingsStoreBuilder AddOrUpdate<TSettings>([JetBrains.Annotations.NotNull] TSettings settings) where TSettings : ISettings
{
settings.Validate();
_settings[settings.GetType()] =
settings ?? throw new ArgumentNullException(nameof(settings));
return this;
}
public SettingsStore Build() =>
new SettingsStore(new ReadOnlyDictionary<Type, ISettings>(_settings));
}
public sealed class SettingsStore
{
private readonly IReadOnlyDictionary<Type, ISettings> _settings;
public SettingsStore([JetBrains.Annotations.NotNull] IReadOnlyDictionary<Type, ISettings> settings)
{
foreach (var s in settings.Values) s.Validate();
_settings = settings;
}
public TSettings GetSettingsFor<TSettings>() where TSettings : ISettings =>
(TSettings)GetSettingsFor(typeof(TSettings));
public ISettings GetSettingsFor(Type settingsType) =>
_settings.TryGetValue(settingsType, out var s)
? s
: throw new NotSupportedException($"No settings registered for {settingsType.GetFriendlyName()}");
}
}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2020.1.0" />
<PackageReference Include="Nemesis.Essentials" Version="1.0.56" />
</ItemGroup>
</Project>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace RecordSettings
{
class Sut
{
public static SettingsStore Create()
{
//F# influenced settings
var borderedDictionary = DictionarySettings.Default with { Start = '{', End = '}', DictionaryKeyValueDelimiter = ',' };
var borderedCollection = CollectionSettings.Default with { Start = '[', End = ']', ListDelimiter = ';' };
var borderedArray = ArraySettings.Default with { Start = '|', End = '|', ListDelimiter = ',' };
var weirdTuple = ValueTupleSettings.Default with { Start = '/', End = '/', Delimiter = '⮿', NullElementMarker = '␀' };
var builder = SettingsStoreBuilder.GetDefault()
.AddOrUpdate(borderedArray)
.AddOrUpdate(borderedCollection)
.AddOrUpdate(borderedDictionary)
.AddOrUpdate(weirdTuple)
;
return builder.Build();
}
}
}
using System;
namespace RecordSettings
{
public abstract record TupleSettingsBase(char Delimiter, char NullElementMarker, char EscapingSequenceStart, char? Start, char? End) : ISettings
{
public override string ToString() =>
$"{Start}Item1{Delimiter}Item2{Delimiter}…{Delimiter}ItemN{End} escaped by '{EscapingSequenceStart}', null marked by '{NullElementMarker}'";
public void Validate()
{
if (Delimiter == NullElementMarker ||
Delimiter == EscapingSequenceStart ||
Delimiter == Start ||
Delimiter == End ||
NullElementMarker == EscapingSequenceStart ||
NullElementMarker == Start ||
NullElementMarker == End ||
EscapingSequenceStart == Start ||
EscapingSequenceStart == End
)
throw new ArgumentException($@"{nameof(TupleSettingsBase)} requires unique characters to be used for parsing/formatting purposes.
Start ('{Start}') and end ('{End}') can be equal to each other");
}
//public TupleHelper ToTupleHelper() => new TupleHelper(Delimiter, NullElementMarker, EscapingSequenceStart, Start, End);
}
public sealed record ValueTupleSettings(char Delimiter, char NullElementMarker, char EscapingSequenceStart, char? Start, char? End)
: TupleSettingsBase(Delimiter, NullElementMarker, EscapingSequenceStart, Start, End)
{
public static ValueTupleSettings Default { get; } = new ValueTupleSettings(',', '∅', '\\', '(', ')');
public override string ToString() => base.ToString();
}
public sealed record KeyValuePairSettings(char Delimiter, char NullElementMarker, char EscapingSequenceStart, char? Start, char? End)
: TupleSettingsBase(Delimiter, NullElementMarker, EscapingSequenceStart, Start, End)
{
public static KeyValuePairSettings Default { get; } = new KeyValuePairSettings('=', '∅', '\\', null, null);
public override string ToString() => $"{Start}Key{Delimiter}Value{End} escaped by '{EscapingSequenceStart}', null marked by '{NullElementMarker}'";
}
public sealed record DeconstructableSettings(char Delimiter, char NullElementMarker, char EscapingSequenceStart, char? Start, char? End, bool UseDeconstructableEmpty)
: TupleSettingsBase(Delimiter, NullElementMarker, EscapingSequenceStart, Start, End)
{
public static DeconstructableSettings Default { get; } =
new DeconstructableSettings(';', '∅', '\\', '(', ')', true);
public override string ToString() =>
$@"{base.ToString()}. {(UseDeconstructableEmpty ? "With" : "Without")} deconstructable empty generator.";
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment