From 9493d9d34f2468789f0e5526e1a71c47928b1d15 Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Wed, 3 Jan 2024 18:20:51 +0100 Subject: [PATCH 1/8] Fix execution --- Linguini.Bundle.Test/Linguini.Bundle.Test.csproj | 2 +- PluralRules.Test/PluralRules.Test.csproj | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Linguini.Bundle.Test/Linguini.Bundle.Test.csproj b/Linguini.Bundle.Test/Linguini.Bundle.Test.csproj index 2caf3f5..dbafcc4 100644 --- a/Linguini.Bundle.Test/Linguini.Bundle.Test.csproj +++ b/Linguini.Bundle.Test/Linguini.Bundle.Test.csproj @@ -11,7 +11,7 @@ - + diff --git a/PluralRules.Test/PluralRules.Test.csproj b/PluralRules.Test/PluralRules.Test.csproj index e8f2a57..5499de4 100644 --- a/PluralRules.Test/PluralRules.Test.csproj +++ b/PluralRules.Test/PluralRules.Test.csproj @@ -5,13 +5,13 @@ enable PluralRules.Test Library - netstandard2.1 + net6.0 - - - + + + From ebca66b6b9b0876a8d8491550f3930e34d5fbf9c Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Wed, 3 Jan 2024 16:38:22 +0100 Subject: [PATCH 2/8] Make FluentBundle abstract --- Linguini.Bundle.Test/Unit/BundleTests.cs | 8 +- Linguini.Bundle.Test/Yaml/YamlSuiteParser.cs | 10 +- Linguini.Bundle/Builder/FluentBundleOption.cs | 2 +- Linguini.Bundle/Builder/LinguiniBuilder.cs | 16 +- Linguini.Bundle/ConcurrentBundle.cs | 125 ++++++ Linguini.Bundle/FluentBundle.cs | 386 +++++++----------- Linguini.Bundle/InsertBehavior.cs | 9 - Linguini.Bundle/NonConcurrentBundle.cs | 122 ++++++ PluralRules.Test/Cldr/CldrParserTest.cs | 1 - 9 files changed, 420 insertions(+), 259 deletions(-) create mode 100644 Linguini.Bundle/ConcurrentBundle.cs delete mode 100644 Linguini.Bundle/InsertBehavior.cs create mode 100644 Linguini.Bundle/NonConcurrentBundle.cs diff --git a/Linguini.Bundle.Test/Unit/BundleTests.cs b/Linguini.Bundle.Test/Unit/BundleTests.cs index f8917b7..48f00d6 100644 --- a/Linguini.Bundle.Test/Unit/BundleTests.cs +++ b/Linguini.Bundle.Test/Unit/BundleTests.cs @@ -208,7 +208,7 @@ public void TestFuncAddBehavior() bundle.TryAddFunction("id", _idFunc); Assert.That(bundle.TryAddFunction("id", _zeroFunc), Is.False); - Assert.Throws(() => bundle.AddFunctionUnchecked("id", _zeroFunc)); + Assert.Throws(() => bundle.AddFunctionUnchecked("id", _zeroFunc)); } [Test] @@ -287,7 +287,7 @@ public void TestDynamicReference(string input) var (bundle, err) = LinguiniBuilder.Builder(useExperimental: true).Locale("en-US") .AddResource(input) .Build(); - Assert.That(err, Is.Empty); + Assert.That(err, Is.Null); var args = new Dictionary() { ["attacker"] = (FluentReference)"cat", @@ -317,7 +317,7 @@ public void TestMacrosFail() var (bundle, err) = LinguiniBuilder.Builder(useExperimental: true).Locale("en-US") .AddResource(Macros) .Build(); - Assert.That(err, Is.Empty); + Assert.That(err, Is.Null); var args = new Dictionary { ["style"] = (FluentString)"chicago", @@ -344,7 +344,7 @@ public void TestDynamicSelectors() .Locale("en-US") .AddResource(DynamicSelectors) .Build(); - Assert.That(err, Is.Empty); + Assert.That(err, Is.Null); var args = new Dictionary { ["object"] = (FluentReference)"creature-elf", diff --git a/Linguini.Bundle.Test/Yaml/YamlSuiteParser.cs b/Linguini.Bundle.Test/Yaml/YamlSuiteParser.cs index 4b141b5..03706a6 100644 --- a/Linguini.Bundle.Test/Yaml/YamlSuiteParser.cs +++ b/Linguini.Bundle.Test/Yaml/YamlSuiteParser.cs @@ -88,7 +88,10 @@ public void YamlTestSuiteMethod(ResolverTestSuite parsedTestSuite, LinguiniBuild foreach (var res in parsedTestSuite.Resources) { bundle.AddResource(res, out var err); - errors.AddRange(err); + if (err != null) + { + errors.AddRange(err); + } } if (parsedTestSuite.Bundle != null) @@ -147,7 +150,10 @@ public void YamlTestSuiteMethod(ResolverTestSuite parsedTestSuite, LinguiniBuild else { testBundle.AddResource(res, out var errs); - errors.AddRange(errs); + if (errs != null) + { + errors.AddRange(errs); + } } } } diff --git a/Linguini.Bundle/Builder/FluentBundleOption.cs b/Linguini.Bundle/Builder/FluentBundleOption.cs index c0aff74..03fa4cb 100644 --- a/Linguini.Bundle/Builder/FluentBundleOption.cs +++ b/Linguini.Bundle/Builder/FluentBundleOption.cs @@ -12,7 +12,7 @@ public class FluentBundleOption public bool UseIsolating { get; init; } = true; public byte MaxPlaceable { get; init; } = 100; - public IList Locales { get; init; } = new List(); + public List Locales { get; init; } = new List(); public IDictionary Functions { get; init; } = new Dictionary(); diff --git a/Linguini.Bundle/Builder/LinguiniBuilder.cs b/Linguini.Bundle/Builder/LinguiniBuilder.cs index 6613f5b..a326402 100644 --- a/Linguini.Bundle/Builder/LinguiniBuilder.cs +++ b/Linguini.Bundle/Builder/LinguiniBuilder.cs @@ -48,7 +48,7 @@ public interface IBuildStep : IStep { FluentBundle UncheckedBuild(); - (FluentBundle, List) Build(); + (FluentBundle, List?) Build(); } public interface IReadyStep : IBuildStep @@ -65,7 +65,6 @@ private class StepBuilder : IReadyStep, ILocaleStep, IResourceStep private CultureInfo _culture; private readonly List _locales = new(); private readonly List _resources = new(); - private readonly List _source = new(); private bool _useIsolating; private Func? _formatterFunc; private Func? _transformFunc; @@ -113,7 +112,7 @@ public FluentBundle UncheckedBuild() { var (bundle, errors) = Build(); - if (errors.Count > 0) + if (errors is { Count: > 0 }) { throw new LinguiniException(errors); } @@ -121,7 +120,7 @@ public FluentBundle UncheckedBuild() return bundle; } - public (FluentBundle, List) Build() + public (FluentBundle, List?) Build() { var concurrent = new FluentBundleOption { @@ -134,18 +133,23 @@ public FluentBundle UncheckedBuild() }; var bundle = FluentBundle.MakeUnchecked(concurrent); bundle.Culture = _culture; + List? errors = null; - var errors = new List(); if (_functions.Count > 0) { bundle.AddFunctions(_functions, out var funcErr); - errors.AddRange(funcErr); + if (funcErr != null) + { + errors ??= new List(); + errors.AddRange(funcErr); + } } foreach (var resource in _resources) { if (!bundle.AddResource(resource,out var resErr)) { + errors ??= new List(); errors.AddRange(resErr); } } diff --git a/Linguini.Bundle/ConcurrentBundle.cs b/Linguini.Bundle/ConcurrentBundle.cs new file mode 100644 index 0000000..a41b44c --- /dev/null +++ b/Linguini.Bundle/ConcurrentBundle.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Linguini.Bundle.Errors; +using Linguini.Bundle.Types; +using Linguini.Syntax.Ast; + +// ReSharper disable UnusedType.Global + +namespace Linguini.Bundle +{ + public sealed class ConcurrentBundle : FluentBundle + { + internal ConcurrentDictionary _funcList = new(); + private ConcurrentDictionary _terms = new(); + private ConcurrentDictionary _messages = new(); + + /// + protected override void AddMessageOverriding(AstMessage message) + { + _messages[message.GetId()] = message; + } + + /// + protected override void AddTermOverriding(AstTerm term) + { + _terms[term.GetId()] = term; + } + + /// + protected override bool TryAddTerm(AstTerm term, List? errors) + { + if (_terms.TryAdd(term.GetId(), term)) return true; + errors ??= new List(); + errors.Add(new OverrideFluentError(term.GetId(), EntryKind.Term)); + return false; + } + + /// + protected override bool TryAddMessage(AstMessage message, List? errors) + { + if (_messages.TryAdd(message.GetId(), message)) return true; + errors ??= new List(); + errors.Add(new OverrideFluentError(message.GetId(), EntryKind.Message)); + return false; + } + + + /// + public override bool TryAddFunction(string funcName, ExternalFunction fluentFunction) + { + return _funcList.TryAdd(funcName, fluentFunction); + } + + /// + public override void AddFunctionOverriding(string funcName, ExternalFunction fluentFunction) + { + _funcList[funcName] = fluentFunction; + } + + /// + public override void AddFunctionUnchecked(string funcName, ExternalFunction fluentFunction) + { + if (_funcList.TryAdd(funcName, fluentFunction)) return; + throw new ArgumentException($"Function with name {funcName} already exist"); + } + + /// + public override bool HasMessage(string identifier) + { + return _messages.ContainsKey(identifier); + } + + /// + public override bool TryGetAstMessage(string ident, [NotNullWhen(true)] out AstMessage? message) + { + return _messages.TryGetValue(ident, out message); + } + + /// + public override bool TryGetAstTerm(string ident, [NotNullWhen(true)] out AstTerm? term) + { + return _terms.TryGetValue(ident, out term); + } + + + /// + public override bool TryGetFunction(string funcName, [NotNullWhen(true)] out FluentFunction? function) + { + return _funcList.TryGetValue(funcName, out function); + } + + /// + public override IEnumerable GetMessageEnumerable() + { + return _messages.Keys.ToArray(); + } + + /// + public override IEnumerable GetFuncEnumerable() + { + return _funcList.Keys.ToArray(); + } + + /// + public override IEnumerable GetTermEnumerable() + { + return _terms.Keys.ToArray(); + } + + + /// + public override FluentBundle DeepClone() + { + return new ConcurrentBundle() + { + _funcList = new ConcurrentDictionary(_funcList), + _terms = new ConcurrentDictionary(_terms), + _messages = new ConcurrentDictionary(_messages), + }; + } + } +} \ No newline at end of file diff --git a/Linguini.Bundle/FluentBundle.cs b/Linguini.Bundle/FluentBundle.cs index cd10cd6..e499042 100644 --- a/Linguini.Bundle/FluentBundle.cs +++ b/Linguini.Bundle/FluentBundle.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.ComponentModel.Design; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; -using System.Runtime.CompilerServices; +using System.Linq; +using System.Xml.Schema; using Linguini.Bundle.Builder; using Linguini.Bundle.Errors; using Linguini.Bundle.Resolver; @@ -16,40 +16,40 @@ namespace Linguini.Bundle { - using FluentArgs = IDictionary; - - public class FluentBundle + public abstract class FluentBundle { - private IDictionary _funcList; - private IDictionary _terms; - private IDictionary _messages; - #region Properties /// /// of the bundle. Primary bundle locale /// - public CultureInfo Culture { get; internal set; } + public CultureInfo Culture { get; internal set; } = CultureInfo.CurrentCulture; + /// /// List of Locales. First element is primary bundle locale, others are fallback locales. /// - public List Locales { get; internal set; } + public List Locales { get; internal set; } = new(); + /// /// When formatting patterns, FluentBundle inserts Unicode Directionality Isolation Marks to indicate that the direction of a placeable may differ from the surrounding message. /// This is important for cases such as when a right-to-left user name is presented in the left-to-right message. /// - public bool UseIsolating { get; set; } + public bool UseIsolating { get; set; } = true; + /// /// Specifies a method that will be applied only on values extending . Useful for defining a special formatter for . /// public Func? TransformFunc { get; set; } + /// /// Specifies a method that will be applied only on values extending . Useful for defining a special formatter for . /// public Func? FormatterFunc { get; init; } + /// /// Limit of placeable within one , when fully expanded (all nested elements count towards it). Useful for preventing billion laughs attack. Defaults to 100. /// - public byte MaxPlaceable { get; private init; } + public byte MaxPlaceable { get; private init; } = 100; + /// /// Whether experimental features are enabled. /// @@ -60,98 +60,26 @@ public class FluentBundle /// term reference as parameters /// /// - public bool EnableExtensions { get; init; } - - #endregion - - #region Constructors - - private FluentBundle() - { - _terms = new Dictionary(); - _messages = new Dictionary(); - _funcList = new Dictionary(); - Culture = CultureInfo.CurrentCulture; - Locales = new List(); - UseIsolating = true; - MaxPlaceable = 100; - EnableExtensions = false; - } - - /// - /// Creates a new instance of FluentBundle with the specified options, possibly throwing exceptions if errors - /// are encountered. - /// - /// The options to configure the FluentBundle. - /// A new instance of FluentBundle with the specified options. - public static FluentBundle MakeUnchecked(FluentBundleOption option) - { - var bundle = ConstructBundle(option); - bundle.AddFunctions(option.Functions, out _); - return bundle; - } - - private static FluentBundle ConstructBundle(FluentBundleOption option) - { - var primaryLocale = option.Locales.Count > 0 - ? option.Locales[0] - : CultureInfo.CurrentCulture.Name; - var cultureInfo = new CultureInfo(primaryLocale, false); - var locales = new List { primaryLocale }; - IDictionary terms; - IDictionary messages; - IDictionary functions; - if (option.UseConcurrent) - { - terms = new ConcurrentDictionary(); - messages = new ConcurrentDictionary(); - functions = new ConcurrentDictionary(); - } - else - { - terms = new Dictionary(); - messages = new Dictionary(); - functions = new Dictionary(); - } - - return new FluentBundle - { - Culture = cultureInfo, - Locales = locales, - _terms = terms, - _messages = messages, - _funcList = functions, - TransformFunc = option.TransformFunc, - FormatterFunc = option.FormatterFunc, - UseIsolating = option.UseIsolating, - MaxPlaceable = option.MaxPlaceable, - EnableExtensions = option.EnableExtensions, - }; - } - - #endregion - - #region AddMethods - - public bool AddResource(string input, out List errors) + public bool EnableExtensions { get; init; } = false; + + public bool AddResource(string input, [NotNullWhen(false)] out List? errors) { var res = new LinguiniParser(input, EnableExtensions).Parse(); return AddResource(res, out errors); } - - public bool AddResource(TextReader reader, out List errors) + + public bool AddResource(TextReader reader, [NotNullWhen(false)] out List? errors) { var res = new LinguiniParser(reader, EnableExtensions).Parse(); return AddResource(res, out errors); } - - internal bool AddResource(Resource res, out List errors) + public bool AddResource(Resource res, [NotNullWhen(false)] out List? errors) { - errors = new List(); + var innerErrors = new List(); foreach (var parseError in res.Errors) { - errors.Add(ParserFluentError.ParseError(parseError)); + innerErrors.Add(ParserFluentError.ParseError(parseError)); } for (var entryPos = 0; entryPos < res.Entries.Count; entryPos++) @@ -160,19 +88,21 @@ internal bool AddResource(Resource res, out List errors) switch (entry) { case AstMessage message: - AddMessage(errors, message); + TryAddMessage(message, innerErrors); break; case AstTerm term: - AddTerm(errors, term); + TryAddTerm(term, innerErrors); break; } } - if (errors.Count == 0) + if (innerErrors.Count == 0) { + errors = null; return true; } + errors = innerErrors; return false; } @@ -194,124 +124,63 @@ private void InternalResourceOverriding(Resource resource) } } - private void AddMessageOverriding(AstMessage message) - { - _messages[message.GetId()] = message; - } + protected abstract void AddMessageOverriding(AstMessage message); - private void AddTermOverriding(AstTerm term) - { - _terms[term.GetId()] = term; - } + protected abstract void AddTermOverriding(AstTerm term); public void AddResourceOverriding(string input) { var res = new LinguiniParser(input, EnableExtensions).Parse(); InternalResourceOverriding(res); } - + public void AddResourceOverriding(TextReader input) { var res = new LinguiniParser(input, EnableExtensions).Parse(); InternalResourceOverriding(res); } - private void AddTerm(List errors, AstTerm term) - { - var termId = term.GetId(); - // ReSharper disable once CanSimplifyDictionaryLookupWithTryAdd - // Using TryAdd here leads to undocumented exceptions - if (_terms.ContainsKey(termId)) - { - errors.Add(new OverrideFluentError(termId, EntryKind.Term)); - } - else - { - _terms[termId] = term; - } - } + protected abstract bool TryAddTerm(AstTerm term, [NotNullWhen(false)] List? errors); - private void AddMessage(List errors, AstMessage msg) - { - var msgId = msg.GetId(); - // ReSharper disable once CanSimplifyDictionaryLookupWithTryAdd - // Using TryAdd here leads to undocumented exceptions - if (_messages.ContainsKey(msgId)) - { - errors.Add(new OverrideFluentError(msg.GetId(), EntryKind.Message)); - } - else - { - _messages[msgId] = msg; - } - } + protected abstract bool TryAddMessage(AstMessage msg, [NotNullWhen(false)] List? errors); - public bool TryAddFunction(string funcName, ExternalFunction fluentFunction) - { - return TryInsert(funcName, fluentFunction, InsertBehavior.None); - } - - public bool AddFunctionOverriding(string funcName, ExternalFunction fluentFunction) - { - return TryInsert(funcName, fluentFunction, InsertBehavior.OverwriteExisting); - } - - public bool AddFunctionUnchecked(string funcName, ExternalFunction fluentFunction) - { - return TryInsert(funcName, fluentFunction); - } - public void AddFunctions(IDictionary functions, out List errors) + public abstract bool TryAddFunction(string funcName, ExternalFunction fluentFunction); + + public abstract void AddFunctionOverriding(string funcName, ExternalFunction fluentFunction); + + public abstract void AddFunctionUnchecked(string funcName, ExternalFunction fluentFunction); + + /// + /// Adds a collection of external functions to the dictionary of available functions. + /// If any function cannot be added, the errors are returned in the list. + /// + /// A dictionary of function names and their corresponding ExternalFunction objects. + /// A list of errors indicating functions that could not be added. + public void AddFunctions(IDictionary functions, out List? errors) { errors = new List(); foreach (var keyValue in functions) { - if (!TryInsert(keyValue.Key, keyValue.Value, InsertBehavior.None)) + if (!TryAddFunction(keyValue.Key, keyValue.Value)) { errors.Add(new OverrideFluentError(keyValue.Key, EntryKind.Func)); } } } - private bool TryInsert(string funcName, ExternalFunction fluentFunction, - InsertBehavior behavior = InsertBehavior.ThrowOnExisting) - { - switch (behavior) - { - case InsertBehavior.None: - if (!_funcList.ContainsKey(funcName)) - { - _funcList.Add(funcName, fluentFunction); - return true; - } - - return false; - case InsertBehavior.OverwriteExisting: - _funcList[funcName] = fluentFunction; - break; - default: - if (_funcList.ContainsKey(funcName)) - { - throw new KeyNotFoundException($"Function {funcName} already exists!"); - } - - _funcList.Add(funcName, fluentFunction); - break; - } - - return true; - } - - #endregion - - #region AttrMessage - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool HasMessage(string identifier) - { - return _messages.ContainsKey(identifier); - } + /// + /// Determines if the provided identifier has a message associated with it. + /// The identifier to check. + /// True if the identifier has a message; otherwise, false. + /// + public abstract bool HasMessage(string identifier); + /// + /// Determines whether the given identifier with attribute has a message. + /// + /// The identifier with attribute. + /// True if the identifier with attribute has a message; otherwise, false. public bool HasAttrMessage(string idWithAttr) { var attributes = idWithAttr.IndexOf('.'); @@ -330,7 +199,14 @@ public bool HasAttrMessage(string idWithAttr) return false; } - public string? GetAttrMessage(string msgWithAttr, FluentArgs? args = null) + /// + /// Retrieves the attribute message by processing the given message template with the provided arguments. + /// + /// The string consisting of `messageId.Attribute`. + /// The dictionary of arguments to be used for resolution in the message template. Can be null. + /// The processed message. + /// Thrown when there are errors encountered during attribute substitution. + public string? GetAttrMessage(string msgWithAttr, IDictionary? args = null) { TryGetAttrMessage(msgWithAttr, args, out var errors, out var message); if (errors.Count > 0) @@ -358,7 +234,7 @@ public bool HasAttrMessage(string idWithAttr) return message; } - public bool TryGetAttrMessage(string msgWithAttr, FluentArgs? args, + public bool TryGetAttrMessage(string msgWithAttr, IDictionary? args, out IList errors, out string? message) { if (msgWithAttr.Contains(".")) @@ -370,11 +246,11 @@ public bool TryGetAttrMessage(string msgWithAttr, FluentArgs? args, return TryGetMessage(msgWithAttr, null, args, out errors, out message); } - public bool TryGetMessage(string id, FluentArgs? args, + public bool TryGetMessage(string id, IDictionary? args, out IList errors, [NotNullWhen(true)] out string? message) => TryGetMessage(id, null, args, out errors, out message); - public bool TryGetMessage(string id, string? attribute, FluentArgs? args, + public bool TryGetMessage(string id, string? attribute, IDictionary? args, out IList errors, [NotNullWhen(true)] out string? message) { string? value = null; @@ -383,7 +259,7 @@ public bool TryGetMessage(string id, string? attribute, FluentArgs? args, if (TryGetAstMessage(id, out var astMessage)) { var pattern = attribute != null - ? astMessage.GetAttribute(attribute)?.Value + ? TypeHelpers.GetAttribute(astMessage, attribute)?.Value : astMessage.Value; if (pattern == null) @@ -403,35 +279,45 @@ public bool TryGetMessage(string id, string? attribute, FluentArgs? args, return message != null; } - public bool TryGetAstMessage(string ident, [NotNullWhen(true)] out AstMessage? message) - { - return _messages.TryGetValue(ident, out message); - } + /// + /// Tries to get the AstMessage associated with the specified ident. + /// + /// The identifier to look for. + /// When this method returns, contains the AstMessage associated with the specified ident, if found; otherwise, null. + /// True if an AstMessage was found for the specified ident; otherwise, false. + public abstract bool TryGetAstMessage(string ident, [NotNullWhen(true)] out AstMessage? message); - public bool TryGetAstTerm(string ident, [NotNullWhen(true)] out AstTerm? term) - { - return _terms.TryGetValue(ident, out term); - } + /// + /// Tries to get a term by its identifier. + /// + /// The identifier of the AST term. + /// When this method returns, contains the AST term associated with the specified identifier, if the identifier is found; otherwise, null. This parameter is passed uninitialized. + /// true if the identifier is found and the corresponding AST term is retrieved; otherwise, false. + public abstract bool TryGetAstTerm(string ident, [NotNullWhen(true)] out AstTerm? term); + /// + /// Tries to get the FluentFunction associated with the specified Identifier. + /// + /// The Identifier used to identify the FluentFunction. + /// When the method returns, contains the FluentFunction associated with the Identifier, if the Identifier is found; otherwise, null. This parameter is passed uninitialized. + /// true if the FluentFunction associated with the Identifier is found; otherwise, false. public bool TryGetFunction(Identifier id, [NotNullWhen(true)] out FluentFunction? function) { return TryGetFunction(id.ToString(), out function); } - public bool TryGetFunction(string funcName, [NotNullWhen(true)] out FluentFunction? function) - { - if (_funcList.ContainsKey(funcName)) - { - return _funcList.TryGetValue(funcName, out function); - } - - function = null; - return false; - } - - #endregion - - public string FormatPattern(Pattern pattern, FluentArgs? args, + /// + /// Tries to retrieve a FluentFunction object by the given function name. + /// + /// The name of the function to retrieve. + /// An output parameter that will hold the retrieved FluentFunction object, if found. + /// + /// True if a FluentFunction object with the specified name was found and assigned to the function output parameter. + /// False if a FluentFunction object with the specified name was not found. + /// + public abstract bool TryGetFunction(string funcName, [NotNullWhen(true)] out FluentFunction? function); + + public string FormatPattern(Pattern pattern, IDictionary? args, out IList errors) { var scope = new Scope(this, args); @@ -440,33 +326,61 @@ public string FormatPattern(Pattern pattern, FluentArgs? args, return value.AsString(); } - public IEnumerable GetMessageEnumerable() - { - return _messages.Keys; - } + /// + /// This method retrieves an enumerable collection of all message identifiers. + /// + /// An enumerable collection of message identifiers. + public abstract IEnumerable GetMessageEnumerable(); - public IEnumerable GetFuncEnumerable() - { - return _funcList.Keys; - } + /// + /// Retrieves an enumerable collection of string function names. + /// + /// An enumerable collection of functions names. + public abstract IEnumerable GetFuncEnumerable(); - public IEnumerable GetTermEnumerable() - { - return _terms.Keys; - } + /// + /// Retrieves an enumerable collection of terms. + /// + /// An enumerable collection of terms. + public abstract IEnumerable GetTermEnumerable(); + + /// + /// Creates a deep clone of the current instance of the AbstractFluentBundle class. + /// + /// A new instance of the AbstractFluentBundle class that is a deep clone of the current instance. + public abstract FluentBundle DeepClone(); - public FluentBundle DeepClone() + public static FluentBundle MakeUnchecked(FluentBundleOption option) { - return new() + var primaryLocale = option.Locales.Count > 0 + ? option.Locales[0] + : CultureInfo.CurrentCulture.Name; + var cultureInfo = new CultureInfo(primaryLocale, false); + var func = option.Functions.ToDictionary(x => x.Key, x => (FluentFunction)x.Value); + return option.UseConcurrent switch { - Culture = (CultureInfo)Culture.Clone(), - FormatterFunc = FormatterFunc, - Locales = new List(Locales), - _messages = new Dictionary(_messages), - _terms = new Dictionary(_terms), - _funcList = new Dictionary(_funcList), - TransformFunc = TransformFunc, - UseIsolating = UseIsolating, + true => new ConcurrentBundle + { + Locales = option.Locales, + Culture = cultureInfo, + EnableExtensions = option.EnableExtensions, + FormatterFunc = option.FormatterFunc, + TransformFunc = option.TransformFunc, + MaxPlaceable = option.MaxPlaceable, + UseIsolating = option.UseIsolating, + _funcList = new ConcurrentDictionary(func), + }, + _ => new NonConcurrentBundle() + { + Locales = option.Locales, + Culture = cultureInfo, + EnableExtensions = option.EnableExtensions, + FormatterFunc = option.FormatterFunc, + TransformFunc = option.TransformFunc, + MaxPlaceable = option.MaxPlaceable, + UseIsolating = option.UseIsolating, + _funcList = func, + } }; } } diff --git a/Linguini.Bundle/InsertBehavior.cs b/Linguini.Bundle/InsertBehavior.cs deleted file mode 100644 index 8ec6bab..0000000 --- a/Linguini.Bundle/InsertBehavior.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Linguini.Bundle -{ - internal enum InsertBehavior : byte - { - None = 0, - OverwriteExisting = 1, - ThrowOnExisting = 2, - } -} \ No newline at end of file diff --git a/Linguini.Bundle/NonConcurrentBundle.cs b/Linguini.Bundle/NonConcurrentBundle.cs new file mode 100644 index 0000000..efbcbf0 --- /dev/null +++ b/Linguini.Bundle/NonConcurrentBundle.cs @@ -0,0 +1,122 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Linguini.Bundle.Errors; +using Linguini.Bundle.Types; +using Linguini.Syntax.Ast; + +// ReSharper disable UnusedType.Global + +namespace Linguini.Bundle +{ + public sealed class NonConcurrentBundle : FluentBundle + { + internal Dictionary _funcList = new(); + internal Dictionary _terms = new(); + internal Dictionary _messages = new(); + + /// + protected override void AddMessageOverriding(AstMessage message) + { + _messages[message.GetId()] = message; + } + + /// + protected override void AddTermOverriding(AstTerm term) + { + _terms[term.GetId()] = term; + } + + /// + protected override bool TryAddTerm(AstTerm term, List? errors) + { + if (_terms.TryAdd(term.GetId(), term)) return true; + errors ??= new List(); + errors.Add(new OverrideFluentError(term.GetId(), EntryKind.Term)); + return false; + } + + /// + protected override bool TryAddMessage(AstMessage message, List? errors) + { + if (_messages.TryAdd(message.GetId(), message)) return true; + errors ??= new List(); + errors.Add(new OverrideFluentError(message.GetId(), EntryKind.Message)); + return false; + } + + + /// + public override bool TryAddFunction(string funcName, ExternalFunction fluentFunction) + { + return _funcList.TryAdd(funcName, fluentFunction); + } + + /// + public override void AddFunctionOverriding(string funcName, ExternalFunction fluentFunction) + { + _funcList[funcName] = fluentFunction; + } + + /// + public override void AddFunctionUnchecked(string funcName, ExternalFunction fluentFunction) + { + _funcList.Add(funcName, fluentFunction); + } + + /// + public override bool HasMessage(string identifier) + { + return _messages.ContainsKey(identifier); + } + + /// + public override bool TryGetAstMessage(string ident, [NotNullWhen(true)] out AstMessage? message) + { + return _messages.TryGetValue(ident, out message); + } + + /// + public override bool TryGetAstTerm(string ident, [NotNullWhen(true)] out AstTerm? term) + { + return _terms.TryGetValue(ident, out term); + } + + + /// + public override bool TryGetFunction(string funcName, [NotNullWhen(true)] out FluentFunction? function) + { + return _funcList.TryGetValue(funcName, out function); + } + + /// + public override IEnumerable GetMessageEnumerable() + { + return _messages.Keys.ToArray(); + } + + /// + public override IEnumerable GetFuncEnumerable() + { + return _funcList.Keys.ToArray(); + } + + /// + public override IEnumerable GetTermEnumerable() + { + return _terms.Keys.ToArray(); + } + + + /// + public override FluentBundle DeepClone() + { + return new NonConcurrentBundle() + { + _funcList = new Dictionary(_funcList), + _terms = new Dictionary(_terms), + _messages = new Dictionary(_messages), + }; + } + } +} \ No newline at end of file diff --git a/PluralRules.Test/Cldr/CldrParserTest.cs b/PluralRules.Test/Cldr/CldrParserTest.cs index ed44313..3b3c28b 100644 --- a/PluralRules.Test/Cldr/CldrParserTest.cs +++ b/PluralRules.Test/Cldr/CldrParserTest.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using NUnit.Framework; using PluralRules.Generator.Cldr; From f4df5d04888e0999d7120f20f451f2af085e1837 Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Sun, 7 Jan 2024 21:04:14 +0100 Subject: [PATCH 3/8] Minor refactors. --- Linguini.Bundle.Test/Yaml/YamlSuiteParser.cs | 2 +- Linguini.Bundle/ConcurrentBundle.cs | 31 ++++++++++++------- Linguini.Bundle/FluentBundle.cs | 18 ++++++----- Linguini.Bundle/FrozenBundle.cs | 7 +++++ .../LinguiniFluentFunction.cs | 2 +- Linguini.Bundle/NonConcurrentBundle.cs | 30 ++++++++++++------ 6 files changed, 60 insertions(+), 30 deletions(-) create mode 100644 Linguini.Bundle/FrozenBundle.cs rename Linguini.Bundle/{Func => Function}/LinguiniFluentFunction.cs (98%) diff --git a/Linguini.Bundle.Test/Yaml/YamlSuiteParser.cs b/Linguini.Bundle.Test/Yaml/YamlSuiteParser.cs index 03706a6..2beee90 100644 --- a/Linguini.Bundle.Test/Yaml/YamlSuiteParser.cs +++ b/Linguini.Bundle.Test/Yaml/YamlSuiteParser.cs @@ -3,7 +3,7 @@ using System.IO; using Linguini.Bundle.Builder; using Linguini.Bundle.Errors; -using Linguini.Bundle.Func; +using Linguini.Bundle.Function; using Linguini.Bundle.Types; using NUnit.Framework; using YamlDotNet.RepresentationModel; diff --git a/Linguini.Bundle/ConcurrentBundle.cs b/Linguini.Bundle/ConcurrentBundle.cs index a41b44c..fb7ab54 100644 --- a/Linguini.Bundle/ConcurrentBundle.cs +++ b/Linguini.Bundle/ConcurrentBundle.cs @@ -2,9 +2,11 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; using Linguini.Bundle.Errors; using Linguini.Bundle.Types; +using Linguini.Shared.Types.Bundle; using Linguini.Syntax.Ast; // ReSharper disable UnusedType.Global @@ -13,7 +15,7 @@ namespace Linguini.Bundle { public sealed class ConcurrentBundle : FluentBundle { - internal ConcurrentDictionary _funcList = new(); + internal ConcurrentDictionary FuncList = new(); private ConcurrentDictionary _terms = new(); private ConcurrentDictionary _messages = new(); @@ -51,19 +53,19 @@ protected override bool TryAddMessage(AstMessage message, List? err /// public override bool TryAddFunction(string funcName, ExternalFunction fluentFunction) { - return _funcList.TryAdd(funcName, fluentFunction); + return FuncList.TryAdd(funcName, fluentFunction); } /// public override void AddFunctionOverriding(string funcName, ExternalFunction fluentFunction) { - _funcList[funcName] = fluentFunction; + FuncList[funcName] = fluentFunction; } /// public override void AddFunctionUnchecked(string funcName, ExternalFunction fluentFunction) { - if (_funcList.TryAdd(funcName, fluentFunction)) return; + if (FuncList.TryAdd(funcName, fluentFunction)) return; throw new ArgumentException($"Function with name {funcName} already exist"); } @@ -89,7 +91,7 @@ public override bool TryGetAstTerm(string ident, [NotNullWhen(true)] out AstTerm /// public override bool TryGetFunction(string funcName, [NotNullWhen(true)] out FluentFunction? function) { - return _funcList.TryGetValue(funcName, out function); + return FuncList.TryGetValue(funcName, out function); } /// @@ -101,7 +103,7 @@ public override IEnumerable GetMessageEnumerable() /// public override IEnumerable GetFuncEnumerable() { - return _funcList.Keys.ToArray(); + return FuncList.Keys.ToArray(); } /// @@ -109,17 +111,24 @@ public override IEnumerable GetTermEnumerable() { return _terms.Keys.ToArray(); } - - - /// + + /// public override FluentBundle DeepClone() { - return new ConcurrentBundle() + return new ConcurrentBundle { - _funcList = new ConcurrentDictionary(_funcList), + FuncList = new ConcurrentDictionary(FuncList), _terms = new ConcurrentDictionary(_terms), _messages = new ConcurrentDictionary(_messages), + Culture = (CultureInfo) Culture.Clone(), + Locales = new List(Locales), + UseIsolating = UseIsolating, + TransformFunc = (Func?)TransformFunc?.Clone(), + FormatterFunc = (Func?)FormatterFunc?.Clone(), + MaxPlaceable = MaxPlaceable, + EnableExtensions = EnableExtensions, }; } + } } \ No newline at end of file diff --git a/Linguini.Bundle/FluentBundle.cs b/Linguini.Bundle/FluentBundle.cs index e499042..bf7530b 100644 --- a/Linguini.Bundle/FluentBundle.cs +++ b/Linguini.Bundle/FluentBundle.cs @@ -5,7 +5,6 @@ using System.Globalization; using System.IO; using System.Linq; -using System.Xml.Schema; using Linguini.Bundle.Builder; using Linguini.Bundle.Errors; using Linguini.Bundle.Resolver; @@ -18,7 +17,6 @@ namespace Linguini.Bundle { public abstract class FluentBundle { - /// /// of the bundle. Primary bundle locale /// @@ -48,7 +46,7 @@ public abstract class FluentBundle /// /// Limit of placeable within one , when fully expanded (all nested elements count towards it). Useful for preventing billion laughs attack. Defaults to 100. /// - public byte MaxPlaceable { get; private init; } = 100; + public byte MaxPlaceable { get; internal init; } = 100; /// /// Whether experimental features are enabled. @@ -60,7 +58,8 @@ public abstract class FluentBundle /// term reference as parameters /// /// - public bool EnableExtensions { get; init; } = false; + // ReSharper disable once MemberCanBeProtected.Global + public bool EnableExtensions { get; init; } public bool AddResource(string input, [NotNullWhen(false)] out List? errors) { @@ -350,6 +349,11 @@ public string FormatPattern(Pattern pattern, IDictionary? a /// A new instance of the AbstractFluentBundle class that is a deep clone of the current instance. public abstract FluentBundle DeepClone(); + /// + /// Creates a FluentBundle object with the specified options. + /// + /// The FluentBundleOption object that contains the options for creating the FluentBundle + /// A FluentBundle object created with the specified options public static FluentBundle MakeUnchecked(FluentBundleOption option) { var primaryLocale = option.Locales.Count > 0 @@ -368,9 +372,9 @@ public static FluentBundle MakeUnchecked(FluentBundleOption option) TransformFunc = option.TransformFunc, MaxPlaceable = option.MaxPlaceable, UseIsolating = option.UseIsolating, - _funcList = new ConcurrentDictionary(func), + FuncList = new ConcurrentDictionary(func), }, - _ => new NonConcurrentBundle() + _ => new NonConcurrentBundle { Locales = option.Locales, Culture = cultureInfo, @@ -379,7 +383,7 @@ public static FluentBundle MakeUnchecked(FluentBundleOption option) TransformFunc = option.TransformFunc, MaxPlaceable = option.MaxPlaceable, UseIsolating = option.UseIsolating, - _funcList = func, + FuncList = func, } }; } diff --git a/Linguini.Bundle/FrozenBundle.cs b/Linguini.Bundle/FrozenBundle.cs new file mode 100644 index 0000000..d673c04 --- /dev/null +++ b/Linguini.Bundle/FrozenBundle.cs @@ -0,0 +1,7 @@ +namespace Linguini.Bundle +{ + public class FrozenBundle + { + + } +} \ No newline at end of file diff --git a/Linguini.Bundle/Func/LinguiniFluentFunction.cs b/Linguini.Bundle/Function/LinguiniFluentFunction.cs similarity index 98% rename from Linguini.Bundle/Func/LinguiniFluentFunction.cs rename to Linguini.Bundle/Function/LinguiniFluentFunction.cs index 5eac61d..12fca9b 100644 --- a/Linguini.Bundle/Func/LinguiniFluentFunction.cs +++ b/Linguini.Bundle/Function/LinguiniFluentFunction.cs @@ -5,7 +5,7 @@ using Linguini.Bundle.Types; using Linguini.Shared.Types.Bundle; -namespace Linguini.Bundle.Func +namespace Linguini.Bundle.Function { public static class LinguiniFluentFunctions { diff --git a/Linguini.Bundle/NonConcurrentBundle.cs b/Linguini.Bundle/NonConcurrentBundle.cs index efbcbf0..72f553b 100644 --- a/Linguini.Bundle/NonConcurrentBundle.cs +++ b/Linguini.Bundle/NonConcurrentBundle.cs @@ -1,8 +1,11 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; using Linguini.Bundle.Errors; using Linguini.Bundle.Types; +using Linguini.Shared.Types.Bundle; using Linguini.Syntax.Ast; // ReSharper disable UnusedType.Global @@ -11,9 +14,9 @@ namespace Linguini.Bundle { public sealed class NonConcurrentBundle : FluentBundle { - internal Dictionary _funcList = new(); - internal Dictionary _terms = new(); - internal Dictionary _messages = new(); + internal Dictionary FuncList = new(); + private Dictionary _terms = new(); + private Dictionary _messages = new(); /// protected override void AddMessageOverriding(AstMessage message) @@ -49,19 +52,19 @@ protected override bool TryAddMessage(AstMessage message, List? err /// public override bool TryAddFunction(string funcName, ExternalFunction fluentFunction) { - return _funcList.TryAdd(funcName, fluentFunction); + return FuncList.TryAdd(funcName, fluentFunction); } /// public override void AddFunctionOverriding(string funcName, ExternalFunction fluentFunction) { - _funcList[funcName] = fluentFunction; + FuncList[funcName] = fluentFunction; } /// public override void AddFunctionUnchecked(string funcName, ExternalFunction fluentFunction) { - _funcList.Add(funcName, fluentFunction); + FuncList.Add(funcName, fluentFunction); } /// @@ -86,7 +89,7 @@ public override bool TryGetAstTerm(string ident, [NotNullWhen(true)] out AstTerm /// public override bool TryGetFunction(string funcName, [NotNullWhen(true)] out FluentFunction? function) { - return _funcList.TryGetValue(funcName, out function); + return FuncList.TryGetValue(funcName, out function); } /// @@ -98,7 +101,7 @@ public override IEnumerable GetMessageEnumerable() /// public override IEnumerable GetFuncEnumerable() { - return _funcList.Keys.ToArray(); + return FuncList.Keys.ToArray(); } /// @@ -113,9 +116,16 @@ public override FluentBundle DeepClone() { return new NonConcurrentBundle() { - _funcList = new Dictionary(_funcList), + FuncList = new Dictionary(FuncList), _terms = new Dictionary(_terms), _messages = new Dictionary(_messages), + Culture = (CultureInfo) Culture.Clone(), + Locales = new List(Locales), + UseIsolating = UseIsolating, + TransformFunc = (Func?)TransformFunc?.Clone(), + FormatterFunc = (Func?)FormatterFunc?.Clone(), + MaxPlaceable = MaxPlaceable, + EnableExtensions = EnableExtensions, }; } } From 27fc5ef9202d998365cbd58d32c7554b6a825177 Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Sun, 7 Jan 2024 21:31:09 +0100 Subject: [PATCH 4/8] Add tests for Equality --- Linguini.Bundle.Test/Unit/BundleTests.cs | 35 +++++++++++++++++++ Linguini.Bundle/ConcurrentBundle.cs | 30 +++++++++++++--- Linguini.Bundle/FluentBundle.cs | 44 ++++++++++++++++++++++-- Linguini.Bundle/NonConcurrentBundle.cs | 26 +++++++++++--- 4 files changed, 124 insertions(+), 11 deletions(-) diff --git a/Linguini.Bundle.Test/Unit/BundleTests.cs b/Linguini.Bundle.Test/Unit/BundleTests.cs index 48f00d6..47e2179 100644 --- a/Linguini.Bundle.Test/Unit/BundleTests.cs +++ b/Linguini.Bundle.Test/Unit/BundleTests.cs @@ -358,5 +358,40 @@ public void TestDynamicSelectors() Assert.That(bundle.TryGetMessage("you-see", args, out _, out var message2)); Assert.That("You see a fairy.", Is.EqualTo(message2)); } + + [Test] + public void TestDeepClone() + { + var originalBundleOption = new FluentBundleOption + { + Locales = { "en-US" }, + MaxPlaceable = 123, + UseIsolating = false, + TransformFunc = _transform, + FormatterFunc = _formatter, + Functions = new Dictionary() + { + ["zero"] = _zeroFunc, + ["id"] = _idFunc, + } + }; + + // Assume FluentBundle object has DeepClone method + FluentBundle originalBundle = FluentBundle.MakeUnchecked(originalBundleOption); + FluentBundle clonedBundle = originalBundle.DeepClone(); + + // Assert that the original and cloned objects are not the same reference + Assert.That(originalBundle, Is.Not.SameAs(clonedBundle)); + + // Assert that the properties are copied properly + Assert.That(originalBundle, Is.EqualTo(clonedBundle)); + + // Assert that if original property is changed, new property isn't. + originalBundle.AddFunctionOverriding("zero", _idFunc); + clonedBundle.TryGetFunction("zero", out var clonedZero); + Assert.That((FluentFunction) _zeroFunc, Is.EqualTo(clonedZero)); + originalBundle.TryGetFunction("zero", out var originalZero); + Assert.That((FluentFunction) _idFunc, Is.EqualTo(originalZero)); + } } } \ No newline at end of file diff --git a/Linguini.Bundle/ConcurrentBundle.cs b/Linguini.Bundle/ConcurrentBundle.cs index fb7ab54..7126a06 100644 --- a/Linguini.Bundle/ConcurrentBundle.cs +++ b/Linguini.Bundle/ConcurrentBundle.cs @@ -13,7 +13,7 @@ namespace Linguini.Bundle { - public sealed class ConcurrentBundle : FluentBundle + public sealed class ConcurrentBundle : FluentBundle, IEquatable { internal ConcurrentDictionary FuncList = new(); private ConcurrentDictionary _terms = new(); @@ -49,7 +49,7 @@ protected override bool TryAddMessage(AstMessage message, List? err return false; } - + /// public override bool TryAddFunction(string funcName, ExternalFunction fluentFunction) { @@ -74,7 +74,7 @@ public override bool HasMessage(string identifier) { return _messages.ContainsKey(identifier); } - + /// public override bool TryGetAstMessage(string ident, [NotNullWhen(true)] out AstMessage? message) { @@ -111,7 +111,7 @@ public override IEnumerable GetTermEnumerable() { return _terms.Keys.ToArray(); } - + /// public override FluentBundle DeepClone() { @@ -120,7 +120,7 @@ public override FluentBundle DeepClone() FuncList = new ConcurrentDictionary(FuncList), _terms = new ConcurrentDictionary(_terms), _messages = new ConcurrentDictionary(_messages), - Culture = (CultureInfo) Culture.Clone(), + Culture = (CultureInfo)Culture.Clone(), Locales = new List(Locales), UseIsolating = UseIsolating, TransformFunc = (Func?)TransformFunc?.Clone(), @@ -129,6 +129,26 @@ public override FluentBundle DeepClone() EnableExtensions = EnableExtensions, }; } + + /// + public bool Equals(ConcurrentBundle? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && FuncList.SequenceEqual(other.FuncList) && _terms.SequenceEqual(other._terms) && + _messages.SequenceEqual(other._messages); + } + + /// + public override bool Equals(object? obj) + { + return ReferenceEquals(this, obj) || obj is ConcurrentBundle other && Equals(other); + } + /// + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), FuncList, _terms, _messages); + } } } \ No newline at end of file diff --git a/Linguini.Bundle/FluentBundle.cs b/Linguini.Bundle/FluentBundle.cs index bf7530b..47ff25d 100644 --- a/Linguini.Bundle/FluentBundle.cs +++ b/Linguini.Bundle/FluentBundle.cs @@ -15,7 +15,7 @@ namespace Linguini.Bundle { - public abstract class FluentBundle + public abstract class FluentBundle : IEquatable { /// /// of the bundle. Primary bundle locale @@ -60,7 +60,7 @@ public abstract class FluentBundle /// // ReSharper disable once MemberCanBeProtected.Global public bool EnableExtensions { get; init; } - + public bool AddResource(string input, [NotNullWhen(false)] out List? errors) { var res = new LinguiniParser(input, EnableExtensions).Parse(); @@ -123,10 +123,23 @@ private void InternalResourceOverriding(Resource resource) } } + /// + /// Adds the given AstMessage to the collection of messages, by overriding any existing messages with the same name. + /// + /// The AstMessage to be added. protected abstract void AddMessageOverriding(AstMessage message); + /// + /// Adds a term to the AstTerm list, overriding any existing term with the same name. + /// + /// The term to be added. protected abstract void AddTermOverriding(AstTerm term); + /// + /// Adds a resource. + /// Any messages or terms in bundle will be overriden by the existing ones. + /// + /// The input string containing the resource data. public void AddResourceOverriding(string input) { var res = new LinguiniParser(input, EnableExtensions).Parse(); @@ -387,5 +400,32 @@ public static FluentBundle MakeUnchecked(FluentBundleOption option) } }; } + + /// + public bool Equals(FluentBundle? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Culture.Equals(other.Culture) && Locales.SequenceEqual(other.Locales) && + UseIsolating == other.UseIsolating && Equals(TransformFunc, other.TransformFunc) && + Equals(FormatterFunc, other.FormatterFunc) && MaxPlaceable == other.MaxPlaceable && + EnableExtensions == other.EnableExtensions; + } + + /// + 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((FluentBundle)obj); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(Culture, Locales, UseIsolating, TransformFunc, FormatterFunc, MaxPlaceable, + EnableExtensions); + } } } \ No newline at end of file diff --git a/Linguini.Bundle/NonConcurrentBundle.cs b/Linguini.Bundle/NonConcurrentBundle.cs index 72f553b..f8c7d2c 100644 --- a/Linguini.Bundle/NonConcurrentBundle.cs +++ b/Linguini.Bundle/NonConcurrentBundle.cs @@ -12,7 +12,7 @@ namespace Linguini.Bundle { - public sealed class NonConcurrentBundle : FluentBundle + public sealed class NonConcurrentBundle : FluentBundle, IEquatable { internal Dictionary FuncList = new(); private Dictionary _terms = new(); @@ -48,7 +48,7 @@ protected override bool TryAddMessage(AstMessage message, List? err return false; } - + /// public override bool TryAddFunction(string funcName, ExternalFunction fluentFunction) { @@ -72,7 +72,7 @@ public override bool HasMessage(string identifier) { return _messages.ContainsKey(identifier); } - + /// public override bool TryGetAstMessage(string ident, [NotNullWhen(true)] out AstMessage? message) { @@ -119,7 +119,7 @@ public override FluentBundle DeepClone() FuncList = new Dictionary(FuncList), _terms = new Dictionary(_terms), _messages = new Dictionary(_messages), - Culture = (CultureInfo) Culture.Clone(), + Culture = (CultureInfo)Culture.Clone(), Locales = new List(Locales), UseIsolating = UseIsolating, TransformFunc = (Func?)TransformFunc?.Clone(), @@ -128,5 +128,23 @@ public override FluentBundle DeepClone() EnableExtensions = EnableExtensions, }; } + + public bool Equals(NonConcurrentBundle? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && FuncList.SequenceEqual(other.FuncList) && _terms.SequenceEqual(other._terms) && + _messages.SequenceEqual(other._messages); + } + + public override bool Equals(object? obj) + { + return ReferenceEquals(this, obj) || obj is NonConcurrentBundle other && Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), FuncList, _terms, _messages); + } } } \ No newline at end of file From ddd30816d79c0ba85df553d39d48309980042a00 Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Mon, 8 Jan 2024 20:34:54 +0100 Subject: [PATCH 5/8] Minor refactors --- Linguini.Bundle.Test/Unit/BundleTests.cs | 25 +++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/Linguini.Bundle.Test/Unit/BundleTests.cs b/Linguini.Bundle.Test/Unit/BundleTests.cs index 47e2179..f94e659 100644 --- a/Linguini.Bundle.Test/Unit/BundleTests.cs +++ b/Linguini.Bundle.Test/Unit/BundleTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Linq; using System.Threading.Tasks; @@ -22,23 +23,23 @@ public class BundleTests private readonly Func _formatter = _ => ""; private readonly Func _transform = str => str.ToUpper(CultureInfo.InvariantCulture); - private static string _res1 = @" + private const string Res1 = @" term = term .attr = 3"; - private static string _wrong = @" + private const string Wrong = @" term = 1"; - private static string _multi = @" + private const string Multi = @" term1 = val1 term2 = val2 .attr = 6"; - private static string _replace1 = @" + private const string Replace1 = @" term1 = val1 term2 = val2"; - private static string _replace2 = @" + private const string Replace2 = @" term1 = xxx new1 = new .attr = 6"; @@ -90,7 +91,7 @@ public void TestReplaceMessage() { var bundler = LinguiniBuilder.Builder() .CultureInfo(new CultureInfo("en")) - .AddResource(_replace1); + .AddResource(Replace1); var bundle = bundler.UncheckedBuild(); Assert.That(bundle.TryGetAttrMessage("term1", null, out _, out var termMsg)); @@ -98,7 +99,7 @@ public void TestReplaceMessage() Assert.That(bundle.TryGetAttrMessage("term2", null, out _, out var termMsg2)); Assert.That("val2", Is.EqualTo(termMsg2)); - bundle.AddResourceOverriding(_replace2); + bundle.AddResourceOverriding(Replace2); Assert.That(bundle.TryGetAttrMessage("term2", null, out _, out _)); Assert.That(bundle.TryGetAttrMessage("term1", null, out _, out termMsg)); Assert.That("xxx", Is.EqualTo(termMsg)); @@ -112,7 +113,7 @@ public void TestExceptions() { var bundler = LinguiniBuilder.Builder() .Locales("en-US", "sr-RS") - .AddResources(_wrong, _res1) + .AddResources(Wrong, Res1) .SetFormatterFunc(_formatter) .SetTransformFunc(_transform) .AddFunction("id", _idFunc) @@ -140,7 +141,7 @@ public void TestEnumeration() { var bundler = LinguiniBuilder.Builder() .Locale("en-US") - .AddResource(_multi) + .AddResource(Multi) .AddFunction("id", _idFunc) .AddFunction("zero", _zeroFunc) .UncheckedBuild(); @@ -221,7 +222,7 @@ public void TestBehavior(string idWithAttr, bool found) { var bundle = LinguiniBuilder.Builder() .CultureInfo(new CultureInfo("en")) - .AddResource(_replace2) + .AddResource(Replace2) .UncheckedBuild(); Assert.That(bundle.TryGetAttrMessage(idWithAttr, null, out _, out _), @@ -238,7 +239,7 @@ public void TestHasAttrMessage(string idWithAttr, bool found) { var bundle = LinguiniBuilder.Builder() .CultureInfo(new CultureInfo("en")) - .AddResource(_replace2) + .AddResource(Replace2) .UncheckedBuild(); Assert.That(bundle.TryGetAttrMessage(idWithAttr, null, out _, out _), @@ -264,6 +265,8 @@ public static IEnumerable TestBundleErrors var (_, error) = LinguiniBuilder.Builder().Locale("en-US") .AddResource(input) .Build(); + Debug.Assert(error != null, nameof(error) + " != null"); + Assert.That(error, Is.Not.Empty); return error.Select(e => e.GetSpan()).ToList(); } From e354f18bbd27e6bd5f847aa224c025cd052c22b6 Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Mon, 8 Jan 2024 22:09:18 +0100 Subject: [PATCH 6/8] Add net8.0 for frozen sets --- Linguini.Bench/Linguini.Bench.csproj | 2 +- Linguini.Bundle.Test/Linguini.Bundle.Test.csproj | 2 +- Linguini.Bundle/Linguini.Bundle.csproj | 2 +- Linguini.Serialization/Linguini.Serialization.csproj | 2 +- Linguini.Shared/Linguini.Shared.csproj | 2 +- Linguini.Syntax.Tests/Linguini.Syntax.Tests.csproj | 2 +- Linguini.Syntax/Linguini.Syntax.csproj | 2 +- global.json | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Linguini.Bench/Linguini.Bench.csproj b/Linguini.Bench/Linguini.Bench.csproj index 471aca5..bfe1545 100644 --- a/Linguini.Bench/Linguini.Bench.csproj +++ b/Linguini.Bench/Linguini.Bench.csproj @@ -3,7 +3,7 @@ Exe false - net6.0 + net6.0;net8.0 diff --git a/Linguini.Bundle.Test/Linguini.Bundle.Test.csproj b/Linguini.Bundle.Test/Linguini.Bundle.Test.csproj index dbafcc4..8ad1b66 100644 --- a/Linguini.Bundle.Test/Linguini.Bundle.Test.csproj +++ b/Linguini.Bundle.Test/Linguini.Bundle.Test.csproj @@ -5,7 +5,7 @@ enable Library 0.7.0 - net6.0 + net6.0;net8.0 diff --git a/Linguini.Bundle/Linguini.Bundle.csproj b/Linguini.Bundle/Linguini.Bundle.csproj index 4ea1738..565736f 100644 --- a/Linguini.Bundle/Linguini.Bundle.csproj +++ b/Linguini.Bundle/Linguini.Bundle.csproj @@ -19,7 +19,7 @@ It provides easy to use and extend system for describing translations.https://github.com/Ygg01/Linguini git 0.7.0 - netstandard2.1;net6.0 + net6.0;netstandard2.1;net8.0 linguini.jpg README.md diff --git a/Linguini.Serialization/Linguini.Serialization.csproj b/Linguini.Serialization/Linguini.Serialization.csproj index 8106097..de7f55a 100644 --- a/Linguini.Serialization/Linguini.Serialization.csproj +++ b/Linguini.Serialization/Linguini.Serialization.csproj @@ -2,11 +2,11 @@ 0.7.0 - net6.0 linguini.jpg README.md serialization, linguini, utility Optional serialization library for Linguini + net6.0;net8.0 diff --git a/Linguini.Shared/Linguini.Shared.csproj b/Linguini.Shared/Linguini.Shared.csproj index 1c3eab8..6d525a3 100644 --- a/Linguini.Shared/Linguini.Shared.csproj +++ b/Linguini.Shared/Linguini.Shared.csproj @@ -10,7 +10,7 @@ fluent, i18n, internationalization, l10n, l20n, globalization, translation false 0.7.0 - netstandard2.1;net6.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 12dbeb5..3599ec3 100644 --- a/Linguini.Syntax.Tests/Linguini.Syntax.Tests.csproj +++ b/Linguini.Syntax.Tests/Linguini.Syntax.Tests.csproj @@ -4,8 +4,8 @@ false enable Library - net6.0 linguini.jpg + net6.0;net8.0 diff --git a/Linguini.Syntax/Linguini.Syntax.csproj b/Linguini.Syntax/Linguini.Syntax.csproj index 8cd0005..27903c9 100644 --- a/Linguini.Syntax/Linguini.Syntax.csproj +++ b/Linguini.Syntax/Linguini.Syntax.csproj @@ -11,7 +11,7 @@ fluent, i18n, internationalization, l10n, l20n, globalization, translation https://github.com/Ygg01/Linguini git - netstandard2.1;net6.0 + net6.0;netstandard2.1;net8.0 0.7.0 README.md linguini.jpg diff --git a/global.json b/global.json index cfd2c16..b93fcd8 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "feature", - "version": "7.0.401" + "version": "8.0.100" } } \ No newline at end of file From 6975655a487b80d7a4c06ad2803f2cfe9d90110e Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Mon, 8 Jan 2024 22:09:34 +0100 Subject: [PATCH 7/8] Add FrozenBundle --- Linguini.Bundle/FluentBundle.cs | 104 +++++++++++++++++++++++++++++--- Linguini.Bundle/FrozenBundle.cs | 87 +++++++++++++++++++++++++- 2 files changed, 182 insertions(+), 9 deletions(-) diff --git a/Linguini.Bundle/FluentBundle.cs b/Linguini.Bundle/FluentBundle.cs index 47ff25d..5a8521a 100644 --- a/Linguini.Bundle/FluentBundle.cs +++ b/Linguini.Bundle/FluentBundle.cs @@ -15,7 +15,97 @@ namespace Linguini.Bundle { - public abstract class FluentBundle : IEquatable + public interface IReadBundle + { + /// + /// Determines if the provided identifier has a message associated with it. + /// The identifier to check. + /// True if the identifier has a message; otherwise, false. + /// + bool HasMessage(string identifier); + + /// + /// Determines whether the given identifier with attribute has a message. + /// + /// The identifier with attribute. + /// True if the identifier with attribute has a message; otherwise, false. + bool HasAttrMessage(string idWithAttr); + + /// + /// Retrieves the attribute message by processing the given message template with the provided arguments. + /// + /// The string consisting of `messageId.Attribute`. + /// The dictionary of arguments to be used for resolution in the message template. Can be null. + /// The processed message. + /// Thrown when there are errors encountered during attribute substitution. + string? GetAttrMessage(string msgWithAttr, IDictionary? args = null); + + string? GetAttrMessage(string msgWithAttr, params (string, IFluentType)[] args); + + bool TryGetAttrMessage(string msgWithAttr, IDictionary? args, + [NotNullWhen(false)] out IList? errors, [NotNullWhen(true)] out string? message); + + bool TryGetMessage(string id, IDictionary? args, + [NotNullWhen(false)] out IList? errors, [NotNullWhen(true)] out string? message); + + bool TryGetMessage(string id, string? attribute, IDictionary? args, + [NotNullWhen(false)] out IList? errors, [NotNullWhen(true)] out string? message); + + /// + /// Tries to get the AstMessage associated with the specified ident. + /// + /// The identifier to look for. + /// When this method returns, contains the AstMessage associated with the specified ident, if found; otherwise, null. + /// True if an AstMessage was found for the specified ident; otherwise, false. + bool TryGetAstMessage(string ident, [NotNullWhen(true)] out AstMessage? message); + + /// + /// Tries to get a term by its identifier. + /// + /// The identifier of the AST term. + /// When this method returns, contains the AST term associated with the specified identifier, if the identifier is found; otherwise, null. This parameter is passed uninitialized. + /// true if the identifier is found and the corresponding AST term is retrieved; otherwise, false. + bool TryGetAstTerm(string ident, [NotNullWhen(true)] out AstTerm? term); + + /// + /// Tries to get the FluentFunction associated with the specified Identifier. + /// + /// The Identifier used to identify the FluentFunction. + /// When the method returns, contains the FluentFunction associated with the Identifier, if the Identifier is found; otherwise, null. This parameter is passed uninitialized. + /// true if the FluentFunction associated with the Identifier is found; otherwise, false. + bool TryGetFunction(Identifier id, [NotNullWhen(true)] out FluentFunction? function); + + /// + /// Tries to retrieve a FluentFunction object by the given function name. + /// + /// The name of the function to retrieve. + /// An output parameter that will hold the retrieved FluentFunction object, if found. + /// + /// True if a FluentFunction object with the specified name was found and assigned to the function output parameter. + /// False if a FluentFunction object with the specified name was not found. + /// + bool TryGetFunction(string funcName, [NotNullWhen(true)] out FluentFunction? function); + + /// + /// This method retrieves an enumerable collection of all message identifiers. + /// + /// An enumerable collection of message identifiers. + IEnumerable GetMessageEnumerable(); + + /// + /// Retrieves an enumerable collection of string function names. + /// + /// An enumerable collection of functions names. + IEnumerable GetFuncEnumerable(); + + /// + /// Retrieves an enumerable collection of terms. + /// + /// An enumerable collection of terms. + IEnumerable GetTermEnumerable(); + } + + public abstract class FluentBundle : IEquatable, IReadBundle { /// /// of the bundle. Primary bundle locale @@ -221,7 +311,7 @@ public bool HasAttrMessage(string idWithAttr) public string? GetAttrMessage(string msgWithAttr, IDictionary? args = null) { TryGetAttrMessage(msgWithAttr, args, out var errors, out var message); - if (errors.Count > 0) + if (errors is { Count: > 0 }) { throw new LinguiniException(errors); } @@ -238,7 +328,7 @@ public bool HasAttrMessage(string idWithAttr) } TryGetAttrMessage(msgWithAttr, dictionary, out var errors, out var message); - if (errors.Count > 0) + if (errors is { Count: > 0 }) { throw new LinguiniException(errors); } @@ -247,7 +337,7 @@ public bool HasAttrMessage(string idWithAttr) } public bool TryGetAttrMessage(string msgWithAttr, IDictionary? args, - out IList errors, out string? message) + [NotNullWhen(false)] out IList? errors, [NotNullWhen(true)] out string? message) { if (msgWithAttr.Contains(".")) { @@ -259,11 +349,11 @@ public bool TryGetAttrMessage(string msgWithAttr, IDictionary? args, - out IList errors, [NotNullWhen(true)] out string? message) + [NotNullWhen(false)] out IList? errors, [NotNullWhen(true)] out string? message) => TryGetMessage(id, null, args, out errors, out message); public bool TryGetMessage(string id, string? attribute, IDictionary? args, - out IList errors, [NotNullWhen(true)] out string? message) + [NotNullWhen(false)] out IList? errors, [NotNullWhen(true)] out string? message) { string? value = null; errors = new List(); @@ -330,7 +420,7 @@ public bool TryGetFunction(Identifier id, [NotNullWhen(true)] out FluentFunction public abstract bool TryGetFunction(string funcName, [NotNullWhen(true)] out FluentFunction? function); public string FormatPattern(Pattern pattern, IDictionary? args, - out IList errors) + [NotNullWhen(false)] out IList? errors) { var scope = new Scope(this, args); var value = pattern.Resolve(scope); diff --git a/Linguini.Bundle/FrozenBundle.cs b/Linguini.Bundle/FrozenBundle.cs index d673c04..ff8c8de 100644 --- a/Linguini.Bundle/FrozenBundle.cs +++ b/Linguini.Bundle/FrozenBundle.cs @@ -1,7 +1,90 @@ -namespace Linguini.Bundle +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Linguini.Bundle.Errors; +using Linguini.Bundle.Types; +using Linguini.Shared.Types.Bundle; +using Linguini.Syntax.Ast; + +namespace Linguini.Bundle { - public class FrozenBundle + public class FrozenBundle : IReadBundle { +#if NET8_0_OR_GREATER +#else + +#endif + public bool HasMessage(string identifier) + { + throw new System.NotImplementedException(); + } + + public bool HasAttrMessage(string idWithAttr) + { + throw new System.NotImplementedException(); + } + + public string? GetAttrMessage(string msgWithAttr, IDictionary? args = null) + { + throw new System.NotImplementedException(); + } + + public string? GetAttrMessage(string msgWithAttr, params (string, IFluentType)[] args) + { + throw new System.NotImplementedException(); + } + + public bool TryGetAttrMessage(string msgWithAttr, IDictionary? args, + [NotNullWhen(false)] out IList? errors, [NotNullWhen(true)] out string? message) + { + throw new System.NotImplementedException(); + } + + public bool TryGetMessage(string id, IDictionary? args, out IList errors, + [NotNullWhen(true)] out string? message) + { + throw new System.NotImplementedException(); + } + + public bool TryGetMessage(string id, string? attribute, IDictionary? args, + [NotNullWhen(false)] out IList? errors, [NotNullWhen(true)] out string? message) + { + throw new System.NotImplementedException(); + } + + public bool TryGetAstMessage(string ident, [NotNullWhen(true)] out AstMessage? message) + { + throw new System.NotImplementedException(); + } + + public bool TryGetAstTerm(string ident, [NotNullWhen(true)] out AstTerm? term) + { + throw new System.NotImplementedException(); + } + + public bool TryGetFunction(Identifier id, [NotNullWhen(true)] out FluentFunction? function) + { + throw new System.NotImplementedException(); + } + + public bool TryGetFunction(string funcName, [NotNullWhen(true)] out FluentFunction? function) + { + throw new System.NotImplementedException(); + } + + public IEnumerable GetMessageEnumerable() + { + throw new System.NotImplementedException(); + } + + public IEnumerable GetFuncEnumerable() + { + throw new System.NotImplementedException(); + } + + public IEnumerable GetTermEnumerable() + { + throw new System.NotImplementedException(); + } } } \ No newline at end of file From 2cbde6c5b69c0710e4f32047abc573454bc09090 Mon Sep 17 00:00:00 2001 From: Ygg01 Date: Tue, 9 Jan 2024 23:04:51 +0100 Subject: [PATCH 8/8] Finish frozen bundle --- Linguini.Bundle.Test/Unit/BundleTests.cs | 29 +- Linguini.Bundle.Test/Yaml/YamlSuiteParser.cs | 4 +- Linguini.Bundle/ConcurrentBundle.cs | 54 ++- Linguini.Bundle/FluentBundle.cs | 430 ++++++------------- Linguini.Bundle/FrozenBundle.cs | 149 +++++-- Linguini.Bundle/IReadBundle.cs | 275 ++++++++++++ Linguini.Bundle/Linguini.Bundle.csproj | 2 +- Linguini.Bundle/NonConcurrentBundle.cs | 50 ++- Linguini.Bundle/Resolver/ResolverHelpers.cs | 2 +- Linguini.Bundle/Resolver/Scope.cs | 46 +- Linguini.Bundle/Resolver/WriterHelpers.cs | 8 +- 11 files changed, 678 insertions(+), 371 deletions(-) create mode 100644 Linguini.Bundle/IReadBundle.cs diff --git a/Linguini.Bundle.Test/Unit/BundleTests.cs b/Linguini.Bundle.Test/Unit/BundleTests.cs index f94e659..401ea50 100644 --- a/Linguini.Bundle.Test/Unit/BundleTests.cs +++ b/Linguini.Bundle.Test/Unit/BundleTests.cs @@ -177,11 +177,16 @@ public void TestConcurrencyOption() UseConcurrent = true, }; var optBundle = FluentBundle.MakeUnchecked(bundleOpt); + Parallel.For(0, 10, i => optBundle.AddResource($"term-1 = {i}", out _)); Parallel.For(0, 10, i => optBundle.AddResource($"term-2= {i}", out _)); Parallel.For(0, 10, i => optBundle.TryGetAttrMessage("term-1", null, out _, out _)); Parallel.For(0, 10, i => optBundle.AddResourceOverriding($"term-2= {i + 1}")); Assert.That(optBundle.HasMessage("term-1")); + + // Frozen bundle are read only and should be thread-safe + var frozenBundle = optBundle.ToFrozenBundle(); + Parallel.For(0, 10, i => frozenBundle.TryGetAttrMessage("term-1", null, out _, out _)); } [Test] @@ -253,7 +258,7 @@ public static IEnumerable TestBundleErrors yield return new TestCaseData("### Comment\r\nterm1") .Returns(new List { - new ErrorSpan(2, 13, 18, 18, 19) + new(2, 13, 18, 18, 19) }); } } @@ -315,7 +320,7 @@ [neuter] It [Test] [Parallelizable] - public void TestMacrosFail() + public void TestExtensionsWork() { var (bundle, err) = LinguiniBuilder.Builder(useExperimental: true).Locale("en-US") .AddResource(Macros) @@ -327,6 +332,11 @@ public void TestMacrosFail() }; Assert.That(bundle.TryGetMessage("call-attr-no-args", args, out _, out var message)); Assert.That("It", Is.EqualTo(message)); + + // Check Frozen bundle behaves similarly + var frozenBundle = bundle.ToFrozenBundle(); + Assert.That(frozenBundle.TryGetMessage("call-attr-no-args", args, out _, out var frozenMessage)); + Assert.That("It", Is.EqualTo(frozenMessage)); } private const string DynamicSelectors = @" -creature-fairy = fairy @@ -360,6 +370,21 @@ public void TestDynamicSelectors() }; Assert.That(bundle.TryGetMessage("you-see", args, out _, out var message2)); Assert.That("You see a fairy.", Is.EqualTo(message2)); + + // Check Frozen bundle behaves similarly + var frozenBundle = bundle.ToFrozenBundle(); + args = new Dictionary + { + ["object"] = (FluentReference)"creature-elf", + }; + Assert.That(frozenBundle.TryGetMessage("you-see", args, out _, out var frozenMessage1)); + Assert.That("You see an elf.", Is.EqualTo(frozenMessage1)); + args = new Dictionary + { + ["object"] = (FluentReference)"creature-fairy", + }; + Assert.That(frozenBundle.TryGetMessage("you-see", args, out _, out var frozenMessage2)); + Assert.That("You see a fairy.", Is.EqualTo(frozenMessage2)); } [Test] diff --git a/Linguini.Bundle.Test/Yaml/YamlSuiteParser.cs b/Linguini.Bundle.Test/Yaml/YamlSuiteParser.cs index 2beee90..4b06ca4 100644 --- a/Linguini.Bundle.Test/Yaml/YamlSuiteParser.cs +++ b/Linguini.Bundle.Test/Yaml/YamlSuiteParser.cs @@ -195,10 +195,10 @@ private static string GetFullPathFor(string file) private static void AssertErrorCases(List expectedErrors, - IList errs, + IList? errs, String testName) { - Assert.That(expectedErrors.Count, Is.EqualTo(errs.Count), testName); + Assert.That(expectedErrors.Count, Is.EqualTo(errs!.Count), testName); for (var i = 0; i < expectedErrors.Count; i++) { var actualError = errs[i]; diff --git a/Linguini.Bundle/ConcurrentBundle.cs b/Linguini.Bundle/ConcurrentBundle.cs index 7126a06..5c9daa7 100644 --- a/Linguini.Bundle/ConcurrentBundle.cs +++ b/Linguini.Bundle/ConcurrentBundle.cs @@ -15,10 +15,27 @@ namespace Linguini.Bundle { public sealed class ConcurrentBundle : FluentBundle, IEquatable { - internal ConcurrentDictionary FuncList = new(); + internal ConcurrentDictionary Functions = new(); private ConcurrentDictionary _terms = new(); private ConcurrentDictionary _messages = new(); + public static ConcurrentBundle Thaw(FrozenBundle frozenBundle) + { + return new ConcurrentBundle + { + _messages = new ConcurrentDictionary(frozenBundle.Messages), + Functions = new ConcurrentDictionary(frozenBundle.Functions), + _terms = new ConcurrentDictionary(frozenBundle.Terms), + FormatterFunc = frozenBundle.FormatterFunc, + Locales = frozenBundle.Locales, + UseIsolating = frozenBundle.UseIsolating, + MaxPlaceable = frozenBundle.MaxPlaceable, + EnableExtensions = frozenBundle.EnableExtensions, + TransformFunc = frozenBundle.TransformFunc, + Culture = frozenBundle.Culture + }; + } + /// protected override void AddMessageOverriding(AstMessage message) { @@ -53,19 +70,19 @@ protected override bool TryAddMessage(AstMessage message, List? err /// public override bool TryAddFunction(string funcName, ExternalFunction fluentFunction) { - return FuncList.TryAdd(funcName, fluentFunction); + return Functions.TryAdd(funcName, fluentFunction); } /// public override void AddFunctionOverriding(string funcName, ExternalFunction fluentFunction) { - FuncList[funcName] = fluentFunction; + Functions[funcName] = fluentFunction; } /// public override void AddFunctionUnchecked(string funcName, ExternalFunction fluentFunction) { - if (FuncList.TryAdd(funcName, fluentFunction)) return; + if (Functions.TryAdd(funcName, fluentFunction)) return; throw new ArgumentException($"Function with name {funcName} already exist"); } @@ -91,25 +108,40 @@ public override bool TryGetAstTerm(string ident, [NotNullWhen(true)] out AstTerm /// public override bool TryGetFunction(string funcName, [NotNullWhen(true)] out FluentFunction? function) { - return FuncList.TryGetValue(funcName, out function); + return Functions.TryGetValue(funcName, out function); } /// public override IEnumerable GetMessageEnumerable() { - return _messages.Keys.ToArray(); + return _messages.Keys; } /// public override IEnumerable GetFuncEnumerable() { - return FuncList.Keys.ToArray(); + return Functions.Keys; } /// public override IEnumerable GetTermEnumerable() { - return _terms.Keys.ToArray(); + return _terms.Keys; + } + + internal override IDictionary GetMessagesDictionary() + { + return _messages; + } + + internal override IDictionary GetTermsDictionary() + { + return _terms; + } + + internal override IDictionary GetFunctionDictionary() + { + return Functions; } /// @@ -117,7 +149,7 @@ public override FluentBundle DeepClone() { return new ConcurrentBundle { - FuncList = new ConcurrentDictionary(FuncList), + Functions = new ConcurrentDictionary(Functions), _terms = new ConcurrentDictionary(_terms), _messages = new ConcurrentDictionary(_messages), Culture = (CultureInfo)Culture.Clone(), @@ -135,7 +167,7 @@ public bool Equals(ConcurrentBundle? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && FuncList.SequenceEqual(other.FuncList) && _terms.SequenceEqual(other._terms) && + return base.Equals(other) && Functions.SequenceEqual(other.Functions) && _terms.SequenceEqual(other._terms) && _messages.SequenceEqual(other._messages); } @@ -148,7 +180,7 @@ public override bool Equals(object? obj) /// public override int GetHashCode() { - return HashCode.Combine(base.GetHashCode(), FuncList, _terms, _messages); + return HashCode.Combine(base.GetHashCode(), Functions, _terms, _messages); } } } \ No newline at end of file diff --git a/Linguini.Bundle/FluentBundle.cs b/Linguini.Bundle/FluentBundle.cs index 5a8521a..04ed59e 100644 --- a/Linguini.Bundle/FluentBundle.cs +++ b/Linguini.Bundle/FluentBundle.cs @@ -15,141 +15,137 @@ namespace Linguini.Bundle { - public interface IReadBundle + public abstract class FluentBundle : IEquatable, IReadBundle { /// - /// Determines if the provided identifier has a message associated with it. - /// The identifier to check. - /// True if the identifier has a message; otherwise, false. - /// - bool HasMessage(string identifier); + /// of the bundle. Primary bundle locale + /// + public CultureInfo Culture { get; internal set; } = CultureInfo.CurrentCulture; /// - /// Determines whether the given identifier with attribute has a message. + /// List of Locales. First element is primary bundle locale, others are fallback locales. /// - /// The identifier with attribute. - /// True if the identifier with attribute has a message; otherwise, false. - bool HasAttrMessage(string idWithAttr); + public List Locales { get; internal set; } = new(); /// - /// Retrieves the attribute message by processing the given message template with the provided arguments. + /// When formatting patterns, FluentBundle inserts Unicode Directionality Isolation Marks to indicate that the + /// direction of a placeable may differ from the surrounding message. + /// This is important for cases such as when a right-to-left user name is presented in the left-to-right message. /// - /// The string consisting of `messageId.Attribute`. - /// The dictionary of arguments to be used for resolution in the message template. Can be null. - /// The processed message. - /// Thrown when there are errors encountered during attribute substitution. - string? GetAttrMessage(string msgWithAttr, IDictionary? args = null); + public bool UseIsolating { get; set; } = true; - string? GetAttrMessage(string msgWithAttr, params (string, IFluentType)[] args); + /// + /// Specifies a method that will be applied only on values extending . Useful for defining a + /// special formatter for . + /// + public Func? TransformFunc { get; set; } - bool TryGetAttrMessage(string msgWithAttr, IDictionary? args, - [NotNullWhen(false)] out IList? errors, [NotNullWhen(true)] out string? message); + /// + /// Specifies a method that will be applied only on values extending . Useful for defining a + /// special formatter for . + /// + public Func? FormatterFunc { get; init; } - bool TryGetMessage(string id, IDictionary? args, - [NotNullWhen(false)] out IList? errors, [NotNullWhen(true)] out string? message); + /// + /// Limit of placeable within one , when fully expanded (all nested + /// elements count towards it). Useful for preventing billion laughs attack. Defaults to 100. + /// + public byte MaxPlaceable { get; internal init; } = 100; - bool TryGetMessage(string id, string? attribute, IDictionary? args, - [NotNullWhen(false)] out IList? errors, [NotNullWhen(true)] out string? message); + /// + /// Whether experimental features are enabled. + /// When `true` experimental features are enabled. Experimental features include stuff like: + /// + /// dynamic reference + /// dynamic reference attributes + /// term reference as parameters + /// + /// + // ReSharper disable once MemberCanBeProtected.Global + public bool EnableExtensions { get; init; } /// - /// Tries to get the AstMessage associated with the specified ident. + /// Determines if the provided identifier has a message associated with it. + /// + /// The identifier to check. + /// True if the identifier has a message; otherwise, false. + public abstract bool HasMessage(string identifier); + + /// + /// Tries to get the AstMessage associated with the specified ident. /// /// The identifier to look for. - /// When this method returns, contains the AstMessage associated with the specified ident, if found; otherwise, null. + /// + /// When this method returns, contains the AstMessage associated with the specified ident, if found; + /// otherwise, null. + /// /// True if an AstMessage was found for the specified ident; otherwise, false. - bool TryGetAstMessage(string ident, [NotNullWhen(true)] out AstMessage? message); + public abstract bool TryGetAstMessage(string ident, [NotNullWhen(true)] out AstMessage? message); /// - /// Tries to get a term by its identifier. + /// Tries to get a term by its identifier. /// /// The identifier of the AST term. - /// When this method returns, contains the AST term associated with the specified identifier, if the identifier is found; otherwise, null. This parameter is passed uninitialized. + /// + /// When this method returns, contains the AST term associated with the specified identifier, if the + /// identifier is found; otherwise, null. This parameter is passed uninitialized. + /// /// true if the identifier is found and the corresponding AST term is retrieved; otherwise, false. - bool TryGetAstTerm(string ident, [NotNullWhen(true)] out AstTerm? term); + public abstract bool TryGetAstTerm(string ident, [NotNullWhen(true)] out AstTerm? term); /// - /// Tries to get the FluentFunction associated with the specified Identifier. + /// Tries to get the FluentFunction associated with the specified Identifier. /// /// The Identifier used to identify the FluentFunction. - /// When the method returns, contains the FluentFunction associated with the Identifier, if the Identifier is found; otherwise, null. This parameter is passed uninitialized. + /// + /// When the method returns, contains the FluentFunction associated with the Identifier, if the + /// Identifier is found; otherwise, null. This parameter is passed uninitialized. + /// /// true if the FluentFunction associated with the Identifier is found; otherwise, false. - bool TryGetFunction(Identifier id, [NotNullWhen(true)] out FluentFunction? function); + public bool TryGetFunction(Identifier id, [NotNullWhen(true)] out FluentFunction? function) + { + return TryGetFunction(id.ToString(), out function); + } /// - /// Tries to retrieve a FluentFunction object by the given function name. + /// Tries to retrieve a FluentFunction object by the given function name. /// /// The name of the function to retrieve. /// An output parameter that will hold the retrieved FluentFunction object, if found. /// - /// True if a FluentFunction object with the specified name was found and assigned to the function output parameter. - /// False if a FluentFunction object with the specified name was not found. + /// True if a FluentFunction object with the specified name was found and assigned to the function output parameter. + /// False if a FluentFunction object with the specified name was not found. /// - bool TryGetFunction(string funcName, [NotNullWhen(true)] out FluentFunction? function); + public abstract bool TryGetFunction(string funcName, [NotNullWhen(true)] out FluentFunction? function); + + public string FormatPattern(Pattern pattern, IDictionary? args, + [NotNullWhen(false)] out IList? errors) + { + var scope = new Scope(this, args); + var value = pattern.Resolve(scope); + errors = scope.Errors; + return value.AsString(); + } /// - /// This method retrieves an enumerable collection of all message identifiers. + /// This method retrieves an enumerable collection of all message identifiers. + /// /// - /// An enumerable collection of message identifiers. - IEnumerable GetMessageEnumerable(); + /// An enumerable collection of message identifiers. + /// + public abstract IEnumerable GetMessageEnumerable(); /// - /// Retrieves an enumerable collection of string function names. + /// Retrieves an enumerable collection of string function names. /// /// An enumerable collection of functions names. - IEnumerable GetFuncEnumerable(); + public abstract IEnumerable GetFuncEnumerable(); /// - /// Retrieves an enumerable collection of terms. + /// Retrieves an enumerable collection of terms. /// /// An enumerable collection of terms. - IEnumerable GetTermEnumerable(); - } - - public abstract class FluentBundle : IEquatable, IReadBundle - { - /// - /// of the bundle. Primary bundle locale - /// - public CultureInfo Culture { get; internal set; } = CultureInfo.CurrentCulture; - - /// - /// List of Locales. First element is primary bundle locale, others are fallback locales. - /// - public List Locales { get; internal set; } = new(); - - /// - /// When formatting patterns, FluentBundle inserts Unicode Directionality Isolation Marks to indicate that the direction of a placeable may differ from the surrounding message. - /// This is important for cases such as when a right-to-left user name is presented in the left-to-right message. - /// - public bool UseIsolating { get; set; } = true; - - /// - /// Specifies a method that will be applied only on values extending . Useful for defining a special formatter for . - /// - public Func? TransformFunc { get; set; } - - /// - /// Specifies a method that will be applied only on values extending . Useful for defining a special formatter for . - /// - public Func? FormatterFunc { get; init; } - - /// - /// Limit of placeable within one , when fully expanded (all nested elements count towards it). Useful for preventing billion laughs attack. Defaults to 100. - /// - public byte MaxPlaceable { get; internal init; } = 100; - - /// - /// Whether experimental features are enabled. - /// - /// When `true` experimental features are enabled. Experimental features include stuff like: - /// - /// dynamic reference - /// dynamic reference attributes - /// term reference as parameters - /// - /// - // ReSharper disable once MemberCanBeProtected.Global - public bool EnableExtensions { get; init; } + public abstract IEnumerable GetTermEnumerable(); public bool AddResource(string input, [NotNullWhen(false)] out List? errors) { @@ -166,10 +162,7 @@ public bool AddResource(TextReader reader, [NotNullWhen(false)] out List? errors) { var innerErrors = new List(); - foreach (var parseError in res.Errors) - { - innerErrors.Add(ParserFluentError.ParseError(parseError)); - } + foreach (var parseError in res.Errors) innerErrors.Add(ParserFluentError.ParseError(parseError)); for (var entryPos = 0; entryPos < res.Entries.Count; entryPos++) { @@ -195,6 +188,19 @@ public bool AddResource(Resource res, [NotNullWhen(false)] out List return false; } + + /// + /// Adds the given AstMessage to the collection of messages, by overriding any existing messages with the same name. + /// + /// The AstMessage to be added. + protected abstract void AddMessageOverriding(AstMessage message); + + /// + /// Adds a term to the AstTerm list, overriding any existing term with the same name. + /// + /// The term to be added. + protected abstract void AddTermOverriding(AstTerm term); + private void InternalResourceOverriding(Resource resource) { for (var entryPos = 0; entryPos < resource.Entries.Count; entryPos++) @@ -214,20 +220,8 @@ private void InternalResourceOverriding(Resource resource) } /// - /// Adds the given AstMessage to the collection of messages, by overriding any existing messages with the same name. - /// - /// The AstMessage to be added. - protected abstract void AddMessageOverriding(AstMessage message); - - /// - /// Adds a term to the AstTerm list, overriding any existing term with the same name. - /// - /// The term to be added. - protected abstract void AddTermOverriding(AstTerm term); - - /// - /// Adds a resource. - /// Any messages or terms in bundle will be overriden by the existing ones. + /// Adds a resource. + /// Any messages or terms in bundle will be overriden by the existing ones. /// /// The input string containing the resource data. public void AddResourceOverriding(string input) @@ -252,10 +246,20 @@ public void AddResourceOverriding(TextReader input) public abstract void AddFunctionOverriding(string funcName, ExternalFunction fluentFunction); public abstract void AddFunctionUnchecked(string funcName, ExternalFunction fluentFunction); + + internal abstract IDictionary GetMessagesDictionary(); + internal abstract IDictionary GetTermsDictionary(); + internal abstract IDictionary GetFunctionDictionary(); + + /// + /// Creates a deep clone of the current instance of the AbstractFluentBundle class. + /// + /// A new instance of the AbstractFluentBundle class that is a deep clone of the current instance. + public abstract FluentBundle DeepClone(); /// - /// Adds a collection of external functions to the dictionary of available functions. - /// If any function cannot be added, the errors are returned in the list. + /// Adds a collection of external functions to the dictionary of available functions. + /// If any function cannot be added, the errors are returned in the list. /// /// A dictionary of function names and their corresponding ExternalFunction objects. /// A list of errors indicating functions that could not be added. @@ -263,197 +267,14 @@ public void AddFunctions(IDictionary functions, out Li { errors = new List(); foreach (var keyValue in functions) - { if (!TryAddFunction(keyValue.Key, keyValue.Value)) - { errors.Add(new OverrideFluentError(keyValue.Key, EntryKind.Func)); - } - } } - /// - /// Determines if the provided identifier has a message associated with it. - /// The identifier to check. - /// True if the identifier has a message; otherwise, false. - /// - public abstract bool HasMessage(string identifier); - - /// - /// Determines whether the given identifier with attribute has a message. - /// - /// The identifier with attribute. - /// True if the identifier with attribute has a message; otherwise, false. - public bool HasAttrMessage(string idWithAttr) - { - var attributes = idWithAttr.IndexOf('.'); - if (attributes < 0) - { - return HasMessage(idWithAttr); - } - - var id = idWithAttr.AsSpan(0, attributes).ToString(); - var attr = idWithAttr.AsSpan(attributes + 1).ToString(); - if (TryGetAstMessage(id, out var astMessage)) - { - return astMessage.GetAttribute(attr) != null; - } - - return false; - } - - /// - /// Retrieves the attribute message by processing the given message template with the provided arguments. - /// - /// The string consisting of `messageId.Attribute`. - /// The dictionary of arguments to be used for resolution in the message template. Can be null. - /// The processed message. - /// Thrown when there are errors encountered during attribute substitution. - public string? GetAttrMessage(string msgWithAttr, IDictionary? args = null) - { - TryGetAttrMessage(msgWithAttr, args, out var errors, out var message); - if (errors is { Count: > 0 }) - { - throw new LinguiniException(errors); - } - - return message; - } - - public string? GetAttrMessage(string msgWithAttr, params (string, IFluentType)[] args) - { - var dictionary = new Dictionary(args.Length); - foreach (var (key, val) in args) - { - dictionary.Add(key, val); - } - - TryGetAttrMessage(msgWithAttr, dictionary, out var errors, out var message); - if (errors is { Count: > 0 }) - { - throw new LinguiniException(errors); - } - return message; - } - - public bool TryGetAttrMessage(string msgWithAttr, IDictionary? args, - [NotNullWhen(false)] out IList? errors, [NotNullWhen(true)] out string? message) - { - if (msgWithAttr.Contains(".")) - { - var split = msgWithAttr.Split('.'); - return TryGetMessage(split[0], split[1], args, out errors, out message); - } - - return TryGetMessage(msgWithAttr, null, args, out errors, out message); - } - - public bool TryGetMessage(string id, IDictionary? args, - [NotNullWhen(false)] out IList? errors, [NotNullWhen(true)] out string? message) - => TryGetMessage(id, null, args, out errors, out message); - - public bool TryGetMessage(string id, string? attribute, IDictionary? args, - [NotNullWhen(false)] out IList? errors, [NotNullWhen(true)] out string? message) - { - string? value = null; - errors = new List(); - - if (TryGetAstMessage(id, out var astMessage)) - { - var pattern = attribute != null - ? TypeHelpers.GetAttribute(astMessage, attribute)?.Value - : astMessage.Value; - - if (pattern == null) - { - var msg = (attribute == null) - ? id - : $"{id}.{attribute}"; - errors.Add(ResolverFluentError.NoValue($"{msg}")); - message = FluentNone.None.ToString(); - return false; - } - - value = FormatPattern(pattern, args, out errors); - } - - message = value; - return message != null; - } - - /// - /// Tries to get the AstMessage associated with the specified ident. - /// - /// The identifier to look for. - /// When this method returns, contains the AstMessage associated with the specified ident, if found; otherwise, null. - /// True if an AstMessage was found for the specified ident; otherwise, false. - public abstract bool TryGetAstMessage(string ident, [NotNullWhen(true)] out AstMessage? message); /// - /// Tries to get a term by its identifier. - /// - /// The identifier of the AST term. - /// When this method returns, contains the AST term associated with the specified identifier, if the identifier is found; otherwise, null. This parameter is passed uninitialized. - /// true if the identifier is found and the corresponding AST term is retrieved; otherwise, false. - public abstract bool TryGetAstTerm(string ident, [NotNullWhen(true)] out AstTerm? term); - - /// - /// Tries to get the FluentFunction associated with the specified Identifier. - /// - /// The Identifier used to identify the FluentFunction. - /// When the method returns, contains the FluentFunction associated with the Identifier, if the Identifier is found; otherwise, null. This parameter is passed uninitialized. - /// true if the FluentFunction associated with the Identifier is found; otherwise, false. - public bool TryGetFunction(Identifier id, [NotNullWhen(true)] out FluentFunction? function) - { - return TryGetFunction(id.ToString(), out function); - } - - /// - /// Tries to retrieve a FluentFunction object by the given function name. - /// - /// The name of the function to retrieve. - /// An output parameter that will hold the retrieved FluentFunction object, if found. - /// - /// True if a FluentFunction object with the specified name was found and assigned to the function output parameter. - /// False if a FluentFunction object with the specified name was not found. - /// - public abstract bool TryGetFunction(string funcName, [NotNullWhen(true)] out FluentFunction? function); - - public string FormatPattern(Pattern pattern, IDictionary? args, - [NotNullWhen(false)] out IList? errors) - { - var scope = new Scope(this, args); - var value = pattern.Resolve(scope); - errors = scope.Errors; - return value.AsString(); - } - - /// - /// This method retrieves an enumerable collection of all message identifiers. - /// - /// An enumerable collection of message identifiers. - public abstract IEnumerable GetMessageEnumerable(); - - /// - /// Retrieves an enumerable collection of string function names. - /// - /// An enumerable collection of functions names. - public abstract IEnumerable GetFuncEnumerable(); - - /// - /// Retrieves an enumerable collection of terms. - /// - /// An enumerable collection of terms. - public abstract IEnumerable GetTermEnumerable(); - - /// - /// Creates a deep clone of the current instance of the AbstractFluentBundle class. - /// - /// A new instance of the AbstractFluentBundle class that is a deep clone of the current instance. - public abstract FluentBundle DeepClone(); - - /// - /// Creates a FluentBundle object with the specified options. + /// Creates a FluentBundle object with the specified options. /// /// The FluentBundleOption object that contains the options for creating the FluentBundle /// A FluentBundle object created with the specified options @@ -475,7 +296,7 @@ public static FluentBundle MakeUnchecked(FluentBundleOption option) TransformFunc = option.TransformFunc, MaxPlaceable = option.MaxPlaceable, UseIsolating = option.UseIsolating, - FuncList = new ConcurrentDictionary(func), + Functions = new ConcurrentDictionary(func) }, _ => new NonConcurrentBundle { @@ -486,12 +307,21 @@ public static FluentBundle MakeUnchecked(FluentBundleOption option) TransformFunc = option.TransformFunc, MaxPlaceable = option.MaxPlaceable, UseIsolating = option.UseIsolating, - FuncList = func, + Functions = func } }; } - /// + /// + /// Converts the current object to a . + /// + /// A new instance of FrozenBundle. + public FrozenBundle ToFrozenBundle() + { + return new FrozenBundle(this); + } + + /// public bool Equals(FluentBundle? other) { if (ReferenceEquals(null, other)) return false; @@ -501,8 +331,8 @@ public bool Equals(FluentBundle? other) Equals(FormatterFunc, other.FormatterFunc) && MaxPlaceable == other.MaxPlaceable && EnableExtensions == other.EnableExtensions; } - - /// + + /// public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) return false; @@ -511,7 +341,7 @@ public override bool Equals(object? obj) return Equals((FluentBundle)obj); } - /// + /// public override int GetHashCode() { return HashCode.Combine(Culture, Locales, UseIsolating, TransformFunc, FormatterFunc, MaxPlaceable, diff --git a/Linguini.Bundle/FrozenBundle.cs b/Linguini.Bundle/FrozenBundle.cs index ff8c8de..ed5b3dc 100644 --- a/Linguini.Bundle/FrozenBundle.cs +++ b/Linguini.Bundle/FrozenBundle.cs @@ -1,90 +1,167 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using Linguini.Bundle.Errors; +using Linguini.Bundle.Resolver; using Linguini.Bundle.Types; using Linguini.Shared.Types.Bundle; using Linguini.Syntax.Ast; +#if NET8_0_OR_GREATER +using System.Collections.Frozen; +#elif NET6_0_OR_GREATER +using System.Collections.Immutable; +#endif + namespace Linguini.Bundle { + /// + /// Represents a frozen bundle i.e. a bundle to which no items can be added or removed. + /// + /// It properly works on net8.0 utilizing its FrozenDictionary implementation. + /// On other platforms it uses a very naive polyfill. + /// Frozen bundle implements the interface. + /// public class FrozenBundle : IReadBundle { -#if NET8_0_OR_GREATER - -#else + /// + /// of the bundle. Primary bundle locale + /// + public CultureInfo Culture { get; } -#endif - public bool HasMessage(string identifier) - { - throw new System.NotImplementedException(); - } + /// + /// List of Locales. First element is primary bundle locale, others are fallback locales. + /// + public List Locales { get; init; } - public bool HasAttrMessage(string idWithAttr) - { - throw new System.NotImplementedException(); - } + /// + /// When formatting patterns, FluentBundle inserts Unicode Directionality Isolation Marks to indicate that the direction of a placeable may differ from the surrounding message. + /// This is important for cases such as when a right-to-left user name is presented in the left-to-right message. + /// + public bool UseIsolating { get; } - public string? GetAttrMessage(string msgWithAttr, IDictionary? args = null) + /// + /// Specifies a method that will be applied only on values extending . Useful for defining a special formatter for . + /// + public Func? FormatterFunc { get; } + + /// + /// Limit of placeable within one , when fully expanded (all nested elements count towards it). Useful for preventing billion laughs attack. Defaults to 100. + /// + public byte MaxPlaceable { get; } + + /// + /// Whether experimental features are enabled. + /// + /// When `true` experimental features are enabled. Experimental features include stuff like: + /// + /// dynamic reference + /// dynamic reference attributes + /// term reference as parameters + /// + /// + // ReSharper disable once MemberCanBeProtected.Global + public bool EnableExtensions { get; init; } + + /// + /// Specifies a method that will be applied only on values extending . Useful for defining a special formatter for . + /// + public Func? TransformFunc { get; } + + internal readonly IDictionary Functions; + internal readonly IDictionary Terms; + internal readonly IDictionary Messages; +#if NET8_0_OR_GREATER + internal FrozenBundle(FluentBundle bundle) { - throw new System.NotImplementedException(); + Culture = bundle.Culture; + MaxPlaceable = bundle.MaxPlaceable; + Locales = bundle.Locales; + UseIsolating = bundle.UseIsolating; + EnableExtensions = bundle.EnableExtensions; + FormatterFunc = bundle.FormatterFunc; + TransformFunc = bundle.TransformFunc; + Messages = bundle.GetMessagesDictionary().ToFrozenDictionary(); + Terms = bundle.GetTermsDictionary().ToFrozenDictionary(); + Functions = bundle.GetFunctionDictionary().ToFrozenDictionary(); } - - public string? GetAttrMessage(string msgWithAttr, params (string, IFluentType)[] args) +#elif NET6_0_OR_GREATER + internal FrozenBundle(FluentBundle bundle) { - throw new System.NotImplementedException(); + Culture = bundle.Culture; + MaxPlaceable = bundle.MaxPlaceable; + Locales = bundle.Locales; + UseIsolating = bundle.UseIsolating; + EnableExtensions = bundle.EnableExtensions; + FormatterFunc = bundle.FormatterFunc; + TransformFunc = bundle.TransformFunc; + Messages = bundle.GetMessagesDictionary().ToImmutableDictionary(); + Terms = bundle.GetTermsDictionary().ToImmutableDictionary(); + Functions = bundle.GetFunctionDictionary().ToImmutableDictionary(); } - - public bool TryGetAttrMessage(string msgWithAttr, IDictionary? args, - [NotNullWhen(false)] out IList? errors, [NotNullWhen(true)] out string? message) +#else + internal FrozenBundle(FluentBundle bundle) { - throw new System.NotImplementedException(); + Culture = bundle.Culture; + MaxPlaceable = bundle.MaxPlaceable; + Locales = bundle.Locales; + UseIsolating = bundle.UseIsolating; + EnableExtensions = bundle.EnableExtensions; + FormatterFunc = bundle.FormatterFunc; + TransformFunc = bundle.TransformFunc; + Messages = new Dictionary(bundle.GetMessagesDictionary()); + Terms = new Dictionary(bundle.GetTermsDictionary()); + Functions = new Dictionary(bundle.GetFunctionDictionary()); } - - public bool TryGetMessage(string id, IDictionary? args, out IList errors, - [NotNullWhen(true)] out string? message) +#endif + public bool HasMessage(string identifier) { - throw new System.NotImplementedException(); + return Messages.ContainsKey(identifier); } - public bool TryGetMessage(string id, string? attribute, IDictionary? args, - [NotNullWhen(false)] out IList? errors, [NotNullWhen(true)] out string? message) + public string FormatPattern(Pattern pattern, IDictionary? args, + [NotNullWhen(false)] out IList? errors) { - throw new System.NotImplementedException(); + var scope = new Scope(this, args); + var value = pattern.Resolve(scope); + errors = scope.Errors; + return value.AsString(); } public bool TryGetAstMessage(string ident, [NotNullWhen(true)] out AstMessage? message) { - throw new System.NotImplementedException(); + return Messages.TryGetValue(ident, out message); } public bool TryGetAstTerm(string ident, [NotNullWhen(true)] out AstTerm? term) { - throw new System.NotImplementedException(); + return Terms.TryGetValue(ident, out term); } public bool TryGetFunction(Identifier id, [NotNullWhen(true)] out FluentFunction? function) { - throw new System.NotImplementedException(); + return Functions.TryGetValue(id.ToString(), out function); } public bool TryGetFunction(string funcName, [NotNullWhen(true)] out FluentFunction? function) { - throw new System.NotImplementedException(); + return Functions.TryGetValue(funcName, out function); } public IEnumerable GetMessageEnumerable() { - throw new System.NotImplementedException(); + return Messages.Keys; } public IEnumerable GetFuncEnumerable() { - throw new System.NotImplementedException(); + return Functions.Keys; } public IEnumerable GetTermEnumerable() { - throw new System.NotImplementedException(); + return Terms.Keys; } } } \ No newline at end of file diff --git a/Linguini.Bundle/IReadBundle.cs b/Linguini.Bundle/IReadBundle.cs new file mode 100644 index 0000000..e4415f5 --- /dev/null +++ b/Linguini.Bundle/IReadBundle.cs @@ -0,0 +1,275 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Linguini.Bundle.Errors; +using Linguini.Bundle.Types; +using Linguini.Shared.Types.Bundle; +using Linguini.Syntax.Ast; + +namespace Linguini.Bundle +{ + public interface IReadBundle + { + /// + /// Determines if the provided identifier has a message associated with it. + /// The identifier to check. + /// True if the identifier has a message; otherwise, false. + /// + bool HasMessage(string identifier); + + string FormatPattern(Pattern pattern, IDictionary? args, + [NotNullWhen(false)] out IList? errors); + + /// + /// Tries to get the AstMessage associated with the specified ident. + /// + /// The identifier to look for. + /// When this method returns, contains the AstMessage associated with the specified ident, if found; otherwise, null. + /// True if an AstMessage was found for the specified ident; otherwise, false. + bool TryGetAstMessage(string ident, [NotNullWhen(true)] out AstMessage? message); + + /// + /// Tries to get a term by its identifier. + /// + /// The identifier of the AST term. + /// When this method returns, contains the AST term associated with the specified identifier, if the identifier is found; otherwise, null. This parameter is passed uninitialized. + /// true if the identifier is found and the corresponding AST term is retrieved; otherwise, false. + bool TryGetAstTerm(string ident, [NotNullWhen(true)] out AstTerm? term); + + /// + /// Tries to get the FluentFunction associated with the specified Identifier. + /// + /// The Identifier used to identify the FluentFunction. + /// When the method returns, contains the FluentFunction associated with the Identifier, if the Identifier is found; otherwise, null. This parameter is passed uninitialized. + /// true if the FluentFunction associated with the Identifier is found; otherwise, false. + bool TryGetFunction(Identifier id, [NotNullWhen(true)] out FluentFunction? function); + + /// + /// Tries to retrieve a FluentFunction object by the given function name. + /// + /// The name of the function to retrieve. + /// An output parameter that will hold the retrieved FluentFunction object, if found. + /// + /// True if a FluentFunction object with the specified name was found and assigned to the function output parameter. + /// False if a FluentFunction object with the specified name was not found. + /// + bool TryGetFunction(string funcName, [NotNullWhen(true)] out FluentFunction? function); + + /// + /// This method retrieves an enumerable collection of all message identifiers. + /// + /// An enumerable collection of message identifiers. + IEnumerable GetMessageEnumerable(); + + /// + /// Retrieves an enumerable collection of string function names. + /// + /// An enumerable collection of functions names. + IEnumerable GetFuncEnumerable(); + + /// + /// Retrieves an enumerable collection of terms. + /// + /// An enumerable collection of terms. + IEnumerable GetTermEnumerable(); + + /// + /// Tries to retrieve a message based on the provided ID and arguments. + /// A convenience method for + /// + /// The ID of the message to retrieve. + /// Optional. A dictionary of arguments to be inserted into the message. + /// Optional. When the method returns false, a list of errors encountered during the retrieval process. + /// Optional. When the method returns true, the retrieved message. Null if no message is found. + /// + /// True if the message was successfully retrieved. False otherwise. + /// + bool TryGetMessage(string id, IDictionary? args, + [NotNullWhen(false)] out IList? errors, [NotNullWhen(true)] out string? message) + { + return this.TryGetMessage(id, null, args, out errors, out message); + } + + + /// + /// Determines whether the given identifier with attribute has a message. + /// + /// The identifier with attribute. + /// True if the identifier with attribute has a message; otherwise, false. + bool HasAttrMessage(string idWithAttr) + { + var attributes = idWithAttr.IndexOf('.'); + if (attributes < 0) + { + return HasMessage(idWithAttr); + } + + var id = idWithAttr.AsSpan(0, attributes).ToString(); + var attr = idWithAttr.AsSpan(attributes + 1).ToString(); + if (TryGetAstMessage(id, out var astMessage)) + { + return astMessage.GetAttribute(attr) != null; + } + + return false; + } + + /// + /// Retrieves the attribute message by processing the given message template with the provided arguments. + /// + /// The string consisting of `messageId.Attribute`. + /// The dictionary of arguments to be used for resolution in the message template. Can be null. + /// The processed message. + /// Thrown when there are errors encountered during attribute substitution. + string? GetAttrMessage(string msgWithAttr, params (string, IFluentType)[] args) + { + var dictionary = new Dictionary(args.Length); + foreach (var (key, val) in args) + { + dictionary.Add(key, val); + } + + TryGetAttrMessage(msgWithAttr, dictionary, out var errors, out var message); + if (errors is { Count: > 0 }) + { + throw new LinguiniException(errors); + } + + return message; + } + + /// + /// Tries to retrieve an attribute message. + /// + /// The message with attribute. + /// The arguments passed with the message. + /// The list of errors that occurred during the message retrieval process. + /// The retrieved message. + /// True if the attribute message is found; otherwise, false. + bool TryGetAttrMessage(string msgWithAttr, IDictionary? args, + [NotNullWhen(false)] out IList? errors, [NotNullWhen(true)] out string? message) + { + if (msgWithAttr.Contains(".")) + { + var split = msgWithAttr.Split('.'); + return TryGetMessage(split[0], split[1], args, out errors, out message); + } + + return TryGetMessage(msgWithAttr, null, args, out errors, out message); + } + + /// + /// Tries to get a message based on the provided parameters. + /// + /// The identifier of the message. + /// The attribute of the message. + /// The arguments to format the message with. + /// The list of errors that occurred during the message retrieval process. + /// The retrieved message. + /// True if the message was successfully retrieved, otherwise false. + public bool TryGetMessage(string id, string? attribute, IDictionary? args, + [NotNullWhen(false)] out IList? errors, [NotNullWhen(true)] out string? message) + { + string? value = null; + errors = new List(); + + if (TryGetAstMessage(id, out var astMessage)) + { + var pattern = attribute != null + ? astMessage.GetAttribute(attribute)?.Value + : astMessage.Value; + + if (pattern == null) + { + var msg = attribute == null + ? id + : $"{id}.{attribute}"; + errors.Add(ResolverFluentError.NoValue($"{msg}")); + message = FluentNone.None.ToString(); + return false; + } + + value = FormatPattern(pattern, args, out errors); + } + + message = value; + return message != null; + } + } + + public static class ReadBundleExtensions + { + /// + /// Convenience method for + /// + /// The bundle to retrieve the message from. + /// The identifier with attribute. + /// True if the identifier with attribute has a message; otherwise, false. + public static bool HasAttrMessage(this IReadBundle bundle, string idWithAttr) + { + return bundle.HasAttrMessage(idWithAttr); + } + + /// + /// Convenience method for + /// + /// The bundle to retrieve the message from. + /// The message with attribute to retrieve. + /// Optional arguments to format the message. + /// The attribute message from the read bundle. + public static string? GetAttrMessage(this IReadBundle bundle, string msgWithAttr, + params (string, IFluentType)[] args) + { + return bundle.GetAttrMessage(msgWithAttr, args); + } + + /// + /// Convenience method for + /// + /// The bundle to retrieve the message from. + /// The message with attribute + /// Optional arguments to be passed to the attribute message. + /// When this method returns, contains any errors that occured during retrieval, if any. + /// When this method returns, contains the retrieved attribute message, if it exists. + /// true if the attribute message was successfully retrieved; otherwise, false. + public static bool TryGetAttrMessage(this IReadBundle bundle, string msgWithAttr, + IDictionary? args, + [NotNullWhen(false)] out IList? errors, [NotNullWhen(true)] out string? message) + { + return bundle.TryGetAttrMessage(msgWithAttr, args, out errors, out message); + } + + /// + /// Convenience method for + /// + /// The bundle to retrieve the message from. + /// The identifier of the message. + /// The attribute of the message (optional). + /// The arguments for the message (optional). + /// When this method returns false, contains a list of errors that occurred while trying to retrieve the message; otherwise, null. + /// When this method returns true, contains the retrieved message; otherwise, null. + /// True if the message was successfully retrieved, otherwise false. + public static bool TryGetMessage(this IReadBundle bundle, string id, string? attribute, + IDictionary? args, + [NotNullWhen(false)] out IList? errors, [NotNullWhen(true)] out string? message) + { + return bundle.TryGetMessage(id, attribute, args, out errors, out message); + } + + /// + /// Tries to get a message from the specified bundle. + /// + /// The bundle from which to retrieve the message. + /// The identifier of the message to retrieve. + /// Optional arguments used to format the message (optional). + /// When this method returns false, contains a list of errors that occurred while trying to retrieve the message; otherwise, null. + /// When this method returns true, contains the retrieved message; otherwise, null. + /// True if the message was successfully retrieved; otherwise, false. + public static bool TryGetMessage(this IReadBundle bundle, string id, + IDictionary? args, + [NotNullWhen(false)] out IList? errors, [NotNullWhen(true)] out string? message) + { + return bundle.TryGetMessage(id, null, args, out errors, out message); + } + } +} \ No newline at end of file diff --git a/Linguini.Bundle/Linguini.Bundle.csproj b/Linguini.Bundle/Linguini.Bundle.csproj index 565736f..9915581 100644 --- a/Linguini.Bundle/Linguini.Bundle.csproj +++ b/Linguini.Bundle/Linguini.Bundle.csproj @@ -19,7 +19,7 @@ It provides easy to use and extend system for describing translations.https://github.com/Ygg01/Linguini git 0.7.0 - net6.0;netstandard2.1;net8.0 + net8.0;netstandard2.1;net6.0 linguini.jpg README.md diff --git a/Linguini.Bundle/NonConcurrentBundle.cs b/Linguini.Bundle/NonConcurrentBundle.cs index f8c7d2c..7f35070 100644 --- a/Linguini.Bundle/NonConcurrentBundle.cs +++ b/Linguini.Bundle/NonConcurrentBundle.cs @@ -14,7 +14,7 @@ namespace Linguini.Bundle { public sealed class NonConcurrentBundle : FluentBundle, IEquatable { - internal Dictionary FuncList = new(); + internal Dictionary Functions = new(); private Dictionary _terms = new(); private Dictionary _messages = new(); @@ -52,19 +52,19 @@ protected override bool TryAddMessage(AstMessage message, List? err /// public override bool TryAddFunction(string funcName, ExternalFunction fluentFunction) { - return FuncList.TryAdd(funcName, fluentFunction); + return Functions.TryAdd(funcName, fluentFunction); } /// public override void AddFunctionOverriding(string funcName, ExternalFunction fluentFunction) { - FuncList[funcName] = fluentFunction; + Functions[funcName] = fluentFunction; } /// public override void AddFunctionUnchecked(string funcName, ExternalFunction fluentFunction) { - FuncList.Add(funcName, fluentFunction); + Functions.Add(funcName, fluentFunction); } /// @@ -89,7 +89,7 @@ public override bool TryGetAstTerm(string ident, [NotNullWhen(true)] out AstTerm /// public override bool TryGetFunction(string funcName, [NotNullWhen(true)] out FluentFunction? function) { - return FuncList.TryGetValue(funcName, out function); + return Functions.TryGetValue(funcName, out function); } /// @@ -101,7 +101,7 @@ public override IEnumerable GetMessageEnumerable() /// public override IEnumerable GetFuncEnumerable() { - return FuncList.Keys.ToArray(); + return Functions.Keys.ToArray(); } /// @@ -110,13 +110,28 @@ public override IEnumerable GetTermEnumerable() return _terms.Keys.ToArray(); } + internal override IDictionary GetMessagesDictionary() + { + return _messages; + } + + internal override IDictionary GetTermsDictionary() + { + return _terms; + } + + internal override IDictionary GetFunctionDictionary() + { + return Functions; + } + /// public override FluentBundle DeepClone() { return new NonConcurrentBundle() { - FuncList = new Dictionary(FuncList), + Functions = new Dictionary(Functions), _terms = new Dictionary(_terms), _messages = new Dictionary(_messages), Culture = (CultureInfo)Culture.Clone(), @@ -128,12 +143,29 @@ public override FluentBundle DeepClone() EnableExtensions = EnableExtensions, }; } + + public static NonConcurrentBundle Thaw(FrozenBundle frozenBundle) + { + return new NonConcurrentBundle + { + _messages = new Dictionary(frozenBundle.Messages), + Functions = new Dictionary(frozenBundle.Functions), + _terms = new Dictionary(frozenBundle.Terms), + FormatterFunc = frozenBundle.FormatterFunc, + Locales = frozenBundle.Locales, + UseIsolating = frozenBundle.UseIsolating, + MaxPlaceable = frozenBundle.MaxPlaceable, + EnableExtensions = frozenBundle.EnableExtensions, + TransformFunc = frozenBundle.TransformFunc, + Culture = frozenBundle.Culture + }; + } public bool Equals(NonConcurrentBundle? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && FuncList.SequenceEqual(other.FuncList) && _terms.SequenceEqual(other._terms) && + return base.Equals(other) && Functions.SequenceEqual(other.Functions) && _terms.SequenceEqual(other._terms) && _messages.SequenceEqual(other._messages); } @@ -144,7 +176,7 @@ public override bool Equals(object? obj) public override int GetHashCode() { - return HashCode.Combine(base.GetHashCode(), FuncList, _terms, _messages); + return HashCode.Combine(base.GetHashCode(), Functions, _terms, _messages); } } } \ No newline at end of file diff --git a/Linguini.Bundle/Resolver/ResolverHelpers.cs b/Linguini.Bundle/Resolver/ResolverHelpers.cs index f14f093..aa8eabc 100644 --- a/Linguini.Bundle/Resolver/ResolverHelpers.cs +++ b/Linguini.Bundle/Resolver/ResolverHelpers.cs @@ -21,7 +21,7 @@ public static IFluentType Resolve(this Pattern self, Scope scope) { if (self.Elements[0] is TextLiteral textLiteral) { - return GetFluentString(textLiteral.ToString(), scope.Bundle.TransformFunc); + return GetFluentString(textLiteral.ToString(), scope.TransformFunc); } } diff --git a/Linguini.Bundle/Resolver/Scope.cs b/Linguini.Bundle/Resolver/Scope.cs index 00c3784..b979730 100644 --- a/Linguini.Bundle/Resolver/Scope.cs +++ b/Linguini.Bundle/Resolver/Scope.cs @@ -1,7 +1,7 @@ -#nullable enable -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.IO; using Linguini.Bundle.Errors; using Linguini.Shared.Types; @@ -12,7 +12,9 @@ namespace Linguini.Bundle.Resolver { public class Scope : IScope { - public readonly FluentBundle Bundle; + public readonly IReadBundle Bundle; + private readonly CultureInfo _culture; + private readonly int _maxPlaceable; private readonly Dictionary? _args; private Dictionary? _localNameArgs; private List? _localPosArgs; @@ -24,6 +26,12 @@ public Scope(FluentBundle fluentBundle, IDictionary? args) { Placeable = 0; Bundle = fluentBundle; + _maxPlaceable = fluentBundle.MaxPlaceable; + _culture = fluentBundle.Culture; + TransformFunc = fluentBundle.TransformFunc; + FormatterFunc = fluentBundle.FormatterFunc; + UseIsolating = fluentBundle.UseIsolating; + FormatterFunc = fluentBundle.FormatterFunc; Dirty = false; _errors = new List(); @@ -35,6 +43,28 @@ public Scope(FluentBundle fluentBundle, IDictionary? args) _errors = new List(); } + + public Scope(FrozenBundle frozenBundle, IDictionary? args) + { + Placeable = 0; + Bundle = frozenBundle; + _maxPlaceable = frozenBundle.MaxPlaceable; + _culture = frozenBundle.Culture; + TransformFunc = frozenBundle.TransformFunc; + FormatterFunc = frozenBundle.FormatterFunc; + UseIsolating = frozenBundle.UseIsolating; + Dirty = false; + + _errors = new List(); + _travelled = new List(); + _args = args != null ? new Dictionary(args) : null; + + _localNameArgs = null; + _localPosArgs = null; + _errors = new List(); + } + + public bool Dirty { get; set; } private short Placeable { get; set; } @@ -44,10 +74,16 @@ public Scope(FluentBundle fluentBundle, IDictionary? args) public IReadOnlyList? LocalPosArgs => _localPosArgs; public IReadOnlyDictionary? Args => _args; + public Func? FormatterFunc { get; } + public Func? TransformFunc { get; } + + + public bool UseIsolating { get; init; } + public bool IncrPlaceable() { - return ++Placeable <= Bundle.MaxPlaceable; + return ++Placeable <= _maxPlaceable; } public void AddError(ResolverFluentError resolverFluentError) @@ -171,7 +207,7 @@ public void ClearLocalArgs() public PluralCategory GetPluralRules(RuleType type, FluentNumber number) { - return ResolverHelpers.PluralRules.GetPluralCategory(Bundle.Culture, type, number); + return ResolverHelpers.PluralRules.GetPluralCategory(_culture, type, number); } public string ResolveReference(Identifier refId) diff --git a/Linguini.Bundle/Resolver/WriterHelpers.cs b/Linguini.Bundle/Resolver/WriterHelpers.cs index 65fd330..7f7f18c 100644 --- a/Linguini.Bundle/Resolver/WriterHelpers.cs +++ b/Linguini.Bundle/Resolver/WriterHelpers.cs @@ -14,7 +14,7 @@ public static class WriterHelpers public static void Write(this Pattern pattern, TextWriter writer, Scope scope) { var len = pattern.Elements.Count; - var transformFunc = scope.Bundle.TransformFunc; + var transformFunc = scope.TransformFunc; var placeablePos = 0; for (var i = 0; i < len; i++) { @@ -46,7 +46,7 @@ public static void Write(this Pattern pattern, TextWriter writer, Scope scope) return; } - var needsIsolating = scope.Bundle.UseIsolating + var needsIsolating = scope.UseIsolating && len > 1; if (needsIsolating) @@ -215,9 +215,9 @@ private static void Write(this IInlineExpression self, TextWriter writer, Scope private static void Write(this IFluentType self, TextWriter writer, Scope scope) { - if (scope.Bundle.FormatterFunc != null) + if (scope.FormatterFunc != null) { - writer.Write(scope.Bundle.FormatterFunc(self)); + writer.Write(scope.FormatterFunc(self)); } writer.Write(self.AsString());