Skip to content

Commit

Permalink
Item converters. (#892)
Browse files Browse the repository at this point in the history
  • Loading branch information
SebastianStehle authored Jun 29, 2022
1 parent c0965bf commit 804b6b1
Show file tree
Hide file tree
Showing 17 changed files with 1,392 additions and 756 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================

using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;

namespace Squidex.Domain.Apps.Core.ConvertContent
{
public sealed class AddSchemaNames : IContentItemConverter
{
private readonly ResolvedComponents components;

public AddSchemaNames(ResolvedComponents components)
{
this.components = components;
}

public JsonObject ConvertItem(IField field, JsonObject source)
{
if (field is IArrayField)
{
return source;
}

if (source.ContainsKey("schemaName"))
{
return source;
}

if (!Component.IsValid(source, out var discriminator))
{
return source;
}

var id = DomainId.Create(discriminator);

if (components.TryGetValue(id, out var schema))
{
source["schemaName"] = schema.Name;
}

return source;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,80 @@
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;

#pragma warning disable RECS0033 // Convert 'if' to '||' expression

namespace Squidex.Domain.Apps.Core.ConvertContent
{
public static class ContentConverter
public sealed class ContentConverter
{
public static ContentData Convert(this ContentData content, Schema schema, params FieldConverter[] converters)
private readonly List<IContentItemConverter> itemConverters = new List<IContentItemConverter>();
private readonly List<IContentFieldAfterConverter> fieldAfterConverters = new List<IContentFieldAfterConverter>();
private readonly List<IContentFieldConverter> fieldConverters = new List<IContentFieldConverter>();
private readonly List<IContentValueConverter> valueConverters = new List<IContentValueConverter>();
private readonly ResolvedComponents components;
private readonly Schema schema;

public ContentConverter(ResolvedComponents components, Schema schema)
{
Guard.NotNull(schema);
this.components = components;
this.schema = schema;
}

var result = new ContentData(content.Count);
public ContentConverter Add(IConverter converter)
{
if (converter is IContentItemConverter itemConverter)
{
itemConverters.Add(itemConverter);
}

if (converters == null || converters.Length == 0)
if (converter is IContentFieldConverter fieldConverter)
{
return result;
fieldConverters.Add(fieldConverter);
}

if (converter is IContentFieldAfterConverter fieldAfterConverter)
{
fieldAfterConverters.Add(fieldAfterConverter);
}

if (converter is IContentValueConverter valueConverter)
{
valueConverters.Add(valueConverter);
}

return this;
}

public ContentData Convert(ContentData content)
{
Guard.NotNull(schema);

// The conversion process assumes that we have ownership of the data and can manipulate it.
// Clones are only created to save allocations.
var result = new ContentData(content.Count);

foreach (var (fieldName, fieldData) in content)
{
if (fieldData == null || !schema.FieldsByName.TryGetValue(fieldName, out var field))
{
continue;
}

ContentFieldData? newData = fieldData;
// Some conversions are faster to do upfront, e.g. to remove hidden fields.
var newData = ConvertField(field, fieldData);

if (newData != null)
if (newData == null)
{
newData = ConvertData(field, newData, converters);
continue;
}

newData = ConvertValues(field, newData);

// Some conversions are faster to do later, e.g. fallback handling for languages.
newData = ConvertFieldAfter(field, newData);

if (newData != null)
{
result.Add(field.Name, newData);
Expand All @@ -47,16 +91,26 @@ public static ContentData Convert(this ContentData content, Schema schema, param
return result;
}

private static ContentFieldData? ConvertData(IRootField field, ContentFieldData data, FieldConverter[] converters)
private ContentFieldData? ConvertField(IRootField field, ContentFieldData? data)
{
if (converters == null || converters.Length == 0)
foreach (var converter in fieldConverters)
{
return data;
data = converter.ConvertField(field, data!);

if (data == null)
{
break;
}
}

foreach (var converter in converters)
return data;
}

private ContentFieldData? ConvertFieldAfter(IRootField field, ContentFieldData? data)
{
foreach (var converter in fieldAfterConverters)
{
data = converter(data!, field)!;
data = converter.ConvertFieldAfter(field, data!);

if (data == null)
{
Expand All @@ -66,5 +120,187 @@ public static ContentData Convert(this ContentData content, Schema schema, param

return data;
}

private (bool Remove, JsonValue) ConvertByType<T>(T field, JsonValue value, IField? parent) where T : IField
{
switch (field)
{
case IArrayField arrayField:
return ConvertArray(arrayField, value);

case IField<ComponentFieldProperties>:
return ConvertComponent(value, field);

case IField<ComponentsFieldProperties>:
return ConvertComponents(value, field);

default:
return ConvertValue(field, value, parent);
}
}

private (bool Remove, JsonValue) ConvertArray(IArrayField field, JsonValue value)
{
if (value.Value is not JsonArray array)
{
return (true, default);
}

for (int i = 0; i < array.Count; i++)
{
var oldValue = array[i];

var (removed, newValue) = ConvertArrayItem(field, oldValue);

if (removed)
{
array.RemoveAt(i);
i--;
}
else if (!ReferenceEquals(newValue.Value, oldValue.Value))
{
array[i] = newValue;
}
}

return (false, array);
}

private (bool Remove, JsonValue) ConvertComponents(JsonValue value, IField parent)
{
if (value.Value is not JsonArray array)
{
return (true, default);
}

for (int i = 0; i < array.Count; i++)
{
var oldValue = array[i];

var (removed, newValue) = ConvertComponent(oldValue, parent);

if (removed)
{
array.RemoveAt(i);
i--;
}
else if (!ReferenceEquals(newValue.Value, oldValue.Value))
{
// Faster to check for reference equality than for deep equals.
array[i] = newValue;
}
}

return (false, array);
}

private (bool Remove, JsonValue) ConvertComponent(JsonValue value, IField parent)
{
if (value.Value is not JsonObject obj || !obj.TryGetValue(Component.Discriminator, out var discriminator))
{
return (true, default);
}

if (!components.TryGetValue(DomainId.Create(discriminator.ToString()), out var component))
{
return (true, default);
}

return (false, ConvertNested(component.FieldCollection, obj, parent));
}

private (bool Remove, JsonValue) ConvertArrayItem(IArrayField field, JsonValue value)
{
if (value.Value is not JsonObject obj)
{
return (true, default);
}

return (false, ConvertNested(field.FieldCollection, obj, field));
}

private ContentFieldData ConvertValues(IField field, ContentFieldData source)
{
ContentFieldData? result = null;

foreach (var (key, oldValue) in source)
{
var (removed, newData) = ConvertByType(field, oldValue, null);

// Create a copy to avoid allocations if nothing has been changed.
if (removed)
{
result ??= new ContentFieldData(source);
result.Remove(key);
}
else if (!ReferenceEquals(newData.Value, oldValue.Value))
{
// Faster to check for reference equality than for deep equals.
result ??= new ContentFieldData(source);
result[key] = newData;
}
}

return result ?? source;
}

private JsonValue ConvertNested<T>(FieldCollection<T> fields, JsonObject source, IField parent) where T : IField
{
JsonObject? result = null;

foreach (var (key, oldValue) in source)
{
var newValue = oldValue;

var remove = false;

if (fields.ByName.TryGetValue(key, out var field))
{
(remove, newValue) = ConvertByType(field, oldValue, parent);
}
else if (key != Component.Discriminator)
{
remove = true;
}

// Create a copy to avoid allocations if nothing has been changed.
if (remove)
{
result ??= new JsonObject(source);
result.Remove(key);
}
else if (!ReferenceEquals(newValue.Value, oldValue.Value))
{
// Faster to check for reference equality than for deep equals.
result ??= new JsonObject(source);
result[key] = newValue;
}
}

result ??= source;

foreach (var converter in itemConverters)
{
result = converter.ConvertItem(parent, result);
}

return result ?? source;
}

private (bool Remove, JsonValue) ConvertValue(IField field, JsonValue value, IField? parent)
{
foreach (var converter in valueConverters)
{
// Use a tuple and not a nullable result to avoid boxing and allocations.
(var remove, value) = converter.ConvertValue(field, value, parent);

if (remove)
{
return (true, default);
}
}

return (false, value);
}
}
}
Loading

0 comments on commit 804b6b1

Please sign in to comment.