Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support predefined class-, property and enum mappings #2820

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from
4 changes: 2 additions & 2 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ dotnet_naming_symbols.constants.applicable_accessibilities = *
dotnet_naming_symbols.constants.required_modifiers = const

dotnet_naming_symbols.static_readonly.applicable_kinds = field
dotnet_naming_symbols.static_readonly.applicable_accessibilities = *
dotnet_naming_symbols.static_readonly.applicable_accessibilities = public
dotnet_naming_symbols.static_readonly.required_modifiers = readonly, static

# Naming styles
Expand Down Expand Up @@ -242,4 +242,4 @@ dotnet_naming_style._camelcase.capitalization = camel_case
dotnet_naming_style.allupper.required_prefix =
dotnet_naming_style.allupper.required_suffix =
dotnet_naming_style.allupper.word_separator =
dotnet_naming_style.allupper.capitalization = all_upper
dotnet_naming_style.allupper.capitalization = all_upper
4 changes: 2 additions & 2 deletions src/Hl7.Fhir.Base/FhirPath/Expressions/Typecasts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ public static object CastTo(object source, Type to)

public static bool IsNullable(this Type t)
{
if (!t.IsAValueType()) return true; // ref-type
if (!t.IsValueType) return true; // ref-type
if (Nullable.GetUnderlyingType(t) != null) return true; // Nullable<T>
return false; // value-type
}
Expand Down Expand Up @@ -235,4 +235,4 @@ public static string ReadableTypeName(Type t)
}
}

}
}
183 changes: 113 additions & 70 deletions src/Hl7.Fhir.Base/Introspection/ClassMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,75 @@ namespace Hl7.Fhir.Introspection
/// <summary>
/// A container for the metadata of a FHIR datatype as present on the (generated) .NET POCO class.
/// </summary>
public class ClassMapping : IStructureDefinitionSummary
public class PocoClassMapping : ClassMapping
{
/// <summary>
/// Construct a default mapping for a type by reflecting on the FHIR metadata attributes.
/// </summary>
public PocoClassMapping(string name, Type nativeType, FhirRelease release)
: base(name, nativeType, release)
{
// Nothing
}

/// <summary>
/// Construct a default mapping for a type by reflecting on the FHIR metadata attributes,
/// but the properties are provided lazily by the caller.
/// </summary>
public PocoClassMapping(string name, Type nativeType, FhirRelease release, Func<IEnumerable<PropertyMapping>> propertyMapper)
:base(name, nativeType,release, propertyMapper)
{
// Nothing
}

/// <summary>
/// Construct a default mapping for a type by reflecting on the FHIR metadata attributes, using the
/// properties passed in to the constructor.
/// </summary>
internal PocoClassMapping(string name, Type nativeType, FhirRelease release, IEnumerable<PropertyMapping> propertyMappings)
:base(name, nativeType,release, propertyMappings)
{
// Nothing
}
}


/// <summary>
/// A container for the metadata of a FHIR datatype.
/// </summary>
public abstract class ClassMapping : IStructureDefinitionSummary
{
/// <summary>
/// Construct a default mapping for a type by reflecting on the FHIR metadata attributes.
/// </summary>
internal ClassMapping(string name, Type nativeType, FhirRelease release)
{
Name = name;
NativeType = nativeType;
Release = release;
_propertyMapper = defaultPropertyMapper;
}

/// <summary>
/// Construct a default mapping for a type by reflecting on the FHIR metadata attributes,
/// but the properties are provided lazily by the caller.
/// </summary>
internal ClassMapping(string name, Type nativeType, FhirRelease release, Func<IEnumerable<PropertyMapping>> propertyMapper)
:this(name, nativeType,release)
{
_propertyMapper = propertyMapper;
}

/// <summary>
/// Construct a default mapping for a type by reflecting on the FHIR metadata attributes, using the
/// properties passed in to the constructor.
/// </summary>
internal ClassMapping(string name, Type nativeType, FhirRelease release, IEnumerable<PropertyMapping> propertyMappings)
:this(name, nativeType,release)
{
_propertyMapper = () => propertyMappings;
}

private static readonly ConcurrentDictionary<(Type, FhirRelease), ClassMapping?> _mappedClasses = new();

public static void Clear() => _mappedClasses.Clear();
Expand Down Expand Up @@ -86,33 +153,22 @@ public static bool TryCreate(Type type, [NotNullWhen(true)]out ClassMapping? res
}

// Now continue with the normal algorithm, types adorned with the [FhirTypeAttribute]
if (GetAttribute<FhirTypeAttribute>(type.GetTypeInfo(), release) is not { } typeAttribute) return false;
if (ReflectionHelper.GetAttribute<FhirTypeAttribute>(type.GetTypeInfo(), release) is not { } typeAttribute) return false;

var backboneAttribute = GetAttribute<BackboneTypeAttribute>(type, release);
var backboneAttribute = ReflectionHelper.GetAttribute<BackboneTypeAttribute>(type, release);

result = new ClassMapping(collectTypeName(typeAttribute, type), type, release)
result = new PocoClassMapping(collectTypeName(typeAttribute, type), type, release)
{
IsResource = typeAttribute.IsResource || type.CanBeTreatedAsType(typeof(Resource)),
IsCodeOfT = ReflectionHelper.IsClosedGenericType(type) &&
ReflectionHelper.IsConstructedFromGenericTypeDefinition(type, typeof(Code<>)),
IsFhirPrimitive = typeof(PrimitiveType).IsAssignableFrom(type),
IsBackboneType = typeAttribute.IsNestedType || backboneAttribute is not null,
DefinitionPath = backboneAttribute?.DefinitionPath,
IsBindable = GetAttribute<BindableAttribute>(type.GetTypeInfo(), release)?.IsBindable ?? false,
IsBindable = ReflectionHelper.GetAttribute<BindableAttribute>(type.GetTypeInfo(), release)?.IsBindable ?? false,
Canonical = typeAttribute.Canonical,
ValidationAttributes = GetAttributes<ValidationAttribute>(type.GetTypeInfo(), release).ToArray(),
ValidationAttributes = ReflectionHelper.GetAttributes<ValidationAttribute>(type.GetTypeInfo(), release).ToArray(),
};

return true;
}

