Browse Source

Simplified language system.

pull/479/head
Sebastian 7 years ago
parent
commit
12fb5914d4
  1. 15
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguageConfig.cs
  2. 34
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguagesConfig.cs
  3. 53
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs
  4. 218
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs
  5. 22
      backend/src/Squidex.Domain.Apps.Core.Model/IFieldPartitionItem.cs
  6. 16
      backend/src/Squidex.Domain.Apps.Core.Model/IFieldPartitioning.cs
  7. 57
      backend/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs
  8. 35
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs
  9. 14
      backend/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs
  10. 12
      backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs
  11. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs
  12. 14
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs
  13. 17
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs
  14. 2
      backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs
  15. 67
      backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs
  16. 2
      backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs
  17. 10
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs
  18. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs
  19. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs
  20. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs
  21. 18
      backend/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs
  22. 20
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs
  23. 7
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs
  24. 14
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigJsonTests.cs
  25. 260
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs
  26. 36
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/InvariantPartitionTests.cs
  27. 11
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs
  28. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs
  29. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs
  30. 4
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs
  31. 9
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs
  32. 28
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppLanguagesTests.cs
  33. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs
  34. 30
      backend/tools/TestSuite/TestSuite.ApiTests/AppTests.cs

15
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguageConfig.cs

@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Core.Apps.Json
public class JsonLanguageConfig
{
[JsonProperty]
public Language[] Fallback { get; set; }
public Language[]? Fallback { get; set; }
[JsonProperty]
public bool IsOptional { get; set; }
@ -28,12 +28,19 @@ namespace Squidex.Domain.Apps.Core.Apps.Json
{
SimpleMapper.Map(config, this);
Fallback = config.LanguageFallbacks.ToArray();
Fallback = config.Fallbacks.ToArray();
}
public LanguageConfig ToConfig(string language)
public LanguageConfig ToConfig()
{
return new LanguageConfig(language, IsOptional, Fallback);
if (!IsOptional && (Fallback == null || Fallback.Length == 0))
{
return LanguageConfig.Default;
}
else
{
return new LanguageConfig(IsOptional, Fallback);
}
}
}
}

34
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguagesConfig.cs

@ -6,8 +6,8 @@
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps.Json
{
@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Core.Apps.Json
public Dictionary<string, JsonLanguageConfig> Languages { get; set; }
[JsonProperty]
public Language? Master { get; set; }
public string Master { get; set; }
public JsonLanguagesConfig()
{
@ -25,38 +25,18 @@ namespace Squidex.Domain.Apps.Core.Apps.Json
public JsonLanguagesConfig(LanguagesConfig value)
{
Languages = new Dictionary<string, JsonLanguageConfig>(value.Count);
Languages = value.Languages.ToDictionary(x => x.Key, x => new JsonLanguageConfig(x.Value));
foreach (LanguageConfig config in value)
{
Languages.Add(config.Language, new JsonLanguageConfig(config));
}
Master = value.Master?.Language;
Master = value.Master;
}
public LanguagesConfig ToConfig()
{
var languagesConfig = new LanguageConfig[Languages?.Count ?? 0];
if (Languages != null)
{
var i = 0;
foreach (var (key, value) in Languages)
{
languagesConfig[i++] = value.ToConfig(key);
}
}
var result = LanguagesConfig.Build(languagesConfig);
var languages = Languages.ToDictionary(x => x.Key, x => x.Value.ToConfig());
if (Master != null)
{
result = result.MakeMaster(Master);
}
var master = Master ?? languages.Keys.FirstOrDefault();
return result;
return new LanguagesConfig(languages, master);
}
}
}

53
backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs

