Created
November 13, 2020 23:50
-
-
Save MichalBrylka/c02ed69d260b1e1b7ce5a11da28e53f3 to your computer and use it in GitHub Desktop.
RecordSettings
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() { } | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()}"); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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