private ClassMapping(string name, Type nativeType, FhirRelease release)
{
Name = name;
NativeType = nativeType;
Release = release;
}

/// <summary>
/// The FHIR release which this mapping reflects.
/// </summary>
Expand All @@ -133,86 +189,97 @@ private ClassMapping(string name, Type nativeType, FhirRelease release)
/// <item>.NET primitive types have their <see cref="Type.FullName"/> name prepended with "Net.", e.g. "Net.System.Int32".</item>
/// </list>
/// </remarks>
public string Name { get; private set; }
public string Name { get; }

/// <summary>
/// The .NET class that implements the FHIR datatype/resource
/// </summary>
public Type NativeType { get; private set; }
public Type NativeType { get; }

/// <summary>
/// Is <c>true</c> when this class represents a Resource datatype.
/// </summary>
public bool IsResource { get; private set; } = false;
public bool IsResource => typeof(Resource).IsAssignableFrom(NativeType);

/// <summary>
/// Is <c>true</c> when this class represents a FHIR primitive
/// </summary>
/// <remarks>This is different from a .NET primitive, as FHIR primitives are complex types with a primitive value.</remarks>
public bool IsFhirPrimitive { get; private set; } = false;
public bool IsFhirPrimitive => typeof(PrimitiveType).IsAssignableFrom(NativeType);

/// <summary>
/// The element is of an atomic .NET type, not a FHIR generated POCO.
/// </summary>
public bool IsPrimitive { get; private set; } = false;
public bool IsPrimitive { get; init; } = false;

/// <summary>
/// Is <c>true</c> when this class represents a code with a required binding.
/// </summary>
/// <remarks>See <see cref="Name"></see>.</remarks>
public bool IsCodeOfT { get; private set; } = false;
public bool IsCodeOfT =>
NativeType is { IsGenericType: true, ContainsGenericParameters: false } &&
NativeType.GetGenericTypeDefinition() == typeof(Code<>);

/// <summary>
/// Indicates whether this class represents the nested complex type for a backbone element.
/// </summary>
[Obsolete("These types are now generally called Backbone types, so use IsBackboneType instead.")]
public bool IsNestedType { get => IsBackboneType; set => IsBackboneType = value; }
public bool IsNestedType { get => IsBackboneType; set => _isBackboneType = value; }

/// <summary>
/// Indicates whether this class represents the nested complex type for a backbone element.
/// </summary>
public bool IsBackboneType { get; private set; } = false;
public bool IsBackboneType { get => _isBackboneType; init => _isBackboneType = value; }

private bool _isBackboneType;

/// <summary>
/// If this is a backbone type (<see cref="IsBackboneType"/>), then this contains the path
/// in the StructureDefinition where the backbone was defined first.
/// </summary>
public string? DefinitionPath { get; private set; }
public string? DefinitionPath { get; init; }

/// <summary>
/// Indicates whether this class can be used for binding.
/// </summary>
public bool IsBindable { get; private set; }
public bool IsBindable { get; init; } = false;

/// <summary>
/// The canonical for the StructureDefinition defining this type
/// </summary>
/// <remarks>Will be null for backbone types.</remarks>
public string? Canonical { get; private set; }
public string? Canonical { get; init; }

/// <summary>
/// The collection of zero or more <see cref="ValidationAttribute"/> (or subclasses) declared
/// on this class.
/// </summary>
public ValidationAttribute[] ValidationAttributes { get; init; } = [];