@ -12,60 +12,33 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class LanguageConfig : IFieldPartitionItem
public sealed class LanguageConfig
{
private readonly Language language;
private readonly Language[] languageFallbacks;
public static readonly LanguageConfig Default = new LanguageConfig();
public bool IsOptional { get; }
public Language Language
{
get { return language; }
}
public IEnumerable<Language> LanguageFallbacks
{
get { return languageFallbacks; }
}
string IFieldPartitionItem.Key
{
get { return language.Iso2Code; }
}
private readonly Language[] fallbacks;
string IFieldPartitionItem.Name
{
get { return language.EnglishName; }
}
IEnumerable<string> IFieldPartitionItem.Fallback
{
get { return LanguageFallbacks.Select(x => x.Iso2Code); }
}
public bool IsOptional { get; }
public LanguageConfig(Language language, bool isOptional = false, IEnumerable<Language>? fallback = null)
: this(language, isOptional, fallback?.ToArray())
public IEnumerable<Language> Fallbacks
{
get { return fallbacks; }
}
public LanguageConfig(Language language, bool isOptional = false, params Language[]? fallback)
public LanguageConfig(bool isOptional = false, params Language[]? fallbacks)
{
Guard.NotNull(language);
IsOptional = isOptional;
this.language = language;
this.languageFallbacks = fallback ?? Array.Empty<Language>();
this.fallbacks = fallbacks ?? Array.Empty<Language>();
}
public LanguageConfig WithoutFallback(Language fallback)
internal LanguageConfig Cleanup(string self, IReadOnlyDictionary<string, LanguageConfig> allowed)
{
Guard.NotNull(fallback);
if (languageFallbacks.Contains(fallback))
if (fallbacks.Any(x => x.Iso2Code == self) || fallbacks.Any(x => !allowed.ContainsKey(x)))
{
return new LanguageConfig(Language, IsOptional, LanguageFallbacks.Except(new[] { fallback }));
var cleaned = Fallbacks.Where(x => x.Iso2Code != self && allowed.ContainsKey(x.Iso2Code)).ToArray();
return new LanguageConfig(IsOptional, cleaned);
}
return this;

218
backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs

@ -5,189 +5,201 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Linq;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
#pragma warning disable IDE0028 // Simplify collection initialization
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class LanguagesConfig : IFieldPartitioning
{
public static readonly LanguagesConfig English = Build(Language.EN);
private readonly ArrayDictionary<Language, LanguageConfig> languages;
private readonly LanguageConfig master;
public static readonly LanguagesConfig English = new LanguagesConfig(
new Dictionary<string, LanguageConfig>
{
[Language.EN] = new LanguageConfig()
},
Language.EN);
public LanguageConfig Master
{
get { return master; }
}
private readonly Dictionary<string, LanguageConfig> languages;
private readonly string master;
IFieldPartitionItem IFieldPartitioning.Master
public string Master
{
get { return master; }
}
IEnumerator IEnumerable.GetEnumerator()
public IEnumerable<string> AllKeys
{
return languages.Values.GetEnumerator();
get { return languages.Keys; }
}
IEnumerator<IFieldPartitionItem> IEnumerable<IFieldPartitionItem>.GetEnumerator()
public IReadOnlyDictionary<string, LanguageConfig> Languages
{
return languages.Values.GetEnumerator();
get { return languages; }
}
public int Count
public LanguagesConfig(Dictionary<string, LanguageConfig> languages, string master)
{
get { return languages.Count; }
}
private LanguagesConfig(ArrayDictionary<Language, LanguageConfig> languages, LanguageConfig master, bool checkMaster = true)
{
if (checkMaster)
{
this.master = master ?? throw new InvalidOperationException("Config has no master language.");
}
Guard.NotNull(languages);
Guard.NotNullOrEmpty(master);
foreach (var languageConfig in languages.Values)
{
foreach (var fallback in languageConfig.LanguageFallbacks)
{
if (!languages.ContainsKey(fallback))
{
var message = $"Config for language '{languageConfig.Language.Iso2Code}' contains unsupported fallback language '{fallback.Iso2Code}'";
throw new InvalidOperationException(message);
}
}
}
Cleanup(languages, ref master);
this.languages = languages;
}
public static LanguagesConfig Build(ICollection<LanguageConfig> configs)
{
Guard.NotNull(configs);
return new LanguagesConfig(configs.ToArrayDictionary(x => x.Language), configs.FirstOrDefault());
this.master = master;
}
public static LanguagesConfig Build(params LanguageConfig[] configs)
[Pure]
public LanguagesConfig MakeMaster(Language language)
{
return Build(configs?.ToList()!);
}
Guard.NotNull(language);
public static LanguagesConfig Build(params Language[] languages)
{
return Build(languages?.Select(x => new LanguageConfig(x)).ToList()!);
return Build(languages, language);
}
[Pure]
public LanguagesConfig Set(Language language, bool isOptional = false, IEnumerable<Language>? fallback = null)
public LanguagesConfig Set(Language language, bool isOptional = false, params Language[] fallbacks)
{
Guard.NotNull(language);
return Set(new LanguageConfig(language, isOptional, fallback));
var newLanguages = new Dictionary<string, LanguageConfig>(languages);
newLanguages[language] = new LanguageConfig(isOptional, fallbacks);
return Build(newLanguages, master);
}
[Pure]
public LanguagesConfig MakeMaster(Language language)
public LanguagesConfig Remove(Language language)
{
Guard.NotNull(language);
var newMaster = new LanguageConfig(languages[language].Language);
var newLanguages = new Dictionary<string, LanguageConfig>(languages);
var newLanguages =
languages
.Without(language)
.With(language, newMaster);
newLanguages.Remove(language);
return Create(newLanguages, newMaster);
return Build(newLanguages, master);
}
[Pure]
public LanguagesConfig Set(LanguageConfig config)
private LanguagesConfig Build(Dictionary<string, LanguageConfig> newLanguages, string newMaster)
{
Guard.NotNull(config);
if (newLanguages.Count == 0)
{
return this;
}
var newLanguages = languages.With(config.Language, config);
Cleanup(newLanguages, ref newMaster);
var newMaster =
Master.Language != config.Language ?
Master :
config;
if (newLanguages.EqualsDictionary(languages, EqualityComparer<string>.Default, DeepComparer<LanguageConfig>.Instance) && Equals(newMaster, master))
{
return this;
}
return Create(newLanguages, newMaster!);
return new LanguagesConfig(newLanguages, newMaster);
}
[Pure]
public LanguagesConfig Remove(Language language)
private void Cleanup(Dictionary<string, LanguageConfig> newLanguages, ref string newMaster)
{
Guard.NotNull(language);
if (!newLanguages.ContainsKey(newMaster))
{
if (newLanguages.ContainsKey(master))
{
newMaster = master;
}
else
{
newMaster = newLanguages.Keys.First();
}
}
var newLanguages =
languages.Values.Where(x => x.Language != language).Select(x => x.WithoutFallback(language)).ToArrayDictionary(x => x.Language);
var masterConfig = newLanguages[newMaster];
var newMaster =
newLanguages.Values.FirstOrDefault(x => x.Language == Master.Language) ??
newLanguages.Values.FirstOrDefault();
if (masterConfig.IsOptional || masterConfig.Fallbacks.Any())
{
newLanguages[newMaster] = LanguageConfig.Default;
}
return Create(newLanguages, newMaster);
foreach (var (key, config) in newLanguages.ToList())
{
newLanguages[key] = config.Cleanup(key, newLanguages);
}
}
private LanguagesConfig Create(ArrayDictionary<Language, LanguageConfig> newLanguages, LanguageConfig newMaster)
public PartitionResolver ToResolver()
{
if (newLanguages.EqualsDictionary(languages, EqualityComparer<Language>.Default, DeepComparer<LanguageConfig>.Instance) && newMaster.Language.Equals(master.Language))
return partitioning =>
{
return this;
}
if (partitioning.Equals(Partitioning.Invariant))
{
return InvariantPartitioning.Instance;
}
return new LanguagesConfig(newLanguages, newMaster);
return this;
};
}
public bool Contains(Language language)
public bool IsMaster(string key)
{
return language != null && languages.ContainsKey(language);
return Equals(Master, key);
}
public bool TryGetConfig(Language language, [MaybeNullWhen(false)] out LanguageConfig config)
public string? GetName(string key)
{
return languages.TryGetValue(language, out config!);
if (key != null && languages.ContainsKey(key))
{
return Language.GetLanguage(key).EnglishName;
}
return null;
}
public bool TryGetItem(string key, [MaybeNullWhen(false)] out IFieldPartitionItem item)
public bool IsOptional(string key)
{
if (Language.IsValidLanguage(key) && languages.TryGetValue(key, out var value))
if (key != null && languages.TryGetValue(key, out var value))
{
item = value;
return true;
return value.IsOptional;
}
else
{
item = null!;
return false;
}
return false;
}
public PartitionResolver ToResolver()
public IEnumerable<string> GetPriorities(string key)
{
return partitioning =>
if (key != null)
{
if (partitioning.Equals(Partitioning.Invariant))
if (Equals(Master, key))
{
return InvariantPartitioning.Instance;
yield return key;
}
else if (languages.TryGetValue(key, out var config))
{
yield return key;
return this;
};
foreach (var fallback in config.Fallbacks)
{
yield return fallback;
}
if (!config.Fallbacks.Any(x => x.Iso2Code == Master))
{
yield return Master;
}
}
}
}
public bool Contains(string key)
{
return key != null && languages.ContainsKey(key);
}
public override string ToString()
{
return "language";
}
}
}

22
backend/src/Squidex.Domain.Apps.Core.Model/IFieldPartitionItem.cs

@ -1,22 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Core
{
public interface IFieldPartitionItem
{
string Key { get; }
string Name { get; }
bool IsOptional { get; }
IEnumerable<string> Fallback { get; }
}
}

16
backend/src/Squidex.Domain.Apps.Core.Model/IFieldPartitioning.cs

@ -9,10 +9,20 @@ using System.Collections.Generic;
namespace Squidex.Domain.Apps.Core
{
public interface IFieldPartitioning : IReadOnlyCollection<IFieldPartitionItem>
public interface IFieldPartitioning
{
IFieldPartitionItem Master { get; }
string Master { get; }
bool TryGetItem(string key, out IFieldPartitionItem item);
IEnumerable<string> AllKeys { get; }
IEnumerable<string> GetPriorities(string key);
bool IsMaster(string key);
bool IsOptional(string key);
bool Contains(string key);
string? GetName(string key);
}
}

57
backend/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs

