Skip to content

Commit

Permalink
Add support for reference to public static properties in settings
Browse files Browse the repository at this point in the history
as a value for parameters, using the syntax `NameSpace.To.ConcreteType::StaticProperty, AssemblyName`
  • Loading branch information
Thibaud DESODT committed Nov 17, 2017
1 parent f0d5d29 commit a20dce5
Show file tree
Hide file tree
Showing 13 changed files with 282 additions and 2 deletions.
1 change: 0 additions & 1 deletion src/Serilog/Configuration/LoggerSettingsConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

using System;
using System.Collections.Generic;
using System.Linq;
using Serilog.Settings.KeyValuePairs;

namespace Serilog.Configuration
Expand Down
46 changes: 46 additions & 0 deletions src/Serilog/Settings/KeyValuePairs/SettingValueConversions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,17 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;

namespace Serilog.Settings.KeyValuePairs
{
class SettingValueConversions
{
// should match "The.NameSpace.TypeName::MemberName" optionnally followed by
// usual assembly qualifiers like :
// ", MyAssembly, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
static Regex StaticMemberAccessorRegex = new Regex("^(?<shortTypeName>[^:]+)::(?<memberName>[A-Za-z][A-Za-z0-9]*)(?<typeNameExtraQualifiers>[^:]*)$");

static Dictionary<Type, Func<string, object>> ExtendedTypeConversions = new Dictionary<Type, Func<string, object>>
{
{ typeof(Uri), s => new Uri(s) },
Expand All @@ -29,6 +35,22 @@ class SettingValueConversions

public static object ConvertToType(string value, Type toType)
{
if (TryParseStaticMemberAccessor(value, out var accessorTypeName, out var memberName))
{
var accessorType = Type.GetType(accessorTypeName, throwOnError: true);
var publicStaticPropertyInfo = accessorType.GetTypeInfo().DeclaredProperties
.Where(x => x.Name == memberName)
.Where(x => x.GetMethod != null)
.Where(x => x.GetMethod.IsPublic)
.FirstOrDefault(x => x.GetMethod.IsStatic);

if (publicStaticPropertyInfo == null)
{
throw new InvalidOperationException($"Could not find public static property `{memberName}` on type `{accessorTypeName}`");
}
return publicStaticPropertyInfo.GetValue(null); // static property, no instance to pass
}

var toTypeInfo = toType.GetTypeInfo();
if (toTypeInfo.IsGenericType && toType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
Expand Down Expand Up @@ -72,5 +94,29 @@ public static object ConvertToType(string value, Type toType)

return Convert.ChangeType(value, toType);
}

internal static bool TryParseStaticMemberAccessor(string input, out string accessorTypeName, out string memberName)
{
if (input == null)
{
accessorTypeName = null;
memberName = null;
return false;
}
if (StaticMemberAccessorRegex.IsMatch(input))
{
var match = StaticMemberAccessorRegex.Match(input);
var shortAccessorTypeName = match.Groups["shortTypeName"].Value;
var rawMemberName = match.Groups["memberName"].Value;
var extraQualifiers = match.Groups["typeNameExtraQualifiers"].Value;

memberName = rawMemberName.Trim();
accessorTypeName = shortAccessorTypeName.Trim() + extraQualifiers.TrimEnd();
return true;
}
accessorTypeName = null;
memberName = null;
return false;
}
}
}
38 changes: 38 additions & 0 deletions test/Serilog.Tests/Settings/KeyValuePairSettingsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
using Serilog.Configuration;
using Serilog.Core;
using Serilog.Formatting;
using TestDummies.Console;
using TestDummies.Console.Themes;

namespace Serilog.Tests.Settings
{
Expand Down Expand Up @@ -343,5 +345,41 @@ public void LoggingLevelSwitchCanBeUsedForMinimumLevelOverrides()
// ReSharper restore HeuristicUnreachableCode
}

[Fact]
public void SinksWithAbstractParamsAreConfiguredWithTypeName()
{
var settings = new Dictionary<string, string>
{
["using:TestDummies"] = typeof(DummyLoggerConfigurationExtensions).GetTypeInfo().Assembly.FullName,
["write-to:DummyConsole.theme"] = "Serilog.Tests.Support.CustomConsoleTheme, Serilog.Tests"
};

DummyConsoleSink.Theme = null;

new LoggerConfiguration()
.ReadFrom.KeyValuePairs(settings)
.CreateLogger();

Assert.NotNull(DummyConsoleSink.Theme);
Assert.IsType<CustomConsoleTheme>(DummyConsoleSink.Theme);
}

[Fact]
public void SinksAreConfiguredWithStaticMember()
{
var settings = new Dictionary<string, string>
{
["using:TestDummies"] = typeof(DummyLoggerConfigurationExtensions).GetTypeInfo().Assembly.FullName,
["write-to:DummyConsole.theme"] = "TestDummies.Console.Themes.ConsoleThemes::Theme1, TestDummies"
};

DummyConsoleSink.Theme = null;

new LoggerConfiguration()
.ReadFrom.KeyValuePairs(settings)
.CreateLogger();

Assert.Equal(ConsoleThemes.Theme1, DummyConsoleSink.Theme);
}
}
}
95 changes: 95 additions & 0 deletions test/Serilog.Tests/Settings/SettingValueConversionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,100 @@ public void TimeSpanValuesCanBeParsed(string input, int expDays, int expHours, i
Assert.IsType<TimeSpan>(actual);
Assert.Equal(expectedTimeSpan, actual);
}

