Skip to content

Commit

Permalink
implementing CustomModel #3557
Browse files Browse the repository at this point in the history
  • Loading branch information
iJungleboy committed Jan 22, 2025
1 parent 7e7d266 commit 3a3f459
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 48 deletions.
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
using System;
using System.Collections.Generic;
using ToSic.Sxc.Data;
using ToSic.Sxc.Data.Internal;

namespace ToSic.Sxc.Tests.DataTests.ModelTests;

internal static class DataModelAnalyzerTestAccessors
{
public static string GetContentTypeNameTac<T>()
//public static string GetContentTypeNameTac<T>()
// where T : class, ICanWrapData
//=> DataModelAnalyzer.GetContentTypeNameCsv<T>();
public static (List<string> List, string Flat) GetContentTypeNamesTac<T>()
where T : class, ICanWrapData
=> DataModelAnalyzer.GetContentTypeNames<T>();
=> DataModelAnalyzer.GetContentTypeNamesList<T>();

public static string GetStreamNameTac<T>()
public static (List<string> List, string Flat) GetStreamNameListTac<T>()
where T : class, ICanWrapData
=> DataModelAnalyzer.GetStreamName<T>();
=> DataModelAnalyzer.GetStreamNameList<T>();

public static Type GetTargetTypeTac<T>()
where T : class, ICanWrapData
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ public class DataModelAnalyzerTests : TestBaseSxcDb
{
private void AssertTypeName<T>(string name)
where T : class, ICanWrapData =>
Assert.AreEqual(name, DataModelAnalyzerTestAccessors.GetContentTypeNameTac<T>());
private void AssertStreamName<T>(string name)
Assert.AreEqual(name, DataModelAnalyzerTestAccessors.GetContentTypeNamesTac<T>().Flat);

private void AssertStreamNames<T>(string namesCsv)
where T : class, ICanWrapData =>
Assert.AreEqual(name, DataModelAnalyzerTestAccessors.GetStreamNameTac<T>());
Assert.AreEqual(namesCsv, DataModelAnalyzerTestAccessors.GetStreamNameListTac<T>().Flat);

class NotDecorated: ICanWrapData;

Expand All @@ -22,18 +23,34 @@ public void NotDecoratedDataModelType() =>

[TestMethod]
public void NotDecoratedDataModelStream() =>
AssertStreamName<NotDecorated>(nameof(NotDecorated));
AssertStreamNames<NotDecorated>(nameof(NotDecorated));

[TestMethod]
public void NotDecoratedDataModelStreamList() =>
AssertStreamNames<NotDecorated>(nameof(NotDecorated));

class NotDecoratedModel : ICanWrapData;

[TestMethod]
public void NotDecoratedModelStreamList() =>
AssertStreamNames<NotDecoratedModel>(nameof(NotDecoratedModel) + "," + nameof(NotDecorated));

// Objects starting with an "I" won't have the "I" removed in the name checks
class INotDecoratedModel : ICanWrapData;

[TestMethod]
public void INotDecoratedModelStreamList() =>
AssertStreamNames<INotDecoratedModel>(nameof(INotDecoratedModel) + ",INotDecorated");

interface INotDecorated: ICanWrapData;

[TestMethod]
public void INotDecoratedType() =>
AssertTypeName<INotDecorated>(nameof(INotDecorated).Substring(1));
AssertTypeName<INotDecorated>(nameof(INotDecorated) + ',' + nameof(INotDecorated).Substring(1));

[TestMethod]
public void INotDecoratedStream() =>
AssertStreamName<INotDecorated>(nameof(INotDecorated));
AssertStreamNames<INotDecorated>(nameof(INotDecorated) + ",NotDecorated");


private const string ForContentType1 = "Abc";
Expand All @@ -47,7 +64,7 @@ public void DecoratedType() =>

[TestMethod]
public void DecoratedStream() =>
AssertStreamName<Decorated>(StreamName1);
AssertStreamNames<Decorated>(StreamName1);


class InheritDecorated : Decorated;
Expand All @@ -58,7 +75,7 @@ public void InheritDecoratedType() =>

[TestMethod]
public void InheritDecoratedStream() =>
AssertStreamName<InheritDecorated>(nameof(InheritDecorated));
AssertStreamNames<InheritDecorated>(nameof(InheritDecorated));


private const string ForContentTypeReDecorated = "ReDec";
Expand All @@ -71,7 +88,7 @@ public void InheritReDecoratedType() =>
AssertTypeName<InheritReDecorated>(ForContentTypeReDecorated);
[TestMethod]
public void InheritReDecoratedStream() =>
AssertStreamName<InheritReDecorated>(StreamNameReDecorated);
AssertStreamNames<InheritReDecorated>(StreamNameReDecorated + ",Abc");


private const string ForContentTypeIDecorated = "IDec";
Expand All @@ -85,6 +102,6 @@ public void IDecoratedType() =>

[TestMethod]
public void IDecoratedStream() =>
AssertStreamName<IDecorated>(StreamNameIDecorated);
AssertStreamNames<IDecorated>(StreamNameIDecorated);

}
6 changes: 3 additions & 3 deletions Src/Sxc/ToSic.Sxc/Apps/IAppDataTyped.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public interface IAppDataTyped: IDataSource
/// </remarks>
public IEnumerable<T> GetAll<T>(NoParamOrder protector = default, string typeName = default,
bool nullIfNotFound = default)
where T : class, ICanWrapData, new();
where T : class, ICanWrapData;

/// <summary>
/// Get a single item from the app of the specified type.
Expand All @@ -78,7 +78,7 @@ public IEnumerable<T> GetAll<T>(NoParamOrder protector = default, string typeNam
/// Released in v17.03.
/// </remarks>
T GetOne<T>(int id, NoParamOrder protector = default, bool skipTypeCheck = false)
where T : class, ICanWrapData, new();
where T : class, ICanWrapData;


/// <summary>
Expand All @@ -93,7 +93,7 @@ T GetOne<T>(int id, NoParamOrder protector = default, bool skipTypeCheck = false
/// Released in v17.03.
/// </remarks>
public T GetOne<T>(Guid id, NoParamOrder protector = default, bool skipTypeCheck = false)
where T : class, ICanWrapData, new();
where T : class, ICanWrapData;

#endregion
}
26 changes: 24 additions & 2 deletions Src/Sxc/ToSic.Sxc/Apps/Internal/AppDataTyped.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using ToSic.Eav.Apps.Internal.Api01;
using ToSic.Eav.DataSource;
using ToSic.Eav.DataSource.Internal.Caching;
using ToSic.Eav.DataSources.Internal;
using ToSic.Lib.DI;
Expand Down Expand Up @@ -42,10 +43,31 @@ private ServiceKit16 Kit
/// <inheritdoc />
IEnumerable<T> IAppDataTyped.GetAll<T>(NoParamOrder protector, string typeName, bool nullIfNotFound)
{
var streamName = typeName ?? DataModelAnalyzer.GetStreamName<T>();
//var streamName = typeName ?? DataModelAnalyzer.GetStreamName<T>();
//var errStreamName = streamName;

var streamNames = typeName == null
? DataModelAnalyzer.GetStreamNameList<T>()
: ([typeName], typeName);

// Get the list - will be null if not found
var list = GetStream(streamName, nullIfNotFound: nullIfNotFound);
IDataStream list = null;
foreach (var streamName2 in streamNames.List)
list ??= GetStream(streamName2, nullIfNotFound: true);


//// If we didn't find it, check if the stream name is *Model and try without that common suffix
//if (list == null && streamName.EndsWith("Model"))
//{
// var shorterName = streamName.Substring(0, streamName.Length - 5);
// errStreamName += "," + shorterName;
// list = GetStream(shorterName, nullIfNotFound: true);
//}

// If we didn't find anything yet, then we must now try to re-access the stream
// but in a way which will throw an exception with the expected stream names
if (list == null && !nullIfNotFound)
list = GetStream(/*errStreamName*/streamNames.Flat, nullIfNotFound: false);

return list == null
? null
Expand Down
8 changes: 5 additions & 3 deletions Src/Sxc/ToSic.Sxc/Custom.Data/CustomItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ namespace Custom.Data;
/// ```c#
/// namespace AppCode.Data
/// {
/// class MyPerson : CustomItem
/// class MyPerson : Custom.Data.CustomItem
/// {
/// // New custom property
/// public string Name => _item.String("Name");
Expand Down Expand Up @@ -273,7 +273,8 @@ public IEnumerable<ITypedItem> Parents(NoParamOrder noParamOrder = default, stri
public IMetadata Metadata => _item.Metadata;

/// <inheritdoc />
public IField Field(string name, NoParamOrder noParamOrder = default, bool? required = default) => _item.Field(name, noParamOrder, required);
public IField Field(string name, NoParamOrder noParamOrder = default, bool? required = default)
=> _item.Field(name, noParamOrder, required);


#region Core Data: Id, Guid, Title, Type
Expand Down Expand Up @@ -365,6 +366,7 @@ protected IEnumerable<T> AsList<T>(IEnumerable<ITypedItem> source, NoParamOrder
/// <summary>
/// Get by name should never throw an error, as it's used to get null if not found.
/// </summary>
object ICanGetByName.Get(string name) => (this as ITypedItem).Get(name, required: false);
object ICanGetByName.Get(string name)
=> (this as ITypedItem).Get(name, required: false);

}
23 changes: 23 additions & 0 deletions Src/Sxc/ToSic.Sxc/Custom.Data/CustomModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using ToSic.Sxc.Data.Models;

// ReSharper disable once CheckNamespace
namespace Custom.Data;

/// <summary>
/// Base class for custom models. Similar to <see cref="CustomItem"/> but without predefined public properties or methods.
/// </summary>
/// <remarks>
/// This is a lightweight custom object which doesn't have public properties
/// like `Id` or methods such as `String(...)`.
///
/// It's ideal for data models which need full control,
/// like for serializing or just to reduce the API surface.
///
/// You can access the underlying (protected) `_item` property to get the raw data.
/// And it also has the (protected) `As&lt;...&gt;()` conversion for typed sub-properties.
///
/// History: New in 19.03
/// </remarks>
[PublicApi]
[ModelSource(ContentTypes = "*")]
public class CustomModel: ModelFromItem;
47 changes: 33 additions & 14 deletions Src/Sxc/ToSic.Sxc/Data/Internal/Factory/CodeDataFactory_AsCustom.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,31 @@ public TCustom AsCustomFrom<TCustom, TData>(TData item)
var bestType = DataModelAnalyzer.GetTargetType<TCustom>();
var newT = ActivatorUtilities.CreateInstance(serviceProvider, bestType) as TCustom;

// Should be an ITypedItemWrapper, but not enforced in the signature
if (newT is ICanWrap<TData> withMatchingSetup)
withMatchingSetup.Setup(item, this);
// DataModelOfEntity can also be filled from Typed (but ATM not the other way around)
else if (newT is ICanWrap<IEntity> forEntity && item is ICanBeEntity canBeEntity)
forEntity.Setup(canBeEntity.Entity, this);
else
throw new($"The custom type {typeof(TCustom).Name} does not implement {nameof(ICanWrap<TData>)}. This is probably a mistake.");
return newT;
switch (newT)
{
// Should be an ITypedItemWrapper, but not enforced in the signature
case ICanWrap<TData> withMatchingSetup:
withMatchingSetup.Setup(item, this);
return newT;
// In some cases the type of the data is already a model, so we need to unwrap it
case ICanWrap<ITypedItem> forItem when item is ICanBeItem canBeItem:
forItem.Setup(canBeItem.Item, this);
return newT;
// DataModelOfEntity can also be filled from Typed (but ATM not the other way around)
case ICanWrap<IEntity> forEntity when item is ICanBeEntity canBeEntity:
forEntity.Setup(canBeEntity.Entity, this);
return newT;
// In some cases we can only wrap an item, but the data is an entity-based model
case ICanWrap<ITypedItem> forTypedItem when item is ICanBeEntity canBeEntity:
forTypedItem.Setup(AsItem(canBeEntity.Entity), this);
return newT;
default:
throw new($"The custom type {typeof(TCustom).Name} does not implement {nameof(ICanWrap<TData>)}. This is probably a mistake.");
}
}

internal TCustom GetOne<TCustom>(Func<IEntity> getItem, object id, bool skipTypeCheck)
where TCustom : class, ICanWrapData, new()
where TCustom : class, ICanWrapData
{
var item = getItem();
if (item == null)
Expand All @@ -54,10 +66,17 @@ internal TCustom GetOne<TCustom>(Func<IEntity> getItem, object id, bool skipType
return AsCustom<TCustom>(item);

// Do Type-Name check
var typeName = DataModelAnalyzer.GetContentTypeNames<TCustom>().Split(',');
if (typeName.FirstOrDefault() != ModelSourceAttribute.ForAnyContentType && !typeName.Any(tn => item.Type.Is(tn)))
throw new($"Item with ID {id} is not a {typeName}. This is probably a mistake, otherwise use {nameof(skipTypeCheck)}: true");
return AsCustom<TCustom>(item);
var typeNames = DataModelAnalyzer.GetContentTypeNamesList<TCustom>();

// Check all type names if they are `*` or match the data ContentType
if (typeNames.List.Any(t => t == ModelSourceAttribute.ForAnyContentType || item.Type.Is(t)))
return AsCustom<TCustom>(item);

throw new(
$"Item with ID {id} is not a '{typeNames.Flat}'. " +
$"This is probably a mistake, otherwise use '{nameof(skipTypeCheck)}: true' " +
$"or apply an attribute [{nameof(ModelSourceAttribute)}({nameof(ModelSourceAttribute.ContentTypes)} = \"expected-type-name\")] to your model class. "
);
}


Expand Down
59 changes: 51 additions & 8 deletions Src/Sxc/ToSic.Sxc/Data/Internal/Factory/DataModelAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ internal class DataModelAnalyzer
/// </summary>
/// <typeparam name="TCustom"></typeparam>
/// <returns></returns>
internal static string GetContentTypeNames<TCustom>() where TCustom : ICanWrapData =>
internal static string GetContentTypeNameCsv<TCustom>() where TCustom : ICanWrapData =>
ContentTypeNames.Get<TCustom, ModelSourceAttribute>(a =>
{
// If we have an attribute, use the value provided (unless not specified)
Expand All @@ -31,18 +31,61 @@ internal static string GetContentTypeNames<TCustom>() where TCustom : ICanWrapDa
});
private static readonly ClassAttributeLookup<string> ContentTypeNames = new();

/// <summary>
/// Figure out the expected ContentTypeName of a DataWrapper type.
/// If it is decorated with <see cref="ModelSourceAttribute"/> then use the information it provides, otherwise
/// use the type name.
/// </summary>
/// <typeparam name="TCustom"></typeparam>
/// <returns></returns>
internal static (List<string> List, string Flat) GetContentTypeNamesList<TCustom>() where TCustom : ICanWrapData
=> ContentTypeNamesList.Get<TCustom, ModelSourceAttribute>(a => UseSpecifiedNameOrDeriveFromType<TCustom>(a?.ContentTypes));
private static readonly ClassAttributeLookup<(List<string> List, string Flat)> ContentTypeNamesList = new();

/// <summary>
/// Get the stream names of the current type.
/// </summary>
/// <typeparam name="TCustom"></typeparam>
/// <returns></returns>
internal static string GetStreamName<TCustom>() where TCustom : ICanWrapData =>
StreamNames.Get<TCustom, ModelSourceAttribute>(a =>
// if we have the attribute, use that
a?.Streams.Split(',').First().Trim()
// If no attribute, use name of type
?? typeof(TCustom).Name);
private static readonly ClassAttributeLookup<string> StreamNames = new();
internal static (List<string> List, string Flat) GetStreamNameList<TCustom>() where TCustom : ICanWrapData
=> StreamNames.Get<TCustom, ModelSourceAttribute>(attribute => UseSpecifiedNameOrDeriveFromType<TCustom>(attribute?.Streams));

private static (List<string> List, string Flat) UseSpecifiedNameOrDeriveFromType<TCustom>(string names)
where TCustom : ICanWrapData
{
var list = !string.IsNullOrWhiteSpace(names)
? names.Split(',').Select(n => n.Trim()).ToList()
: CreateListOfNameVariants(typeof(TCustom).Name, typeof(TCustom).IsInterface);
return (list, string.Join(",", list));
}

private static readonly ClassAttributeLookup<(List<string> List, string Flat)> StreamNames = new();

/// <summary>
/// Take a class/interface name and create a list
/// which also checks for the same name without leading "I" or without trailing "Model".
/// </summary>
private static List<string> CreateListOfNameVariants(string name, bool isInterface)
{
// Start list with initial name
List<string> result = [name];
// Check if it ends with Model
var nameWithoutModelSuffix = name.EndsWith("Model")
? name.Substring(0, name.Length - 5)
: null;
if (nameWithoutModelSuffix != null)
result.Add(nameWithoutModelSuffix);

if (isInterface && name.Length > 1 && name.StartsWith("I") && char.IsUpper(name, 1))
{
result.Add(name.Substring(1));
if (nameWithoutModelSuffix != null)
result.Add(nameWithoutModelSuffix.Substring(1));
}

return result;
}


internal static Type GetTargetType<TCustom>()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
/// </example>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = false)]
[InternalApi_DoNotUse_MayChangeWithoutNotice]
public class ModelCreationAttribute: Attribute
public sealed class ModelCreationAttribute: Attribute
{
/// <summary>
/// The type to use when creating a model of this interface.
Expand Down
Loading

0 comments on commit 3a3f459

Please sign in to comment.