@ -5,70 +5,63 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Squidex.Domain.Apps.Core
{
public sealed class InvariantPartitioning : IFieldPartitioning, IFieldPartitionItem
public sealed class InvariantPartitioning : IFieldPartitioning
{
public static readonly InvariantPartitioning Instance = new InvariantPartitioning();
public static readonly string Key = "iv";
public int Count
public string Master
{
get { return 1; }
get { return Key; }
}
public IFieldPartitionItem Master
public IEnumerable<string> AllKeys
{
get { return this; }
get { yield return Key; }
}
string IFieldPartitionItem.Key
public string? GetName(string key)
{
get { return Key; }
}
if (Contains(key))
{
return "Invariant";
}
string IFieldPartitionItem.Name
{
get { return "Invariant"; }
return null;
}
bool IFieldPartitionItem.IsOptional
public IEnumerable<string> GetPriorities(string key)
{
get { return false; }
}
if (Contains(key))
{
yield return Key;
}
IEnumerable<string> IFieldPartitionItem.Fallback
{
get { return Enumerable.Empty<string>(); }
yield break;
}
private InvariantPartitioning()
public bool Contains(string key)
{
return Equals(Key, key);
}
public bool TryGetItem(string key, [MaybeNullWhen(false)] out IFieldPartitionItem item)
public bool IsMaster(string key)
{
var isFound = string.Equals(key, Key, StringComparison.OrdinalIgnoreCase);
item = isFound ? this : null!;
return isFound;
return Contains(key);
}
IEnumerator<IFieldPartitionItem> IEnumerable<IFieldPartitionItem>.GetEnumerator()
public bool IsOptional(string key)
{
yield return this;
return false;
}
IEnumerator IEnumerable.GetEnumerator()
public override string ToString()
{
yield return this;
return "invariant value";
}
}
}

35
backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs

@ -158,10 +158,10 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
};
}
public static FieldConverter ResolveInvariant(LanguagesConfig config)
public static FieldConverter ResolveInvariant(LanguagesConfig languages)
{
var codeForInvariant = InvariantPartitioning.Key;
var codeForMasterLanguage = config.Master.Language.Iso2Code;
var codeForMasterLanguage = languages.Master;
return (data, field) =>
{
@ -189,7 +189,7 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
};
}
public static FieldConverter ResolveLanguages(LanguagesConfig config)
public static FieldConverter ResolveLanguages(LanguagesConfig languages)
{
var codeForInvariant = InvariantPartitioning.Key;
@ -199,15 +199,13 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
{
var result = new ContentFieldData();
foreach (var languageConfig in config)
foreach (var languageCode in languages.AllKeys)
{
var languageCode = languageConfig.Key;
if (data.TryGetValue(languageCode, out var value))
{
result[languageCode] = value;
}
else if (languageConfig == config.Master && data.TryGetValue(codeForInvariant, out value))
else if (languages.IsMaster(languageCode) && data.TryGetValue(codeForInvariant, out value))
{
result[languageCode] = value;
}
@ -220,39 +218,26 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
};
}
public static FieldConverter ResolveFallbackLanguages(LanguagesConfig config)
public static FieldConverter ResolveFallbackLanguages(LanguagesConfig languages)
{
var master = config.Master;
var master = languages.Master;
return (data, field) =>
{
if (field.Partitioning.Equals(Partitioning.Language))
{
foreach (var languageConfig in config)
foreach (var languageCode in languages.AllKeys)
{
var languageCode = languageConfig.Key;
if (!data.TryGetValue(languageCode, out var value))
{
var dataFound = false;
foreach (var fallback in languageConfig.Fallback)
foreach (var fallback in languages.GetPriorities(languageCode))
{
if (data.TryGetValue(fallback, out value))
{
data[languageCode] = value;
dataFound = true;
break;
}
}
if (!dataFound && languageConfig != master)
{
if (data.TryGetValue(master.Language, out value))
{
data[languageCode] = value;
}
}
}
}
}
@ -280,7 +265,7 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
if (languageSet.Count == 0)
{
languageSet.Add(config.Master.Language.Iso2Code);
languageSet.Add(config.Master);
}
return (data, field) =>

14
backend/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs

@ -38,11 +38,11 @@ namespace Squidex.Domain.Apps.Core.EnrichContent
if (fieldData != null)
{
var fieldPartition = partitionResolver(field.Partitioning);
var partitioning = partitionResolver(field.Partitioning);
foreach (var partitionItem in fieldPartition)
foreach (var partitionKey in partitioning.AllKeys)
{
Enrich(field, fieldData, partitionItem);
Enrich(field, fieldData, partitionKey);
}
if (fieldData.Count > 0)
@ -53,7 +53,7 @@ namespace Squidex.Domain.Apps.Core.EnrichContent
}
}
private static void Enrich(IField field, ContentFieldData fieldData, IFieldPartitionItem partitionItem)
private static void Enrich(IField field, ContentFieldData fieldData, string partitionKey)
{
Guard.NotNull(fieldData);
@ -64,11 +64,9 @@ namespace Squidex.Domain.Apps.Core.EnrichContent
return;
}
var key = partitionItem.Key;
if (!fieldData.TryGetValue(key, out var value) || ShouldApplyDefaultValue(field, value))
if (!fieldData.TryGetValue(partitionKey, out var value) || ShouldApplyDefaultValue(field, value))
{
fieldData.AddJsonValue(key, defaultValue);
fieldData.AddJsonValue(partitionKey, defaultValue);
}
}

12
backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs

@ -8,7 +8,6 @@
using System;
using System.Collections.Generic;
using System.Text;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
@ -91,21 +90,22 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
}
}
public static JsonObject FormatReferences(this NamedContentData data, Schema schema, LanguagesConfig languages, string separator = ", ")
public static JsonObject FormatReferences(this NamedContentData data, Schema schema, IFieldPartitioning partitioning, string separator = ", ")
{
Guard.NotNull(schema);
Guard.NotNull(partitioning);
var result = JsonValue.Object();
foreach (var language in languages)
foreach (var partitionKey in partitioning.AllKeys)
{
result[language.Key] = JsonValue.Create(data.FormatReferenceFields(schema, language.Key, separator));
result[partitionKey] = JsonValue.Create(data.FormatReferenceFields(schema, partitionKey, separator));
}
return result;
}
private static string FormatReferenceFields(this NamedContentData data, Schema schema, string partition, string separator)
private static string FormatReferenceFields(this NamedContentData data, Schema schema, string partitionKey, string separator)
{
Guard.NotNull(schema);
@ -127,7 +127,7 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
{
if (data.TryGetValue(referenceField.Name, out var fieldData) && fieldData != null)
{
if (fieldData.TryGetValue(partition, out var value))
if (fieldData.TryGetValue(partitionKey, out var value))
{
AddValue(value);
}

6
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs

@ -52,11 +52,11 @@ namespace Squidex.Domain.Apps.Core.GenerateEdmSchema
if (created)
{
var partition = partitionResolver(field.Partitioning);
var partitioning = partitionResolver(field.Partitioning);
foreach (var partitionItem in partition)
foreach (var partitionKey in partitioning.AllKeys)
{
partitionType.AddStructuralProperty(partitionItem.Key.EscapeEdmField(), fieldEdmType);
partitionType.AddStructuralProperty(partitionKey.EscapeEdmField(), fieldEdmType);
}
}

14
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs

@ -26,18 +26,22 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
foreach (var field in schema.Fields.ForApi(withHidden))
{
var partitionObject = Builder.Object();
var partitionSet = partitionResolver(field.Partitioning);
var partitioning = partitionResolver(field.Partitioning);
foreach (var partitionItem in partitionSet)
foreach (var partitionKey in partitioning.AllKeys)
{
var partitionItemProperty = field.Accept(jsonTypeVisitor);
if (partitionItemProperty != null)
{
partitionItemProperty.Description = partitionItem.Name;
partitionItemProperty.IsRequired = field.RawProperties.IsRequired && !partitionItem.IsOptional;
var isOptional = partitioning.IsOptional(partitionKey);
partitionObject.Properties.Add(partitionItem.Key, partitionItemProperty);
var name = partitioning.GetName(partitionKey);
partitionItemProperty.Description = name;
partitionItemProperty.IsRequired = field.RawProperties.IsRequired && !isOptional;
partitionObject.Properties.Add(partitionKey, partitionItemProperty);
}
}

17
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs

@ -87,22 +87,19 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
var fieldValidator = field.CreateValidator();
var fieldsValidators = new Dictionary<string, (bool IsOptional, IValidator Validator)>();
foreach (var partition in partitioning)
foreach (var partitionKey in partitioning.AllKeys)
{
fieldsValidators[partition.Key] = (partition.IsOptional, fieldValidator);
var optional = partitioning.IsOptional(partitionKey);
fieldsValidators[partitionKey] = (optional, fieldValidator);
}
var typeName = partitioning.ToString()!;
return new AggregateValidator(
field.CreateBagValidator()
.Union(Enumerable.Repeat(
new ObjectValidator<IJsonValue>(fieldsValidators, isPartial, TypeName(field)), 1)));
}
private static string TypeName(IRootField field)
{
var isLanguage = field.Partitioning.Equals(Partitioning.Language);
return isLanguage ? "language" : "invariant value";
new ObjectValidator<IJsonValue>(fieldsValidators, isPartial, typeName), 1)));
}
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs

