Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add asynchronous parsing #2

Merged
merged 10 commits into from
Oct 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 52 additions & 25 deletions src/Chronicler/ChronicleCollection.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
using Chronicler.Converters;
using Chronicler.Converters.Internal;
using Chronicler.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;

namespace Chronicler
{
Expand All @@ -27,33 +31,56 @@ public static ChronicleCollection ParseJson(JsonDocument ck2json)
.GetProperty("character_player_data")
.GetProperty("chronicle_collection");

var chronicleElements = chronicleCollection
.EnumerateObject()
.Where(prop => prop.NameEquals("chronicle"))
.Select(prop => prop.Value);
return new JsonElementSerializer(chronicleCollection).ToObject<ChronicleCollection>();
}

public static async Task<ChronicleCollection> ParseJsonAsync(Stream jsonStream)
{
static int ReadPIDFromHeader(ReadOnlySpan<byte> headerChunk)
{
var reader = new Utf8JsonReader(headerChunk);

var chronicles = chronicleElements
.Select(chronicle => new
while (reader.Read())
{
chronicle,
chapters = chronicle.EnumerateObject()
.Where(prop => prop.NameEquals("chronicle_chapter"))
.Select(prop => prop.Value).Select(chapter => new
{
entries = chapter.EnumerateObject()
.Where(prop => prop.NameEquals("chronicle_entry"))
.Select(prop => prop.Value)
.Select(entry => new
{
entry,
text = entry.GetProperty("text").GetString(),
//pi
}),
year = chapter.GetProperty("year").GetInt32()
})
});
switch (reader.TokenType)
{
case JsonTokenType.PropertyName when reader.ValueTextEquals("id"):
if (reader.Read())
{
return reader.GetInt32();
}
goto fail;
}
}

return new JsonElementSerializer(chronicleCollection).ToObject<ChronicleCollection>();
fail:
throw new JsonException("couldn't find player ID");
}

int pid;
{
var headerChunk = System.Buffers.ArrayPool<byte>.Shared.Rent(0x200);

var bytesRead = await jsonStream.ReadAsync(headerChunk, 0, headerChunk.Length);
if (bytesRead < headerChunk.Length)
throw new InvalidOperationException("failed to read header");

pid = ReadPIDFromHeader(headerChunk);

System.Buffers.ArrayPool<byte>.Shared.Return(headerChunk);
}
jsonStream.Seek(0, SeekOrigin.Begin);

var ck2pcConverter = new CK2PlayerCharacterConverter
{
PlayerID = pid
};

var options = new JsonSerializerOptions();
options.Converters.Add(ck2pcConverter);

var ck = await JsonSerializer.DeserializeAsync<CK2txt>(jsonStream, options);
return ck.PlayerCharacter.PlayerCharacterData.ChronicleCollection;
}
}
}
2 changes: 1 addition & 1 deletion src/Chronicler/Chronicler.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<Description>Chronicle parsing library for Crusader Kings II</Description>
<PackageProjectUrl>https://github.com/scorpdx/chronicler</PackageProjectUrl>
<RepositoryUrl>https://github.com/scorpdx/chronicler</RepositoryUrl>
<Version>0.1.0</Version>
<Version>0.2.0</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
21 changes: 20 additions & 1 deletion src/Chronicler/Converters/ChronicleCollectionConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,26 @@ public class ChronicleCollectionConverter : JsonConverter<ChronicleCollection>
public override ChronicleCollection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var chronicles = new List<Chronicle>();
while (reader.Read())

switch (reader.TokenType)
{
case JsonTokenType.None:
case JsonTokenType.PropertyName:
if (!reader.Read())
{
throw new JsonException("expected token to parse");
}
break;
}

