From a1efe36252f0ff4fae2dd012076fc0f0721bce57 Mon Sep 17 00:00:00 2001 From: Phill Date: Thu, 24 Apr 2025 11:18:32 -0400 Subject: [PATCH 1/7] implement reading json 5 sources --- source/JSON/JSONArray.cs | 69 +++++++++---------- source/JSON/JSONObject.cs | 115 ++++++++++++++++--------------- source/JSON/JSONProperty.cs | 73 +++++++++++--------- source/JSON/JSONReader.cs | 125 ++++++++++++++++----------------- source/JSON/JSONWriter.cs | 8 +-- source/JSON/Token.cs | 4 -- source/SharedFunctions.cs | 12 ++++ source/XML/XMLReader.cs | 8 +-- tests/JSONTests.cs | 133 +++++++++++++++++++++++++++++++----- 9 files changed, 328 insertions(+), 219 deletions(-) create mode 100644 source/SharedFunctions.cs diff --git a/source/JSON/JSONArray.cs b/source/JSON/JSONArray.cs index cb3773d..13350d8 100644 --- a/source/JSON/JSONArray.cs +++ b/source/JSON/JSONArray.cs @@ -211,56 +211,55 @@ readonly void ISerializable.Write(ByteWriter writer) void ISerializable.Read(ByteReader reader) { value = Implementation.Allocate(); - ParseArray(new(reader), reader, this); - static void ParseArray(JSONReader jsonReader, ByteReader reader, JSONArray jsonArray) + using Text textBuffer = new(256); + JSONReader jsonReader = new(reader); + while (jsonReader.ReadToken(out Token token)) { - while (jsonReader.ReadToken(out Token token)) + if (token.type == Token.Type.Text) { - if (token.type == Token.Type.True) + int capacity = token.length * 4; + if (textBuffer.Length < capacity) { - jsonArray.Add(jsonReader.GetBoolean(token)); + textBuffer.SetLength(capacity); } - else if (token.type == Token.Type.False) - { - jsonArray.Add(jsonReader.GetBoolean(token)); - } - else if (token.type == Token.Type.Null) - { - jsonArray.AddNull(); - } - else if (token.type == Token.Type.Number) + + int textLength = jsonReader.GetText(token, textBuffer.AsSpan()); + Span text = textBuffer.Slice(0, textLength); + if (double.TryParse(text, out double number)) { - jsonArray.Add(jsonReader.GetNumber(token)); + Add(number); } - else if (token.type == Token.Type.Text) + else if (text.SequenceEqual("true")) { - Text textBuffer = new(token.length * 4); - Span bufferSpan = textBuffer.AsSpan(); - int textLength = jsonReader.GetText(token, bufferSpan); - Span text = bufferSpan.Slice(0, textLength); - if (text.Length > 0 && text[0] == '"') - { - text = text.Slice(1, text.Length - 2); - } - - jsonArray.Add(text); - textBuffer.Dispose(); + Add(true); } - else if (token.type == Token.Type.StartObject) + else if (text.SequenceEqual("false")) { - JSONObject newObject = reader.ReadObject(); - jsonArray.Add(newObject); + Add(false); } - else if (token.type == Token.Type.StartArray) + else if (text.SequenceEqual("null")) { - JSONArray newArray = reader.ReadObject(); - jsonArray.Add(newArray); + AddNull(); } - else if (token.type == Token.Type.EndArray) + else { - break; + Add(text); } } + else if (token.type == Token.Type.StartObject) + { + JSONObject newObject = reader.ReadObject(); + Add(newObject); + } + else if (token.type == Token.Type.StartArray) + { + JSONArray newArray = reader.ReadObject(); + Add(newArray); + } + else if (token.type == Token.Type.EndArray) + { + break; + } } } diff --git a/source/JSON/JSONObject.cs b/source/JSON/JSONObject.cs index 5232400..9ad1309 100644 --- a/source/JSON/JSONObject.cs +++ b/source/JSON/JSONObject.cs @@ -453,95 +453,94 @@ void ISerializable.Read(ByteReader reader) { value = Implementation.Allocate(); JSONReader jsonReader = new(reader); - if (jsonReader.PeekToken(out Token nextToken)) + if (jsonReader.PeekToken(out Token nextToken, out int readBytes)) { if (nextToken.type == Token.Type.StartObject) { - jsonReader.ReadToken(out _); + //start of object + reader.Advance(readBytes); } } - ParseObject(jsonReader, reader, this); - - static void ParseObject(JSONReader jsonReader, ByteReader reader, JSONObject jsonObject) + using Text nameTextBuffer = new(256); + using Text nextTextBuffer = new(256); + while (jsonReader.ReadToken(out Token token)) { - Span buffer = stackalloc char[256]; - while (jsonReader.ReadToken(out Token token)) + if (token.type == Token.Type.Text) { - if (token.type == Token.Type.Text) + int capacity = token.length * 4; + if (nameTextBuffer.Length < capacity) + { + nameTextBuffer.SetLength(capacity); + } + + int nameTextLength = jsonReader.GetText(token, nameTextBuffer.AsSpan()); + Span name = nameTextBuffer.Slice(0, nameTextLength); + if (jsonReader.ReadToken(out nextToken)) { - int length = jsonReader.GetText(token, buffer); - if (jsonReader.ReadToken(out Token nextToken)) + int nextCapacity = nextToken.length * 4; + if (nextTextBuffer.Length < nextCapacity) { - ReadOnlySpan nameSpan = buffer.Slice(0, length); - if (nameSpan.Length > 0 && nameSpan[0] == '"') - { - nameSpan = nameSpan.Slice(1, nameSpan.Length - 2); - } + nextTextBuffer.SetLength(nextCapacity); + } - if (nextToken.type == Token.Type.True) - { - jsonObject.Add(nameSpan, true); - } - else if (nextToken.type == Token.Type.False) - { - jsonObject.Add(nameSpan, false); - } - else if (nextToken.type == Token.Type.Null) - { - jsonObject.AddNull(nameSpan); - } - else if (nextToken.type == Token.Type.Number) - { - jsonObject.Add(nameSpan, jsonReader.GetNumber(nextToken)); - } - else if (nextToken.type == Token.Type.Text) + if (nextToken.type == Token.Type.Text) + { + int nextTextLength = jsonReader.GetText(nextToken, nextTextBuffer.AsSpan()); + ReadOnlySpan nextText = nextTextBuffer.Slice(0, nextTextLength); + if (double.TryParse(nextText, out double number)) { - Text textBuffer = new(nextToken.length * 4); - Span bufferSpan = textBuffer.AsSpan(); - int textLength = jsonReader.GetText(nextToken, bufferSpan); - ReadOnlySpan text = bufferSpan.Slice(0, textLength); - if (text.Length > 0 && text[0] == '"') - { - text = text.Slice(1, text.Length - 2); - } - - jsonObject.Add(nameSpan, text); - textBuffer.Dispose(); + Add(name, number); } - else if (nextToken.type == Token.Type.StartObject) + else if (nextText.SequenceEqual("true")) { - JSONObject newObject = reader.ReadObject(); - jsonObject.Add(nameSpan, newObject); + Add(name, true); } - else if (nextToken.type == Token.Type.StartArray) + else if (nextText.SequenceEqual("false")) { - JSONArray newArray = reader.ReadObject(); - jsonObject.Add(nameSpan, newArray); + Add(name, false); } - else if (nextToken.type == Token.Type.EndObject) + else if (nextText.SequenceEqual("null")) { - break; + AddNull(name); } else { - throw new InvalidOperationException($"Invalid JSON token at position {nextToken.position}"); + Add(name, nextText); } } + else if (nextToken.type == Token.Type.StartObject) + { + JSONObject newObject = reader.ReadObject(); + Add(name, newObject); + } + else if (nextToken.type == Token.Type.StartArray) + { + JSONArray newArray = reader.ReadObject(); + Add(name, newArray); + } + else if (nextToken.type == Token.Type.EndObject) + { + break; + } else { - throw new InvalidOperationException($"Invalid JSON token at position {token.position}, expected value."); + throw new InvalidOperationException($"Invalid JSON token at position {nextToken.position}"); } } - else if (token.type == Token.Type.EndObject) - { - break; - } else { - throw new InvalidOperationException($"Invalid JSON token at position {token.position}"); + throw new InvalidOperationException($"No succeeding token available after {name.ToString()}"); } } + else if (token.type == Token.Type.EndObject) + { + break; + } + else + { + throw new InvalidOperationException($"Unexpected token `{token.type}`, expected }} or another text token"); + } } } diff --git a/source/JSON/JSONProperty.cs b/source/JSON/JSONProperty.cs index 649a0b6..cd6ca01 100644 --- a/source/JSON/JSONProperty.cs +++ b/source/JSON/JSONProperty.cs @@ -55,28 +55,12 @@ readonly get } } - public readonly ref double Number - { - get - { - return ref value.Read(); - } - } + public readonly ref double Number => ref value.Read(); + public readonly ref bool Boolean => ref value.Read(); - public readonly ref bool Boolean + public readonly JSONObject Object { - get - { - return ref value.Read(); - } - } - - public readonly unsafe JSONObject Object - { - get - { - return value.Read(); - } + get => value.Read(); set { if (IsObject) @@ -91,12 +75,9 @@ public readonly unsafe JSONObject Object } } - public readonly unsafe JSONArray Array + public readonly JSONArray Array { - get - { - return value.Read(); - } + get => value.Read(); set { if (IsArray) @@ -236,13 +217,43 @@ public unsafe readonly void ToString(Text result, bool prefixName, ReadOnlySpan< } } - public readonly override string ToString() + public unsafe readonly override string ToString() { - Text buffer = new(0); - ToString(buffer, true); - string result = buffer.ToString(); - buffer.Dispose(); - return result; + if (type == Type.Text) + { + return Text.ToString(); + } + else if (type == Type.Number) + { + double number = Number; + Span buffer = stackalloc char[64]; + int length = number.ToString(buffer); + return buffer.Slice(0, length).ToString(); + } + else if (type == Type.Boolean) + { + return Boolean ? "true" : "false"; + } + else if (type == Type.Object) + { + void* ptr = (void*)value.Read(); + JSONObject obj = new(ptr); + return obj.ToString(); + } + else if (type == Type.Array) + { + void* ptr = (void*)value.Read(); + JSONArray array = new(ptr); + return array.ToString(); + } + else if (type == Type.Null) + { + return "null"; + } + else + { + throw new InvalidOperationException($"Property is of an unknown type: {type}"); + } } public readonly bool TryGetText(out ReadOnlySpan text) diff --git a/source/JSON/JSONReader.cs b/source/JSON/JSONReader.cs index 3e28ce2..0ddda4f 100644 --- a/source/JSON/JSONReader.cs +++ b/source/JSON/JSONReader.cs @@ -30,113 +30,116 @@ public JSONReader(ByteReader reader) this.reader = reader; } - public readonly bool PeekToken(out Token token) + public readonly bool PeekToken(out Token token, out int readBytes) { - Span buffer = stackalloc char[8]; token = default; int position = reader.Position; - while (position < reader.Length) + int length = reader.Length; + while (position < length) { byte cLength = reader.PeekUTF8(position, out char c, out _); if (c == '{') { token = new Token(position, cLength, Token.Type.StartObject); + readBytes = position - reader.Position + 1; return true; } else if (c == '}') { token = new Token(position, cLength, Token.Type.EndObject); + readBytes = position - reader.Position + 1; return true; } else if (c == '[') { token = new Token(position, cLength, Token.Type.StartArray); + readBytes = position - reader.Position + 1; return true; } else if (c == ']') { token = new Token(position, cLength, Token.Type.EndArray); + readBytes = position - reader.Position + 1; return true; } + else if (c == ',' || c == ':' || SharedFunctions.IsWhitespace(c)) + { + position += cLength; + } else if (c == '"') { - int start = position; position += cLength; - while (position < reader.Length) + int start = position; + while (position < length) { cLength = reader.PeekUTF8(position, out c, out _); - position += cLength; if (c == '"') { token = new Token(start, position - start, Token.Type.Text); + readBytes = position - reader.Position + 2; return true; } - } - throw new InvalidOperationException($"Invalid JSON token at position {position}, expected '\"'."); + position += cLength; + } } - else if (c == 't' || c == 'f') + else if (c == '\'') { - int peekLength = reader.PeekUTF8(position, 5, buffer); - if (buffer.Slice(0, peekLength).SequenceEqual("false".AsSpan())) + position += cLength; + int start = position; + while (position < length) { - token = new Token(position, peekLength, Token.Type.False); - return true; - } + cLength = reader.PeekUTF8(position, out c, out _); + if (c == '\'') + { + token = new Token(start, position - start, Token.Type.Text); + readBytes = position - reader.Position + 2; + return true; + } - Span smallerBuffer = buffer.Slice(0, peekLength - 1); - if (smallerBuffer.SequenceEqual("true".AsSpan())) - { - token = new Token(position, peekLength - 1, Token.Type.True); - return true; + position += cLength; } - - throw new InvalidOperationException($"Unexpected token {c} at {position}."); } - else if (char.IsDigit(c) || c == '.' || c == '-') + else { int start = position; position += cLength; - while (position < reader.Length) + while (position < length) { cLength = reader.PeekUTF8(position, out c, out _); - if (!char.IsDigit(c) && c != '.' && c != '-') + if (c == '{' || c == '}' || c == '[' || c == ']' || c == ',' || c == ':' || SharedFunctions.IsWhitespace(c)) { - token = new Token(start, position - start, Token.Type.Number); + token = new Token(start, position - start, Token.Type.Text); + readBytes = position - reader.Position; return true; } position += cLength; } - throw new InvalidOperationException($"Invalid JSON token at position {position}, expected number."); - } - else - { - //skip - position += cLength; + throw new InvalidOperationException($"Unexpected end of stream while reading token, expected a JSON token to finish the text"); } } + readBytes = default; return false; } - public Token ReadToken() + public readonly Token ReadToken() { - PeekToken(out Token token); - reader.Position = token.position + token.length; + PeekToken(out Token token, out int readBytes); + reader.Advance(readBytes); return token; } - public bool ReadToken(out Token token) + public readonly bool ReadToken(out Token token) { - bool read = PeekToken(out token); - int end = token.position + token.length; - reader.Position = end; + bool read = PeekToken(out token, out int readBytes); + reader.Advance(readBytes); return read; } - public int ReadText(Span buffer) + public readonly int ReadText(Span buffer) { while (ReadToken(out Token token)) { @@ -157,7 +160,7 @@ public int ReadText(Span buffer) throw new InvalidOperationException("Expected token for text but none found"); } - public double ReadNumber() + public readonly double ReadNumber() { while (ReadToken(out Token token)) { @@ -165,7 +168,7 @@ public double ReadNumber() { //skip } - else if (token.type == Token.Type.Number) + else if (token.type == Token.Type.Text) { return GetNumber(token); } @@ -178,21 +181,28 @@ public double ReadNumber() throw new InvalidOperationException("Expected token for number but none found"); } - public bool ReadBoolean() + public readonly bool ReadBoolean() { + Span buffer = stackalloc char[32]; while (ReadToken(out Token token)) { if (token.type == Token.Type.EndObject || token.type == Token.Type.EndArray) { //skip } - else if (token.type == Token.Type.True) - { - return true; - } - else if (token.type == Token.Type.False) + else if (token.type == Token.Type.Text) { - return false; + int length = GetText(token, buffer); + if (buffer.Slice(0, length).SequenceEqual("true")) + { + return true; + } + else if (buffer.Slice(0, length).SequenceEqual("false")) + { + return false; + } + + throw new InvalidOperationException($"Could not parse {buffer.Slice(0, length).ToString()} as a boolean"); } else { @@ -203,7 +213,7 @@ public bool ReadBoolean() throw new InvalidOperationException("Expected token for boolean but none more found"); } - public T ReadObject() where T : unmanaged, IJSONSerializable + public readonly T ReadObject() where T : unmanaged, IJSONSerializable { while (ReadToken(out Token token)) { @@ -215,9 +225,10 @@ public T ReadObject() where T : unmanaged, IJSONSerializable { T obj = default; obj.Read(this); - if (PeekToken(out Token peek) && peek.type == Token.Type.EndObject) + if (PeekToken(out Token peek, out int readBytes) && peek.type == Token.Type.EndObject) { - ReadToken(); + reader.Advance(readBytes); + //reached end of object } return obj; @@ -233,17 +244,7 @@ public T ReadObject() where T : unmanaged, IJSONSerializable public unsafe readonly int GetText(Token token, Span destination) { - int length = reader.PeekUTF8(token.position, token.length, destination); - if (destination[0] == '"') - { - for (int i = 0; i < length - 1; i++) - { - destination[i] = destination[i + 1]; - } - - return length - 2; - } - else return length; + return reader.PeekUTF8(token.position, token.length, destination); } public readonly double GetNumber(Token token) diff --git a/source/JSON/JSONWriter.cs b/source/JSON/JSONWriter.cs index ecafdc6..27f43c2 100644 --- a/source/JSON/JSONWriter.cs +++ b/source/JSON/JSONWriter.cs @@ -99,7 +99,7 @@ public void WriteNumber(double number) Span buffer = stackalloc char[32]; int length = number.ToString(buffer); - last = new(writer.Position, sizeof(char) * length, Token.Type.Number); + last = new(writer.Position, sizeof(char) * length, Token.Type.Text); writer.WriteUTF8(buffer.Slice(0, length)); } @@ -107,19 +107,19 @@ public void WriteBoolean(bool value) { if (value) { - last = new(writer.Position, sizeof(char) * 4, Token.Type.True); + last = new(writer.Position, sizeof(char) * 4, Token.Type.Text); writer.WriteUTF8("true".AsSpan()); } else { - last = new(writer.Position, sizeof(char) * 5, Token.Type.False); + last = new(writer.Position, sizeof(char) * 5, Token.Type.Text); writer.WriteUTF8("false".AsSpan()); } } public void WriteNull() { - last = new(writer.Position, sizeof(char) * 4, Token.Type.Null); + last = new(writer.Position, sizeof(char) * 4, Token.Type.Text); writer.WriteUTF8("null".AsSpan()); } diff --git a/source/JSON/Token.cs b/source/JSON/Token.cs index dd65af4..72dbc07 100644 --- a/source/JSON/Token.cs +++ b/source/JSON/Token.cs @@ -35,10 +35,6 @@ public enum Type : byte StartArray, EndArray, Text, - Number, - True, - False, - Null } } } diff --git a/source/SharedFunctions.cs b/source/SharedFunctions.cs new file mode 100644 index 0000000..078d2e1 --- /dev/null +++ b/source/SharedFunctions.cs @@ -0,0 +1,12 @@ +namespace Serialization +{ + internal static class SharedFunctions + { + private const char BOM = (char)65279; + + public static bool IsWhitespace(char character) + { + return character == ' ' || character == '\t' || character == '\n' || character == '\r' || character == BOM; + } + } +} \ No newline at end of file diff --git a/source/XML/XMLReader.cs b/source/XML/XMLReader.cs index 5a8bb01..634d706 100644 --- a/source/XML/XMLReader.cs +++ b/source/XML/XMLReader.cs @@ -82,7 +82,7 @@ public readonly bool PeekToken(int position, out Token token) //skip position += cLength; } - else if (char.IsLetterOrDigit(c) || !IsWhitespace(c)) + else if (char.IsLetterOrDigit(c) || !SharedFunctions.IsWhitespace(c)) { int start = position; position += cLength; @@ -186,11 +186,5 @@ public readonly int GetText(Token token, Text destination) destination.Append(buffer.Slice(0, length)); return length; } - - private static bool IsWhitespace(char c) - { - const char BOM = (char)65279; - return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == BOM; - } } } \ No newline at end of file diff --git a/tests/JSONTests.cs b/tests/JSONTests.cs index f0b8ed7..67828ec 100644 --- a/tests/JSONTests.cs +++ b/tests/JSONTests.cs @@ -1,3 +1,4 @@ +using Collections.Generic; using Serialization.JSON; using System; using System.Text.Json; @@ -9,6 +10,103 @@ namespace Serialization.Tests { public class JSONTests : UnmanagedTests { + [Test] + public void ParseTokens() + { + JsonObject json = new() + { + { "name", "John Programming" }, + { "age", 42 }, + { "isAlive", true }, + { + "address", new JsonObject + { + { "streetAddress", "21 2nd Street" }, + { "city", "New York" }, + { "state", "NY" }, + { "postalCode", "10021-3100" } + } + } + }; + + string jsonString = json.ToString(); + using ByteReader reader = ByteReader.CreateFromUTF8(jsonString); + JSONReader jsonReader = new(reader); + using List tokens = new(); + while (jsonReader.ReadToken(out Token token)) + { + tokens.Add(token); + Console.WriteLine($"{token.type} = {token.ToString(jsonReader)}"); + } + + Assert.That(tokens.Count, Is.EqualTo(19)); + Assert.That(tokens[0].type == Token.Type.StartObject); + Assert.That(tokens[1].type == Token.Type.Text); //name + Assert.That(tokens[2].type == Token.Type.Text); //John Doe + Assert.That(tokens[3].type == Token.Type.Text); //age + Assert.That(tokens[4].type == Token.Type.Text); //42 + Assert.That(tokens[5].type == Token.Type.Text); //isAlive + Assert.That(tokens[6].type == Token.Type.Text); //true + Assert.That(tokens[7].type == Token.Type.Text); //address + Assert.That(tokens[8].type == Token.Type.StartObject); + Assert.That(tokens[9].type == Token.Type.Text); //streetAddress + Assert.That(tokens[10].type == Token.Type.Text); //21 2nd Street + Assert.That(tokens[11].type == Token.Type.Text); //city + Assert.That(tokens[12].type == Token.Type.Text); //New York + Assert.That(tokens[13].type == Token.Type.Text); //state + Assert.That(tokens[14].type == Token.Type.Text); //NY + Assert.That(tokens[15].type == Token.Type.Text); //postalCode + Assert.That(tokens[16].type == Token.Type.Text); //10021-3100 + Assert.That(tokens[17].type == Token.Type.EndObject); + Assert.That(tokens[18].type == Token.Type.EndObject); + } + + [Test] + public void ParseJSON5Tokens() + { + string source = @"{ + name: 'John Programming', + age: 42, + isAlive: true, + address: { + streetAddress: '21 2nd Street', + city: 'New York', + state: 'NY', + postalCode: '10021-3100' + } + }"; + + using ByteReader reader = ByteReader.CreateFromUTF8(source); + JSONReader jsonReader = new(reader); + using List tokens = new(); + while (jsonReader.ReadToken(out Token token)) + { + tokens.Add(token); + Console.WriteLine($"{token.type} = {token.ToString(jsonReader)}"); + } + + Assert.That(tokens.Count, Is.EqualTo(19)); + Assert.That(tokens[0].type == Token.Type.StartObject); + Assert.That(tokens[1].type == Token.Type.Text); //name + Assert.That(tokens[2].type == Token.Type.Text); //John Doe + Assert.That(tokens[3].type == Token.Type.Text); //age + Assert.That(tokens[4].type == Token.Type.Text); //42 + Assert.That(tokens[5].type == Token.Type.Text); //isAlive + Assert.That(tokens[6].type == Token.Type.Text); //true + Assert.That(tokens[7].type == Token.Type.Text); //address + Assert.That(tokens[8].type == Token.Type.StartObject); + Assert.That(tokens[9].type == Token.Type.Text); //streetAddress + Assert.That(tokens[10].type == Token.Type.Text); //21 2nd Street + Assert.That(tokens[11].type == Token.Type.Text); //city + Assert.That(tokens[12].type == Token.Type.Text); //New York + Assert.That(tokens[13].type == Token.Type.Text); //state + Assert.That(tokens[14].type == Token.Type.Text); //NY + Assert.That(tokens[15].type == Token.Type.Text); //postalCode + Assert.That(tokens[16].type == Token.Type.Text); //10021-3100 + Assert.That(tokens[17].type == Token.Type.EndObject); + Assert.That(tokens[18].type == Token.Type.EndObject); + } + [Test] public void ReadSampleJSON() { @@ -138,24 +236,22 @@ public void ListOfSettings() { length = jsonReader.GetText(next, buffer); string value = buffer.Slice(0, length).ToString(); - settingsList.Add((name, value)); + if (double.TryParse(value, out double number)) + { + settingsList.Add((name, number)); + } + else if (bool.TryParse(value, out bool boolean)) + { + settingsList.Add((name, boolean)); + } + else + { + settingsList.Add((name, value)); + } } - else if (next.type == Token.Type.Number) + else { - double value = jsonReader.GetNumber(next); - settingsList.Add((name, value)); - } - else if (next.type == Token.Type.True) - { - settingsList.Add((name, true)); - } - else if (next.type == Token.Type.False) - { - settingsList.Add((name, false)); - } - else if (next.type == Token.Type.Null) - { - settingsList.Add((name, null)); + throw new Exception($"Expected text token, but got {next.type}"); } } else @@ -181,7 +277,8 @@ public void ReadJSONWithArray() { JsonObject json = new(); JsonArray inventory = new(); - for (uint i = 0; i < 32; i++) + const int ItemCount = 32; + for (uint i = 0; i < ItemCount; i++) { JsonObject item = new(); item.Add("name", $"Item {i}"); @@ -196,7 +293,7 @@ public void ReadJSONWithArray() using ByteReader reader = ByteReader.CreateFromUTF8(jsonString); JSONObject obj = reader.ReadObject(); JSONArray array = obj.GetArray("inventory"); - Assert.That(array.Count, Is.EqualTo(32)); + Assert.That(array.Count, Is.EqualTo(ItemCount)); string otherString = obj.ToString(); Assert.That(jsonString, Is.EqualTo(otherString)); obj.Dispose(); From af7285dc2d9fd49a479e120d7fd16090df928b93 Mon Sep 17 00:00:00 2001 From: Phill Date: Thu, 24 Apr 2025 11:25:55 -0400 Subject: [PATCH 2/7] Update README.md --- README.md | 61 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index ae73b96..48536f2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,17 @@ # Serialization -Unmanaged library for working with JSON and XML using readers and writers with bytes directly. -As well as intermediary types for representing objects within the supported formats. -### JSON Reader and writer +Unmanaged library for working with common human readable formats using readers and +writers with bytes directly. As well as intermediary/high-level types for representing objects +within the supported formats. + +### Supported formats + +- JSON +- JSON 5 +- XML + +### JSON reader and writer + The reader and writers are used to iteratively progress over data. How the data is stored should be known ahead of time (and can be tested). ```cs @@ -18,7 +27,8 @@ ReadOnlySpan propertyValue = reader.ReadText(out ReadOnlySpan proper reader.ReadEndObject(); ``` -### Generic JSON Object +### Generic JSON object + This is an alternative type thats able to represent a JSON object without the need for interacting with the reader or writer. ```cs @@ -39,48 +49,43 @@ jsonObject.Add("inventory", inventory); jsonObject["age"].Number++; -using UnmanagedList buffer = new(); -jsonObject.ToString(buffer, " ", true, true); -ReadOnlySpan jsonText = buffer.AsSpan(); -Console.WriteLine(jsonText.ToString()); +using Text jsonText = new(); +jsonObject.ToString(jsonText, " ", true, true); +Console.WriteLine(jsonText); ``` + +JSON result: ```json { - "name":"John Doe", - "age":43, - "alive":true, - "inventory":[ + "name": "John Doe", + "age": 43, + "alive": true, + "inventory": [ "apples", "oranges", { - "name":"cherry", - "color":"red" + "name": "cherry", + "color": "red" } ] } ``` ### JSON to C# and back -The readers and writers have API for serializing/deserializing `IJSONObject` values. -If the JSON object contains text values, then a `ReadOnlySpan` will be expected -to be stored inside the value type. Using types like `string` and `List` makes the type -not acceptable because of the unmanaged requirement, an `UnmanagedArray` is used instead. + +The readers and writers have API for serializing/deserializing `IJSONObject` values: ```cs public struct Player : IJSONObject, IDisposable { public int hp; public bool alive; - private UnmanagedArray name; + private Text name; public readonly Span Name { get => name.AsSpan(); - set - { - name.Resize(value.Length); - value.CopyTo(name.AsSpan()); - } + set => name.CopyFrom(value); } public Player(int hp, bool alive, ReadOnlySpan name) @@ -112,15 +117,17 @@ public struct Player : IJSONObject, IDisposable } byte[] jsonBytes = File.ReadAllBytes("player.json"); -using JSONReader reader = new(jsonBytes); -using Player player = reader.ReadObject(); +using ByteReader reader = new(jsonBytes); +JSONReader jsonReader = new(reader); +using Player player = jsonReader.ReadObject(); ReadOnlySpan name = player.Name; ``` ### XML + XML is supported through the `XMLNode` type, which can be created from either a byte or a char array. Each node has a name, content, and a list of children. Attributes can be read using the indexer. -```csharp +```cs byte[] xmlData = File.ReadAllBytes("solution.csproj"); using XMLNode project = new(xmlData); XMLAttribute sdk = project["Sdk"]; From c5ba452a114b77347457875b6b54de7c3ebeef61 Mon Sep 17 00:00:00 2001 From: Phill Date: Fri, 25 Apr 2025 10:39:41 -0400 Subject: [PATCH 3/7] polish json types --- source/JSON/JSONArray.cs | 126 +++++++------- source/JSON/JSONObject.cs | 214 +++++++++++++++--------- source/JSON/JSONProperty.cs | 318 ++++++++++++++++++++++++------------ source/JSON/Token.cs | 4 + 4 files changed, 419 insertions(+), 243 deletions(-) diff --git a/source/JSON/JSONArray.cs b/source/JSON/JSONArray.cs index 13350d8..7d829c5 100644 --- a/source/JSON/JSONArray.cs +++ b/source/JSON/JSONArray.cs @@ -9,12 +9,30 @@ namespace Serialization.JSON [SkipLocalsInit] public unsafe struct JSONArray : IDisposable, ISerializable { - private Implementation* value; + private Implementation* jsonArray; - public readonly int Count => value->elements.Count; - public readonly bool IsDisposed => value is null; - public readonly nint Address => (nint)value; - public readonly ReadOnlySpan Elements => value->elements.AsSpan(); + public readonly int Count + { + get + { + ThrowIfDisposed(); + + return jsonArray->elements.Count; + } + } + + public readonly bool IsDisposed => jsonArray is null; + public readonly nint Address => (nint)jsonArray; + + public readonly ReadOnlySpan Elements + { + get + { + ThrowIfDisposed(); + + return jsonArray->elements.AsSpan(); + } + } public readonly JSONProperty this[int index] { @@ -23,26 +41,35 @@ public readonly JSONProperty this[int index] ThrowIfDisposed(); ThrowIfOutOfRange(index); - return value->elements[index]; + return jsonArray->elements[index]; } } #if NET public JSONArray() { - value = Implementation.Allocate(); + jsonArray = MemoryAddress.AllocatePointer(); + jsonArray->elements = new(4); } #endif public JSONArray(void* value) { - this.value = (Implementation*)value; + this.jsonArray = (Implementation*)value; } public void Dispose() { ThrowIfDisposed(); - Implementation.Free(ref value); + + Span elements = jsonArray->elements.AsSpan(); + for (int i = 0; i < elements.Length; i++) + { + elements[i].Dispose(); + } + + jsonArray->elements.Dispose(); + MemoryAddress.Free(ref jsonArray); } public readonly void ToString(Text result, ReadOnlySpan indent = default, bool cr = false, bool lf = false, byte depth = 0) @@ -50,7 +77,7 @@ public readonly void ToString(Text result, ReadOnlySpan indent = default, ThrowIfDisposed(); result.Append('['); - if (value->elements.Count > 0) + if (jsonArray->elements.Count > 0) { NewLine(); for (int i = 0; i <= depth; i++) @@ -61,7 +88,7 @@ public readonly void ToString(Text result, ReadOnlySpan indent = default, int position = 0; while (true) { - ref JSONProperty element = ref value->elements[position]; + ref JSONProperty element = ref jsonArray->elements[position]; byte childDepth = depth; childDepth++; element.ToString(result, false, indent, cr, lf, childDepth); @@ -110,6 +137,8 @@ void Indent(ReadOnlySpan indent) public readonly override string ToString() { + ThrowIfDisposed(); + Text result = new(0); ToString(result); string text = result.ToString(); @@ -129,7 +158,7 @@ private readonly void ThrowIfDisposed() [Conditional("DEBUG")] private readonly void ThrowIfOutOfRange(int index) { - if (index >= Count) + if (index >= Count || index < 0) { throw new IndexOutOfRangeException($"Index {index} is out of range"); } @@ -140,9 +169,9 @@ public readonly void Add(ReadOnlySpan text) ThrowIfDisposed(); Span nameBuffer = stackalloc char[16]; - int index = value->elements.Count; + int index = jsonArray->elements.Count; int length = index.ToString(nameBuffer); - value->elements.Add(new JSONProperty(nameBuffer.Slice(0, length), text)); + jsonArray->elements.Add(new JSONProperty(nameBuffer.Slice(0, length), text)); } public readonly void Add(string text) @@ -155,9 +184,9 @@ public readonly void Add(double number) ThrowIfDisposed(); Span nameBuffer = stackalloc char[16]; - int index = value->elements.Count; + int index = jsonArray->elements.Count; int length = index.ToString(nameBuffer); - value->elements.Add(new JSONProperty(nameBuffer.Slice(0, length), number)); + jsonArray->elements.Add(new JSONProperty(nameBuffer.Slice(0, length), number)); } public readonly void Add(bool boolean) @@ -165,9 +194,9 @@ public readonly void Add(bool boolean) ThrowIfDisposed(); Span nameBuffer = stackalloc char[16]; - int index = value->elements.Count; + int index = jsonArray->elements.Count; int length = index.ToString(nameBuffer); - value->elements.Add(new JSONProperty(nameBuffer.Slice(0, length), boolean)); + jsonArray->elements.Add(new JSONProperty(nameBuffer.Slice(0, length), boolean)); } public readonly void Add(JSONObject jsonObject) @@ -175,9 +204,9 @@ public readonly void Add(JSONObject jsonObject) ThrowIfDisposed(); Span nameBuffer = stackalloc char[16]; - int index = value->elements.Count; + int index = jsonArray->elements.Count; int length = index.ToString(nameBuffer); - value->elements.Add(new JSONProperty(nameBuffer.Slice(0, length), jsonObject)); + jsonArray->elements.Add(new JSONProperty(nameBuffer.Slice(0, length), jsonObject)); } public readonly void Add(JSONArray jsonArray) @@ -185,9 +214,9 @@ public readonly void Add(JSONArray jsonArray) ThrowIfDisposed(); Span nameBuffer = stackalloc char[16]; - int index = value->elements.Count; + int index = this.jsonArray->elements.Count; int length = index.ToString(nameBuffer); - value->elements.Add(new JSONProperty(nameBuffer.Slice(0, length), jsonArray)); + this.jsonArray->elements.Add(new JSONProperty(nameBuffer.Slice(0, length), jsonArray)); } public readonly void AddNull() @@ -195,13 +224,15 @@ public readonly void AddNull() ThrowIfDisposed(); Span nameBuffer = stackalloc char[16]; - int index = value->elements.Count; + int index = jsonArray->elements.Count; int length = index.ToString(nameBuffer); - value->elements.Add(new JSONProperty(nameBuffer.Slice(0, length))); + jsonArray->elements.Add(new JSONProperty(nameBuffer.Slice(0, length))); } readonly void ISerializable.Write(ByteWriter writer) { + ThrowIfDisposed(); + Text list = new(0); ToString(list); writer.WriteUTF8(list.AsSpan()); @@ -210,7 +241,8 @@ readonly void ISerializable.Write(ByteWriter writer) void ISerializable.Read(ByteReader reader) { - value = Implementation.Allocate(); + jsonArray = MemoryAddress.AllocatePointer(); + jsonArray->elements = new(4); using Text textBuffer = new(256); JSONReader jsonReader = new(reader); while (jsonReader.ReadToken(out Token token)) @@ -229,15 +261,15 @@ void ISerializable.Read(ByteReader reader) { Add(number); } - else if (text.SequenceEqual("true")) + else if (text.SequenceEqual(Token.True)) { Add(true); } - else if (text.SequenceEqual("false")) + else if (text.SequenceEqual(Token.False)) { Add(false); } - else if (text.SequenceEqual("null")) + else if (text.SequenceEqual(Token.Null)) { AddNull(); } @@ -265,42 +297,14 @@ void ISerializable.Read(ByteReader reader) public static JSONArray Create() { - return new(Implementation.Allocate()); + Implementation* jsonArray = MemoryAddress.AllocatePointer(); + jsonArray->elements = new(4); + return new(jsonArray); } - public readonly struct Implementation + private struct Implementation { - public readonly List elements; - - private Implementation(List elements) - { - this.elements = elements; - } - - public static Implementation* Allocate() - { - List elements = new(4); - ref Implementation value = ref MemoryAddress.Allocate(); - value = new(elements); - fixed (Implementation* pointer = &value) - { - return pointer; - } - } - - public static void Free(ref Implementation* array) - { - MemoryAddress.ThrowIfDefault(array); - - for (int i = 0; i < array->elements.Count; i++) - { - JSONProperty property = array->elements[i]; - property.Dispose(); - } - - array->elements.Dispose(); - MemoryAddress.Free(ref array); - } + public List elements; } } } \ No newline at end of file diff --git a/source/JSON/JSONObject.cs b/source/JSON/JSONObject.cs index 9ad1309..a03e542 100644 --- a/source/JSON/JSONObject.cs +++ b/source/JSON/JSONObject.cs @@ -2,6 +2,7 @@ using System; using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Text.Json.Nodes; using Unmanaged; namespace Serialization.JSON @@ -12,22 +13,38 @@ namespace Serialization.JSON [SkipLocalsInit] public unsafe struct JSONObject : IDisposable, ISerializable { - private Implementation* value; + private Implementation* jsonObject; - public readonly ReadOnlySpan Properties => value->properties.AsSpan(); - public readonly int Count => value->properties.Count; - public readonly bool IsDisposed => value is null; + public readonly ReadOnlySpan Properties + { + get + { + ThrowIfDisposed(); + + return jsonObject->properties.AsSpan(); + } + } + + public readonly int Count + { + get + { + ThrowIfDisposed(); + + return jsonObject->properties.Count; + } + } + + public readonly bool IsDisposed => jsonObject is null; public readonly ref JSONProperty this[int index] { get { - if (index >= Count) - { - throw new IndexOutOfRangeException(); - } + ThrowIfDisposed(); + ThrowIfPropertyIndexIsOutOfRange(index); - return ref value->properties[index]; + return ref jsonObject->properties[index]; } } @@ -35,23 +52,45 @@ public readonly ref JSONProperty this[ReadOnlySpan name] { get { + ThrowIfDisposed(); + ThrowIfPropertyIsMissing(name); + int count = Count; for (int i = 0; i < count; i++) { - ref JSONProperty property = ref value->properties[i]; + ref JSONProperty property = ref jsonObject->properties[i]; if (property.Name.SequenceEqual(name)) { return ref property; } } - throw new NullReferenceException($"Property `{name.ToString()}` not found"); + return ref Unsafe.AsRef(default); } } - public readonly ref JSONProperty this[string name] => ref this[name.AsSpan()]; + public readonly ref JSONProperty this[string name] + { + get + { + ThrowIfDisposed(); + ThrowIfPropertyIsMissing(name); - public readonly nint Address => (nint)value; + int count = Count; + for (int i = 0; i < count; i++) + { + ref JSONProperty property = ref jsonObject->properties[i]; + if (property.Name.SequenceEqual(name)) + { + return ref property; + } + } + + return ref Unsafe.AsRef(default); + } + } + + public readonly nint Address => (nint)jsonObject; #if NET /// @@ -59,32 +98,42 @@ public readonly ref JSONProperty this[ReadOnlySpan name] /// public JSONObject() { - value = Implementation.Allocate(); + jsonObject = MemoryAddress.AllocatePointer(); + jsonObject->properties = new(4); } #endif public JSONObject(void* value) { - this.value = (Implementation*)value; + this.jsonObject = (Implementation*)value; } public void Dispose() { ThrowIfDisposed(); - Implementation.Free(ref value); + Span properties = jsonObject->properties.AsSpan(); + for (int i = 0; i < properties.Length; i++) + { + properties[i].Dispose(); + } + + jsonObject->properties.Dispose(); + MemoryAddress.Free(ref jsonObject); } public readonly void Clear() { - value->properties.Clear(); + ThrowIfDisposed(); + + jsonObject->properties.Clear(); } public readonly void RemoveAt(int index) { ThrowIfDisposed(); - value->properties.RemoveAtBySwapping(index); + jsonObject->properties.RemoveAtBySwapping(index); } public readonly T As() where T : unmanaged, IJSONSerializable @@ -106,7 +155,7 @@ public readonly void ToString(Text result, ReadOnlySpan indent = default, ThrowIfDisposed(); result.Append('{'); - if (value->properties.Count > 0) + if (jsonObject->properties.Count > 0) { NewLine(); for (int i = 0; i <= depth; i++) @@ -117,7 +166,7 @@ public readonly void ToString(Text result, ReadOnlySpan indent = default, int position = 0; while (true) { - ref JSONProperty property = ref value->properties[position]; + ref JSONProperty property = ref jsonObject->properties[position]; byte childDepth = depth; childDepth++; property.ToString(result, true, indent, cr, lf, childDepth); @@ -166,6 +215,8 @@ void Indent(ReadOnlySpan indent) public readonly override string ToString() { + ThrowIfDisposed(); + Text buffer = new(0); ToString(buffer); string result = buffer.ToString(); @@ -182,17 +233,38 @@ private readonly void ThrowIfDisposed() } } + [Conditional("DEBUG")] + private readonly void ThrowIfPropertyIsMissing(ReadOnlySpan name) + { + if (!Contains(name)) + { + throw new NullReferenceException($"Property `{name.ToString()}` not found"); + } + } + + [Conditional("DEBUG")] + private readonly void ThrowIfPropertyIndexIsOutOfRange(int index) + { + if (index < 0 || index >= Count) + { + throw new IndexOutOfRangeException($"Property index `{index}` is out of range"); + } + } + public readonly void Add(ReadOnlySpan name, ReadOnlySpan text) { ThrowIfDisposed(); JSONProperty property = new(name, text); - value->properties.Add(property); + jsonObject->properties.Add(property); } public readonly void Add(string name, string text) { - Add(name.AsSpan(), text.AsSpan()); + ThrowIfDisposed(); + + JSONProperty property = new(name, text); + jsonObject->properties.Add(property); } public readonly void Add(ReadOnlySpan name, double number) @@ -200,12 +272,15 @@ public readonly void Add(ReadOnlySpan name, double number) ThrowIfDisposed(); JSONProperty property = new(name, number); - value->properties.Add(property); + jsonObject->properties.Add(property); } public readonly void Add(string name, double number) { - Add(name.AsSpan(), number); + ThrowIfDisposed(); + + JSONProperty property = new(name, number); + jsonObject->properties.Add(property); } public readonly void Add(ReadOnlySpan name, bool boolean) @@ -213,38 +288,47 @@ public readonly void Add(ReadOnlySpan name, bool boolean) ThrowIfDisposed(); JSONProperty property = new(name, boolean); - value->properties.Add(property); + jsonObject->properties.Add(property); } public readonly void Add(string name, bool boolean) { - Add(name.AsSpan(), boolean); + ThrowIfDisposed(); + + JSONProperty property = new(name, boolean); + jsonObject->properties.Add(property); } - public readonly void Add(ReadOnlySpan name, JSONObject obj) + public readonly void Add(ReadOnlySpan name, JSONObject jsonObject) { ThrowIfDisposed(); - JSONProperty property = new(name, obj); - value->properties.Add(property); + JSONProperty property = new(name, jsonObject); + this.jsonObject->properties.Add(property); } - public readonly void Add(string name, JSONObject obj) + public readonly void Add(string name, JSONObject jsonObject) { - Add(name.AsSpan(), obj); + ThrowIfDisposed(); + + JSONProperty property = new(name, jsonObject); + this.jsonObject->properties.Add(property); } - public readonly void Add(ReadOnlySpan name, JSONArray array) + public readonly void Add(ReadOnlySpan name, JSONArray jsonArray) { ThrowIfDisposed(); - JSONProperty property = new(name, array); - value->properties.Add(property); + JSONProperty property = new(name, jsonArray); + jsonObject->properties.Add(property); } - public readonly void Add(string name, JSONArray array) + public readonly void Add(string name, JSONArray jsonArray) { - Add(name.AsSpan(), array); + ThrowIfDisposed(); + + JSONProperty property = new(name, jsonArray); + jsonObject->properties.Add(property); } public readonly void AddNull(ReadOnlySpan name) @@ -252,12 +336,15 @@ public readonly void AddNull(ReadOnlySpan name) ThrowIfDisposed(); JSONProperty property = new(name); - value->properties.Add(property); + jsonObject->properties.Add(property); } public readonly void AddNull(string name) { - AddNull(name.AsSpan()); + ThrowIfDisposed(); + + JSONProperty property = new(name); + jsonObject->properties.Add(property); } public readonly bool Contains(ReadOnlySpan name) @@ -267,7 +354,7 @@ public readonly bool Contains(ReadOnlySpan name) int count = Count; for (int i = 0; i < count; i++) { - ref JSONProperty property = ref value->properties[i]; + ref JSONProperty property = ref jsonObject->properties[i]; if (property.Name.SequenceEqual(name)) { return true; @@ -451,7 +538,8 @@ readonly void ISerializable.Write(ByteWriter writer) void ISerializable.Read(ByteReader reader) { - value = Implementation.Allocate(); + jsonObject = MemoryAddress.AllocatePointer(); + jsonObject->properties = new(4); JSONReader jsonReader = new(reader); if (jsonReader.PeekToken(out Token nextToken, out int readBytes)) { @@ -462,6 +550,7 @@ void ISerializable.Read(ByteReader reader) } } + //todo: share these temp buffers? using Text nameTextBuffer = new(256); using Text nextTextBuffer = new(256); while (jsonReader.ReadToken(out Token token)) @@ -492,15 +581,15 @@ void ISerializable.Read(ByteReader reader) { Add(name, number); } - else if (nextText.SequenceEqual("true")) + else if (nextText.SequenceEqual(Token.True)) { Add(name, true); } - else if (nextText.SequenceEqual("false")) + else if (nextText.SequenceEqual(Token.False)) { Add(name, false); } - else if (nextText.SequenceEqual("null")) + else if (nextText.SequenceEqual(Token.Null)) { AddNull(name); } @@ -546,43 +635,14 @@ void ISerializable.Read(ByteReader reader) public static JSONObject Create() { - return new(Implementation.Allocate()); + Implementation* jsonObject = MemoryAddress.AllocatePointer(); + jsonObject->properties = new(4); + return new JSONObject(jsonObject); } - public readonly struct Implementation + private struct Implementation { - public readonly List properties; - - private Implementation(List properties) - { - this.properties = properties; - } - - public static Implementation* Allocate() - { - List properties = new(4); - ref Implementation value = ref MemoryAddress.Allocate(); - value = new(properties); - fixed (Implementation* pointer = &value) - { - return pointer; - } - } - - public static void Free(ref Implementation* obj) - { - MemoryAddress.ThrowIfDefault(obj); - - int count = obj->properties.Count; - for (int i = 0; i < count; i++) - { - JSONProperty property = obj->properties[i]; - property.Dispose(); - } - - obj->properties.Dispose(); - MemoryAddress.Free(ref obj); - } + public List properties; } } } \ No newline at end of file diff --git a/source/JSON/JSONProperty.cs b/source/JSON/JSONProperty.cs index cd6ca01..246d879 100644 --- a/source/JSON/JSONProperty.cs +++ b/source/JSON/JSONProperty.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Runtime.CompilerServices; using Unmanaged; @@ -12,83 +13,170 @@ public struct JSONProperty : IDisposable private int length; private Type type; - public readonly bool IsText => type == Type.Text; - public readonly bool IsNumber => type == Type.Number; - public readonly bool IsBoolean => type == Type.Boolean; - public readonly bool IsObject => type == Type.Object; - public readonly bool IsArray => type == Type.Array; - public readonly bool IsNull => type == Type.Null; - public readonly Type PropertyType => type; - public readonly ReadOnlySpan Name => name.AsSpan(); + public readonly bool IsText + { + get + { + ThrowIfDisposed(); + + return type == Type.Text; + } + } + + public readonly bool IsNumber + { + get + { + ThrowIfDisposed(); + + return type == Type.Number; + } + } + + public readonly bool IsBoolean + { + get + { + ThrowIfDisposed(); + + return type == Type.Boolean; + } + } + + public readonly bool IsObject + { + get + { + ThrowIfDisposed(); + + return type == Type.Object; + } + } + + public readonly bool IsArray + { + get + { + ThrowIfDisposed(); + + return type == Type.Array; + } + } + + public readonly bool IsNull + { + get + { + ThrowIfDisposed(); + + return type == Type.Null; + } + } + + public readonly Type PropertyType + { + get + { + ThrowIfDisposed(); + + return type; + } + } + + public readonly ReadOnlySpan Name + { + get + { + ThrowIfDisposed(); + + return name.AsSpan(); + } + } + public readonly bool IsDisposed => type == default || name.IsDisposed; - public unsafe ReadOnlySpan Text + public ReadOnlySpan Text { readonly get { - if (IsText) - { - return new(value.Pointer, length / sizeof(char)); - } - else - { - throw new InvalidOperationException($"Property is not of type {Type.Text}"); - } + ThrowIfDisposed(); + ThrowIfTypeMismatch(Type.Text); + + return value.GetSpan(length / sizeof(char)); } set { - if (IsText) - { - int newLength = value.Length * sizeof(char); - if (length < newLength) - { - MemoryAddress.Resize(ref this.value, newLength); - } - - length = newLength; - this.value.Write(0, value); - } - else + ThrowIfDisposed(); + ThrowIfTypeMismatch(Type.Text); + + int newLength = value.Length * sizeof(char); + if (length < newLength) { - throw new InvalidOperationException($"Property is not of type {Type.Text}"); + MemoryAddress.Resize(ref this.value, newLength); } + + length = newLength; + this.value.Write(0, value); + } + } + + public readonly ref double Number + { + get + { + ThrowIfDisposed(); + ThrowIfTypeMismatch(Type.Number); + + return ref value.Read(); } } - public readonly ref double Number => ref value.Read(); - public readonly ref bool Boolean => ref value.Read(); + public readonly ref bool Boolean + { + get + { + ThrowIfDisposed(); + ThrowIfTypeMismatch(Type.Boolean); + + return ref value.Read(); + } + } public readonly JSONObject Object { - get => value.Read(); + get + { + ThrowIfDisposed(); + ThrowIfTypeMismatch(Type.Object); + + return value.Read(); + } set { - if (IsObject) - { - this.value.Read().Dispose(); - this.value.Write(0, value); - } - else - { - throw new InvalidOperationException($"Property is not of type {Type.Object}"); - } + ThrowIfDisposed(); + ThrowIfTypeMismatch(Type.Object); + + this.value.Read().Dispose(); + this.value.Write(0, value); } } public readonly JSONArray Array { - get => value.Read(); + get + { + ThrowIfDisposed(); + ThrowIfTypeMismatch(Type.Array); + + return value.Read(); + } set { - if (IsArray) - { - this.value.Read().Dispose(); - this.value.Write(0, value); - } - else - { - throw new InvalidOperationException($"Property is not of type {Type.Array}"); - } + ThrowIfDisposed(); + ThrowIfTypeMismatch(Type.Array); + + this.value.Read().Dispose(); + this.value.Write(0, value); } } @@ -104,39 +192,34 @@ public JSONProperty(ReadOnlySpan name, ReadOnlySpan text) public JSONProperty(ReadOnlySpan name, double number) { this.name = new(name); - length = sizeof(double); - value = MemoryAddress.Allocate(length); - value.Write(0, number); + value = MemoryAddress.AllocateValue(number, out length); type = Type.Number; } public JSONProperty(ReadOnlySpan name, bool boolean) { this.name = new(name); - length = sizeof(bool); - value = MemoryAddress.Allocate(length); - value.Write(0, boolean); + value = MemoryAddress.AllocateValue(boolean, out length); type = Type.Boolean; } - public unsafe JSONProperty(ReadOnlySpan name, JSONObject obj) + public JSONProperty(ReadOnlySpan name, JSONObject jsonObject) { this.name = new(name); - length = sizeof(nint); - value = MemoryAddress.Allocate(length); - value.Write(0, obj.Address); + value = MemoryAddress.AllocateValue(jsonObject, out length); type = Type.Object; } - public unsafe JSONProperty(ReadOnlySpan name, JSONArray array) + public JSONProperty(ReadOnlySpan name, JSONArray jsonArray) { this.name = new(name); - length = sizeof(nint); - value = MemoryAddress.Allocate(length); - value.Write(0, array.Address); + value = MemoryAddress.AllocateValue(jsonArray, out length); type = Type.Array; } + /// + /// Creates a null property. + /// public JSONProperty(ReadOnlySpan name) { this.name = new(name); @@ -145,18 +228,36 @@ public JSONProperty(ReadOnlySpan name) type = Type.Null; } - public unsafe void Dispose() + [Conditional("DEBUG")] + private readonly void ThrowIfDisposed() + { + if (type == default) + { + throw new ObjectDisposedException(nameof(JSONProperty), "The JSON property has been disposed"); + } + } + + [Conditional("DEBUG")] + private readonly void ThrowIfTypeMismatch(Type desiredType) { + if (type != desiredType) + { + throw new InvalidOperationException($"Property is not of type {desiredType}"); + } + } + + public void Dispose() + { + ThrowIfDisposed(); + if (type == Type.Object) { - nint address = value.Read(); - JSONObject jsonObject = new((void*)address); + JSONObject jsonObject = value.Read(); jsonObject.Dispose(); } else if (type == Type.Array) { - nint address = value.Read(); - JSONArray jsonArray = new((void*)address); + JSONArray jsonArray = value.Read(); jsonArray.Dispose(); } @@ -165,8 +266,10 @@ public unsafe void Dispose() type = default; } - public unsafe readonly void ToString(Text result, bool prefixName, ReadOnlySpan indent = default, bool cr = false, bool lf = false, byte depth = 0) + public readonly void ToString(Text result, bool prefixName, ReadOnlySpan indent = default, bool cr = false, bool lf = false, byte depth = 0) { + ThrowIfDisposed(); + if (prefixName) { result.Append('\"'); @@ -183,33 +286,28 @@ public unsafe readonly void ToString(Text result, bool prefixName, ReadOnlySpan< } else if (type == Type.Number) { - double number = Number; + double number = value.Read(); Span buffer = stackalloc char[64]; int length = number.ToString(buffer); result.Append(buffer.Slice(0, length)); } else if (type == Type.Boolean) { - result.Append(Boolean ? "true".AsSpan() : "false".AsSpan()); + result.Append(value.Read() ? Token.True : Token.False); } else if (type == Type.Object) { - void* ptr = (void*)value.Read(); - JSONObject obj = new(ptr); - obj.ToString(result, indent, cr, lf, depth); + JSONObject jsonObject = value.Read(); + jsonObject.ToString(result, indent, cr, lf, depth); } else if (type == Type.Array) { - void* ptr = (void*)value.Read(); - JSONArray array = new(ptr); - array.ToString(result, indent, cr, lf, depth); + JSONArray jsonArray = value.Read(); + jsonArray.ToString(result, indent, cr, lf, depth); } else if (type == Type.Null) { - result.Append('n'); - result.Append('u'); - result.Append('l'); - result.Append('l'); + result.Append(Token.Null); } else { @@ -217,8 +315,10 @@ public unsafe readonly void ToString(Text result, bool prefixName, ReadOnlySpan< } } - public unsafe readonly override string ToString() + public readonly override string ToString() { + ThrowIfDisposed(); + if (type == Type.Text) { return Text.ToString(); @@ -232,23 +332,21 @@ public unsafe readonly override string ToString() } else if (type == Type.Boolean) { - return Boolean ? "true" : "false"; + return Boolean ? Token.True : Token.False; } else if (type == Type.Object) { - void* ptr = (void*)value.Read(); - JSONObject obj = new(ptr); - return obj.ToString(); + JSONObject jsonObject = value.Read(); + return jsonObject.ToString(); } else if (type == Type.Array) { - void* ptr = (void*)value.Read(); - JSONArray array = new(ptr); - return array.ToString(); + JSONArray jsonArray = value.Read(); + return jsonArray.ToString(); } else if (type == Type.Null) { - return "null"; + return Token.Null; } else { @@ -258,7 +356,9 @@ public unsafe readonly override string ToString() public readonly bool TryGetText(out ReadOnlySpan text) { - if (IsText) + ThrowIfDisposed(); + + if (type == Type.Text) { text = Text; return true; @@ -270,9 +370,11 @@ public readonly bool TryGetText(out ReadOnlySpan text) public readonly bool TryGetNumber(out double number) { - if (IsNumber) + ThrowIfDisposed(); + + if (type == Type.Number) { - number = Number; + number = value.Read(); return true; } @@ -282,7 +384,9 @@ public readonly bool TryGetNumber(out double number) public readonly bool TryGetBoolean(out bool boolean) { - if (IsBoolean) + ThrowIfDisposed(); + + if (type == Type.Boolean) { boolean = value.Read(); return true; @@ -292,27 +396,31 @@ public readonly bool TryGetBoolean(out bool boolean) return false; } - public unsafe readonly bool TryGetObject(out JSONObject obj) + public readonly bool TryGetObject(out JSONObject jsonObject) { - if (IsObject) + ThrowIfDisposed(); + + if (type == Type.Object) { - obj = Object; + jsonObject = value.Read(); return true; } - obj = default; + jsonObject = default; return false; } - public unsafe readonly bool TryGetArray(out JSONArray array) + public readonly bool TryGetArray(out JSONArray jsonArray) { - if (IsArray) + ThrowIfDisposed(); + + if (type == Type.Array) { - array = Array; + jsonArray = value.Read(); return true; } - array = default; + jsonArray = default; return false; } diff --git a/source/JSON/Token.cs b/source/JSON/Token.cs index 72dbc07..5ce7307 100644 --- a/source/JSON/Token.cs +++ b/source/JSON/Token.cs @@ -4,6 +4,10 @@ namespace Serialization.JSON { public readonly struct Token { + public const string True = "true"; + public const string False = "false"; + public const string Null = "null"; + public readonly int position; public readonly int length; public readonly Type type; From ab203b0f1c83ba742140216ea084c4ff68a0deb5 Mon Sep 17 00:00:00 2001 From: Phill Date: Fri, 25 Apr 2025 12:12:15 -0400 Subject: [PATCH 4/7] support writing for json 5 compatible format --- source/JSON/IJSONSerializable.cs | 2 +- source/JSON/JSONArray.cs | 39 +++----- source/JSON/JSONObject.cs | 44 ++++----- source/JSON/JSONProperty.cs | 19 ++-- source/JSON/JSONReader.cs | 13 ++- source/JSON/JSONWriter.cs | 97 ++++++++++++++++---- source/JSON/SerializationSettings.cs | 98 ++++++++++++++++++++ tests/JSONTests.cs | 131 +++++++++++++++++++++++++-- 8 files changed, 348 insertions(+), 95 deletions(-) create mode 100644 source/JSON/SerializationSettings.cs diff --git a/source/JSON/IJSONSerializable.cs b/source/JSON/IJSONSerializable.cs index 1e94754..d458349 100644 --- a/source/JSON/IJSONSerializable.cs +++ b/source/JSON/IJSONSerializable.cs @@ -3,6 +3,6 @@ public interface IJSONSerializable { void Read(JSONReader reader); - void Write(JSONWriter writer); + void Write(ref JSONWriter writer); } } \ No newline at end of file diff --git a/source/JSON/JSONArray.cs b/source/JSON/JSONArray.cs index 7d829c5..899dc6d 100644 --- a/source/JSON/JSONArray.cs +++ b/source/JSON/JSONArray.cs @@ -72,17 +72,22 @@ public void Dispose() MemoryAddress.Free(ref jsonArray); } - public readonly void ToString(Text result, ReadOnlySpan indent = default, bool cr = false, bool lf = false, byte depth = 0) + public readonly void ToString(Text result, SerializationSettings settings = default) + { + ToString(result, settings, 0); + } + + internal readonly void ToString(Text result, SerializationSettings settings, byte depth) { ThrowIfDisposed(); result.Append('['); if (jsonArray->elements.Count > 0) { - NewLine(); + settings.NewLine(result); for (int i = 0; i <= depth; i++) { - Indent(indent); + settings.Indent(result); } int position = 0; @@ -91,7 +96,7 @@ public readonly void ToString(Text result, ReadOnlySpan indent = default, ref JSONProperty element = ref jsonArray->elements[position]; byte childDepth = depth; childDepth++; - element.ToString(result, false, indent, cr, lf, childDepth); + element.ToString(result, settings, childDepth); position++; if (position == Count) @@ -100,39 +105,21 @@ public readonly void ToString(Text result, ReadOnlySpan indent = default, } result.Append(','); - NewLine(); + settings.NewLine(result); for (int i = 0; i <= depth; i++) { - Indent(indent); + settings.Indent(result); } } - NewLine(); + settings.NewLine(result); for (int i = 0; i < depth; i++) { - Indent(indent); + settings.Indent(result); } } result.Append(']'); - - void NewLine() - { - if (cr) - { - result.Append('\r'); - } - - if (lf) - { - result.Append('\n'); - } - } - - void Indent(ReadOnlySpan indent) - { - result.Append(indent); - } } public readonly override string ToString() diff --git a/source/JSON/JSONObject.cs b/source/JSON/JSONObject.cs index a03e542..1322459 100644 --- a/source/JSON/JSONObject.cs +++ b/source/JSON/JSONObject.cs @@ -2,7 +2,6 @@ using System; using System.Diagnostics; using System.Runtime.CompilerServices; -using System.Text.Json.Nodes; using Unmanaged; namespace Serialization.JSON @@ -150,17 +149,22 @@ public readonly T As() where T : unmanaged, IJSONSerializable return value; } - public readonly void ToString(Text result, ReadOnlySpan indent = default, bool cr = false, bool lf = false, byte depth = 0) + public readonly void ToString(Text result, SerializationSettings settings = default) + { + ToString(result, settings, 0); + } + + internal readonly void ToString(Text result, SerializationSettings settings, byte depth) { ThrowIfDisposed(); result.Append('{'); if (jsonObject->properties.Count > 0) { - NewLine(); + settings.NewLine(result); for (int i = 0; i <= depth; i++) { - Indent(indent); + settings.Indent(result); } int position = 0; @@ -169,7 +173,11 @@ public readonly void ToString(Text result, ReadOnlySpan indent = default, ref JSONProperty property = ref jsonObject->properties[position]; byte childDepth = depth; childDepth++; - property.ToString(result, true, indent, cr, lf, childDepth); + result.Append('\"'); + result.Append(property.Name); + result.Append('\"'); + result.Append(':'); + property.ToString(result, settings, childDepth); position++; if (position == Count) @@ -178,39 +186,21 @@ public readonly void ToString(Text result, ReadOnlySpan indent = default, } result.Append(','); - NewLine(); + settings.NewLine(result); for (int i = 0; i <= depth; i++) { - Indent(indent); + settings.Indent(result); } } - NewLine(); + settings.NewLine(result); for (int i = 0; i < depth; i++) { - Indent(indent); + settings.Indent(result); } } result.Append('}'); - - void NewLine() - { - if (cr) - { - result.Append('\r'); - } - - if (lf) - { - result.Append('\n'); - } - } - - void Indent(ReadOnlySpan indent) - { - result.Append(indent); - } } public readonly override string ToString() diff --git a/source/JSON/JSONProperty.cs b/source/JSON/JSONProperty.cs index 246d879..4116a8c 100644 --- a/source/JSON/JSONProperty.cs +++ b/source/JSON/JSONProperty.cs @@ -266,17 +266,14 @@ public void Dispose() type = default; } - public readonly void ToString(Text result, bool prefixName, ReadOnlySpan indent = default, bool cr = false, bool lf = false, byte depth = 0) + public readonly void ToString(Text result, SerializationSettings settings = default) { - ThrowIfDisposed(); + ToString(result, settings, 0); + } - if (prefixName) - { - result.Append('\"'); - result.Append(Name); - result.Append('\"'); - result.Append(':'); - } + internal readonly void ToString(Text result, SerializationSettings settings, byte depth) + { + ThrowIfDisposed(); if (type == Type.Text) { @@ -298,12 +295,12 @@ public readonly void ToString(Text result, bool prefixName, ReadOnlySpan i else if (type == Type.Object) { JSONObject jsonObject = value.Read(); - jsonObject.ToString(result, indent, cr, lf, depth); + jsonObject.ToString(result, settings, depth); } else if (type == Type.Array) { JSONArray jsonArray = value.Read(); - jsonArray.ToString(result, indent, cr, lf, depth); + jsonArray.ToString(result, settings, depth); } else if (type == Type.Null) { diff --git a/source/JSON/JSONReader.cs b/source/JSON/JSONReader.cs index 0ddda4f..66796e0 100644 --- a/source/JSON/JSONReader.cs +++ b/source/JSON/JSONReader.cs @@ -30,6 +30,11 @@ public JSONReader(ByteReader reader) this.reader = reader; } + public readonly bool PeekToken(out Token token) + { + return PeekToken(out token, out _); + } + public readonly bool PeekToken(out Token token, out int readBytes) { token = default; @@ -193,11 +198,11 @@ public readonly bool ReadBoolean() else if (token.type == Token.Type.Text) { int length = GetText(token, buffer); - if (buffer.Slice(0, length).SequenceEqual("true")) + if (buffer.Slice(0, length).SequenceEqual(Token.True)) { return true; } - else if (buffer.Slice(0, length).SequenceEqual("false")) + else if (buffer.Slice(0, length).SequenceEqual(Token.False)) { return false; } @@ -242,7 +247,7 @@ public readonly T ReadObject() where T : unmanaged, IJSONSerializable throw new InvalidOperationException("Expected start object token."); } - public unsafe readonly int GetText(Token token, Span destination) + public readonly int GetText(Token token, Span destination) { return reader.PeekUTF8(token.position, token.length, destination); } @@ -258,7 +263,7 @@ public readonly bool GetBoolean(Token token) { Span buffer = stackalloc char[token.length]; int length = GetText(token, buffer); - return buffer.Slice(0, length).SequenceEqual("true".AsSpan()); + return buffer.Slice(0, length).SequenceEqual(Token.True); } } } \ No newline at end of file diff --git a/source/JSON/JSONWriter.cs b/source/JSON/JSONWriter.cs index 27f43c2..53b3a78 100644 --- a/source/JSON/JSONWriter.cs +++ b/source/JSON/JSONWriter.cs @@ -7,27 +7,29 @@ namespace Serialization.JSON [SkipLocalsInit] public struct JSONWriter : IDisposable { + private readonly SerializationSettings settings; private readonly ByteWriter writer; private Token last; + private int depth; public readonly bool IsDisposed => writer.IsDisposed; public readonly int Position => writer.Position; -#if NET - [Obsolete("Default constructor not available", true)] public JSONWriter() { - throw new NotImplementedException(); + settings = default; + writer = new ByteWriter(4); + last = default; } -#endif - public JSONWriter(ByteWriter writer) + public JSONWriter(SerializationSettings settings) { - this.writer = writer; + this.settings = settings; + this.writer = new ByteWriter(4); last = default; } - public unsafe override readonly string ToString() + public override readonly string ToString() { ByteReader reader = new(AsSpan()); Text tempBuffer = new(Position * 2); @@ -51,12 +53,32 @@ public readonly void Dispose() public void WriteStartObject() { + if (last.type == Token.Type.EndObject) + { + writer.WriteUTF8(','); + settings.NewLine(writer); + } + + for (int i = 0; i < depth; i++) + { + settings.Indent(writer); + } + last = new(writer.Position, sizeof(char), Token.Type.StartObject); writer.WriteUTF8('{'); + settings.NewLine(writer); + depth++; } public void WriteEndObject() { + depth--; + settings.NewLine(writer); + for (int i = 0; i < depth; i++) + { + settings.Indent(writer); + } + last = new(writer.Position, sizeof(char), Token.Type.EndObject); writer.WriteUTF8('}'); } @@ -65,20 +87,29 @@ public void WriteStartArray() { last = new(writer.Position, sizeof(char), Token.Type.StartArray); writer.WriteUTF8('['); + settings.NewLine(writer); + depth++; } public void WriteEndArray() { + depth--; + settings.NewLine(writer); + for (int i = 0; i < depth; i++) + { + settings.Indent(writer); + } + last = new(writer.Position, sizeof(char), Token.Type.EndArray); writer.WriteUTF8(']'); } - private void WriteText(ReadOnlySpan value) + private void WriteText(ReadOnlySpan value, SerializationSettings settings) { last = new(writer.Position, sizeof(char) * (2 + value.Length), Token.Type.Text); - writer.WriteUTF8('"'); + settings.WriteTextQuoteCharacter(writer); writer.WriteUTF8(value); - writer.WriteUTF8('"'); + settings.WriteTextQuoteCharacter(writer); } /// @@ -89,9 +120,10 @@ public void WriteTextElement(ReadOnlySpan value) if (last.type != Token.Type.StartObject && last.type != Token.Type.StartArray && last.type != Token.Type.Unknown) { writer.WriteUTF8(','); + settings.NewLine(writer); } - WriteText(value); + WriteText(value, settings); } public void WriteNumber(double number) @@ -108,25 +140,25 @@ public void WriteBoolean(bool value) if (value) { last = new(writer.Position, sizeof(char) * 4, Token.Type.Text); - writer.WriteUTF8("true".AsSpan()); + writer.WriteUTF8(Token.True); } else { last = new(writer.Position, sizeof(char) * 5, Token.Type.Text); - writer.WriteUTF8("false".AsSpan()); + writer.WriteUTF8(Token.False); } } public void WriteNull() { last = new(writer.Position, sizeof(char) * 4, Token.Type.Text); - writer.WriteUTF8("null".AsSpan()); + writer.WriteUTF8(Token.Null); } public void WriteObject(T obj) where T : unmanaged, IJSONSerializable { WriteStartObject(); - obj.Write(this); + obj.Write(ref this); WriteEndObject(); } @@ -138,10 +170,20 @@ public void WriteName(ReadOnlySpan name) if (last.type != Token.Type.StartObject && last.type != Token.Type.StartArray && last.type != Token.Type.Unknown) { writer.WriteUTF8(','); + settings.NewLine(writer); } - WriteText(name); + for (int i = 0; i < depth; i++) + { + settings.Indent(writer); + } + + last = new(writer.Position, sizeof(char) * (2 + name.Length), Token.Type.Text); + settings.WriteNameQuoteCharacter(writer); + writer.WriteUTF8(name); + settings.WriteNameQuoteCharacter(writer); writer.WriteUTF8(':'); + settings.SpaceAfterColon(writer); } public void WriteName(string name) @@ -149,10 +191,27 @@ public void WriteName(string name) WriteName(name.AsSpan()); } + public void WriteArray(ReadOnlySpan name, ReadOnlySpan items) where T : unmanaged, IJSONSerializable + { + WriteName(name); + WriteStartArray(); + foreach (T item in items) + { + WriteObject(item); + } + + WriteEndArray(); + } + + public void WriteArray(string name, ReadOnlySpan items) where T : unmanaged, IJSONSerializable + { + WriteArray(name.AsSpan(), items); + } + public void WriteProperty(ReadOnlySpan name, ReadOnlySpan text) { WriteName(name); - WriteText(text); + WriteText(text, settings); } public void WriteProperty(string name, ReadOnlySpan text) @@ -193,9 +252,9 @@ public void WriteProperty(string name, T obj) where T : unmanaged, IJSONSeria WriteProperty(name.AsSpan(), obj); } - public static JSONWriter Create() + public static JSONWriter Create(SerializationSettings settings = default) { - return new(new ByteWriter(4)); + return new(settings); } } } \ No newline at end of file diff --git a/source/JSON/SerializationSettings.cs b/source/JSON/SerializationSettings.cs new file mode 100644 index 0000000..f170af4 --- /dev/null +++ b/source/JSON/SerializationSettings.cs @@ -0,0 +1,98 @@ +using System; +using Unmanaged; + +namespace Serialization.JSON +{ + public struct SerializationSettings + { + public static readonly SerializationSettings PrettyPrinted = new(Flags.CarrierReturn | Flags.LineFeed | Flags.SpaceAfterColon, 4); + public static readonly SerializationSettings JSON5PrettyPrinted = new(Flags.CarrierReturn | Flags.LineFeed | Flags.QuotelessNames | Flags.SingleQuotedText | Flags.SpaceAfterColon, 4); + + public Flags flags; + public int indent; + + public SerializationSettings(Flags flags, int indent) + { + this.flags = flags; + this.indent = indent; + } + + public readonly void Indent(Text text) + { + text.Append(' ', indent); + } + + public readonly void Indent(ByteWriter writer) + { + for (int i = 0; i < indent; i++) + { + writer.WriteUTF8(' '); + } + } + + public readonly void NewLine(Text text) + { + if ((flags & Flags.CarrierReturn) != 0) + { + text.Append('\r'); + } + + if ((flags & Flags.LineFeed) != 0) + { + text.Append('\n'); + } + } + + public readonly void NewLine(ByteWriter writer) + { + if ((flags & Flags.CarrierReturn) != 0) + { + writer.WriteUTF8('\r'); + } + + if ((flags & Flags.LineFeed) != 0) + { + writer.WriteUTF8('\n'); + } + } + + public readonly void WriteTextQuoteCharacter(ByteWriter writer) + { + if ((flags & Flags.SingleQuotedText) != 0) + { + writer.WriteUTF8('\''); + } + else + { + writer.WriteUTF8('"'); + } + } + + public readonly void WriteNameQuoteCharacter(ByteWriter writer) + { + if ((flags & Flags.QuotelessNames) == 0) + { + writer.WriteUTF8('"'); + } + } + + public readonly void SpaceAfterColon(ByteWriter writer) + { + if ((flags & Flags.SpaceAfterColon) != 0) + { + writer.WriteUTF8(' '); + } + } + + [Flags] + public enum Flags : byte + { + None = 0, + CarrierReturn = 1, + LineFeed = 2, + QuotelessNames = 4, + SingleQuotedText = 8, + SpaceAfterColon = 16, + } + } +} \ No newline at end of file diff --git a/tests/JSONTests.cs b/tests/JSONTests.cs index 67828ec..3fb5c8e 100644 --- a/tests/JSONTests.cs +++ b/tests/JSONTests.cs @@ -207,7 +207,7 @@ public void ExampleUsage() jsonObject["age"].Number++; using Text buffer = new(); - jsonObject.ToString(buffer, " ".AsSpan(), true, true); + jsonObject.ToString(buffer, SerializationSettings.PrettyPrinted); Console.WriteLine(buffer.ToString()); } @@ -324,11 +324,51 @@ public void DeserializeIntoStruct() public void SerializeFromStruct() { using DummyJSONObject dummy = new("abacus", "212-4", 32, false); - using JSONWriter writer = JSONWriter.Create(); + using JSONWriter writer = new(); writer.WriteObject(dummy); - string jsonString = writer.ToString(); - Console.WriteLine(jsonString); - Assert.That(jsonString, Is.EqualTo("{\"name\":\"abacus\",\"value\":\"212-4\",\"quantity\":32,\"isRare\":false}")); + string jsonSource = writer.ToString(); + Assert.That(jsonSource, Is.EqualTo("{\"name\":\"abacus\",\"value\":\"212-4\",\"quantity\":32,\"isRare\":false}")); + } + + [Test] + public void WriteArrayToJSON5() + { + using Player player = new("playerName", 100, "red"); + player.AddItem("abacus", "212-4", 32, false); + player.AddItem("itemId", "forgot what this is", 1, true); + using JSONWriter jsonWriter = new(SerializationSettings.JSON5PrettyPrinted); + jsonWriter.WriteObject(player); + string jsonSource = jsonWriter.ToString(); + + using ByteReader reader = ByteReader.CreateFromUTF8(jsonSource); + JSONReader jsonReader = new(reader); + using Player readPlayer = jsonReader.ReadObject(); + Assert.That(readPlayer.Name.SequenceEqual(player.Name), Is.True); + Assert.That(readPlayer.HP, Is.EqualTo(player.HP)); + Assert.That(readPlayer.Items.Length, Is.EqualTo(player.Items.Length)); + Assert.That(readPlayer.Color, Is.EqualTo(player.Color)); + + string expectedSource = +@"{ + name: 'playerName', + hp: 100, + items: [ + { + name: 'abacus', + value: '212-4', + quantity: 32, + isRare: false + }, + { + name: 'itemId', + value: 'forgot what this is', + quantity: 1, + isRare: true + } + ], + htmlColor: 'red' +}"; + Assert.That(jsonSource, Is.EqualTo(expectedSource)); } [Test] @@ -449,6 +489,7 @@ public void Dispose() void IJSONSerializable.Read(JSONReader reader) { + //for all properties, skip reading the name, and read the value directly (assumes layout is perfect) Span buffer = stackalloc char[64]; reader.ReadToken(); int length = reader.ReadText(buffer); @@ -462,7 +503,7 @@ void IJSONSerializable.Read(JSONReader reader) isRare = reader.ReadBoolean(); } - readonly void IJSONSerializable.Write(JSONWriter writer) + readonly void IJSONSerializable.Write(ref JSONWriter writer) { writer.WriteProperty(nameof(name), name.AsSpan()); writer.WriteProperty(nameof(value), value.AsSpan()); @@ -470,5 +511,81 @@ readonly void IJSONSerializable.Write(JSONWriter writer) writer.WriteProperty(nameof(isRare), isRare); } } + + public struct Player : IJSONSerializable, IDisposable + { + private Text name; + private int hp; + private List items; + private ASCIIText16 htmlColor; + + public readonly ReadOnlySpan Name => name.AsSpan(); + public readonly int HP => hp; + public readonly ReadOnlySpan Items => items.AsSpan(); + public readonly ASCIIText16 Color => htmlColor; + + public Player(ReadOnlySpan name, int hp, ReadOnlySpan htmlColor) + { + this.name = new(name); + this.hp = hp; + this.items = new(); + this.htmlColor = new(htmlColor); + } + + public void Dispose() + { + for (int i = 0; i < items.Count; i++) + { + items[i].Dispose(); + } + + items.Dispose(); + name.Dispose(); + } + + public readonly void AddItem(ReadOnlySpan name, ReadOnlySpan value, int quantity, bool isRare) + { + items.Add(new(name, value, quantity, isRare)); + } + + void IJSONSerializable.Read(JSONReader reader) + { + reader.ReadToken(); //name + Span buffer = stackalloc char[64]; + int nameLength = reader.ReadText(buffer); + name = new(buffer.Slice(0, nameLength)); + + reader.ReadToken(); //name + hp = (int)reader.ReadNumber(); + + items = new(); + reader.ReadToken(); //name + reader.ReadToken(); //[ + while (reader.PeekToken(out Token token)) + { + if (token.type == Token.Type.EndArray) + { + break; + } + + DummyJSONObject item = reader.ReadObject(); + items.Add(item); + } + + reader.ReadToken(); //] + + reader.ReadToken(); //name + int colorLength = reader.ReadText(buffer); + htmlColor = new(buffer.Slice(0, colorLength)); + } + + readonly void IJSONSerializable.Write(ref JSONWriter writer) + { + writer.WriteProperty(nameof(name), name.AsSpan()); + writer.WriteProperty(nameof(hp), hp); + writer.WriteArray(nameof(items), items.AsSpan()); + writer.WriteProperty(nameof(htmlColor), htmlColor.ToString()); + } + } } -} +} \ No newline at end of file From 2b1a694b51316b4cd215a53949d394b25435c11e Mon Sep 17 00:00:00 2001 From: Phill Date: Fri, 25 Apr 2025 12:17:55 -0400 Subject: [PATCH 5/7] remove carrier return if on linux --- tests/JSONTests.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/JSONTests.cs b/tests/JSONTests.cs index 3fb5c8e..ea8b666 100644 --- a/tests/JSONTests.cs +++ b/tests/JSONTests.cs @@ -336,7 +336,14 @@ public void WriteArrayToJSON5() using Player player = new("playerName", 100, "red"); player.AddItem("abacus", "212-4", 32, false); player.AddItem("itemId", "forgot what this is", 1, true); - using JSONWriter jsonWriter = new(SerializationSettings.JSON5PrettyPrinted); + + SerializationSettings settings = SerializationSettings.JSON5PrettyPrinted; + if (Environment.OSVersion.Platform != PlatformID.Win32NT) + { + settings.flags &= ~SerializationSettings.Flags.CarrierReturn; + } + + using JSONWriter jsonWriter = new(settings); jsonWriter.WriteObject(player); string jsonSource = jsonWriter.ToString(); From 3251476caa5bd321a1914e0b6e0d47b7a4d6d7c8 Mon Sep 17 00:00:00 2001 From: Phill Date: Fri, 25 Apr 2025 12:39:41 -0400 Subject: [PATCH 6/7] add constants for other json settings --- source/JSON/SerializationSettings.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/source/JSON/SerializationSettings.cs b/source/JSON/SerializationSettings.cs index f170af4..4c97040 100644 --- a/source/JSON/SerializationSettings.cs +++ b/source/JSON/SerializationSettings.cs @@ -5,13 +5,16 @@ namespace Serialization.JSON { public struct SerializationSettings { - public static readonly SerializationSettings PrettyPrinted = new(Flags.CarrierReturn | Flags.LineFeed | Flags.SpaceAfterColon, 4); - public static readonly SerializationSettings JSON5PrettyPrinted = new(Flags.CarrierReturn | Flags.LineFeed | Flags.QuotelessNames | Flags.SingleQuotedText | Flags.SpaceAfterColon, 4); + public const int DefaultIndentation = 4; + public static readonly SerializationSettings Default = new(); + public static readonly SerializationSettings JSON5 = new(Flags.QuotelessNames | Flags.SingleQuotedText); + public static readonly SerializationSettings PrettyPrinted = new(Flags.CarrierReturn | Flags.LineFeed | Flags.SpaceAfterColon, DefaultIndentation); + public static readonly SerializationSettings JSON5PrettyPrinted = new(Flags.CarrierReturn | Flags.LineFeed | Flags.QuotelessNames | Flags.SingleQuotedText | Flags.SpaceAfterColon, DefaultIndentation); public Flags flags; public int indent; - public SerializationSettings(Flags flags, int indent) + public SerializationSettings(Flags flags, int indent = 0) { this.flags = flags; this.indent = indent; From a2d993d75066087d2b30d38456c837ffdbdd9ded Mon Sep 17 00:00:00 2001 From: Phill Date: Fri, 25 Apr 2025 12:39:52 -0400 Subject: [PATCH 7/7] update readme with more json5 info --- README.md | 59 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 48536f2..971ce9d 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,17 @@ # Serialization -Unmanaged library for working with common human readable formats using readers and -writers with bytes directly. As well as intermediary/high-level types for representing objects -within the supported formats. +Native library for working with common human readable formats using readers and writers. +With high-level types available for representing objects. ### Supported formats - JSON -- JSON 5 +- JSON 5 (named after ECMAScript 5) - XML ### JSON reader and writer -The reader and writers are used to iteratively progress over data. How the data -is stored should be known ahead of time (and can be tested). +The reader and writers are low-level concepts used to traverse and write data: ```cs using JSONWriter writer = new(); writer.WriteStartObject(); @@ -29,8 +27,8 @@ reader.ReadEndObject(); ### Generic JSON object -This is an alternative type thats able to represent a JSON object without the need -for interacting with the reader or writer. +This is the high-level type that represents a JSON object without the need +for interacting with the reader/writer types: ```cs JSONObject fruit = new(); fruit.Add("name", "cherry"); @@ -50,11 +48,12 @@ jsonObject.Add("inventory", inventory); jsonObject["age"].Number++; using Text jsonText = new(); -jsonObject.ToString(jsonText, " ", true, true); +SerializationSettings settings = SerializationSettings.PrettyPrint; +jsonObject.ToString(jsonText, settings); Console.WriteLine(jsonText); ``` -JSON result: +Output: ```json { "name": "John Doe", @@ -71,6 +70,18 @@ JSON result: } ``` +### JSON 5 support + +The reading mechanism supports both old and new JSON formats. But for the writer, +some settings need to be adjusted: +```cs +SerializationSettings settings = new(); +settings.flags |= SerializationFlags.QuotelessNames; +settings.flags |= SerializationFlags.SingleQuotedText; +``` + +The shorthand for these settings is `SerializationSettings.JSON5` and `SerializationSettings.JSON5PrettyPrint`. + ### JSON to C# and back The readers and writers have API for serializing/deserializing `IJSONObject` values: @@ -102,17 +113,26 @@ public struct Player : IJSONObject, IDisposable void IJSONObject.Read(ref JSONReader reader) { - //should initialize itself fully + //read hp + reader.ReadToken(); hp = (int)reader.ReadNumber(out _); + + //read alive + reader.ReadToken(); alive = reader.ReadBoolean(out _); - name = new(reader.ReadText(out _)); + + //read name + reader.ReadToken(); + Span nameBuffer = stackalloc char[32]; + int nameLength = reader.ReadText(nameBuffer); + name = new(nameBuffer.Slice(0, nameLength)); } void IJSONObject.Write(JSONWriter writer) { - writer.WriteNumber(hp); - writer.WriteBoolean(alive); - writer.WriteText(name.AsSpan()); + writer.WriteProperty(nameof(hp), hp); + writer.WriteProperty(nameof(alive), alive); + writer.WriteProperty(nameof(name), name.AsSpan()); } } @@ -136,4 +156,11 @@ project.TryGetFirst("PropertyGroup", out XMLNode propertyGroup); project.TryGetFirst("TargetFramework", out XMLNode tfm); tfm.Content = "net9.0"; File.WriteAllText("solution.csproj", project.ToString()); -``` \ No newline at end of file +``` + +### Contributing and design + +Although the name of the library is `serialization`, it's not to solve serialization itself. +But instead, for providing implementations of common and easy to read/edit formats very efficiently. + +And despite "common" being difficult to define, contributions to this are welcome. \ No newline at end of file