@ -18,6 +18,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands
public bool IsMaster { get; set; }
public List<Language> Fallback { get; set; }
public List<Language>? Fallback { get; set; }
}
}

67
backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs

@ -20,11 +20,13 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
Validate.It(() => "Cannot add language.", e =>
{
if (command.Language == null)
var language = command.Language;
if (language == null)
{
e(Not.Defined("Language code"), nameof(command.Language));
}
else if (languages.Contains(command.Language))
else if (languages.Contains(language))
{
e("Language has already been added.");
}
@ -35,18 +37,22 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
Guard.NotNull(command);
var config = GetConfigOrThrow(languages, command.Language);
Validate.It(() => "Cannot remove language.", e =>
{
if (command.Language == null)
var language = command.Language;
if (language == null)
{
e(Not.Defined("Language code"), nameof(command.Language));
}
if (languages.Master == config)
else
{
e("Master language cannot be removed.");
EnsureConfigExists(languages, language);
if (languages.IsMaster(language))
{
e("Master language cannot be removed.");
}
}
});
}
@ -55,48 +61,45 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
Guard.NotNull(command);
var config = GetConfigOrThrow(languages, command.Language);
Validate.It(() => "Cannot update language.", e =>
{
if (command.Language == null)
var language = command.Language;
if (language == null)
{
e(Not.Defined("Language code"), nameof(command.Language));
}
if ((languages.Master == config || command.IsMaster) && command.IsOptional)
else
{
e("Master language cannot be made optional.", nameof(command.IsMaster));
}
EnsureConfigExists(languages, language);
if (command.Fallback == null)
{
return;
}
if ((languages.IsMaster(language) || command.IsMaster) && command.IsOptional)
{
e("Master language cannot be made optional.", nameof(command.IsMaster));
}
foreach (var fallback in command.Fallback)
{
if (!languages.Contains(fallback))
if (command.Fallback == null)
{
e($"App does not have fallback language '{fallback}'.", nameof(command.Fallback));
return;
}
foreach (var fallback in command.Fallback)
{
if (!languages.Contains(fallback))
{
e($"App does not have fallback language '{fallback}'.", nameof(command.Fallback));
}
}
}
});
}
private static LanguageConfig? GetConfigOrThrow(LanguagesConfig languages, Language language)
private static void EnsureConfigExists(LanguagesConfig languages, Language language)
{
if (language == null)
{
return null;
}
if (!languages.TryGetConfig(language, out var languageConfig))
if (!languages.Contains(language))
{
throw new DomainObjectNotFoundException(language, "Languages", typeof(IAppEntity));
}
return languageConfig;
}
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs

@ -142,7 +142,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
case AppLanguageUpdated e:
return UpdateLanguages(e, (ev, l) =>
{
l = l.Set(ev.Language, ev.IsOptional, ev.Fallback);
l = l.Set(ev.Language, ev.IsOptional, ev.Fallback?.ToArray());
if (ev.IsMaster)
{

10
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs

@ -35,17 +35,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Name = $"{schemaType}Data{typeName}Dto"
};
var partition = model.ResolvePartition(field.Partitioning);
var partitioning = model.ResolvePartition(field.Partitioning);
foreach (var partitionItem in partition)
foreach (var partitionKey in partitioning.AllKeys)
{
var key = partitionItem.Key;
fieldGraphType.AddField(new FieldType
{
Name = key.EscapePartition(),
Name = partitionKey.EscapePartition(),
Arguments = args,
Resolver = PartitionResolver(valueResolver, key),
Resolver = PartitionResolver(valueResolver, partitionKey),
ResolvedType = resolvedType,
Description = field.RawProperties.Hints
});

2
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs

@ -170,7 +170,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
var context = (GraphQLExecutionContext)c.UserContext;
return action(c.Source)?.ToFlatten(context.Context.App.LanguagesConfig.Master.Language);
return action(c.Source)?.ToFlatten(context.Context.App.LanguagesConfig.Master);
});
}
}

4
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs

@ -278,9 +278,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var value = JsonValue.Object();
foreach (var language in context.App.LanguagesConfig)
foreach (var partitionKey in context.App.LanguagesConfig.AllKeys)
{
value.Add(language.Key, text);
value.Add(partitionKey, text);
}
return value;

3
backend/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs

@ -7,7 +7,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Core.Contents;
@ -110,7 +109,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text
private static SearchContext CreateContext(IAppEntity app, Scope scope)
{
var languages = new HashSet<string>(app.LanguagesConfig.Select(x => x.Key));
var languages = new HashSet<string>(app.LanguagesConfig.AllKeys);
return new SearchContext { Languages = languages, Scope = scope };
}

18
backend/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs

@ -12,11 +12,13 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
#pragma warning disable IDE0044 // Add readonly modifier
#pragma warning disable RECS0108 // Warns about static fields in generic types
namespace Squidex.Infrastructure.Collections
{
public class ArrayDictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue> where TKey : notnull
{
private static readonly KeyValuePair<TKey, TValue>[] EmptyItems = Array.Empty<KeyValuePair<TKey, TValue>>();
private readonly IEqualityComparer<TKey> keyComparer;
private KeyValuePair<TKey, TValue>[] items;
@ -49,7 +51,7 @@ namespace Squidex.Infrastructure.Collections
}
public ArrayDictionary()
: this(EqualityComparer<TKey>.Default, Array.Empty<KeyValuePair<TKey, TValue>>())
: this(EqualityComparer<TKey>.Default, EmptyItems)
{
}
@ -123,17 +125,15 @@ namespace Squidex.Infrastructure.Collections
return Self<TArray>();
}
var result = Array.Empty<KeyValuePair<TKey, TValue>>();
if (items.Length > 1)
if (Count == 1)
{
result = new KeyValuePair<TKey, TValue>[items.Length - 1];
return Create<TArray>(EmptyItems);
}
var afterIndex = items.Length - index - 1;
var result = new KeyValuePair<TKey, TValue>[items.Length - 1];
Array.Copy(items, 0, result, 0, index);
Array.Copy(items, index + 1, result, index, afterIndex);
}
Array.Copy(items, 0, result, 0, index);
Array.Copy(items, index + 1, result, index, items.Length - index - 1);
return Create<TArray>(result);
}