// This list is created lazily. This not only improves initial startup time of
// applications but also ensures circular references between types will not cause loops.
private PropertyMappingCollection? _mappings;
private PropertyMappingCollection propertyMappings
private readonly Func<IEnumerable<PropertyMapping>> _propertyMapper;

private PropertyMappingCollection AllPropertyMappings =>
LazyInitializer.EnsureInitialized(ref _mappings,
() => new PropertyMappingCollection(this, _propertyMapper()))!;

// Enumerate this class' properties using reflection and create PropertyMappings.
// Is used when no external mapping has been passed to the constructor.
private IEnumerable<PropertyMapping> defaultPropertyMapper()
{
get
foreach (var property in ReflectionHelper.FindPublicProperties(NativeType))
{
LazyInitializer.EnsureInitialized(ref _mappings, inspectProperties);
return _mappings!;
if (!PropertyMapping.TryCreate(property, out var propMapping, this)) continue;
yield return propMapping!;
}
}

/// <summary>
/// List of PropertyMappings for this class, in the order of listing in the FHIR specification.
/// </summary>
public IReadOnlyList<PropertyMapping> PropertyMappings => propertyMappings.ByOrder;

/// <summary>
/// The collection of zero or more <see cref="ValidationAttribute"/> (or subclasses) declared
/// on this class.
/// </summary>
public ValidationAttribute[] ValidationAttributes { get; private set; } = Array.Empty<ValidationAttribute>();
public IReadOnlyList<PropertyMapping> PropertyMappings => AllPropertyMappings.ByOrder;

/// <summary>
/// Holds a reference to a property that represents the value of a FHIR Primitive. This
Expand All @@ -236,7 +303,7 @@ private PropertyMappingCollection propertyMappings
/// </summary>
public PropertyMapping? FindMappedElementByName(string name) =>
name != null
? propertyMappings.ByName.TryGetValue(name, out var mapping) ? mapping : null
? AllPropertyMappings.ByName.GetValueOrDefault(name)
: throw Error.ArgumentNull(nameof(name));

/// <summary>
Expand All @@ -255,7 +322,7 @@ private PropertyMappingCollection propertyMappings
if (FindMappedElementByName(name) is { } pm) return pm;

// Now, check the choice elements for a match.
var matches = propertyMappings.ChoiceProperties
var matches = AllPropertyMappings.ChoiceProperties
.Where(m => name.StartsWith(m.Name)).ToList();

// Loop through possible matches and return the longest match.
Expand All @@ -271,15 +338,6 @@ private PropertyMappingCollection propertyMappings
}
}

internal static T? GetAttribute<T>(MemberInfo t, FhirRelease version) where T : Attribute => GetAttributes<T>(t, version).LastOrDefault();

internal static IEnumerable<T> GetAttributes<T>(MemberInfo t, FhirRelease version) where T : Attribute
{
return ReflectionHelper.GetAttributes<T>(t).Where(isRelevant);

bool isRelevant(Attribute a) => a is not IFhirVersionDependent vd || a.AppliesToRelease(version);
}

#region IStructureDefinitionSummary members
/// <inheritdoc />
string IStructureDefinitionSummary.TypeName =>
Expand Down Expand Up @@ -320,21 +378,6 @@ IReadOnlyCollection<IElementDefinitionSummary> IStructureDefinitionSummary.GetEl

private Func<IList>? _listFactory;

// Enumerate this class' properties using reflection, create PropertyMappings
// for them and add them to the PropertyMappings.
private PropertyMappingCollection inspectProperties()
{
return new PropertyMappingCollection(map());

IEnumerable<PropertyMapping> map()
{
foreach (var property in ReflectionHelper.FindPublicProperties(NativeType))
{
if (!PropertyMapping.TryCreate(property, out var propMapping, this, Release)) continue;
yield return propMapping!;
}
}
}

private static string collectTypeName(FhirTypeAttribute attr, Type type)
{
Expand All @@ -352,22 +395,22 @@ private static string collectTypeName(FhirTypeAttribute attr, Type type)

// This is the list of .NET "primitive" types that can be used in the generated POCOs and that we
// can generate ClassMappings for.
internal static Type[] SupportedDotNetPrimitiveTypes = new[]
{
internal static Type[] SupportedDotNetPrimitiveTypes =
[
typeof(int), typeof(uint), typeof(long), typeof(ulong),
typeof(float), typeof(double), typeof(decimal),
typeof(string),
typeof(bool),
typeof(DateTimeOffset),
typeof(byte[]),
typeof(Enum)
};
];

private static ClassMapping buildCqlClassMapping(Type t, FhirRelease release) =>
new("System." + t.Name, t, release);
new PocoClassMapping("System." + t.Name, t, release);

private static ClassMapping buildNetPrimitiveClassMapping(Type t, FhirRelease release) =>
new("Net." + t.FullName, t, release) { IsPrimitive = true };
new PocoClassMapping("Net." + t.FullName, t, release) { IsPrimitive = true };
}
}

Expand Down
Loading