From e24e5343e8b83603a0162c7a8ce007f415823919 Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Fri, 29 Dec 2023 17:24:06 +0100 Subject: [PATCH 01/19] Make Ast API readonly and add builders --- Linguini.Syntax/Ast/Base.cs | 52 ++++++++++++++++++++---- Linguini.Syntax/Ast/Entry.cs | 37 +++++++++-------- Linguini.Syntax/Ast/Expression.cs | 30 +++++++------- Linguini.Syntax/Parser/LinguiniParser.cs | 4 +- 4 files changed, 79 insertions(+), 44 deletions(-) diff --git a/Linguini.Syntax/Ast/Base.cs b/Linguini.Syntax/Ast/Base.cs index 8d2a231..d6d7605 100644 --- a/Linguini.Syntax/Ast/Base.cs +++ b/Linguini.Syntax/Ast/Base.cs @@ -1,15 +1,17 @@ using System; 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 Identifier Id; - public Pattern Value; + public readonly Identifier Id; + public readonly Pattern Value; public Attribute(Identifier id, Pattern value) { @@ -22,11 +24,16 @@ public void Deconstruct(out Identifier id, out Pattern value) id = Id; value = Value; } + + public static Attribute From(string id, PatternBuilder patternBuilder) + { + return new Attribute(Identifier.From(id), patternBuilder.Build()); + } } public class Pattern { - public List Elements; + public readonly List Elements; public Pattern(List elements) { @@ -39,6 +46,30 @@ public Pattern() } } + public class PatternBuilder + { + private readonly List _patternElements = new(); + + private PatternBuilder() { } + + public PatternBuilder AddText(string textLiteral) + { + _patternElements.Add(new TextLiteral(textLiteral.AsMemory())); + return this; + } + + public PatternBuilder AddPlaceable(IExpression expr) + { + _patternElements.Add(new Placeable(expr)); + return this; + } + + public Pattern Build() + { + return new Pattern(_patternElements); + } + } + public class Identifier : IEquatable { @@ -49,6 +80,11 @@ public Identifier(ReadOnlyMemory name) Name = name; } + public static Identifier From(string id) + { + return new Identifier(id.AsMemory()); + } + public override string ToString() { return Name.Span.ToString(); @@ -94,12 +130,10 @@ 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(); diff --git a/Linguini.Syntax/Ast/Entry.cs b/Linguini.Syntax/Ast/Entry.cs index ca8ef0b..33831ac 100644 --- a/Linguini.Syntax/Ast/Entry.cs +++ b/Linguini.Syntax/Ast/Entry.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Text; -using Linguini.Syntax.Parser; using Linguini.Syntax.Parser.Error; @@ -22,17 +20,19 @@ public Resource(List body, List errors) public class AstMessage : 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 AstMessage(Identifier id, Pattern? pattern, List attrs, AstComment? comment) + public AstComment? Comment => InternalComment; + protected internal AstComment? InternalComment; + + public AstMessage(Identifier id, Pattern? pattern, List attrs, AstComment? internalComment) { Id = id; Value = pattern; Attributes = attrs; - Comment = comment; + InternalComment = internalComment; } public string GetId() @@ -43,17 +43,18 @@ public string GetId() 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; } @@ -65,26 +66,26 @@ public string GetId() public class AstComment : IEntry { - 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(); diff --git a/Linguini.Syntax/Ast/Expression.cs b/Linguini.Syntax/Ast/Expression.cs index a9e1ce4..d555e09 100644 --- a/Linguini.Syntax/Ast/Expression.cs +++ b/Linguini.Syntax/Ast/Expression.cs @@ -46,7 +46,7 @@ public override string ToString() public class FunctionReference : IInlineExpression { - public Identifier Id; + public readonly Identifier Id; public CallArguments Arguments; public FunctionReference(Identifier id, CallArguments arguments) @@ -58,8 +58,8 @@ public FunctionReference(Identifier id, CallArguments arguments) public class MessageReference : IInlineExpression { - public Identifier Id; - public Identifier? Attribute; + public readonly Identifier Id; + public readonly Identifier? Attribute; public MessageReference(Identifier id, Identifier? attribute) { @@ -70,8 +70,8 @@ public MessageReference(Identifier id, Identifier? attribute) public class DynamicReference : IInlineExpression { - public Identifier Id; - public Identifier? Attribute; + public readonly Identifier Id; + public readonly Identifier? Attribute; public CallArguments? Arguments; public DynamicReference(Identifier id, Identifier? attribute, CallArguments? arguments) @@ -84,8 +84,8 @@ public DynamicReference(Identifier id, Identifier? attribute, CallArguments? arg public class TermReference : IInlineExpression { - public Identifier Id; - public Identifier? Attribute; + public readonly Identifier Id; + public readonly Identifier? Attribute; public CallArguments? Arguments; public TermReference(Identifier id, Identifier? attribute, CallArguments? arguments) @@ -98,7 +98,7 @@ public TermReference(Identifier id, Identifier? attribute, CallArguments? argume public class VariableReference : IInlineExpression { - public Identifier Id; + public readonly Identifier Id; public VariableReference(Identifier id) { @@ -108,7 +108,7 @@ public VariableReference(Identifier id) public class Placeable : IInlineExpression, IPatternElementPlaceholder, IPatternElement { - public IExpression Expression; + public readonly IExpression Expression; public Placeable(IExpression expression) { @@ -127,8 +127,8 @@ public bool Equals(IPatternElement? other) public struct CallArguments { - public List PositionalArgs; - public List NamedArgs; + public readonly List PositionalArgs; + public readonly List NamedArgs; public CallArguments(List positionalArgs, List namedArgs) { @@ -139,8 +139,8 @@ public CallArguments(List positionalArgs, List public struct NamedArgument { - public Identifier Name; - public IInlineExpression Value; + public readonly Identifier Name; + public readonly IInlineExpression Value; public NamedArgument(Identifier name, IInlineExpression value) { @@ -151,8 +151,8 @@ public NamedArgument(Identifier name, IInlineExpression value) public class SelectExpression : IExpression { - public IInlineExpression Selector; - public List Variants; + public readonly IInlineExpression Selector; + public readonly List Variants; public SelectExpression(IInlineExpression selector, List variants) { diff --git a/Linguini.Syntax/Parser/LinguiniParser.cs b/Linguini.Syntax/Parser/LinguiniParser.cs index 7c7093e..78d5535 100644 --- a/Linguini.Syntax/Parser/LinguiniParser.cs +++ b/Linguini.Syntax/Parser/LinguiniParser.cs @@ -142,12 +142,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 { From 2d32d56cdcc23e2fd8c71a8c0e941bedf40c7b8e Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Sat, 30 Dec 2023 10:06:27 +0100 Subject: [PATCH 02/19] Add builder for AstMessage/AstTerm --- Linguini.Syntax/Ast/Base.cs | 56 ++++++++++-- Linguini.Syntax/Ast/Expression.cs | 141 +++++++++++++++++++++++++++--- 2 files changed, 179 insertions(+), 18 deletions(-) diff --git a/Linguini.Syntax/Ast/Base.cs b/Linguini.Syntax/Ast/Base.cs index d6d7605..ac37eae 100644 --- a/Linguini.Syntax/Ast/Base.cs +++ b/Linguini.Syntax/Ast/Base.cs @@ -27,7 +27,7 @@ public void Deconstruct(out Identifier id, out Pattern value) public static Attribute From(string id, PatternBuilder patternBuilder) { - return new Attribute(Identifier.From(id), patternBuilder.Build()); + return new Attribute(new Identifier(id), patternBuilder.Build()); } } @@ -50,17 +50,57 @@ public class PatternBuilder { private readonly List _patternElements = new(); - private PatternBuilder() { } - public PatternBuilder AddText(string textLiteral) { - _patternElements.Add(new TextLiteral(textLiteral.AsMemory())); + _patternElements.Add(new TextLiteral(textLiteral)); + return this; + } + + public PatternBuilder AddNumberLiteral(float number) + { + _patternElements.Add(new Placeable(new NumberLiteral(number))); return this; } - public PatternBuilder AddPlaceable(IExpression expr) + 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 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(expr)); + _patternElements.Add(new Placeable(selectExpressionBuilder.Build())); return this; } @@ -80,9 +120,9 @@ public Identifier(ReadOnlyMemory name) Name = name; } - public static Identifier From(string id) + public Identifier(string id) { - return new Identifier(id.AsMemory()); + Name = id.AsMemory(); } public override string ToString() diff --git a/Linguini.Syntax/Ast/Expression.cs b/Linguini.Syntax/Ast/Expression.cs index d555e09..d5ec2b6 100644 --- a/Linguini.Syntax/Ast/Expression.cs +++ b/Linguini.Syntax/Ast/Expression.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; +using System.Globalization; +// ReSharper disable UnusedMember.Global namespace Linguini.Syntax.Ast { public class TextLiteral : IInlineExpression, IPatternElement { - public ReadOnlyMemory Value; + public readonly ReadOnlyMemory Value; public TextLiteral(ReadOnlyMemory value) { @@ -22,6 +24,11 @@ public bool Equals(IPatternElement? other) return false; } + public TextLiteral(string id) + { + Value = id.AsMemory(); + } + public override string ToString() { return Value.Span.ToString(); @@ -30,12 +37,23 @@ public override string ToString() public class NumberLiteral : IInlineExpression { - 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() { @@ -47,13 +65,19 @@ public override string ToString() public class FunctionReference : IInlineExpression { public readonly Identifier Id; - public CallArguments Arguments; + 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 class MessageReference : IInlineExpression @@ -66,13 +90,22 @@ 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 class DynamicReference : IInlineExpression { public readonly Identifier Id; public readonly Identifier? Attribute; - public CallArguments? Arguments; + public readonly CallArguments? Arguments; public DynamicReference(Identifier id, Identifier? attribute, CallArguments? arguments) { @@ -80,13 +113,27 @@ 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 class TermReference : IInlineExpression { public readonly Identifier Id; public readonly Identifier? Attribute; - public CallArguments? Arguments; + public readonly CallArguments? Arguments; public TermReference(Identifier id, Identifier? attribute, CallArguments? arguments) { @@ -94,6 +141,20 @@ 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 class VariableReference : IInlineExpression @@ -105,7 +166,7 @@ public VariableReference(Identifier id) Id = id; } } - + public class Placeable : IInlineExpression, IPatternElementPlaceholder, IPatternElement { public readonly IExpression Expression; @@ -136,7 +197,7 @@ public CallArguments(List positionalArgs, List NamedArgs = namedArgs; } } - + public struct NamedArgument { public readonly Identifier Name; @@ -148,7 +209,7 @@ public NamedArgument(Identifier name, IInlineExpression value) Value = value; } } - + public class SelectExpression : IExpression { public readonly IInlineExpression Selector; @@ -161,6 +222,50 @@ public SelectExpression(IInlineExpression selector, List variants) } } + public class SelectExpressionBuilder : IAddVariant + { + private readonly IInlineExpression _selector; + private 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].IsDefault = 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 { Identifier, @@ -170,7 +275,7 @@ public enum VariantType : byte public class Variant { - public VariantType Type; + public readonly VariantType Type; public ReadOnlyMemory Key; public Pattern Value; public bool IsDefault; @@ -182,5 +287,21 @@ public Variant(VariantType type, ReadOnlyMemory key) Value = new Pattern(); IsDefault = false; } + + public Variant(string key, PatternBuilder builder) + { + Type = VariantType.Identifier; + Key = key.AsMemory(); + Value = builder.Build(); + IsDefault = false; + } + + public Variant(float key, PatternBuilder builder) + { + Type = VariantType.NumberLiteral; + Key = key.ToString(CultureInfo.InvariantCulture).AsMemory(); + Value = builder.Build(); + IsDefault = false; + } } } \ No newline at end of file From 64cef82f368841a71083d75d0e49824ac57b9f60 Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Sat, 13 Jan 2024 11:32:24 +0100 Subject: [PATCH 03/19] Make more fields/properties readonly --- Linguini.Bundle/Resolver/WriterHelpers.cs | 8 +++----- Linguini.Syntax/Ast/Entry.cs | 13 ++++++++++-- Linguini.Syntax/Ast/Expression.cs | 25 +++++++++++++---------- Linguini.Syntax/Parser/LinguiniParser.cs | 11 +++++----- 4 files changed, 33 insertions(+), 24 deletions(-) diff --git a/Linguini.Bundle/Resolver/WriterHelpers.cs b/Linguini.Bundle/Resolver/WriterHelpers.cs index 7f7f18c..d9c38fe 100644 --- a/Linguini.Bundle/Resolver/WriterHelpers.cs +++ b/Linguini.Bundle/Resolver/WriterHelpers.cs @@ -100,11 +100,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()); diff --git a/Linguini.Syntax/Ast/Entry.cs b/Linguini.Syntax/Ast/Entry.cs index 33831ac..59b6594 100644 --- a/Linguini.Syntax/Ast/Entry.cs +++ b/Linguini.Syntax/Ast/Entry.cs @@ -3,7 +3,6 @@ using System.Text; using Linguini.Syntax.Parser.Error; - namespace Linguini.Syntax.Ast { public record Resource @@ -99,7 +98,17 @@ public string GetId() public class Junk : IEntry { - public ReadOnlyMemory Content; + public readonly ReadOnlyMemory Content; + + public Junk() + { + Content = ReadOnlyMemory.Empty; + } + + public Junk(ReadOnlyMemory content) + { + Content = content; + } public string AsStr() { diff --git a/Linguini.Syntax/Ast/Expression.cs b/Linguini.Syntax/Ast/Expression.cs index d5ec2b6..4400459 100644 --- a/Linguini.Syntax/Ast/Expression.cs +++ b/Linguini.Syntax/Ast/Expression.cs @@ -225,7 +225,7 @@ public SelectExpression(IInlineExpression selector, List variants) public class SelectExpressionBuilder : IAddVariant { private readonly IInlineExpression _selector; - private List _variants = new(); + private readonly List _variants = new(); public SelectExpressionBuilder(IInlineExpression selector) { @@ -249,7 +249,7 @@ public SelectExpressionBuilder SetDefault(int? defaultSelector = null) var selector = defaultSelector is >= 0 && defaultSelector < _variants.Count ? _variants.Count - 1 : defaultSelector!.Value; - _variants[selector].IsDefault = true; + _variants[selector].InternalDefault = true; return this; } @@ -276,32 +276,35 @@ public enum VariantType : byte public class Variant { public readonly VariantType Type; - public ReadOnlyMemory Key; - public Pattern Value; - public bool IsDefault; + 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(string key, PatternBuilder builder) { Type = VariantType.Identifier; Key = key.AsMemory(); - Value = builder.Build(); - IsDefault = false; + InternalValue = builder.Build(); + InternalDefault = false; } public Variant(float key, PatternBuilder builder) { Type = VariantType.NumberLiteral; Key = key.ToString(CultureInfo.InvariantCulture).AsMemory(); - Value = builder.Build(); - IsDefault = false; + InternalValue = builder.Build(); + InternalDefault = false; } } } \ No newline at end of file diff --git a/Linguini.Syntax/Parser/LinguiniParser.cs b/Linguini.Syntax/Parser/LinguiniParser.cs index 78d5535..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; @@ -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(); } From 0acd3528dc18af7ba0d60564b59f98bf22c0d84e Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Sat, 13 Jan 2024 15:31:05 +0100 Subject: [PATCH 04/19] Bump version 0.8.0 --- Linguini.Bench/Linguini.Bench.csproj | 1 + Linguini.Bundle.Test/Linguini.Bundle.Test.csproj | 2 +- Linguini.Bundle/Linguini.Bundle.csproj | 2 +- Linguini.Serialization/Linguini.Serialization.csproj | 6 +----- Linguini.Shared/Linguini.Shared.csproj | 2 +- Linguini.Syntax.Tests/Linguini.Syntax.Tests.csproj | 1 + Linguini.Syntax/Linguini.Syntax.csproj | 2 +- PluralRules.Generator/PluralRules.Generator.csproj | 1 + PluralRules.Test/PluralRules.Test.csproj | 1 + 9 files changed, 9 insertions(+), 9 deletions(-) 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/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.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/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/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/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 From b6e412b9af760fea4f19f8f26e2905bdaa41c180 Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Tue, 16 Jan 2024 23:27:46 +0100 Subject: [PATCH 05/19] Added serialization for everything. --- .../AttributeSerializerTest.cs | 38 +++++++ .../Linguini.Serialization.Test.csproj | 20 ++++ Linguini.Serialization.Test/TestUtil.cs | 41 +++++++ .../Converters/AttributeSerializer.cs | 48 ++++++++- .../Converters/CallArgumentsSerializer.cs | 56 +++++++++- .../Converters/DynamicReferenceSerializer.cs | 35 +++++- .../Converters/FunctionReferenceSerializer.cs | 28 ++++- .../Converters/IdentifierSerializer.cs | 64 ++++++++++- .../Converters/MessageReferenceSerializer.cs | 20 ++++ .../Converters/PatternSerializer.cs | 101 +++++++++++++++++- .../Converters/PlaceableSerializer.cs | 22 +++- .../Converters/ResourceSerializer.cs | 90 +++++++++++++++- .../Converters/SelectExpressionSerializer.cs | 29 ++++- .../Converters/TermReferenceSerializer.cs | 34 +++++- .../Converters/VariableReferenceSerializer.cs | 21 +++- .../Parser/LinguiniFtlParserTest.cs | 16 ++- Linguini.Syntax/Ast/Base.cs | 40 +++++++ Linguini.Syntax/Ast/Expression.cs | 8 ++ Linguini.sln | 6 ++ 19 files changed, 681 insertions(+), 36 deletions(-) create mode 100644 Linguini.Serialization.Test/AttributeSerializerTest.cs create mode 100644 Linguini.Serialization.Test/Linguini.Serialization.Test.csproj create mode 100644 Linguini.Serialization.Test/TestUtil.cs diff --git a/Linguini.Serialization.Test/AttributeSerializerTest.cs b/Linguini.Serialization.Test/AttributeSerializerTest.cs new file mode 100644 index 0000000..15c3d8f --- /dev/null +++ b/Linguini.Serialization.Test/AttributeSerializerTest.cs @@ -0,0 +1,38 @@ +using System.Text.Json; +using Linguini.Serialization.Converters; +using Linguini.Syntax.Ast; +using NUnit.Framework; +using Attribute = Linguini.Syntax.Ast.Attribute; + +namespace Linguini.Serialization.Test; + +[TestFixture] +public class AttributeSerializerTest +{ + [Test] + [TestOf(typeof(AttributeSerializer))] + [Parallelizable] + public void TestAttributeSerializer() + { + Attribute expected = new Attribute("desc", new PatternBuilder("description")); + string attributeJson = @" +{ + ""type"": ""Attribute"", + ""id"": { + ""type"": ""Identifier"", + ""value"": ""desc"" + }, + ""value"": { + ""type"": ""Pattern"", + ""elements"": [ + { + ""type"": ""TextLiteral"", + ""value"": ""description"" + } + ] + } +}"; + Attribute? actual = JsonSerializer.Deserialize(attributeJson, TestUtil.Options); + Assert.That(actual, Is.EqualTo(expected)); + } +} \ 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/TestUtil.cs b/Linguini.Serialization.Test/TestUtil.cs new file mode 100644 index 0000000..41455da --- /dev/null +++ b/Linguini.Serialization.Test/TestUtil.cs @@ -0,0 +1,41 @@ +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using Linguini.Serialization.Converters; + +namespace Linguini.Serialization.Test +{ + public static class TestUtil + { + public 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/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..74c0abb 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,7 +8,7 @@ namespace Linguini.Serialization.Converters { - public class CallArgumentsSerializer: JsonConverter + public class CallArgumentsSerializer : JsonConverter { public override CallArguments Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) @@ -25,6 +27,7 @@ public override void Write(Utf8JsonWriter writer, CallArguments value, JsonSeria { ResourceSerializer.WriteInlineExpression(writer, positionalArg, options); } + writer.WriteEndArray(); writer.WritePropertyName("named"); writer.WriteStartArray(); @@ -32,8 +35,57 @@ 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 (TryReadNamedArguments(arg, options, out var namedArg)) + { + namedArgs.Add(namedArg.Value); + } + } + + callArguments = new CallArguments(positionalArgs, namedArgs); + return true; + } + + + 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/DynamicReferenceSerializer.cs b/Linguini.Serialization/Converters/DynamicReferenceSerializer.cs index 50bc1d3..f54ca72 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,7 +8,8 @@ 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(); } @@ -19,20 +21,45 @@ 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) + { + Identifier? identifier = null; + if (!el.TryGetProperty("id", out var jsonId) && + !IdentifierSerializer.TryGetIdentifier(jsonId, options, out 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..639a671 100644 --- a/Linguini.Serialization/Converters/FunctionReferenceSerializer.cs +++ b/Linguini.Serialization/Converters/FunctionReferenceSerializer.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,7 +8,8 @@ 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(); } @@ -23,5 +25,27 @@ 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) + { + Identifier? ident = null; + if (!el.TryGetProperty("id", out JsonElement value) && + !IdentifierSerializer.TryGetIdentifier(value, options, out 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..719a136 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,54 @@ 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 (true) + { + 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 "value": + 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 +70,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("value", 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/MessageReferenceSerializer.cs b/Linguini.Serialization/Converters/MessageReferenceSerializer.cs index 2e948e8..8e6a284 100644 --- a/Linguini.Serialization/Converters/MessageReferenceSerializer.cs +++ b/Linguini.Serialization/Converters/MessageReferenceSerializer.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; using Linguini.Syntax.Ast; @@ -27,5 +28,24 @@ 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)) + { + IdentifierSerializer.TryGetIdentifier(prop, options, out attr); + } + + return new MessageReference(ident, attr); + } + + throw new JsonException("MessageReference requires `id` field"); + + } } } diff --git a/Linguini.Serialization/Converters/PatternSerializer.cs b/Linguini.Serialization/Converters/PatternSerializer.cs index 156df2e..98e790f 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,63 @@ 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(ResourceSerializer.ReadExpression(el, options)); + } } public override void Write(Utf8JsonWriter writer, Pattern pattern, JsonSerializerOptions options) @@ -55,5 +113,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(new Placeable(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..230b926 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 IInlineExpression 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..77a4953 100644 --- a/Linguini.Serialization/Converters/ResourceSerializer.cs +++ b/Linguini.Serialization/Converters/ResourceSerializer.cs @@ -1,7 +1,7 @@ -#nullable enable - -using System; +using System; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; using Linguini.Syntax.Ast; @@ -90,5 +90,87 @@ 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) + { + return new(el.GetProperty("value").GetDouble()); + } + + public static IExpression ReadExpression(JsonElement el, JsonSerializerOptions options) + { + IExpression x = el.GetProperty("type").GetString() 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), + "SelectExpression" => SelectExpressionSerializer.ProcessSelectExpression(el, options), + _ => throw new JsonException("Unexpected value") + }; + return x; + } + + + 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) + && TryReadInlineExpression(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; + } + + var (x, id) = key switch + { + NumberLiteral numberLiteral => (VariantType.NumberLiteral, numberLiteral.Value), + TextLiteral identifier => (VariantType.Identifier, identifier.Value), + _ => throw new JsonException("Variant can only be number or identifier.") + }; + + return new Variant(x, id, pattern, isDefault); + } + } + + throw new NotImplementedException(); + } + + public static bool TryReadInlineExpression(JsonElement el, JsonSerializerOptions options, + [MaybeNullWhen(false)] out IInlineExpression o) + { + o = el.GetProperty("type").GetString() 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") + }; + return true; + } } -} +} \ No newline at end of file diff --git a/Linguini.Serialization/Converters/SelectExpressionSerializer.cs b/Linguini.Serialization/Converters/SelectExpressionSerializer.cs index 4ddc2ee..7596fd6 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,7 +8,8 @@ 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(); } @@ -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(ResourceSerializer.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..ba956fe 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; @@ -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..51ac55a 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(); } - 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.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/Ast/Base.cs b/Linguini.Syntax/Ast/Base.cs index ac37eae..4b55121 100644 --- a/Linguini.Syntax/Ast/Base.cs +++ b/Linguini.Syntax/Ast/Base.cs @@ -18,6 +18,12 @@ public Attribute(Identifier id, Pattern value) Id = id; Value = value; } + + public Attribute(string id, PatternBuilder builder) + { + Id = new Identifier(id); + Value = builder.Build(); + } public void Deconstruct(out Identifier id, out Pattern value) { @@ -50,6 +56,21 @@ 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)); @@ -103,6 +124,25 @@ public PatternBuilder AddSelectExpression(SelectExpressionBuilder selectExpressi _patternElements.Add(new Placeable(selectExpressionBuilder.Build())); return this; } + + public PatternBuilder AddExpression(IExpression expr) + { + if (expr is TextLiteral text) + { + _patternElements.Add(text); + } + else + { + _patternElements.Add(new Placeable(expr)); + } + return this; + } + + public PatternBuilder AddPlaceable(Placeable placeable) + { + _patternElements.Add(placeable); + return this; + } public Pattern Build() { diff --git a/Linguini.Syntax/Ast/Expression.cs b/Linguini.Syntax/Ast/Expression.cs index 4400459..3ee3cee 100644 --- a/Linguini.Syntax/Ast/Expression.cs +++ b/Linguini.Syntax/Ast/Expression.cs @@ -291,6 +291,14 @@ public Variant(VariantType type, ReadOnlyMemory key) 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; 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 From 4415ce3bbc9fe647ef3d8d7b9d982ffe0ddad933 Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Tue, 16 Jan 2024 23:37:54 +0100 Subject: [PATCH 06/19] Fix equality of Pattern/Build. --- Linguini.Syntax/Ast/Base.cs | 56 +++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/Linguini.Syntax/Ast/Base.cs b/Linguini.Syntax/Ast/Base.cs index 4b55121..87b260e 100644 --- a/Linguini.Syntax/Ast/Base.cs +++ b/Linguini.Syntax/Ast/Base.cs @@ -8,7 +8,7 @@ namespace Linguini.Syntax.Ast { - public class Attribute + public class Attribute : IEquatable { public readonly Identifier Id; public readonly Pattern Value; @@ -35,9 +35,29 @@ 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 readonly List Elements; @@ -50,6 +70,38 @@ 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 (!patternElement.Equals(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 From 18725f3ffb5e0e9e29e83d29be141dee9118309a Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Fri, 19 Jan 2024 03:37:39 +0100 Subject: [PATCH 07/19] Add equatable and serializers for Ast --- .../AttributeSerializerTest.cs | 4 +- .../CallArgumentsSerializerTest.cs | 50 ++ .../Converters/CallArgumentsSerializer.cs | 8 +- .../Converters/IdentifierSerializer.cs | 8 +- .../Converters/MessageReferenceSerializer.cs | 2 +- Linguini.Syntax/Ast/Base.cs | 43 ++ Linguini.Syntax/Ast/Expression.cs | 482 ++++++++++++++++-- Linguini.Syntax/Ast/Pattern.cs | 32 +- 8 files changed, 586 insertions(+), 43 deletions(-) create mode 100644 Linguini.Serialization.Test/CallArgumentsSerializerTest.cs diff --git a/Linguini.Serialization.Test/AttributeSerializerTest.cs b/Linguini.Serialization.Test/AttributeSerializerTest.cs index 15c3d8f..00c178a 100644 --- a/Linguini.Serialization.Test/AttributeSerializerTest.cs +++ b/Linguini.Serialization.Test/AttributeSerializerTest.cs @@ -14,13 +14,12 @@ public class AttributeSerializerTest [Parallelizable] public void TestAttributeSerializer() { - Attribute expected = new Attribute("desc", new PatternBuilder("description")); string attributeJson = @" { ""type"": ""Attribute"", ""id"": { ""type"": ""Identifier"", - ""value"": ""desc"" + ""name"": ""desc"" }, ""value"": { ""type"": ""Pattern"", @@ -32,6 +31,7 @@ public void TestAttributeSerializer() ] } }"; + Attribute expected = new Attribute("desc", new PatternBuilder("description")); Attribute? actual = JsonSerializer.Deserialize(attributeJson, TestUtil.Options); Assert.That(actual, Is.EqualTo(expected)); } diff --git a/Linguini.Serialization.Test/CallArgumentsSerializerTest.cs b/Linguini.Serialization.Test/CallArgumentsSerializerTest.cs new file mode 100644 index 0000000..da23848 --- /dev/null +++ b/Linguini.Serialization.Test/CallArgumentsSerializerTest.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using Linguini.Serialization.Converters; +using Linguini.Syntax.Ast; +using NUnit.Framework; + +namespace Linguini.Serialization.Test; + +[TestFixture] +public class CallArgumentsSerializerTest +{ + [Test] + [TestOf(typeof(CallArgumentsSerializer))] + [Parallelizable] + public void TestCallSerializer() + { + string callJson = @" +{ + ""type"": ""CallArguments"", + ""positional"": [ + { + ""type"": ""MessageReference"", + ""id"": { + ""type"": ""Identifier"", + ""name"": ""x"" + }, + ""attribute"": null + } + ], + ""named"": [ + { + ""type"": ""NamedArgument"", + ""name"": { + ""type"": ""Identifier"", + ""name"": ""y"" + }, + ""value"": { + ""value"": 3, + ""type"": ""NumberLiteral"" + } + } + ] +}"; + var expected = new CallArgumentsBuilder() + .AddPositionalArg(InlineExpressionBuilder.CreateMessageReference("x")) + .AddNamedArg("y", 3) + .Build(); + CallArguments? actual = JsonSerializer.Deserialize(callJson, TestUtil.Options); + Assert.That(actual, Is.EqualTo(expected)); + } +} \ No newline at end of file diff --git a/Linguini.Serialization/Converters/CallArgumentsSerializer.cs b/Linguini.Serialization/Converters/CallArgumentsSerializer.cs index 74c0abb..da85f73 100644 --- a/Linguini.Serialization/Converters/CallArgumentsSerializer.cs +++ b/Linguini.Serialization/Converters/CallArgumentsSerializer.cs @@ -13,7 +13,13 @@ 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) diff --git a/Linguini.Serialization/Converters/IdentifierSerializer.cs b/Linguini.Serialization/Converters/IdentifierSerializer.cs index 719a136..1a62a3f 100644 --- a/Linguini.Serialization/Converters/IdentifierSerializer.cs +++ b/Linguini.Serialization/Converters/IdentifierSerializer.cs @@ -18,10 +18,8 @@ public override Identifier Read(ref Utf8JsonReader reader, Type typeToConvert, J string? id = null; - while (true) + while (reader.Read()) { - reader.Read(); - if (reader.TokenType == JsonTokenType.EndObject) { break; @@ -44,7 +42,7 @@ public override Identifier Read(ref Utf8JsonReader reader, Type typeToConvert, J } break; - case "value": + case "name": id = reader.GetString(); break; default: @@ -74,7 +72,7 @@ public override void Write(Utf8JsonWriter writer, Identifier identifier, JsonSer public static bool TryGetIdentifier(JsonElement el, JsonSerializerOptions options, [MaybeNullWhen(false)] out Identifier ident) { - if (!el.TryGetProperty("value", out var valueElement) || valueElement.ValueKind != JsonValueKind.String) + if (!el.TryGetProperty("name", out var valueElement) || valueElement.ValueKind != JsonValueKind.String) { ident = null; return false; diff --git a/Linguini.Serialization/Converters/MessageReferenceSerializer.cs b/Linguini.Serialization/Converters/MessageReferenceSerializer.cs index 8e6a284..951ad86 100644 --- a/Linguini.Serialization/Converters/MessageReferenceSerializer.cs +++ b/Linguini.Serialization/Converters/MessageReferenceSerializer.cs @@ -36,7 +36,7 @@ public static MessageReference ProcessMessageReference(JsonElement el, && IdentifierSerializer.TryGetIdentifier(getProp, options, out var ident)) { Identifier? attr = null; - if (el.TryGetProperty("attribute", out var prop)) + if (el.TryGetProperty("attribute", out var prop) && prop.ValueKind != JsonValueKind.Null) { IdentifierSerializer.TryGetIdentifier(prop, options, out attr); } diff --git a/Linguini.Syntax/Ast/Base.cs b/Linguini.Syntax/Ast/Base.cs index 87b260e..47231e7 100644 --- a/Linguini.Syntax/Ast/Base.cs +++ b/Linguini.Syntax/Ast/Base.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Text; // ReSharper disable ClassNeverInstantiated.Global @@ -165,6 +166,12 @@ public PatternBuilder AddFunctionReference(string functionName, CallArguments fu 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))); @@ -254,6 +261,42 @@ public interface IEntry public interface IInlineExpression : IExpression { + public static InlineExpressionComparer Comparer = new(); + } + + 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 diff --git a/Linguini.Syntax/Ast/Expression.cs b/Linguini.Syntax/Ast/Expression.cs index 3ee3cee..ade3167 100644 --- a/Linguini.Syntax/Ast/Expression.cs +++ b/Linguini.Syntax/Ast/Expression.cs @@ -1,11 +1,13 @@ 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 readonly ReadOnlyMemory Value; @@ -14,16 +16,6 @@ public TextLiteral(ReadOnlyMemory value) Value = value; } - public bool Equals(IPatternElement? other) - { - if (other is TextLiteral textLiteralOther) - { - return Value.Span.SequenceEqual(textLiteralOther.Value.Span); - } - - return false; - } - public TextLiteral(string id) { Value = id.AsMemory(); @@ -33,9 +25,29 @@ 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 readonly ReadOnlyMemory Value; @@ -43,7 +55,7 @@ public NumberLiteral(ReadOnlyMemory value) { Value = value; } - + public NumberLiteral(float num) { Value = num.ToString(CultureInfo.InvariantCulture).AsMemory(); @@ -55,14 +67,39 @@ public NumberLiteral(double num) } + public bool Equals(IInlineExpression other) + { + throw new NotImplementedException(); + } + 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 readonly Identifier Id; public readonly CallArguments Arguments; @@ -78,9 +115,29 @@ 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 readonly Identifier Id; public readonly Identifier? Attribute; @@ -90,7 +147,7 @@ public MessageReference(Identifier id, Identifier? attribute) Id = id; Attribute = attribute; } - + public MessageReference(string id, string? attribute = null) { Id = new Identifier(id); @@ -99,9 +156,29 @@ public MessageReference(string id, string? 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) && 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() != this.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 readonly Identifier Id; public readonly Identifier? Attribute; @@ -113,7 +190,7 @@ 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); @@ -127,9 +204,44 @@ public DynamicReference(string id, string? attribute = null, CallArguments? argu 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) && 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 readonly Identifier Id; public readonly Identifier? Attribute; @@ -141,7 +253,7 @@ 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); @@ -155,9 +267,43 @@ public TermReference(string id, string? attribute = null, CallArguments? argumen 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) && 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 readonly Identifier Id; @@ -165,9 +311,34 @@ 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 Id.Equals(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 readonly IExpression Expression; @@ -182,11 +353,57 @@ public bool Equals(IPatternElement? other) { return Expression == otherPlaceable.Expression; } + return false; } + + public bool Equals(Placeable? other) + { + 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 readonly List PositionalArgs; public readonly List NamedArgs; @@ -196,9 +413,25 @@ public CallArguments(List positionalArgs, List 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 struct NamedArgument : IEquatable { public readonly Identifier Name; public readonly IInlineExpression Value; @@ -208,9 +441,152 @@ 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 SelectExpression : IExpression + 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 InlineExpressionBuilder + { + 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 builder) + { + return new InlineExpressionBuilder(builder); + } + + 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; @@ -220,6 +596,27 @@ 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 @@ -273,13 +670,13 @@ public enum VariantType : byte } - public class Variant + public class Variant : IEquatable { public readonly VariantType Type; public readonly ReadOnlyMemory Key; public Pattern Value => InternalValue; public bool IsDefault => InternalDefault; - + protected internal bool InternalDefault; protected internal Pattern InternalValue; @@ -290,7 +687,7 @@ public Variant(VariantType type, ReadOnlyMemory key) InternalValue = new Pattern(); InternalDefault = false; } - + public Variant(VariantType type, ReadOnlyMemory key, Pattern pattern, bool isDefault = false) { Type = type; @@ -298,7 +695,7 @@ public Variant(VariantType type, ReadOnlyMemory key, Pattern pattern, bool InternalValue = pattern; InternalDefault = isDefault; } - + public Variant(string key, PatternBuilder builder) { Type = VariantType.Identifier; @@ -306,7 +703,7 @@ public Variant(string key, PatternBuilder builder) InternalValue = builder.Build(); InternalDefault = false; } - + public Variant(float key, PatternBuilder builder) { Type = VariantType.NumberLiteral; @@ -314,5 +711,26 @@ public Variant(float key, PatternBuilder builder) 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.Equals(other.Key) && 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 From b557ce4cabd6a95292d564565c46956424b215a0 Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Fri, 19 Jan 2024 03:39:17 +0100 Subject: [PATCH 08/19] Remove TODO --- Linguini.Syntax/Ast/Expression.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Linguini.Syntax/Ast/Expression.cs b/Linguini.Syntax/Ast/Expression.cs index ade3167..4e2f485 100644 --- a/Linguini.Syntax/Ast/Expression.cs +++ b/Linguini.Syntax/Ast/Expression.cs @@ -66,12 +66,6 @@ public NumberLiteral(double num) Value = num.ToString(CultureInfo.InvariantCulture).AsMemory(); } - - public bool Equals(IInlineExpression other) - { - throw new NotImplementedException(); - } - public override string ToString() { return Value.Span.ToString(); From a511ed8503c0c7f087de51f384c4f92391edc5b1 Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Fri, 19 Jan 2024 03:39:29 +0100 Subject: [PATCH 09/19] Fix WriteHelper errors. --- Linguini.Bundle/Resolver/WriterHelpers.cs | 55 +++++++---------------- 1 file changed, 17 insertions(+), 38 deletions(-) diff --git a/Linguini.Bundle/Resolver/WriterHelpers.cs b/Linguini.Bundle/Resolver/WriterHelpers.cs index d9c38fe..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; @@ -310,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 From e140a1c5940bba2200c8663460acf7514e614502 Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Fri, 19 Jan 2024 09:50:18 +0100 Subject: [PATCH 10/19] Minor changes --- Linguini.Bundle/Errors/FluentError.cs | 13 +++++----- .../AttributeSerializerTest.cs | 22 ++++++++++++++-- .../CallArgumentsSerializerTest.cs | 23 ++++++++++++++++- Linguini.Syntax/Ast/Base.cs | 25 ++++++++++++++++--- 4 files changed, 70 insertions(+), 13 deletions(-) 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.Serialization.Test/AttributeSerializerTest.cs b/Linguini.Serialization.Test/AttributeSerializerTest.cs index 00c178a..f0f41ee 100644 --- a/Linguini.Serialization.Test/AttributeSerializerTest.cs +++ b/Linguini.Serialization.Test/AttributeSerializerTest.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Text; +using System.Text.Json; using Linguini.Serialization.Converters; using Linguini.Syntax.Ast; using NUnit.Framework; @@ -25,7 +26,7 @@ public void TestAttributeSerializer() ""type"": ""Pattern"", ""elements"": [ { - ""type"": ""TextLiteral"", + ""type"": ""TextElement"", ""value"": ""description"" } ] @@ -35,4 +36,21 @@ public void TestAttributeSerializer() Attribute? actual = JsonSerializer.Deserialize(attributeJson, TestUtil.Options); Assert.That(actual, Is.EqualTo(expected)); } + + [Test] + [TestOf(typeof(AttributeSerializer))] + [Parallelizable] + public void RoundTrip() + { + Attribute start = new Attribute("desc", new PatternBuilder("d1")); + var text = ""; + using (var memoryStream = new MemoryStream()) + { + JsonSerializer.Serialize(memoryStream, start, TestUtil.Options); + text = Encoding.UTF8.GetString(memoryStream.ToArray()); + } + + Attribute deserialized = JsonSerializer.Deserialize(text, TestUtil.Options)!; + Assert.That(deserialized, Is.EqualTo(start)); + } } \ No newline at end of file diff --git a/Linguini.Serialization.Test/CallArgumentsSerializerTest.cs b/Linguini.Serialization.Test/CallArgumentsSerializerTest.cs index da23848..7f305eb 100644 --- a/Linguini.Serialization.Test/CallArgumentsSerializerTest.cs +++ b/Linguini.Serialization.Test/CallArgumentsSerializerTest.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Text; +using System.Text.Json; using Linguini.Serialization.Converters; using Linguini.Syntax.Ast; using NUnit.Framework; @@ -47,4 +48,24 @@ public void TestCallSerializer() CallArguments? actual = JsonSerializer.Deserialize(callJson, TestUtil.Options); Assert.That(actual, Is.EqualTo(expected)); } + + [Test] + [TestOf(typeof(CallArgumentsSerializer))] + [Parallelizable] + public void RoundTrip() + { + var start = new CallArgumentsBuilder() + .AddNamedArg("x", 3) + .AddPositionalArg(InlineExpressionBuilder.CreateTermReference("x", "y")) + .Build(); + var text = ""; + using (var memoryStream = new MemoryStream()) + { + JsonSerializer.Serialize(memoryStream, start, TestUtil.Options); + text = Encoding.UTF8.GetString(memoryStream.ToArray()); + } + + CallArguments deserialized = JsonSerializer.Deserialize(text, TestUtil.Options); + Assert.That(deserialized, Is.EqualTo(start)); + } } \ No newline at end of file diff --git a/Linguini.Syntax/Ast/Base.cs b/Linguini.Syntax/Ast/Base.cs index 47231e7..35da7f1 100644 --- a/Linguini.Syntax/Ast/Base.cs +++ b/Linguini.Syntax/Ast/Base.cs @@ -212,7 +212,26 @@ public Pattern Build() 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) { @@ -233,12 +252,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); } } @@ -261,7 +280,7 @@ public interface IEntry public interface IInlineExpression : IExpression { - public static InlineExpressionComparer Comparer = new(); + public static readonly InlineExpressionComparer Comparer = new(); } public class InlineExpressionComparer : IEqualityComparer From cbc3671a1743e3a84d2daf4501b6f7b65cc7941d Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Sat, 20 Jan 2024 20:40:20 +0100 Subject: [PATCH 11/19] Fix tests --- Linguini.Serialization.Test/AttributeSerializerTest.cs | 2 +- .../CallArgumentsSerializerTest.cs | 2 +- .../Converters/ResourceSerializer.cs | 10 ++++++++-- Linguini.Syntax/Ast/Expression.cs | 3 ++- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Linguini.Serialization.Test/AttributeSerializerTest.cs b/Linguini.Serialization.Test/AttributeSerializerTest.cs index f0f41ee..2e0530d 100644 --- a/Linguini.Serialization.Test/AttributeSerializerTest.cs +++ b/Linguini.Serialization.Test/AttributeSerializerTest.cs @@ -26,7 +26,7 @@ public void TestAttributeSerializer() ""type"": ""Pattern"", ""elements"": [ { - ""type"": ""TextElement"", + ""type"": ""StringLiteral"", ""value"": ""description"" } ] diff --git a/Linguini.Serialization.Test/CallArgumentsSerializerTest.cs b/Linguini.Serialization.Test/CallArgumentsSerializerTest.cs index 7f305eb..211173f 100644 --- a/Linguini.Serialization.Test/CallArgumentsSerializerTest.cs +++ b/Linguini.Serialization.Test/CallArgumentsSerializerTest.cs @@ -35,7 +35,7 @@ public void TestCallSerializer() ""name"": ""y"" }, ""value"": { - ""value"": 3, + ""value"": ""3"", ""type"": ""NumberLiteral"" } } diff --git a/Linguini.Serialization/Converters/ResourceSerializer.cs b/Linguini.Serialization/Converters/ResourceSerializer.cs index 77a4953..58459b0 100644 --- a/Linguini.Serialization/Converters/ResourceSerializer.cs +++ b/Linguini.Serialization/Converters/ResourceSerializer.cs @@ -99,7 +99,13 @@ public static TextLiteral ProcessTextLiteral(JsonElement el, JsonSerializerOptio public static NumberLiteral ProcessNumberLiteral(JsonElement el, JsonSerializerOptions options) { - return new(el.GetProperty("value").GetDouble()); + if (el.TryGetProperty("value", out var v) && v.ValueKind == JsonValueKind.String && + !"".Equals(v.GetString())) + { + return new NumberLiteral(v.GetString().AsMemory()); + } + + throw new JsonException("Expected value to be a valid number"); } public static IExpression ReadExpression(JsonElement el, JsonSerializerOptions options) @@ -112,7 +118,7 @@ public static IExpression ReadExpression(JsonElement el, JsonSerializerOptions o "NumberLiteral" => ProcessNumberLiteral(el, options), "Placeable" => PlaceableSerializer.ProcessPlaceable(el, options), "TermReference" => TermReferenceSerializer.ProcessTermReference(el, options), - "TextLiteral" => ProcessTextLiteral(el, options), + "StringLiteral" or "TextElement" or "TextLiteral" => ProcessTextLiteral(el, options), "VariableReference" => VariableReferenceSerializer.ProcessVariableReference(el, options), "SelectExpression" => SelectExpressionSerializer.ProcessSelectExpression(el, options), _ => throw new JsonException("Unexpected value") diff --git a/Linguini.Syntax/Ast/Expression.cs b/Linguini.Syntax/Ast/Expression.cs index 4e2f485..7c754e5 100644 --- a/Linguini.Syntax/Ast/Expression.cs +++ b/Linguini.Syntax/Ast/Expression.cs @@ -280,7 +280,8 @@ public bool Equals(TermReference? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return Id.Equals(other.Id) && Equals(Attribute, other.Attribute) && Nullable.Equals(Arguments, other.Arguments); + return Id.Equals(other.Id) && Identifier.Comparator.Equals(Attribute, other.Attribute) && + Nullable.Equals(Arguments, other.Arguments); } public override bool Equals(object? obj) From d9f27577731f488c38f85fa2bda73ed38bac8829 Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Sun, 21 Jan 2024 16:08:36 +0100 Subject: [PATCH 12/19] WIP --- .../AttributeSerializerTest.cs | 2 +- .../CallArgumentsSerializerTest.cs | 2 +- .../DynamicReferenceSerializerTest.cs | 84 +++++++++++++++++++ Linguini.Syntax/Ast/Expression.cs | 9 +- 4 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 Linguini.Serialization.Test/DynamicReferenceSerializerTest.cs diff --git a/Linguini.Serialization.Test/AttributeSerializerTest.cs b/Linguini.Serialization.Test/AttributeSerializerTest.cs index 2e0530d..a19774b 100644 --- a/Linguini.Serialization.Test/AttributeSerializerTest.cs +++ b/Linguini.Serialization.Test/AttributeSerializerTest.cs @@ -40,7 +40,7 @@ public void TestAttributeSerializer() [Test] [TestOf(typeof(AttributeSerializer))] [Parallelizable] - public void RoundTrip() + public void Serde() { Attribute start = new Attribute("desc", new PatternBuilder("d1")); var text = ""; diff --git a/Linguini.Serialization.Test/CallArgumentsSerializerTest.cs b/Linguini.Serialization.Test/CallArgumentsSerializerTest.cs index 211173f..40441b6 100644 --- a/Linguini.Serialization.Test/CallArgumentsSerializerTest.cs +++ b/Linguini.Serialization.Test/CallArgumentsSerializerTest.cs @@ -52,7 +52,7 @@ public void TestCallSerializer() [Test] [TestOf(typeof(CallArgumentsSerializer))] [Parallelizable] - public void RoundTrip() + public void Serde() { var start = new CallArgumentsBuilder() .AddNamedArg("x", 3) diff --git a/Linguini.Serialization.Test/DynamicReferenceSerializerTest.cs b/Linguini.Serialization.Test/DynamicReferenceSerializerTest.cs new file mode 100644 index 0000000..ad8c191 --- /dev/null +++ b/Linguini.Serialization.Test/DynamicReferenceSerializerTest.cs @@ -0,0 +1,84 @@ +using System.Text; +using System.Text.Json; +using Linguini.Serialization.Converters; +using Linguini.Syntax.Ast; +using NUnit.Framework; + +namespace Linguini.Serialization.Test; + +[TestFixture] +public class DynamicReferenceSerializerTest +{ + [Test] + [TestOf(typeof(DynamicReferenceSerializer))] + [Parallelizable] + public void TestDynamicReference() + { + string dynamicReference = @" +{ + ""type"": ""DynamicReference"", + ""id"": { + ""type"": ""Identifier"", + ""name"": ""dyn"" + }, + ""attribute"": { + ""type"": ""Identifier"", + ""name"": ""attr"" + }, + ""arguments"": { + ""type"": ""CallArguments"", + ""positional"": [ + { + ""type"": ""MessageReference"", + ""id"": { + ""type"": ""Identifier"", + ""name"": ""x"" + }, + ""attribute"": null + } + ], + ""named"": [ + { + ""type"": ""NamedArgument"", + ""name"": { + ""type"": ""Identifier"", + ""name"": ""y"" + }, + ""value"": { + ""value"": ""3"", + ""type"": ""NumberLiteral"" + } + } + ] + } +}"; + var callArgs = new CallArgumentsBuilder() + .AddPositionalArg(InlineExpressionBuilder.CreateMessageReference("x")) + .AddNamedArg("y", 3); + var expected = new DynamicReference("dyn", "attr", callArgs); + DynamicReference? actual = JsonSerializer.Deserialize(dynamicReference, TestUtil.Options); + Assert.That(actual, Is.EqualTo(expected)); + } + + [Test] + [TestOf(typeof(DynamicReferenceSerializer))] + [Parallelizable] + public void Serde() + { + var callArgs = new CallArgumentsBuilder() + .AddPositionalArg(InlineExpressionBuilder.CreateMessageReference("x")) + .AddNamedArg("y", 3) + .AddPositionalArg("z") + .AddNamedArg("om", InlineExpressionBuilder.CreateMessageReference("a", "z")); + var start = new DynamicReference("dyn", "attr", callArgs); + var text = ""; + using (var memoryStream = new MemoryStream()) + { + JsonSerializer.Serialize(memoryStream, start, TestUtil.Options); + text = Encoding.UTF8.GetString(memoryStream.ToArray()); + } + + DynamicReference deserialized = JsonSerializer.Deserialize(text, TestUtil.Options)!; + Assert.That(deserialized, Is.EqualTo(start)); + } +} \ No newline at end of file diff --git a/Linguini.Syntax/Ast/Expression.cs b/Linguini.Syntax/Ast/Expression.cs index 7c754e5..3b25f2f 100644 --- a/Linguini.Syntax/Ast/Expression.cs +++ b/Linguini.Syntax/Ast/Expression.cs @@ -554,9 +554,14 @@ public static InlineExpressionBuilder CreateNumber(float numberLiteral) return new InlineExpressionBuilder(new NumberLiteral(numberLiteral)); } - public static InlineExpressionBuilder CreatePlaceable(Placeable builder) + public static InlineExpressionBuilder CreatePlaceable(Placeable placeable) { - return new InlineExpressionBuilder(builder); + return new InlineExpressionBuilder(placeable); + } + + public static InlineExpressionBuilder CreatePlaceable(PlaceableBuilder placeable) + { + return new InlineExpressionBuilder(placeable.Build()); } public static InlineExpressionBuilder CreateTermReference(string id, string? attribute = null, From f58f3bf78ce1136dd6a785ef1182aa09227e9ddf Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Sun, 21 Jan 2024 21:48:21 +0100 Subject: [PATCH 13/19] Refactor and fix serializers for FunctionReference and DynamicReference. The serializers for FunctionReference and DynamicReference have been refactored and fixed. This includes updating conditions for properties and handling potential null values. Removed redundant converter in TestUtil following restructuring of serialization tests. --- .../AttributeSerializerTest.cs | 56 ------------ .../CallArgumentsSerializerTest.cs | 71 --------------- .../DynamicReferenceSerializerTest.cs | 84 ------------------ .../SerializeAndDeserializeTest.cs | 86 +++++++++++++++++++ Linguini.Serialization.Test/TestUtil.cs | 35 ++------ .../Converters/DynamicReferenceSerializer.cs | 10 +-- .../Converters/FunctionReferenceSerializer.cs | 15 ++-- .../Parser/LinguiniTestDetailedErrors.cs | 2 - Linguini.Syntax/Ast/Expression.cs | 16 +--- PluralRules.Test/Cldr/CldrParserTest.cs | 4 +- 10 files changed, 108 insertions(+), 271 deletions(-) delete mode 100644 Linguini.Serialization.Test/AttributeSerializerTest.cs delete mode 100644 Linguini.Serialization.Test/CallArgumentsSerializerTest.cs delete mode 100644 Linguini.Serialization.Test/DynamicReferenceSerializerTest.cs create mode 100644 Linguini.Serialization.Test/SerializeAndDeserializeTest.cs diff --git a/Linguini.Serialization.Test/AttributeSerializerTest.cs b/Linguini.Serialization.Test/AttributeSerializerTest.cs deleted file mode 100644 index a19774b..0000000 --- a/Linguini.Serialization.Test/AttributeSerializerTest.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Text; -using System.Text.Json; -using Linguini.Serialization.Converters; -using Linguini.Syntax.Ast; -using NUnit.Framework; -using Attribute = Linguini.Syntax.Ast.Attribute; - -namespace Linguini.Serialization.Test; - -[TestFixture] -public class AttributeSerializerTest -{ - [Test] - [TestOf(typeof(AttributeSerializer))] - [Parallelizable] - public void TestAttributeSerializer() - { - string attributeJson = @" -{ - ""type"": ""Attribute"", - ""id"": { - ""type"": ""Identifier"", - ""name"": ""desc"" - }, - ""value"": { - ""type"": ""Pattern"", - ""elements"": [ - { - ""type"": ""StringLiteral"", - ""value"": ""description"" - } - ] - } -}"; - Attribute expected = new Attribute("desc", new PatternBuilder("description")); - Attribute? actual = JsonSerializer.Deserialize(attributeJson, TestUtil.Options); - Assert.That(actual, Is.EqualTo(expected)); - } - - [Test] - [TestOf(typeof(AttributeSerializer))] - [Parallelizable] - public void Serde() - { - Attribute start = new Attribute("desc", new PatternBuilder("d1")); - var text = ""; - using (var memoryStream = new MemoryStream()) - { - JsonSerializer.Serialize(memoryStream, start, TestUtil.Options); - text = Encoding.UTF8.GetString(memoryStream.ToArray()); - } - - Attribute deserialized = JsonSerializer.Deserialize(text, TestUtil.Options)!; - Assert.That(deserialized, Is.EqualTo(start)); - } -} \ No newline at end of file diff --git a/Linguini.Serialization.Test/CallArgumentsSerializerTest.cs b/Linguini.Serialization.Test/CallArgumentsSerializerTest.cs deleted file mode 100644 index 40441b6..0000000 --- a/Linguini.Serialization.Test/CallArgumentsSerializerTest.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Text; -using System.Text.Json; -using Linguini.Serialization.Converters; -using Linguini.Syntax.Ast; -using NUnit.Framework; - -namespace Linguini.Serialization.Test; - -[TestFixture] -public class CallArgumentsSerializerTest -{ - [Test] - [TestOf(typeof(CallArgumentsSerializer))] - [Parallelizable] - public void TestCallSerializer() - { - string callJson = @" -{ - ""type"": ""CallArguments"", - ""positional"": [ - { - ""type"": ""MessageReference"", - ""id"": { - ""type"": ""Identifier"", - ""name"": ""x"" - }, - ""attribute"": null - } - ], - ""named"": [ - { - ""type"": ""NamedArgument"", - ""name"": { - ""type"": ""Identifier"", - ""name"": ""y"" - }, - ""value"": { - ""value"": ""3"", - ""type"": ""NumberLiteral"" - } - } - ] -}"; - var expected = new CallArgumentsBuilder() - .AddPositionalArg(InlineExpressionBuilder.CreateMessageReference("x")) - .AddNamedArg("y", 3) - .Build(); - CallArguments? actual = JsonSerializer.Deserialize(callJson, TestUtil.Options); - Assert.That(actual, Is.EqualTo(expected)); - } - - [Test] - [TestOf(typeof(CallArgumentsSerializer))] - [Parallelizable] - public void Serde() - { - var start = new CallArgumentsBuilder() - .AddNamedArg("x", 3) - .AddPositionalArg(InlineExpressionBuilder.CreateTermReference("x", "y")) - .Build(); - var text = ""; - using (var memoryStream = new MemoryStream()) - { - JsonSerializer.Serialize(memoryStream, start, TestUtil.Options); - text = Encoding.UTF8.GetString(memoryStream.ToArray()); - } - - CallArguments deserialized = JsonSerializer.Deserialize(text, TestUtil.Options); - Assert.That(deserialized, Is.EqualTo(start)); - } -} \ No newline at end of file diff --git a/Linguini.Serialization.Test/DynamicReferenceSerializerTest.cs b/Linguini.Serialization.Test/DynamicReferenceSerializerTest.cs deleted file mode 100644 index ad8c191..0000000 --- a/Linguini.Serialization.Test/DynamicReferenceSerializerTest.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Text; -using System.Text.Json; -using Linguini.Serialization.Converters; -using Linguini.Syntax.Ast; -using NUnit.Framework; - -namespace Linguini.Serialization.Test; - -[TestFixture] -public class DynamicReferenceSerializerTest -{ - [Test] - [TestOf(typeof(DynamicReferenceSerializer))] - [Parallelizable] - public void TestDynamicReference() - { - string dynamicReference = @" -{ - ""type"": ""DynamicReference"", - ""id"": { - ""type"": ""Identifier"", - ""name"": ""dyn"" - }, - ""attribute"": { - ""type"": ""Identifier"", - ""name"": ""attr"" - }, - ""arguments"": { - ""type"": ""CallArguments"", - ""positional"": [ - { - ""type"": ""MessageReference"", - ""id"": { - ""type"": ""Identifier"", - ""name"": ""x"" - }, - ""attribute"": null - } - ], - ""named"": [ - { - ""type"": ""NamedArgument"", - ""name"": { - ""type"": ""Identifier"", - ""name"": ""y"" - }, - ""value"": { - ""value"": ""3"", - ""type"": ""NumberLiteral"" - } - } - ] - } -}"; - var callArgs = new CallArgumentsBuilder() - .AddPositionalArg(InlineExpressionBuilder.CreateMessageReference("x")) - .AddNamedArg("y", 3); - var expected = new DynamicReference("dyn", "attr", callArgs); - DynamicReference? actual = JsonSerializer.Deserialize(dynamicReference, TestUtil.Options); - Assert.That(actual, Is.EqualTo(expected)); - } - - [Test] - [TestOf(typeof(DynamicReferenceSerializer))] - [Parallelizable] - public void Serde() - { - var callArgs = new CallArgumentsBuilder() - .AddPositionalArg(InlineExpressionBuilder.CreateMessageReference("x")) - .AddNamedArg("y", 3) - .AddPositionalArg("z") - .AddNamedArg("om", InlineExpressionBuilder.CreateMessageReference("a", "z")); - var start = new DynamicReference("dyn", "attr", callArgs); - var text = ""; - using (var memoryStream = new MemoryStream()) - { - JsonSerializer.Serialize(memoryStream, start, TestUtil.Options); - text = Encoding.UTF8.GetString(memoryStream.ToArray()); - } - - DynamicReference deserialized = JsonSerializer.Deserialize(text, TestUtil.Options)!; - Assert.That(deserialized, Is.EqualTo(start)); - } -} \ No newline at end of file diff --git a/Linguini.Serialization.Test/SerializeAndDeserializeTest.cs b/Linguini.Serialization.Test/SerializeAndDeserializeTest.cs new file mode 100644 index 0000000..7f18b67 --- /dev/null +++ b/Linguini.Serialization.Test/SerializeAndDeserializeTest.cs @@ -0,0 +1,86 @@ +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 SerializeDeserializeTest(object x) + { + SerializeAndDeserializeTest.SerializeDeserializeTest(x); + } + + public static IEnumerable AstExamples() + { + yield return new CallArgumentsBuilder() + .AddPositionalArg(InlineExpressionBuilder.CreateMessageReference("x")) + .AddNamedArg("y", 3) + .Build(); + yield return new Attribute("desc", new PatternBuilder("description")); + 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"); + } + + private static void SerializeDeserializeTest(T expected) + { + // Serialize the object to JSON string. + var jsonString = JsonSerializer.Serialize(expected, Options); + + // Deserialize the JSON string back into an object. + Debug.Assert(expected != null, nameof(expected) + " != null"); + var deserializedObject = JsonSerializer.Deserialize(jsonString, expected.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(expected)); + } + + 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 index 41455da..35c2f15 100644 --- a/Linguini.Serialization.Test/TestUtil.cs +++ b/Linguini.Serialization.Test/TestUtil.cs @@ -2,40 +2,15 @@ using System.Text.Json; using System.Text.Json.Serialization; using Linguini.Serialization.Converters; +using NUnit.Framework; namespace Linguini.Serialization.Test { public static class TestUtil { - public 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/Converters/DynamicReferenceSerializer.cs b/Linguini.Serialization/Converters/DynamicReferenceSerializer.cs index f54ca72..d7d4198 100644 --- a/Linguini.Serialization/Converters/DynamicReferenceSerializer.cs +++ b/Linguini.Serialization/Converters/DynamicReferenceSerializer.cs @@ -11,7 +11,8 @@ public class DynamicReferenceSerializer : JsonConverter 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) @@ -40,9 +41,8 @@ public override void Write(Utf8JsonWriter writer, DynamicReference dynRef, JsonS public static DynamicReference ProcessDynamicReference(JsonElement el, JsonSerializerOptions options) { - Identifier? identifier = null; - if (!el.TryGetProperty("id", out var jsonId) && - !IdentifierSerializer.TryGetIdentifier(jsonId, options, out identifier)) + 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"); } @@ -59,7 +59,7 @@ public static DynamicReference ProcessDynamicReference(JsonElement el, CallArgumentsSerializer.TryGetCallArguments(jsonArgs, options, out arguments); } - return new DynamicReference(identifier!, attribute, 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 639a671..5e7f7c7 100644 --- a/Linguini.Serialization/Converters/FunctionReferenceSerializer.cs +++ b/Linguini.Serialization/Converters/FunctionReferenceSerializer.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; using Linguini.Syntax.Ast; @@ -11,7 +10,8 @@ public class FunctionReferenceSerializer : JsonConverter 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) @@ -29,23 +29,22 @@ public override void Write(Utf8JsonWriter writer, FunctionReference value, JsonS public static FunctionReference ProcessFunctionReference(JsonElement el, JsonSerializerOptions options) { - Identifier? ident = null; - if (!el.TryGetProperty("id", out JsonElement value) && - !IdentifierSerializer.TryGetIdentifier(value, options, out ident)) + 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) + 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); + return new FunctionReference(ident, arguments.Value); } } } \ No newline at end of file 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/Expression.cs b/Linguini.Syntax/Ast/Expression.cs index 3b25f2f..3fe907c 100644 --- a/Linguini.Syntax/Ast/Expression.cs +++ b/Linguini.Syntax/Ast/Expression.cs @@ -155,14 +155,14 @@ public bool Equals(MessageReference? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return Id.Equals(other.Id) && Equals(Attribute, other.Attribute); + 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() != this.GetType()) return false; + if (obj.GetType() != GetType()) return false; return Equals((MessageReference)obj); } @@ -217,7 +217,7 @@ public bool Equals(DynamicReference? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return Id.Equals(other.Id) && Equals(Attribute, other.Attribute) && + return Id.Equals(other.Id) && Identifier.Comparator.Equals(Attribute, other.Attribute) && Nullable.Equals(Arguments, other.Arguments); } @@ -342,16 +342,6 @@ public Placeable(IExpression expression) Expression = expression; } - public bool Equals(IPatternElement? other) - { - if (other is Placeable otherPlaceable) - { - return Expression == otherPlaceable.Expression; - } - - return false; - } - public bool Equals(Placeable? other) { if (ReferenceEquals(null, other)) return false; 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(); From 0839296cbeebff338f63fbb80ba3cecc78fc9e55 Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Sun, 21 Jan 2024 23:01:06 +0100 Subject: [PATCH 14/19] Update serializers and tests in Linguini.Serialization Updated and implemented multiple serializers in Linguini.Serialization to handle different types of data, including patterns, placeables, and named arguments. Additional tests were added in SerializeAndDeserializeTest.cs to verify correct serialization. Significant changes were also made to PatternSerializer and NamedArgumentSerializer. --- .../SerializeAndDeserializeTest.cs | 3 + .../Converters/CallArgumentsSerializer.cs | 19 +------ .../Converters/MessageReferenceSerializer.cs | 12 ++-- .../Converters/NamedArgumentSerializer.cs | 29 +++++++++- .../Converters/PatternSerializer.cs | 13 ++++- .../Converters/PlaceableSerializer.cs | 2 +- .../Converters/ResourceSerializer.cs | 6 +- Linguini.Syntax/Ast/Base.cs | 55 +++++++++---------- Linguini.Syntax/Ast/Expression.cs | 7 ++- 9 files changed, 81 insertions(+), 65 deletions(-) diff --git a/Linguini.Serialization.Test/SerializeAndDeserializeTest.cs b/Linguini.Serialization.Test/SerializeAndDeserializeTest.cs index 7f18b67..c5786cb 100644 --- a/Linguini.Serialization.Test/SerializeAndDeserializeTest.cs +++ b/Linguini.Serialization.Test/SerializeAndDeserializeTest.cs @@ -37,6 +37,9 @@ public static IEnumerable AstExamples() .Build() ); yield return new Identifier("test"); + yield return new MessageReference("message", "attribute"); + yield return new PatternBuilder("text ").AddMessage("x").AddText(" more text").Build(); + } private static void SerializeDeserializeTest(T expected) diff --git a/Linguini.Serialization/Converters/CallArgumentsSerializer.cs b/Linguini.Serialization/Converters/CallArgumentsSerializer.cs index da85f73..b81d0d0 100644 --- a/Linguini.Serialization/Converters/CallArgumentsSerializer.cs +++ b/Linguini.Serialization/Converters/CallArgumentsSerializer.cs @@ -67,7 +67,7 @@ public static bool TryGetCallArguments(JsonElement el, var namedArgs = new List(); foreach (var arg in named.EnumerateArray()) { - if (TryReadNamedArguments(arg, options, out var namedArg)) + if (NamedArgumentSerializer.TryReadNamedArguments(arg, options, out var namedArg)) { namedArgs.Add(namedArg.Value); } @@ -76,22 +76,5 @@ public static bool TryGetCallArguments(JsonElement el, callArguments = new CallArguments(positionalArgs, namedArgs); return true; } - - - 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/MessageReferenceSerializer.cs b/Linguini.Serialization/Converters/MessageReferenceSerializer.cs index 951ad86..50aaed6 100644 --- a/Linguini.Serialization/Converters/MessageReferenceSerializer.cs +++ b/Linguini.Serialization/Converters/MessageReferenceSerializer.cs @@ -8,9 +8,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) @@ -32,7 +33,7 @@ public override void Write(Utf8JsonWriter writer, MessageReference msgRef, JsonS public static MessageReference ProcessMessageReference(JsonElement el, JsonSerializerOptions options) { - if (el.TryGetProperty("id", out var getProp) + if (el.TryGetProperty("id", out var getProp) && IdentifierSerializer.TryGetIdentifier(getProp, options, out var ident)) { Identifier? attr = null; @@ -43,9 +44,8 @@ public static MessageReference ProcessMessageReference(JsonElement el, return new MessageReference(ident, attr); } - + throw new JsonException("MessageReference requires `id` field"); - } } -} +} \ 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 98e790f..c90321e 100644 --- a/Linguini.Serialization/Converters/PatternSerializer.cs +++ b/Linguini.Serialization/Converters/PatternSerializer.cs @@ -68,10 +68,21 @@ private static void AddElements(ref Utf8JsonReader reader, PatternBuilder builde if (reader.TokenType != JsonTokenType.StartObject) continue; var el = JsonSerializer.Deserialize(ref reader, options); - builder.AddExpression(ResourceSerializer.ReadExpression(el, 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) { writer.WriteStartObject(); diff --git a/Linguini.Serialization/Converters/PlaceableSerializer.cs b/Linguini.Serialization/Converters/PlaceableSerializer.cs index 230b926..55d9f72 100644 --- a/Linguini.Serialization/Converters/PlaceableSerializer.cs +++ b/Linguini.Serialization/Converters/PlaceableSerializer.cs @@ -45,7 +45,7 @@ public static bool TryProcessPlaceable(JsonElement el, JsonSerializerOptions opt return true; } - public static IInlineExpression ProcessPlaceable(JsonElement el, JsonSerializerOptions options) + public static Placeable ProcessPlaceable(JsonElement el, JsonSerializerOptions options) { if (!TryProcessPlaceable(el, options, out var placeable)) throw new JsonException("Expected placeable!"); diff --git a/Linguini.Serialization/Converters/ResourceSerializer.cs b/Linguini.Serialization/Converters/ResourceSerializer.cs index 58459b0..3827b74 100644 --- a/Linguini.Serialization/Converters/ResourceSerializer.cs +++ b/Linguini.Serialization/Converters/ResourceSerializer.cs @@ -1,7 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Text.Json; -using System.Text.Json.Nodes; using System.Text.Json.Serialization; using Linguini.Syntax.Ast; @@ -110,7 +109,8 @@ public static NumberLiteral ProcessNumberLiteral(JsonElement el, public static IExpression ReadExpression(JsonElement el, JsonSerializerOptions options) { - IExpression x = el.GetProperty("type").GetString() switch + var type = el.GetProperty("type").GetString(); + IExpression x = type switch { "DynamicReference" => DynamicReferenceSerializer.ProcessDynamicReference(el, options), "FunctionReference" => FunctionReferenceSerializer.ProcessFunctionReference(el, options), @@ -121,7 +121,7 @@ public static IExpression ReadExpression(JsonElement el, JsonSerializerOptions o "StringLiteral" or "TextElement" or "TextLiteral" => ProcessTextLiteral(el, options), "VariableReference" => VariableReferenceSerializer.ProcessVariableReference(el, options), "SelectExpression" => SelectExpressionSerializer.ProcessSelectExpression(el, options), - _ => throw new JsonException("Unexpected value") + _ => throw new JsonException($"Unexpected type {type}") }; return x; } diff --git a/Linguini.Syntax/Ast/Base.cs b/Linguini.Syntax/Ast/Base.cs index 35da7f1..2d6ca66 100644 --- a/Linguini.Syntax/Ast/Base.cs +++ b/Linguini.Syntax/Ast/Base.cs @@ -1,14 +1,13 @@ using System; -using System.Collections; 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 : IEquatable { public readonly Identifier Id; @@ -19,7 +18,7 @@ public Attribute(Identifier id, Pattern value) Id = id; Value = value; } - + public Attribute(string id, PatternBuilder builder) { Id = new Identifier(id); @@ -31,7 +30,7 @@ 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()); @@ -62,6 +61,7 @@ public class Pattern : IEquatable { public readonly List Elements; + public Pattern(List elements) { Elements = elements; @@ -85,7 +85,10 @@ public bool Equals(Pattern? other) { var patternElement = Elements[index]; var otherPatternElement = other.Elements[index]; - if (!patternElement.Equals(otherPatternElement)) return false; + if (!IPatternElement.PatternComparer.Equals(patternElement, otherPatternElement)) + { + return false; + } } return true; @@ -111,7 +114,6 @@ public class PatternBuilder public PatternBuilder() { - } public PatternBuilder(string text) @@ -123,38 +125,39 @@ 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) + + public PatternBuilder AddDynamicReference(string id, string? attribute = null, + CallArguments? callArguments = null) { _patternElements.Add(new Placeable(new DynamicReference(id, attribute, callArguments))); return this; @@ -165,13 +168,13 @@ public PatternBuilder AddFunctionReference(string functionName, CallArguments fu _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))); @@ -183,20 +186,13 @@ public PatternBuilder AddSelectExpression(SelectExpressionBuilder selectExpressi _patternElements.Add(new Placeable(selectExpressionBuilder.Build())); return this; } - - public PatternBuilder AddExpression(IExpression expr) + + public PatternBuilder AddExpression(IPatternElement expr) { - if (expr is TextLiteral text) - { - _patternElements.Add(text); - } - else - { - _patternElements.Add(new Placeable(expr)); - } + _patternElements.Add(expr); return this; } - + public PatternBuilder AddPlaceable(Placeable placeable) { _patternElements.Add(placeable); @@ -230,8 +226,8 @@ public int GetHashCode(Identifier obj) } public readonly ReadOnlyMemory Name; - - public static readonly IdentifierComparator Comparator= new (); + + public static readonly IdentifierComparator Comparator = new(); public Identifier(ReadOnlyMemory name) { @@ -320,7 +316,6 @@ public int GetHashCode(IInlineExpression obj) public static class Base { - public static string Stringify(this Pattern? pattern) { var sb = new StringBuilder(); @@ -333,4 +328,4 @@ public static string Stringify(this Pattern? pattern) return sb.ToString(); } } -} +} \ No newline at end of file diff --git a/Linguini.Syntax/Ast/Expression.cs b/Linguini.Syntax/Ast/Expression.cs index 3fe907c..84d6b55 100644 --- a/Linguini.Syntax/Ast/Expression.cs +++ b/Linguini.Syntax/Ast/Expression.cs @@ -416,7 +416,7 @@ public override int GetHashCode() } } - public struct NamedArgument : IEquatable + public readonly struct NamedArgument : IEquatable { public readonly Identifier Name; public readonly IInlineExpression Value; @@ -548,7 +548,7 @@ public static InlineExpressionBuilder CreatePlaceable(Placeable placeable) { return new InlineExpressionBuilder(placeable); } - + public static InlineExpressionBuilder CreatePlaceable(PlaceableBuilder placeable) { return new InlineExpressionBuilder(placeable.Build()); @@ -706,7 +706,8 @@ public bool Equals(Variant? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return Type == other.Type && Key.Equals(other.Key) && InternalDefault == other.InternalDefault && + return Type == other.Type && Key.Span.SequenceEqual(other.Key.Span) && + InternalDefault == other.InternalDefault && InternalValue.Equals(other.InternalValue); } From 94ab26aad695d4ea1dfcfe3a56cb24ce1aa7eb33 Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Sun, 21 Jan 2024 23:07:25 +0100 Subject: [PATCH 15/19] Extend test cases and fix Identifier comparison A suite of new test cases have been added to SerializeAndDeserializeTest for comprehensive testing. Changes have been incorporated to the way of comparing Identifiers, using Identifier.Comparator for accurate comparisons. --- .../SerializeAndDeserializeTest.cs | 11 ++++++++++- Linguini.Syntax/Ast/Expression.cs | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Linguini.Serialization.Test/SerializeAndDeserializeTest.cs b/Linguini.Serialization.Test/SerializeAndDeserializeTest.cs index c5786cb..d9b8327 100644 --- a/Linguini.Serialization.Test/SerializeAndDeserializeTest.cs +++ b/Linguini.Serialization.Test/SerializeAndDeserializeTest.cs @@ -39,7 +39,16 @@ public static IEnumerable AstExamples() yield return new Identifier("test"); yield return new MessageReference("message", "attribute"); 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)); + yield return new AstComment(CommentLevel.Comment, new() { "test".AsMemory() }); + yield return new Junk("Test".AsMemory()); } private static void SerializeDeserializeTest(T expected) diff --git a/Linguini.Syntax/Ast/Expression.cs b/Linguini.Syntax/Ast/Expression.cs index 84d6b55..cf0024c 100644 --- a/Linguini.Syntax/Ast/Expression.cs +++ b/Linguini.Syntax/Ast/Expression.cs @@ -316,7 +316,7 @@ public bool Equals(VariableReference? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return Id.Equals(other.Id); + return Identifier.Comparator.Equals(Id , other.Id); } public override bool Equals(object? obj) From 378d71d7098de4d2fda78dc84d0d045636c640ca Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Sun, 21 Jan 2024 23:24:41 +0100 Subject: [PATCH 16/19] Implement AstComment equality logic and JSON deserialization Implemented equality logic for AstComment class in Entry.cs allowing for more complex comparisons. In addition to this, JSON deserialization has been implemented in CommentSerializer.cs allowing for reading AstComment from JSON. Now AstComment can be accurately compared and serialized from JSON format. --- .../Converters/CommentSerializer.cs | 58 ++++++++++++++++++- Linguini.Syntax/Ast/Entry.cs | 35 ++++++++++- 2 files changed, 90 insertions(+), 3 deletions(-) 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.Syntax/Ast/Entry.cs b/Linguini.Syntax/Ast/Entry.cs index 59b6594..a91c23a 100644 --- a/Linguini.Syntax/Ast/Entry.cs +++ b/Linguini.Syntax/Ast/Entry.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text; using Linguini.Syntax.Parser.Error; @@ -63,7 +64,7 @@ public string GetId() } } - public class AstComment : IEntry + public class AstComment : IEntry, IEquatable { public readonly CommentLevel CommentLevel; public readonly List> Content; @@ -94,6 +95,38 @@ 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 From f4965f9e099cf8ddb7754882efe7416780aff3a5 Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Mon, 22 Jan 2024 00:37:45 +0100 Subject: [PATCH 17/19] Refactor variant and number literal serialization The commit moves the methods for variant and number literal serializations from the ResourceSerializer class to the VariantSerializer class. The move aids in organization and code readability, as variant serialization is now better isolated and number literal serialization is directly utilized in variant serialization. Exception messages for invalid inputs have also been improved during the refactoing. --- .../Converters/ResourceSerializer.cs | 58 +++++++------------ .../Converters/SelectExpressionSerializer.cs | 4 +- .../Converters/VariantSerializer.cs | 48 +++++++++++++++ 3 files changed, 70 insertions(+), 40 deletions(-) diff --git a/Linguini.Serialization/Converters/ResourceSerializer.cs b/Linguini.Serialization/Converters/ResourceSerializer.cs index 3827b74..51d36ff 100644 --- a/Linguini.Serialization/Converters/ResourceSerializer.cs +++ b/Linguini.Serialization/Converters/ResourceSerializer.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; @@ -97,14 +98,27 @@ public static TextLiteral ProcessTextLiteral(JsonElement el, JsonSerializerOptio 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())) { - return new NumberLiteral(v.GetString().AsMemory()); + numberLiteral = new NumberLiteral(v.GetString().AsMemory()); + return true; } - throw new JsonException("Expected value to be a valid number"); + numberLiteral = null; + return false; } public static IExpression ReadExpression(JsonElement el, JsonSerializerOptions options) @@ -127,44 +141,12 @@ public static IExpression ReadExpression(JsonElement el, JsonSerializerOptions o } - 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) - && TryReadInlineExpression(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; - } - - var (x, id) = key switch - { - NumberLiteral numberLiteral => (VariantType.NumberLiteral, numberLiteral.Value), - TextLiteral identifier => (VariantType.Identifier, identifier.Value), - _ => throw new JsonException("Variant can only be number or identifier.") - }; - - return new Variant(x, id, pattern, isDefault); - } - } - - throw new NotImplementedException(); - } - + public static bool TryReadInlineExpression(JsonElement el, JsonSerializerOptions options, [MaybeNullWhen(false)] out IInlineExpression o) { - o = el.GetProperty("type").GetString() switch + var type = el.GetProperty("type").GetString(); + o = type switch { "DynamicReference" => DynamicReferenceSerializer.ProcessDynamicReference(el, options), "FunctionReference" => FunctionReferenceSerializer.ProcessFunctionReference(el, options), @@ -174,7 +156,7 @@ public static bool TryReadInlineExpression(JsonElement el, JsonSerializerOptions "TermReference" => TermReferenceSerializer.ProcessTermReference(el, options), "TextLiteral" => ProcessTextLiteral(el, options), "VariableReference" => VariableReferenceSerializer.ProcessVariableReference(el, options), - _ => throw new JsonException("Unexpected value") + _ => throw new JsonException($"Unexpected value {type}") }; return true; } diff --git a/Linguini.Serialization/Converters/SelectExpressionSerializer.cs b/Linguini.Serialization/Converters/SelectExpressionSerializer.cs index 7596fd6..6769017 100644 --- a/Linguini.Serialization/Converters/SelectExpressionSerializer.cs +++ b/Linguini.Serialization/Converters/SelectExpressionSerializer.cs @@ -11,7 +11,7 @@ public class SelectExpressionSerializer : JsonConverter 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) @@ -48,7 +48,7 @@ public static SelectExpression ProcessSelectExpression(JsonElement el, var variants = new List(); foreach (var variantEl in variantsProp.EnumerateArray()) { - variants.Add(ResourceSerializer.ReadVariant(variantEl, options)); + variants.Add(VariantSerializer.ReadVariant(variantEl, options)); } return new SelectExpression(selector, variants); diff --git a/Linguini.Serialization/Converters/VariantSerializer.cs b/Linguini.Serialization/Converters/VariantSerializer.cs index 8f3c28d..a13fc55 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; @@ -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 From ea5d022b3a3fe619733692f74c13e2e3c54dd721 Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Mon, 22 Jan 2024 00:56:38 +0100 Subject: [PATCH 18/19] Implement deserialization methods in Converters Previously, all deserialization methods in the Converters threw a 'NotImplementedException'. The new updates replace these methods with actual implementations, ensuring that when serialized objects are read back, they are correctly converted back to their original form. This change improves the overall functionality and efficiency of the serialization process. --- Linguini.Serialization/Converters/PatternSerializer.cs | 2 +- Linguini.Serialization/Converters/TermReferenceSerializer.cs | 2 +- .../Converters/VariableReferenceSerializer.cs | 2 +- Linguini.Serialization/Converters/VariantSerializer.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Linguini.Serialization/Converters/PatternSerializer.cs b/Linguini.Serialization/Converters/PatternSerializer.cs index c90321e..5ac6921 100644 --- a/Linguini.Serialization/Converters/PatternSerializer.cs +++ b/Linguini.Serialization/Converters/PatternSerializer.cs @@ -153,7 +153,7 @@ public static bool TryReadPattern(JsonElement jsonValue, JsonSerializerOptions o case "Placeable": if (PlaceableSerializer.TryProcessPlaceable(element, options, out var placeable)) { - patternElements.Add(new Placeable(placeable)); + patternElements.Add(placeable); } break; diff --git a/Linguini.Serialization/Converters/TermReferenceSerializer.cs b/Linguini.Serialization/Converters/TermReferenceSerializer.cs index ba956fe..0df8a00 100644 --- a/Linguini.Serialization/Converters/TermReferenceSerializer.cs +++ b/Linguini.Serialization/Converters/TermReferenceSerializer.cs @@ -10,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) diff --git a/Linguini.Serialization/Converters/VariableReferenceSerializer.cs b/Linguini.Serialization/Converters/VariableReferenceSerializer.cs index 51ac55a..df8cb16 100644 --- a/Linguini.Serialization/Converters/VariableReferenceSerializer.cs +++ b/Linguini.Serialization/Converters/VariableReferenceSerializer.cs @@ -11,7 +11,7 @@ public class VariableReferenceSerializer : JsonConverter 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, diff --git a/Linguini.Serialization/Converters/VariantSerializer.cs b/Linguini.Serialization/Converters/VariantSerializer.cs index a13fc55..f454086 100644 --- a/Linguini.Serialization/Converters/VariantSerializer.cs +++ b/Linguini.Serialization/Converters/VariantSerializer.cs @@ -11,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) From 2f2539352288e4e269a51b3c186a0846cccff7d4 Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Mon, 22 Jan 2024 22:36:03 +0100 Subject: [PATCH 19/19] Refactor serialization and deserialization test suite The serialization and deserialization test suite has been updated for clarity and performance. The process is now consolidated into a single method called `RoundTripTest()`. Additionally, equality implementations for `AstMessage` and `Junk` classes have been added, allowing for more reliable object comparison in tests. Implementations for processing `Junk` and `AstMessage` have been tweaked for correctness and better error reporting. --- .../SerializeAndDeserializeTest.cs | 45 +++++++++------- .../Converters/JunkSerializer.cs | 13 ++++- .../Converters/MessageReferenceSerializer.cs | 1 - .../Converters/MessageSerializer.cs | 35 +++++++++++- Linguini.Syntax/Ast/Base.cs | 20 +++++++ Linguini.Syntax/Ast/Entry.cs | 53 +++++++++++++++++-- 6 files changed, 141 insertions(+), 26 deletions(-) diff --git a/Linguini.Serialization.Test/SerializeAndDeserializeTest.cs b/Linguini.Serialization.Test/SerializeAndDeserializeTest.cs index d9b8327..ac91a89 100644 --- a/Linguini.Serialization.Test/SerializeAndDeserializeTest.cs +++ b/Linguini.Serialization.Test/SerializeAndDeserializeTest.cs @@ -16,18 +16,28 @@ public class SerializeAndDeserializeTest [Test] [TestCaseSource(nameof(AstExamples))] [Parallelizable] - public void SerializeDeserializeTest(object x) + public void RoundTripTest(object x) { - SerializeAndDeserializeTest.SerializeDeserializeTest(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 Attribute("desc", new PatternBuilder("description")); + yield return new AstComment(CommentLevel.Comment, new() { "test".AsMemory() }); yield return new DynamicReference("dyn", "attr", new CallArgumentsBuilder() .AddPositionalArg(InlineExpressionBuilder.CreateMessageReference("x")) .AddNamedArg("y", 3)); @@ -37,7 +47,20 @@ public static IEnumerable AstExamples() .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")) @@ -47,22 +70,6 @@ public static IEnumerable AstExamples() yield return new TermReference("x", "y"); yield return new VariableReference("x"); yield return new Variant(2.0f, new PatternBuilder(3)); - yield return new AstComment(CommentLevel.Comment, new() { "test".AsMemory() }); - yield return new Junk("Test".AsMemory()); - } - - private static void SerializeDeserializeTest(T expected) - { - // Serialize the object to JSON string. - var jsonString = JsonSerializer.Serialize(expected, Options); - - // Deserialize the JSON string back into an object. - Debug.Assert(expected != null, nameof(expected) + " != null"); - var deserializedObject = JsonSerializer.Deserialize(jsonString, expected.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(expected)); } private static readonly JsonSerializerOptions Options = new() 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 50aaed6..e7d6b69 100644 --- a/Linguini.Serialization/Converters/MessageReferenceSerializer.cs +++ b/Linguini.Serialization/Converters/MessageReferenceSerializer.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; using Linguini.Syntax.Ast; 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.Syntax/Ast/Base.cs b/Linguini.Syntax/Ast/Base.cs index 2d6ca66..644de8b 100644 --- a/Linguini.Syntax/Ast/Base.cs +++ b/Linguini.Syntax/Ast/Base.cs @@ -13,6 +13,26 @@ public class Attribute : IEquatable 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) { Id = id; diff --git a/Linguini.Syntax/Ast/Entry.cs b/Linguini.Syntax/Ast/Entry.cs index a91c23a..970787a 100644 --- a/Linguini.Syntax/Ast/Entry.cs +++ b/Linguini.Syntax/Ast/Entry.cs @@ -18,7 +18,7 @@ public Resource(List body, List errors) } } - public class AstMessage : IEntry + public class AstMessage : IEntry, IEquatable { public readonly Identifier Id; public readonly Pattern? Value; @@ -39,6 +39,28 @@ 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 @@ -129,7 +151,7 @@ public override int GetHashCode() } } - public class Junk : IEntry + public class Junk : IEntry, IEquatable { public readonly ReadOnlyMemory Content; @@ -137,12 +159,17 @@ public Junk() { Content = ReadOnlyMemory.Empty; } - + public Junk(ReadOnlyMemory content) { Content = content; } + public Junk(string content) + { + Content = content.AsMemory(); + } + public string AsStr() { return Content.Span.ToString(); @@ -152,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