diff --git a/src/Squidex.Core/ContentEnricher.cs b/src/Squidex.Core/ContentEnricher.cs index 387b13c32..c1ab9010f 100644 --- a/src/Squidex.Core/ContentEnricher.cs +++ b/src/Squidex.Core/ContentEnricher.cs @@ -11,29 +11,27 @@ using Squidex.Core.Contents; using Squidex.Core.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Json; -using System.Collections.Generic; namespace Squidex.Core { public sealed class ContentEnricher { private readonly Schema schema; - private readonly HashSet languages; + private readonly LanguagesConfig languagesConfig; - public ContentEnricher(HashSet languages, Schema schema) + public ContentEnricher(LanguagesConfig languagesConfig, Schema schema) { Guard.NotNull(schema, nameof(schema)); - Guard.NotNull(languages, nameof(languages)); + Guard.NotNull(languagesConfig, nameof(languagesConfig)); this.schema = schema; - this.languages = languages; + this.languagesConfig = languagesConfig; } public void Enrich(ContentData data) { Guard.NotNull(data, nameof(data)); - Guard.NotEmpty(languages, nameof(languages)); foreach (var field in schema.FieldsByName.Values) { @@ -41,9 +39,9 @@ namespace Squidex.Core if (field.RawProperties.IsLocalizable) { - foreach (var language in languages) + foreach (var languageConfig in languagesConfig) { - Enrich(field, fieldData, language); + Enrich(field, fieldData, languageConfig.Language); } } else @@ -61,7 +59,6 @@ namespace Squidex.Core private static void Enrich(Field field, ContentFieldData fieldData, Language language) { Guard.NotNull(fieldData, nameof(fieldData)); - Guard.NotNull(language, nameof(language)); var defaultValue = field.RawProperties.GetDefaultValue(); @@ -70,9 +67,9 @@ namespace Squidex.Core return; } - if (!fieldData.TryGetValue(language.Iso2Code, out JToken value) || value == null || value.Type == JTokenType.Null) + if (!fieldData.TryGetValue(language, out JToken value) || value == null || value.Type == JTokenType.Null) { - fieldData.AddValue(language.Iso2Code, defaultValue); + fieldData.AddValue(language, defaultValue); } } } diff --git a/src/Squidex.Core/ContentExtensions.cs b/src/Squidex.Core/ContentExtensions.cs index 7ad7ac1b7..09abfd048 100644 --- a/src/Squidex.Core/ContentExtensions.cs +++ b/src/Squidex.Core/ContentExtensions.cs @@ -16,18 +16,18 @@ namespace Squidex.Core { public static class ContentExtensions { - public static ContentData Enrich(this ContentData data, Schema schema, HashSet languages) + public static ContentData Enrich(this ContentData data, Schema schema, LanguagesConfig languagesConfig) { - var enricher = new ContentEnricher(languages, schema); + var enricher = new ContentEnricher(languagesConfig, schema); enricher.Enrich(data); return data; } - public static async Task ValidateAsync(this ContentData data, Schema schema, HashSet languages, IList errors) + public static async Task ValidateAsync(this ContentData data, Schema schema, LanguagesConfig languagesConfig, IList errors) { - var validator = new ContentValidator(schema, languages); + var validator = new ContentValidator(schema, languagesConfig); await validator.ValidateAsync(data); @@ -37,9 +37,9 @@ namespace Squidex.Core } } - public static async Task ValidatePartialAsync(this ContentData data, Schema schema, HashSet languages, IList errors) + public static async Task ValidatePartialAsync(this ContentData data, Schema schema, LanguagesConfig languagesConfig, IList errors) { - var validator = new ContentValidator(schema, languages); + var validator = new ContentValidator(schema, languagesConfig); await validator.ValidatePartialAsync(data); diff --git a/src/Squidex.Core/ContentValidator.cs b/src/Squidex.Core/ContentValidator.cs index 39fc32f2a..775f1e58e 100644 --- a/src/Squidex.Core/ContentValidator.cs +++ b/src/Squidex.Core/ContentValidator.cs @@ -7,7 +7,6 @@ // ========================================================================== using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json.Linq; using Squidex.Core.Contents; @@ -19,22 +18,22 @@ namespace Squidex.Core public sealed class ContentValidator { private readonly Schema schema; - private readonly HashSet languages; + private readonly LanguagesConfig languagesConfig; private readonly List errors = new List(); - public ContentValidator(Schema schema, HashSet languages) + public IReadOnlyList Errors + { + get { return errors; } + } + + public ContentValidator(Schema schema, LanguagesConfig languagesConfig) { Guard.NotNull(schema, nameof(schema)); - Guard.NotNull(languages, nameof(languages)); + Guard.NotNull(languagesConfig, nameof(languagesConfig)); this.schema = schema; - this.languages = languages; - } - - public IReadOnlyList Errors - { - get { return errors; } + this.languagesConfig = languagesConfig; } public async Task ValidatePartialAsync(ContentData data) @@ -47,51 +46,40 @@ namespace Squidex.Core if (!schema.FieldsByName.TryGetValue(fieldData.Key, out Field field)) { - AddError(" is not a known field", fieldName); + errors.AddError(" is not a known field", fieldName); } else { if (field.RawProperties.IsLocalizable) { - await ValidateLocalizableFieldPartialAsync(field, fieldData.Value); + await ValidateFieldPartialAsync(field, fieldData.Value, languagesConfig); } else { - await ValidateNonLocalizableFieldPartialAsync(field, fieldData.Value); + await ValidateFieldPartialAsync(field, fieldData.Value, LanguagesConfig.Invariant); } } } } - private async Task ValidateLocalizableFieldPartialAsync(Field field, ContentFieldData fieldData) + private async Task ValidateFieldPartialAsync(Field field, ContentFieldData fieldData, LanguagesConfig languages) { foreach (var languageValue in fieldData) { - if (!Language.TryGetLanguage(languageValue.Key, out Language language)) + if (!Language.TryGetLanguage(languageValue.Key, out var language)) { - AddError($" has an invalid language '{languageValue.Key}'", field); + errors.AddError($" has an invalid language '{languageValue.Key}'", field); } - else if (!languages.Contains(language)) + else if (!languages.TryGetConfig(language, out var languageConfig)) { - AddError($" has an unsupported language '{languageValue.Key}'", field); + errors.AddError($" has an unsupported language '{languageValue.Key}'", field); } else { - await ValidateAsync(field, languageValue.Value, language); - } - } - } - - private async Task ValidateNonLocalizableFieldPartialAsync(Field field, ContentFieldData fieldData) - { - if (fieldData.Keys.Any(x => x != Language.Invariant.Iso2Code)) - { - AddError($" can only contain a single entry for invariant language ({Language.Invariant.Iso2Code})", field); - } + var config = languageConfig; - if (fieldData.TryGetValue(Language.Invariant.Iso2Code, out JToken value)) - { - await ValidateAsync(field, value); + await field.ValidateAsync(languageValue.Value, config.IsOptional, m => errors.AddError(m, field, config.Language)); + } } } @@ -107,11 +95,11 @@ namespace Squidex.Core if (field.RawProperties.IsLocalizable) { - await ValidateLocalizableFieldAsync(field, fieldData); + await ValidateFieldAsync(field, fieldData, languagesConfig); } else { - await ValidateNonLocalizableField(field, fieldData); + await ValidateFieldAsync(field, fieldData, LanguagesConfig.Invariant); } } } @@ -122,69 +110,32 @@ namespace Squidex.Core { if (!schema.FieldsByName.ContainsKey(fieldData.Key)) { - AddError(" is not a known field", fieldData.Key); + errors.AddError(" is not a known field", fieldData.Key); } } } - private async Task ValidateLocalizableFieldAsync(Field field, ContentFieldData fieldData) + private async Task ValidateFieldAsync(Field field, ContentFieldData fieldData, LanguagesConfig languages) { foreach (var valueLanguage in fieldData.Keys) { if (!Language.TryGetLanguage(valueLanguage, out Language language)) { - AddError($" has an invalid language '{valueLanguage}'", field); + errors.AddError($" has an invalid language '{valueLanguage}'", field); } else if (!languages.Contains(language)) { - AddError($" has an unsupported language '{valueLanguage}'", field); + errors.AddError($" has an unsupported language '{valueLanguage}'", field); } } - foreach (var language in languages) - { - var value = fieldData.GetOrCreate(language.Iso2Code, k => JValue.CreateNull()); - - await ValidateAsync(field, value, language); - } - } - - private async Task ValidateNonLocalizableField(Field field, ContentFieldData fieldData) - { - if (fieldData.Keys.Any(x => x != Language.Invariant.Iso2Code)) + foreach (var languageConfig in languages) { - AddError($" can only contain a single entry for invariant language ({Language.Invariant.Iso2Code})", field); - } + var config = languageConfig; + var value = fieldData.GetOrCreate(config.Language, k => JValue.CreateNull()); - var value = fieldData.GetOrCreate(Language.Invariant.Iso2Code, k => JValue.CreateNull()); - - await ValidateAsync(field, value); - } - - private Task ValidateAsync(Field field, JToken value, Language language = null) - { - return field.ValidateAsync(value, m => AddError(m, field, language)); - } - - private void AddError(string message, Field field, Language language = null) - { - var displayName = !string.IsNullOrWhiteSpace(field.RawProperties.Label) ? field.RawProperties.Label : field.Name; - - if (language != null) - { - displayName += $" ({language.Iso2Code})"; + await field.ValidateAsync(value, config.IsOptional, m => errors.AddError(m, field, config.Language)); } - - message = message.Replace("", displayName); - - errors.Add(new ValidationError(message, field.Name)); - } - - private void AddError(string message, string fieldName) - { - message = message.Replace("", fieldName); - - errors.Add(new ValidationError(message, fieldName)); } } } diff --git a/src/Squidex.Core/Contents/ContentData.cs b/src/Squidex.Core/Contents/ContentData.cs index a3715ebdd..397a2daf3 100644 --- a/src/Squidex.Core/Contents/ContentData.cs +++ b/src/Squidex.Core/Contents/ContentData.cs @@ -122,13 +122,13 @@ namespace Squidex.Core.Contents return result; } - public ContentData ToApiModel(Schema schema, IReadOnlyCollection languages, Language masterLanguage, bool excludeHidden = true) + public ContentData ToApiModel(Schema schema, LanguagesConfig languagesConfig, IReadOnlyCollection languagePreferences = null, bool excludeHidden = true) { Guard.NotNull(schema, nameof(schema)); - Guard.NotNull(languages, nameof(languages)); - Guard.NotNull(masterLanguage, nameof(masterLanguage)); + Guard.NotNull(languagesConfig, nameof(languagesConfig)); - var invariantCode = Language.Invariant.Iso2Code; + var codeForInvariant = Language.Invariant.Iso2Code; + var codeForMasterLanguage = languagesConfig.Master.Language.Iso2Code; var result = new ContentData(); @@ -144,15 +144,15 @@ namespace Squidex.Core.Contents if (field.RawProperties.IsLocalizable) { - foreach (var language in languages) + foreach (var languageConfig in languagesConfig) { - var languageCode = language.Iso2Code; + string languageCode = languageConfig.Language; if (fieldValues.TryGetValue(languageCode, out JToken value)) { fieldResult.Add(languageCode, value); } - else if (language.Equals(masterLanguage) && fieldValues.TryGetValue(invariantCode, out value)) + else if (languageConfig == languagesConfig.Master && fieldValues.TryGetValue(codeForInvariant, out value)) { fieldResult.Add(languageCode, value); } @@ -160,17 +160,17 @@ namespace Squidex.Core.Contents } else { - if (fieldValues.TryGetValue(invariantCode, out JToken value)) + if (fieldValues.TryGetValue(codeForInvariant, out JToken value)) { - fieldResult.Add(invariantCode, value); + fieldResult.Add(codeForInvariant, value); } - else if (fieldValues.TryGetValue(masterLanguage.Iso2Code, out value)) + else if (fieldValues.TryGetValue(codeForMasterLanguage, out value)) { - fieldResult.Add(invariantCode, value); + fieldResult.Add(codeForInvariant, value); } else if (fieldValues.Count > 0) { - fieldResult.Add(invariantCode, fieldValues.Values.First()); + fieldResult.Add(codeForInvariant, fieldValues.Values.First()); } } @@ -180,13 +180,20 @@ namespace Squidex.Core.Contents return result; } - public object ToLanguageModel(IReadOnlyCollection languagePreferences = null) + public object ToLanguageModel(LanguagesConfig languagesConfig, IReadOnlyCollection languagePreferences = null) { - if (languagePreferences == null) + Guard.NotNull(languagesConfig, nameof(languagesConfig)); + + if (languagePreferences == null || languagePreferences.Count == 0) { return this; } + if (languagePreferences.Count == 1 && languagesConfig.TryGetConfig(languagePreferences.First(), out var languageConfig)) + { + languagePreferences = languagePreferences.Union(languageConfig.Fallback).ToList(); + } + var result = new Dictionary(); foreach (var fieldValue in this) @@ -195,9 +202,7 @@ namespace Squidex.Core.Contents foreach (var language in languagePreferences) { - var languageCode = language.Iso2Code; - - if (fieldValues.TryGetValue(languageCode, out JToken value) && value != null) + if (fieldValues.TryGetValue(language, out JToken value) && value != null) { result[fieldValue.Key] = value; diff --git a/src/Squidex.Core/Contents/ContentFieldData.cs b/src/Squidex.Core/Contents/ContentFieldData.cs index 90ec8d79d..a8e9ab0bc 100644 --- a/src/Squidex.Core/Contents/ContentFieldData.cs +++ b/src/Squidex.Core/Contents/ContentFieldData.cs @@ -22,7 +22,7 @@ namespace Squidex.Core.Contents public ContentFieldData SetValue(JToken value) { - this[Language.Invariant.Iso2Code] = value; + this[Language.Invariant] = value; return this; } diff --git a/src/Squidex.Core/FieldExtensions.cs b/src/Squidex.Core/FieldExtensions.cs index 1128a2190..507b356b4 100644 --- a/src/Squidex.Core/FieldExtensions.cs +++ b/src/Squidex.Core/FieldExtensions.cs @@ -7,6 +7,7 @@ // ========================================================================== using System; +using System.Collections.Generic; using System.Threading.Tasks; using Newtonsoft.Json.Linq; using Squidex.Core.Schemas; @@ -17,7 +18,27 @@ namespace Squidex.Core { public static class FieldExtensions { - public static async Task ValidateAsync(this Field field, JToken value, Action addError) + public static void AddError(this ICollection errors, string message, Field field, Language language = null) + { + AddError(errors, message, !string.IsNullOrWhiteSpace(field.RawProperties.Label) ? field.RawProperties.Label : field.Name, field.Name, language); + } + + public static void AddError(this ICollection errors, string message, string fieldName, Language language = null) + { + AddError(errors, message, fieldName, fieldName, language); + } + + public static void AddError(this ICollection errors, string message, string displayName, string fieldName, Language language = null) + { + if (language != null && language != Language.Invariant) + { + displayName += $" ({language.Iso2Code})"; + } + + errors.Add(new ValidationError(message.Replace("", displayName), fieldName)); + } + + public static async Task ValidateAsync(this Field field, JToken value, bool isOptional, Action addError) { Guard.NotNull(value, nameof(value)); @@ -27,7 +48,7 @@ namespace Squidex.Core foreach (var validator in field.Validators) { - await validator.ValidateAsync(typedValue, addError); + await validator.ValidateAsync(typedValue, isOptional, addError); } } catch diff --git a/src/Squidex.Core/LanguageConfig.cs b/src/Squidex.Core/LanguageConfig.cs new file mode 100644 index 000000000..4e4503ca2 --- /dev/null +++ b/src/Squidex.Core/LanguageConfig.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// LanguageConfig.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Infrastructure; +using System.Collections.Immutable; + +namespace Squidex.Core +{ + public sealed class LanguageConfig + { + public bool IsOptional { get; } + + public Language Language { get; } + + public ImmutableList Fallback { get; } + + public LanguageConfig(Language language, bool isOptional, params Language[] fallback) + : this(language, isOptional, fallback?.ToImmutableList()) + { + } + + public LanguageConfig(Language language, bool isOptional, IEnumerable fallback) + : this(language, isOptional, fallback?.ToImmutableList()) + { + } + + public LanguageConfig(Language language, bool isOptional = false, ImmutableList fallback = null) + { + Guard.NotNull(language, nameof(language)); + + Language = language; + + IsOptional = isOptional; + + Fallback = fallback ?? ImmutableList.Empty; + } + } +} diff --git a/src/Squidex.Core/LanguagesConfig.cs b/src/Squidex.Core/LanguagesConfig.cs new file mode 100644 index 000000000..4bf5187b8 --- /dev/null +++ b/src/Squidex.Core/LanguagesConfig.cs @@ -0,0 +1,184 @@ +// ========================================================================== +// LanguagesConfig.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Squidex.Infrastructure; + +// ReSharper disable InvertIf + +namespace Squidex.Core +{ + public sealed class LanguagesConfig : IEnumerable + { + private readonly ImmutableDictionary languages; + private readonly LanguageConfig master; + + public static readonly LanguagesConfig Empty = Create(); + public static readonly LanguagesConfig Invariant = Create(Language.Invariant); + + public LanguageConfig Master + { + get { return master; } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return languages.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return languages.Values.GetEnumerator(); + } + + private LanguagesConfig(ImmutableDictionary languages, LanguageConfig master) + { + this.languages = ValidateLanguages(languages); + + this.master = master; + } + + public static LanguagesConfig Create(ICollection languageConfigs) + { + Guard.NotNull(languageConfigs, nameof(languageConfigs)); + + var validated = ValidateLanguages(languageConfigs.ToImmutableDictionary(c => c.Language)); + + return new LanguagesConfig(validated, languageConfigs.FirstOrDefault()); + } + + public static LanguagesConfig Create(params Language[] languages) + { + Guard.NotNull(languages, nameof(languages)); + + var languageConfigs = languages.Select(l => new LanguageConfig(l)).ToList(); + + return Create(languageConfigs); + } + + public LanguagesConfig MakeMaster(Language language) + { + ThrowIfNotFound(language); + + return new LanguagesConfig(languages, languages[language]); + } + + public LanguagesConfig Add(Language language) + { + ThrowIfFound(language, () => $"Cannot add language '{language.Iso2Code}'."); + + var newLanguages = languages.Add(language, new LanguageConfig(language)); + + return new LanguagesConfig(newLanguages, master ?? newLanguages.Values.First()); + } + + public LanguagesConfig Update(Language language, bool isOptional, IEnumerable fallback) + { + ThrowIfNotFound(language); + + if (isOptional) + { + ThrowIfMaster(language, () => $"Cannot cannot make language '{language.Iso2Code}' optional"); + } + + var newLanguages = ValidateLanguages(languages.SetItem(language, new LanguageConfig(language, isOptional, fallback))); + + return new LanguagesConfig(newLanguages, master); + } + + public LanguagesConfig Remove(Language language) + { + ThrowIfNotFound(language); + ThrowIfMaster(language, () => $"Cannot remove language '{language.Iso2Code}'"); + + var newLanguages = languages.Remove(language); + + foreach (var languageConfig in newLanguages.Values) + { + if (languageConfig.Fallback.Contains(language)) + { + newLanguages = + newLanguages.SetItem(languageConfig.Language, + new LanguageConfig( + languageConfig.Language, + languageConfig.IsOptional, + languageConfig.Fallback.Remove(language))); + } + } + + return new LanguagesConfig(newLanguages, master); + } + + public bool TryGetConfig(Language language, out LanguageConfig value) + { + return languages.TryGetValue(language, out value); + } + + public bool Contains(Language language) + { + return language != null && languages.ContainsKey(language); + } + + private static ImmutableDictionary ValidateLanguages(ImmutableDictionary languages) + { + var errors = new List(); + + foreach (var languageConfig in languages.Values) + { + foreach (var fallback in languageConfig.Fallback) + { + if (!languages.ContainsKey(fallback)) + { + var message = $"Config for language '{languageConfig.Language.Iso2Code}' contains unsupported fallback language '{fallback.Iso2Code}'"; + + errors.Add(new ValidationError(message)); + } + } + } + + if (errors.Count > 0) + { + throw new ValidationException("Cannot configure language", errors); + } + + return languages; + } + + private void ThrowIfNotFound(Language language) + { + if (!Contains(language)) + { + throw new DomainObjectNotFoundException(language, "Languages", typeof(LanguagesConfig)); + } + } + + private void ThrowIfFound(Language language, Func message) + { + if (Contains(language)) + { + var error = new ValidationError("Language is already part of the app", "Language"); + + throw new ValidationException(message(), error); + } + } + + private void ThrowIfMaster(Language language, Func message) + { + if (master?.Language == language) + { + var error = new ValidationError("Language is the master language", "Language"); + + throw new ValidationException(message(), error); + } + } + } +} diff --git a/src/Squidex.Core/Schemas/Field.cs b/src/Squidex.Core/Schemas/Field.cs index 60d6d2b2a..f2e9cbd89 100644 --- a/src/Squidex.Core/Schemas/Field.cs +++ b/src/Squidex.Core/Schemas/Field.cs @@ -104,15 +104,15 @@ namespace Squidex.Core.Schemas return Clone(clone => clone.name = newName); } - public void AddToEdmType(EdmStructuredType edmType, IEnumerable languages, string schemaName, Func typeResolver) + public void AddToEdmType(EdmStructuredType edmType, LanguagesConfig languagesConfig, string schemaName, Func typeResolver) { Guard.NotNull(edmType, nameof(edmType)); - Guard.NotNull(languages, nameof(languages)); Guard.NotNull(typeResolver, nameof(typeResolver)); + Guard.NotNull(languagesConfig, nameof(languagesConfig)); if (!RawProperties.IsLocalizable) { - languages = new[] { Language.Invariant }; + languagesConfig = LanguagesConfig.Invariant; } var edmValueType = CreateEdmType(); @@ -124,35 +124,35 @@ namespace Squidex.Core.Schemas var languageType = typeResolver(new EdmComplexType("Squidex", $"{schemaName}{Name.ToPascalCase()}Property")); - foreach (var language in languages) + foreach (var languageConfig in languagesConfig) { - languageType.AddStructuralProperty(language.Iso2Code, edmValueType); + languageType.AddStructuralProperty(languageConfig.Language, edmValueType); } edmType.AddStructuralProperty(Name, new EdmComplexTypeReference(languageType, false)); } - public void AddToJsonSchema(JsonSchema4 schema, IEnumerable languages, string schemaName, Func schemaResolver) + public void AddToJsonSchema(JsonSchema4 schema, LanguagesConfig languagesConfig, string schemaName, Func schemaResolver) { Guard.NotNull(schema, nameof(schema)); - Guard.NotNull(languages, nameof(languages)); Guard.NotNull(schemaResolver, nameof(schemaResolver)); + Guard.NotNull(languagesConfig, nameof(languagesConfig)); if (!RawProperties.IsLocalizable) { - languages = new[] { Language.Invariant }; + languagesConfig = LanguagesConfig.Invariant; } var languagesProperty = CreateProperty(); var languagesObject = new JsonSchema4 { Type = JsonObjectType.Object, AllowAdditionalProperties = false }; - foreach (var language in languages) + foreach (var languageConfig in languagesConfig) { - var languageProperty = new JsonProperty { Description = language.EnglishName, IsRequired = RawProperties.IsRequired }; + var languageProperty = new JsonProperty { Description = languageConfig.Language.EnglishName, IsRequired = RawProperties.IsRequired }; PrepareJsonSchema(languageProperty, schemaResolver); - languagesObject.Properties.Add(language.Iso2Code, languageProperty); + languagesObject.Properties.Add(languageConfig.Language, languageProperty); } languagesProperty.SchemaReference = schemaResolver($"{schemaName}{Name.ToPascalCase()}Property", languagesObject); diff --git a/src/Squidex.Core/Schemas/Schema.cs b/src/Squidex.Core/Schemas/Schema.cs index 08c438d4b..cbf83f260 100644 --- a/src/Squidex.Core/Schemas/Schema.cs +++ b/src/Squidex.Core/Schemas/Schema.cs @@ -202,10 +202,10 @@ namespace Squidex.Core.Schemas return new Schema(name, isPublished, properties, newFields); } - public EdmComplexType BuildEdmType(HashSet languages, Func typeResolver) + public EdmComplexType BuildEdmType(LanguagesConfig languagesConfig, Func typeResolver) { - Guard.NotEmpty(languages, nameof(languages)); Guard.NotNull(typeResolver, nameof(typeResolver)); + Guard.NotNull(languagesConfig, nameof(languagesConfig)); var schemaName = Name.ToPascalCase(); @@ -213,16 +213,16 @@ namespace Squidex.Core.Schemas foreach (var field in fieldsByName.Values.Where(x => !x.IsHidden)) { - field.AddToEdmType(edmType, languages, schemaName, typeResolver); + field.AddToEdmType(edmType, languagesConfig, schemaName, typeResolver); } return edmType; } - public JsonSchema4 BuildJsonSchema(HashSet languages, Func schemaResolver) + public JsonSchema4 BuildJsonSchema(LanguagesConfig languagesConfig, Func schemaResolver) { - Guard.NotEmpty(languages, nameof(languages)); Guard.NotNull(schemaResolver, nameof(schemaResolver)); + Guard.NotNull(languagesConfig, nameof(languagesConfig)); var schemaName = Name.ToPascalCase(); @@ -230,7 +230,7 @@ namespace Squidex.Core.Schemas foreach (var field in fieldsByName.Values.Where(x => !x.IsHidden)) { - field.AddToJsonSchema(schema, languages, schemaName, schemaResolver); + field.AddToJsonSchema(schema, languagesConfig, schemaName, schemaResolver); } return schema; diff --git a/src/Squidex.Core/Schemas/Validators/AllowedValuesValidator.cs b/src/Squidex.Core/Schemas/Validators/AllowedValuesValidator.cs index 0a7402e32..a30455357 100644 --- a/src/Squidex.Core/Schemas/Validators/AllowedValuesValidator.cs +++ b/src/Squidex.Core/Schemas/Validators/AllowedValuesValidator.cs @@ -25,7 +25,7 @@ namespace Squidex.Core.Schemas.Validators this.allowedValues = allowedValues; } - public Task ValidateAsync(object value, Action addError) + public Task ValidateAsync(object value, bool isOptional, Action addError) { if (value == null) { diff --git a/src/Squidex.Core/Schemas/Validators/AssetsValidator.cs b/src/Squidex.Core/Schemas/Validators/AssetsValidator.cs index 5a9af1929..770799c25 100644 --- a/src/Squidex.Core/Schemas/Validators/AssetsValidator.cs +++ b/src/Squidex.Core/Schemas/Validators/AssetsValidator.cs @@ -23,13 +23,13 @@ namespace Squidex.Core.Schemas.Validators this.isRequired = isRequired; } - public async Task ValidateAsync(object value, Action addError) + public async Task ValidateAsync(object value, bool isOptional, Action addError) { var assets = value as AssetsValue; if (assets == null || assets.AssetIds.Count == 0) { - if (isRequired) + if (isRequired && !isOptional) { addError(" is required"); } diff --git a/src/Squidex.Core/Schemas/Validators/IValidator.cs b/src/Squidex.Core/Schemas/Validators/IValidator.cs index a604e9a70..1720aa40c 100644 --- a/src/Squidex.Core/Schemas/Validators/IValidator.cs +++ b/src/Squidex.Core/Schemas/Validators/IValidator.cs @@ -13,6 +13,6 @@ namespace Squidex.Core.Schemas.Validators { public interface IValidator { - Task ValidateAsync(object value, Action addError); + Task ValidateAsync(object value, bool isOptional, Action addError); } } diff --git a/src/Squidex.Core/Schemas/Validators/PatternValidator.cs b/src/Squidex.Core/Schemas/Validators/PatternValidator.cs index aa806be54..0e41e3ef5 100644 --- a/src/Squidex.Core/Schemas/Validators/PatternValidator.cs +++ b/src/Squidex.Core/Schemas/Validators/PatternValidator.cs @@ -28,11 +28,11 @@ namespace Squidex.Core.Schemas.Validators regex = new Regex("^" + pattern + "$"); } - public Task ValidateAsync(object value, Action addError) + public Task ValidateAsync(object value, bool isOptional, Action addError) { if (value is string stringValue) { - if (!regex.IsMatch(stringValue)) + if (!string.IsNullOrEmpty(stringValue) && !regex.IsMatch(stringValue)) { if (string.IsNullOrWhiteSpace(errorMessage)) { diff --git a/src/Squidex.Core/Schemas/Validators/RangeValidator.cs b/src/Squidex.Core/Schemas/Validators/RangeValidator.cs index 0b5669228..117957e31 100644 --- a/src/Squidex.Core/Schemas/Validators/RangeValidator.cs +++ b/src/Squidex.Core/Schemas/Validators/RangeValidator.cs @@ -28,7 +28,7 @@ namespace Squidex.Core.Schemas.Validators this.max = max; } - public Task ValidateAsync(object value, Action addError) + public Task ValidateAsync(object value, bool isOptional, Action addError) { if (value == null) { diff --git a/src/Squidex.Core/Schemas/Validators/RequiredStringValidator.cs b/src/Squidex.Core/Schemas/Validators/RequiredStringValidator.cs index f64d61c9a..c0986e19d 100644 --- a/src/Squidex.Core/Schemas/Validators/RequiredStringValidator.cs +++ b/src/Squidex.Core/Schemas/Validators/RequiredStringValidator.cs @@ -21,9 +21,9 @@ namespace Squidex.Core.Schemas.Validators this.validateEmptyStrings = validateEmptyStrings; } - public Task ValidateAsync(object value, Action addError) + public Task ValidateAsync(object value, bool isOptional, Action addError) { - if (value != null && !(value is string)) + if (isOptional || (value != null && !(value is string))) { return TaskHelper.Done; } diff --git a/src/Squidex.Core/Schemas/Validators/RequiredValidator.cs b/src/Squidex.Core/Schemas/Validators/RequiredValidator.cs index 5e86ad0cb..cc591eea9 100644 --- a/src/Squidex.Core/Schemas/Validators/RequiredValidator.cs +++ b/src/Squidex.Core/Schemas/Validators/RequiredValidator.cs @@ -14,9 +14,9 @@ namespace Squidex.Core.Schemas.Validators { public class RequiredValidator : IValidator { - public Task ValidateAsync(object value, Action addError) + public Task ValidateAsync(object value, bool isOptional, Action addError) { - if (value == null) + if (value == null && !isOptional) { addError(" is required"); } diff --git a/src/Squidex.Core/Schemas/Validators/StringLengthValidator.cs b/src/Squidex.Core/Schemas/Validators/StringLengthValidator.cs index 73360646c..29683f670 100644 --- a/src/Squidex.Core/Schemas/Validators/StringLengthValidator.cs +++ b/src/Squidex.Core/Schemas/Validators/StringLengthValidator.cs @@ -30,9 +30,9 @@ namespace Squidex.Core.Schemas.Validators this.maxLength = maxLength; } - public Task ValidateAsync(object value, Action addError) + public Task ValidateAsync(object value, bool isOptional, Action addError) { - if (value is string stringValue) + if (value is string stringValue && !string.IsNullOrEmpty(stringValue)) { if (minLength.HasValue && stringValue.Length < minLength.Value) { diff --git a/src/Squidex.Events/Apps/AppLanguageUpdated.cs b/src/Squidex.Events/Apps/AppLanguageUpdated.cs new file mode 100644 index 000000000..10b0afda0 --- /dev/null +++ b/src/Squidex.Events/Apps/AppLanguageUpdated.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// AppLanguageUpdated.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Infrastructure; + +namespace Squidex.Events.Apps +{ + [TypeName("AppLanguageUpdated")] + public sealed class AppLanguageUpdated : AppEvent + { + public Language Language { get; set; } + + public bool IsOptional { get; set; } + + public List Fallback { get; set; } + } +} diff --git a/src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs b/src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs index d7774e075..c9bf4a866 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs @@ -128,6 +128,11 @@ namespace Squidex.Infrastructure.CQRS.Events { var @event = ParseEvent(storedEvent); + if (@event == null) + { + return; + } + await DispatchConsumer(@event, eventConsumer); await eventConsumerInfoRepository.SetLastHandledEventNumberAsync(consumerName, storedEvent.EventNumber); } @@ -213,6 +218,10 @@ namespace Squidex.Infrastructure.CQRS.Events return @event; } + catch (ArgumentException) + { + return null; + } catch (Exception ex) { log.LogFatal(ex, w => w diff --git a/src/Squidex.Infrastructure/Language.cs b/src/Squidex.Infrastructure/Language.cs index cfef595bd..98dc58223 100644 --- a/src/Squidex.Infrastructure/Language.cs +++ b/src/Squidex.Infrastructure/Language.cs @@ -82,6 +82,16 @@ namespace Squidex.Infrastructure return AllLanguagesField.TryGetValue(iso2Code, out language); } + public static implicit operator string(Language language) + { + return language?.Iso2Code; + } + + public static implicit operator Language(string iso2Code) + { + return GetLanguage(iso2Code); + } + public static Language ParseOrNull(string input) { if (string.IsNullOrWhiteSpace(input)) diff --git a/src/Squidex.Infrastructure/RefToken.cs b/src/Squidex.Infrastructure/RefToken.cs index 96848c1e2..bbb209dfc 100644 --- a/src/Squidex.Infrastructure/RefToken.cs +++ b/src/Squidex.Infrastructure/RefToken.cs @@ -31,7 +31,7 @@ namespace Squidex.Infrastructure { Guard.NotNullOrEmpty(input, nameof(input)); - var parts = input.Split(new [] { ':' }, StringSplitOptions.RemoveEmptyEntries); + var parts = input.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length < 2) { diff --git a/src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs b/src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs index 7c4e34fe3..46cc40ff8 100644 --- a/src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs +++ b/src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs @@ -6,34 +6,39 @@ // All rights reserved. // ========================================================================== +using System; using System.Collections.Generic; using System.Linq; using MongoDB.Bson.Serialization.Attributes; +using Squidex.Core; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; using Squidex.Read.Apps; +// ReSharper disable InvertIf // ReSharper disable AutoPropertyCanBeMadeGetOnly.Global namespace Squidex.Read.MongoDb.Apps { public sealed class MongoAppEntity : MongoEntity, IAppEntity { + private LanguagesConfig languagesConfig = LanguagesConfig.Empty; + [BsonRequired] [BsonElement] public string Name { get; set; } [BsonRequired] [BsonElement] - public string MasterLanguage { get; set; } + public long Version { get; set; } [BsonRequired] [BsonElement] - public long Version { get; set; } + public string MasterLanguage { get; set; } [BsonRequired] [BsonElement] - public HashSet Languages { get; set; } = new HashSet(); + public List Languages { get; set; } = new List(); [BsonRequired] [BsonElement] @@ -43,6 +48,11 @@ namespace Squidex.Read.MongoDb.Apps [BsonElement] public Dictionary Contributors { get; set; } = new Dictionary(); + public LanguagesConfig LanguagesConfig + { + get { return languagesConfig ?? (languagesConfig = CreateLanguagesConfig()); } + } + IReadOnlyCollection IAppEntity.Clients { get { return Clients.Values; } @@ -53,14 +63,32 @@ namespace Squidex.Read.MongoDb.Apps get { return Contributors.Values; } } - IReadOnlyCollection IAppEntity.Languages + public void UpdateLanguages(Func updater) + { + var newConfig = updater(LanguagesConfig); + + if (languagesConfig != newConfig) + { + languagesConfig = newConfig; + Languages = newConfig.Select(FromLanguageConfig).ToList(); + + MasterLanguage = newConfig.Master.Language; + } + } + + private LanguagesConfig CreateLanguagesConfig() + { + return LanguagesConfig.Create(Languages.Select(ToLanguageConfig).ToList()).MakeMaster(MasterLanguage); + } + + private static MongoAppLanguage FromLanguageConfig(LanguageConfig l) { - get { return Languages.Select(Language.GetLanguage).ToList(); } + return new MongoAppLanguage { Iso2Code = l.Language, IsOptional = l.IsOptional, Fallback = l.Fallback.Select(x => x.Iso2Code).ToList() }; } - Language IAppEntity.MasterLanguage + private static LanguageConfig ToLanguageConfig(MongoAppLanguage l) { - get { return Language.GetLanguage(MasterLanguage); } + return new LanguageConfig(l.Iso2Code, l.IsOptional, l.Fallback?.Select(f => f)); } } } diff --git a/src/Squidex.Read.MongoDb/Apps/MongoAppLanguage.cs b/src/Squidex.Read.MongoDb/Apps/MongoAppLanguage.cs new file mode 100644 index 000000000..a4ecd3a23 --- /dev/null +++ b/src/Squidex.Read.MongoDb/Apps/MongoAppLanguage.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// MongoAppLanguage.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using MongoDB.Bson.Serialization.Attributes; + +namespace Squidex.Read.MongoDb.Apps +{ + public sealed class MongoAppLanguage + { + [BsonRequired] + [BsonElement] + public string Iso2Code { get; set; } + + [BsonRequired] + [BsonElement] + public bool IsOptional { get; set; } + + [BsonRequired] + [BsonElement] + public List Fallback { get; set; } + } +} diff --git a/src/Squidex.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs b/src/Squidex.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs index 96b6a6c41..952a2ffa7 100644 --- a/src/Squidex.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs +++ b/src/Squidex.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs @@ -77,7 +77,7 @@ namespace Squidex.Read.MongoDb.Apps { return Collection.UpdateAsync(@event, headers, a => { - a.Languages.Add(@event.Language.Iso2Code); + a.UpdateLanguages(c => c.Add(@event.Language)); }); } @@ -85,7 +85,15 @@ namespace Squidex.Read.MongoDb.Apps { return Collection.UpdateAsync(@event, headers, a => { - a.Languages.Remove(@event.Language.Iso2Code); + a.UpdateLanguages(c => c.Remove(@event.Language)); + }); + } + + protected Task On(AppLanguageUpdated @event, EnvelopeHeaders headers) + { + return Collection.UpdateAsync(@event, headers, a => + { + a.UpdateLanguages(c => c.Update(@event.Language, @event.IsOptional, @event.Fallback)); }); } @@ -93,7 +101,7 @@ namespace Squidex.Read.MongoDb.Apps { return Collection.UpdateAsync(@event, headers, a => { - a.MasterLanguage = @event.Language.Iso2Code; + a.UpdateLanguages(c => c.MakeMaster(@event.Language)); }); } diff --git a/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs index 99bcb9223..706191adc 100644 --- a/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs @@ -12,6 +12,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.OData.Core; using MongoDB.Driver; +using Squidex.Core; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Events; using Squidex.Read.Contents; @@ -49,7 +50,7 @@ namespace Squidex.Read.MongoDb.Contents this.modelBuilder = modelBuilder; } - public async Task> QueryAsync(Guid schemaId, bool nonPublished, string odataQuery, HashSet languages) + public async Task> QueryAsync(Guid schemaId, bool nonPublished, string odataQuery, LanguagesConfig languagesConfig) { List result = null; @@ -58,7 +59,7 @@ namespace Squidex.Read.MongoDb.Contents IFindFluent cursor; try { - var model = modelBuilder.BuildEdmModel(schemaEntity, languages); + var model = modelBuilder.BuildEdmModel(schemaEntity, languagesConfig); var parser = model.ParseQuery(odataQuery); @@ -90,7 +91,7 @@ namespace Squidex.Read.MongoDb.Contents return result; } - public async Task CountAsync(Guid schemaId, bool nonPublished, string odataQuery, HashSet languages) + public async Task CountAsync(Guid schemaId, bool nonPublished, string odataQuery, LanguagesConfig languagesConfig) { var result = 0L; @@ -99,7 +100,7 @@ namespace Squidex.Read.MongoDb.Contents IFindFluent cursor; try { - var model = modelBuilder.BuildEdmModel(schemaEntity, languages); + var model = modelBuilder.BuildEdmModel(schemaEntity, languagesConfig); var parser = model.ParseQuery(odataQuery); diff --git a/src/Squidex.Read/Apps/IAppEntity.cs b/src/Squidex.Read/Apps/IAppEntity.cs index 455aa00dd..aea99ea88 100644 --- a/src/Squidex.Read/Apps/IAppEntity.cs +++ b/src/Squidex.Read/Apps/IAppEntity.cs @@ -7,7 +7,7 @@ // ========================================================================== using System.Collections.Generic; -using Squidex.Infrastructure; +using Squidex.Core; namespace Squidex.Read.Apps { @@ -15,12 +15,10 @@ namespace Squidex.Read.Apps { string Name { get; } - Language MasterLanguage { get; } + LanguagesConfig LanguagesConfig { get; } IReadOnlyCollection Clients { get; } IReadOnlyCollection Contributors { get; } - - IReadOnlyCollection Languages { get; } } } diff --git a/src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs b/src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs index 3acb4e287..4c6c82a45 100644 --- a/src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs +++ b/src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs @@ -7,11 +7,11 @@ // ========================================================================== using System; -using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Caching.Memory; using Microsoft.OData.Edm; using Microsoft.OData.Edm.Library; +using Squidex.Core; using Squidex.Core.Schemas; using Squidex.Infrastructure; using Squidex.Read.Schemas; @@ -26,30 +26,32 @@ namespace Squidex.Read.Contents.Builders { } - public IEdmModel BuildEdmModel(ISchemaEntityWithSchema schemaEntity, HashSet languages) + public IEdmModel BuildEdmModel(ISchemaEntityWithSchema schemaEntity, LanguagesConfig languagesConfig) { - Guard.NotNull(languages, nameof(languages)); + Guard.NotNull(languagesConfig, nameof(languagesConfig)); Guard.NotNull(schemaEntity, nameof(schemaEntity)); - var cacheKey = $"{schemaEntity.Id}_{schemaEntity.Version}_{string.Join(",", languages.Select(x => x.Iso2Code).OrderBy(x => x))}"; + var isoCodes = string.Join(",", languagesConfig.Select(x => x.Language.Iso2Code).OrderBy(x => x)); + + var cacheKey = $"{schemaEntity.Id}_{schemaEntity.Version}_{isoCodes}"; var result = Cache.GetOrCreate(cacheKey, entry => { entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(60); - return BuildEdmModel(schemaEntity.Schema, languages); + return BuildEdmModel(schemaEntity.Schema, languagesConfig); }); return result; } - private static EdmModel BuildEdmModel(Schema schema, HashSet languages) + private static EdmModel BuildEdmModel(Schema schema, LanguagesConfig languagesConfig) { var model = new EdmModel(); var container = new EdmEntityContainer("Squidex", "Container"); - var schemaType = schema.BuildEdmType(languages, x => + var schemaType = schema.BuildEdmType(languagesConfig, x => { model.AddElement(x); diff --git a/src/Squidex.Read/Contents/Repositories/IContentRepository.cs b/src/Squidex.Read/Contents/Repositories/IContentRepository.cs index cdde53612..cd73e558a 100644 --- a/src/Squidex.Read/Contents/Repositories/IContentRepository.cs +++ b/src/Squidex.Read/Contents/Repositories/IContentRepository.cs @@ -9,15 +9,15 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Squidex.Infrastructure; +using Squidex.Core; namespace Squidex.Read.Contents.Repositories { public interface IContentRepository { - Task> QueryAsync(Guid schemaId, bool nonPublished, string odataQuery, HashSet languages); + Task> QueryAsync(Guid schemaId, bool nonPublished, string odataQuery, LanguagesConfig languagesConfig); - Task CountAsync(Guid schemaId, bool nonPublished, string odataQuery, HashSet languages); + Task CountAsync(Guid schemaId, bool nonPublished, string odataQuery, LanguagesConfig languagesConfig); Task FindContentAsync(Guid schemaId, Guid id); } diff --git a/src/Squidex.Write/Apps/AppCommandHandler.cs b/src/Squidex.Write/Apps/AppCommandHandler.cs index 6657528e2..54fe46b6f 100644 --- a/src/Squidex.Write/Apps/AppCommandHandler.cs +++ b/src/Squidex.Write/Apps/AppCommandHandler.cs @@ -109,6 +109,11 @@ namespace Squidex.Write.Apps return handler.UpdateAsync(context, a => a.RemoveLanguage(command)); } + protected Task On(UpdateLanguage command, CommandContext context) + { + return handler.UpdateAsync(context, a => a.UpdateLanguage(command)); + } + protected Task On(SetMasterLanguage command, CommandContext context) { return handler.UpdateAsync(context, a => a.SetMasterLanguage(command)); diff --git a/src/Squidex.Write/Apps/AppDomainObject.cs b/src/Squidex.Write/Apps/AppDomainObject.cs index 35fbbdbe0..12d27f499 100644 --- a/src/Squidex.Write/Apps/AppDomainObject.cs +++ b/src/Squidex.Write/Apps/AppDomainObject.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; +using Squidex.Core; using Squidex.Core.Apps; using Squidex.Events; using Squidex.Events.Apps; @@ -26,8 +27,8 @@ namespace Squidex.Write.Apps { private static readonly Language DefaultLanguage = Language.EN; private readonly AppContributors contributors = new AppContributors(); - private readonly AppLanguages languages = new AppLanguages(); private readonly AppClients clients = new AppClients(); + private LanguagesConfig languagesConfig = LanguagesConfig.Empty; private string name; public string Name @@ -77,17 +78,22 @@ namespace Squidex.Write.Apps protected void On(AppLanguageAdded @event) { - languages.Add(@event.Language); + languagesConfig = languagesConfig.Add(@event.Language); } protected void On(AppLanguageRemoved @event) { - languages.Remove(@event.Language); + languagesConfig = languagesConfig.Remove(@event.Language); + } + + protected void On(AppLanguageUpdated @event) + { + languagesConfig = languagesConfig.Update(@event.Language, @event.IsOptional, @event.Fallback); } protected void On(AppMasterLanguageSet @event) { - languages.SetMasterLanguage(@event.Language); + languagesConfig = languagesConfig.MakeMaster(@event.Language); } protected override void DispatchEvent(Envelope @event) @@ -107,7 +113,6 @@ namespace Squidex.Write.Apps RaiseEvent(SimpleMapper.Map(command, CreateInitialOwner(appId, command))); RaiseEvent(SimpleMapper.Map(command, CreateInitialLanguage(appId))); - RaiseEvent(SimpleMapper.Map(command, CreateInitialMasterLanguage(appId))); return this; } @@ -189,6 +194,17 @@ namespace Squidex.Write.Apps return this; } + public AppDomainObject UpdateLanguage(UpdateLanguage command) + { + Guard.Valid(command, nameof(command), () => "Cannot update language"); + + ThrowIfNotCreated(); + + RaiseEvent(SimpleMapper.Map(command, new AppLanguageUpdated())); + + return this; + } + public AppDomainObject SetMasterLanguage(SetMasterLanguage command) { Guard.Valid(command, nameof(command), () => "Cannot set master language"); @@ -215,11 +231,6 @@ namespace Squidex.Write.Apps return new AppLanguageAdded { AppId = id, Language = DefaultLanguage }; } - private static AppMasterLanguageSet CreateInitialMasterLanguage(NamedId id) - { - return new AppMasterLanguageSet { AppId = id, Language = DefaultLanguage }; - } - private static AppContributorAssigned CreateInitialOwner(NamedId id, SquidexCommand command) { return new AppContributorAssigned { AppId = id, ContributorId = command.Actor.Identifier, Permission = PermissionLevel.Owner }; diff --git a/src/Squidex.Write/Apps/AppLanguages.cs b/src/Squidex.Write/Apps/AppLanguages.cs deleted file mode 100644 index 1d3261e6e..000000000 --- a/src/Squidex.Write/Apps/AppLanguages.cs +++ /dev/null @@ -1,73 +0,0 @@ -// ========================================================================== -// AppLanguages.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Infrastructure; - -// ReSharper disable InvertIf - -namespace Squidex.Write.Apps -{ - public class AppLanguages - { - private readonly HashSet languages = new HashSet(); - private Language masterLanguage; - - public void Add(Language language) - { - ThrowIfFound(language, () => "Cannot add language"); - - languages.Add(language); - } - - public void Remove(Language language) - { - ThrowIfNotFound(language); - ThrowIfMasterLanguage(language, () => "Cannot remove language"); - - languages.Remove(language); - } - - public void SetMasterLanguage(Language language) - { - ThrowIfNotFound(language); - ThrowIfMasterLanguage(language, () => "Cannot set master language"); - - masterLanguage = language; - } - - private void ThrowIfNotFound(Language language) - { - if (!languages.Contains(language)) - { - throw new DomainObjectNotFoundException(language.Iso2Code, "Languages", typeof(AppDomainObject)); - } - } - - private void ThrowIfFound(Language language, Func message) - { - if (languages.Contains(language)) - { - var error = new ValidationError("Language is already part of the app", "Language"); - - throw new ValidationException(message(), error); - } - } - - private void ThrowIfMasterLanguage(Language language, Func message) - { - if (masterLanguage != null && masterLanguage.Equals(language)) - { - var error = new ValidationError("Language is the master language", "Language"); - - throw new ValidationException(message(), error); - } - } - } -} diff --git a/src/Squidex.Write/Apps/Commands/UpdateLanguage.cs b/src/Squidex.Write/Apps/Commands/UpdateLanguage.cs new file mode 100644 index 000000000..b1497a40c --- /dev/null +++ b/src/Squidex.Write/Apps/Commands/UpdateLanguage.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// UpdateLanguage.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Infrastructure; + +namespace Squidex.Write.Apps.Commands +{ + public sealed class UpdateLanguage : AppAggregateCommand, IValidatable + { + public Language Language { get; set; } + + public bool IsOptional { get; set; } + + public List Fallback { get; set; } + + public void Validate(IList errors) + { + if (Language == null) + { + errors.Add(new ValidationError("Language cannot be null", nameof(Language))); + } + } + } +} diff --git a/src/Squidex.Write/Contents/ContentCommandHandler.cs b/src/Squidex.Write/Contents/ContentCommandHandler.cs index 51d3a5337..d6e7add93 100644 --- a/src/Squidex.Write/Contents/ContentCommandHandler.cs +++ b/src/Squidex.Write/Contents/ContentCommandHandler.cs @@ -95,13 +95,11 @@ namespace Squidex.Write.Contents var taskForSchema = schemas.FindSchemaByIdAsync(command.SchemaId.Id); await Task.WhenAll(taskForApp, taskForSchema); - - var languages = new HashSet(taskForApp.Result.Languages); - + var schemaObject = taskForSchema.Result.Schema; var schemaErrors = new List(); - await command.Data.ValidateAsync(schemaObject, languages, schemaErrors); + await command.Data.ValidateAsync(schemaObject, taskForApp.Result.LanguagesConfig, schemaErrors); if (schemaErrors.Count > 0) { @@ -110,7 +108,7 @@ namespace Squidex.Write.Contents if (enrich) { - command.Data.Enrich(schemaObject, languages); + command.Data.Enrich(schemaObject, taskForApp.Result.LanguagesConfig); } } } diff --git a/src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs b/src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs index 6764b99e8..55ddad316 100644 --- a/src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs +++ b/src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; @@ -60,12 +61,13 @@ namespace Squidex.Controllers.Api.Apps return NotFound(); } - var model = entity.Languages.Select(x => - { - var isMasterLanguage = x.Equals(entity.MasterLanguage); - - return SimpleMapper.Map(x, new AppLanguageDto { IsMasterLanguage = isMasterLanguage }); - }).ToList(); + var model = entity.LanguagesConfig.Select(x => + SimpleMapper.Map(x.Language, + new AppLanguageDto + { + IsMaster = x == entity.LanguagesConfig.Master, + IsOptional = x.IsOptional + })).ToList(); Response.Headers["ETag"] = new StringValues(entity.Version.ToString()); @@ -112,9 +114,11 @@ namespace Squidex.Controllers.Api.Apps [Route("apps/{app}/languages/{language}")] public async Task Update(string app, string language, [FromBody] UpdateAppLanguageDto model) { - if (model.IsMasterLanguage) + await CommandBus.PublishAsync(SimpleMapper.Map(model, new UpdateLanguage())); + + if (model.IsMaster == true) { - await CommandBus.PublishAsync(new SetMasterLanguage { Language = Language.GetLanguage(language) }); + await CommandBus.PublishAsync(new SetMasterLanguage { Language = ParseLanguage(language) }); } return NoContent(); @@ -135,9 +139,21 @@ namespace Squidex.Controllers.Api.Apps [Route("apps/{app}/languages/{language}")] public async Task DeleteLanguage(string app, string language) { - await CommandBus.PublishAsync(new RemoveLanguage { Language = Language.GetLanguage(language) }); + await CommandBus.PublishAsync(new RemoveLanguage { Language = ParseLanguage(language) }); return NoContent(); } + + private static Language ParseLanguage(string language) + { + try + { + return Language.GetLanguage(language); + } + catch (NotSupportedException) + { + throw new ValidationException($"Language '{language}' is not valid."); + } + } } } diff --git a/src/Squidex/Controllers/Api/Apps/Models/AppLanguageDto.cs b/src/Squidex/Controllers/Api/Apps/Models/AppLanguageDto.cs index 54d97931f..97de2029d 100644 --- a/src/Squidex/Controllers/Api/Apps/Models/AppLanguageDto.cs +++ b/src/Squidex/Controllers/Api/Apps/Models/AppLanguageDto.cs @@ -27,6 +27,11 @@ namespace Squidex.Controllers.Api.Apps.Models /// /// Indicates if the language is the master language. /// - public bool IsMasterLanguage { get; set; } + public bool IsMaster { get; set; } + + /// + /// Indicates if the language is optional. + /// + public bool IsOptional { get; set; } } } diff --git a/src/Squidex/Controllers/Api/Apps/Models/UpdateAppLanguageDto.cs b/src/Squidex/Controllers/Api/Apps/Models/UpdateAppLanguageDto.cs index ef54c58c8..ac50d3f5d 100644 --- a/src/Squidex/Controllers/Api/Apps/Models/UpdateAppLanguageDto.cs +++ b/src/Squidex/Controllers/Api/Apps/Models/UpdateAppLanguageDto.cs @@ -5,6 +5,10 @@ // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== + +using System.Collections.Generic; +using Squidex.Infrastructure; + namespace Squidex.Controllers.Api.Apps.Models { public class UpdateAppLanguageDto @@ -12,6 +16,16 @@ namespace Squidex.Controllers.Api.Apps.Models /// /// Set the value to true to make the language to the master language. /// - public bool IsMasterLanguage { get; set; } + public bool? IsMaster { get; set; } + + /// + /// Set the value to true to make the language optional. + /// + public bool IsOptional { get; set; } + + /// + /// Optional fallback languages. + /// + public List Fallback { get; set; } } } diff --git a/src/Squidex/Controllers/ContentApi/ContentsController.cs b/src/Squidex/Controllers/ContentApi/ContentsController.cs index 886350840..46674d64b 100644 --- a/src/Squidex/Controllers/ContentApi/ContentsController.cs +++ b/src/Squidex/Controllers/ContentApi/ContentsController.cs @@ -7,7 +7,6 @@ // ========================================================================== using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; @@ -17,7 +16,6 @@ using NSwag.Annotations; using Squidex.Controllers.ContentApi.Models; using Squidex.Core.Contents; using Squidex.Core.Identity; -using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; @@ -54,13 +52,11 @@ namespace Squidex.Controllers.ContentApi { return NotFound(); } - - var languages = new HashSet(App.Languages); - + var query = Request.QueryString.ToString(); - var taskForContents = contentRepository.QueryAsync(schemaEntity.Id, nonPublished, query, languages); - var taskForCount = contentRepository.CountAsync(schemaEntity.Id, nonPublished, query, languages); + var taskForContents = contentRepository.QueryAsync(schemaEntity.Id, nonPublished, query, App.LanguagesConfig); + var taskForCount = contentRepository.CountAsync(schemaEntity.Id, nonPublished, query, App.LanguagesConfig); await Task.WhenAll(taskForContents, taskForCount); @@ -73,7 +69,7 @@ namespace Squidex.Controllers.ContentApi if (x.Data != null) { - itemModel.Data = x.Data.ToApiModel(schemaEntity.Schema, App.Languages, App.MasterLanguage); + itemModel.Data = x.Data.ToApiModel(schemaEntity.Schema, App.LanguagesConfig); } return itemModel; @@ -105,7 +101,7 @@ namespace Squidex.Controllers.ContentApi if (entity.Data != null) { - model.Data = entity.Data.ToApiModel(schemaEntity.Schema, App.Languages, App.MasterLanguage, hidden); + model.Data = entity.Data.ToApiModel(schemaEntity.Schema, App.LanguagesConfig, null, hidden); } Response.Headers["ETag"] = new StringValues(entity.Version.ToString()); diff --git a/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs b/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs index d0fa3a0eb..3a844c949 100644 --- a/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs +++ b/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs @@ -42,7 +42,6 @@ namespace Squidex.Controllers.ContentApi.Generator private readonly MyUrlsOptions urlOptions; private readonly string schemaQueryDescription; private readonly string schemaBodyDescription; - private HashSet languages; private JsonSchema4 errorDtoSchema; private string appBasePath; private IAppEntity app; @@ -62,15 +61,13 @@ namespace Squidex.Controllers.ContentApi.Generator schemaQueryDescription = SwaggerHelper.LoadDocs("schemaquery"); } - public async Task Generate(IAppEntity appEntity, IEnumerable schemas) + public async Task Generate(IAppEntity targetApp, IEnumerable schemas) { - app = appEntity; - - languages = new HashSet(appEntity.Languages); + app = targetApp; await GenerateBasicSchemas(); - GenerateBasePath(appEntity); + GenerateBasePath(); GenerateTitle(); GenerateRequestInfo(); GenerateContentTypes(); @@ -84,9 +81,9 @@ namespace Squidex.Controllers.ContentApi.Generator return document; } - private void GenerateBasePath(IAppEntity appEntity) + private void GenerateBasePath() { - appBasePath = $"/content/{appEntity.Name}"; + appBasePath = $"/content/{app.Name}"; } private void GenerateSchemes() @@ -181,7 +178,7 @@ namespace Squidex.Controllers.ContentApi.Generator Name = schemaName, Description = $"API to managed {schemaName} contents." }); - var dataSchema = AppendSchema($"{schemaIdentifier}Dto", schema.BuildJsonSchema(languages, AppendSchema)); + var dataSchema = AppendSchema($"{schemaIdentifier}Dto", schema.BuildJsonSchema(app.LanguagesConfig, AppendSchema)); var schemaOperations = new List { diff --git a/tests/Squidex.Core.Tests/ContentEnrichmentTests.cs b/tests/Squidex.Core.Tests/ContentEnrichmentTests.cs index 6a495d741..a5a7fe584 100644 --- a/tests/Squidex.Core.Tests/ContentEnrichmentTests.cs +++ b/tests/Squidex.Core.Tests/ContentEnrichmentTests.cs @@ -6,7 +6,6 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; using Moq; using NodaTime; using NodaTime.Text; @@ -19,7 +18,7 @@ namespace Squidex.Core { public class ContentEnrichmentTests { - private readonly HashSet languages = new HashSet(new[] { Language.DE, Language.EN }); + private readonly LanguagesConfig languagesConfig = LanguagesConfig.Create(Language.DE, Language.EN); [Fact] private void Should_enrich_with_default_values() @@ -52,7 +51,7 @@ namespace Squidex.Core new ContentFieldData() .AddValue("iv", 456)); - data.Enrich(schema, languages); + data.Enrich(schema, languagesConfig); Assert.Equal(456, (int)data["my-number"]["iv"]); diff --git a/tests/Squidex.Core.Tests/ContentValidationTests.cs b/tests/Squidex.Core.Tests/ContentValidationTests.cs index 7a5b22c77..40e0992f4 100644 --- a/tests/Squidex.Core.Tests/ContentValidationTests.cs +++ b/tests/Squidex.Core.Tests/ContentValidationTests.cs @@ -18,7 +18,7 @@ namespace Squidex.Core { public class ContentValidationTests { - private readonly HashSet languages = new HashSet(new[] { Language.DE, Language.EN }); + private readonly LanguagesConfig languagesConfig = LanguagesConfig.Create(Language.DE, Language.EN); private readonly List errors = new List(); private Schema schema = Schema.Create("my-name", new SchemaProperties()); @@ -30,7 +30,7 @@ namespace Squidex.Core .AddField("unknown", new ContentFieldData()); - await data.ValidateAsync(schema, languages, errors); + await data.ValidateAsync(schema, languagesConfig, errors); errors.ShouldBeEquivalentTo( new List @@ -50,7 +50,7 @@ namespace Squidex.Core new ContentFieldData() .SetValue(1000)); - await data.ValidateAsync(schema, languages, errors); + await data.ValidateAsync(schema, languagesConfig, errors); errors.ShouldBeEquivalentTo( new List @@ -60,7 +60,7 @@ namespace Squidex.Core } [Fact] - public async Task Should_add_error_non_localizable_data_field_contains_language() + public async Task Should_add_error_if_non_localizable_data_field_contains_language() { schema = schema.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties())); @@ -71,12 +71,13 @@ namespace Squidex.Core .AddValue("es", 1) .AddValue("it", 1)); - await data.ValidateAsync(schema, languages, errors); + await data.ValidateAsync(schema, languagesConfig, errors); errors.ShouldBeEquivalentTo( new List { - new ValidationError("my-field can only contain a single entry for invariant language (iv)", "my-field") + new ValidationError("my-field has an unsupported language 'es'", "my-field"), + new ValidationError("my-field has an unsupported language 'it'", "my-field") }); } @@ -88,7 +89,7 @@ namespace Squidex.Core var data = new ContentData(); - await data.ValidateAsync(schema, languages, errors); + await data.ValidateAsync(schema, languagesConfig, errors); errors.ShouldBeEquivalentTo( new List @@ -106,7 +107,7 @@ namespace Squidex.Core var data = new ContentData(); - await data.ValidateAsync(schema, languages, errors); + await data.ValidateAsync(schema, languagesConfig, errors); errors.ShouldBeEquivalentTo( new List @@ -127,7 +128,7 @@ namespace Squidex.Core .AddValue("de", 1) .AddValue("xx", 1)); - await data.ValidateAsync(schema, languages, errors); + await data.ValidateAsync(schema, languagesConfig, errors); errors.ShouldBeEquivalentTo( new List @@ -136,6 +137,25 @@ namespace Squidex.Core }); } + [Fact] + public async Task Should_not_add_error_if_required_field_has_no_value_for_optional_language() + { + var optionalConfig = + LanguagesConfig.Create(Language.ES, Language.IT).Update(Language.IT, true, null); + + schema = schema.AddOrUpdateField(new StringField(1, "my-field", new StringFieldProperties { IsLocalizable = true, IsRequired = true })); + + var data = + new ContentData() + .AddField("my-field", + new ContentFieldData() + .AddValue("es", "value")); + + await data.ValidateAsync(schema, optionalConfig, errors); + + Assert.Equal(0, errors.Count); + } + [Fact] public async Task Should_add_error_if_data_contains_unsupported_language() { @@ -148,7 +168,7 @@ namespace Squidex.Core .AddValue("es", 1) .AddValue("it", 1)); - await data.ValidateAsync(schema, languages, errors); + await data.ValidateAsync(schema, languagesConfig, errors); errors.ShouldBeEquivalentTo( new List @@ -166,7 +186,7 @@ namespace Squidex.Core .AddField("unknown", new ContentFieldData()); - await data.ValidatePartialAsync(schema, languages, errors); + await data.ValidatePartialAsync(schema, languagesConfig, errors); errors.ShouldBeEquivalentTo( new List @@ -186,7 +206,7 @@ namespace Squidex.Core new ContentFieldData() .SetValue(1000)); - await data.ValidatePartialAsync(schema, languages, errors); + await data.ValidatePartialAsync(schema, languagesConfig, errors); errors.ShouldBeEquivalentTo( new List @@ -196,7 +216,7 @@ namespace Squidex.Core } [Fact] - public async Task Should_add_error_non_localizable_partial_data_field_contains_language() + public async Task Should_add_error_if_non_localizable_partial_data_field_contains_language() { schema = schema.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties())); @@ -207,12 +227,13 @@ namespace Squidex.Core .AddValue("es", 1) .AddValue("it", 1)); - await data.ValidatePartialAsync(schema, languages, errors); + await data.ValidatePartialAsync(schema, languagesConfig, errors); errors.ShouldBeEquivalentTo( new List { - new ValidationError("my-field can only contain a single entry for invariant language (iv)", "my-field") + new ValidationError("my-field has an unsupported language 'es'", "my-field"), + new ValidationError("my-field has an unsupported language 'it'", "my-field") }); } @@ -224,7 +245,7 @@ namespace Squidex.Core var data = new ContentData(); - await data.ValidatePartialAsync(schema, languages, errors); + await data.ValidatePartialAsync(schema, languagesConfig, errors); Assert.Equal(0, errors.Count); } @@ -237,7 +258,7 @@ namespace Squidex.Core var data = new ContentData(); - await data.ValidatePartialAsync(schema, languages, errors); + await data.ValidatePartialAsync(schema, languagesConfig, errors); Assert.Equal(0, errors.Count); } @@ -254,7 +275,7 @@ namespace Squidex.Core .AddValue("de", 1) .AddValue("xx", 1)); - await data.ValidatePartialAsync(schema, languages, errors); + await data.ValidatePartialAsync(schema, languagesConfig, errors); errors.ShouldBeEquivalentTo( new List @@ -275,7 +296,7 @@ namespace Squidex.Core .AddValue("es", 1) .AddValue("it", 1)); - await data.ValidatePartialAsync(schema, languages, errors); + await data.ValidatePartialAsync(schema, languagesConfig, errors); errors.ShouldBeEquivalentTo( new List diff --git a/tests/Squidex.Core.Tests/Contents/ContentDataTests.cs b/tests/Squidex.Core.Tests/Contents/ContentDataTests.cs index ca0446480..435a3e5b3 100644 --- a/tests/Squidex.Core.Tests/Contents/ContentDataTests.cs +++ b/tests/Squidex.Core.Tests/Contents/ContentDataTests.cs @@ -25,8 +25,7 @@ namespace Squidex.Core.Contents .AddOrUpdateField(new NumberField(3, "field3", new NumberFieldProperties { IsLocalizable = false })) .HideField(3); - private readonly Language[] languages = { Language.DE, Language.EN }; - private readonly Language masterLanguage = Language.EN; + private readonly LanguagesConfig languagesConfig = LanguagesConfig.Create(Language.EN, Language.DE); [Fact] public void Should_convert_to_id_model() @@ -109,7 +108,7 @@ namespace Squidex.Core.Contents .AddValue("en", "en_string") .AddValue("de", "de_string")); - var actual = input.ToApiModel(schema, languages, masterLanguage); + var actual = input.ToApiModel(schema, languagesConfig); Assert.Equal(expected, actual); } @@ -131,7 +130,7 @@ namespace Squidex.Core.Contents .AddValue("de", "de_string") .AddValue("it", "it_string")); - var actual = input.ToApiModel(schema, languages, masterLanguage); + var actual = input.ToApiModel(schema, languagesConfig); Assert.Equal(expected, actual); } @@ -152,7 +151,7 @@ namespace Squidex.Core.Contents .AddValue("de", 2) .AddValue("en", 3)); - var actual = input.ToApiModel(schema, languages, masterLanguage); + var actual = input.ToApiModel(schema, languagesConfig); Assert.Equal(expected, actual); } @@ -172,7 +171,7 @@ namespace Squidex.Core.Contents new ContentFieldData() .AddValue("iv", 3)); - var actual = input.ToApiModel(schema, languages, masterLanguage); + var actual = input.ToApiModel(schema, languagesConfig); Assert.Equal(expected, actual); } @@ -215,7 +214,7 @@ namespace Squidex.Core.Contents .AddValue("de", 2) .AddValue("it", 3)); - var actual = input.ToApiModel(schema, languages, masterLanguage); + var actual = input.ToApiModel(schema, languagesConfig); Assert.Equal(expected, actual); } @@ -238,7 +237,7 @@ namespace Squidex.Core.Contents new ContentFieldData() .AddValue("iv", 2)); - var actual = input.ToApiModel(schema, languages, masterLanguage); + var actual = input.ToApiModel(schema, languagesConfig); Assert.Equal(expected, actual); } @@ -252,7 +251,43 @@ namespace Squidex.Core.Contents new ContentFieldData() .AddValue("iv", 1)); - Assert.Same(data, data.ToLanguageModel()); + Assert.Same(data, data.ToLanguageModel(languagesConfig)); + } + + [Fact] + public void Should_return_flat_list_when_single_languages_specified() + { + var data = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("de", 1) + .AddValue("en", 2)) + .AddField("field2", + new ContentFieldData() + .AddValue("de", null) + .AddValue("en", 4)) + .AddField("field3", + new ContentFieldData() + .AddValue("en", 6)) + .AddField("field4", + new ContentFieldData() + .AddValue("it", 7)); + + var fallbackConfig = + LanguagesConfig.Create(Language.DE).Add(Language.EN) + .Update(Language.DE, false, new[] { Language.EN }); + + var output = (Dictionary)data.ToLanguageModel(fallbackConfig, new List { Language.DE }); + + var expected = new Dictionary + { + { "field1", 1 }, + { "field2", 4 }, + { "field3", 6 } + }; + + Assert.True(expected.EqualsDictionary(output)); } [Fact] @@ -275,7 +310,7 @@ namespace Squidex.Core.Contents new ContentFieldData() .AddValue("it", 7)); - var output = (Dictionary)data.ToLanguageModel(new List { Language.DE, Language.EN }); + var output = (Dictionary)data.ToLanguageModel(languagesConfig, new List { Language.DE, Language.EN }); var expected = new Dictionary { diff --git a/tests/Squidex.Core.Tests/LanguagesConfigTests.cs b/tests/Squidex.Core.Tests/LanguagesConfigTests.cs new file mode 100644 index 000000000..3a6d1f519 --- /dev/null +++ b/tests/Squidex.Core.Tests/LanguagesConfigTests.cs @@ -0,0 +1,213 @@ +// ========================================================================== +// LanguagesConfigTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Core +{ + public class LanguagesConfigTests + { + [Fact] + public void Should_create_initial_config() + { + var config = LanguagesConfig.Create(Language.DE); + + config.ToList().ShouldBeEquivalentTo( + new List + { + new LanguageConfig(Language.DE) + }); + + Assert.Equal(Language.DE, config.Master.Language); + } + + [Fact] + public void Should_create_initial_config_with_multiple_languages() + { + var config = LanguagesConfig.Create(Language.DE, Language.EN, Language.IT); + + config.ToList().ShouldBeEquivalentTo( + new List + { + new LanguageConfig(Language.DE), + new LanguageConfig(Language.EN), + new LanguageConfig(Language.IT) + }); + + Assert.Equal(Language.DE, config.Master.Language); + } + + [Fact] + public void Should_create_initial_config_with_configs() + { + var configs = new[] + { + new LanguageConfig(Language.DE), + new LanguageConfig(Language.EN), + new LanguageConfig(Language.IT), + }; + var config = LanguagesConfig.Create(configs); + + config.ToList().ShouldBeEquivalentTo(configs); + + Assert.Equal(configs[0], config.Master); + } + + [Fact] + public void Should_add_language() + { + var config = LanguagesConfig.Create(Language.DE).Add(Language.IT); + + config.ToList().ShouldBeEquivalentTo( + new List + { + new LanguageConfig(Language.DE), + new LanguageConfig(Language.IT) + }); + } + + [Fact] + public void Should_make_first_language_to_master() + { + var config = LanguagesConfig.Empty.Add(Language.IT); + + Assert.Equal(Language.IT, config.Master.Language); + } + + [Fact] + public void Should_throw_exception_if_language_to_add_already_exists() + { + var config = LanguagesConfig.Create(Language.DE); + + Assert.Throws(() => config.Add(Language.DE)); + } + + [Fact] + public void Should_make_master_language() + { + var config = LanguagesConfig.Create(Language.DE).Add(Language.IT).MakeMaster(Language.IT); + + Assert.Equal(Language.IT, config.Master.Language); + } + + [Fact] + public void Should_throw_exception_if_language_to_make_master_is_not_found() + { + var config = LanguagesConfig.Create(Language.DE); + + Assert.Throws(() => config.MakeMaster(Language.EN)); + } + + [Fact] + public void Should_not_throw_exception_if_language_is_already_master_language() + { + var config = LanguagesConfig.Create(Language.DE); + + config.MakeMaster(Language.DE); + } + + [Fact] + public void Should_remove_language() + { + var config = LanguagesConfig.Create(Language.DE).Add(Language.IT).Add(Language.RU).Remove(Language.IT); + + config.ToList().ShouldBeEquivalentTo( + new List + { + new LanguageConfig(Language.DE), + new LanguageConfig(Language.RU) + }); + } + + [Fact] + public void Should_remove_fallbacks_when_removing_language() + { + var config = + LanguagesConfig.Create(Language.DE) + .Add(Language.IT) + .Add(Language.RU) + .Update(Language.DE, false, new[] { Language.RU, Language.IT }) + .Update(Language.RU, false, new[] { Language.DE, Language.IT }) + .Remove(Language.IT); + + config.ToList().ShouldBeEquivalentTo( + new List + { + new LanguageConfig(Language.DE, false, Language.RU), + new LanguageConfig(Language.RU, false, Language.DE) + }); + } + + [Fact] + public void Should_throw_exception_if_language_to_remove_is_not_found() + { + var config = LanguagesConfig.Create(Language.DE); + + Assert.Throws(() => config.Remove(Language.EN)); + } + + [Fact] + public void Should_throw_exception_if_language_to_remove_is_master_language() + { + var config = LanguagesConfig.Create(Language.DE); + + Assert.Throws(() => config.Remove(Language.DE)); + } + + [Fact] + public void Should_update_language() + { + var config = LanguagesConfig.Create(Language.DE).Add(Language.IT).Update(Language.IT, true, new[] { Language.DE }); + + config.ToList().ShouldBeEquivalentTo( + new List + { + new LanguageConfig(Language.DE), + new LanguageConfig(Language.IT, true, Language.DE) + }); + } + + [Fact] + public void Should_throw_exception_if_language_to_update_is_not_found() + { + var config = LanguagesConfig.Create(Language.DE); + + Assert.Throws(() => config.Update(Language.EN, true, null)); + } + + [Fact] + public void Should_throw_exception_if_fallback_language_is_invalid() + { + var config = LanguagesConfig.Create(Language.DE); + + Assert.Throws(() => config.Update(Language.DE, true, new [] { Language.EN })); + } + + [Fact] + public void Should_throw_exception_if_language_to_make_optional_is_master_language() + { + var config = LanguagesConfig.Create(Language.DE); + + Assert.Throws(() => config.Update(Language.DE, true, null)); + } + + [Fact] + public void Should_provide_enumerators() + { + var config = LanguagesConfig.Create(); + + Assert.NotNull(((IEnumerable)config).GetEnumerator()); + Assert.NotNull(((IEnumerable)config).GetEnumerator()); + } + } +} diff --git a/tests/Squidex.Core.Tests/Schemas/AssetsFieldTests.cs b/tests/Squidex.Core.Tests/Schemas/AssetsFieldTests.cs index eceba73ff..56c2b87e8 100644 --- a/tests/Squidex.Core.Tests/Schemas/AssetsFieldTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/AssetsFieldTests.cs @@ -47,7 +47,7 @@ namespace Squidex.Core.Schemas var sut = new AssetsField(1, "my-asset", new AssetsFieldProperties(), assetTester.Object); - await sut.ValidateAsync(CreateValue(assetId), errors); + await sut.ValidateAsync(CreateValue(assetId), false, errors); Assert.Empty(errors); } @@ -57,7 +57,7 @@ namespace Squidex.Core.Schemas { var sut = new AssetsField(1, "my-asset", new AssetsFieldProperties(), assetTester.Object); - await sut.ValidateAsync(CreateValue(null), errors); + await sut.ValidateAsync(CreateValue(null), false, errors); Assert.Empty(errors); } @@ -67,7 +67,7 @@ namespace Squidex.Core.Schemas { var sut = new AssetsField(1, "my-asset", new AssetsFieldProperties { IsRequired = true }, assetTester.Object); - await sut.ValidateAsync(CreateValue(null), errors); + await sut.ValidateAsync(CreateValue(null), false, errors); errors.ShouldBeEquivalentTo( new[] { " is required" }); @@ -78,7 +78,7 @@ namespace Squidex.Core.Schemas { var sut = new AssetsField(1, "my-asset", new AssetsFieldProperties { IsRequired = true }, assetTester.Object); - await sut.ValidateAsync(CreateValue(), errors); + await sut.ValidateAsync(CreateValue(), false, errors); errors.ShouldBeEquivalentTo( new[] { " is required" }); @@ -89,7 +89,7 @@ namespace Squidex.Core.Schemas { var sut = new AssetsField(1, "my-asset", new AssetsFieldProperties(), assetTester.Object); - await sut.ValidateAsync("invalid", errors); + await sut.ValidateAsync("invalid", false, errors); errors.ShouldBeEquivalentTo( new[] { " is not a valid value" }); @@ -104,7 +104,7 @@ namespace Squidex.Core.Schemas var sut = new AssetsField(1, "my-asset", new AssetsFieldProperties(), assetTester.Object); - await sut.ValidateAsync(CreateValue(assetId), errors); + await sut.ValidateAsync(CreateValue(assetId), false, errors); errors.ShouldBeEquivalentTo( new[] { $" contains invalid asset '{assetId}'" }); diff --git a/tests/Squidex.Core.Tests/Schemas/BooleanFieldTests.cs b/tests/Squidex.Core.Tests/Schemas/BooleanFieldTests.cs index 546c9262a..6cf2f6eb6 100644 --- a/tests/Squidex.Core.Tests/Schemas/BooleanFieldTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/BooleanFieldTests.cs @@ -39,7 +39,7 @@ namespace Squidex.Core.Schemas { var sut = new BooleanField(1, "my-bolean", new BooleanFieldProperties()); - await sut.ValidateAsync(CreateValue(null), errors); + await sut.ValidateAsync(CreateValue(null), false, errors); Assert.Empty(errors); } @@ -49,7 +49,7 @@ namespace Squidex.Core.Schemas { var sut = new BooleanField(1, "my-bolean", new BooleanFieldProperties()); - await sut.ValidateAsync(CreateValue(true), errors); + await sut.ValidateAsync(CreateValue(true), false, errors); Assert.Empty(errors); } @@ -59,7 +59,7 @@ namespace Squidex.Core.Schemas { var sut = new BooleanField(1, "my-bolean", new BooleanFieldProperties { IsRequired = true }); - await sut.ValidateAsync(CreateValue(null), errors); + await sut.ValidateAsync(CreateValue(null), false, errors); errors.ShouldBeEquivalentTo( new[] { " is required" }); @@ -70,7 +70,7 @@ namespace Squidex.Core.Schemas { var sut = new BooleanField(1, "my-bolean", new BooleanFieldProperties()); - await sut.ValidateAsync(CreateValue("Invalid"), errors); + await sut.ValidateAsync(CreateValue("Invalid"), false, errors); errors.ShouldBeEquivalentTo( new[] { " is not a valid value" }); diff --git a/tests/Squidex.Core.Tests/Schemas/DateTimeFieldTests.cs b/tests/Squidex.Core.Tests/Schemas/DateTimeFieldTests.cs index a7602eb5e..4066e33cf 100644 --- a/tests/Squidex.Core.Tests/Schemas/DateTimeFieldTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/DateTimeFieldTests.cs @@ -41,7 +41,7 @@ namespace Squidex.Core.Schemas { var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties()); - await sut.ValidateAsync(CreateValue(null), errors); + await sut.ValidateAsync(CreateValue(null), false, errors); Assert.Empty(errors); } @@ -51,7 +51,7 @@ namespace Squidex.Core.Schemas { var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties { IsRequired = true }); - await sut.ValidateAsync(CreateValue(null), errors); + await sut.ValidateAsync(CreateValue(null), false, errors); errors.ShouldBeEquivalentTo( new[] { " is required" }); @@ -62,7 +62,7 @@ namespace Squidex.Core.Schemas { var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties { MinValue = FutureDays(10) }); - await sut.ValidateAsync(CreateValue(FutureDays(0)), errors); + await sut.ValidateAsync(CreateValue(FutureDays(0)), false, errors); errors.ShouldBeEquivalentTo( new[] { $" must be greater than '{FutureDays(10)}'" }); @@ -73,7 +73,7 @@ namespace Squidex.Core.Schemas { var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties { MaxValue = FutureDays(10) }); - await sut.ValidateAsync(CreateValue(FutureDays(20)), errors); + await sut.ValidateAsync(CreateValue(FutureDays(20)), false, errors); errors.ShouldBeEquivalentTo( new[] { $" must be less than '{FutureDays(10)}'" }); @@ -84,7 +84,7 @@ namespace Squidex.Core.Schemas { var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties()); - await sut.ValidateAsync(CreateValue("Invalid"), errors); + await sut.ValidateAsync(CreateValue("Invalid"), false, errors); errors.ShouldBeEquivalentTo( new[] { " is not a valid value" }); @@ -95,7 +95,7 @@ namespace Squidex.Core.Schemas { var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties()); - await sut.ValidateAsync(CreateValue(123), errors); + await sut.ValidateAsync(CreateValue(123), false, errors); errors.ShouldBeEquivalentTo( new[] { " is not a valid value" }); diff --git a/tests/Squidex.Core.Tests/Schemas/GeolocationFieldTests.cs b/tests/Squidex.Core.Tests/Schemas/GeolocationFieldTests.cs index cd799d5b8..faebd3293 100644 --- a/tests/Squidex.Core.Tests/Schemas/GeolocationFieldTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/GeolocationFieldTests.cs @@ -39,7 +39,7 @@ namespace Squidex.Core.Schemas { var sut = new GeolocationField(1, "my-geolocation", new GeolocationFieldProperties()); - await sut.ValidateAsync(CreateValue(JValue.CreateNull()), errors); + await sut.ValidateAsync(CreateValue(JValue.CreateNull()), false, errors); Assert.Empty(errors); } @@ -53,7 +53,7 @@ namespace Squidex.Core.Schemas new JProperty("latitude", 0), new JProperty("longitude", 0)); - await sut.ValidateAsync(CreateValue(geolocation), errors); + await sut.ValidateAsync(CreateValue(geolocation), false, errors); Assert.Empty(errors); } @@ -67,7 +67,7 @@ namespace Squidex.Core.Schemas new JProperty("latitude", 200), new JProperty("longitude", 0)); - await sut.ValidateAsync(CreateValue(geolocation), errors); + await sut.ValidateAsync(CreateValue(geolocation), false, errors); errors.ShouldBeEquivalentTo( new[] { " is not a valid value" }); @@ -83,7 +83,7 @@ namespace Squidex.Core.Schemas new JProperty("latitude", 0), new JProperty("longitude", 0)); - await sut.ValidateAsync(CreateValue(geolocation), errors); + await sut.ValidateAsync(CreateValue(geolocation), false, errors); errors.ShouldBeEquivalentTo( new[] { " is not a valid value" }); @@ -94,7 +94,7 @@ namespace Squidex.Core.Schemas { var sut = new GeolocationField(1, "my-geolocation", new GeolocationFieldProperties { IsRequired = true }); - await sut.ValidateAsync(CreateValue(JValue.CreateNull()), errors); + await sut.ValidateAsync(CreateValue(JValue.CreateNull()), false, errors); errors.ShouldBeEquivalentTo( new[] { " is required" }); diff --git a/tests/Squidex.Core.Tests/Schemas/JsonFieldTests.cs b/tests/Squidex.Core.Tests/Schemas/JsonFieldTests.cs index e77e17404..ea5aabd48 100644 --- a/tests/Squidex.Core.Tests/Schemas/JsonFieldTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/JsonFieldTests.cs @@ -39,7 +39,7 @@ namespace Squidex.Core.Schemas { var sut = new JsonField(1, "my-json", new JsonFieldProperties()); - await sut.ValidateAsync(CreateValue(new JValue(1)), errors); + await sut.ValidateAsync(CreateValue(new JValue(1)), false, errors); Assert.Empty(errors); } @@ -49,7 +49,7 @@ namespace Squidex.Core.Schemas { var sut = new JsonField(1, "my-json", new JsonFieldProperties { IsRequired = true }); - await sut.ValidateAsync(CreateValue(JValue.CreateNull()), errors); + await sut.ValidateAsync(CreateValue(JValue.CreateNull()), false, errors); errors.ShouldBeEquivalentTo( new[] { " is required" }); diff --git a/tests/Squidex.Core.Tests/Schemas/NumberFieldTests.cs b/tests/Squidex.Core.Tests/Schemas/NumberFieldTests.cs index c2bd13063..1a1eb5f77 100644 --- a/tests/Squidex.Core.Tests/Schemas/NumberFieldTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/NumberFieldTests.cs @@ -40,7 +40,7 @@ namespace Squidex.Core.Schemas { var sut = new NumberField(1, "my-number", new NumberFieldProperties()); - await sut.ValidateAsync(CreateValue(null), errors); + await sut.ValidateAsync(CreateValue(null), false, errors); Assert.Empty(errors); } @@ -49,11 +49,11 @@ namespace Squidex.Core.Schemas public async Task Should_add_errors_if_number_is_required() { var sut = new NumberField(1, "my-number", new NumberFieldProperties { IsRequired = true }); - - await sut.ValidateAsync(CreateValue(null), errors); + + await sut.ValidateAsync(CreateValue(null), false, errors); errors.ShouldBeEquivalentTo( - new [] { " is required" }); + new[] { " is required" }); } [Fact] @@ -61,7 +61,7 @@ namespace Squidex.Core.Schemas { var sut = new NumberField(1, "my-number", new NumberFieldProperties { MinValue = 10 }); - await sut.ValidateAsync(CreateValue(5), errors); + await sut.ValidateAsync(CreateValue(5), false, errors); errors.ShouldBeEquivalentTo( new[] { " must be greater than '10'" }); @@ -72,7 +72,7 @@ namespace Squidex.Core.Schemas { var sut = new NumberField(1, "my-number", new NumberFieldProperties { MaxValue = 10 }); - await sut.ValidateAsync(CreateValue(20), errors); + await sut.ValidateAsync(CreateValue(20), false, errors); errors.ShouldBeEquivalentTo( new[] { " must be less than '10'" }); @@ -83,7 +83,7 @@ namespace Squidex.Core.Schemas { var sut = new NumberField(1, "my-number", new NumberFieldProperties { AllowedValues = ImmutableList.Create(10d) }); - await sut.ValidateAsync(CreateValue(20), errors); + await sut.ValidateAsync(CreateValue(20), false, errors); errors.ShouldBeEquivalentTo( new[] { " is not an allowed value" }); @@ -94,7 +94,7 @@ namespace Squidex.Core.Schemas { var sut = new NumberField(1, "my-number", new NumberFieldProperties()); - await sut.ValidateAsync(CreateValue("Invalid"), errors); + await sut.ValidateAsync(CreateValue("Invalid"), false, errors); errors.ShouldBeEquivalentTo( new[] { " is not a valid value" }); diff --git a/tests/Squidex.Core.Tests/Schemas/SchemaTests.cs b/tests/Squidex.Core.Tests/Schemas/SchemaTests.cs index 5bebbdbbd..8f5019b97 100644 --- a/tests/Squidex.Core.Tests/Schemas/SchemaTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/SchemaTests.cs @@ -295,9 +295,9 @@ namespace Squidex.Core.Schemas [Fact] public void Should_build_schema() { - var languages = new HashSet(new[] { Language.DE, Language.EN }); + var languagesConfig = LanguagesConfig.Create(Language.DE, Language.EN); - var jsonSchema = BuildMixedSchema().BuildJsonSchema(languages, (n, s) => new JsonSchema4 { SchemaReference = s }); + var jsonSchema = BuildMixedSchema().BuildJsonSchema(languagesConfig, (n, s) => new JsonSchema4 { SchemaReference = s }); Assert.NotNull(jsonSchema); } @@ -305,9 +305,9 @@ namespace Squidex.Core.Schemas [Fact] public void Should_build_edm_model() { - var languages = new HashSet(new[] { Language.DE, Language.EN }); + var languagesConfig = LanguagesConfig.Create(Language.DE, Language.EN); - var edmModel = BuildMixedSchema().BuildEdmType(languages, x => x); + var edmModel = BuildMixedSchema().BuildEdmType(languagesConfig, x => x); Assert.NotNull(edmModel); } diff --git a/tests/Squidex.Core.Tests/Schemas/StringFieldTests.cs b/tests/Squidex.Core.Tests/Schemas/StringFieldTests.cs index 6ad3b59b3..315eef187 100644 --- a/tests/Squidex.Core.Tests/Schemas/StringFieldTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/StringFieldTests.cs @@ -40,7 +40,7 @@ namespace Squidex.Core.Schemas { var sut = new StringField(1, "my-string", new StringFieldProperties { Label = "" }); - await sut.ValidateAsync(CreateValue(null), errors); + await sut.ValidateAsync(CreateValue(null), false, errors); Assert.Empty(errors); } @@ -50,7 +50,7 @@ namespace Squidex.Core.Schemas { var sut = new StringField(1, "my-string", new StringFieldProperties { IsRequired = true }); - await sut.ValidateAsync(CreateValue(null), errors); + await sut.ValidateAsync(CreateValue(null), false, errors); errors.ShouldBeEquivalentTo( new[] { " is required" }); @@ -61,7 +61,7 @@ namespace Squidex.Core.Schemas { var sut = new StringField(1, "my-string", new StringFieldProperties { MinLength = 10 }); - await sut.ValidateAsync(CreateValue("123"), errors); + await sut.ValidateAsync(CreateValue("123"), false, errors); errors.ShouldBeEquivalentTo( new[] { " must have more than '10' characters" }); @@ -72,7 +72,7 @@ namespace Squidex.Core.Schemas { var sut = new StringField(1, "my-string", new StringFieldProperties { MaxLength = 5 }); - await sut.ValidateAsync(CreateValue("12345678"), errors); + await sut.ValidateAsync(CreateValue("12345678"), false, errors); errors.ShouldBeEquivalentTo( new[] { " must have less than '5' characters" }); @@ -83,7 +83,7 @@ namespace Squidex.Core.Schemas { var sut = new StringField(1, "my-string", new StringFieldProperties { AllowedValues = ImmutableList.Create("Foo") }); - await sut.ValidateAsync(CreateValue("Bar"), errors); + await sut.ValidateAsync(CreateValue("Bar"), false, errors); errors.ShouldBeEquivalentTo( new[] { " is not an allowed value" }); @@ -94,7 +94,7 @@ namespace Squidex.Core.Schemas { var sut = new StringField(1, "my-string", new StringFieldProperties { Pattern = "[0-9]{3}" }); - await sut.ValidateAsync(CreateValue("abc"), errors); + await sut.ValidateAsync(CreateValue("abc"), false, errors); errors.ShouldBeEquivalentTo( new[] { " is not valid" }); @@ -105,7 +105,7 @@ namespace Squidex.Core.Schemas { var sut = new StringField(1, "my-string", new StringFieldProperties { Pattern = "[0-9]{3}", PatternMessage = "Custom Error Message" }); - await sut.ValidateAsync(CreateValue("abc"), errors); + await sut.ValidateAsync(CreateValue("abc"), false, errors); errors.ShouldBeEquivalentTo( new[] { "Custom Error Message" }); diff --git a/tests/Squidex.Core.Tests/Schemas/ValidationTestExtensions.cs b/tests/Squidex.Core.Tests/Schemas/ValidationTestExtensions.cs index 55b454c8f..45cc35b37 100644 --- a/tests/Squidex.Core.Tests/Schemas/ValidationTestExtensions.cs +++ b/tests/Squidex.Core.Tests/Schemas/ValidationTestExtensions.cs @@ -14,9 +14,9 @@ namespace Squidex.Core.Schemas { public static class ValidationTestExtensions { - public static Task ValidateAsync(this Field field, JToken value, IList errors) + public static Task ValidateAsync(this Field field, JToken value, bool isOptional, IList errors) { - return field.ValidateAsync(value, errors.Add); + return field.ValidateAsync(value, isOptional, errors.Add); } } } diff --git a/tests/Squidex.Core.Tests/Schemas/Validators/AllowedValuesValidatorTests.cs b/tests/Squidex.Core.Tests/Schemas/Validators/AllowedValuesValidatorTests.cs index e7e400a5d..293765945 100644 --- a/tests/Squidex.Core.Tests/Schemas/Validators/AllowedValuesValidatorTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/Validators/AllowedValuesValidatorTests.cs @@ -22,7 +22,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new AllowedValuesValidator(100, 200); - await sut.ValidateAsync(null, errors.Add); + await sut.ValidateAsync(null, false, errors.Add); Assert.Equal(0, errors.Count); } @@ -32,7 +32,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new AllowedValuesValidator(100, 200); - await sut.ValidateAsync(100, errors.Add); + await sut.ValidateAsync(100, false, errors.Add); Assert.Equal(0, errors.Count); } @@ -42,7 +42,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new AllowedValuesValidator(100, 200); - await sut.ValidateAsync(50, errors.Add); + await sut.ValidateAsync(50, false, errors.Add); errors.ShouldBeEquivalentTo( new[] { " is not an allowed value" }); diff --git a/tests/Squidex.Core.Tests/Schemas/Validators/PatternValidatorTests.cs b/tests/Squidex.Core.Tests/Schemas/Validators/PatternValidatorTests.cs index a7787da55..bccf15819 100644 --- a/tests/Squidex.Core.Tests/Schemas/Validators/PatternValidatorTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/Validators/PatternValidatorTests.cs @@ -22,7 +22,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new PatternValidator("[a-z]{3}:[0-9]{2}"); - await sut.ValidateAsync("abc:12", errors.Add); + await sut.ValidateAsync("abc:12", false, errors.Add); Assert.Equal(0, errors.Count); } @@ -32,7 +32,17 @@ namespace Squidex.Core.Schemas.Validators { var sut = new PatternValidator("[a-z]{3}:[0-9]{2}"); - await sut.ValidateAsync(null, errors.Add); + await sut.ValidateAsync(null, false, errors.Add); + + Assert.Equal(0, errors.Count); + } + + [Fact] + public async Task Should_not_add_error_if_value_is_empty() + { + var sut = new PatternValidator("[a-z]{3}:[0-9]{2}"); + + await sut.ValidateAsync("", false, errors.Add); Assert.Equal(0, errors.Count); } @@ -42,7 +52,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new PatternValidator("[a-z]{3}:[0-9]{2}"); - await sut.ValidateAsync("foo", errors.Add); + await sut.ValidateAsync("foo", false, errors.Add); errors.ShouldBeEquivalentTo( new[] { " is not valid" }); @@ -53,7 +63,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new PatternValidator("[a-z]{3}:[0-9]{2}", "Custom Error Message"); - await sut.ValidateAsync("foo", errors.Add); + await sut.ValidateAsync("foo", false, errors.Add); errors.ShouldBeEquivalentTo( new[] { "Custom Error Message" }); diff --git a/tests/Squidex.Core.Tests/Schemas/Validators/RangeValidatorTests.cs b/tests/Squidex.Core.Tests/Schemas/Validators/RangeValidatorTests.cs index 638d9ad20..414e5a922 100644 --- a/tests/Squidex.Core.Tests/Schemas/Validators/RangeValidatorTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/Validators/RangeValidatorTests.cs @@ -23,7 +23,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new RangeValidator(100, 200); - await sut.ValidateAsync(null, errors.Add); + await sut.ValidateAsync(null, false, errors.Add); Assert.Equal(0, errors.Count); } @@ -37,7 +37,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new RangeValidator(min, max); - await sut.ValidateAsync(1500, errors.Add); + await sut.ValidateAsync(1500, false, errors.Add); Assert.Equal(0, errors.Count); } @@ -55,7 +55,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new RangeValidator(2000, null); - await sut.ValidateAsync(1500, errors.Add); + await sut.ValidateAsync(1500, false, errors.Add); errors.ShouldBeEquivalentTo( new[] { " must be greater than '2000'" }); @@ -66,7 +66,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new RangeValidator(null, 1000); - await sut.ValidateAsync(1500, errors.Add); + await sut.ValidateAsync(1500, false, errors.Add); errors.ShouldBeEquivalentTo( new[] { " must be less than '1000'" }); diff --git a/tests/Squidex.Core.Tests/Schemas/Validators/RequiredStringValidatorTests.cs b/tests/Squidex.Core.Tests/Schemas/Validators/RequiredStringValidatorTests.cs index 24df1015f..bd0ff2ca7 100644 --- a/tests/Squidex.Core.Tests/Schemas/Validators/RequiredStringValidatorTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/Validators/RequiredStringValidatorTests.cs @@ -26,7 +26,17 @@ namespace Squidex.Core.Schemas.Validators { var sut = new RequiredStringValidator(); - await sut.ValidateAsync(value, errors.Add); + await sut.ValidateAsync(value, false, errors.Add); + + Assert.Equal(0, errors.Count); + } + + [Fact] + public async Task Should_not_add_error_if_optional() + { + var sut = new RequiredStringValidator(); + + await sut.ValidateAsync(string.Empty, true, errors.Add); Assert.Equal(0, errors.Count); } @@ -36,7 +46,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new RequiredStringValidator(); - await sut.ValidateAsync(true, errors.Add); + await sut.ValidateAsync(true, false, errors.Add); Assert.Equal(0, errors.Count); } @@ -46,7 +56,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new RequiredStringValidator(true); - await sut.ValidateAsync(string.Empty, errors.Add); + await sut.ValidateAsync(string.Empty, false, errors.Add); errors.ShouldBeEquivalentTo( new[] { " is required" }); @@ -57,7 +67,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new RequiredStringValidator(); - await sut.ValidateAsync(null, errors.Add); + await sut.ValidateAsync(null, false, errors.Add); errors.ShouldBeEquivalentTo( new[] { " is required" }); diff --git a/tests/Squidex.Core.Tests/Schemas/Validators/RequiredValidatorTests.cs b/tests/Squidex.Core.Tests/Schemas/Validators/RequiredValidatorTests.cs index bcd3c6e1d..fa64b1417 100644 --- a/tests/Squidex.Core.Tests/Schemas/Validators/RequiredValidatorTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/Validators/RequiredValidatorTests.cs @@ -22,7 +22,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new RequiredValidator(); - await sut.ValidateAsync(true, errors.Add); + await sut.ValidateAsync(true, false, errors.Add); Assert.Equal(0, errors.Count); } @@ -32,7 +32,17 @@ namespace Squidex.Core.Schemas.Validators { var sut = new RequiredValidator(); - await sut.ValidateAsync(string.Empty, errors.Add); + await sut.ValidateAsync(string.Empty, false, errors.Add); + + Assert.Equal(0, errors.Count); + } + + [Fact] + public async Task Should_not_add_error_if_optional() + { + var sut = new RequiredValidator(); + + await sut.ValidateAsync(null, true, errors.Add); Assert.Equal(0, errors.Count); } @@ -42,7 +52,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new RequiredValidator(); - await sut.ValidateAsync(null, errors.Add); + await sut.ValidateAsync(null, false, errors.Add); errors.ShouldBeEquivalentTo( new[] { " is required" }); diff --git a/tests/Squidex.Core.Tests/Schemas/Validators/StringLengthValidatorTests.cs b/tests/Squidex.Core.Tests/Schemas/Validators/StringLengthValidatorTests.cs index f1c728c1a..6f8797f49 100644 --- a/tests/Squidex.Core.Tests/Schemas/Validators/StringLengthValidatorTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/Validators/StringLengthValidatorTests.cs @@ -24,7 +24,17 @@ namespace Squidex.Core.Schemas.Validators { var sut = new StringLengthValidator(100, 200); - await sut.ValidateAsync(null, errors.Add); + await sut.ValidateAsync(null, false, errors.Add); + + Assert.Equal(0, errors.Count); + } + + [Fact] + public async Task Should_not_error_if_value_is_empty() + { + var sut = new StringLengthValidator(100, 200); + + await sut.ValidateAsync(string.Empty, false, errors.Add); Assert.Equal(0, errors.Count); } @@ -38,7 +48,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new StringLengthValidator(min, max); - await sut.ValidateAsync(CreateString(1500), errors.Add); + await sut.ValidateAsync(CreateString(1500), false, errors.Add); Assert.Equal(0, errors.Count); } @@ -56,7 +66,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new StringLengthValidator(2000, null); - await sut.ValidateAsync(CreateString(1500), errors.Add); + await sut.ValidateAsync(CreateString(1500), false, errors.Add); errors.ShouldBeEquivalentTo( new[] { " must have more than '2000' characters" }); @@ -67,7 +77,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new StringLengthValidator(null, 1000); - await sut.ValidateAsync(CreateString(1500), errors.Add); + await sut.ValidateAsync(CreateString(1500), false, errors.Add); errors.ShouldBeEquivalentTo( new[] { " must have less than '1000' characters" }); diff --git a/tests/Squidex.Infrastructure.Tests/GuardTests.cs b/tests/Squidex.Infrastructure.Tests/GuardTests.cs index 527947af6..1c11571bc 100644 --- a/tests/Squidex.Infrastructure.Tests/GuardTests.cs +++ b/tests/Squidex.Infrastructure.Tests/GuardTests.cs @@ -325,7 +325,7 @@ namespace Squidex.Infrastructure [Fact] public void NotEmpty_should_do_nothing_for_value_collection() { - Guard.NotEmpty(new [] { 1, 2, 3 }, "parameter"); + Guard.NotEmpty(new[] { 1, 2, 3 }, "parameter"); } [Fact] diff --git a/tests/Squidex.Infrastructure.Tests/LanguageTests.cs b/tests/Squidex.Infrastructure.Tests/LanguageTests.cs index 6afdddf17..e9b771217 100644 --- a/tests/Squidex.Infrastructure.Tests/LanguageTests.cs +++ b/tests/Squidex.Infrastructure.Tests/LanguageTests.cs @@ -66,6 +66,22 @@ namespace Squidex.Infrastructure Assert.False(Language.IsValidLanguage("xx")); } + [Fact] + public void Should_make_implicit_conversion_to_language() + { + Language language = "de"; + + Assert.Equal(Language.DE, language); + } + + [Fact] + public void Should_make_implicit_conversion_to_string() + { + string iso2Code = Language.DE; + + Assert.Equal("de", iso2Code); + } + [Theory] [InlineData("de", "German")] [InlineData("en", "English")] diff --git a/tests/Squidex.Infrastructure.Tests/Reflection/ReflectionExtensionTests.cs b/tests/Squidex.Infrastructure.Tests/Reflection/ReflectionExtensionTests.cs index 1168cd459..4ef03cb63 100644 --- a/tests/Squidex.Infrastructure.Tests/Reflection/ReflectionExtensionTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Reflection/ReflectionExtensionTests.cs @@ -40,7 +40,7 @@ namespace Squidex.Infrastructure.Reflection { var properties = typeof(IMyMain).GetPublicProperties().Select(x => x.Name).OrderBy(x => x).ToArray(); - Assert.Equal(new [] { "MainProp", "Sub1Prop", "Sub2Prop" }, properties); + Assert.Equal(new[] { "MainProp", "Sub1Prop", "Sub2Prop" }, properties); } [Fact] diff --git a/tests/Squidex.Read.Tests/MongoDb/Contents/ODataQueryTests.cs b/tests/Squidex.Read.Tests/MongoDb/Contents/ODataQueryTests.cs index dca834f67..580f65008 100644 --- a/tests/Squidex.Read.Tests/MongoDb/Contents/ODataQueryTests.cs +++ b/tests/Squidex.Read.Tests/MongoDb/Contents/ODataQueryTests.cs @@ -7,7 +7,6 @@ // ========================================================================== using System; -using System.Collections.Generic; using System.Collections.Immutable; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; @@ -15,6 +14,7 @@ using Microsoft.OData.Edm; using MongoDB.Bson.Serialization; using MongoDB.Driver; using Moq; +using Squidex.Core; using Squidex.Core.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; @@ -45,11 +45,7 @@ namespace Squidex.Read.MongoDb.Contents private readonly IBsonSerializerRegistry registry = BsonSerializer.SerializerRegistry; private readonly IBsonSerializer serializer = BsonSerializer.SerializerRegistry.GetSerializer(); private readonly IEdmModel edmModel; - private readonly HashSet languages = new HashSet - { - Language.EN, - Language.DE - }; + private readonly LanguagesConfig languagesConfig = LanguagesConfig.Create(Language.EN, Language.DE); static ODataQueryTests() { @@ -65,7 +61,7 @@ namespace Squidex.Read.MongoDb.Contents schemaEntity.Setup(x => x.Version).Returns(3); schemaEntity.Setup(x => x.Schema).Returns(schema); - edmModel = builder.BuildEdmModel(schemaEntity.Object, languages); + edmModel = builder.BuildEdmModel(schemaEntity.Object, languagesConfig); } [Fact] diff --git a/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs index 367911618..45e940379 100644 --- a/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs +++ b/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs @@ -227,6 +227,20 @@ namespace Squidex.Write.Apps }); } + [Fact] + public async Task UpdateLanguage_should_update_domain_object() + { + CreateApp() + .AddLanguage(CreateCommand(new AddLanguage { Language = language })); + + var context = CreateContextForCommand(new UpdateLanguage { Language = language }); + + await TestUpdate(app, async _ => + { + await sut.HandleAsync(context); + }); + } + private AppDomainObject CreateApp() { app.Create(CreateCommand(new CreateApp { Name = AppName })); diff --git a/tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs b/tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs index 750f1c8c2..523680dbf 100644 --- a/tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs +++ b/tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs @@ -7,6 +7,7 @@ // ========================================================================== using System; +using System.Collections.Generic; using System.Linq; using Squidex.Core.Apps; using Squidex.Events.Apps; @@ -64,8 +65,7 @@ namespace Squidex.Write.Apps .ShouldHaveSameEvents( CreateEvent(new AppCreated { Name = AppName }), CreateEvent(new AppContributorAssigned { ContributorId = User.Identifier, Permission = PermissionLevel.Owner }), - CreateEvent(new AppLanguageAdded { Language = Language.EN }), - CreateEvent(new AppMasterLanguageSet { Language = Language.EN }) + CreateEvent(new AppLanguageAdded { Language = Language.EN }) ); } @@ -432,7 +432,7 @@ namespace Squidex.Write.Apps public void RemoveLanguage_should_create_events() { CreateApp(); - CreateLanguage(); + CreateLanguage(Language.DE); sut.RemoveLanguage(CreateCommand(new RemoveLanguage { Language = Language.DE })); @@ -474,27 +474,72 @@ namespace Squidex.Write.Apps } [Fact] - public void SetMasterLanguage_should_throw_if_already_master_language() + public void SetMasterLanguage_should_create_events() + { + CreateApp(); + CreateLanguage(Language.DE); + + sut.SetMasterLanguage(CreateCommand(new SetMasterLanguage { Language = Language.DE })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new AppMasterLanguageSet { Language = Language.DE }) + ); + } + + [Fact] + public void UpdateLanguage_should_throw_if_not_created() + { + Assert.Throws(() => + { + sut.UpdateLanguage(CreateCommand(new UpdateLanguage { Language = Language.EN })); + }); + } + + [Fact] + public void UpdateLanguage_should_throw_if_command_is_not_valid() { CreateApp(); Assert.Throws(() => { - sut.SetMasterLanguage(CreateCommand(new SetMasterLanguage { Language = Language.EN })); + sut.UpdateLanguage(CreateCommand(new UpdateLanguage())); }); } [Fact] - public void SetMasterLanguage_should_create_events() + public void UpdateLanguage_should_throw_if_language_not_found() { CreateApp(); - CreateLanguage(); - sut.SetMasterLanguage(CreateCommand(new SetMasterLanguage { Language = Language.DE })); + Assert.Throws(() => + { + sut.UpdateLanguage(CreateCommand(new UpdateLanguage { Language = Language.DE })); + }); + } + + [Fact] + public void UpdateLanguage_should_throw_if_master_language() + { + CreateApp(); + + Assert.Throws(() => + { + sut.UpdateLanguage(CreateCommand(new UpdateLanguage { Language = Language.EN, IsOptional = true })); + }); + } + + [Fact] + public void UpdateLanguage_should_create_events() + { + CreateApp(); + CreateLanguage(Language.DE); + + sut.UpdateLanguage(CreateCommand(new UpdateLanguage { Language = Language.DE, Fallback = new List { Language.EN } })); sut.GetUncomittedEvents() .ShouldHaveSameEvents( - CreateEvent(new AppMasterLanguageSet { Language = Language.DE }) + CreateEvent(new AppLanguageUpdated { Language = Language.DE, Fallback = new List { Language.EN } }) ); } @@ -512,9 +557,9 @@ namespace Squidex.Write.Apps ((IAggregate)sut).ClearUncommittedEvents(); } - private void CreateLanguage() + private void CreateLanguage(Language language) { - sut.AddLanguage(CreateCommand(new AddLanguage { Language = Language.DE })); + sut.AddLanguage(CreateCommand(new AddLanguage { Language = language })); ((IAggregate)sut).ClearUncommittedEvents(); } diff --git a/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs index d7867095f..19597d1ed 100644 --- a/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs +++ b/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs @@ -9,6 +9,7 @@ using System; using System.Threading.Tasks; using Moq; +using Squidex.Core; using Squidex.Core.Contents; using Squidex.Core.Schemas; using Squidex.Infrastructure; @@ -34,6 +35,7 @@ namespace Squidex.Write.Contents private readonly Mock schemaEntity = new Mock(); private readonly Mock appEntity = new Mock(); private readonly ContentData data = new ContentData().AddField("my-field", new ContentFieldData().SetValue(1)); + private readonly LanguagesConfig languagesConfig = LanguagesConfig.Create(Language.DE); private readonly Guid contentId = Guid.NewGuid(); public ContentCommandHandlerTests() @@ -47,7 +49,7 @@ namespace Squidex.Write.Contents sut = new ContentCommandHandler(Handler, appProvider.Object, schemaProvider.Object); - appEntity.Setup(x => x.Languages).Returns(new[] { Language.DE }); + appEntity.Setup(x => x.LanguagesConfig).Returns(languagesConfig); appProvider.Setup(x => x.FindAppByIdAsync(AppId)).Returns(Task.FromResult(appEntity.Object)); schemaEntity.Setup(x => x.Schema).Returns(schema);