Skip to content

Commit

Permalink
fix: Prevent modifications of the default serialization options instance
Browse files Browse the repository at this point in the history
MdSerializationOptions.Default provides access to the default
serialization options. While the 'Default' field is readonly, the
individual settings of the instance could be modified because
MdSerializationOptions provides setters for all properties.

To prevent such modifications without introducing additional
abstractions the property setters will now check if it is allowed to
modify the instance and throw when a trying to set a property of the
default instance.
  • Loading branch information
ap0llo committed Oct 2, 2019
1 parent eafac27 commit 7d7f531
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using Xunit;

namespace Grynwald.MarkdownGenerator.Test
{
public class MdSerializationOptionsTest
{
public static IEnumerable<object[]> Properties()
{
foreach (var property in typeof(MdSerializationOptions).GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
yield return new[] { property.Name };
}
}

[Theory]
[MemberData(nameof(Properties))]
public void Properties_of_the_default_instance_cannot_be_modified(string propertyName)
{
// ARRANGE
var instance = MdSerializationOptions.Default;

var property = typeof(MdSerializationOptions).GetProperty(propertyName);

var testValue = GetTestValue(property.PropertyType);

// ACT / ASSERT
var exception = Assert.Throws<TargetInvocationException>(() => property.SetMethod.Invoke(instance, new[] { testValue }));
Assert.IsType<InvalidOperationException>(exception.InnerException);

// exception message should indicate which property cannot be set
Assert.Contains(propertyName, exception.InnerException.Message);
}

[Theory]
[MemberData(nameof(Properties))]
public void Properties_of_non_default_instance_can_be_modified(string propertyName)
{
// ARRANGE
var instance = new MdSerializationOptions();

var property = typeof(MdSerializationOptions).GetProperty(propertyName);

var newValue = GetTestValue(property.PropertyType);

// ACT / ASSERT
property.SetMethod.Invoke(instance, new[] { newValue });
var actualValue = property.GetMethod.Invoke(instance, Array.Empty<object>());
Assert.Equal(newValue, actualValue);
}


private object GetTestValue(Type type)
{
if(!type.IsValueType)
{
return null;
}

var defaultValue = Activator.CreateInstance(type);
if(type.IsEnum)
{
var values = Enum.GetValues(type);
if (values.Length <= 1)
return defaultValue;
else
return values.Cast<object>().First(x => !x.Equals(defaultValue));
}
else
{
return defaultValue;
}
}

}
}
93 changes: 80 additions & 13 deletions src/MarkdownGenerator/_Model/_Options/MdSerializationOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Grynwald.MarkdownGenerator
using System;

