Skip to content

Commit

Permalink
Add support for 1.0.0 (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
xoofx committed Jan 14, 2022
1 parent 7978e34 commit dacad96
Show file tree
Hide file tree
Showing 20 changed files with 424 additions and 119 deletions.
2 changes: 1 addition & 1 deletion ext/toml-test
Submodule toml-test updated 557 files
16 changes: 8 additions & 8 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ Tomlyn is a TOML parser, validator and authoring library for .NET Framework and

## Features

- Very fast parser, GC friendly
- Compatible with latest [TOML 0.5 specs](https://github.com/toml-lang/toml)
- Support perfect load/save roundtrip while preserving all spaces, new line, comments
- Provides a validator with the `Toml.Validate` method
- Provides accurate parsing and validation error messages with precise source location
- Allow to work on the syntax tree directly (preserving styles) through the `Toml.Parse`
- Allow to work with a runtime representation `Toml.ToModel` (but cannot be saved back to TOML)
- Supports for .NET Framework 4.5+, .NET Standard 1.3 and .NET Standard 2.0+ (Core)
- Very fast parser, GC friendly.
- Compatible with the latest [TOML 1.0.0 specs](https://github.com/toml-lang/toml).
- Support perfect load/save roundtrip while preserving all spaces, new line, comments.
- Provides a validator with the `Toml.Validate` method.
- Provides accurate parsing and validation error messages with precise source location.
- Allow to work on the syntax tree directly (preserving styles) through the `Toml.Parse`.
- Allow to work with a runtime representation `Toml.ToModel` (but cannot be saved back to TOML).
- Supports for .NET Standard 2.0+.

## Usage

Expand Down
14 changes: 6 additions & 8 deletions src/Tomlyn.Tests/ModelHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// See license.txt file in the project root for full license information.
using System;
using System.Globalization;
using System.Linq;
using Newtonsoft.Json.Linq;
using Tomlyn.Model;

Expand All @@ -22,11 +23,8 @@ public static JToken ToJson(TomlObject obj)
{
value.Add(ToJson(element));
}
return isLikeTableArray ? (JToken)value : new JObject()
{
{"type", "array"},
{"value", value}
};

return value;
}
case TomlBoolean tomlBoolean:
return new JObject
Expand All @@ -45,10 +43,10 @@ public static JToken ToJson(TomlObject obj)
kindStr = "datetime-local";
break;
case ObjectKind.LocalDate:
kindStr = "date";
kindStr = "date-local";
break;
case ObjectKind.LocalTime:
kindStr = "time";
kindStr = "time-local";
break;
}
return new JObject
Expand Down Expand Up @@ -78,7 +76,7 @@ public static JToken ToJson(TomlObject obj)
{
var json = new JObject();
// For the test we order by string key
foreach (var keyPair in tomlTable.GetTomlEnumerator())
foreach (var keyPair in tomlTable.GetTomlEnumerator().OrderBy(x => x.Key))
{
json.Add(keyPair.Key, ToJson(keyPair.Value));
}
Expand Down
2 changes: 1 addition & 1 deletion src/Tomlyn.Tests/ModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public static void TestPrimitives()
Assert.AreEqual(true, model["b"]);
Assert.AreEqual(1.0, model["c"]);
Assert.AreEqual("yo", model["d"]);
Assert.AreEqual(new DateTime(1980, 01, 20), model["e"]);
Assert.AreEqual(new DateTimeValue(1980, 01, 20), model["e"]);
}

[Test]
Expand Down
66 changes: 57 additions & 9 deletions src/Tomlyn.Tests/StandardTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using Tomlyn.Model;
using Tomlyn.Syntax;

namespace Tomlyn.Tests
Expand All @@ -36,6 +39,7 @@ public static void SpecInvalid(string inputName, string toml, string json)

private static void ValidateSpec(string type, string inputName, string toml, string json)
{
Console.WriteLine($"Testing {inputName}");
var doc = Toml.Parse(toml, inputName);
var roundtrip = doc.ToString();
Dump(toml, doc, roundtrip);
Expand All @@ -46,25 +50,24 @@ private static void ValidateSpec(string type, string inputName, string toml, str
// Only in the case of a valid spec we check for rountrip
Assert.AreEqual(toml, roundtrip, "The roundtrip doesn't match");


// Read the original json
var srcJson = JObject.Parse(json);
var expectedJson = (JObject)NormalizeJson(JObject.Parse(json));
// Convert the syntax tree into a model
var model = doc.ToModel();
// Convert the model into the expected json
var modelJson = ModelHelper.ToJson(model);
var computedJson = ModelHelper.ToJson(model);

// Write back the result to a string
var srcJsonAsString = srcJson.ToString(Formatting.Indented);
var dstJsonAsString = modelJson.ToString(Formatting.Indented);
var expectedJsonAsString = expectedJson.ToString(Formatting.Indented);
var computedJsonAsString = computedJson.ToString(Formatting.Indented);

DisplayHeader("json");
Console.WriteLine(srcJsonAsString);
Console.WriteLine(computedJsonAsString);

DisplayHeader("expected json");
Console.WriteLine(dstJsonAsString);
Console.WriteLine(expectedJsonAsString);

Assert.AreEqual(srcJsonAsString, dstJsonAsString);
Assert.AreEqual(expectedJsonAsString, computedJsonAsString);
break;
case InvalidSpec:
Assert.True(doc.HasErrors, "The TOML requires parsing/validation errors");
Expand All @@ -78,6 +81,51 @@ private static void ValidateSpec(string type, string inputName, string toml, str
}
}

private static JToken NormalizeJson(JToken token)
{
if (token is JArray array)
{
var newArray = new JArray();
foreach (var item in array)
{
newArray.Add(NormalizeJson(item));
}

return newArray;
}
else if (token is JObject obj)
{
var newObject = new JObject();

var items = new List<KeyValuePair<string, JToken?>>();
foreach (var item in obj)
{
items.Add(item);
}

foreach (var item in items.OrderBy(x => x.Key))
{
newObject.Add(item.Key, NormalizeJson(item.Value));
}

return newObject;
}
else if (token is JValue value && value.Value is string str)
{
if (string.CompareOrdinal(str, "inf") == 0) return new JValue("+inf");
if (str.Length > 0 && char.IsDigit(str[0]) && str.Contains('.') && str.Contains('e'))
{
if (double.TryParse(str, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
{
return new JValue(new TomlFloat(doubleValue).ToString());
}
}
}

return token;
}


public static void Dump(string input, DocumentSyntax doc, string roundtrip)
{
Console.WriteLine();
Expand Down Expand Up @@ -118,7 +166,7 @@ public static IEnumerable ListTomlFiles(string type)
{
var functionName = Path.GetFileName(file);

var input = File.ReadAllText(file, Encoding.UTF8);
var input = Encoding.UTF8.GetString(File.ReadAllBytes(file));

string json = null;
if (type == "valid")
Expand Down
8 changes: 4 additions & 4 deletions src/Tomlyn.Tests/Tomlyn.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="nunit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
32 changes: 32 additions & 0 deletions src/Tomlyn/DateTimeValue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// Licensed under the BSD-Clause 2 license.
// See license.txt file in the project root for full license information.

namespace Tomlyn;
using System;

/// <summary>
/// A datetime value that can represent a TOML:
/// - An offset DateTime
/// - A local DateTime
/// - A local Date
/// - A local Time,
/// </summary>
/// <param name="DateTime"></param>
/// <param name="SecondPrecision"></param>
/// <param name="OffsetKind"></param>
public record struct DateTimeValue(DateTimeOffset DateTime, int SecondPrecision, DateTimeValueOffsetKind OffsetKind)
{
public DateTimeValue(int year, int month, int day) : this(new DateTimeOffset(new DateTime(year, month, day)), 0, DateTimeValueOffsetKind.None)
{
}

public DateTimeValue(DateTime datetime) : this(new DateTimeOffset(datetime), 0, DateTimeValueOffsetKind.None)
{
}

public static implicit operator DateTimeValue(DateTime dateTime)
{
return new DateTimeValue(dateTime, 0, DateTimeValueOffsetKind.None);
}
}
24 changes: 24 additions & 0 deletions src/Tomlyn/DateTimeValueOffsetKind.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// Licensed under the BSD-Clause 2 license.
// See license.txt file in the project root for full license information.

namespace Tomlyn;

/// <summary>
/// Offsets used for a <see cref="DateTimeValue"/>
/// </summary>
public enum DateTimeValueOffsetKind
{
/// <summary>
/// No offset.
/// </summary>
None,
/// <summary>
/// Zero offset.
/// </summary>
Zero,
/// <summary>
/// Number offset.
/// </summary>
Number
}
75 changes: 66 additions & 9 deletions src/Tomlyn/Helpers/DateTimeRFC3339.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// See license.txt file in the project root for full license information.
using System;
using System.Globalization;
using Tomlyn.Model;

namespace Tomlyn.Helpers
{
Expand Down Expand Up @@ -79,7 +80,7 @@ internal static class DateTimeRFC3339
"yyyy-MM-ddTHH:mm:ss.fffffff",

// Specs says that T might be omitted
"yyyy-MM-dd HH:mm:ss", // With Z postfix
"yyyy-MM-dd HH:mm:ss",
"yyyy-MM-dd HH:mm:ss.f",
"yyyy-MM-dd HH:mm:ss.ff",
"yyyy-MM-dd HH:mm:ss.fff",
Expand All @@ -102,24 +103,80 @@ internal static class DateTimeRFC3339
"HH:mm:ss.fffffff",
};

public static bool TryParseOffsetDateTime(string str, out DateTime time)
public static bool TryParseOffsetDateTime(string str, out DateTimeValue time)
{
return DateTime.TryParseExact(str, OffsetDateTimeFormats, CultureInfo.InvariantCulture, DateTimeStyles.None, out time);
return TryParseExactWithPrecision(str.ToUpperInvariant(), OffsetDateTimeFormats, TryParseDateTimeOffset, DateTimeStyles.None, out time);
}

public static bool TryParseLocalDateTime(string str, out DateTime time)
public static bool TryParseLocalDateTime(string str, out DateTimeValue time)
{
return DateTime.TryParseExact(str, LocalDateTimeFormats, CultureInfo.InvariantCulture, DateTimeStyles.None, out time);
return TryParseExactWithPrecision(str.ToUpperInvariant(), LocalDateTimeFormats, TryParseDateTime, DateTimeStyles.AssumeLocal, out time);
}

public static bool TryParseLocalDate(string str, out DateTime time)
public static bool TryParseLocalDate(string str, out DateTimeValue time)
{
return DateTime.TryParseExact(str, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out time);
if (DateTime.TryParseExact(str.ToUpperInvariant(), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var rawtime))
{
time = new DateTimeValue(rawtime, 0, DateTimeValueOffsetKind.None);
return true;
}

time = default;
return false;
}

public static bool TryParseLocalTime(string str, out DateTimeValue time)
{
return TryParseExactWithPrecision(str.ToUpperInvariant(), LocalTimeFormats, TryParseDateTime, DateTimeStyles.None, out time);
}

public static bool TryParseLocalTime(string str, out DateTime time)
private static readonly ParseDelegate TryParseDateTime = (string text, string format, CultureInfo culture, DateTimeStyles style, out DateTimeOffset time) =>
{
return DateTime.TryParseExact(str, LocalTimeFormats, CultureInfo.InvariantCulture, DateTimeStyles.None, out time);
time = default;
if (DateTime.TryParseExact(text, format, culture, style, out var rawTime))
{
time = new DateTimeOffset(rawTime);
return true;
}

return false;
};
private static readonly ParseDelegate TryParseDateTimeOffset = DateTimeOffset.TryParseExact;

private delegate bool ParseDelegate(string text, string format, CultureInfo culture, DateTimeStyles style, out DateTimeOffset time);

private static bool TryParseExactWithPrecision(string str, string[] formats, ParseDelegate parser, DateTimeStyles style, out DateTimeValue time)
{
time = default;
for (int i = 0; i < formats.Length; i++)
{
var format = formats[i];
if (parser(str, format, CultureInfo.InvariantCulture, style, out var rawTime))
{
// 0
// fffffff
int precision = i;
if (precision >= LocalTimeFormats.Length)
{
precision -= LocalTimeFormats.Length;
}

var offsetKind = DateTimeValueOffsetKind.None;
if (format.EndsWith("Z"))
{
offsetKind = DateTimeValueOffsetKind.Zero;
}
else if (format.EndsWith("zzz"))
{
offsetKind = DateTimeValueOffsetKind.Number;
}

time = new DateTimeValue(rawTime, precision, offsetKind);
return true;
}
}

return false;
}
}
}
2 changes: 2 additions & 0 deletions src/Tomlyn/Model/ObjectKind.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ namespace Tomlyn.Model
/// </summary>
public enum ObjectKind
{
InlineTable,

Table,

TableArray,
Expand Down
3 changes: 2 additions & 1 deletion src/Tomlyn/Model/SyntaxTransform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ internal class SyntaxTransform : SyntaxVisitor
{
private readonly TomlTable _rootTable;
private TomlTable _currentTable;
private object _currentValue;
private object _currentValue;
private bool? hasExponent;

public SyntaxTransform(TomlTable rootTable)
{
Expand Down
Loading

0 comments on commit dacad96

Please sign in to comment.