diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index db1ca69c5..d262148b9 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -3,9 +3,7 @@ // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // -using System.Collections; using System.Diagnostics.CodeAnalysis; -using System.Text; using Hl7.Fhir.Model; using Hl7.Fhir.Utility; using Microsoft.Health.Fhir.CodeGen.FhirExtensions; @@ -2968,7 +2966,7 @@ private void WriteElements( orderOffset); } } - private void BuildFhirElementAttribute(string name, string summary, string? isModifier, ElementDefinition element, int orderOffset, string choice, string fiveWs, string? since = null, (string, string)? until = null, string? xmlSerialization = null) + private void WriteFhirElementAttribute(string name, string summary, string? isModifier, ElementDefinition element, int orderOffset, string choice, string fiveWs, string? since = null, (string, string)? until = null, string? xmlSerialization = null) { var xmlser = xmlSerialization is null ? null : $", XmlSerialization = XmlRepresentation.{xmlSerialization}"; string attributeText = $"[FhirElement(\"{name}\"{xmlser}{summary}{isModifier}, Order={GetOrder(element)}{choice}{fiveWs}"; @@ -3046,18 +3044,18 @@ private void WriteElement( if (path == "OperationOutcome.issue.severity") { - BuildFhirElementAttribute(name, summary, ", IsModifier=true", element, orderOffset, choice, fiveWs); - BuildFhirElementAttribute(name, summary, null, element, orderOffset, choice, fiveWs, since: "R4"); + WriteFhirElementAttribute(name, summary, ", IsModifier=true", element, orderOffset, choice, fiveWs); + WriteFhirElementAttribute(name, summary, null, element, orderOffset, choice, fiveWs, since: "R4"); } else if (path is "Signature.who" or "Signature.onBehalfOf") { - BuildFhirElementAttribute(name, summary, isModifier, element, orderOffset, ", Choice = ChoiceType.DatatypeChoice", fiveWs); - BuildFhirElementAttribute(name, summary, isModifier, element, orderOffset, "", fiveWs, since: since); + WriteFhirElementAttribute(name, summary, isModifier, element, orderOffset, ", Choice = ChoiceType.DatatypeChoice", fiveWs); + WriteFhirElementAttribute(name, summary, isModifier, element, orderOffset, "", fiveWs, since: since); _writer.WriteLineIndented($"[DeclaredType(Type = typeof(ResourceReference), Since = FhirRelease.R4)]"); } else { - BuildFhirElementAttribute(name, summary, isModifier, element, orderOffset, choice, fiveWs, since, until, xmlSerialization); + WriteFhirElementAttribute(name, summary, isModifier, element, orderOffset, choice, fiveWs, since, until, xmlSerialization); } if (ei.PropertyType is CqlTypeReference ctr) @@ -3077,6 +3075,22 @@ private void WriteElement( if (_elementTypeChanges.TryGetValue(path, out ElementTypeChange[]? changes)) { + IEnumerable? ats = changes.Select(c => c.DeclaredTypeReference); + _writer.WriteLineIndented("[CLSCompliant(false)]"); + _writer.WriteLineIndented(BuildAllowedTypesAttribute(ats, null)); + + // Write comments for future improved AllowedTypesAttribute, with a Since + _writer.WriteIndentedComment( + "Attribute validation is not sensitive to FHIR version, so the next, more precise validations, will not work yet.", + isSummary: false, singleLine: true); + foreach(ElementTypeChange change in changes) + { + string allowedType = BuildAllowedTypesAttribute([change.DeclaredTypeReference], change.Since); + _writer.WriteIndentedComment(allowedType, isSummary: false, singleLine: true); + } + + // Write the DeclaredTypes with the since, that will at least make sure + // the metadata for the property is correct for each version. foreach(ElementTypeChange change in changes) { _writer.WriteIndented($"[DeclaredType(Type = typeof({change.DeclaredTypeReference.PropertyTypeString})"); @@ -3085,9 +3099,6 @@ private void WriteElement( } } - //if (element.cgIsSimple() && element.Type.Count == 1 && element.Type.Single().cgName() == "uri") - // _writer.WriteLineIndented("[UriPattern]"); - bool notClsCompliant = !string.IsNullOrEmpty(allowedTypes) || !string.IsNullOrEmpty(resourceReferences); @@ -3160,7 +3171,7 @@ private void WriteElement( return $"{baseDescription}. {changedDescription}"; } - private static PrimitiveTypeReference BuildTypeReferenceForCode(DefinitionCollection info, ElementDefinition element, Dictionary writtenValueSets) + private static (string? enumName, string? enumClass) GetVsInfoForCodedElement(DefinitionCollection info, ElementDefinition element, Dictionary writtenValueSets) { if ((element.Binding?.Strength != Hl7.Fhir.Model.BindingStrength.Required) || (!info.TryExpandVs(element.Binding.ValueSet, out ValueSet? vs)) || @@ -3168,7 +3179,7 @@ private static PrimitiveTypeReference BuildTypeReferenceForCode(DefinitionCollec (_codedElementOverrides.Contains(element.Path) && info.FhirSequence >= FhirReleases.FhirSequenceCodes.R4) || !writtenValueSets.TryGetValue(vs.Url, out WrittenValueSetInfo vsInfo)) { - return PrimitiveTypeReference.GetTypeReference("code"); + return (null, null); } string vsClass = vsInfo.ClassName; @@ -3176,7 +3187,7 @@ private static PrimitiveTypeReference BuildTypeReferenceForCode(DefinitionCollec if (string.IsNullOrEmpty(vsClass)) { - return new CodedTypeReference(vsName, null); + return (vsName, null); } string pascal = element.cgName().ToPascalCase(); @@ -3187,7 +3198,7 @@ private static PrimitiveTypeReference BuildTypeReferenceForCode(DefinitionCollec $"Change the name of the valueset '{vs.Url}' by adapting the _enumNamesOverride variable in the generator and rerun."); } - return new CodedTypeReference(vsName, vsClass); + return (vsName, vsClass); } private static TypeReference DetermineTypeReferenceForFhirElement( @@ -3216,21 +3227,12 @@ TypeReference determineTypeReferenceForFhirElementName() string initialTypeName = getTypeNameFromElement(); - // Elements that use multiple datatypes are of type DataType - // TODO: Probably need the list of types later to be able to render the - // AllowedTypes. - if (initialTypeName == "DataType") - return new ChoiceTypeReference(); - // Elements of type Code or Code have their own naming/types, so handle those separately. - if (initialTypeName == "code") - return BuildTypeReferenceForCode(info, element, writtenValueSets); + var (vsName,vsClass) = initialTypeName == "code" + ? GetVsInfoForCodedElement(info, element, writtenValueSets) + : (null,null); - if (PrimitiveTypeReference.IsFhirPrimitiveType(initialTypeName)) - return PrimitiveTypeReference.GetTypeReference(initialTypeName); - - // Otherwise, this is a "normal" name for a complex type. - return new ComplexTypeReference(initialTypeName, getPocoNameForComplexTypeReference(initialTypeName)); + return TypeReference.BuildFromFhirTypeName(initialTypeName, vsName, vsClass); string getTypeNameFromElement() { @@ -3239,7 +3241,7 @@ string getTypeNameFromElement() { // TODO(ginoc): this should move into cgBaseTypeName(); // check to see if the referenced element has an explicit name - if (info.TryFindElementByPath(btn, out StructureDefinition? targetSd, out ElementDefinition? targetEd)) + if (info.TryFindElementByPath(btn, out StructureDefinition? _, out ElementDefinition? targetEd)) { return BuildTypeNameForNestedComplexType(targetEd, btn); } @@ -3251,13 +3253,6 @@ string getTypeNameFromElement() ? element.Type.First().cgName() : "DataType"; } - - string getPocoNameForComplexTypeReference(string name) - { - return name.Contains('.') - ? BuildTypeNameForNestedComplexType(element, name) - : TypeReference.MapTypeName(name); - } } } @@ -3463,22 +3458,6 @@ private static string MostGeneralValueAccessorType(PrimitiveTypeReference ptr) /// A string. private static string BuildTypeNameForNestedComplexType(ElementDefinition ed, string type) { - // ginoc 2024.03.12: Release has happened and these are no longer needed - leaving here but commented out until confirmed - /* - // TODO: the following renames (repairs) should be removed when release 4B is official and there is an - // explicit name in the definition for attributes: - // - Statistic.attributeEstimate.attributeEstimate - // - Citation.contributorship.summary - - if (type.StartsWith("Citation") || type.StartsWith("Statistic") || type.StartsWith("DeviceDefinition")) - { - string parentName = type.Substring(0, type.IndexOf('.')); - var sillyBackboneName = type.Substring(parentName.Length); - type = parentName + "." + capitalizeThoseSillyBackboneNames(sillyBackboneName) + "Component"; - } - // end of repair - */ - string explicitTypeName = ed.cgExplicitName(); if (!string.IsNullOrEmpty(explicitTypeName)) @@ -3602,7 +3581,7 @@ internal static void BuildElementOptionalFlags( // present in the current version of the standard. So, in principle, we don't generate // this attribute in the base subset, unless all types mentioned are present in the // exception list above. - bool isPrimitive(string name) => char.IsLower(name[0]); + static bool isPrimitive(string name) => char.IsLower(name[0]); bool allTypesAvailable = elementTypes.Keys.All(en => isPrimitive(en) // primitives are available everywhere @@ -3613,44 +3592,15 @@ internal static void BuildElementOptionalFlags( if (allTypesAvailable) { - StringBuilder sb = new(); - sb.Append("[AllowedTypes("); - - bool needsSep = false; - foreach ((string etName, ElementDefinition.TypeRefComponent _) in elementTypes) - { - if (needsSep) - { - sb.Append(','); - } - - needsSep = true; - - sb.Append("typeof("); - sb.Append(Namespace); - sb.Append('.'); - - if (TypeNameMappings.TryGetValue(etName, out string? tmValue)) - { - sb.Append(tmValue); - } - else - { - sb.Append(FhirSanitizationUtils.SanitizedToConvention(etName, NamingConvention.PascalCase)); - } - - sb.Append(')'); - } - - sb.Append(")]"); - allowedTypes = sb.ToString(); + IEnumerable typeRefs = elementTypes.Values.Select(v => TypeReference.BuildFromFhirTypeName(v.Code)); + allowedTypes = BuildAllowedTypesAttribute(typeRefs, null); } } } if (elementTypes.Any()) { - foreach ((string etName, ElementDefinition.TypeRefComponent elementType) in elementTypes.Where(kvp => (kvp.Key == "Reference") && kvp.Value.TargetProfile.Any())) + foreach ((string _, ElementDefinition.TypeRefComponent elementType) in elementTypes.Where(kvp => (kvp.Key == "Reference") && kvp.Value.TargetProfile.Any())) { resourceReferences = "[References(" + string.Join(",", elementType.cgTargetProfiles().Keys.Select(name => "\"" + name + "\"")) + diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirelyCommon.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirelyCommon.cs index e1a9b4169..e967e2a4d 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirelyCommon.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirelyCommon.cs @@ -4,8 +4,12 @@ // using System.ComponentModel; +using System.Text; using Hl7.Fhir.Model; using Microsoft.Health.Fhir.CodeGen.FhirExtensions; +using Microsoft.Health.Fhir.CodeGenCommon.Extensions; +using Microsoft.Health.Fhir.CodeGenCommon.Packaging; +using Microsoft.Health.Fhir.CodeGenCommon.Utils; #if NETSTANDARD2_0 using Microsoft.Health.Fhir.CodeGenCommon.Polyfill; @@ -251,6 +255,21 @@ public static int GetOrder(int relativeOrder) { return (relativeOrder * 10) + 10; } + + public static string BuildAllowedTypesAttribute(IEnumerable types, FhirReleases.FhirSequenceCodes? since) + { + StringBuilder sb = new(); + sb.Append("[AllowedTypes("); + + string typesList = string.Join(",", + types.Select(t => $"typeof({t.PropertyTypeString})")); + + sb.Append(typesList); + if (since is not null) + sb.Append($", Since = FhirRelease.{since}"); + sb.Append(")]"); + return sb.ToString(); + } } diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/FirelyNetIG.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/FirelyNetIG.cs index 9db94765b..5083edfc8 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/FirelyNetIG.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/FirelyNetIG.cs @@ -3567,7 +3567,7 @@ private ExtensionData GetExtensionData( Expression = "DataType", }, ContextTarget = null, - ContextElementInfo = new("", "", "", new ChoiceTypeReference(), null), + ContextElementInfo = new("", "", "", ComplexTypeReference.DataTypeReference, null), //ContextElementInfo = new CSharpFirely2.WrittenElementInfo() //{ // ElementType = "Hl7.Fhir.Model.DataType", diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/TypeReference.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/TypeReference.cs index bc36057f7..cf8360a8b 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/TypeReference.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/TypeReference.cs @@ -5,6 +5,19 @@ namespace Microsoft.Health.Fhir.CodeGen.Language.Firely; public abstract record TypeReference(string Name) { + public static TypeReference BuildFromFhirTypeName(string name, string? vsName=null, string? vsClass=null) + { + // Elements of type Code or Code have their own naming/types, so handle those separately. + if (name == "code" && vsName is not null) + return new CodedTypeReference(vsName, vsClass); + + if (PrimitiveTypeReference.IsFhirPrimitiveType(name)) + return PrimitiveTypeReference.GetTypeReference(name); + + // Otherwise, this is a "normal" name for a complex type. + return new ComplexTypeReference(name, MapTypeName(name)); + } + public abstract string PropertyTypeString { get; } internal static string MapTypeName(string name) @@ -103,8 +116,6 @@ public ComplexTypeReference(string name) : this(name, name) { } public static readonly ComplexTypeReference DataTypeReference = new("DataType"); } -public record ChoiceTypeReference() : ComplexTypeReference("DataType"); - public record CodedTypeReference(string EnumName, string? EnumClassName) : PrimitiveTypeReference("code", EnumName, typeof(Enum)) {