var startingDepth = reader.CurrentDepth;
do
{
if (!reader.Read())
{
throw new JsonException("expected token to parse");
}

switch (reader.TokenType)
{
case JsonTokenType.PropertyName when reader.ValueTextEquals("chronicle"):
Expand All @@ -20,6 +38,7 @@ public override ChronicleCollection Read(ref Utf8JsonReader reader, Type typeToC
break;
}
}
while (reader.CurrentDepth > startingDepth);

return new ChronicleCollection
{
Expand Down
64 changes: 64 additions & 0 deletions src/Chronicler/Converters/Internal/CK2PlayerCharacterConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using Chronicler.Internal;
using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Chronicler.Converters.Internal
{
internal class CK2PlayerCharacterConverter : JsonConverter<CK2PlayerCharacter>
{
public int? PlayerID { get; set; }

public override CK2PlayerCharacter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (PlayerID == null)
throw new ArgumentNullException(nameof(PlayerID));

switch (reader.TokenType)
{
case JsonTokenType.None:
case JsonTokenType.PropertyName:
if (!reader.Read())
{
throw new JsonException("expected token to parse");
}
break;
}

CK2PlayerCharacter result = null;
var pid = PlayerID.ToString().AsSpan();
var startingDepth = reader.CurrentDepth;
do
{
if (!reader.Read())
{
throw new JsonException("expected token to parse");
}
else if (result == null)
{
switch (reader.TokenType)
{
case JsonTokenType.PropertyName when reader.ValueTextEquals(pid):
if (!reader.Read())
{
throw new JsonException("expected token to parse");
}
result = JsonSerializer.Deserialize<CK2PlayerCharacter>(ref reader); /* exclude options or we loop */
break;
default:
reader.TrySkip();
break;
}
}
}
while (reader.CurrentDepth > startingDepth);

return result;
}

public override void Write(Utf8JsonWriter writer, CK2PlayerCharacter value, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
}
}
10 changes: 10 additions & 0 deletions src/Chronicler/Internal/CK2Player.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Text.Json.Serialization;

namespace Chronicler.Internal
{
internal class CK2Player
{
[JsonPropertyName("id")]
public int ID { get; set; }
}
}
10 changes: 10 additions & 0 deletions src/Chronicler/Internal/CK2PlayerCharacter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Text.Json.Serialization;

namespace Chronicler.Internal
{
internal class CK2PlayerCharacter
{
[JsonPropertyName("character_player_data")]
public PlayerCharacterData PlayerCharacterData { get; set; }
}
}
13 changes: 13 additions & 0 deletions src/Chronicler/Internal/CK2txt.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Text.Json.Serialization;

namespace Chronicler.Internal
{
internal class CK2txt
{
[JsonPropertyName("player")]
public CK2Player Player { get; set; }

[JsonPropertyName("character")]
public CK2PlayerCharacter PlayerCharacter { get; set; }
}
}
10 changes: 10 additions & 0 deletions src/Chronicler/Internal/PlayerCharacterData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Text.Json.Serialization;

namespace Chronicler.Internal
{
internal class PlayerCharacterData
{
[JsonPropertyName("chronicle_collection")]
public ChronicleCollection ChronicleCollection { get; set; }
}
}
16 changes: 16 additions & 0 deletions test/ChroniclerTests/ChronicleCollectionTest.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using Chronicler;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using Xunit;

namespace ChroniclerTests
Expand All @@ -8,12 +10,26 @@ public class ChronicleCollectionTest
{
private static string CK2JsonPath => Path.Combine("resources", "Ironman_West_Francia_HaR.json");

private static Stream GetCK2JsonStream() => File.OpenRead(CK2JsonPath);
private static readonly JsonDocument CK2Json = JsonDocument.Parse(File.ReadAllText(CK2JsonPath));

[Fact]
public void TestParseJson()
{
var chronicleCollection = Chronicler.ChronicleCollection.ParseJson(CK2Json);
VerifyJson(chronicleCollection);
}

[Fact]
public async Task TestParseJsonAsync()
{
using var stream = GetCK2JsonStream();
var chronicleCollection = await Chronicler.ChronicleCollection.ParseJsonAsync(stream);
VerifyJson(chronicleCollection);
}

private void VerifyJson(ChronicleCollection chronicleCollection)
{
Assert.NotNull(chronicleCollection);
Assert.NotEmpty(chronicleCollection.Chronicles);
Assert.Collection(chronicleCollection.Chronicles[0].Chapters,
Expand Down