[Theory]
[InlineData("My.NameSpace.Class+InnerClass::Member",
"My.NameSpace.Class+InnerClass", "Member")]
[InlineData(" TrimMe.NameSpace.Class::NeedsTrimming ",
"TrimMe.NameSpace.Class", "NeedsTrimming")]
[InlineData("My.NameSpace.Class::Member",
"My.NameSpace.Class", "Member")]
[InlineData("My.NameSpace.Class::Member, MyAssembly",
"My.NameSpace.Class, MyAssembly", "Member")]
[InlineData("My.NameSpace.Class::Member, MyAssembly, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
"My.NameSpace.Class, MyAssembly, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", "Member")]
[InlineData("Just a random string with :: in it",
null, null)]
[InlineData("Its::a::trapWithColonsAppearingTwice",
null, null)]
[InlineData("ThereIsNoMemberHere::",
null, null)]
[InlineData(null,
null, null)]
[InlineData(" " ,
null, null)]
public void TryParseStaticMemberAccessorReturnsExpectedResults(string input, string expectedAccessorType, string expectedPropertyName)
{
var actual = SettingValueConversions.TryParseStaticMemberAccessor(input,
out var actualAccessorType,
out var actualMemberName);

if (expectedAccessorType == null)
{
Assert.False(actual, $"Should not parse {input}");
}
else
{
Assert.True(actual, $"should successfully parse {input}");
Assert.Equal(expectedAccessorType, actualAccessorType);
Assert.Equal(expectedPropertyName, actualMemberName);
}
}