namespace Grynwald.MarkdownGenerator
{
/// <summary>
/// Encapsulates settings that control how a document is serialized.
Expand All @@ -7,45 +9,93 @@
/// as well as the Save() method of <see cref="MdDocument"/>
/// </summary>
public class MdSerializationOptions
{
//TODO: Make class immutable, prevent Default settings to be changed
{
public static readonly MdSerializationOptions Default = new MdSerializationOptions(isReadOnly: true);

private readonly bool m_IsReadOnly;

private MdEmphasisStyle m_EmphasisStyle = MdEmphasisStyle.Asterisk;
private MdThematicBreakStyle m_ThematicBreakStyle = MdThematicBreakStyle.Underscore;
private MdHeadingStyle m_HeadingStyle = MdHeadingStyle.Atx;
private MdCodeBlockStyle m_CodeBlockStyle = MdCodeBlockStyle.Backtick;
private MdBulletListStyle m_BulletListStyle = MdBulletListStyle.Dash;
private MdOrderedListStyle m_OrderedListStyle = MdOrderedListStyle.Dot;
private MdTableStyle m_TableStyle = MdTableStyle.GFM;
private int m_MaxLineLength = -1;
private ITextFormatter m_TextFormatter = DefaultTextFormatter.Instance;


public MdSerializationOptions() : this(isReadOnly: false)
{ }

private MdSerializationOptions(bool isReadOnly)
{
m_IsReadOnly = isReadOnly;
}

public static readonly MdSerializationOptions Default = new MdSerializationOptions();

/// <summary>
/// Gets or sets the style for emphasized and strongly emphasized text
/// </summary>
public MdEmphasisStyle EmphasisStyle { get; set; } = MdEmphasisStyle.Asterisk;
public MdEmphasisStyle EmphasisStyle
{
get => m_EmphasisStyle;
set => SetValue(nameof(EmphasisStyle), value, ref m_EmphasisStyle);
}

/// <summary>
/// Gets or sets the style to use for thematic breaks
/// </summary>
public MdThematicBreakStyle ThematicBreakStyle { get; set; } = MdThematicBreakStyle.Underscore;
public MdThematicBreakStyle ThematicBreakStyle
{
get => m_ThematicBreakStyle;
set => SetValue(nameof(ThematicBreakStyle), value, ref m_ThematicBreakStyle);
}

/// <summary>
/// Gets or sets the style for headings
/// </summary>
public MdHeadingStyle HeadingStyle { get; set; } = MdHeadingStyle.Atx;
public MdHeadingStyle HeadingStyle
{
get => m_HeadingStyle;
set => SetValue(nameof(HeadingStyle), value, ref m_HeadingStyle);
}

/// <summary>
/// Gets or sets the style of code blocks
/// </summary>
public MdCodeBlockStyle CodeBlockStyle { get; set; } = MdCodeBlockStyle.Backtick;
public MdCodeBlockStyle CodeBlockStyle
{
get => m_CodeBlockStyle;
set => SetValue(nameof(CodeBlockStyle), value, ref m_CodeBlockStyle);
}

/// <summary>
/// Gets or sets the style for bullet list items
/// </summary>
public MdBulletListStyle BulletListStyle { get; set; } = MdBulletListStyle.Dash;
public MdBulletListStyle BulletListStyle
{
get => m_BulletListStyle;
set => SetValue(nameof(BulletListStyle), value, ref m_BulletListStyle);
}

/// <summary>
/// Gets or sets the style for ordered list items
/// </summary>
public MdOrderedListStyle OrderedListStyle { get; set; } = MdOrderedListStyle.Dot;
public MdOrderedListStyle OrderedListStyle
{
get => m_OrderedListStyle;
set => SetValue(nameof(OrderedListStyle), value, ref m_OrderedListStyle);
}

/// <summary>
/// Gets or sets the style for tables
/// </summary>
public MdTableStyle TableStyle { get; set; } = MdTableStyle.GFM;
public MdTableStyle TableStyle
{
get => m_TableStyle;
set => SetValue(nameof(TableStyle), value, ref m_TableStyle);
}

/// <summary>
/// Gets or sets the maximum length of a line in the output.
Expand All @@ -59,7 +109,11 @@ public class MdSerializationOptions
/// the length of a word exceeds the maximum line length the max length
/// cannot be adhered to
/// </remarks>
public int MaxLineLength { get; set; } = -1;
public int MaxLineLength
{
get => m_MaxLineLength;
set => SetValue(nameof(MaxLineLength), value, ref m_MaxLineLength);
}

/// <summary>
/// Gets or sets the implementation to use for escaping text when saving a Markdown document.
Expand All @@ -71,6 +125,19 @@ public class MdSerializationOptions
/// When no escaper is set, <see cref="DefaultTextFormatter"/> will be used.
/// </para>
/// </remarks>
public ITextFormatter TextFormatter { get; set; } = DefaultTextFormatter.Instance;
public ITextFormatter TextFormatter
{
get => m_TextFormatter;
set => SetValue(nameof(TextFormatter), value, ref m_TextFormatter);
}


private void SetValue<T>(string propertyName, T value, ref T backingField)
{
if (m_IsReadOnly)
throw new InvalidOperationException($"Cannot set property '{propertyName}' of read-only instance of {nameof(MdSerializationOptions)}");

backingField = value;
}
}
}

0 comments on commit 7d7f531

Please sign in to comment.