20
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs

@ -10,7 +10,6 @@ using System.Linq;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web;
@ -46,15 +45,16 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary>
public bool IsOptional { get; set; }
public static AppLanguageDto FromLanguage(LanguageConfig language, IAppEntity app)
public static AppLanguageDto FromLanguage(Language language, LanguageConfig config, LanguagesConfig languages)
{
var result = SimpleMapper.Map(language.Language,
new AppLanguageDto
{
IsMaster = language == app.LanguagesConfig.Master,
IsOptional = language.IsOptional,
Fallback = language.LanguageFallbacks.ToArray()
});
var result = new AppLanguageDto
{
EnglishName = language.EnglishName,
IsMaster = languages.IsMaster(language),
IsOptional = languages.IsOptional(language),
Iso2Code = language.Iso2Code,
Fallback = config.Fallbacks.ToArray()
};
return result;
}
@ -70,7 +70,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
AddPutLink("update", controller.Url<AppLanguagesController>(x => nameof(x.PutLanguage), values));
}
if (controller.HasPermission(Permissions.AppLanguagesDelete, app.Name) && app.LanguagesConfig.Count > 1)
if (controller.HasPermission(Permissions.AppLanguagesDelete, app.Name) && app.LanguagesConfig.Languages.Count > 1)
{
AddDeleteLink("delete", controller.Url<AppLanguagesController>(x => nameof(x.DeleteLanguage), values));
}

7
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguagesDto.cs

@ -7,7 +7,6 @@
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Shared;
using Squidex.Web;
@ -24,10 +23,12 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
public static AppLanguagesDto FromApp(IAppEntity app, ApiController controller)
{
var config = app.LanguagesConfig;
var result = new AppLanguagesDto
{
Items = app.LanguagesConfig.OfType<LanguageConfig>()
.Select(x => AppLanguageDto.FromLanguage(x, app))
Items = config.Languages
.Select(x => AppLanguageDto.FromLanguage(x.Key, x.Value, config))
.Select(x => x.WithLinks(controller, app))
.OrderByDescending(x => x.IsMaster).ThenBy(x => x.Iso2Code)
.ToArray()

14
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigJsonTests.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using FluentAssertions;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Infrastructure;
@ -18,17 +17,16 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
[Fact]
public void Should_serialize_and_deserialize()
{
var languages = LanguagesConfig.Build(
new LanguageConfig(Language.EN),
new LanguageConfig(Language.DE, true, Language.EN),
new LanguageConfig(Language.IT, false, Language.DE))
.MakeMaster(Language.IT);
var languages =
LanguagesConfig.English
.Set(Language.FR)
.Set(Language.IT, false)
.Set(Language.DE, true, new Language[] { Language.IT })
.MakeMaster(Language.FR);
var serialized = languages.SerializeAndDeserialize();
serialized.Should().BeEquivalentTo(languages);
Assert.Same(serialized.FirstOrDefault(x => x.Key == "it"), serialized.Master);
}
}
}

260
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs

