Files
speckle-sharp-connectors/Sdk/Speckle.Converters.Common/ToHost/ConverterWithFallback.cs
T
Dimitrie Stefanescu e855f92c35 chore: cleanup
2024-12-19 18:06:15 +00:00

89 lines
3.9 KiB
C#

using System.Collections;
using Microsoft.Extensions.Logging;
using Speckle.Sdk.Common.Exceptions;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Extensions;
namespace Speckle.Converters.Common.ToHost;
// POC: CNX-9394 Find a better home for this outside `DependencyInjection` project
/// <summary>
/// <inheritdoc cref="ConverterWithoutFallback"/>
/// <br/>
/// If no suitable converter conversion is found, and the target <see cref="Base"/> object has a displayValue property
/// a converter with strong name of <see cref="ConverterWithoutFallback"/> is resolved for.
/// </summary>
/// <seealso cref="DisplayableObject"/>
public sealed class ConverterWithFallback : IRootToHostConverter
{
private readonly ILogger<ConverterWithFallback> _logger;
private readonly ConverterWithoutFallback _baseConverter;
public ConverterWithFallback(ConverterWithoutFallback baseConverter, ILogger<ConverterWithFallback> logger)
{
_logger = logger;
_baseConverter = baseConverter;
}
/// <summary>
/// Converts a <see cref="Base"/> instance to a host object.
/// </summary>
/// <param name="target">The <see cref="Base"/> instance to convert.</param>
/// <returns>The converted host object.
/// Fallbacks to display value if a direct conversion is not possible.</returns>
/// <remarks>
/// The conversion is done in the following order of preference:
/// 1. Direct conversion using the <see cref="ConverterWithoutFallback"/>.
/// 2. Fallback to display value using the <see cref="Speckle.Sdk.Models.Extensions.BaseExtensions.TryGetDisplayValue{T}"/> method, if a direct conversion is not possible.
///
/// If the direct conversion is not available and there is no displayValue, a <see cref="System.NotSupportedException"/> is thrown.
/// </remarks>
/// <exception cref="System.NotSupportedException">Thrown when no conversion is found for <paramref name="target"/>.</exception>
public object Convert(Base target)
{
Type type = target.GetType();
try
{
return _baseConverter.Convert(target);
}
catch (ConversionNotSupportedException e)
{
// NOTE: `ConversionNotSupportedException` should not be an exception - we know what we can convert and what we can't; this is doubly true
// on hot paths where we *know* that we will fallback to a display value anyway.
_logger.LogInformation(e, "Attempt to find conversion for type {type} failed", type);
}
// NOTE: this section becomes a bit obsolete now, as we're implementing direct converters for data objects.
// Fallback to display value if it exists.
var displayValue = target.TryGetDisplayValue<Base>();
if (displayValue == null || (displayValue is IList && !displayValue.Any()))
{
// TODO: I'm not sure if this should be a ConversionNotSupported instead, but it kinda mixes support + validation so I went for normal conversion exception
throw new ConversionException(
$"No direct conversion found for type {type} and it's fallback display value was null/empty"
);
}
return FallbackToDisplayValue(displayValue); // 1 - many mapping
}
private object FallbackToDisplayValue(IReadOnlyList<Base> displayValue)
{
var tempDisplayableObject = new DisplayableObject(displayValue);
var conversionResult = _baseConverter.Convert(tempDisplayableObject);
// if the host app returns a list of objects as the result of the fallback conversion, we zip them together with the original base display value objects that generated them.
if (conversionResult is IEnumerable<object> result)
{
// Further note: this madness can now go away, as the actual direct converter for the host app can control what
// is being returned. In rhino, this map is done in the actual data object converter.
return result.Zip(displayValue, (a, b) => (a, b));
}
// if not, and the host app "merges" together somehow multiple display values into one entity, we return that.
return conversionResult;
}
}