diff --git a/README.md b/README.md index ae73b96..971ce9d 100644 --- a/README.md +++ b/README.md @@ -1,10 +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 -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). +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 (named after ECMAScript 5) +- XML + +### JSON reader and writer + +The reader and writers are low-level concepts used to traverse and write data: ```cs using JSONWriter writer = new(); writer.WriteStartObject(); @@ -18,9 +25,10 @@ ReadOnlySpan propertyValue = reader.ReadText(out ReadOnlySpan proper 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. +### Generic JSON object + +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"); @@ -39,48 +47,56 @@ 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(); +SerializationSettings settings = SerializationSettings.PrettyPrint; +jsonObject.ToString(jsonText, settings); +Console.WriteLine(jsonText); ``` + +Output: ```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 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. -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) @@ -97,30 +113,41 @@ 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()); } } 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"]; @@ -129,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 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 cb3773d..899dc6d 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,48 +41,62 @@ 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, SerializationSettings settings = default) + { + ToString(result, settings, 0); } - public readonly void ToString(Text result, ReadOnlySpan indent = default, bool cr = false, bool lf = false, byte depth = 0) + internal readonly void ToString(Text result, SerializationSettings settings, byte depth) { ThrowIfDisposed(); result.Append('['); - if (value->elements.Count > 0) + if (jsonArray->elements.Count > 0) { - NewLine(); + settings.NewLine(result); for (int i = 0; i <= depth; i++) { - Indent(indent); + settings.Indent(result); } 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); + element.ToString(result, settings, childDepth); position++; if (position == Count) @@ -73,43 +105,27 @@ 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() { + ThrowIfDisposed(); + Text result = new(0); ToString(result); string text = result.ToString(); @@ -129,7 +145,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 +156,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 +171,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 +181,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 +191,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 +201,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 +211,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,98 +228,70 @@ 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) + jsonArray = MemoryAddress.AllocatePointer(); + jsonArray->elements = new(4); + 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) - { - jsonArray.Add(jsonReader.GetBoolean(token)); - } - else if (token.type == Token.Type.False) - { - jsonArray.Add(jsonReader.GetBoolean(token)); - } - else if (token.type == Token.Type.Null) + int capacity = token.length * 4; + if (textBuffer.Length < capacity) { - jsonArray.AddNull(); + textBuffer.SetLength(capacity); } - 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(Token.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(Token.False)) { - JSONObject newObject = reader.ReadObject(); - jsonArray.Add(newObject); + Add(false); } - else if (token.type == Token.Type.StartArray) + else if (text.SequenceEqual(Token.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; + } } } 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 5232400..1322459 100644 --- a/source/JSON/JSONObject.cs +++ b/source/JSON/JSONObject.cs @@ -12,22 +12,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 +51,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); + + 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)value; + public readonly nint Address => (nint)jsonObject; #if NET /// @@ -59,32 +97,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 @@ -101,26 +149,35 @@ 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 (value->properties.Count > 0) + if (jsonObject->properties.Count > 0) { - NewLine(); + settings.NewLine(result); for (int i = 0; i <= depth; i++) { - Indent(indent); + settings.Indent(result); } 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); + result.Append('\"'); + result.Append(property.Name); + result.Append('\"'); + result.Append(':'); + property.ToString(result, settings, childDepth); position++; if (position == Count) @@ -129,43 +186,27 @@ 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() { + ThrowIfDisposed(); + Text buffer = new(0); ToString(buffer); string result = buffer.ToString(); @@ -182,17 +223,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 +262,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 +278,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 +326,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 +344,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,139 +528,111 @@ 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)) + 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) + //todo: share these temp buffers? + 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) { - int length = jsonReader.GetText(token, buffer); - if (jsonReader.ReadToken(out Token nextToken)) + nameTextBuffer.SetLength(capacity); + } + + int nameTextLength = jsonReader.GetText(token, nameTextBuffer.AsSpan()); + Span name = nameTextBuffer.Slice(0, nameTextLength); + if (jsonReader.ReadToken(out 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(Token.True)) { - JSONObject newObject = reader.ReadObject(); - jsonObject.Add(nameSpan, newObject); + Add(name, true); } - else if (nextToken.type == Token.Type.StartArray) + else if (nextText.SequenceEqual(Token.False)) { - JSONArray newArray = reader.ReadObject(); - jsonObject.Add(nameSpan, newArray); + Add(name, false); } - else if (nextToken.type == Token.Type.EndObject) + else if (nextText.SequenceEqual(Token.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"); + } } } 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 649a0b6..4116a8c 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,46 +13,110 @@ 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); } } @@ -59,6 +124,9 @@ public readonly ref double Number { get { + ThrowIfDisposed(); + ThrowIfTypeMismatch(Type.Number); + return ref value.Read(); } } @@ -67,47 +135,48 @@ public readonly ref bool Boolean { get { + ThrowIfDisposed(); + ThrowIfTypeMismatch(Type.Boolean); + return ref value.Read(); } } - public readonly unsafe JSONObject Object + public readonly JSONObject Object { 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 unsafe JSONArray Array + public readonly JSONArray Array { 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); } } @@ -123,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); @@ -164,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(); } @@ -184,15 +266,14 @@ 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, SerializationSettings settings = default) { - if (prefixName) - { - result.Append('\"'); - result.Append(Name); - result.Append('\"'); - result.Append(':'); - } + ToString(result, settings, 0); + } + + internal readonly void ToString(Text result, SerializationSettings settings, byte depth) + { + ThrowIfDisposed(); if (type == Type.Text) { @@ -202,33 +283,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, settings, 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, settings, depth); } else if (type == Type.Null) { - result.Append('n'); - result.Append('u'); - result.Append('l'); - result.Append('l'); + result.Append(Token.Null); } else { @@ -238,16 +314,48 @@ public unsafe readonly void ToString(Text result, bool prefixName, ReadOnlySpan< public readonly override string ToString() { - Text buffer = new(0); - ToString(buffer, true); - string result = buffer.ToString(); - buffer.Dispose(); - return result; + ThrowIfDisposed(); + + 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 ? Token.True : Token.False; + } + else if (type == Type.Object) + { + JSONObject jsonObject = value.Read(); + return jsonObject.ToString(); + } + else if (type == Type.Array) + { + JSONArray jsonArray = value.Read(); + return jsonArray.ToString(); + } + else if (type == Type.Null) + { + return Token.Null; + } + else + { + throw new InvalidOperationException($"Property is of an unknown type: {type}"); + } } public readonly bool TryGetText(out ReadOnlySpan text) { - if (IsText) + ThrowIfDisposed(); + + if (type == Type.Text) { text = Text; return true; @@ -259,9 +367,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; } @@ -271,7 +381,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; @@ -281,27 +393,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/JSONReader.cs b/source/JSON/JSONReader.cs index 3e28ce2..66796e0 100644 --- a/source/JSON/JSONReader.cs +++ b/source/JSON/JSONReader.cs @@ -32,111 +32,119 @@ public JSONReader(ByteReader reader) public readonly bool PeekToken(out Token token) { - Span buffer = stackalloc char[8]; + return PeekToken(out token, out _); + } + + public readonly bool PeekToken(out Token token, out int readBytes) + { 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 +165,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 +173,7 @@ public double ReadNumber() { //skip } - else if (token.type == Token.Type.Number) + else if (token.type == Token.Type.Text) { return GetNumber(token); } @@ -178,21 +186,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(Token.True)) + { + return true; + } + else if (buffer.Slice(0, length).SequenceEqual(Token.False)) + { + return false; + } + + throw new InvalidOperationException($"Could not parse {buffer.Slice(0, length).ToString()} as a boolean"); } else { @@ -203,7 +218,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 +230,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; @@ -231,19 +247,9 @@ public 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) { - 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) @@ -257,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 ecafdc6..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) @@ -99,7 +131,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,26 +139,26 @@ public void WriteBoolean(bool value) { if (value) { - last = new(writer.Position, sizeof(char) * 4, Token.Type.True); - writer.WriteUTF8("true".AsSpan()); + last = new(writer.Position, sizeof(char) * 4, Token.Type.Text); + writer.WriteUTF8(Token.True); } else { - last = new(writer.Position, sizeof(char) * 5, Token.Type.False); - writer.WriteUTF8("false".AsSpan()); + last = new(writer.Position, sizeof(char) * 5, Token.Type.Text); + writer.WriteUTF8(Token.False); } } public void WriteNull() { - last = new(writer.Position, sizeof(char) * 4, Token.Type.Null); - writer.WriteUTF8("null".AsSpan()); + last = new(writer.Position, sizeof(char) * 4, Token.Type.Text); + 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..4c97040 --- /dev/null +++ b/source/JSON/SerializationSettings.cs @@ -0,0 +1,101 @@ +using System; +using Unmanaged; + +namespace Serialization.JSON +{ + public struct SerializationSettings + { + 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 = 0) + { + 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/source/JSON/Token.cs b/source/JSON/Token.cs index dd65af4..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; @@ -35,10 +39,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..ea8b666 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() { @@ -109,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()); } @@ -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(); @@ -227,11 +324,58 @@ 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); + + 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(); + + 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] @@ -352,6 +496,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); @@ -365,7 +510,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()); @@ -373,5 +518,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