@ -5,10 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Infrastructure;
@ -20,104 +17,138 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
{
public class LanguagesConfigTests
{
private readonly LanguagesConfig config_0 = LanguagesConfig.Build(Language.DE);
private readonly LanguagesConfig config_0 = LanguagesConfig.English;
[Fact]
public void Should_create_initial_config_0()
public void Should_make_contains_test()
{
config_0.OfType<LanguageConfig>().ToList().Should().BeEquivalentTo(
new List<LanguageConfig>
{
new LanguageConfig(Language.DE)
});
Assert.True(config_0.Contains(Language.EN));
Assert.Equal(Language.DE, config_0.Master.Language);
Assert.Equal("de", ((IFieldPartitioning)config_0).Master.Key);
Assert.False(config_0.Contains(Language.FR));
Assert.False(config_0.Contains(null!));
}
[Fact]
public void Should_create_initial_config_0_with_multiple_languages()
public void Should_provide_name()
{
var config = LanguagesConfig.Build(Language.DE, Language.EN, Language.IT);
Assert.Equal("English", config_0.GetName(Language.EN));
config.OfType<LanguageConfig>().ToList().Should().BeEquivalentTo(
new List<LanguageConfig>
{
new LanguageConfig(Language.DE),
new LanguageConfig(Language.EN),
new LanguageConfig(Language.IT)
});
Assert.Null(config_0.GetName(Language.FR));
Assert.Null(config_0.GetName(null!));
}
config.TryGetConfig(Language.DE, out var master);
[Fact]
public void Should_make_master_test()
{
var config =
LanguagesConfig.English
.Set(Language.DE)
.Set(Language.ES, true);
Assert.Equal(Language.DE, config.Master.Language);
Assert.Equal(3, config.Count);
Assert.True(config.IsMaster(Language.EN));
Assert.Same(master, config.Master);
Assert.False(config.IsMaster(Language.DE));
Assert.False(config.IsMaster(Language.ES));
Assert.False(config.IsMaster(Language.FR));
Assert.False(config.IsMaster(null!));
}
[Fact]
public void Should_create_initial_config_0_with_config_0s()
public void Should_make_optional_test()
{
var configs = new[]
{
new LanguageConfig(Language.DE),
new LanguageConfig(Language.EN),
new LanguageConfig(Language.IT)
};
var config = LanguagesConfig.Build(configs);
var config =
LanguagesConfig.English
.Set(Language.DE)
.Set(Language.ES, true);
config.OfType<LanguageConfig>().ToList().Should().BeEquivalentTo(configs);
Assert.True(config.IsOptional(Language.ES));
Assert.Equal(configs[0], config.Master);
Assert.Same(configs[0], config.Master);
Assert.False(config.IsOptional(Language.EN));
Assert.False(config.IsOptional(Language.DE));
Assert.False(config.IsOptional(Language.FR));
Assert.False(config.IsOptional(null!));
}
[Fact]
public void Should_add_language()
public void Should_provide_priorities()
{
var config_1 = config_0.Set(Language.IT);
var config =
LanguagesConfig.English
.Set(Language.DE)
.Set(Language.ES, true, Language.DE);
Assert.Empty(config.GetPriorities(Language.FR));
Assert.Empty(config.GetPriorities(null!));
Assert.Equal(new string[] { Language.ES, Language.DE, Language.EN }, config.GetPriorities(Language.ES));
Assert.Equal(new string[] { Language.DE, Language.EN }, config.GetPriorities(Language.DE));
Assert.Equal(new string[] { Language.EN }, config.GetPriorities(Language.EN));
}
config_1.OfType<LanguageConfig>().ToList().Should().BeEquivalentTo(
new List<LanguageConfig>
[Fact]
public void Should_create_initial_config()
{
config_0.Languages.Should().BeEquivalentTo(
new Dictionary<string, LanguageConfig>
{
new LanguageConfig(Language.DE),
new LanguageConfig(Language.IT)
[Language.EN] = new LanguageConfig()
});
Assert.True(config_1.TryGetConfig(Language.IT, out _));
Assert.True(config_1.Contains(Language.IT));
Assert.Equal(Language.EN, config_0.Master);
}
[Fact]
public void Should_make_first_language_to_master()
public void Should_create_initial_config_0_with_multiple_languages()
{
Assert.Equal(Language.DE, config_0.Master.Language);
var config =
LanguagesConfig.English
.Set(Language.DE)
.Set(Language.ES, true)
.Set(Language.IT, true, Language.ES)
.MakeMaster(Language.DE);
config.Languages.Should().BeEquivalentTo(
new Dictionary<string, LanguageConfig>
{
[Language.EN] = new LanguageConfig(),
[Language.DE] = new LanguageConfig(),
[Language.ES] = new LanguageConfig(true),
[Language.IT] = new LanguageConfig(true, Language.ES)
});
Assert.Equal(Language.DE, config.Master);
}
[Fact]
public void Should_not_throw_exception_if_language_to_add_already_exists()
{
config_0.Set(Language.DE);
config_0.Set(Language.EN);
}
[Fact]
public void Should_make_master_language()
{
var config_1 = config_0.Set(Language.UK);
var config_2 = config_1.Set(Language.IT, true, new[] { Language.UK });
var config_3 = config_2.MakeMaster(Language.IT);
var config =
LanguagesConfig.English
.Set(Language.DE)
.Set(Language.IT, true, Language.ES)
.MakeMaster(Language.IT);
Assert.Equal(Language.IT, config_3.Master.Language);
config.Languages.Should().BeEquivalentTo(
new Dictionary<string, LanguageConfig>
{
[Language.EN] = new LanguageConfig(),
[Language.DE] = new LanguageConfig(),
[Language.IT] = new LanguageConfig()
});
Assert.False(config_3.Master.IsOptional);
Assert.Equal(Language.IT, config.Master);
}
[Fact]
public void Should_return_same_languages_if_master_language_is_already_master()
{
var config_1 = config_0.Set(Language.UK);
var config_1 = config_0.Set(Language.DE);
var config_2 = config_1.Set(Language.IT);
var config_3 = config_2.MakeMaster(Language.IT);
var config_4 = config_3.MakeMaster(Language.IT);
@ -126,106 +157,115 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
}
[Fact]
public void Should_throw_exception_if_language_to_make_master_is_not_found()
public void Should_keep_master_language_if_language_to_make_master_is_not_found()
{
Assert.Throws<KeyNotFoundException>(() => config_0.MakeMaster(Language.EN));
}
var config_1 = config_0.Set(Language.DE);
var config_2 = config_1.Set(Language.IT);
var config_3 = config_2.MakeMaster(Language.IT);
var config_4 = config_3.MakeMaster(Language.FR);
[Fact]
public void Should_not_throw_exception_if_language_is_already_master_language()
{
config_0.MakeMaster(Language.DE);
Assert.Same(config_3, config_4);
Assert.Equal(Language.IT, config_4.Master);
}
[Fact]
public void Should_remove_language()
{
var config = LanguagesConfig.Build(Language.DE, Language.IT, Language.RU);
var config_1 = config.Remove(Language.IT);
Assert.Same(config_1.Master, config_1.OfType<LanguageConfig>().FirstOrDefault(x => x.Language == Language.DE));
var config_1 = config_0.Set(Language.DE);
var config_2 = config_1.Set(Language.IT);
var config_3 = config_2.Remove(Language.DE);
config_1.ToList().Should().BeEquivalentTo(
new List<LanguageConfig>
config_3.Languages.Should().BeEquivalentTo(
new Dictionary<string, LanguageConfig>
{
new LanguageConfig(Language.DE),
new LanguageConfig(Language.RU)
[Language.EN] = new LanguageConfig(),
[Language.IT] = new LanguageConfig()
});
Assert.Equal(Language.EN, config_3.Master);
}
[Fact]
public void Should_remove_fallbacks_when_removing_language()
{
var config =
LanguagesConfig.Build(
new LanguageConfig(Language.DE),
new LanguageConfig(Language.IT, false, Language.RU, Language.IT),
new LanguageConfig(Language.RU, false, Language.DE, Language.IT));
var config_1 = config.Remove(Language.IT);
config_1.OfType<LanguageConfig>().ToList().Should().BeEquivalentTo(
new List<LanguageConfig>
var config_1 = config_0.Set(Language.DE);
var config_2 = config_1.Set(Language.IT, true, Language.UK);
var config_3 = config_2.Remove(Language.DE);
config_3.Languages.Should().BeEquivalentTo(
new Dictionary<string, LanguageConfig>
{
new LanguageConfig(Language.DE),
new LanguageConfig(Language.RU, false, Language.DE)
[Language.EN] = new LanguageConfig(),
[Language.IT] = new LanguageConfig(true)
});
Assert.Equal(Language.EN, config_3.Master);
}
[Fact]
public void Should_return_same_languages_if_language_to_remove_is_not_found()
public void Should_same_langauges_if_removing_single_language()
{
var config_1 = config_0.Remove(Language.EN);
Assert.Equal(config_0, config_1);
}
[Fact]
public void Should_throw_exception_if_language_to_remove_is_master()
{
Assert.Throws<InvalidOperationException>(() => config_0.Remove(Language.DE));
Assert.Same(config_0, config_1);
}
[Fact]
public void Should_update_language()
public void Should_update_master_language_if_removed()
{
var config_1 = config_0.Set(new LanguageConfig(Language.IT));
var config_2 = config_1.Set(new LanguageConfig(Language.IT, true, Language.DE));
var config_1 = config_0.Set(Language.DE);
var config_2 = config_1.Set(Language.IT);
var config_3 = config_2.Remove(Language.EN);
config_2.OfType<LanguageConfig>().ToList().Should().BeEquivalentTo(
new List<LanguageConfig>
config_3.Languages.Should().BeEquivalentTo(
new Dictionary<string, LanguageConfig>
{
new LanguageConfig(Language.DE),
new LanguageConfig(Language.IT, true, Language.DE)
[Language.DE] = new LanguageConfig(),
[Language.IT] = new LanguageConfig()
});
}
[Fact]
public void Should_throw_exception_if_fallback_language_is_invalid()
{
Assert.Throws<InvalidOperationException>(() => config_0.Set(new LanguageConfig(Language.DE, false, Language.EN)));
Assert.Equal(Language.DE, config_3.Master);
}
[Fact]
public void Should_provide_enumerators()
public void Should_return_same_languages_if_language_to_remove_is_not_found()
{
Assert.NotEmpty(config_0);
var config_1 = config_0.Remove(Language.IT);
Assert.NotNull(((IEnumerable)config_0).GetEnumerator());
Assert.NotNull(((IEnumerable<IFieldPartitionItem>)config_0).GetEnumerator());
Assert.Equal(config_0, config_1);
}
[Fact]
public void Should_resolve_language_by_string()
public void Should_update_language()
{
Assert.True(config_0.TryGetItem("de", out var item));
Assert.NotNull(item);
var config_1 = config_0.Set(Language.IT);
var config_2 = config_1.Set(Language.IT, true, Language.EN);
config_2.Languages.Should().BeEquivalentTo(
new Dictionary<string, LanguageConfig>
{
[Language.EN] = new LanguageConfig(),
[Language.IT] = new LanguageConfig(true, Language.EN)
});
Assert.Equal(Language.EN, config_2.Master);
}
[Fact]
public void Should_not_resolve_language_fór_invalid_language()
public void Should_eliminate_invalid_fallbacks_and_self()
{
Assert.False(config_0.TryGetItem("invalid", out var item));
Assert.Null(item);
var config_1 = config_0.Set(Language.IT);
var config_2 = config_1.Set(Language.IT);
var config_3 = config_2.Set(Language.IT, true, Language.EN, Language.IT, Language.DE);
config_3.Languages.Should().BeEquivalentTo(
new Dictionary<string, LanguageConfig>
{
[Language.EN] = new LanguageConfig(),
[Language.IT] = new LanguageConfig(true, Language.EN)
});
Assert.Equal(Language.EN, config_2.Master);
}
}
}

36
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/InvariantPartitionTests.cs

@ -5,8 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Xunit;
@ -17,14 +15,11 @@ namespace Squidex.Domain.Apps.Core.Model
public class InvariantPartitionTests
{
[Fact]
public void Should_provide_single_value()
public void Should_provide_name()
{
var sut = InvariantPartitioning.Instance;
Assert.Equal(1, sut.Count);
Assert.Same(sut.Master, ((IEnumerable<IFieldPartitionItem>)sut).SingleOrDefault());
Assert.Same(sut.Master, ((IEnumerable)sut).OfType<IFieldPartitionItem>().SingleOrDefault());
Assert.Equal("invariant value", sut.ToString());
}
[Fact]
@ -32,11 +27,30 @@ namespace Squidex.Domain.Apps.Core.Model
{
var sut = InvariantPartitioning.Instance;
Assert.Equal("iv", sut.Master.Key);
Assert.Equal("Invariant", sut.Master.Name);
Assert.Equal("iv", sut.Master);
Assert.Equal("Invariant", sut.GetName("iv"));
Assert.Equal(new[] { "iv" }, sut.AllKeys.ToArray());
Assert.Equal(new[] { "iv" }, sut.GetPriorities("iv").ToArray());
Assert.True(sut.IsMaster("iv"));
Assert.True(sut.Contains("iv"));
Assert.False(sut.IsOptional("iv"));
}
[Fact]
public void Should_handle_unsupported_key()
{
var sut = InvariantPartitioning.Instance;
Assert.Null(sut.GetName("invalid"));
Assert.Empty(sut.GetPriorities("invalid"));
Assert.False(sut.Master.Fallback.Any());
Assert.False(sut.Master.IsOptional);
Assert.False(sut.IsMaster("invalid"));
Assert.False(sut.IsOptional("invalid"));
Assert.False(sut.Contains("invalid"));
}
}
}

11
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs

@ -21,7 +21,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
public class FieldConvertersTests
{
private readonly IAssetUrlGenerator assetUrlGenerator = A.Fake<IAssetUrlGenerator>();
private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE);
private readonly LanguagesConfig languagesConfig = LanguagesConfig.English.Set(Language.DE);
public FieldConvertersTests()
{
@ -357,8 +357,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
{
var field = Fields.String(1, "string", Partitioning.Language);
var config_1 = languagesConfig.Set(new LanguageConfig(Language.IT));
var config_2 = config_1.Set(new LanguageConfig(Language.ES, false, Language.IT));
var config =
LanguagesConfig.English
.Set(Language.DE)
.Set(Language.IT)
.Set(Language.ES, false, Language.IT);
var source =
new ContentFieldData()
@ -372,7 +375,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
.AddValue("it", "IT")
.AddValue("es", "IT");
var result = FieldConverters.ResolveFallbackLanguages(config_2)(source, field);
var result = FieldConverters.ResolveFallbackLanguages(config)(source, field);
Assert.Equal(expected, result);
}

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs

@ -21,7 +21,7 @@ namespace Squidex.Domain.Apps.Core.Operations.EnrichContent
public class ContentEnrichmentTests
{
private readonly Instant now = Instant.FromUtc(2017, 10, 12, 16, 30, 10);
private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE, Language.EN);
private readonly LanguagesConfig languagesConfig = LanguagesConfig.English.Set(Language.DE);
private readonly Schema schema;
public ContentEnrichmentTests()

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs

@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateEdmSchema
[Fact]
public void Should_build_edm_model()
{
var languagesConfig = LanguagesConfig.Build(Language.DE, Language.EN);
var languagesConfig = LanguagesConfig.English.Set(Language.DE);
var typeFactory = new EdmTypeFactory(names =>
{

4
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs

@ -22,7 +22,7 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema
[Fact]
public void Should_build_json_schema()
{
var languagesConfig = LanguagesConfig.Build(Language.DE, Language.EN);
var languagesConfig = LanguagesConfig.English.Set(Language.DE);
var jsonSchema = schema.BuildJsonSchema(languagesConfig.ToResolver(), (n, s) => new JsonSchema { Reference = s });
var jsonProperties = AllPropertyNames(jsonSchema);
@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema
[Fact]
public void Should_build_data_schema()
{
var languagesConfig = LanguagesConfig.Build(Language.DE, Language.EN);
var languagesConfig = LanguagesConfig.English.Set(Language.DE);
var jsonSchema = schema.BuildJsonSchema(languagesConfig.ToResolver(), (n, s) => new JsonSchema { Reference = s });

9
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs

@ -21,7 +21,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
public class ContentValidationTests
{
private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE, Language.EN);
private readonly LanguagesConfig languagesConfig = LanguagesConfig.English.Set(Language.DE);
private readonly List<ValidationError> errors = new List<ValidationError>();
private readonly ValidationContext context = ValidationTestExtensions.ValidContext;
private Schema schema = new Schema("my-schema");
@ -166,9 +166,10 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
public async Task Should_not_add_error_if_required_field_has_no_value_for_optional_language()
{
var optionalConfig =
LanguagesConfig.Build(
new LanguageConfig(Language.ES, false),
new LanguageConfig(Language.IT, true));
LanguagesConfig.English
.Set(Language.ES)
.Set(Language.IT, true)
.Remove(Language.EN);
schema = schema.AddString(1, "my-field", Partitioning.Language,
new StringFieldProperties { IsRequired = true });

28
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppLanguagesTests.cs

@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
public class GuardAppLanguagesTests
{
private readonly LanguagesConfig languages_0 = LanguagesConfig.Build(Language.DE);
private readonly LanguagesConfig languages_0 = LanguagesConfig.English;
[Fact]
public void CanAddLanguage_should_throw_exception_if_language_is_null()
@ -33,7 +33,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
[Fact]
public void CanAddLanguage_should_throw_exception_if_language_already_added()
{
var command = new AddLanguage { Language = Language.DE };
var command = new AddLanguage { Language = Language.EN };
ValidationAssert.Throws(() => GuardAppLanguages.CanAdd(languages_0, command),
new ValidationError("Language has already been added."));
@ -42,7 +42,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
[Fact]
public void CanAddLanguage_should_not_throw_exception_if_language_valid()
{
var command = new AddLanguage { Language = Language.EN };
var command = new AddLanguage { Language = Language.DE };
GuardAppLanguages.CanAdd(languages_0, command);
}
@ -59,7 +59,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
[Fact]
public void CanRemoveLanguage_should_throw_exception_if_language_not_found()
{
var command = new RemoveLanguage { Language = Language.EN };
var command = new RemoveLanguage { Language = Language.DE };
Assert.Throws<DomainObjectNotFoundException>(() => GuardAppLanguages.CanRemove(languages_0, command));
}
@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
[Fact]
public void CanRemoveLanguage_should_throw_exception_if_language_is_master()
{
var command = new RemoveLanguage { Language = Language.DE };
var command = new RemoveLanguage { Language = Language.EN };
ValidationAssert.Throws(() => GuardAppLanguages.CanRemove(languages_0, command),
new ValidationError("Master language cannot be removed."));
@ -76,9 +76,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
[Fact]
public void CanRemoveLanguage_should_not_throw_exception_if_language_is_valid()
{
var command = new RemoveLanguage { Language = Language.EN };
var command = new RemoveLanguage { Language = Language.DE };
var languages_1 = languages_0.Set(new LanguageConfig(Language.EN));
var languages_1 = languages_0.Set(Language.DE);
GuardAppLanguages.CanRemove(languages_1, command);
}
@ -88,7 +88,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
var command = new UpdateLanguage();
var languages_1 = languages_0.Set(new LanguageConfig(Language.EN));
var languages_1 = languages_0.Set(Language.DE);
ValidationAssert.Throws(() => GuardAppLanguages.CanUpdate(languages_1, command),
new ValidationError("Language code is required.", "Language"));
@ -97,9 +97,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
[Fact]
public void CanUpdateLanguage_should_throw_exception_if_language_is_optional_and_master()
{
var command = new UpdateLanguage { Language = Language.DE, IsOptional = true };
var command = new UpdateLanguage { Language = Language.EN, IsOptional = true };
var languages_1 = languages_0.Set(new LanguageConfig(Language.EN));
var languages_1 = languages_0.Set(Language.DE);
ValidationAssert.Throws(() => GuardAppLanguages.CanUpdate(languages_1, command),
new ValidationError("Master language cannot be made optional.", "IsMaster"));
@ -108,9 +108,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
[Fact]
public void CanUpdateLanguage_should_throw_exception_if_language_has_invalid_fallback()
{
var command = new UpdateLanguage { Language = Language.DE, Fallback = new List<Language> { Language.IT } };
var command = new UpdateLanguage { Language = Language.EN, Fallback = new List<Language> { Language.IT } };
var languages_1 = languages_0.Set(new LanguageConfig(Language.EN));
var languages_1 = languages_0.Set(Language.DE);
ValidationAssert.Throws(() => GuardAppLanguages.CanUpdate(languages_1, command),
new ValidationError("App does not have fallback language 'Italian'.", "Fallback"));
@ -121,7 +121,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
var command = new UpdateLanguage { Language = Language.IT };
var languages_1 = languages_0.Set(new LanguageConfig(Language.EN));
var languages_1 = languages_0.Set(Language.DE);
Assert.Throws<DomainObjectNotFoundException>(() => GuardAppLanguages.CanUpdate(languages_1, command));
}
@ -131,7 +131,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
var command = new UpdateLanguage { Language = Language.DE, Fallback = new List<Language> { Language.EN } };
var languages_1 = languages_0.Set(new LanguageConfig(Language.EN));
var languages_1 = languages_0.Set(Language.DE);
GuardAppLanguages.CanUpdate(languages_1, command);
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs

@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
private static readonly IBsonSerializerRegistry Registry = BsonSerializer.SerializerRegistry;
private static readonly IBsonSerializer<MongoContentEntity> Serializer = BsonSerializer.SerializerRegistry.GetSerializer<MongoContentEntity>();
private readonly Schema schemaDef;
private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE);
private readonly LanguagesConfig languagesConfig = LanguagesConfig.English.Set(Language.DE);
static MongoDbQueryTests()
{

30
backend/tools/TestSuite/TestSuite.ApiTests/AppTests.cs

@ -307,7 +307,7 @@ namespace TestSuite.ApiTests
Assert.True(languageEN_1.IsMaster);
// STEP 3: Update English language.
// STEP 3: Update German language.
var updateRequest1 = new UpdateLanguageDto
{
Fallback = new string[]
@ -325,43 +325,43 @@ namespace TestSuite.ApiTests
Assert.True(languageDE_2.IsOptional);
// STEP 4: Update German language.
// STEP 4: Update Italian language.
var updateRequest2 = new UpdateLanguageDto
{
Fallback = new string[]
{
"fr",
"it"
"de"
}
};
var languages_3 = await _.Apps.PutLanguageAsync(appName, "en", updateRequest2);
var languageEN_3 = languages_3.Items.First(x => x.Iso2Code == "en");
var languages_3 = await _.Apps.PutLanguageAsync(appName, "it", updateRequest2);
var languageDE_3 = languages_3.Items.First(x => x.Iso2Code == "it");
Assert.Equal(new string[] { "fr", "it" }, languageEN_3.Fallback.ToArray());
Assert.Equal(new string[] { "fr", "de" }, languageDE_3.Fallback.ToArray());
// STEP 5: Change master language.
var masterRequest = new UpdateLanguageDto { IsMaster = true };
var languages_4 = await _.Apps.PutLanguageAsync(appName, "de", masterRequest);
var languages_4 = await _.Apps.PutLanguageAsync(appName, "it", masterRequest);
var languageDE_4 = languages_4.Items.First(x => x.Iso2Code == "de");
var languageIT_4 = languages_4.Items.First(x => x.Iso2Code == "it");
var languageEN_4 = languages_4.Items.First(x => x.Iso2Code == "en");
Assert.True(languageDE_4.IsMaster);
Assert.False(languageDE_4.IsOptional);
Assert.True(languageIT_4.IsMaster);
Assert.False(languageIT_4.IsOptional);
Assert.False(languageEN_4.IsMaster);
Assert.Empty(languageDE_4.Fallback);
Assert.Equal(new string[] { "de", "en", "fr", "it" }, languages_4.Items.Select(x => x.Iso2Code).ToArray());
Assert.Empty(languageIT_4.Fallback);
Assert.Equal(new string[] { "it", "de", "en", "fr" }, languages_4.Items.Select(x => x.Iso2Code).ToArray());
// STEP 6: Remove language.
var languages_5 = await _.Apps.DeleteLanguageAsync(appName, "fr");
var languageEN_5 = languages_5.Items.First(x => x.Iso2Code == "en");
var languageDE_5 = languages_5.Items.First(x => x.Iso2Code == "de");
Assert.Equal(new string[] { "it" }, languageEN_5.Fallback.ToArray());
Assert.Equal(new string[] { "de", "en", "it" }, languages_5.Items.Select(x => x.Iso2Code).ToArray());
Assert.Equal(new string[] { "it" }, languageDE_5.Fallback.ToArray());
Assert.Equal(new string[] { "it", "de", "en" }, languages_5.Items.Select(x => x.Iso2Code).ToArray());
}
}
}

Loading…
Cancel
Save