[Theory]
[InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::StringProperty, Serilog.Tests", typeof(string), "StringProperty")]
[InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::IntProperty, Serilog.Tests", typeof(int), 42)]
public void StaticMembersAccessorsCanBeUsedForSImpleTypes(string input, Type targetType, object expectedValue)
{
var actual = SettingValueConversions.ConvertToType(input, targetType);

Assert.IsAssignableFrom(targetType, actual);
Assert.Equal(expectedValue, actual);
}

[Theory]
[InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::InterfaceProperty, Serilog.Tests", typeof(IAmAnInterface))]
[InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::AbstractProperty, Serilog.Tests", typeof(AnAbstractClass))]
public void StaticMembersAccessorsCanBeUsedForReferenceTypes(string input, Type targetType)
{
var actual = SettingValueConversions.ConvertToType(input, targetType);

Assert.IsAssignableFrom(targetType, actual);
Assert.Equal(ConcreteImpl.Instance, actual);
}

[Theory]
// unknown type
[InlineData("Namespace.ThisIsNotAKnownType::StringProperty, Serilog.Tests", typeof(string))]
// good type name, but wrong namespace
[InlineData("Random.Namespace.ClassWithStaticAccessors::StringProperty, Serilog.Tests", typeof(string))]
// good full type name, but missing or wrong assembly
[InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::StringProperty", typeof(string))]
[InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::StringProperty, TestDummies", typeof(string))]
public void StaticAccessorOnUnknownTypeThrowsTypeLoadException(string input, Type targetType)
{
Assert.Throws<TypeLoadException>(() =>
SettingValueConversions.ConvertToType(input, targetType)
);
}

[Theory]
// unknown member
[InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::UnknownMember, Serilog.Tests", typeof(string))]
// static property exists but it's private
[InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::PrivateStringProperty, Serilog.Tests", typeof(string))]
// public member exists but it's a field
[InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::StringField, Serilog.Tests", typeof(string))]
// public property exists but it's not static
[InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::InstanceStringProperty, Serilog.Tests", typeof(string))]
public void StaticAccessorWithInvalidMemberThrowsInvalidOperationException(string input, Type targetType)
{
var exception = Assert.Throws<InvalidOperationException>(() =>
SettingValueConversions.ConvertToType(input, targetType)
);

Assert.Contains("Could not find public static property ", exception.Message);
Assert.Contains("on type `Serilog.Tests.Support.ClassWithStaticAccessors, Serilog.Tests`", exception.Message);
}
}
}
1 change: 0 additions & 1 deletion test/Serilog.Tests/Support/ClassHierarchy.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
namespace Serilog.Tests.Support
{

public abstract class DummyAbstractClass
{
}
Expand Down
8 changes: 8 additions & 0 deletions test/Serilog.Tests/Support/CustomConsoleTheme.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using TestDummies.Console.Themes;

namespace Serilog.Tests.Support
{
class CustomConsoleTheme : ConsoleTheme
{
}
}
35 changes: 35 additions & 0 deletions test/Serilog.Tests/Support/StaticAccessorClasses.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@

namespace Serilog.Tests.Support
{
public interface IAmAnInterface
{

}

public abstract class AnAbstractClass
{

}

class ConcreteImpl : AnAbstractClass, IAmAnInterface
{
ConcreteImpl()
{

}

public static ConcreteImpl Instance { get; } = new ConcreteImpl();
}

public class ClassWithStaticAccessors
{
public static string StringProperty => nameof(StringProperty);
public static int IntProperty => 42;
public static IAmAnInterface InterfaceProperty => ConcreteImpl.Instance;
public static AnAbstractClass AbstractProperty => ConcreteImpl.Instance;
// ReSharper disable once UnusedMember.Local
static string PrivateStringProperty => nameof(PrivateStringProperty);
public string InstanceStringProperty => nameof(InstanceStringProperty);
public static string StringField = nameof(StringField);
}
}
24 changes: 24 additions & 0 deletions test/TestDummies/Console/DummyConsoleSink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using Serilog.Core;
using Serilog.Events;
using TestDummies.Console.Themes;

namespace TestDummies.Console
{
public class DummyConsoleSink : ILogEventSink
{
public DummyConsoleSink(ConsoleTheme theme = null)
{
Theme = theme ?? ConsoleTheme.None;
}

[ThreadStatic]
public static ConsoleTheme Theme;

public void Emit(LogEvent logEvent)
{
}
}

}

6 changes: 6 additions & 0 deletions test/TestDummies/Console/Themes/ConcreteConsoleTheme.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace TestDummies.Console.Themes
{
class ConcreteConsoleTheme : ConsoleTheme
{
}
}
7 changes: 7 additions & 0 deletions test/TestDummies/Console/Themes/ConsoleTheme.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace TestDummies.Console.Themes
{
public abstract class ConsoleTheme
{
public static ConsoleTheme None { get; } = new EmptyConsoleTheme();
}
}
7 changes: 7 additions & 0 deletions test/TestDummies/Console/Themes/ConsoleThemes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace TestDummies.Console.Themes
{
public static class ConsoleThemes
{
public static ConsoleTheme Theme1 { get; } = new ConcreteConsoleTheme();
}
}
6 changes: 6 additions & 0 deletions test/TestDummies/Console/Themes/EmptyConsoleTheme.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace TestDummies.Console.Themes
{
class EmptyConsoleTheme : ConsoleTheme
{
}
}
10 changes: 10 additions & 0 deletions test/TestDummies/DummyLoggerConfigurationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using Serilog.Formatting;
using Serilog.Configuration;
using Serilog.Core;
using TestDummies.Console;
using TestDummies.Console.Themes;

namespace TestDummies
{
Expand Down Expand Up @@ -51,6 +53,14 @@ public static LoggerConfiguration DummyWithLevelSwitch(
return loggerSinkConfiguration.Sink(new DummyWithLevelSwitchSink(controlLevelSwitch), restrictedToMinimumLevel);
}

public static LoggerConfiguration DummyConsole(
this LoggerSinkConfiguration loggerSinkConfiguration,
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
ConsoleTheme theme = null)
{
return loggerSinkConfiguration.Sink(new DummyConsoleSink(theme), restrictedToMinimumLevel);
}

public static LoggerConfiguration Dummy(
this LoggerSinkConfiguration loggerSinkConfiguration,
Action<LoggerSinkConfiguration> wrappedSinkAction)
Expand Down

0 comments on commit a20dce5

Please sign in to comment.