diff --git a/Linguini.Bench/Linguini.Bench.csproj b/Linguini.Bench/Linguini.Bench.csproj index bfe1545..8d1a815 100644 --- a/Linguini.Bench/Linguini.Bench.csproj +++ b/Linguini.Bench/Linguini.Bench.csproj @@ -4,6 +4,7 @@ Exe false net6.0;net8.0 + 0.8.0 diff --git a/Linguini.Bundle.Test/Linguini.Bundle.Test.csproj b/Linguini.Bundle.Test/Linguini.Bundle.Test.csproj index 8ad1b66..2311158 100644 --- a/Linguini.Bundle.Test/Linguini.Bundle.Test.csproj +++ b/Linguini.Bundle.Test/Linguini.Bundle.Test.csproj @@ -4,7 +4,7 @@ false enable Library - 0.7.0 + 0.8.0 net6.0;net8.0 diff --git a/Linguini.Bundle/Errors/FluentError.cs b/Linguini.Bundle/Errors/FluentError.cs index fc2a097..86c3df7 100644 --- a/Linguini.Bundle/Errors/FluentError.cs +++ b/Linguini.Bundle/Errors/FluentError.cs @@ -1,5 +1,4 @@ using System; -using Linguini.Shared.Util; using Linguini.Syntax.Ast; using Linguini.Syntax.Parser.Error; @@ -51,23 +50,23 @@ public override string ToString() public record ResolverFluentError : FluentError { - private string Description; - private ErrorType Kind; + private readonly string _description; + private readonly ErrorType _kind; private ResolverFluentError(string desc, ErrorType kind) { - Description = desc; - Kind = kind; + _description = desc; + _kind = kind; } public override ErrorType ErrorKind() { - return Kind; + return _kind; } public override string ToString() { - return Description; + return _description; } public static ResolverFluentError NoValue(ReadOnlyMemory idName) diff --git a/Linguini.Bundle/Linguini.Bundle.csproj b/Linguini.Bundle/Linguini.Bundle.csproj index 9915581..69b93d4 100644 --- a/Linguini.Bundle/Linguini.Bundle.csproj +++ b/Linguini.Bundle/Linguini.Bundle.csproj @@ -18,7 +18,7 @@ It provides easy to use and extend system for describing translations. https://github.com/Ygg01/Linguini git - 0.7.0 + 0.8.0 net8.0;netstandard2.1;net6.0 linguini.jpg README.md diff --git a/Linguini.Bundle/Resolver/WriterHelpers.cs b/Linguini.Bundle/Resolver/WriterHelpers.cs index 7f7f18c..ddaeed1 100644 --- a/Linguini.Bundle/Resolver/WriterHelpers.cs +++ b/Linguini.Bundle/Resolver/WriterHelpers.cs @@ -1,5 +1,4 @@ -#nullable enable -using System; +using System; using System.Collections.Generic; using System.IO; using Linguini.Bundle.Errors; @@ -100,11 +99,9 @@ public static bool TryWrite(this IExpression expression, TextWriter writer, Scop for (var i = 0; i < selectExpression.Variants.Count; i++) { var variant = selectExpression.Variants[i]; - if (variant.IsDefault) - { - variant.Value.Write(writer, scope); - return errors.Count == 0; - } + if (!variant.IsDefault) continue; + variant.Value.Write(writer, scope); + return errors.Count == 0; } errors.Add(ResolverFluentError.MissingDefault()); @@ -312,46 +309,26 @@ public static void WriteError(this IExpression self, TextWriter writer) public static void WriteError(this IInlineExpression self, TextWriter writer) { - if (self is MessageReference msgRef) + switch (self) { - if (msgRef.Attribute == null) - { - writer.Write($"{msgRef.Id}"); + case MessageReference msgRef: + writer.Write(msgRef.Attribute == null ? $"{msgRef.Id}" : $"{msgRef.Id}.{msgRef.Attribute}"); return; - } - - writer.Write($"{msgRef.Id}.{msgRef.Attribute}"); - return; - } - - if (self is TermReference termRef) - { - if (termRef.Attribute == null) - { - writer.Write($"-{termRef.Id}"); + case TermReference termReference: + writer.Write(termReference.Attribute == null ? $"-{termReference.Id}" : $"-{termReference.Id}.{termReference.Attribute}"); return; - } - - writer.Write($"-{termRef.Id}.{termRef.Attribute}"); - } - else if (self is FunctionReference funcRef) - { - writer.Write($"{funcRef.Id}()"); - return; - } - else if (self is VariableReference varRef) - { - writer.Write($"${varRef.Id}"); - return; - } - else if (self is DynamicReference dynamicReference) - { - writer.Write($"$${dynamicReference.Id}"); - return; + case FunctionReference funcRef: + writer.Write($"{funcRef.Id}()"); + return; + case VariableReference varRef: + writer.Write($"${varRef.Id}"); + return; + case DynamicReference dynamicReference: + writer.Write($"$${dynamicReference.Id}"); + return; + default: + throw new ArgumentException($"Unexpected inline expression `{self.GetType()}`!"); } - - - throw new ArgumentException($"Unexpected inline expression `{self.GetType()}`!"); } } } \ No newline at end of file diff --git a/Linguini.Serialization.Test/Linguini.Serialization.Test.csproj b/Linguini.Serialization.Test/Linguini.Serialization.Test.csproj new file mode 100644 index 0000000..f527355 --- /dev/null +++ b/Linguini.Serialization.Test/Linguini.Serialization.Test.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + 10 + + + + + + + + + + + + + diff --git a/Linguini.Serialization.Test/SerializeAndDeserializeTest.cs b/Linguini.Serialization.Test/SerializeAndDeserializeTest.cs new file mode 100644 index 0000000..ac91a89 --- /dev/null +++ b/Linguini.Serialization.Test/SerializeAndDeserializeTest.cs @@ -0,0 +1,105 @@ +using System.Diagnostics; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using Linguini.Serialization.Converters; +using Linguini.Syntax.Ast; +using NUnit.Framework; +using Attribute = Linguini.Syntax.Ast.Attribute; + + +namespace Linguini.Serialization.Test; + +[TestFixture] +public class SerializeAndDeserializeTest +{ + [Test] + [TestCaseSource(nameof(AstExamples))] + [Parallelizable] + public void RoundTripTest(object x) + { + // Serialize the object to JSON string. + var jsonString = JsonSerializer.Serialize(x, Options); + + // Deserialize the JSON string back into an object. + Debug.Assert(x != null, nameof(x) + " != null"); + var deserializedObject = JsonSerializer.Deserialize(jsonString, x.GetType(), Options); + + // Now you have a 'deserializedObject' which should be equivalent to the original 'expected' object. + Assert.That(deserializedObject, Is.Not.Null); + Assert.That(deserializedObject, Is.EqualTo(x)); + } + + public static IEnumerable AstExamples() + { + yield return new Attribute("desc", new PatternBuilder("description")); + yield return new CallArgumentsBuilder() + .AddPositionalArg(InlineExpressionBuilder.CreateMessageReference("x")) + .AddNamedArg("y", 3) + .Build(); + yield return new AstComment(CommentLevel.Comment, new() { "test".AsMemory() }); + yield return new DynamicReference("dyn", "attr", new CallArgumentsBuilder() + .AddPositionalArg(InlineExpressionBuilder.CreateMessageReference("x")) + .AddNamedArg("y", 3)); + yield return new FunctionReference("foo", new CallArgumentsBuilder() + .AddPositionalArg(3) + .AddNamedArg("test", InlineExpressionBuilder.CreateTermReference("x", "y")) + .Build() + ); + yield return new Identifier("test"); + yield return new Junk("Test".AsMemory()); + yield return new MessageReference("message", "attribute"); + yield return new AstMessage( + new Identifier("x"), + new PatternBuilder(3).Build(), + new List() + { + new("attr1", new PatternBuilder("value1")), + new("attr2", new PatternBuilder("value2")) + }, + new(CommentLevel.ResourceComment, new() + { + "test".AsMemory() + })); + yield return new PatternBuilder("text ").AddMessage("x").AddText(" more text").Build(); + yield return new SelectExpressionBuilder(new VariableReference("x")) + .AddVariant("one", new PatternBuilder("select 1")) + .AddVariant("other", new PatternBuilder("select other")) + .SetDefault(1) + .Build(); + yield return new TermReference("x", "y"); + yield return new VariableReference("x"); + yield return new Variant(2.0f, new PatternBuilder(3)); + } + + private static readonly JsonSerializerOptions Options = new() + { + IgnoreReadOnlyFields = false, + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = + { + new AttributeSerializer(), + new CallArgumentsSerializer(), + new CommentSerializer(), + new FunctionReferenceSerializer(), + new IdentifierSerializer(), + new JunkSerializer(), + new MessageReferenceSerializer(), + new MessageSerializer(), + new DynamicReferenceSerializer(), + new NamedArgumentSerializer(), + new ParseErrorSerializer(), + new PatternSerializer(), + new PlaceableSerializer(), + new ResourceSerializer(), + new PlaceableSerializer(), + new SelectExpressionSerializer(), + new TermReferenceSerializer(), + new TermSerializer(), + new VariantSerializer(), + new VariableReferenceSerializer(), + } + }; +} \ No newline at end of file diff --git a/Linguini.Serialization.Test/TestUtil.cs b/Linguini.Serialization.Test/TestUtil.cs new file mode 100644 index 0000000..35c2f15 --- /dev/null +++ b/Linguini.Serialization.Test/TestUtil.cs @@ -0,0 +1,16 @@ +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using Linguini.Serialization.Converters; +using NUnit.Framework; + +namespace Linguini.Serialization.Test +{ + public static class TestUtil + { + + + + + } +} \ No newline at end of file diff --git a/Linguini.Serialization/Converters/AttributeSerializer.cs b/Linguini.Serialization/Converters/AttributeSerializer.cs index 6ebc763..c2ab997 100644 --- a/Linguini.Serialization/Converters/AttributeSerializer.cs +++ b/Linguini.Serialization/Converters/AttributeSerializer.cs @@ -1,6 +1,7 @@ using System; using System.Text.Json; using System.Text.Json.Serialization; +using Linguini.Syntax.Ast; using Attribute = Linguini.Syntax.Ast.Attribute; namespace Linguini.Serialization.Converters @@ -9,7 +10,52 @@ public class AttributeSerializer : JsonConverter { public override Attribute Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - throw new NotImplementedException(); + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException(); + } + + var id = new Identifier(""); + var value = new Pattern(); + + while (reader.Read()) + { + + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + if (reader.TokenType == JsonTokenType.PropertyName) + { + string? propertyName = reader.GetString(); + + reader.Read(); + + switch (propertyName) + { + case "id": + id = JsonSerializer.Deserialize(ref reader, options); + break; + + case "value": + value = JsonSerializer.Deserialize(ref reader, options); + break; + case "type": + var typeField = reader.GetString(); + if (typeField != "Attribute") + { + throw new JsonException( + $"Invalid type: Expected 'Attribute' found {typeField} instead"); + } + break; + default: + throw new JsonException($"Unexpected property: {propertyName}"); + } + } + } + + return new Attribute(id!, value!); } public override void Write(Utf8JsonWriter writer, Attribute attribute, JsonSerializerOptions options) diff --git a/Linguini.Serialization/Converters/CallArgumentsSerializer.cs b/Linguini.Serialization/Converters/CallArgumentsSerializer.cs index 5e2f3a7..b81d0d0 100644 --- a/Linguini.Serialization/Converters/CallArgumentsSerializer.cs +++ b/Linguini.Serialization/Converters/CallArgumentsSerializer.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; using Linguini.Syntax.Ast; @@ -6,12 +8,18 @@ namespace Linguini.Serialization.Converters { - public class CallArgumentsSerializer: JsonConverter + public class CallArgumentsSerializer : JsonConverter { public override CallArguments Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - throw new NotImplementedException(); + var el = JsonSerializer.Deserialize(ref reader, options); + if (TryGetCallArguments(el, options, out var value)) + { + return value.Value; + } + + throw new JsonException("Invalid CallArguments"); } public override void Write(Utf8JsonWriter writer, CallArguments value, JsonSerializerOptions options) @@ -25,6 +33,7 @@ public override void Write(Utf8JsonWriter writer, CallArguments value, JsonSeria { ResourceSerializer.WriteInlineExpression(writer, positionalArg, options); } + writer.WriteEndArray(); writer.WritePropertyName("named"); writer.WriteStartArray(); @@ -32,8 +41,40 @@ public override void Write(Utf8JsonWriter writer, CallArguments value, JsonSeria { JsonSerializer.Serialize(writer, namedArg, options); } + writer.WriteEndArray(); writer.WriteEndObject(); } + + public static bool TryGetCallArguments(JsonElement el, + JsonSerializerOptions options, + [NotNullWhen(true)] out CallArguments? callArguments) + { + if (!el.TryGetProperty("positional", out var positional) || !el.TryGetProperty("named", out var named)) + { + throw new JsonException("CallArguments fields `positional` and `named` properties are mandatory"); + } + + var positionalArgs = new List(); + foreach (var arg in positional.EnumerateArray()) + { + if (ResourceSerializer.TryReadInlineExpression(arg, options, out var posArgs)) + { + positionalArgs.Add(posArgs); + } + } + + var namedArgs = new List(); + foreach (var arg in named.EnumerateArray()) + { + if (NamedArgumentSerializer.TryReadNamedArguments(arg, options, out var namedArg)) + { + namedArgs.Add(namedArg.Value); + } + } + + callArguments = new CallArguments(positionalArgs, namedArgs); + return true; + } } -} +} \ No newline at end of file diff --git a/Linguini.Serialization/Converters/CommentSerializer.cs b/Linguini.Serialization/Converters/CommentSerializer.cs index e9ec728..797c7dc 100644 --- a/Linguini.Serialization/Converters/CommentSerializer.cs +++ b/Linguini.Serialization/Converters/CommentSerializer.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using Linguini.Syntax.Ast; @@ -10,7 +12,58 @@ public class CommentSerializer : JsonConverter { public override AstComment Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - throw new NotImplementedException(); + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException(); + } + + var commentLevel = CommentLevel.None; + var content = new List>(); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + if (reader.TokenType == JsonTokenType.PropertyName) + { + string? propertyName = reader.GetString(); + + reader.Read(); + + switch (propertyName) + { + case "type": + var type = reader.GetString(); + commentLevel = type switch + { + "Comment" => CommentLevel.Comment, + "GroupComment" => CommentLevel.GroupComment, + "ResourceComment" => CommentLevel.ResourceComment, + _ => CommentLevel.None, + }; + break; + case "content": + var s = reader.GetString(); + content = s != null + ? s.Split().Select(x => x.AsMemory()).ToList() + // ReSharper disable once ArrangeObjectCreationWhenTypeNotEvident + : new(); + break; + default: + throw new JsonException($"Unexpected property: {propertyName}"); + } + } + } + + if (commentLevel == CommentLevel.None) + { + throw new JsonException("Comment must have some level of nesting"); + } + + return new AstComment(commentLevel, content); } public override void Write(Utf8JsonWriter writer, AstComment comment, JsonSerializerOptions options) @@ -31,9 +84,10 @@ public override void Write(Utf8JsonWriter writer, AstComment comment, JsonSerial default: throw new InvalidEnumArgumentException($"Unexpected comment `{comment.CommentLevel}`"); } + writer.WritePropertyName("content"); writer.WriteStringValue(comment.AsStr()); writer.WriteEndObject(); } } -} +} \ No newline at end of file diff --git a/Linguini.Serialization/Converters/DynamicReferenceSerializer.cs b/Linguini.Serialization/Converters/DynamicReferenceSerializer.cs index 50bc1d3..d7d4198 100644 --- a/Linguini.Serialization/Converters/DynamicReferenceSerializer.cs +++ b/Linguini.Serialization/Converters/DynamicReferenceSerializer.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; using Linguini.Syntax.Ast; @@ -7,9 +8,11 @@ namespace Linguini.Serialization.Converters { public class DynamicReferenceSerializer : JsonConverter { - public override DynamicReference? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DynamicReference? Read(ref Utf8JsonReader reader, Type typeToConvert, + JsonSerializerOptions options) { - throw new NotImplementedException(); + var el = JsonSerializer.Deserialize(ref reader, options); + return ProcessDynamicReference(el, options); } public override void Write(Utf8JsonWriter writer, DynamicReference dynRef, JsonSerializerOptions options) @@ -19,20 +22,44 @@ public override void Write(Utf8JsonWriter writer, DynamicReference dynRef, JsonS writer.WriteStringValue("DynamicReference"); writer.WritePropertyName("id"); JsonSerializer.Serialize(writer, dynRef.Id, options); - + if (dynRef.Attribute != null || options.DefaultIgnoreCondition != JsonIgnoreCondition.WhenWritingNull) { writer.WritePropertyName("attribute"); JsonSerializer.Serialize(writer, dynRef.Attribute, options); } - + if (dynRef.Arguments != null || options.DefaultIgnoreCondition != JsonIgnoreCondition.WhenWritingNull) { writer.WritePropertyName("arguments"); JsonSerializer.Serialize(writer, dynRef.Arguments, options); } - + writer.WriteEndObject(); } + + public static DynamicReference ProcessDynamicReference(JsonElement el, + JsonSerializerOptions options) + { + if (!el.TryGetProperty("id", out var jsonId) || + !IdentifierSerializer.TryGetIdentifier(jsonId, options, out var identifier)) + { + throw new JsonException("Dynamic reference must contain at least `id` field"); + } + + Identifier? attribute = null; + CallArguments? arguments = null; + if (el.TryGetProperty("attribute", out var jsonAttribute)) + { + IdentifierSerializer.TryGetIdentifier(jsonAttribute, options, out attribute); + } + + if (el.TryGetProperty("arguments", out var jsonArgs)) + { + CallArgumentsSerializer.TryGetCallArguments(jsonArgs, options, out arguments); + } + + return new DynamicReference(identifier, attribute, arguments); + } } } \ No newline at end of file diff --git a/Linguini.Serialization/Converters/FunctionReferenceSerializer.cs b/Linguini.Serialization/Converters/FunctionReferenceSerializer.cs index 7b5c345..5e7f7c7 100644 --- a/Linguini.Serialization/Converters/FunctionReferenceSerializer.cs +++ b/Linguini.Serialization/Converters/FunctionReferenceSerializer.cs @@ -7,9 +7,11 @@ namespace Linguini.Serialization.Converters { public class FunctionReferenceSerializer : JsonConverter { - public override FunctionReference Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override FunctionReference Read(ref Utf8JsonReader reader, Type typeToConvert, + JsonSerializerOptions options) { - throw new NotImplementedException(); + var el = JsonSerializer.Deserialize(ref reader, options); + return ProcessFunctionReference(el, options); } public override void Write(Utf8JsonWriter writer, FunctionReference value, JsonSerializerOptions options) @@ -23,5 +25,26 @@ public override void Write(Utf8JsonWriter writer, FunctionReference value, JsonS JsonSerializer.Serialize(writer, value.Arguments, options); writer.WriteEndObject(); } + + public static FunctionReference ProcessFunctionReference(JsonElement el, + JsonSerializerOptions options) + { + if (!el.TryGetProperty("id", out JsonElement value) || + !IdentifierSerializer.TryGetIdentifier(value, options, out var ident)) + { + throw new JsonException("Function reference must contain `id` field"); + } + + CallArguments? arguments = null; + + if (!el.TryGetProperty("arguments", out var jsonArguments) || + !CallArgumentsSerializer.TryGetCallArguments(jsonArguments, options, out arguments) + ) + { + throw new JsonException("Function reference must contain `arguments` field"); + } + + return new FunctionReference(ident, arguments.Value); + } } -} +} \ No newline at end of file diff --git a/Linguini.Serialization/Converters/IdentifierSerializer.cs b/Linguini.Serialization/Converters/IdentifierSerializer.cs index f3be4c2..1a62a3f 100644 --- a/Linguini.Serialization/Converters/IdentifierSerializer.cs +++ b/Linguini.Serialization/Converters/IdentifierSerializer.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; using Linguini.Syntax.Ast; @@ -10,7 +11,52 @@ public class IdentifierSerializer : JsonConverter { public override Identifier Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - throw new NotImplementedException(); + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException(); + } + + string? id = null; + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + if (reader.TokenType == JsonTokenType.PropertyName) + { + string? propertyName = reader.GetString(); + + reader.Read(); + + switch (propertyName) + { + case "type": + var typeField = reader.GetString(); + if (typeField != "Identifier") + { + throw new JsonException( + $"Invalid type: Expected 'Attribute' found {typeField} instead"); + } + + break; + case "name": + id = reader.GetString(); + break; + default: + throw new JsonException($"Unexpected property: {propertyName}"); + } + } + } + + if (id == null) + { + throw new JsonException("No id for Identifier found"); + } + + return new Identifier(id); } public override void Write(Utf8JsonWriter writer, Identifier identifier, JsonSerializerOptions options) @@ -22,5 +68,19 @@ public override void Write(Utf8JsonWriter writer, Identifier identifier, JsonSer writer.WriteStringValue(identifier.Name.Span); writer.WriteEndObject(); } + + public static bool TryGetIdentifier(JsonElement el, JsonSerializerOptions options, + [MaybeNullWhen(false)] out Identifier ident) + { + if (!el.TryGetProperty("name", out var valueElement) || valueElement.ValueKind != JsonValueKind.String) + { + ident = null; + return false; + } + + var value = valueElement.GetString() ?? ""; + ident = new Identifier(value); + return true; + } } } \ No newline at end of file diff --git a/Linguini.Serialization/Converters/JunkSerializer.cs b/Linguini.Serialization/Converters/JunkSerializer.cs index b9ece27..ef0702d 100644 --- a/Linguini.Serialization/Converters/JunkSerializer.cs +++ b/Linguini.Serialization/Converters/JunkSerializer.cs @@ -10,7 +10,18 @@ public class JunkSerializer : JsonConverter { public override Junk Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - throw new NotImplementedException(); + return ProcessJunk(JsonSerializer.Deserialize(ref reader, options), options); + } + + private Junk ProcessJunk(JsonElement el, JsonSerializerOptions options) + { + if (el.TryGetProperty("content", out var content)) + { + var str = content.GetString() ?? ""; + return new Junk(str); + } + + throw new JsonException("Junk must have content"); } public override void Write(Utf8JsonWriter writer, Junk value, JsonSerializerOptions options) diff --git a/Linguini.Serialization/Converters/MessageReferenceSerializer.cs b/Linguini.Serialization/Converters/MessageReferenceSerializer.cs index 2e948e8..e7d6b69 100644 --- a/Linguini.Serialization/Converters/MessageReferenceSerializer.cs +++ b/Linguini.Serialization/Converters/MessageReferenceSerializer.cs @@ -7,9 +7,10 @@ namespace Linguini.Serialization.Converters { public class MessageReferenceSerializer : JsonConverter { - public override MessageReference Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override MessageReference Read(ref Utf8JsonReader reader, Type typeToConvert, + JsonSerializerOptions options) { - throw new NotImplementedException(); + return ProcessMessageReference(JsonSerializer.Deserialize(ref reader, options), options); } public override void Write(Utf8JsonWriter writer, MessageReference msgRef, JsonSerializerOptions options) @@ -27,5 +28,23 @@ public override void Write(Utf8JsonWriter writer, MessageReference msgRef, JsonS writer.WriteEndObject(); } + + public static MessageReference ProcessMessageReference(JsonElement el, + JsonSerializerOptions options) + { + if (el.TryGetProperty("id", out var getProp) + && IdentifierSerializer.TryGetIdentifier(getProp, options, out var ident)) + { + Identifier? attr = null; + if (el.TryGetProperty("attribute", out var prop) && prop.ValueKind != JsonValueKind.Null) + { + IdentifierSerializer.TryGetIdentifier(prop, options, out attr); + } + + return new MessageReference(ident, attr); + } + + throw new JsonException("MessageReference requires `id` field"); + } } -} +} \ No newline at end of file diff --git a/Linguini.Serialization/Converters/MessageSerializer.cs b/Linguini.Serialization/Converters/MessageSerializer.cs index b4b7fb6..438c30c 100644 --- a/Linguini.Serialization/Converters/MessageSerializer.cs +++ b/Linguini.Serialization/Converters/MessageSerializer.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; using Linguini.Syntax.Ast; +using Attribute = Linguini.Syntax.Ast.Attribute; namespace Linguini.Serialization.Converters { @@ -9,7 +11,36 @@ public class MessageSerializer : JsonConverter { public override AstMessage Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - throw new NotImplementedException(); + var el = JsonSerializer.Deserialize(ref reader, options); + if (!el.TryGetProperty("id", out var jsonId) || + !IdentifierSerializer.TryGetIdentifier(jsonId, options, out var identifier)) + { + throw new JsonException("AstMessage must have at least `id` element"); + } + + Pattern? value = null; + AstComment? comment = null; + var attrs = new List(); + if (el.TryGetProperty("value", out var patternJson) && patternJson.ValueKind == JsonValueKind.Object) + { + PatternSerializer.TryReadPattern(patternJson, options, out value); + } + + if (el.TryGetProperty("comment", out var commentJson) && patternJson.ValueKind == JsonValueKind.Object) + { + comment = JsonSerializer.Deserialize(commentJson.GetRawText(), options); + } + + if (el.TryGetProperty("attributes", out var attrsJson) && attrsJson.ValueKind == JsonValueKind.Array) + { + foreach (var attributeJson in attrsJson.EnumerateArray()) + { + var attr = JsonSerializer.Deserialize(attributeJson.GetRawText(), options); + if (attr != null) attrs.Add(attr); + } + } + + return new AstMessage(identifier, value, attrs, comment); } public override void Write(Utf8JsonWriter writer, AstMessage msg, JsonSerializerOptions options) @@ -48,4 +79,4 @@ public override void Write(Utf8JsonWriter writer, AstMessage msg, JsonSerializer writer.WriteEndObject(); } } -} +} \ No newline at end of file diff --git a/Linguini.Serialization/Converters/NamedArgumentSerializer.cs b/Linguini.Serialization/Converters/NamedArgumentSerializer.cs index b21d864..3f7a01e 100644 --- a/Linguini.Serialization/Converters/NamedArgumentSerializer.cs +++ b/Linguini.Serialization/Converters/NamedArgumentSerializer.cs @@ -1,16 +1,23 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; using Linguini.Syntax.Ast; namespace Linguini.Serialization.Converters { - public class NamedArgumentSerializer: JsonConverter + public class NamedArgumentSerializer : JsonConverter { public override NamedArgument Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - throw new NotImplementedException(); + if (TryReadNamedArguments(JsonSerializer.Deserialize(ref reader, options), options, + out var namedArgument)) + { + return namedArgument.Value; + } + + throw new JsonException("Invalid `NamedArgument`!"); } public override void Write(Utf8JsonWriter writer, NamedArgument value, JsonSerializerOptions options) @@ -24,5 +31,21 @@ public override void Write(Utf8JsonWriter writer, NamedArgument value, JsonSeria ResourceSerializer.WriteInlineExpression(writer, value.Value, options); writer.WriteEndObject(); } + + public static bool TryReadNamedArguments(JsonElement el, JsonSerializerOptions options, + [NotNullWhen(true)] out NamedArgument? o) + { + if (el.TryGetProperty("name", out var namedArg) + && IdentifierSerializer.TryGetIdentifier(namedArg, options, out var id) + && el.TryGetProperty("value", out var valueArg) + && ResourceSerializer.TryReadInlineExpression(valueArg, options, out var inline) + ) + { + o = new NamedArgument(id, inline); + return true; + } + + throw new JsonException("NamedArgument fields `name` and `value` properties are mandatory"); + } } -} +} \ No newline at end of file diff --git a/Linguini.Serialization/Converters/PatternSerializer.cs b/Linguini.Serialization/Converters/PatternSerializer.cs index 156df2e..5ac6921 100644 --- a/Linguini.Serialization/Converters/PatternSerializer.cs +++ b/Linguini.Serialization/Converters/PatternSerializer.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -11,7 +13,74 @@ public class PatternSerializer : JsonConverter { public override Pattern Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - throw new NotImplementedException(); + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException(); + } + + var builder = new PatternBuilder(); + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + if (reader.TokenType == JsonTokenType.PropertyName) + { + string? propertyName = reader.GetString(); + + reader.Read(); + + switch (propertyName) + { + case "type": + var typeField = reader.GetString(); + if (typeField != "Pattern") + { + throw new JsonException( + $"Invalid type: Expected 'Attribute' found {typeField} instead"); + } + + break; + case "elements": + AddElements(ref reader, builder, options); + break; + } + } + } + + return builder.Build(); + } + + private static void AddElements(ref Utf8JsonReader reader, PatternBuilder builder, + JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartArray) + { + throw new JsonException(); + } + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndArray) break; + + if (reader.TokenType != JsonTokenType.StartObject) continue; + + var el = JsonSerializer.Deserialize(ref reader, options); + builder.AddExpression(ReadPatternExpression(el, options)); + } + } + + private static IPatternElement ReadPatternExpression(JsonElement el, JsonSerializerOptions options) + { + var type = el.GetProperty("type").GetString(); + return type switch + { + "TextElement" => ResourceSerializer.ProcessTextLiteral(el, options), + "Placeable" => PlaceableSerializer.ProcessPlaceable(el, options), + _ => throw new JsonException($"Unexpected type `{type}`") + }; } public override void Write(Utf8JsonWriter writer, Pattern pattern, JsonSerializerOptions options) @@ -55,5 +124,44 @@ private static void WriteMergedText(Utf8JsonWriter writer, StringBuilder? textLi writer.WriteEndObject(); } } + + public static bool TryReadPattern(JsonElement jsonValue, JsonSerializerOptions options, + [MaybeNullWhen(false)] out Pattern pattern) + { + if (!jsonValue.TryGetProperty("type", out var jsonType) + && "Placeable".Equals(jsonType.GetString())) + { + throw new JsonException("Placeable must have `type` equal to `Placeable`."); + } + + if (!jsonValue.TryGetProperty("elements", out var elements) + && elements.ValueKind != JsonValueKind.Array) + { + throw new JsonException("Placeable must have an `elements` array."); + } + + var patternElements = new List(); + foreach (var element in elements.EnumerateArray()) + { + var elementType = element.GetProperty("type").GetString(); + switch (elementType) + { + case "TextElement": + var textValue = element.GetProperty("value").GetString() ?? ""; + patternElements.Add(new TextLiteral(textValue)); + break; + case "Placeable": + if (PlaceableSerializer.TryProcessPlaceable(element, options, out var placeable)) + { + patternElements.Add(placeable); + } + + break; + } + } + + pattern = new Pattern(patternElements); + return true; + } } -} +} \ No newline at end of file diff --git a/Linguini.Serialization/Converters/PlaceableSerializer.cs b/Linguini.Serialization/Converters/PlaceableSerializer.cs index aa24c68..55d9f72 100644 --- a/Linguini.Serialization/Converters/PlaceableSerializer.cs +++ b/Linguini.Serialization/Converters/PlaceableSerializer.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; using Linguini.Syntax.Ast; @@ -31,5 +32,24 @@ public override void Write(Utf8JsonWriter writer, Placeable value, JsonSerialize writer.WriteEndObject(); } + + public static bool TryProcessPlaceable(JsonElement el, JsonSerializerOptions options, + [MaybeNullWhen(false)] out Placeable placeable) + { + if (!el.TryGetProperty("expression", out var expr)) + { + throw new JsonException("Placeable must have `expression` value."); + } + + placeable = new Placeable(ResourceSerializer.ReadExpression(expr, options)); + return true; + } + + public static Placeable ProcessPlaceable(JsonElement el, JsonSerializerOptions options) + { + if (!TryProcessPlaceable(el, options, out var placeable)) throw new JsonException("Expected placeable!"); + + return placeable; + } } -} +} \ No newline at end of file diff --git a/Linguini.Serialization/Converters/ResourceSerializer.cs b/Linguini.Serialization/Converters/ResourceSerializer.cs index 16ff272..51d36ff 100644 --- a/Linguini.Serialization/Converters/ResourceSerializer.cs +++ b/Linguini.Serialization/Converters/ResourceSerializer.cs @@ -1,6 +1,6 @@ -#nullable enable - -using System; +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; using Linguini.Syntax.Ast; @@ -90,5 +90,75 @@ public static void WriteInlineExpression(Utf8JsonWriter writer, IInlineExpressio JsonSerializer.Serialize(writer, dynamicReference, options); } } + + public static TextLiteral ProcessTextLiteral(JsonElement el, JsonSerializerOptions options) + { + return new(el.GetProperty("value").GetString() ?? ""); + } + + public static NumberLiteral ProcessNumberLiteral(JsonElement el, + JsonSerializerOptions options) + { + if (TryReadProcessNumberLiteral(el, options, out var numberLiteral)) + { + return numberLiteral; + } + + throw new JsonException("Expected value to be a valid number"); + } + + public static bool TryReadProcessNumberLiteral(JsonElement el, JsonSerializerOptions options, + [MaybeNullWhen(false)] out NumberLiteral numberLiteral) + { + if (el.TryGetProperty("value", out var v) && v.ValueKind == JsonValueKind.String && + !"".Equals(v.GetString())) + { + numberLiteral = new NumberLiteral(v.GetString().AsMemory()); + return true; + } + + numberLiteral = null; + return false; + } + + public static IExpression ReadExpression(JsonElement el, JsonSerializerOptions options) + { + var type = el.GetProperty("type").GetString(); + IExpression x = type switch + { + "DynamicReference" => DynamicReferenceSerializer.ProcessDynamicReference(el, options), + "FunctionReference" => FunctionReferenceSerializer.ProcessFunctionReference(el, options), + "MessageReference" => MessageReferenceSerializer.ProcessMessageReference(el, options), + "NumberLiteral" => ProcessNumberLiteral(el, options), + "Placeable" => PlaceableSerializer.ProcessPlaceable(el, options), + "TermReference" => TermReferenceSerializer.ProcessTermReference(el, options), + "StringLiteral" or "TextElement" or "TextLiteral" => ProcessTextLiteral(el, options), + "VariableReference" => VariableReferenceSerializer.ProcessVariableReference(el, options), + "SelectExpression" => SelectExpressionSerializer.ProcessSelectExpression(el, options), + _ => throw new JsonException($"Unexpected type {type}") + }; + return x; + } + + + + public static bool TryReadInlineExpression(JsonElement el, JsonSerializerOptions options, + [MaybeNullWhen(false)] out IInlineExpression o) + { + var type = el.GetProperty("type").GetString(); + o = type switch + { + "DynamicReference" => DynamicReferenceSerializer.ProcessDynamicReference(el, options), + "FunctionReference" => FunctionReferenceSerializer.ProcessFunctionReference(el, options), + "MessageReference" => MessageReferenceSerializer.ProcessMessageReference(el, options), + "NumberLiteral" => ProcessNumberLiteral(el, options), + "Placeable" => PlaceableSerializer.ProcessPlaceable(el, options), + "TermReference" => TermReferenceSerializer.ProcessTermReference(el, options), + "TextLiteral" => ProcessTextLiteral(el, options), + "VariableReference" => VariableReferenceSerializer.ProcessVariableReference(el, options), + _ => throw new JsonException($"Unexpected value {type}") + }; + return true; + } } -} +} \ No newline at end of file diff --git a/Linguini.Serialization/Converters/SelectExpressionSerializer.cs b/Linguini.Serialization/Converters/SelectExpressionSerializer.cs index 4ddc2ee..6769017 100644 --- a/Linguini.Serialization/Converters/SelectExpressionSerializer.cs +++ b/Linguini.Serialization/Converters/SelectExpressionSerializer.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; using Linguini.Syntax.Ast; @@ -7,9 +8,10 @@ namespace Linguini.Serialization.Converters { public class SelectExpressionSerializer : JsonConverter { - public override SelectExpression Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override SelectExpression Read(ref Utf8JsonReader reader, Type typeToConvert, + JsonSerializerOptions options) { - throw new NotImplementedException(); + return ProcessSelectExpression(JsonSerializer.Deserialize(ref reader, options), options); } public override void Write(Utf8JsonWriter writer, SelectExpression value, JsonSerializerOptions options) @@ -25,8 +27,31 @@ public override void Write(Utf8JsonWriter writer, SelectExpression value, JsonSe { JsonSerializer.Serialize(writer, variant, options); } + writer.WriteEndArray(); writer.WriteEndObject(); } + + public static SelectExpression ProcessSelectExpression(JsonElement el, + JsonSerializerOptions options) + { + if (!el.TryGetProperty("selector", out var prop)) throw new JsonException("Select needs a `selector`"); + if (!ResourceSerializer.TryReadInlineExpression(prop, options, out var selector)) + { + throw new JsonException("No inline expression found!"); + } + + + if (el.TryGetProperty("variants", out var variantsProp) && variantsProp.ValueKind != JsonValueKind.Array) + throw new JsonException("Select `variants` must be a an array"); + + var variants = new List(); + foreach (var variantEl in variantsProp.EnumerateArray()) + { + variants.Add(VariantSerializer.ReadVariant(variantEl, options)); + } + + return new SelectExpression(selector, variants); + } } -} +} \ No newline at end of file diff --git a/Linguini.Serialization/Converters/TermReferenceSerializer.cs b/Linguini.Serialization/Converters/TermReferenceSerializer.cs index b163d5f..0df8a00 100644 --- a/Linguini.Serialization/Converters/TermReferenceSerializer.cs +++ b/Linguini.Serialization/Converters/TermReferenceSerializer.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; using Linguini.Syntax.Ast; @@ -9,7 +10,7 @@ public class TermReferenceSerializer : JsonConverter { public override TermReference Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - throw new NotImplementedException(); + return ProcessTermReference(JsonSerializer.Deserialize(ref reader, options), options); } public override void Write(Utf8JsonWriter writer, TermReference value, JsonSerializerOptions options) @@ -19,20 +20,45 @@ public override void Write(Utf8JsonWriter writer, TermReference value, JsonSeria writer.WriteStringValue("TermReference"); writer.WritePropertyName("id"); JsonSerializer.Serialize(writer, value.Id, options); - + if (value.Attribute != null || options.DefaultIgnoreCondition != JsonIgnoreCondition.WhenWritingNull) { writer.WritePropertyName("attribute"); JsonSerializer.Serialize(writer, value.Attribute, options); } - + if (value.Arguments != null || options.DefaultIgnoreCondition != JsonIgnoreCondition.WhenWritingNull) { writer.WritePropertyName("arguments"); JsonSerializer.Serialize(writer, value.Arguments, options); } - + writer.WriteEndObject(); } + + + public static TermReference ProcessTermReference(JsonElement el, + JsonSerializerOptions options) + { + if (!el.TryGetProperty("id", out JsonElement value) || + !IdentifierSerializer.TryGetIdentifier(value, options, out var id)) + { + throw new JsonException("Term reference must contain at least `id` field"); + } + + Identifier? attribute = null; + CallArguments? arguments = null; + if (el.TryGetProperty("attribute", out var attr)) + { + IdentifierSerializer.TryGetIdentifier(attr, options, out attribute); + } + + if (el.TryGetProperty("arguments", out var callarg)) + { + CallArgumentsSerializer.TryGetCallArguments(callarg, options, out arguments); + } + + return new TermReference(id, attribute, arguments); + } } -} +} \ No newline at end of file diff --git a/Linguini.Serialization/Converters/VariableReferenceSerializer.cs b/Linguini.Serialization/Converters/VariableReferenceSerializer.cs index 7133404..df8cb16 100644 --- a/Linguini.Serialization/Converters/VariableReferenceSerializer.cs +++ b/Linguini.Serialization/Converters/VariableReferenceSerializer.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; using Linguini.Syntax.Ast; @@ -7,12 +8,14 @@ namespace Linguini.Serialization.Converters { public class VariableReferenceSerializer : JsonConverter { - public override VariableReference Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override VariableReference Read(ref Utf8JsonReader reader, Type typeToConvert, + JsonSerializerOptions options) { - throw new NotImplementedException(); + return ProcessVariableReference(JsonSerializer.Deserialize(ref reader, options), options); } - public override void Write(Utf8JsonWriter writer, VariableReference variableReference, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, VariableReference variableReference, + JsonSerializerOptions options) { writer.WriteStartObject(); writer.WritePropertyName("type"); @@ -21,5 +24,17 @@ public override void Write(Utf8JsonWriter writer, VariableReference variableRefe JsonSerializer.Serialize(writer, variableReference.Id, options); writer.WriteEndObject(); } + + public static VariableReference ProcessVariableReference(JsonElement el, + JsonSerializerOptions options) + { + if (el.TryGetProperty("id", out var value) && + IdentifierSerializer.TryGetIdentifier(value, options, out var ident)) + { + return new VariableReference(ident); + } + + throw new JsonException("Variable reference must contain `id` field"); + } } -} +} \ No newline at end of file diff --git a/Linguini.Serialization/Converters/VariantSerializer.cs b/Linguini.Serialization/Converters/VariantSerializer.cs index 8f3c28d..f454086 100644 --- a/Linguini.Serialization/Converters/VariantSerializer.cs +++ b/Linguini.Serialization/Converters/VariantSerializer.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; using Linguini.Syntax.Ast; @@ -10,7 +11,7 @@ public class VariantSerializer : JsonConverter { public override Variant Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - throw new NotImplementedException(); + return ReadVariant(JsonSerializer.Deserialize(ref reader, options), options); } public override void Write(Utf8JsonWriter writer, Variant variant, JsonSerializerOptions options) @@ -52,5 +53,52 @@ private static void WriteKey(Utf8JsonWriter writer, Variant value) writer.WriteEndObject(); } + + public static Variant ReadVariant(JsonElement el, JsonSerializerOptions options) + { + if (!el.TryGetProperty("type", out var jsonType) + && "Variant".Equals(jsonType.GetString())) + { + throw new JsonException("Variant must have `type` equal to `Variant`."); + } + + if (el.TryGetProperty("key", out var jsonKey) + && TryReadKey(jsonKey, options, out var key)) + { + if (el.TryGetProperty("value", out var jsonValue) + && PatternSerializer.TryReadPattern(jsonValue, options, out var pattern)) + { + var isDefault = false; + if (el.TryGetProperty("default", out var jsonDefault)) + { + isDefault = jsonDefault.ValueKind == JsonValueKind.True; + } + + return new Variant(key.Value.Item1, key.Value.Item2, pattern, isDefault); + } + } + + throw new JsonException("Variant must have `key` and `value`."); + } + + private static bool TryReadKey(JsonElement jsonKey, JsonSerializerOptions options, + [NotNullWhen(true)] out (VariantType, ReadOnlyMemory)? key) + { + if (IdentifierSerializer.TryGetIdentifier(jsonKey, options, out var id)) + { + key = (VariantType.Identifier, id.Name); + return true; + } + + if (ResourceSerializer.TryReadProcessNumberLiteral(jsonKey, options, out var num)) + { + key = (VariantType.NumberLiteral, num.Value); + return true; + } + + key = null; + return false; + } + } } \ No newline at end of file diff --git a/Linguini.Serialization/Linguini.Serialization.csproj b/Linguini.Serialization/Linguini.Serialization.csproj index de7f55a..3d67254 100644 --- a/Linguini.Serialization/Linguini.Serialization.csproj +++ b/Linguini.Serialization/Linguini.Serialization.csproj @@ -1,7 +1,7 @@ - 0.7.0 + 0.8.0 linguini.jpg README.md serialization, linguini, utility @@ -23,8 +23,4 @@ - - - - diff --git a/Linguini.Shared/Linguini.Shared.csproj b/Linguini.Shared/Linguini.Shared.csproj index 6d525a3..597cc20 100644 --- a/Linguini.Shared/Linguini.Shared.csproj +++ b/Linguini.Shared/Linguini.Shared.csproj @@ -9,7 +9,7 @@ MIT OR Apache-2.0 fluent, i18n, internationalization, l10n, l20n, globalization, translation false - 0.7.0 + 0.8.0 net6.0;netstandard2.1;net8.0 linguini.jpg README.md diff --git a/Linguini.Syntax.Tests/Linguini.Syntax.Tests.csproj b/Linguini.Syntax.Tests/Linguini.Syntax.Tests.csproj index 3599ec3..ae7fb81 100644 --- a/Linguini.Syntax.Tests/Linguini.Syntax.Tests.csproj +++ b/Linguini.Syntax.Tests/Linguini.Syntax.Tests.csproj @@ -6,6 +6,7 @@ Library linguini.jpg net6.0;net8.0 + 0.8.0 diff --git a/Linguini.Syntax.Tests/Parser/LinguiniFtlParserTest.cs b/Linguini.Syntax.Tests/Parser/LinguiniFtlParserTest.cs index b4c3dc5..485bfb8 100644 --- a/Linguini.Syntax.Tests/Parser/LinguiniFtlParserTest.cs +++ b/Linguini.Syntax.Tests/Parser/LinguiniFtlParserTest.cs @@ -1,10 +1,8 @@ -#nullable enable -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; -using FluentAssertions.Execution; using FluentAssertions.Json; using Linguini.Serialization.Converters; using Linguini.Syntax.Ast; @@ -37,9 +35,8 @@ private static string BaseTestDir } } - private static JsonSerializerOptions TestJsonOptions() - { - return new JsonSerializerOptions + private static JsonSerializerOptions TestJsonOptions = + new() { IgnoreReadOnlyFields = false, WriteIndented = true, @@ -69,7 +66,6 @@ private static JsonSerializerOptions TestJsonOptions() new VariableReferenceSerializer(), }, }; - } private static string GetFullPathFor(string file) { @@ -117,7 +113,7 @@ public void TestLinguiniErrors(string file, bool ignoreComments = false) ? ParseFtlFileFast(@$"{path}.ftl") : ParseFtlFile(@$"{path}.ftl"); - var actual = WrapArray(JArray.Parse(JsonSerializer.Serialize(resource.Errors, TestJsonOptions()))); + var actual = WrapArray(JArray.Parse(JsonSerializer.Serialize(resource.Errors, TestJsonOptions))); actual.Should().BeEquivalentTo(expected); } @@ -171,7 +167,7 @@ public void TestReadFile(string file) { var path = GetFullPathFor(file); var res = ParseFtlFile(@$"{path}.ftl"); - var ftlAstJson = JsonSerializer.Serialize(res, TestJsonOptions()); + var ftlAstJson = JsonSerializer.Serialize(res, TestJsonOptions); var expected = JToken.Parse(File.ReadAllText($@"{path}.json")); var actual = JToken.Parse(ftlAstJson); @@ -223,7 +219,7 @@ public void TestLinguiniExt(string file) { var path = GetFullPathFor(file); var res = ParseFtlFile(@$"{path}.ftl", true); - var ftlAstJson = JsonSerializer.Serialize(res, TestJsonOptions()); + var ftlAstJson = JsonSerializer.Serialize(res, TestJsonOptions); var expected = JToken.Parse(File.ReadAllText($@"{path}.json")); var actual = JToken.Parse(ftlAstJson); diff --git a/Linguini.Syntax.Tests/Parser/LinguiniTestDetailedErrors.cs b/Linguini.Syntax.Tests/Parser/LinguiniTestDetailedErrors.cs index d437bb7..d7581fd 100644 --- a/Linguini.Syntax.Tests/Parser/LinguiniTestDetailedErrors.cs +++ b/Linguini.Syntax.Tests/Parser/LinguiniTestDetailedErrors.cs @@ -1,8 +1,6 @@ using System; -using Linguini.Syntax.Ast; using Linguini.Syntax.Parser; using NUnit.Framework; -using NUnit.Framework.Legacy; namespace Linguini.Syntax.Tests.Parser { diff --git a/Linguini.Syntax/Ast/Base.cs b/Linguini.Syntax/Ast/Base.cs index 8d2a231..644de8b 100644 --- a/Linguini.Syntax/Ast/Base.cs +++ b/Linguini.Syntax/Ast/Base.cs @@ -2,14 +2,36 @@ using System.Collections.Generic; using System.Text; +// ReSharper disable ClassNeverInstantiated.Global +// ReSharper disable UnusedMember.Global +// ReSharper disable ForCanBeConvertedToForeach namespace Linguini.Syntax.Ast { - - public class Attribute + public class Attribute : IEquatable { - public Identifier Id; - public Pattern Value; + public readonly Identifier Id; + public readonly Pattern Value; + + public static AttributeComparer Comparer = new(); + + public class AttributeComparer : IEqualityComparer + { + public bool Equals(Attribute? x, Attribute? y) + { + if (ReferenceEquals(x, y)) return true; + if (ReferenceEquals(x, null)) return false; + if (ReferenceEquals(y, null)) return false; + if (x.GetType() != y.GetType()) return false; + return Identifier.Comparator.Equals(x.Id, y.Id) && + x.Value.Equals(y.Value); + } + + public int GetHashCode(Attribute obj) + { + return HashCode.Combine(obj.Id, obj.Value); + } + } public Attribute(Identifier id, Pattern value) { @@ -17,16 +39,48 @@ public Attribute(Identifier id, Pattern value) Value = value; } + public Attribute(string id, PatternBuilder builder) + { + Id = new Identifier(id); + Value = builder.Build(); + } + public void Deconstruct(out Identifier id, out Pattern value) { id = Id; value = Value; } + + public static Attribute From(string id, PatternBuilder patternBuilder) + { + return new Attribute(new Identifier(id), patternBuilder.Build()); + } + + public bool Equals(Attribute? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Id.Equals(other.Id) && Value.Equals(other.Value); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((Attribute)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(Id, Value); + } } - public class Pattern + public class Pattern : IEquatable { - public List Elements; + public readonly List Elements; + public Pattern(List elements) { @@ -37,18 +91,174 @@ public Pattern() { Elements = new List(); } + + public bool Equals(Pattern? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + if (Elements.Count != other.Elements.Count) + { + return false; + } + + for (var index = 0; index < Elements.Count; index++) + { + var patternElement = Elements[index]; + var otherPatternElement = other.Elements[index]; + if (!IPatternElement.PatternComparer.Equals(patternElement, otherPatternElement)) + { + return false; + } + } + + return true; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((Pattern)obj); + } + + public override int GetHashCode() + { + return Elements.GetHashCode(); + } + } + + public class PatternBuilder + { + private readonly List _patternElements = new(); + + public PatternBuilder() + { + } + + public PatternBuilder(string text) + { + _patternElements.Add(new TextLiteral(text)); + } + + public PatternBuilder(float number) + { + _patternElements.Add(new Placeable(new NumberLiteral(number))); + } + + public PatternBuilder AddText(string textLiteral) + { + _patternElements.Add(new TextLiteral(textLiteral)); + return this; + } + + public PatternBuilder AddNumberLiteral(float number) + { + _patternElements.Add(new Placeable(new NumberLiteral(number))); + return this; + } + + public PatternBuilder AddNumberLiteral(double number) + { + _patternElements.Add(new Placeable(new NumberLiteral(number))); + return this; + } + + public PatternBuilder AddMessage(string id, string? attribute = null) + { + _patternElements.Add(new Placeable(new MessageReference(id, attribute))); + return this; + } + + public PatternBuilder AddTermReference(string id, string? attribute = null, CallArguments? callArguments = null) + { + _patternElements.Add(new Placeable(new TermReference(id, attribute, callArguments))); + return this; + } + + public PatternBuilder AddDynamicReference(string id, string? attribute = null, + CallArguments? callArguments = null) + { + _patternElements.Add(new Placeable(new DynamicReference(id, attribute, callArguments))); + return this; + } + + public PatternBuilder AddFunctionReference(string functionName, CallArguments funcArgs = default) + { + _patternElements.Add(new Placeable(new FunctionReference(functionName, funcArgs))); + return this; + } + + public PatternBuilder AddFunctionReference(string functionName, CallArgumentsBuilder builder) + { + _patternElements.Add(new Placeable(new FunctionReference(functionName, builder.Build()))); + return this; + } + + public PatternBuilder AddMessageReference(string messageId, string? attribute = null) + { + _patternElements.Add(new Placeable(new MessageReference(messageId, attribute))); + return this; + } + + public PatternBuilder AddSelectExpression(SelectExpressionBuilder selectExpressionBuilder) + { + _patternElements.Add(new Placeable(selectExpressionBuilder.Build())); + return this; + } + + public PatternBuilder AddExpression(IPatternElement expr) + { + _patternElements.Add(expr); + return this; + } + + public PatternBuilder AddPlaceable(Placeable placeable) + { + _patternElements.Add(placeable); + return this; + } + + public Pattern Build() + { + return new Pattern(_patternElements); + } } public class Identifier : IEquatable { + public class IdentifierComparator : IEqualityComparer + { + public bool Equals(Identifier? x, Identifier? y) + { + if (ReferenceEquals(x, y)) return true; + if (ReferenceEquals(x, null)) return false; + if (ReferenceEquals(y, null)) return false; + if (x.GetType() != y.GetType()) return false; + return x.Name.Span.SequenceEqual(y.Name.Span); + } + + public int GetHashCode(Identifier obj) + { + return obj.Name.GetHashCode(); + } + } + public readonly ReadOnlyMemory Name; + public static readonly IdentifierComparator Comparator = new(); + public Identifier(ReadOnlyMemory name) { Name = name; } + public Identifier(string id) + { + Name = id.AsMemory(); + } + public override string ToString() { return Name.Span.ToString(); @@ -58,12 +268,12 @@ public bool Equals(Identifier? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return ToString().Equals(other.ToString()); + return Comparator.Equals(this, other); } public override int GetHashCode() { - return Name.GetHashCode(); + return Comparator.GetHashCode(this); } } @@ -86,23 +296,56 @@ public interface IEntry public interface IInlineExpression : IExpression { + public static readonly InlineExpressionComparer Comparer = new(); } - public static class Base + public class InlineExpressionComparer : IEqualityComparer { + public bool Equals(IInlineExpression? left, IInlineExpression? right) + { + return (left, right) switch + { + (DynamicReference l, DynamicReference r) => l.Equals(r), + (FunctionReference l, FunctionReference r) => l.Equals(r), + (MessageReference l, MessageReference r) => l.Equals(r), + (NumberLiteral l, NumberLiteral r) => l.Equals(r), + (Placeable l, Placeable r) => l.Equals(r), + (TermReference l, TermReference r) => l.Equals(r), + (TextLiteral l, TextLiteral r) => l.Equals(r), + (VariableReference l, VariableReference r) => l.Equals(r), + _ => false + }; + } + public int GetHashCode(IInlineExpression obj) + { + return obj switch + { + DynamicReference dr => dr.GetHashCode(), + FunctionReference fr => fr.GetHashCode(), + MessageReference mr => mr.GetHashCode(), + NumberLiteral nl => nl.GetHashCode(), + Placeable p => p.GetHashCode(), + TermReference term => term.GetHashCode(), + TextLiteral tl => tl.GetHashCode(), + VariableReference vr => vr.GetHashCode(), + _ => throw new ArgumentOutOfRangeException(nameof(obj), obj, null) + }; + } + } + + public static class Base + { public static string Stringify(this Pattern? pattern) { var sb = new StringBuilder(); - if (pattern != null && pattern.Elements.Count > 0) + if (pattern == null || pattern.Elements.Count <= 0) return sb.ToString(); + for (var i = 0; i < pattern.Elements.Count; i++) { - for (var i = 0; i < pattern.Elements.Count; i++) - { - sb.Append(pattern.Elements[i]); - } + sb.Append(pattern.Elements[i]); } return sb.ToString(); } } -} +} \ No newline at end of file diff --git a/Linguini.Syntax/Ast/Entry.cs b/Linguini.Syntax/Ast/Entry.cs index ca8ef0b..970787a 100644 --- a/Linguini.Syntax/Ast/Entry.cs +++ b/Linguini.Syntax/Ast/Entry.cs @@ -1,11 +1,9 @@ using System; using System.Collections.Generic; -using System.IO; +using System.Linq; using System.Text; -using Linguini.Syntax.Parser; using Linguini.Syntax.Parser.Error; - namespace Linguini.Syntax.Ast { public record Resource @@ -20,40 +18,65 @@ public Resource(List body, List errors) } } - public class AstMessage : IEntry + public class AstMessage : IEntry, IEquatable { - public Identifier Id; - public Pattern? Value; - public List Attributes; - public AstComment? Comment; + public readonly Identifier Id; + public readonly Pattern? Value; + public readonly List Attributes; + + public AstComment? Comment => InternalComment; + protected internal AstComment? InternalComment; - public AstMessage(Identifier id, Pattern? pattern, List attrs, AstComment? comment) + public AstMessage(Identifier id, Pattern? pattern, List attrs, AstComment? internalComment) { Id = id; Value = pattern; Attributes = attrs; - Comment = comment; + InternalComment = internalComment; } public string GetId() { return Id.ToString(); } + + public bool Equals(AstMessage? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Identifier.Comparator.Equals(Id, other.Id) && Equals(Value, other.Value) && + Attributes.SequenceEqual(other.Attributes, Attribute.Comparer) && + Equals(InternalComment, other.InternalComment); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((AstMessage)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(Id, Value, Attributes, InternalComment); + } } public class AstTerm : IEntry { - public Identifier Id; - public Pattern Value; - public List Attributes; - public AstComment? Comment; + public readonly Identifier Id; + public readonly Pattern Value; + public readonly List Attributes; + public AstComment? Comment => InternalComment; + protected internal AstComment? InternalComment; public AstTerm(Identifier id, Pattern value, List attributes, AstComment? comment) { Id = id; Value = value; Attributes = attributes; - Comment = comment; + InternalComment = comment; } @@ -63,28 +86,28 @@ public string GetId() } } - public class AstComment : IEntry + public class AstComment : IEntry, IEquatable { - public CommentLevel CommentLevel; - public readonly List> _content; + public readonly CommentLevel CommentLevel; + public readonly List> Content; public AstComment(CommentLevel commentLevel, List> content) { CommentLevel = commentLevel; - _content = content; + Content = content; } public string AsStr(string lineEnd = "\n") { StringBuilder sb = new(); - for (int i = 0; i < _content.Count; i++) + for (int i = 0; i < Content.Count; i++) { if (i > 0) { sb.Append(lineEnd); } - sb.Append(_content[i].Span.ToString()); + sb.Append(Content[i].Span.ToString()); } return sb.ToString(); @@ -94,11 +117,58 @@ public string GetId() { return "Comment"; } + + public bool Equals(AstComment? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + if (CommentLevel != other.CommentLevel) return false; + if (Content.Count != other.Content.Count) return false; + for (int i = 0; i < Content.Count; i++) + { + var l = Content[i]; + var r = other.Content[i]; + if (!l.Span.SequenceEqual(r.Span)) + { + return false; + } + } + + return true; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((AstComment)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine((int)CommentLevel, Content); + } } - public class Junk : IEntry + public class Junk : IEntry, IEquatable { - public ReadOnlyMemory Content; + public readonly ReadOnlyMemory Content; + + public Junk() + { + Content = ReadOnlyMemory.Empty; + } + + public Junk(ReadOnlyMemory content) + { + Content = content; + } + + public Junk(string content) + { + Content = content.AsMemory(); + } public string AsStr() { @@ -109,5 +179,25 @@ public string GetId() { return Content.Span.ToString(); } + + public bool Equals(Junk? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Content.Span.SequenceEqual(other.Content.Span); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((Junk)obj); + } + + public override int GetHashCode() + { + return Content.GetHashCode(); + } } } \ No newline at end of file diff --git a/Linguini.Syntax/Ast/Expression.cs b/Linguini.Syntax/Ast/Expression.cs index a9e1ce4..cf0024c 100644 --- a/Linguini.Syntax/Ast/Expression.cs +++ b/Linguini.Syntax/Ast/Expression.cs @@ -1,78 +1,182 @@ using System; using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +// ReSharper disable UnusedMember.Global namespace Linguini.Syntax.Ast { - public class TextLiteral : IInlineExpression, IPatternElement + public class TextLiteral : IInlineExpression, IPatternElement, IEquatable { - public ReadOnlyMemory Value; + public readonly ReadOnlyMemory Value; public TextLiteral(ReadOnlyMemory value) { Value = value; } - public bool Equals(IPatternElement? other) + public TextLiteral(string id) { - if (other is TextLiteral textLiteralOther) - { - return Value.Span.SequenceEqual(textLiteralOther.Value.Span); - } - - return false; + Value = id.AsMemory(); } public override string ToString() { return Value.Span.ToString(); } + + public bool Equals(TextLiteral? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Value.Span.SequenceEqual(other.Value.Span); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((TextLiteral)obj); + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } } - public class NumberLiteral : IInlineExpression + public class NumberLiteral : IInlineExpression, IEquatable { - public ReadOnlyMemory Value; + public readonly ReadOnlyMemory Value; public NumberLiteral(ReadOnlyMemory value) { Value = value; } + public NumberLiteral(float num) + { + Value = num.ToString(CultureInfo.InvariantCulture).AsMemory(); + } + + public NumberLiteral(double num) + { + Value = num.ToString(CultureInfo.InvariantCulture).AsMemory(); + } + public override string ToString() { return Value.Span.ToString(); } + + public bool Equals(NumberLiteral? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Value.Span.SequenceEqual(other.Value.Span); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((NumberLiteral)obj); + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } } - public class FunctionReference : IInlineExpression + public class FunctionReference : IInlineExpression, IEquatable { - public Identifier Id; - public CallArguments Arguments; + public readonly Identifier Id; + public readonly CallArguments Arguments; public FunctionReference(Identifier id, CallArguments arguments) { Id = id; Arguments = arguments; } + + public FunctionReference(string id, CallArguments arguments) + { + Id = new Identifier(id); + Arguments = arguments; + } + + public bool Equals(FunctionReference? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Id.Equals(other.Id) && Arguments.Equals(other.Arguments); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((FunctionReference)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(Id, Arguments); + } } - public class MessageReference : IInlineExpression + public class MessageReference : IInlineExpression, IEquatable { - public Identifier Id; - public Identifier? Attribute; + public readonly Identifier Id; + public readonly Identifier? Attribute; public MessageReference(Identifier id, Identifier? attribute) { Id = id; Attribute = attribute; } + + public MessageReference(string id, string? attribute = null) + { + Id = new Identifier(id); + if (attribute != null) + { + Attribute = new Identifier(attribute); + } + } + + public bool Equals(MessageReference? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Id.Equals(other.Id) && Identifier.Comparator.Equals(Attribute, other.Attribute); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((MessageReference)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(Id, Attribute); + } } - public class DynamicReference : IInlineExpression + public class DynamicReference : IInlineExpression, IEquatable { - public Identifier Id; - public Identifier? Attribute; - public CallArguments? Arguments; + public readonly Identifier Id; + public readonly Identifier? Attribute; + public readonly CallArguments? Arguments; public DynamicReference(Identifier id, Identifier? attribute, CallArguments? arguments) { @@ -80,13 +184,62 @@ public DynamicReference(Identifier id, Identifier? attribute, CallArguments? arg Attribute = attribute; Arguments = arguments; } + + public DynamicReference(string id, string? attribute = null, CallArguments? arguments = null) + { + Id = new Identifier(id); + if (attribute != null) + { + Attribute = new Identifier(attribute); + } + + if (arguments != null) + { + Arguments = arguments.Value; + } + } + + public DynamicReference(string id, string? attribute, CallArgumentsBuilder? callArgumentsBuilder) + { + Id = new Identifier(id); + if (attribute != null) + { + Attribute = new Identifier(attribute); + } + + if (callArgumentsBuilder != null) + { + Arguments = callArgumentsBuilder.Build(); + } + } + + public bool Equals(DynamicReference? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Id.Equals(other.Id) && Identifier.Comparator.Equals(Attribute, other.Attribute) && + Nullable.Equals(Arguments, other.Arguments); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((DynamicReference)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(Id, Attribute, Arguments); + } } - - public class TermReference : IInlineExpression + + public class TermReference : IInlineExpression, IEquatable { - public Identifier Id; - public Identifier? Attribute; - public CallArguments? Arguments; + public readonly Identifier Id; + public readonly Identifier? Attribute; + public readonly CallArguments? Arguments; public TermReference(Identifier id, Identifier? attribute, CallArguments? arguments) { @@ -94,71 +247,410 @@ public TermReference(Identifier id, Identifier? attribute, CallArguments? argume Attribute = attribute; Arguments = arguments; } + + public TermReference(string id, string? attribute = null, CallArguments? arguments = null) + { + Id = new Identifier(id); + if (attribute != null) + { + Attribute = new Identifier(attribute); + } + + if (arguments != null) + { + Arguments = arguments.Value; + } + } + + public TermReference(string id, string? attribute, CallArgumentsBuilder? argumentsBuilder) + { + Id = new Identifier(id); + if (attribute != null) + { + Attribute = new Identifier(attribute); + } + + if (argumentsBuilder != null) + { + Arguments = argumentsBuilder.Build(); + } + } + + public bool Equals(TermReference? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Id.Equals(other.Id) && Identifier.Comparator.Equals(Attribute, other.Attribute) && + Nullable.Equals(Arguments, other.Arguments); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((TermReference)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(Id, Attribute, Arguments); + } } - - public class VariableReference : IInlineExpression + + public class VariableReference : IInlineExpression, IEquatable { - public Identifier Id; + public readonly Identifier Id; public VariableReference(Identifier id) { Id = id; } + + public VariableReference(string id) + { + Id = new Identifier(id); + } + + public bool Equals(VariableReference? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Identifier.Comparator.Equals(Id , other.Id); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((VariableReference)obj); + } + + public override int GetHashCode() + { + return Id.GetHashCode(); + } } - - public class Placeable : IInlineExpression, IPatternElementPlaceholder, IPatternElement + + public class Placeable : IInlineExpression, IPatternElementPlaceholder, IPatternElement, IEquatable { - public IExpression Expression; + public readonly IExpression Expression; public Placeable(IExpression expression) { Expression = expression; } - public bool Equals(IPatternElement? other) + public bool Equals(Placeable? other) { - if (other is Placeable otherPlaceable) - { - return Expression == otherPlaceable.Expression; - } - return false; + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Expression.Equals(other.Expression); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((Placeable)obj); + } + + public override int GetHashCode() + { + return Expression.GetHashCode(); + } + } + + public class PlaceableBuilder + { + private readonly IExpression _expression; + + private PlaceableBuilder(IExpression expression) + { + _expression = expression; + } + + public static PlaceableBuilder InlineExpression(InlineExpressionBuilder inlineBuilder) + { + return new PlaceableBuilder(inlineBuilder.Build()); + } + + public static PlaceableBuilder InlineExpression(SelectExpressionBuilder selectorExpression) + { + return new PlaceableBuilder(selectorExpression.Build()); + } + + public Placeable Build() + { + return new Placeable(_expression); } } - - public struct CallArguments + + public struct CallArguments : IEquatable { - public List PositionalArgs; - public List NamedArgs; + public readonly List PositionalArgs; + public readonly List NamedArgs; public CallArguments(List positionalArgs, List namedArgs) { PositionalArgs = positionalArgs; NamedArgs = namedArgs; } + + public bool Equals(CallArguments other) + { + return PositionalArgs.SequenceEqual(other.PositionalArgs, IInlineExpression.Comparer) + && NamedArgs.SequenceEqual(other.NamedArgs); + } + + public override bool Equals(object? obj) + { + return obj is CallArguments other && Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Combine(PositionalArgs, NamedArgs); + } } - - public struct NamedArgument + + public readonly struct NamedArgument : IEquatable { - public Identifier Name; - public IInlineExpression Value; + public readonly Identifier Name; + public readonly IInlineExpression Value; public NamedArgument(Identifier name, IInlineExpression value) { Name = name; Value = value; } + + public bool Equals(NamedArgument other) + { + return Name.Equals(other.Name) && IInlineExpression.Comparer.Equals(Value, other.Value); + } + + public override bool Equals(object? obj) + { + return obj is NamedArgument other && Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Combine(Name, Value); + } + } + + public class CallArgumentsBuilder + { + private readonly List _positionalArgs = new(); + private readonly List _namedArgs = new(); + + public CallArgumentsBuilder AddPositionalArg(InlineExpressionBuilder arg) + { + _positionalArgs.Add(arg.Build()); + return this; + } + + public CallArgumentsBuilder AddPositionalArg(string text) + { + _positionalArgs.Add(new TextLiteral(text)); + return this; + } + + public CallArgumentsBuilder AddPositionalArg(double number) + { + _positionalArgs.Add(new NumberLiteral(number)); + return this; + } + + public CallArgumentsBuilder AddPositionalArg(float number) + { + _positionalArgs.Add(new NumberLiteral(number)); + return this; + } + + public CallArgumentsBuilder AddNamedArg(string identifier, InlineExpressionBuilder inlineExpression) + { + _namedArgs.Add(new NamedArgument(new Identifier(identifier), inlineExpression.Build())); + return this; + } + + public CallArgumentsBuilder AddNamedArg(string identifier, IInlineExpression inlineExpression) + { + _namedArgs.Add(new NamedArgument(new Identifier(identifier), inlineExpression)); + return this; + } + + public CallArgumentsBuilder AddNamedArg(string identifier, string text) + { + _namedArgs.Add(new NamedArgument(new Identifier(identifier), new TextLiteral(text))); + return this; + } + + public CallArgumentsBuilder AddNamedArg(string identifier, float number) + { + _namedArgs.Add(new NamedArgument(new Identifier(identifier), new NumberLiteral(number))); + return this; + } + + public CallArgumentsBuilder AddNamedArg(string identifier, double number) + { + _namedArgs.Add(new NamedArgument(new Identifier(identifier), new NumberLiteral(number))); + return this; + } + + public CallArguments Build() + { + return new CallArguments(_positionalArgs, _namedArgs); + } } - - public class SelectExpression : IExpression + + public class InlineExpressionBuilder { - public IInlineExpression Selector; - public List Variants; + private IInlineExpression _expression; + + private InlineExpressionBuilder(IInlineExpression expression) + { + _expression = expression; + } + + public static InlineExpressionBuilder CreateDynamicReference(string id, string? attribute = null, + CallArgumentsBuilder? callArgumentsBuilder = null) + { + return new InlineExpressionBuilder(new DynamicReference(id, attribute, callArgumentsBuilder)); + } + + public static InlineExpressionBuilder CreateFunctionReference(string id, + CallArgumentsBuilder callArgumentsBuilder) + { + return new InlineExpressionBuilder(new FunctionReference(id, callArgumentsBuilder.Build())); + } + + public static InlineExpressionBuilder CreateMessageReference(string id, string? attribute = null) + { + return new InlineExpressionBuilder(new MessageReference(id, attribute)); + } + + public static InlineExpressionBuilder CreateNumber(double numberLiteral) + { + return new InlineExpressionBuilder(new NumberLiteral(numberLiteral)); + } + + public static InlineExpressionBuilder CreateNumber(float numberLiteral) + { + return new InlineExpressionBuilder(new NumberLiteral(numberLiteral)); + } + + public static InlineExpressionBuilder CreatePlaceable(Placeable placeable) + { + return new InlineExpressionBuilder(placeable); + } + + public static InlineExpressionBuilder CreatePlaceable(PlaceableBuilder placeable) + { + return new InlineExpressionBuilder(placeable.Build()); + } + + public static InlineExpressionBuilder CreateTermReference(string id, string? attribute = null, + CallArgumentsBuilder? callArgumentsBuilder = null) + { + return new InlineExpressionBuilder(new TermReference(id, attribute, callArgumentsBuilder)); + } + + public static InlineExpressionBuilder CreateTextLiteral(string textLiteral) + { + return new InlineExpressionBuilder(new TextLiteral(textLiteral)); + } + + public static InlineExpressionBuilder CreateVariableReferences(string textLiteral) + { + return new InlineExpressionBuilder(new VariableReference(textLiteral)); + } + + public IInlineExpression Build() + { + return _expression; + } + } + + public class SelectExpression : IExpression, IEquatable + { + public readonly IInlineExpression Selector; + public readonly List Variants; public SelectExpression(IInlineExpression selector, List variants) { Selector = selector; Variants = variants; } + + public bool Equals(SelectExpression? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return IInlineExpression.Comparer.Equals(Selector, other.Selector) + && Variants.SequenceEqual(other.Variants); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((SelectExpression)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(Selector, Variants); + } + } + + public class SelectExpressionBuilder : IAddVariant + { + private readonly IInlineExpression _selector; + private readonly List _variants = new(); + + public SelectExpressionBuilder(IInlineExpression selector) + { + _selector = selector; + } + + public IAddVariant AddVariant(string selector, PatternBuilder patternBuilder) + { + _variants.Add(new Variant(selector, patternBuilder)); + return this; + } + + public IAddVariant AddVariant(float selector, PatternBuilder patternBuilder) + { + _variants.Add(new Variant(selector, patternBuilder)); + return this; + } + + public SelectExpressionBuilder SetDefault(int? defaultSelector = null) + { + var selector = defaultSelector is >= 0 && defaultSelector < _variants.Count + ? _variants.Count - 1 + : defaultSelector!.Value; + _variants[selector].InternalDefault = true; + return this; + } + + public SelectExpression Build() + { + return new SelectExpression(_selector, _variants); + } + } + + public interface IAddVariant + { + public IAddVariant AddVariant(string selector, PatternBuilder patternBuilder); + public IAddVariant AddVariant(float selector, PatternBuilder patternBuilder); + public SelectExpressionBuilder SetDefault(int? defaultSelector = null); } public enum VariantType : byte @@ -168,19 +660,68 @@ public enum VariantType : byte } - public class Variant + public class Variant : IEquatable { - public VariantType Type; - public ReadOnlyMemory Key; - public Pattern Value; - public bool IsDefault; + public readonly VariantType Type; + public readonly ReadOnlyMemory Key; + public Pattern Value => InternalValue; + public bool IsDefault => InternalDefault; + + protected internal bool InternalDefault; + protected internal Pattern InternalValue; public Variant(VariantType type, ReadOnlyMemory key) { Type = type; Key = key; - Value = new Pattern(); - IsDefault = false; + InternalValue = new Pattern(); + InternalDefault = false; + } + + public Variant(VariantType type, ReadOnlyMemory key, Pattern pattern, bool isDefault = false) + { + Type = type; + Key = key; + InternalValue = pattern; + InternalDefault = isDefault; + } + + public Variant(string key, PatternBuilder builder) + { + Type = VariantType.Identifier; + Key = key.AsMemory(); + InternalValue = builder.Build(); + InternalDefault = false; + } + + public Variant(float key, PatternBuilder builder) + { + Type = VariantType.NumberLiteral; + Key = key.ToString(CultureInfo.InvariantCulture).AsMemory(); + InternalValue = builder.Build(); + InternalDefault = false; + } + + public bool Equals(Variant? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Type == other.Type && Key.Span.SequenceEqual(other.Key.Span) && + InternalDefault == other.InternalDefault && + InternalValue.Equals(other.InternalValue); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((Variant)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine((int)Type, Key, InternalDefault, InternalValue); } } } \ No newline at end of file diff --git a/Linguini.Syntax/Ast/Pattern.cs b/Linguini.Syntax/Ast/Pattern.cs index 54a7a5b..abbc9cb 100644 --- a/Linguini.Syntax/Ast/Pattern.cs +++ b/Linguini.Syntax/Ast/Pattern.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; namespace Linguini.Syntax.Ast @@ -29,8 +30,35 @@ public interface IPatternElementPlaceholder { } - public interface IPatternElement: IEquatable - { + public interface IPatternElement + { + public static PatternComparer PatternComparer = new(); + } + + public class PatternComparer : IEqualityComparer + { + public bool Equals(IPatternElement? left, IPatternElement? right) + { + return (left, right) switch + { + (TextLiteral l, TextLiteral r) => l.Equals(r), + (Placeable l, Placeable r) => l.Equals(r), + _ => false, + }; + } + + public int GetHashCode(IPatternElement obj) + { + switch (obj) + { + case TextLiteral textLiteral: + return textLiteral.GetHashCode(); + case Placeable placeable: + return placeable.GetHashCode(); + default: + throw new ArgumentException("Unexpected type", nameof(obj)); + } + } } public class TextElementPlaceholder : IPatternElementPlaceholder diff --git a/Linguini.Syntax/Linguini.Syntax.csproj b/Linguini.Syntax/Linguini.Syntax.csproj index 27903c9..92df86e 100644 --- a/Linguini.Syntax/Linguini.Syntax.csproj +++ b/Linguini.Syntax/Linguini.Syntax.csproj @@ -12,7 +12,7 @@ https://github.com/Ygg01/Linguini git net6.0;netstandard2.1;net8.0 - 0.7.0 + 0.8.0 README.md linguini.jpg diff --git a/Linguini.Syntax/Parser/LinguiniParser.cs b/Linguini.Syntax/Parser/LinguiniParser.cs index 7c7093e..91f973a 100644 --- a/Linguini.Syntax/Parser/LinguiniParser.cs +++ b/Linguini.Syntax/Parser/LinguiniParser.cs @@ -1,5 +1,4 @@ - -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; @@ -142,12 +141,12 @@ public Resource ParseWithComments() if (entry is AstMessage message && lastBlankCount < 2) { - message.Comment = lastComment; + message.InternalComment = lastComment; } else if (entry is AstTerm term && lastBlankCount < 2) { - term.Comment = lastComment; + term.InternalComment = lastComment; } else { @@ -187,9 +186,9 @@ private void AddError(ParseError error, int entryStart, List errors, _reader.SkipToNextEntry(); error.Slice = new Range(entryStart, _reader.Position); errors.Add(error); - Junk junk = new(); + var contentSpan = _reader.ReadSlice(entryStart, _reader.Position); - junk.Content = contentSpan; + Junk junk = new(contentSpan); body.Add(junk); } @@ -947,8 +946,8 @@ private bool TryGetVariants(out List variants, out ParseError? error) if (value != null) { - variant.Value = value; - variant.IsDefault = isDefault; + variant.InternalValue = value; + variant.InternalDefault = isDefault; variants.Add(variant); _reader.SkipBlank(); } diff --git a/Linguini.sln b/Linguini.sln index c43d808..201f10d 100644 --- a/Linguini.sln +++ b/Linguini.sln @@ -18,6 +18,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Linguini.Serialization", "L EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Linguini.Bench", "Linguini.Bench\Linguini.Bench.csproj", "{464CC3E4-7259-4840-B342-B346F3533CED}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Linguini.Serialization.Test", "Linguini.Serialization.Test\Linguini.Serialization.Test.csproj", "{32A38C1D-CA5E-41DA-8FCB-07551D35D382}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -64,5 +66,9 @@ Global {464CC3E4-7259-4840-B342-B346F3533CED}.Debug|Any CPU.Build.0 = Debug|Any CPU {464CC3E4-7259-4840-B342-B346F3533CED}.Release|Any CPU.ActiveCfg = Release|Any CPU {464CC3E4-7259-4840-B342-B346F3533CED}.Release|Any CPU.Build.0 = Release|Any CPU + {32A38C1D-CA5E-41DA-8FCB-07551D35D382}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {32A38C1D-CA5E-41DA-8FCB-07551D35D382}.Debug|Any CPU.Build.0 = Debug|Any CPU + {32A38C1D-CA5E-41DA-8FCB-07551D35D382}.Release|Any CPU.ActiveCfg = Release|Any CPU + {32A38C1D-CA5E-41DA-8FCB-07551D35D382}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/PluralRules.Generator/PluralRules.Generator.csproj b/PluralRules.Generator/PluralRules.Generator.csproj index 3dbbc12..dc6ed6b 100644 --- a/PluralRules.Generator/PluralRules.Generator.csproj +++ b/PluralRules.Generator/PluralRules.Generator.csproj @@ -10,6 +10,7 @@ README.md linguini.jpg sourcegen, plural rules, icu + 0.8.0 diff --git a/PluralRules.Test/Cldr/CldrParserTest.cs b/PluralRules.Test/Cldr/CldrParserTest.cs index 3b3c28b..7339254 100644 --- a/PluralRules.Test/Cldr/CldrParserTest.cs +++ b/PluralRules.Test/Cldr/CldrParserTest.cs @@ -60,8 +60,8 @@ public void ParseEmpty() [Parallelizable] [TestCase("n is 12 @integer 0, 5, 7~20", new[] {"0", "5", "7~20"}, new string[] { })] [TestCase("n is 12 @integer 0, 5, 7~20 @decimal 1, 3~6,...", new[] {"0", "5", "7~20"}, - new string[] {"1", "3~6"})] - [TestCase("@integer 0, 11~25, 100, 1000, …", new string[] {"0", "11~25", "100", "1000"}, new string[] { })] + new[] {"1", "3~6"})] + [TestCase("@integer 0, 11~25, 100, 1000, …", new[] {"0", "11~25", "100", "1000"}, new string[] { })] public void ParseSamples(string input, string[] expIntRangeList, string[] expDecRangeList) { var rule = new CldrParser(input).ParseRule(); diff --git a/PluralRules.Test/PluralRules.Test.csproj b/PluralRules.Test/PluralRules.Test.csproj index 5499de4..734a65e 100644 --- a/PluralRules.Test/PluralRules.Test.csproj +++ b/PluralRules.Test/PluralRules.Test.csproj @@ -6,6 +6,7 @@ PluralRules.Test Library net6.0 + 0.8.0