diff --git a/Tomlet.Tests/CommentProvider/TestInlineCommentProvider.cs b/Tomlet.Tests/CommentProvider/TestInlineCommentProvider.cs new file mode 100644 index 0000000..2425f95 --- /dev/null +++ b/Tomlet.Tests/CommentProvider/TestInlineCommentProvider.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Tomlet.Tests.CommentProvider; + +public class TestInlineCommentProvider : ICommentProvider +{ + public static Dictionary Comments = new Dictionary(); + + private readonly string _name; + + public TestInlineCommentProvider(string name) + { + _name = name; + } + + public string GetComment() + { + return Comments[_name]; + } +} \ No newline at end of file diff --git a/Tomlet.Tests/CommentProvider/TestPrecedingCommentProvider.cs b/Tomlet.Tests/CommentProvider/TestPrecedingCommentProvider.cs new file mode 100644 index 0000000..b71dc5b --- /dev/null +++ b/Tomlet.Tests/CommentProvider/TestPrecedingCommentProvider.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Tomlet.Tests.CommentProvider; + +public class TestPrecedingCommentProvider : ICommentProvider +{ + public static Dictionary Comments = new Dictionary(); + + private readonly string _name; + + public TestPrecedingCommentProvider(string name) + { + _name = name; + } + + public string GetComment() + { + return Comments[_name]; + } +} \ No newline at end of file diff --git a/Tomlet.Tests/CommentSerializationTests.cs b/Tomlet.Tests/CommentSerializationTests.cs index f53aa9f..3b6b54c 100644 --- a/Tomlet.Tests/CommentSerializationTests.cs +++ b/Tomlet.Tests/CommentSerializationTests.cs @@ -1,11 +1,19 @@ +using System; +using System.Collections.Generic; using Tomlet.Models; +using Tomlet.Tests.CommentProvider; using Tomlet.Tests.TestModelClasses; using Xunit; +using Xunit.Abstractions; namespace Tomlet.Tests; public class CommentSerializationTests { + public CommentSerializationTests(ITestOutputHelper testOutputHelper) + { + } + [Fact] public void CommentsOnSimpleKeyValuePairsWork() { @@ -72,7 +80,7 @@ public void CommentsOnTableArraysWork() tomlString.Comments.InlineComment = "Inline comment on value"; table.PutValue("key", tomlString); - var tableArray = new TomlArray {table}; + var tableArray = new TomlArray { table }; tableArray.Comments.PrecedingComment = "This is a preceding comment on the table-array itself"; doc.PutValue("table-array", tableArray); @@ -91,7 +99,7 @@ public void CommentsOnTableArraysWork() public void CommentsOnPrimitiveArraysWork() { var doc = TomlDocument.CreateEmpty(); - var tomlNumbers = new TomlArray {1, 2, 3}; + var tomlNumbers = new TomlArray { 1, 2, 3 }; doc.PutValue("numbers", tomlNumbers); tomlNumbers[0].Comments.PrecedingComment = "This is a preceding comment on the first value of the array"; @@ -103,7 +111,7 @@ public void CommentsOnPrimitiveArraysWork() 2, # This is an inline comment on the second value of the array 3, ]".ReplaceLineEndings(); - + //Replace tabs with spaces because this source file uses spaces var actual = doc.SerializedValue.Trim().Replace("\t", " ").ReplaceLineEndings(); Assert.Equal(expected, actual); @@ -115,10 +123,88 @@ public void CommentAttributesWork() var config = TomletMain.To(TestResources.ExampleMailboxConfigurationTestInput); var doc = TomletMain.DocumentFrom(config); - + Assert.Equal("The name of the mailbox", doc.GetValue("mailbox").Comments.InlineComment); Assert.Equal("Your username for the mailbox", doc.GetValue("username").Comments.InlineComment); Assert.Equal("The password you use to access the mailbox", doc.GetValue("password").Comments.InlineComment); Assert.Equal("The rules for the mailbox follow", doc.GetArray("rules").Comments.PrecedingComment); } + + [Fact] + public void CommentProviderTest() + { + TestPrecedingCommentProvider.Comments["PrecedingComment"] = Guid.NewGuid().ToString(); + TestInlineCommentProvider.Comments["InlineComment"] = Guid.NewGuid().ToString(); + + var data = new CommentProviderTestModel() + { + PrecedingComment = "Dynamic Preceding Comment", + InlineComment = "Inline Comment", + }; + + var doc = TomletMain.DocumentFrom(data); + + Assert.Equal(TestPrecedingCommentProvider.Comments["PrecedingComment"], + doc.GetValue("PrecedingComment").Comments.PrecedingComment); + Assert.Equal("PlainInlineComment", doc.GetValue("PrecedingComment").Comments.InlineComment); + + Assert.Equal(TestInlineCommentProvider.Comments["InlineComment"], + doc.GetValue("InlineComment").Comments.InlineComment); + Assert.Equal("PlainPrecedingComment", doc.GetValue("InlineComment").Comments.PrecedingComment); + } + + [Fact] + public void PaddingLinesTest() + { + var data = new PaddingTestModel() + { + A = "str a", + B = 1, + C = new PaddingTestModel.NestedModel() + { + E = "str", + F = 2, + }, + D = new List() + { + new() + { + E = "str0", + F = 0, + }, + new() + { + E = "str1", + F = 1, + }, + } + }; + + var expected = @" +A = ""str a"" +B = 1 + +# Nested Object +[C] +# Preceding Comment +E = ""str"" # Preceding Comment +# Preceding Comment +F = 2 # Preceding Comment + + +# Nested Array +[[D]] +# Preceding Comment +E = ""str0"" # Preceding Comment +# Preceding Comment +F = 0 # Preceding Comment + +[[D]] +# Preceding Comment +E = ""str1"" # Preceding Comment +# Preceding Comment +F = 1 # Preceding Comment +".Trim(); + Assert.Equal(expected.ReplaceLineEndings(), TomletMain.TomlStringFrom(data).ReplaceLineEndings().Trim()); + } } \ No newline at end of file diff --git a/Tomlet.Tests/TestModelClasses/CommentProviderTestModel.cs b/Tomlet.Tests/TestModelClasses/CommentProviderTestModel.cs new file mode 100644 index 0000000..0998a4c --- /dev/null +++ b/Tomlet.Tests/TestModelClasses/CommentProviderTestModel.cs @@ -0,0 +1,15 @@ +using Tomlet.Attributes; +using Tomlet.Tests.CommentProvider; + +namespace Tomlet.Tests.TestModelClasses; + +public class CommentProviderTestModel +{ + [TomlPrecedingCommentProvider(typeof(TestPrecedingCommentProvider), new object[] { "PrecedingComment" })] + [TomlInlineComment("PlainInlineComment")] + public string PrecedingComment { get; set; } + + [TomlInlineCommentProvider(typeof(TestInlineCommentProvider), new object[] { "InlineComment" })] + [TomlPrecedingComment("PlainPrecedingComment")] + public string InlineComment { get; set; } +} \ No newline at end of file diff --git a/Tomlet.Tests/TestModelClasses/PaddingTestModel.cs b/Tomlet.Tests/TestModelClasses/PaddingTestModel.cs new file mode 100644 index 0000000..7af9e77 --- /dev/null +++ b/Tomlet.Tests/TestModelClasses/PaddingTestModel.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using Tomlet.Attributes; + +namespace Tomlet.Tests.TestModelClasses; + +public class PaddingTestModel +{ + public string A { get; set; } + + public int B { get; set; } + + [TomlPaddingLines(1)] + [TomlDoNotInlineObject] + [TomlPrecedingComment("Nested Object")] + public NestedModel C { get; set; } + + [TomlPaddingLines(1)] + [TomlDoNotInlineObject] + [TomlPrecedingComment("Nested Array")] + public List D { get; set; } + + public class NestedModel + { + [TomlPrecedingComment("Preceding Comment")] + [TomlInlineComment("Preceding Comment")] + public string E { get; set; } + + [TomlPrecedingComment("Preceding Comment")] + [TomlInlineComment("Preceding Comment")] + public int F { get; set; } + } +} \ No newline at end of file diff --git a/Tomlet/Attributes/TomlCommentProviderAttribute.cs b/Tomlet/Attributes/TomlCommentProviderAttribute.cs new file mode 100644 index 0000000..47d16f4 --- /dev/null +++ b/Tomlet/Attributes/TomlCommentProviderAttribute.cs @@ -0,0 +1,33 @@ +using System; +using System.Linq; + +namespace Tomlet.Attributes; + +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +public class TomlCommentProviderAttribute : Attribute +{ + private readonly Type _provider; + private readonly object[] _args; + private readonly Type[] _constructorParamsType; + + public string GetComment() + { + var constructor = _provider.GetConstructor(_constructorParamsType) ?? + throw new ArgumentException("Fail to get a constructor matching the parameters"); + var instance = constructor.Invoke(_args) as ICommentProvider ?? + throw new Exception("Fail to create an instance of the provider"); + return instance.GetComment(); + } + + public TomlCommentProviderAttribute(Type provider, object[] args) + { + if (!typeof(ICommentProvider).IsAssignableFrom(provider)) + { + throw new ArgumentException("Provider must implement ICommentProvider"); + } + + _provider = provider; + _args = args ?? new object[] { }; + _constructorParamsType = args?.Select(a => a.GetType()).ToArray() ?? new Type[] { }; + } +} \ No newline at end of file diff --git a/Tomlet/Attributes/TomlInlineCommentAttribute.cs b/Tomlet/Attributes/TomlInlineCommentAttribute.cs index ddee5f5..7de822a 100644 --- a/Tomlet/Attributes/TomlInlineCommentAttribute.cs +++ b/Tomlet/Attributes/TomlInlineCommentAttribute.cs @@ -3,12 +3,10 @@ namespace Tomlet.Attributes; [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] -public class TomlInlineCommentAttribute : Attribute +public class TomlInlineCommentAttribute : TomlInlineCommentProviderAttribute { - internal string Comment { get; } - - public TomlInlineCommentAttribute(string comment) + public TomlInlineCommentAttribute(string comment) : base(typeof(TomlSimpleCommentProvider), + new object[] { comment }) { - Comment = comment; } } \ No newline at end of file diff --git a/Tomlet/Attributes/TomlInlineCommentProviderAttribute.cs b/Tomlet/Attributes/TomlInlineCommentProviderAttribute.cs new file mode 100644 index 0000000..76a9d55 --- /dev/null +++ b/Tomlet/Attributes/TomlInlineCommentProviderAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace Tomlet.Attributes; + +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +public class TomlInlineCommentProviderAttribute : TomlCommentProviderAttribute +{ + public TomlInlineCommentProviderAttribute(Type provider) : base(provider, new object[] { }) + { + } + + public TomlInlineCommentProviderAttribute(Type provider, object[] args) : base(provider, args) + { + } +} \ No newline at end of file diff --git a/Tomlet/Attributes/TomlPaddingLinesAttribute.cs b/Tomlet/Attributes/TomlPaddingLinesAttribute.cs new file mode 100644 index 0000000..7c05349 --- /dev/null +++ b/Tomlet/Attributes/TomlPaddingLinesAttribute.cs @@ -0,0 +1,14 @@ +using System; + +namespace Tomlet.Attributes; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Field | AttributeTargets.Property)] +public class TomlPaddingLinesAttribute: Attribute +{ + public int PaddingLines { get; } + + public TomlPaddingLinesAttribute(int paddingLines) + { + PaddingLines = paddingLines; + } +} \ No newline at end of file diff --git a/Tomlet/Attributes/TomlPrecedingCommentAttribute.cs b/Tomlet/Attributes/TomlPrecedingCommentAttribute.cs index 0413092..e061bac 100644 --- a/Tomlet/Attributes/TomlPrecedingCommentAttribute.cs +++ b/Tomlet/Attributes/TomlPrecedingCommentAttribute.cs @@ -3,12 +3,10 @@ namespace Tomlet.Attributes; [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] -public class TomlPrecedingCommentAttribute : Attribute +public class TomlPrecedingCommentAttribute : TomlPrecedingCommentProviderAttribute { - internal string Comment { get; } - - public TomlPrecedingCommentAttribute(string comment) + public TomlPrecedingCommentAttribute(string comment) : base(typeof(TomlSimpleCommentProvider), + new object[] { comment }) { - Comment = comment; } } \ No newline at end of file diff --git a/Tomlet/Attributes/TomlPrecedingCommentProviderAttribute.cs b/Tomlet/Attributes/TomlPrecedingCommentProviderAttribute.cs new file mode 100644 index 0000000..45ab6e6 --- /dev/null +++ b/Tomlet/Attributes/TomlPrecedingCommentProviderAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace Tomlet.Attributes; + +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +public class TomlPrecedingCommentProviderAttribute : TomlCommentProviderAttribute +{ + public TomlPrecedingCommentProviderAttribute(Type provider) : base(provider, new object[] { }) + { + } + + public TomlPrecedingCommentProviderAttribute(Type provider, object[] args) : base(provider, args) + { + } +} \ No newline at end of file diff --git a/Tomlet/CommentProviderUtil.cs b/Tomlet/CommentProviderUtil.cs new file mode 100644 index 0000000..76b4f80 --- /dev/null +++ b/Tomlet/CommentProviderUtil.cs @@ -0,0 +1,18 @@ +using System; + +namespace Tomlet; + +internal static class CommentProviderUtil +{ + public static string GetComment(Type provider) + { + var constructor = provider.GetConstructor(Type.EmptyTypes); + if (constructor == null) + { + throw new ArgumentException("Provider must have a parameterless constructor"); + } + + var instance = (ICommentProvider)constructor.Invoke(null); + return instance.GetComment(); + } +} \ No newline at end of file diff --git a/Tomlet/Extensions/GenericExtensions.cs b/Tomlet/Extensions/GenericExtensions.cs index 363ce76..bb15e54 100644 --- a/Tomlet/Extensions/GenericExtensions.cs +++ b/Tomlet/Extensions/GenericExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -148,7 +148,8 @@ public static void Deconstruct(this KeyValuePair pai public static bool IsNullOrWhiteSpace(this string s) => string.IsNullOrEmpty(s) || string.IsNullOrEmpty(s.Trim()); - internal static T? GetCustomAttribute(this MemberInfo info) where T : Attribute => info.GetCustomAttributes(false).Where(a => a is T).Cast().FirstOrDefault(); + internal static T? GetCustomAttribute(this MemberInfo info) where T : Attribute + => info.GetCustomAttributes(false).Where(a => typeof(T).IsAssignableFrom(a.GetType())).Cast().FirstOrDefault(); internal static void EnsureLegalChar(this int c, int currentLineNum) { diff --git a/Tomlet/ICommentProvider.cs b/Tomlet/ICommentProvider.cs new file mode 100644 index 0000000..c67590c --- /dev/null +++ b/Tomlet/ICommentProvider.cs @@ -0,0 +1,6 @@ +namespace Tomlet; + +public interface ICommentProvider +{ + public string GetComment(); +} \ No newline at end of file diff --git a/Tomlet/Models/TomlArray.cs b/Tomlet/Models/TomlArray.cs index 75c7fbc..6edd975 100644 --- a/Tomlet/Models/TomlArray.cs +++ b/Tomlet/Models/TomlArray.cs @@ -105,7 +105,10 @@ public string SerializeTableArray(string key) //if we have a preceding comment on the array itself, we add a blank line //prior to the preceding comment on the first table. builder.Append('\n'); - + + if (first && value.Comments.PrecedingPaddingLines > 0) + builder.Append('\n', value.Comments.PrecedingPaddingLines); + builder.Append(value.Comments.FormatPrecedingComment()) .Append('\n'); } diff --git a/Tomlet/Models/TomlCommentData.cs b/Tomlet/Models/TomlCommentData.cs index 87ec927..5e6d4f7 100644 --- a/Tomlet/Models/TomlCommentData.cs +++ b/Tomlet/Models/TomlCommentData.cs @@ -6,6 +6,8 @@ namespace Tomlet.Models; public class TomlCommentData { + public int PrecedingPaddingLines { get; set; } = 0; + private string? _inlineComment; public string? PrecedingComment { get; set; } @@ -20,23 +22,23 @@ public string? InlineComment _inlineComment = null; return; } - + if (value.Contains("\n") || value.Contains("\r")) throw new TomlNewlineInInlineCommentException(); - + _inlineComment = value; } } - + public bool ThereAreNoComments => InlineComment == null && PrecedingComment == null; internal string FormatPrecedingComment(int indentCount = 0) { - if(PrecedingComment == null) + if (PrecedingComment == null) throw new Exception("Preceding comment is null"); var builder = new StringBuilder(); - + var lines = PrecedingComment.Split('\n'); var first = true; foreach (var line in lines) @@ -44,11 +46,11 @@ internal string FormatPrecedingComment(int indentCount = 0) if (!first) builder.Append('\n'); first = false; - + var correctIndent = new string('\t', indentCount); builder.Append(correctIndent).Append("# ").Append(line); } - + return builder.ToString(); } } \ No newline at end of file diff --git a/Tomlet/Models/TomlTable.cs b/Tomlet/Models/TomlTable.cs index 67fac2d..6c7318e 100644 --- a/Tomlet/Models/TomlTable.cs +++ b/Tomlet/Models/TomlTable.cs @@ -100,6 +100,9 @@ private void WriteValueToStringBuilder(string? keyName, string subKey, StringBui var hadBlankLine = builder.Length < 2 || builder[builder.Length - 2] == '\n'; + if (value.Comments.PrecedingPaddingLines > 0) + builder.Append('\n', value.Comments.PrecedingPaddingLines); + //Handle any preceding comment - this will ALWAYS go before any sort of value if (value.Comments.PrecedingComment != null) builder.Append(value.Comments.FormatPrecedingComment()) diff --git a/Tomlet/TomlCompositeSerializer.cs b/Tomlet/TomlCompositeSerializer.cs index f92f254..b21a701 100644 --- a/Tomlet/TomlCompositeSerializer.cs +++ b/Tomlet/TomlCompositeSerializer.cs @@ -35,11 +35,24 @@ public static TomlSerializationMethods.Serialize For(Type type, TomlSeri var fields = type.GetFields(memberFlags); var fieldAttribs = fields - .ToDictionary(f => f, f => new {inline = GenericExtensions.GetCustomAttribute(f), preceding = GenericExtensions.GetCustomAttribute(f), noInline = GenericExtensions.GetCustomAttribute(f)}); + .ToDictionary(f => f, f => new + { + inline = GenericExtensions.GetCustomAttribute(f), + preceding = GenericExtensions.GetCustomAttribute(f), + noInline = GenericExtensions.GetCustomAttribute(f), + paddingLines = GenericExtensions.GetCustomAttribute(f), + }); var props = type.GetProperties(memberFlags) .ToArray(); var propAttribs = props - .ToDictionary(p => p, p => new {inline = GenericExtensions.GetCustomAttribute(p), preceding = GenericExtensions.GetCustomAttribute(p), prop = GenericExtensions.GetCustomAttribute(p), noInline = GenericExtensions.GetCustomAttribute(p)}); + .ToDictionary(p => p, p => new + { + inline = GenericExtensions.GetCustomAttribute(p), + preceding = GenericExtensions.GetCustomAttribute(p), + prop = GenericExtensions.GetCustomAttribute(p), + noInline = GenericExtensions.GetCustomAttribute(p), + paddingLines = GenericExtensions.GetCustomAttribute(p), + }); var isForcedNoInline = GenericExtensions.GetCustomAttribute(type) != null; @@ -81,8 +94,9 @@ public static TomlSerializationMethods.Serialize For(Type type, TomlSeri //in its supertype. continue; - tomlValue.Comments.InlineComment = commentAttribs.inline?.Comment; - tomlValue.Comments.PrecedingComment = commentAttribs.preceding?.Comment; + tomlValue.Comments.InlineComment = commentAttribs.inline?.GetComment(); + tomlValue.Comments.PrecedingComment = commentAttribs.preceding?.GetComment(); + tomlValue.Comments.PrecedingPaddingLines = commentAttribs.paddingLines?.PaddingLines ?? 0; if(commentAttribs.noInline != null && tomlValue is TomlTable table) table.ForceNoInline = true; @@ -110,8 +124,9 @@ public static TomlSerializationMethods.Serialize For(Type type, TomlSeri var thisPropAttribs = propAttribs[prop]; - tomlValue.Comments.InlineComment = thisPropAttribs.inline?.Comment; - tomlValue.Comments.PrecedingComment = thisPropAttribs.preceding?.Comment; + tomlValue.Comments.InlineComment = thisPropAttribs.inline?.GetComment(); + tomlValue.Comments.PrecedingComment = thisPropAttribs.preceding?.GetComment(); + tomlValue.Comments.PrecedingPaddingLines = thisPropAttribs.paddingLines?.PaddingLines ?? 0; if (thisPropAttribs.noInline != null && tomlValue is TomlTable table) table.ForceNoInline = true; diff --git a/Tomlet/TomlSimpleCommentProvider.cs b/Tomlet/TomlSimpleCommentProvider.cs new file mode 100644 index 0000000..c5cb530 --- /dev/null +++ b/Tomlet/TomlSimpleCommentProvider.cs @@ -0,0 +1,16 @@ +namespace Tomlet; + +internal class TomlSimpleCommentProvider : ICommentProvider +{ + private readonly string _comment; + + public TomlSimpleCommentProvider(string comment) + { + _comment = comment; + } + + public string GetComment() + { + return _comment; + } +} \ No newline at end of file