From da95bf53a364983e42760b3f857845e25753478e Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Wed, 16 Oct 2024 17:10:15 +0200 Subject: [PATCH 01/52] Added support for IDictionary (SetValue) --- .../Language/Firely/CSharpFirely2.cs | 121 +++++++++++++----- .../Microsoft.Health.Fhir.CodeGen.csproj | 4 +- ...Microsoft.Health.Fhir.CodeGenCommon.csproj | 2 +- .../Microsoft.Health.Fhir.CrossVersion.csproj | 2 +- ...crosoft.Health.Fhir.MappingLanguage.csproj | 2 +- src/fhir-codegen/fhir-codegen.csproj | 2 +- 6 files changed, 94 insertions(+), 39 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index dd4d280db..c96fd9b06 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -1294,16 +1294,6 @@ private void WriteInterface( GenSubset subset) { string exportName = "I" + complex.Name.ToPascalCase(); - - //writtenModels.Add( - // complex.Name, - // new WrittenModelInfo() - // { - // FhirName = complex.Name, - // CsName = $"{Namespace}.{exportName}", - // IsAbstract = complex.Abstract == true, - // }); - string filename = Path.Combine(_exportDirectory, "Generated", $"{exportName}.cs"); _modelWriter.WriteLineIndented($"// {exportName}.cs"); @@ -1954,7 +1944,8 @@ private void WriteComponent( // open class OpenScope(); - WritePropertyTypeName(complex.cgName()); + if(complex.Structure.Abstract != true) + WritePropertyTypeName(complex.cgName()); string validationRegEx = complex.cgValidationRegEx(); if (!string.IsNullOrEmpty(validationRegEx)) @@ -2093,23 +2084,21 @@ private string DetermineExportedBaseTypeName(string baseTypeName) return baseTypeName; } - private void WriteIDictionarySupport(string exportName, IEnumerable exportedElements) + private void WriteIDictionarySupport(string exportName, List exportedElements) { WriteDictionaryTryGetValue(exportName, exportedElements); + WriteDictionaryTrySetValue(exportName, exportedElements); WriteDictionaryPairs(exportName, exportedElements); } - private string NullCheck(string propertyName, bool isList) => propertyName + (isList ? "?.Any() == true" : " is not null"); - private void WriteDictionaryPairs(string exportName, IEnumerable exportedElements) + private void WriteDictionaryPairs(string exportName, List exportedElements) { - // Base implementation differs from subclasses. + // Base implementation differs from subclasses and is hand-written code in a separate partical class if (exportName == "Base") { - _writer.WriteLineIndented("protected virtual IEnumerable> GetElementPairs() => Enumerable.Empty>();"); - _writer.WriteLine(string.Empty); return; } @@ -2132,18 +2121,13 @@ private void WriteDictionaryPairs(string exportName, IEnumerable exportedElements) + private void WriteDictionaryTryGetValue(string exportName, List exportedElements) { - // Base implementation differs from subclasses. - if (exportName == "Base") - { - _writer.WriteLineIndented("protected virtual bool TryGetValue(string key, out object value)"); - OpenScope(); - _writer.WriteLineIndented("value = default;"); - _writer.WriteLineIndented("return false;"); - CloseScope(); - return; - } + // Base implementation differs from subclasses and is hand-written code in a separate partical class + if (exportName == "Base") + { + return; + } // Don't override anything if there are no additional elements. if (!exportedElements.Any()) @@ -2188,6 +2172,74 @@ void writeCase(string key, string propName, bool isList) void writeBaseTryGetValue() => _writer.WriteLineIndented("return base.TryGetValue(key, out value);"); } + + private void WriteDictionaryTrySetValue(string exportName, List exportedElements) + { + // Base implementation differs from subclasses and is hand-written code in a separate partical class + if (exportName == "Base") + { + return; + } + + // Don't override anything if there are no additional elements. + if (!exportedElements.Any()) + { + return; + } + + _writer.WriteLineIndented("protected override Base SetValue(string key, object value)"); + OpenScope(); + + // switch + _writer.WriteLineIndented("switch (key)"); + OpenScope(); + + foreach (WrittenElementInfo info in exportedElements) + { + writeSetValueCase(info.FhirElementName, null, + $"{info.PropertyName} = ({info.PropertyType.PropertyTypeString})value;"); + + // if (info.PropertyType is ListTypeReference ltr) + // { + // writeSetValueCase(info.FhirElementName, $"value is IEnumerable<{ltr.Element.PropertyTypeString}> v", + // $"{info.PropertyName} = new {info.PropertyType.PropertyTypeString}(v);"); + // } + // else + // { + // writeSetValueCase(info.FhirElementName, $"value is {info.PropertyType.PropertyTypeString} v", + // $"{info.PropertyName} = v;"); + // } + // + // writeSetValueCase(info.FhirElementName, "value is null", + // $"{info.PropertyName} = null;"); + } + + void writeSetValueCase(string name, string? when, string statement) + { + _writer.WriteLineIndented(when is not null ? $"case \"{name}\" when {when}:" : $"case \"{name}\":"); + + _writer.IncreaseIndent(); + + _writer.WriteLineIndented(statement); + //_writer.WriteLineIndented($"return true;"); + _writer.WriteLineIndented($"return this;"); + _writer.DecreaseIndent(); + } + + _writer.WriteLineIndented("default:"); + _writer.IncreaseIndent(); + writeBaseTrySetValue(); + + _writer.DecreaseIndent(); + + // end switch + CloseScope(includeSemicolon: false); + + CloseScope(); + + void writeBaseTrySetValue() => _writer.WriteLineIndented("return base.SetValue(key, value);"); + } + /// Writes the children of this item. /// Name of the exported class. /// The exported elements. @@ -2523,7 +2575,8 @@ private void WriteConstrainedQuantity( // open class OpenScope(); - WritePropertyTypeName(complex.Structure.Name); + if(complex.Structure.Abstract != true) + WritePropertyTypeName(complex.Structure.Name); _writer.WriteLineIndented("public override IDeepCopyable DeepCopy()"); OpenScope(); @@ -2612,7 +2665,8 @@ private void WriteBackboneComponent( // open class OpenScope(); - WritePropertyTypeName(componentName); + if(complex.Structure.Abstract != true) + WritePropertyTypeName(componentName); WriteElements(complex, exportName, ref exportedElements, subset); @@ -3649,8 +3703,8 @@ internal static void BuildElementOptionalFlags( private void WritePropertyTypeName(string name) { WriteIndentedComment("FHIR Type Name"); - var specifier = name == "Base" ? "virtual" : "override"; - _writer.WriteLineIndented($"public {specifier} string TypeName {{ get {{ return \"{name}\"; }} }}"); + + _writer.WriteLineIndented($"public override string TypeName {{ get {{ return \"{name}\"; }} }}"); _writer.WriteLine(string.Empty); } @@ -3764,7 +3818,8 @@ private void WritePrimitiveType( // open class OpenScope(); - WritePropertyTypeName(primitive.Name); + if(primitive.Abstract != true) + WritePropertyTypeName(primitive.Name); if (!string.IsNullOrEmpty(primitive.cgpValidationRegEx())) { diff --git a/src/Microsoft.Health.Fhir.CodeGen/Microsoft.Health.Fhir.CodeGen.csproj b/src/Microsoft.Health.Fhir.CodeGen/Microsoft.Health.Fhir.CodeGen.csproj index cef55dd98..f5018a55c 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Microsoft.Health.Fhir.CodeGen.csproj +++ b/src/Microsoft.Health.Fhir.CodeGen/Microsoft.Health.Fhir.CodeGen.csproj @@ -33,7 +33,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -51,5 +51,5 @@ - + diff --git a/src/Microsoft.Health.Fhir.CodeGenCommon/Microsoft.Health.Fhir.CodeGenCommon.csproj b/src/Microsoft.Health.Fhir.CodeGenCommon/Microsoft.Health.Fhir.CodeGenCommon.csproj index 2ed73f60f..7fbee7921 100644 --- a/src/Microsoft.Health.Fhir.CodeGenCommon/Microsoft.Health.Fhir.CodeGenCommon.csproj +++ b/src/Microsoft.Health.Fhir.CodeGenCommon/Microsoft.Health.Fhir.CodeGenCommon.csproj @@ -10,7 +10,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/Microsoft.Health.Fhir.CrossVersion/Microsoft.Health.Fhir.CrossVersion.csproj b/src/Microsoft.Health.Fhir.CrossVersion/Microsoft.Health.Fhir.CrossVersion.csproj index 7c434ab8b..735bed944 100644 --- a/src/Microsoft.Health.Fhir.CrossVersion/Microsoft.Health.Fhir.CrossVersion.csproj +++ b/src/Microsoft.Health.Fhir.CrossVersion/Microsoft.Health.Fhir.CrossVersion.csproj @@ -19,7 +19,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/Microsoft.Health.Fhir.MappingLanguage/Microsoft.Health.Fhir.MappingLanguage.csproj b/src/Microsoft.Health.Fhir.MappingLanguage/Microsoft.Health.Fhir.MappingLanguage.csproj index 594c46597..68c146ff8 100644 --- a/src/Microsoft.Health.Fhir.MappingLanguage/Microsoft.Health.Fhir.MappingLanguage.csproj +++ b/src/Microsoft.Health.Fhir.MappingLanguage/Microsoft.Health.Fhir.MappingLanguage.csproj @@ -16,7 +16,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/fhir-codegen/fhir-codegen.csproj b/src/fhir-codegen/fhir-codegen.csproj index 270a5021f..574351448 100644 --- a/src/fhir-codegen/fhir-codegen.csproj +++ b/src/fhir-codegen/fhir-codegen.csproj @@ -37,7 +37,7 @@ - + From 89786fb36d76a8c4f82727ea113266f01bf7a53e Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Fri, 18 Oct 2024 17:13:34 +0200 Subject: [PATCH 02/52] Removed generation of nested/resource args to fhirtype. --- .../Language/Firely/CSharpFirely2.cs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index c96fd9b06..cd2de2d07 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -1535,8 +1535,6 @@ private void WriteInterfaceComponent( string fhirTypeConstructor = $"\"{complexName}\",\"{complex.cgUrl()}\""; - //_writer.WriteLineIndented($"[FhirType({fhirTypeConstructor}, IsResource=true)]"); - StructureDefinition? parentInterface = _info.GetParentInterface(complex.Structure); if (parentInterface == null) @@ -1868,15 +1866,7 @@ private void WriteComponent( WriteSerializable(); string fhirTypeConstructor = $"\"{complexName}\",\"{complex.cgUrl()}\""; - - if (isResource) - { - _writer.WriteLineIndented($"[FhirType({fhirTypeConstructor}, IsResource=true)]"); - } - else - { - _writer.WriteLineIndented($"[FhirType({fhirTypeConstructor})]"); - } + _writer.WriteLineIndented($"[FhirType({fhirTypeConstructor})]"); var isPatientClass = false; @@ -2653,7 +2643,7 @@ private void WriteBackboneComponent( string componentName = parentExportName + "#" + explicitNamePart; WriteSerializable(); - _writer.WriteLineIndented($"[FhirType(\"{componentName}\", IsNestedType=true)]"); + _writer.WriteLineIndented($"[FhirType(\"{componentName}\")]"); _writer.WriteLineIndented($"[BackboneType(\"{complex.Element.Path}\")]"); From df9dd967b71dbf0068935f0ffcad2ccc4c028565 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Fri, 18 Oct 2024 17:52:15 +0200 Subject: [PATCH 03/52] Removed BackboneTypeAttribute. --- .../Language/Firely/CSharpFirely2.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index cd2de2d07..cbf52971b 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2636,16 +2636,10 @@ private void WriteBackboneComponent( */ bool useConcatenationInName = complex.Structure.Name == "Citation"; - - string explicitNamePart = string.IsNullOrEmpty(explicitName) - ? complex.cgName(NamingConvention.PascalCase, useConcatenationInName, useConcatenationInName) - : explicitName; - string componentName = parentExportName + "#" + explicitNamePart; + string componentName = complex.Element.Path; WriteSerializable(); - _writer.WriteLineIndented($"[FhirType(\"{componentName}\")]"); - - _writer.WriteLineIndented($"[BackboneType(\"{complex.Element.Path}\")]"); + _writer.WriteLineIndented($"[FhirType(\"{componentName}\", IsBackboneType=true)]"); _writer.WriteLineIndented( $"public partial class" + From 02cb7c15bfbea38f38c92d53369c6ef610cadb09 Mon Sep 17 00:00:00 2001 From: mmsmits Date: Fri, 25 Oct 2024 16:11:01 +0200 Subject: [PATCH 04/52] fix: attachment.url type --- .../Language/Firely/CSharpFirely2.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index dd4d280db..c665a4141 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -4,7 +4,6 @@ // using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Text; using Hl7.Fhir.Model; using Hl7.Fhir.Utility; @@ -3110,6 +3109,10 @@ private void WriteElement( { _writer.WriteLineIndented($"[DeclaredType(Type = typeof(Code), Since = FhirRelease.R5)]"); } + else if (path == "Attachment.url") + { + _writer.WriteLineIndented($"[DeclaredType(Type = typeof(FhirUrl), Since = FhirRelease.R4)]"); + } else if (path == "Attachment.size") { _writer.WriteLineIndented($"[DeclaredType(Type = typeof(UnsignedInt), Since = FhirRelease.STU3)]"); @@ -3240,6 +3243,14 @@ TypeReference determineTypeReferenceForFhirElementName() return PrimitiveTypeReference.GetTypeReference("uri"); } + if (element.Path is "Attachment.url") + { + /* we want to share Attachment across different FHIR versions, + * so we use the "most common" type to the versions, which + * is uri rather than the more specific url. */ + return PrimitiveTypeReference.GetTypeReference("uri"); + } + if (element.Path is "Element.id" or "Extension.url") { /* these two properties formally use a CQL primitive (at least, From 1aa4f03171b3795afba7afe2ebd477bb04c2a241 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Tue, 19 Nov 2024 11:23:36 +0100 Subject: [PATCH 05/52] Tried replacing Element.id, Extension.url with FHIR datatype instead of .net primitive. --- .../Language/Firely/CSharpFirely2.cs | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index f23cb4df0..dacc44b76 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2298,8 +2298,8 @@ private static string NamedChildrenFhirTypeWrapper(WrittenElementInfo info) return info.FhirElementPath switch { "Narrative.div" => $"new FhirString({info.PropertyName}.Value)", - "Element.id" => $"new FhirString({info.PropertyName})", - "Extension.url" => $"new FhirUri({info.PropertyName})", + // "Element.id" => $"new FhirString({info.PropertyName})", + // "Extension.url" => $"new FhirUri({info.PropertyName})", _ => $"{info.PropertyName}" }; } @@ -3114,6 +3114,7 @@ private void WriteElement( }; string? xmlSerialization = path == "Narrative.div" ? "XHtml" : + path is "Extension.url" or "Element.id" ? "XmlAttr" : ei.PropertyType is CqlTypeReference ? "XmlAttr" : null; @@ -3176,8 +3177,8 @@ private void WriteElement( _writer.WriteLineIndented($"[Binding(\"{element.cgBindingName()}\")]"); } - if (element.cgIsSimple() && element.Type.Count == 1 && element.Type.Single().cgName() == "uri") - _writer.WriteLineIndented("[UriPattern]"); + // if (element.cgIsSimple() && element.Type.Count == 1 && element.Type.Single().cgName() == "uri") + // _writer.WriteLineIndented("[UriPattern]"); bool notClsCompliant = !string.IsNullOrEmpty(allowedTypes) || !string.IsNullOrEmpty(resourceReferences); @@ -3289,12 +3290,12 @@ TypeReference determineTypeReferenceForFhirElementName() return PrimitiveTypeReference.GetTypeReference("uri"); } - if (element.Path is "Element.id" or "Extension.url") - { - /* these two properties formally use a CQL primitive (at least, - * that's how they are encoded in the StructureDefinition. */ - return CqlTypeReference.SystemString; - } + // if (element.Path is "Element.id" or "Extension.url") + // { + // /* these two properties formally use a CQL primitive (at least, + // * that's how they are encoded in the StructureDefinition. */ + // return CqlTypeReference.SystemString; + // } var initialTypeName = getTypeNameFromElement(); @@ -3847,7 +3848,29 @@ private void WritePrimitiveType( _writer.WriteLineIndented("[DataMember]"); _writer.WriteLineIndented($"public {typeName} Value"); OpenScope(); - _writer.WriteLineIndented($"get {{ return ({typeName})ObjectValue; }}"); + + // A bit of a hack until we have a proper way to handle the primitives + // in https://github.com/FirelyTeam/firely-net-sdk/issues/2781 + if (typeName == "long?") + { + _writer.WriteLineIndented( + """ + get + { + return ObjectValue switch + { + null => null, + long l => l, + _ => Convert.ToInt64(ObjectValue) + }; + } + """); + } + else + { + _writer.WriteLineIndented($"get {{ return ({typeName})ObjectValue; }}"); + } + _writer.WriteLineIndented("set { ObjectValue = value; OnPropertyChanged(\"Value\"); }"); CloseScope(); From 83e5cd4f875504f8758cdd5a602bef6072b68c88 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Tue, 19 Nov 2024 18:02:11 +0100 Subject: [PATCH 06/52] Stop generating Children/NamedChildren. --- .../Language/Firely/CSharpFirely2.cs | 266 +++++++++--------- 1 file changed, 133 insertions(+), 133 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index f23cb4df0..78a5f88d2 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2013,10 +2013,10 @@ private void WriteComponent( WriteMatches(exportName, exportedElements); WriteIsExactly(exportName, exportedElements); - WriteChildren(exportName, exportedElements); - WriteNamedChildren(exportName, exportedElements); + // WriteChildren(exportName, exportedElements); + // WriteNamedChildren(exportName, exportedElements); - WriteIDictionarySupport(exportName, exportedElements); + WriteDictionarySupport(exportName, exportedElements); // close class CloseScope(); @@ -2073,7 +2073,7 @@ private string DetermineExportedBaseTypeName(string baseTypeName) return baseTypeName; } - private void WriteIDictionarySupport(string exportName, List exportedElements) + private void WriteDictionarySupport(string exportName, List exportedElements) { WriteDictionaryTryGetValue(exportName, exportedElements); WriteDictionaryTrySetValue(exportName, exportedElements); @@ -2096,7 +2096,7 @@ private void WriteDictionaryPairs(string exportName, List ex return; } - _writer.WriteLineIndented("protected override IEnumerable> GetElementPairs()"); + _writer.WriteLineIndented("internal protected override IEnumerable> GetElementPairs()"); OpenScope(); _writer.WriteLineIndented("foreach (var kvp in base.GetElementPairs()) yield return kvp;"); @@ -2112,7 +2112,7 @@ private void WriteDictionaryPairs(string exportName, List ex private void WriteDictionaryTryGetValue(string exportName, List exportedElements) { - // Base implementation differs from subclasses and is hand-written code in a separate partical class + // Base implementation differs from subclasses and is hand-written code in a separate partial class if (exportName == "Base") { return; @@ -2124,7 +2124,7 @@ private void WriteDictionaryTryGetValue(string exportName, ListWrites the children of this item. /// Name of the exported class. /// The exported elements. - private void WriteNamedChildren(string exportName, - List exportedElements) - { - // Base implementation differs from subclasses. - if (exportName == "Base") - { - _writer.WriteIndentedComment(""" - Enumerate all child nodes. - Return a sequence of child elements, components and/or properties. - Child nodes are returned as tuples with the name and the node itself, in the order defined - by the FHIR specification. - First returns child nodes inherited from any base class(es), recursively. - Finally returns child nodes defined by the current class. - """); - _writer.WriteLineIndented("[IgnoreDataMember]"); - _writer.WriteLineIndented("public virtual IEnumerable NamedChildren => Enumerable.Empty();"); - _writer.WriteLine(string.Empty); - return; - } - - // Don't override anything if there are no additional elements. - if (!exportedElements.Any()) - { - return; - } - - _writer.WriteLineIndented("[IgnoreDataMember]"); - _writer.WriteLineIndented("public override IEnumerable NamedChildren"); - - OpenScope(); - _writer.WriteLineIndented("get"); - OpenScope(); - _writer.WriteLineIndented($"foreach (var item in base.NamedChildren) yield return item;"); - - foreach (WrittenElementInfo info in exportedElements) - { - if (info.PropertyType is ListTypeReference) - { - _writer.WriteLineIndented( - $"foreach (var elem in {info.PropertyName})" + - $" {{ if (elem != null)" + - $" yield return new ElementValue(\"{info.FhirElementName}\", elem);" + - $" }}"); - } - else - { - string yr = NamedChildrenFhirTypeWrapper(info); - - _writer.WriteLineIndented( - $"if ({info.PropertyName} != null)" + - $" yield return new ElementValue(\"{info.FhirElementName}\", {yr});"); - } - } - - CloseScope(suppressNewline: true); - CloseScope(); - } + // private void WriteNamedChildren(string exportName, + // List exportedElements) + // { + // // Base implementation differs from subclasses. + // if (exportName == "Base") + // { + // _writer.WriteIndentedComment(""" + // Enumerate all child nodes. + // Return a sequence of child elements, components and/or properties. + // Child nodes are returned as tuples with the name and the node itself, in the order defined + // by the FHIR specification. + // First returns child nodes inherited from any base class(es), recursively. + // Finally returns child nodes defined by the current class. + // """); + // _writer.WriteLineIndented("[IgnoreDataMember]"); + // _writer.WriteLineIndented("public virtual IEnumerable NamedChildren => Enumerable.Empty();"); + // _writer.WriteLine(string.Empty); + // return; + // } + // + // // Don't override anything if there are no additional elements. + // if (!exportedElements.Any()) + // { + // return; + // } + // + // _writer.WriteLineIndented("[IgnoreDataMember]"); + // _writer.WriteLineIndented("public override IEnumerable NamedChildren"); + // + // OpenScope(); + // _writer.WriteLineIndented("get"); + // OpenScope(); + // _writer.WriteLineIndented($"foreach (var item in base.NamedChildren) yield return item;"); + // + // foreach (WrittenElementInfo info in exportedElements) + // { + // if (info.PropertyType is ListTypeReference) + // { + // _writer.WriteLineIndented( + // $"foreach (var elem in {info.PropertyName})" + + // $" {{ if (elem != null)" + + // $" yield return new ElementValue(\"{info.FhirElementName}\", elem);" + + // $" }}"); + // } + // else + // { + // string yr = NamedChildrenFhirTypeWrapper(info); + // + // _writer.WriteLineIndented( + // $"if ({info.PropertyName} != null)" + + // $" yield return new ElementValue(\"{info.FhirElementName}\", {yr});"); + // } + // } + // + // CloseScope(suppressNewline: true); + // CloseScope(); + // } // For a limited set of exceptional elements, the Children functions return a // complex FHIR type wrapper. - private static string NamedChildrenFhirTypeWrapper(WrittenElementInfo info) - { - - return info.FhirElementPath switch - { - "Narrative.div" => $"new FhirString({info.PropertyName}.Value)", - "Element.id" => $"new FhirString({info.PropertyName})", - "Extension.url" => $"new FhirUri({info.PropertyName})", - _ => $"{info.PropertyName}" - }; - } + // private static string NamedChildrenFhirTypeWrapper(WrittenElementInfo info) + // { + // + // return info.FhirElementPath switch + // { + // "Narrative.div" => $"new FhirString({info.PropertyName}.Value)", + // "Element.id" => $"new FhirString({info.PropertyName})", + // "Extension.url" => $"new FhirUri({info.PropertyName})", + // _ => $"{info.PropertyName}" + // }; + // } /// Writes the children of this item. /// Name of the exported class. /// The exported elements. - private void WriteChildren(string exportName, - List exportedElements) - { - // Base implementation differs from subclasses. - if (exportName == "Base") - { - _writer.WriteIndentedComment( - """ - Enumerate all child nodes. - Return a sequence of child elements, components and/or properties. - Child nodes are returned in the order defined by the FHIR specification. - First returns child nodes inherited from any base class(es), recursively. - Finally returns child nodes defined by the current class. - """); - _writer.WriteLineIndented("[IgnoreDataMember]"); - _writer.WriteLineIndented("public virtual IEnumerable Children => Enumerable.Empty();"); - _writer.WriteLine(string.Empty); - return; - } - - // Don't override anything if there are no additional elements. - if (!exportedElements.Any()) - { - return; - } - - _writer.WriteLineIndented("[IgnoreDataMember]"); - _writer.WriteLineIndented("public override IEnumerable Children"); - - OpenScope(); - _writer.WriteLineIndented("get"); - OpenScope(); - _writer.WriteLineIndented($"foreach (var item in base.Children) yield return item;"); - - foreach (WrittenElementInfo info in exportedElements) - { - if (info.PropertyType is ListTypeReference) - { - _writer.WriteLineIndented( - $"foreach (var elem in {info.PropertyName})" + - $" {{ if (elem != null) yield return elem; }}"); - } - else - { - string yr = NamedChildrenFhirTypeWrapper(info); - _writer.WriteLineIndented( - $"if ({info.PropertyName} != null)" + - $" yield return {yr};"); - } - } - - CloseScope(suppressNewline: true); - CloseScope(); - } + // private void WriteChildren(string exportName, + // List exportedElements) + // { + // // Base implementation differs from subclasses. + // if (exportName == "Base") + // { + // _writer.WriteIndentedComment( + // """ + // Enumerate all child nodes. + // Return a sequence of child elements, components and/or properties. + // Child nodes are returned in the order defined by the FHIR specification. + // First returns child nodes inherited from any base class(es), recursively. + // Finally returns child nodes defined by the current class. + // """); + // _writer.WriteLineIndented("[IgnoreDataMember]"); + // _writer.WriteLineIndented("public virtual IEnumerable Children => Enumerable.Empty();"); + // _writer.WriteLine(string.Empty); + // return; + // } + // + // // Don't override anything if there are no additional elements. + // if (!exportedElements.Any()) + // { + // return; + // } + // + // _writer.WriteLineIndented("[IgnoreDataMember]"); + // _writer.WriteLineIndented("public override IEnumerable Children"); + // + // OpenScope(); + // _writer.WriteLineIndented("get"); + // OpenScope(); + // _writer.WriteLineIndented($"foreach (var item in base.Children) yield return item;"); + // + // foreach (WrittenElementInfo info in exportedElements) + // { + // if (info.PropertyType is ListTypeReference) + // { + // _writer.WriteLineIndented( + // $"foreach (var elem in {info.PropertyName})" + + // $" {{ if (elem != null) yield return elem; }}"); + // } + // else + // { + // string yr = NamedChildrenFhirTypeWrapper(info); + // _writer.WriteLineIndented( + // $"if ({info.PropertyName} != null)" + + // $" yield return {yr};"); + // } + // } + // + // CloseScope(suppressNewline: true); + // CloseScope(); + // } /// Writes the matches. /// Name of the exported class. @@ -2664,9 +2664,9 @@ private void WriteBackboneComponent( { WriteMatches(exportName, exportedElements); WriteIsExactly(exportName, exportedElements); - WriteChildren(exportName, exportedElements); - WriteNamedChildren(exportName, exportedElements); - WriteIDictionarySupport(exportName, exportedElements); + //WriteChildren(exportName, exportedElements); + //WriteNamedChildren(exportName, exportedElements); + WriteDictionarySupport(exportName, exportedElements); } // close class From 0293f532ffeaa9863de95558db1ef5fb2d2f4919 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Wed, 20 Nov 2024 19:03:43 +0100 Subject: [PATCH 07/52] Working on improving doccomments given details about changed types. --- .../Language/Firely/CSharpFirely2.cs | 121 ++++++++++++------ .../Language/Firely/CSharpFirelyCommon.cs | 8 +- .../Language/Firely/TypeReference.cs | 42 +++--- 3 files changed, 113 insertions(+), 58 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index f23cb4df0..f3b4f7100 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -11,8 +11,10 @@ using Microsoft.Health.Fhir.CodeGen.Models; using Microsoft.Health.Fhir.CodeGen.Utils; using Microsoft.Health.Fhir.CodeGenCommon.FhirExtensions; +using Microsoft.Health.Fhir.CodeGenCommon.Models; using Microsoft.Health.Fhir.CodeGenCommon.Packaging; using Microsoft.Health.Fhir.CodeGenCommon.Utils; +using Ncqa.Cql.Model; using static Microsoft.Health.Fhir.CodeGen.Language.Firely.CSharpFirelyCommon; using static Microsoft.Health.Fhir.CodeGenCommon.Extensions.FhirNameConventionExtensions; @@ -31,7 +33,7 @@ bool IFileHashTestable.GenerateHashesInsteadOfOutput set => _generateHashesInsteadOfOutput = value; } - private Dictionary _fileHashes = []; + private readonly Dictionary _fileHashes = []; Dictionary IFileHashTestable.FileHashes => _fileHashes; /// (Immutable) Name of the language. @@ -294,7 +296,19 @@ internal record class ValueSetBehaviorOverrides ["http://hl7.org/fhir/ValueSet/fhir-types"] = "FHIRAllTypes" }; - private record SinceVersion(FhirReleases.FhirSequenceCodes Since); + private record ElementTypeChange(FhirReleases.FhirSequenceCodes Since, TypeReference DeclaredType); + + private static readonly Dictionary _elementTypeChanges = new() + { + ["Attachment.size"] = [ + new(FhirReleases.FhirSequenceCodes.STU3, PrimitiveTypeReference.UnsignedInt), + new (FhirReleases.FhirSequenceCodes.R5, PrimitiveTypeReference.Integer64), + ], + ["Attachment.url"] = [ + new(FhirReleases.FhirSequenceCodes.STU3, PrimitiveTypeReference.Uri), + new(FhirReleases.FhirSequenceCodes.R4, PrimitiveTypeReference.Url), + ] + }; private readonly Dictionary _sinceAttributes = new() { @@ -392,7 +406,7 @@ private record SinceVersion(FhirReleases.FhirSequenceCodes Since); /// Gets the FHIR primitive type map. /// The FHIR primitive type map. - Dictionary ILanguage.FhirPrimitiveTypeMap => CSharpFirelyCommon.PrimitiveTypeMap; + Dictionary ILanguage.FhirPrimitiveTypeMap => PrimitiveTypeMap; /// If a Cql ModelInfo is available, this will be the parsed XML model file. private Ncqa.Cql.Model.ModelInfo? _cqlModelInfo = null; @@ -414,9 +428,9 @@ public void Export(object untypedOptions, DefinitionCollection info) // STU3 satellite is a combination of satellite and conformance if ((info.FhirSequence == FhirReleases.FhirSequenceCodes.STU3) && - (subset == CSharpFirelyCommon.GenSubset.Satellite)) + (subset == GenSubset.Satellite)) { - subset = CSharpFirelyCommon.GenSubset.Satellite | CSharpFirelyCommon.GenSubset.Conformance; + subset = GenSubset.Satellite | GenSubset.Conformance; } // only generate base definitions for R5 @@ -456,8 +470,8 @@ public void Export(object untypedOptions, DefinitionCollection info) string cqlModelResourceKey = options.CqlModel; if (!string.IsNullOrEmpty(cqlModelResourceKey)) { - _cqlModelInfo = Ncqa.Cql.Model.CqlModels.LoadEmbeddedResource(cqlModelResourceKey); - _cqlModelClassInfo = Ncqa.Cql.Model.CqlModels.ClassesByName(_cqlModelInfo); + _cqlModelInfo = CqlModels.LoadEmbeddedResource(cqlModelResourceKey); + _cqlModelClassInfo = CqlModels.ClassesByName(_cqlModelInfo); } var allPrimitives = new Dictionary(); @@ -484,21 +498,21 @@ public void Export(object untypedOptions, DefinitionCollection info) WriteGenerationComment(infoWriter); - if (options.ExportStructures.Contains(CodeGenCommon.Models.FhirArtifactClassEnum.ValueSet)) + if (options.ExportStructures.Contains(FhirArtifactClassEnum.ValueSet)) { WriteSharedValueSets(subset); } _modelWriter.WriteLineIndented("// Generated items"); - if (options.ExportStructures.Contains(CodeGenCommon.Models.FhirArtifactClassEnum.PrimitiveType)) + if (options.ExportStructures.Contains(FhirArtifactClassEnum.PrimitiveType)) { WritePrimitiveTypes(_info.PrimitiveTypesByName.Values, ref dummy, subset); } AddModels(allPrimitives, _info.PrimitiveTypesByName.Values); - if (options.ExportStructures.Contains(CodeGenCommon.Models.FhirArtifactClassEnum.ComplexType)) + if (options.ExportStructures.Contains(FhirArtifactClassEnum.ComplexType)) { WriteComplexDataTypes(_info.ComplexTypesByName.Values, ref dummy, subset); } @@ -506,14 +520,14 @@ public void Export(object untypedOptions, DefinitionCollection info) AddModels(allComplexTypes, _info.ComplexTypesByName.Values); AddModels(allComplexTypes, _sharedR5DataTypes); - if (options.ExportStructures.Contains(CodeGenCommon.Models.FhirArtifactClassEnum.Resource)) + if (options.ExportStructures.Contains(FhirArtifactClassEnum.Resource)) { WriteResources(_info.ResourcesByName.Values, ref dummy, subset); } AddModels(allResources, _info.ResourcesByName.Values); - if (options.ExportStructures.Contains(CodeGenCommon.Models.FhirArtifactClassEnum.Interface)) + if (options.ExportStructures.Contains(FhirArtifactClassEnum.Interface)) { WriteInterfaces(_info.InterfacesByName.Values, ref dummy, subset); } @@ -1775,7 +1789,7 @@ private void WriteInterfaceElements( var since = _sinceAttributes.TryGetValue(element.Path, out string? s) ? s : null; var until = _untilAttributes.TryGetValue(element.Path, out (string, string) u) ? u : default((string, string)?); - var description = AttributeDescriptionWithSinceInfo(name, element.Short.Replace("{{title}}", structureName), since, until); + var description = MakeAttributeRemarkForNewOrDeprecatedProperties(name, element.Short.Replace("{{title}}", structureName), since, until); if (TryGetPrimitiveType(ei.PropertyType, out PrimitiveTypeReference? eiPTR)) { @@ -3099,26 +3113,27 @@ private void WriteElement( } string path = element.cgPath(); - var since = _sinceAttributes.GetValueOrDefault(path); var until = _untilAttributes.TryGetValue(path, out (string, string) u) ? u : default((string, string)?); // TODO: Modify these elements in ModifyDefinitionsForConsistency - var description = path switch + var remarks = path switch { - "Signature.who" => element.Short + ".\nNote 1: Since R4 the type of this element should be a fixed type (ResourceReference). For backwards compatibility it remains of type DataType.\nNote 2: Since R5 the cardinality is expanded to 0..1 (previous it was 1..1).", - "Signature.onBehalfOf" => element.Short + ".\nNote: Since R4 the type of this element should be a fixed type (ResourceReference). For backwards compatibility it remains of type DataType.", - "Signature.when" => element.Short + ".\nNote: Since R5 the cardinality is expanded to 0..1 (previous it was 1..1).", - "Signature.type" => element.Short + ".\nNote: Since R5 the cardinality is expanded to 0..* (previous it was 1..*).", - _ => AttributeDescriptionWithSinceInfo(name, element.Short, since, until) + "Signature.who" => "Note 1: Since R4 the type of this element should be a fixed type (ResourceReference). For backwards compatibility it remains of type DataType.\nNote 2: Since R5 the cardinality is expanded to 0..1 (previous it was 1..1).", + "Signature.onBehalfOf" => "Since R4 the type of this element should be a fixed type (ResourceReference). For backwards compatibility it remains of type DataType.", + "Signature.when" => "Since R5 the cardinality is expanded to 0..1 (previous it was 1..1).", + "Signature.type" => "Since R5 the cardinality is expanded to 0..* (previous it was 1..*).", + _ => MakeAttributeRemarkForNewOrDeprecatedProperties(name, null, since, until) }; + if(element.Short is not null) WriteIndentedComment(element.Short.EnsurePeriod()); + remarks = MakeAttributeRemarkForChangedTypes(path, remarks); + if (remarks is not null) WriteIndentedComment(remarks, isSummary: false, isRemarks: true); + string? xmlSerialization = path == "Narrative.div" ? "XHtml" : ei.PropertyType is CqlTypeReference ? "XmlAttr" : null; - if (description is not null) WriteIndentedComment(description); - if (path == "OperationOutcome.issue.severity") { BuildFhirElementAttribute(name, summary, ", IsModifier=true", element, orderOffset, choice, fiveWs); @@ -3147,15 +3162,6 @@ private void WriteElement( { _writer.WriteLineIndented($"[DeclaredType(Type = typeof(Code), Since = FhirRelease.R5)]"); } - else if (path == "Attachment.url") - { - _writer.WriteLineIndented($"[DeclaredType(Type = typeof(FhirUrl), Since = FhirRelease.R4)]"); - } - else if (path == "Attachment.size") - { - _writer.WriteLineIndented($"[DeclaredType(Type = typeof(UnsignedInt), Since = FhirRelease.STU3)]"); - _writer.WriteLineIndented($"[DeclaredType(Type = typeof(Integer64), Since = FhirRelease.R5)]"); - } else if (path is "ElementDefinition.constraint.requirements" or "ElementDefinition.binding.description" or @@ -3176,6 +3182,16 @@ private void WriteElement( _writer.WriteLineIndented($"[Binding(\"{element.cgBindingName()}\")]"); } + if (_elementTypeChanges.TryGetValue(path, out ElementTypeChange[]? changes)) + { + foreach(var change in changes) + { + _writer.WriteIndented($"[DeclaredType(Type = typeof({change.DeclaredType.PropertyTypeString})"); + _writer.Write($", Since = FhirRelease.{change.Since}"); + _writer.WriteLine(")]"); + } + } + if (element.cgIsSimple() && element.Type.Count == 1 && element.Type.Single().cgName() == "uri") _writer.WriteLineIndented("[UriPattern]"); @@ -3216,21 +3232,42 @@ private void WriteElement( } - private static string? AttributeDescriptionWithSinceInfo(string name, string baseDescription, string? since = null, (string, string)? until = null) + private static string? MakeAttributeRemarkForNewOrDeprecatedProperties(string name, string? baseRemark, string? since = null, (string, string)? until = null) { - return (since, until, baseDescription) switch - { - (_, _, null) => null, - (not null, _, _) => baseDescription + - $". Note: Element was introduced in {since}, do not use when working with older releases.", - (_, (var release, ""), _) => baseDescription + - $". Note: Element is deprecated since {release}, do not use with {release} and newer releases.", - (_, (var release, var replacedBy), _) => baseDescription + - $". Note: Element is replaced by '{replacedBy}' since {release}. Do not use this element '{name}' with {release} and newer releases.", - _ => baseDescription + var deprecationRemark = (since, until, baseRemark) switch + { + (not null, _, _) => $"Element was introduced in {since}, do not use when working with older releases.", + (_, (var release, ""), _) => $"Element is deprecated since {release}, do not use with {release} and newer releases.", + (_, (var release, var replacedBy), _) => $"Element is replaced by '{replacedBy}' since {release}. Do not use this element '{name}' with {release} and newer releases.", + _ => null }; + + if(baseRemark is null) + { + return deprecationRemark; + } + + return $"{baseRemark}. {deprecationRemark}"; + } + + private static string? MakeAttributeRemarkForChangedTypes(string path, string? baseDescription) + { + if(!_elementTypeChanges.TryGetValue(path, out ElementTypeChange[]? changes)) + { + return baseDescription; + } + + var changedDescription = $"The type of this element has changed over time. Make sure to use " + + string.Join(", ", + changes.Select(change => $"{change.DeclaredType.PropertyTypeString} in {change.Since}")) + "."; + + if (baseDescription is null) + return changedDescription; + + return $"{baseDescription}. {changedDescription}"; } + private static PrimitiveTypeReference BuildTypeReferenceForCode(DefinitionCollection info, ElementDefinition element, Dictionary writtenValueSets) { if ((element.Binding?.Strength != Hl7.Fhir.Model.BindingStrength.Required) || diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirelyCommon.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirelyCommon.cs index a3662d2c7..e1a9b4169 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirelyCommon.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirelyCommon.cs @@ -17,7 +17,7 @@ public static class CSharpFirelyCommon { /// Dictionary mapping FHIR primitive types to language equivalents (see Template-Model.tt#1252). - public static readonly Dictionary PrimitiveTypeMap = new Dictionary() + public static readonly Dictionary PrimitiveTypeMap = new() { { "base64Binary", "byte[]" }, { "boolean", "bool?" }, @@ -252,3 +252,9 @@ public static int GetOrder(int relativeOrder) return (relativeOrder * 10) + 10; } } + + +public static class StringHelpers +{ + public static string EnsurePeriod(this string s) => s.EndsWith('.') ? s : s + "."; +} diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/TypeReference.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/TypeReference.cs index 19b7b2a09..e18ee6dfe 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/TypeReference.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/TypeReference.cs @@ -1,5 +1,3 @@ -#nullable enable - using Microsoft.Health.Fhir.CodeGenCommon.Extensions; using Microsoft.Health.Fhir.CodeGenCommon.Utils; @@ -43,27 +41,41 @@ public record PrimitiveTypeReference(string Name, string PocoTypeName, Type Conv public static PrimitiveTypeReference ForTypeName(string name, Type propertyType) => new(name, MapTypeName(name), propertyType); + public static readonly PrimitiveTypeReference Boolean = ForTypeName("boolean", typeof(bool)); + public static readonly PrimitiveTypeReference Base64Binary = ForTypeName("base64Binary", typeof(byte[])); + public static readonly PrimitiveTypeReference Canonical = ForTypeName("canonical", typeof(string)); + public static readonly PrimitiveTypeReference Code = ForTypeName("code", typeof(string)); + public static readonly PrimitiveTypeReference Date = ForTypeName("date", typeof(string)); + public static readonly PrimitiveTypeReference DateTime = ForTypeName("dateTime", typeof(string)); + public static readonly PrimitiveTypeReference Decimal = ForTypeName("decimal", typeof(decimal)); + public static readonly PrimitiveTypeReference Id = ForTypeName("id", typeof(string)); + public static readonly PrimitiveTypeReference Instant = ForTypeName("instant", typeof(DateTimeOffset)); + public static readonly PrimitiveTypeReference Integer = ForTypeName("integer", typeof(int)); + public static readonly PrimitiveTypeReference Integer64 = ForTypeName("integer64", typeof(long)); + public static readonly PrimitiveTypeReference Oid = ForTypeName("oid", typeof(string)); + public static readonly PrimitiveTypeReference PositiveInt = ForTypeName("positiveInt", typeof(int)); + public static readonly PrimitiveTypeReference String = ForTypeName("string", typeof(string)); + public static readonly PrimitiveTypeReference Time = ForTypeName("time", typeof(string)); + public static readonly PrimitiveTypeReference UnsignedInt = ForTypeName("unsignedInt", typeof(int)); + public static readonly PrimitiveTypeReference Uri = ForTypeName("uri", typeof(string)); + public static readonly PrimitiveTypeReference Url = ForTypeName("url", typeof(string)); + public static readonly PrimitiveTypeReference Xhtml = ForTypeName("xhtml", typeof(string)); + public static readonly PrimitiveTypeReference Markdown = ForTypeName("markdown", typeof(string)); + public static readonly IReadOnlyCollection PrimitiveList = [ - ForTypeName("base64Binary", typeof(byte[])), ForTypeName("boolean", typeof(bool)), - ForTypeName("canonical", typeof(string)), ForTypeName("code", typeof(string)), - ForTypeName("date", typeof(string)), ForTypeName("dateTime", typeof(string)), - ForTypeName("decimal", typeof(decimal)), ForTypeName("id", typeof(string)), - ForTypeName("instant", typeof(DateTimeOffset)), ForTypeName("integer", typeof(int)), - ForTypeName("integer64", typeof(long)), ForTypeName("oid", typeof(string)), - ForTypeName("positiveInt", typeof(int)), ForTypeName("string", typeof(string)), - ForTypeName("time", typeof(string)), ForTypeName("unsignedInt", typeof(int)), - ForTypeName("uri", typeof(string)), ForTypeName("url", typeof(string)), - ForTypeName("xhtml", typeof(string)), ForTypeName("markdown", typeof(string)) + Boolean, Base64Binary, Canonical, Code, Date, DateTime, Decimal, Id, + Instant, Integer, Integer64, Oid, PositiveInt, String, Time, UnsignedInt, + Uri, Url, Xhtml, Markdown ]; - private static readonly Dictionary s_primitiveDictionary = + private static readonly Dictionary _primitiveDictionary = PrimitiveList.ToDictionary(ptr => ptr.Name); - public static bool IsFhirPrimitiveType(string name) => s_primitiveDictionary.ContainsKey(name); + public static bool IsFhirPrimitiveType(string name) => _primitiveDictionary.ContainsKey(name); public static PrimitiveTypeReference GetTypeReference(string name) => - s_primitiveDictionary.TryGetValue(name, out var tr) + _primitiveDictionary.TryGetValue(name, out var tr) ? tr : throw new InvalidOperationException($"Unknown FHIR primitive {name}"); From 1198ba8e79814de21a228cb23c658f409ad3fcb2 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Mon, 25 Nov 2024 12:25:41 +0100 Subject: [PATCH 08/52] WIP --- .../Language/Firely/CSharpFirely2.cs | 199 +++++++++--------- .../Language/Firely/FirelyNetIG.cs | 6 +- .../Language/Firely/TypeReference.cs | 7 +- 3 files changed, 114 insertions(+), 98 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 6c7ed11aa..1a111d4b9 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -298,6 +298,12 @@ internal record class ValueSetBehaviorOverrides private record ElementTypeChange(FhirReleases.FhirSequenceCodes Since, TypeReference DeclaredType); + private static readonly ElementTypeChange[] stringToMarkdown = + [ + new(FhirReleases.FhirSequenceCodes.STU3, PrimitiveTypeReference.String), + new(FhirReleases.FhirSequenceCodes.R5, PrimitiveTypeReference.Markdown) + ]; + private static readonly Dictionary _elementTypeChanges = new() { ["Attachment.size"] = [ @@ -307,7 +313,20 @@ private record ElementTypeChange(FhirReleases.FhirSequenceCodes Since, TypeRefer ["Attachment.url"] = [ new(FhirReleases.FhirSequenceCodes.STU3, PrimitiveTypeReference.Uri), new(FhirReleases.FhirSequenceCodes.R4, PrimitiveTypeReference.Url), - ] + ], + ["Meta.profile"] = [ + new(FhirReleases.FhirSequenceCodes.STU3, PrimitiveTypeReference.Uri), + new(FhirReleases.FhirSequenceCodes.R4, PrimitiveTypeReference.Canonical), + ], + ["Bundle.link.relation"] = [ + new(FhirReleases.FhirSequenceCodes.STU3, PrimitiveTypeReference.String), + new(FhirReleases.FhirSequenceCodes.R5, PrimitiveTypeReference.Code) + ], + + ["ElementDefinition.constraint.requirements"] = stringToMarkdown, + ["ElementDefinition.binding.description"] = stringToMarkdown, + ["ElementDefinition.mapping.comment"] = stringToMarkdown, + ["CapabilityStatement.implementation.description"] = stringToMarkdown, }; private readonly Dictionary _sinceAttributes = new() @@ -1772,7 +1791,7 @@ private void WriteInterfaceElements( string exportedComplexName, ref List exportedElements) { - var elementsToGenerate = complex.cgGetChildren() + IOrderedEnumerable elementsToGenerate = complex.cgGetChildren() .Where(e => !e.cgIsInherited(complex.Structure)) .OrderBy(e => e.cgFieldOrder()); @@ -1786,27 +1805,19 @@ private void WriteInterfaceElements( exportedElements.Add(ei); string name = element.cgName(removeChoiceMarker: true); - var since = _sinceAttributes.TryGetValue(element.Path, out string? s) ? s : null; - var until = _untilAttributes.TryGetValue(element.Path, out (string, string) u) ? u : default((string, string)?); + string? since = _sinceAttributes.TryGetValue(element.Path, out string? s) ? s : null; + (string, string)? until = _untilAttributes.TryGetValue(element.Path, out (string, string) u) ? u : default((string, string)?); - var description = MakeAttributeRemarkForNewOrDeprecatedProperties(name, element.Short.Replace("{{title}}", structureName), since, until); + string? description = MakeAttributeRemarkForNewOrDeprecatedProperties(name, element.Short.Replace("{{title}}", structureName), since, until); - if (TryGetPrimitiveType(ei.PropertyType, out PrimitiveTypeReference? eiPTR)) + if (TryGetPrimitiveType(ei.PropertyType, out PrimitiveTypeReference? eiPtr)) { WriteIndentedComment(element.Short.Replace("{{title}}", structureName)); _writer.WriteLineIndented($"/// This uses the native .NET datatype, rather than the FHIR equivalent"); - _writer.WriteLineIndented($"{eiPTR.ConveniencePropertyTypeString} {ei.PrimitiveHelperName} {{ get; set; }}"); + _writer.WriteLineIndented($"{eiPtr.ConveniencePropertyTypeString} {ei.PrimitiveHelperName} {{ get; set; }}"); _writer.WriteLine(); } - //if (ei.IsPrimitive) - //{ - // WriteIndentedComment(element.Short.Replace("{{title}}", structureName)); - // _writer.WriteLineIndented($"/// This uses the native .NET datatype, rather than the FHIR equivalent"); - // _writer.WriteLineIndented($"{ei.PrimitiveHelperType?.Replace("Hl7.Fhir.Model.", string.Empty) ?? string.Empty} {ei.PrimitiveHelperName} {{ get; set; }}"); - // _writer.WriteLine(); - //} - if (description != null) WriteIndentedComment(description); _writer.WriteLineIndented($"{ei.PropertyType.PropertyTypeString ?? string.Empty} {ei.PropertyName} {{ get; set; }}"); _writer.WriteLine(); @@ -3113,11 +3124,11 @@ private void WriteElement( } string path = element.cgPath(); - var since = _sinceAttributes.GetValueOrDefault(path); - var until = _untilAttributes.TryGetValue(path, out (string, string) u) ? u : default((string, string)?); + string? since = _sinceAttributes.GetValueOrDefault(path); + (string, string)? until = _untilAttributes.TryGetValue(path, out (string, string) u) ? u : default((string, string)?); // TODO: Modify these elements in ModifyDefinitionsForConsistency - var remarks = path switch + string? remarks = path switch { "Signature.who" => "Note 1: Since R4 the type of this element should be a fixed type (ResourceReference). For backwards compatibility it remains of type DataType.\nNote 2: Since R5 the cardinality is expanded to 0..1 (previous it was 1..1).", "Signature.onBehalfOf" => "Since R4 the type of this element should be a fixed type (ResourceReference). For backwards compatibility it remains of type DataType.", @@ -3154,25 +3165,8 @@ private void WriteElement( { _writer.WriteLineIndented($"[DeclaredType(Type = typeof({ctr.DeclaredTypeString}))]"); } - else if (path == "Meta.profile") - { - _writer.WriteLineIndented($"[DeclaredType(Type = typeof(Canonical), Since = FhirRelease.R4)]"); - } - else if (path == "Bundle.link.relation") - { - _writer.WriteLineIndented($"[DeclaredType(Type = typeof(Code), Since = FhirRelease.R5)]"); - } - else if (path is - "ElementDefinition.constraint.requirements" or - "ElementDefinition.binding.description" or - "ElementDefinition.mapping.comment" or - "CapabilityStatement.implementation.description") - { - _writer.WriteLineIndented($"[DeclaredType(Type = typeof(FhirString))]"); - _writer.WriteLineIndented($"[DeclaredType(Type = typeof(Markdown), Since = FhirRelease.R5)]"); - } - if (TryGetPrimitiveType(ei.PropertyType, out var ptr) && ptr is CodedTypeReference) + if (TryGetPrimitiveType(ei.PropertyType, out PrimitiveTypeReference? ptr) && ptr is CodedTypeReference) { _writer.WriteLineIndented("[DeclaredType(Type = typeof(Code))]"); } @@ -3184,7 +3178,7 @@ private void WriteElement( if (_elementTypeChanges.TryGetValue(path, out ElementTypeChange[]? changes)) { - foreach(var change in changes) + foreach(ElementTypeChange change in changes) { _writer.WriteIndented($"[DeclaredType(Type = typeof({change.DeclaredType.PropertyTypeString})"); _writer.Write($", Since = FhirRelease.{change.Since}"); @@ -3228,7 +3222,7 @@ private void WriteElement( _writer.WriteLineIndented($"[Cardinality(Min={element.Min},Max={element.cgCardinalityMax()})]"); } - writeElementGettersAndSetters(element, ei); + WriteElementGettersAndSetters(element, ei); } @@ -3310,20 +3304,16 @@ private static TypeReference DetermineTypeReferenceForFhirElement( TypeReference determineTypeReferenceForFhirElementName() { - if (element.Path is "Meta.profile") + if(_elementTypeChanges.TryGetValue(element.Path, out ElementTypeChange[]? changes)) { - /* we want to share Meta across different FHIR versions, - * so we use the "most common" type to the versions, which - * is uri rather than the more specific canonical. */ - return PrimitiveTypeReference.GetTypeReference("uri"); - } + // If the element has a type change, we need to use DataType, to make + // sure the property can capture all the types. + if(changes.All(c => c.DeclaredType is PrimitiveTypeReference)) + { + return PrimitiveTypeReference.PrimitiveType; + } - if (element.Path is "Attachment.url") - { - /* we want to share Attachment across different FHIR versions, - * so we use the "most common" type to the versions, which - * is uri rather than the more specific url. */ - return PrimitiveTypeReference.GetTypeReference("uri"); + return ComplexTypeReference.DataTypeReference; } if (element.Path is "Element.id" or "Extension.url") @@ -3431,7 +3421,7 @@ internal static WrittenElementInfo BuildElementInfo( ); } - private void writeElementGettersAndSetters(ElementDefinition element, WrittenElementInfo ei) + private void WriteElementGettersAndSetters(ElementDefinition element, WrittenElementInfo ei) { _writer.WriteLineIndented("[DataMember]"); @@ -3465,62 +3455,83 @@ private void writeElementGettersAndSetters(ElementDefinition element, WrittenEle PrimitiveTypeReference or ListTypeReference { Element: PrimitiveTypeReference }; - if (!needsPrimitiveProperty) + if (needsPrimitiveProperty) { - return; + if(_elementTypeChanges.TryGetValue(element.Path, out ElementTypeChange[]? changes)) + { + var newest = changes.Select(c => c.Since).OrderDescending().First(); + + foreach(ElementTypeChange change in changes) + { + WritePrimitiveHelperProperty(element, ei, change.DeclaredType, change.Since, usePrimaryName: change.Since == newest); + } + } + else + { + WritePrimitiveHelperProperty(element, ei); + } } + } + private void WritePrimitiveHelperProperty(ElementDefinition element, WrittenElementInfo ei, + TypeReference? propTypeOverride = null, FhirReleases.FhirSequenceCodes? since = null, bool usePrimaryName = true) + { WriteIndentedComment(element.Short); _writer.WriteLineIndented($"/// This uses the native .NET datatype, rather than the FHIR equivalent"); _writer.WriteLineIndented("[IgnoreDataMember]"); - if (ei.PropertyType is PrimitiveTypeReference ptr) - { - _writer.WriteLineIndented($"public {ptr.ConveniencePropertyTypeString} {ei.PrimitiveHelperName}"); + var helperPropName = (usePrimaryName || propTypeOverride is null) + ? ei.PrimitiveHelperName : $"{ei.PrimitiveHelperName}_{since}"; - OpenScope(); - _writer.WriteLineIndented($"get {{ return {ei.PropertyName} != null ? {ei.PropertyName}.Value : null; }}"); - _writer.WriteLineIndented("set"); - OpenScope(); - - _writer.WriteLineIndented($"if (value == null)"); - - _writer.IncreaseIndent(); - _writer.WriteLineIndented($"{ei.PropertyName} = null;"); - _writer.DecreaseIndent(); - _writer.WriteLineIndented("else"); - _writer.IncreaseIndent(); - _writer.WriteLineIndented($"{ei.PropertyName} = new {ei.PropertyType.PropertyTypeString}(value);"); - _writer.DecreaseIndent(); - _writer.WriteLineIndented($"OnPropertyChanged(\"{ei.PrimitiveHelperName}\");"); - CloseScope(suppressNewline: true); - CloseScope(); - } - else if (ei.PropertyType is ListTypeReference { Element: PrimitiveTypeReference lptr }) + var propType = propTypeOverride ?? ei.PropertyType; + switch (propType) { - _writer.WriteLineIndented($"public IEnumerable<{lptr.ConveniencePropertyTypeString}> {ei.PrimitiveHelperName}"); + case PrimitiveTypeReference ptr: + _writer.WriteLineIndented($"public {ptr.ConveniencePropertyTypeString} {helperPropName}"); - OpenScope(); - _writer.WriteLineIndented($"get {{ return {ei.PropertyName} != null ? {ei.PropertyName}.Select(elem => elem.Value) : null; }}"); - _writer.WriteLineIndented("set"); - OpenScope(); - - _writer.WriteLineIndented($"if (value == null)"); + OpenScope(); + _writer.WriteLineIndented($"get {{ return {ei.PropertyName} != null ? {ei.PropertyName}.Value : null; }}"); + _writer.WriteLineIndented("set"); + OpenScope(); - _writer.IncreaseIndent(); - _writer.WriteLineIndented($"{ei.PropertyName} = null;"); - _writer.DecreaseIndent(); - _writer.WriteLineIndented("else"); - _writer.IncreaseIndent(); - _writer.WriteLineIndented($"{ei.PropertyName} = " + - $"new {ei.PropertyType.PropertyTypeString}" + - $"(value.Select(elem=>new {lptr.PropertyTypeString}(elem)));"); - _writer.DecreaseIndent(); + _writer.WriteLineIndented($"if (value == null)"); - _writer.WriteLineIndented($"OnPropertyChanged(\"{ei.PrimitiveHelperName}\");"); - CloseScope(suppressNewline: true); - CloseScope(); + _writer.IncreaseIndent(); + _writer.WriteLineIndented($"{ei.PropertyName} = null;"); + _writer.DecreaseIndent(); + _writer.WriteLineIndented("else"); + _writer.IncreaseIndent(); + _writer.WriteLineIndented($"{ei.PropertyName} = new {ptr.PropertyTypeString}(value);"); + _writer.DecreaseIndent(); + _writer.WriteLineIndented($"OnPropertyChanged(\"{helperPropName}\");"); + CloseScope(suppressNewline: true); + CloseScope(); + break; + case ListTypeReference { Element: PrimitiveTypeReference lptr }: + _writer.WriteLineIndented($"public IEnumerable<{lptr.ConveniencePropertyTypeString}> {helperPropName}"); + + OpenScope(); + _writer.WriteLineIndented($"get {{ return {ei.PropertyName} != null ? {ei.PropertyName}.Select(elem => elem.Value) : null; }}"); + _writer.WriteLineIndented("set"); + OpenScope(); + + _writer.WriteLineIndented($"if (value == null)"); + + _writer.IncreaseIndent(); + _writer.WriteLineIndented($"{ei.PropertyName} = null;"); + _writer.DecreaseIndent(); + _writer.WriteLineIndented("else"); + _writer.IncreaseIndent(); + _writer.WriteLineIndented($"{ei.PropertyName} = " + + $"new {ei.PropertyType.PropertyTypeString}" + + $"(value.Select(elem=>new {lptr.PropertyTypeString}(elem)));"); + _writer.DecreaseIndent(); + + _writer.WriteLineIndented($"OnPropertyChanged(\"{helperPropName}\");"); + CloseScope(suppressNewline: true); + CloseScope(); + break; } } diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/FirelyNetIG.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/FirelyNetIG.cs index 1a1a64635..9db94765b 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/FirelyNetIG.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/FirelyNetIG.cs @@ -2159,9 +2159,9 @@ private void WriteProfile(StructureDefinition sd) break; //throw new Exception($"Found multiple discriminators for {id}"); } - + if (discriminators.Length == 1) - { + { discriminator = discriminators[0]; bool isExtensionSlice = _findExtensionPathRegex.IsMatch(discriminator.Path); @@ -3361,7 +3361,7 @@ private ExtensionData GetExtensionData( remarks = (remarks == null ? string.Empty : remarks + "\n") + $"Structure Definition Name: {cd.Structure.Name}"; } - + string directive; if (_info.TryGetPackageSource(cd.Structure, out string packageId, out string packageVersion)) { diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/TypeReference.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/TypeReference.cs index e18ee6dfe..bc36057f7 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/TypeReference.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/TypeReference.cs @@ -41,6 +41,7 @@ public record PrimitiveTypeReference(string Name, string PocoTypeName, Type Conv public static PrimitiveTypeReference ForTypeName(string name, Type propertyType) => new(name, MapTypeName(name), propertyType); + public static readonly PrimitiveTypeReference PrimitiveType = ForTypeName("PrimitiveType", typeof(object)); public static readonly PrimitiveTypeReference Boolean = ForTypeName("boolean", typeof(bool)); public static readonly PrimitiveTypeReference Base64Binary = ForTypeName("base64Binary", typeof(byte[])); public static readonly PrimitiveTypeReference Canonical = ForTypeName("canonical", typeof(string)); @@ -95,10 +96,14 @@ public record CqlTypeReference(string Name, Type PropertyType) : TypeReference(N public record ComplexTypeReference(string Name, string PocoTypeName) : TypeReference(Name) { + public ComplexTypeReference(string name) : this(name, name) { } + public override string PropertyTypeString => $"Hl7.Fhir.Model.{PocoTypeName}"; + + public static readonly ComplexTypeReference DataTypeReference = new("DataType"); } -public record ChoiceTypeReference() : ComplexTypeReference("DataType", "DataType"); +public record ChoiceTypeReference() : ComplexTypeReference("DataType"); public record CodedTypeReference(string EnumName, string? EnumClassName) : PrimitiveTypeReference("code", EnumName, typeof(Enum)) From 234a1b029992a4726e1285ff6513e00b0a04bb41 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Mon, 25 Nov 2024 18:29:53 +0100 Subject: [PATCH 09/52] Some cosmetics --- .../Language/Firely/CSharpFirely2.cs | 134 ------------------ 1 file changed, 134 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 23c9a440b..7846af261 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2210,7 +2210,6 @@ void writeSetValueCase(string name, string? when, string statement) _writer.IncreaseIndent(); _writer.WriteLineIndented(statement); - //_writer.WriteLineIndented($"return true;"); _writer.WriteLineIndented($"return this;"); _writer.DecreaseIndent(); } @@ -2229,139 +2228,6 @@ void writeSetValueCase(string name, string? when, string statement) void writeBaseTrySetValue() => _writer.WriteLineIndented("return base.SetValue(key, value);"); } - /// Writes the children of this item. - /// Name of the exported class. - /// The exported elements. - // private void WriteNamedChildren(string exportName, - // List exportedElements) - // { - // // Base implementation differs from subclasses. - // if (exportName == "Base") - // { - // _writer.WriteIndentedComment(""" - // Enumerate all child nodes. - // Return a sequence of child elements, components and/or properties. - // Child nodes are returned as tuples with the name and the node itself, in the order defined - // by the FHIR specification. - // First returns child nodes inherited from any base class(es), recursively. - // Finally returns child nodes defined by the current class. - // """); - // _writer.WriteLineIndented("[IgnoreDataMember]"); - // _writer.WriteLineIndented("public virtual IEnumerable NamedChildren => Enumerable.Empty();"); - // _writer.WriteLine(string.Empty); - // return; - // } - // - // // Don't override anything if there are no additional elements. - // if (!exportedElements.Any()) - // { - // return; - // } - // - // _writer.WriteLineIndented("[IgnoreDataMember]"); - // _writer.WriteLineIndented("public override IEnumerable NamedChildren"); - // - // OpenScope(); - // _writer.WriteLineIndented("get"); - // OpenScope(); - // _writer.WriteLineIndented($"foreach (var item in base.NamedChildren) yield return item;"); - // - // foreach (WrittenElementInfo info in exportedElements) - // { - // if (info.PropertyType is ListTypeReference) - // { - // _writer.WriteLineIndented( - // $"foreach (var elem in {info.PropertyName})" + - // $" {{ if (elem != null)" + - // $" yield return new ElementValue(\"{info.FhirElementName}\", elem);" + - // $" }}"); - // } - // else - // { - // string yr = NamedChildrenFhirTypeWrapper(info); - // - // _writer.WriteLineIndented( - // $"if ({info.PropertyName} != null)" + - // $" yield return new ElementValue(\"{info.FhirElementName}\", {yr});"); - // } - // } - // - // CloseScope(suppressNewline: true); - // CloseScope(); - // } - - // For a limited set of exceptional elements, the Children functions return a - // complex FHIR type wrapper. - // private static string NamedChildrenFhirTypeWrapper(WrittenElementInfo info) - // { - // - // return info.FhirElementPath switch - // { - // "Narrative.div" => $"new FhirString({info.PropertyName}.Value)", - // "Element.id" => $"new FhirString({info.PropertyName})", - // "Extension.url" => $"new FhirUri({info.PropertyName})", - // _ => $"{info.PropertyName}" - // }; - // } - - /// Writes the children of this item. - /// Name of the exported class. - /// The exported elements. - // private void WriteChildren(string exportName, - // List exportedElements) - // { - // // Base implementation differs from subclasses. - // if (exportName == "Base") - // { - // _writer.WriteIndentedComment( - // """ - // Enumerate all child nodes. - // Return a sequence of child elements, components and/or properties. - // Child nodes are returned in the order defined by the FHIR specification. - // First returns child nodes inherited from any base class(es), recursively. - // Finally returns child nodes defined by the current class. - // """); - // _writer.WriteLineIndented("[IgnoreDataMember]"); - // _writer.WriteLineIndented("public virtual IEnumerable Children => Enumerable.Empty();"); - // _writer.WriteLine(string.Empty); - // return; - // } - // - // // Don't override anything if there are no additional elements. - // if (!exportedElements.Any()) - // { - // return; - // } - // - // _writer.WriteLineIndented("[IgnoreDataMember]"); - // _writer.WriteLineIndented("public override IEnumerable Children"); - // - // OpenScope(); - // _writer.WriteLineIndented("get"); - // OpenScope(); - // _writer.WriteLineIndented($"foreach (var item in base.Children) yield return item;"); - // - // foreach (WrittenElementInfo info in exportedElements) - // { - // if (info.PropertyType is ListTypeReference) - // { - // _writer.WriteLineIndented( - // $"foreach (var elem in {info.PropertyName})" + - // $" {{ if (elem != null) yield return elem; }}"); - // } - // else - // { - // string yr = NamedChildrenFhirTypeWrapper(info); - // _writer.WriteLineIndented( - // $"if ({info.PropertyName} != null)" + - // $" yield return {yr};"); - // } - // } - // - // CloseScope(suppressNewline: true); - // CloseScope(); - // } - /// Writes the matches. /// Name of the exported class. /// The exported elements. From 42d429d7826a94f35de4c9ee510710adfdd63223 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Tue, 26 Nov 2024 17:41:46 +0100 Subject: [PATCH 10/52] Made it work. --- .../Language/Firely/CSharpFirely2.cs | 147 ++++++++++++------ .../FhirNameConventionExtensions.cs | 6 +- 2 files changed, 102 insertions(+), 51 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 9be587fd2..9aadf8acc 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -3,6 +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; @@ -296,7 +297,8 @@ internal record class ValueSetBehaviorOverrides ["http://hl7.org/fhir/ValueSet/fhir-types"] = "FHIRAllTypes" }; - private record ElementTypeChange(FhirReleases.FhirSequenceCodes Since, TypeReference DeclaredType); + private record ElementTypeChange(FhirReleases.FhirSequenceCodes Since, + TypeReference DeclaredTypeReference); private static readonly ElementTypeChange[] stringToMarkdown = [ @@ -304,6 +306,41 @@ private record ElementTypeChange(FhirReleases.FhirSequenceCodes Since, TypeRefer new(FhirReleases.FhirSequenceCodes.R5, PrimitiveTypeReference.Markdown) ]; + /// + /// Given one of the versions, returns a string that describes from which version until which + /// version that change was in effect. + /// + private static string VersionChangeMessage(ElementTypeChange[] changeSet, ElementTypeChange thisChange, bool capitalize) + { + int index = Array.IndexOf(changeSet, thisChange); + if (index == -1) throw new ArgumentException("Change needs to be part of the set", nameof(thisChange)); + + if (index + 1 >= changeSet.Length) + { + // This is the last change in the set + return $"{(capitalize ? "S" : "s")}tarting from {thisChange.Since}"; + } + + int now = (int)thisChange.Since; + int next = (int)changeSet[index + 1].Since; + IEnumerable versionOrdinals = + Enumerable.Range(now, next - now).Cast(); + string versions = string.Join(", ", versionOrdinals.Select(v => v.ToString())); + versions = replaceLastOccurrence(versions, ", ", " and "); + return $"{(capitalize ? "I" : "i")}n {versions}"; + + static string replaceLastOccurrence(string source, string find, string replace) + { + int place = source.LastIndexOf(find, StringComparison.Ordinal); + + if (place == -1) + return source; + + return source.Remove(place, find.Length).Insert(place, replace); + } + } + + // ReSharper disable ArrangeObjectCreationWhenTypeNotEvident private static readonly Dictionary _elementTypeChanges = new() { ["Attachment.size"] = [ @@ -328,6 +365,7 @@ private record ElementTypeChange(FhirReleases.FhirSequenceCodes Since, TypeRefer ["ElementDefinition.mapping.comment"] = stringToMarkdown, ["CapabilityStatement.implementation.description"] = stringToMarkdown, }; + // ReSharper restore ArrangeObjectCreationWhenTypeNotEvident private readonly Dictionary _sinceAttributes = new() { @@ -452,6 +490,11 @@ public void Export(object untypedOptions, DefinitionCollection info) subset = GenSubset.Satellite | GenSubset.Conformance; } + // By definition, we should not have any element type changes for sattelites, they + // should only have their own, defined types from the spec. + if (subset.HasFlag(GenSubset.Satellite)) + _elementTypeChanges.Clear(); + // only generate base definitions for R5 if (subset.HasFlag(GenSubset.Base) && info.FhirSequence != FhirReleases.FhirSequenceCodes.R5) { @@ -707,13 +750,8 @@ private void ModifyDefinitionsForConsistency() edOnBehalfOf.Base.Path = "Signature.onBehalfOf[x]"; edOnBehalfOf.Type.Add(new() { Code = "uri" }); - int prevFO = edOnBehalfOf.cgFieldOrder(); - int prevCFO = edOnBehalfOf.cgComponentFieldOrder(); - // TODO: fix the order (should be 6th total, 5th in component) edOnBehalfOf.cgSetFieldOrder(6, 5); - - //_ = _info.TryUpdateElement(sdSignature, edOnBehalfOf, prevFO, prevCFO); } } @@ -1563,10 +1601,6 @@ private void WriteInterfaceComponent( WriteIndentedComment($"{complex.Element.Short}"); - //WriteSerializable(); - - string fhirTypeConstructor = $"\"{complexName}\",\"{complex.cgUrl()}\""; - StructureDefinition? parentInterface = _info.GetParentInterface(complex.Structure); if (parentInterface == null) @@ -1717,15 +1751,17 @@ private void WriteInterfaceElementGettersAndSetters( else { WriteIndentedComment( - $"{resourceExportName}.{resourceEi.PropertyName} ({resourceEi.PropertyType}) is incompatible with\n" + - $"{interfaceExportName}.{interfaceEi.FhirElementName} ({interfaceEi.PropertyType})", + $"{resourceExportName}.{resourceEi.PropertyName} ({resourceEi.PropertyType.PropertyTypeString}) is incompatible with\n" + + $"{interfaceExportName}.{interfaceEi.FhirElementName} ({interfaceEi.PropertyType.PropertyTypeString})", isSummary: false, isRemarks: true); _writer.WriteLineIndented("[IgnoreDataMember]"); _writer.WriteLineIndented($"{it} {pn}"); OpenScope(); _writer.WriteLineIndented($"get {{ return null; }}"); - _writer.WriteLineIndented($"set {{ throw new NotImplementedException(\"{resourceExportName}.{resourceEi.PropertyName} ({resourceEi.PropertyType}) is incompatible with {interfaceExportName}.{interfaceEi.FhirElementName} ({interfaceEi.PropertyType})\");}}"); + _writer.WriteLineIndented($"set {{ throw new NotImplementedException(\"{resourceExportName}.{resourceEi.PropertyName} " + + $"({resourceEi.PropertyType.PropertyTypeString}) is incompatible with" + + $" {interfaceExportName}.{interfaceEi.FhirElementName} ({interfaceEi.PropertyType.PropertyTypeString})\");}}"); CloseScope(); } @@ -1744,7 +1780,8 @@ private void WriteInterfaceElementGettersAndSetters( _writer.WriteLineIndented($"{pit} {ppn}"); OpenScope(); _writer.WriteLineIndented($"get {{ return null; }}"); - _writer.WriteLineIndented($"set {{ throw new NotImplementedException(\"Resource {resourceExportName} does not implement {interfaceExportName}.{interfaceEi.FhirElementName}\");}}"); + _writer.WriteLineIndented($"set {{ throw new NotImplementedException(\"Resource {resourceExportName}" + + $" does not implement {interfaceExportName}.{interfaceEi.FhirElementName}\");}}"); CloseScope(); } else if (interfaceEi.PropertyType == resourceEi.PropertyType) @@ -1795,8 +1832,6 @@ private void WriteInterfaceElements( .Where(e => !e.cgIsInherited(complex.Structure)) .OrderBy(e => e.cgFieldOrder()); - int orderOffset = complex.Element.cgFieldOrder(); - string structureName = complex.cgName(); foreach (ElementDefinition element in elementsToGenerate) @@ -1805,7 +1840,7 @@ private void WriteInterfaceElements( exportedElements.Add(ei); string name = element.cgName(removeChoiceMarker: true); - string? since = _sinceAttributes.TryGetValue(element.Path, out string? s) ? s : null; + string? since = _sinceAttributes.GetValueOrDefault(element.Path); (string, string)? until = _untilAttributes.TryGetValue(element.Path, out (string, string) u) ? u : default((string, string)?); string? description = MakeAttributeRemarkForNewOrDeprecatedProperties(name, element.Short.Replace("{{title}}", structureName), since, until); @@ -2038,9 +2073,6 @@ private void WriteComponent( WriteMatches(exportName, exportedElements); WriteIsExactly(exportName, exportedElements); - // WriteChildren(exportName, exportedElements); - // WriteNamedChildren(exportName, exportedElements); - WriteDictionarySupport(exportName, exportedElements); // close class @@ -3047,7 +3079,7 @@ private void WriteElement( { foreach(ElementTypeChange change in changes) { - _writer.WriteIndented($"[DeclaredType(Type = typeof({change.DeclaredType.PropertyTypeString})"); + _writer.WriteIndented($"[DeclaredType(Type = typeof({change.DeclaredTypeReference.PropertyTypeString})"); _writer.Write($", Since = FhirRelease.{change.Since}"); _writer.WriteLine(")]"); } @@ -3120,7 +3152,7 @@ private void WriteElement( var changedDescription = $"The type of this element has changed over time. Make sure to use " + string.Join(", ", - changes.Select(change => $"{change.DeclaredType.PropertyTypeString} in {change.Since}")) + "."; + changes.Select(change => $"{change.DeclaredTypeReference.PropertyTypeString} {VersionChangeMessage(changes, change, false)}")) + "."; if (baseDescription is null) return changedDescription; @@ -3128,7 +3160,6 @@ private void WriteElement( return $"{baseDescription}. {changedDescription}"; } - private static PrimitiveTypeReference BuildTypeReferenceForCode(DefinitionCollection info, ElementDefinition element, Dictionary writtenValueSets) { if ((element.Binding?.Strength != Hl7.Fhir.Model.BindingStrength.Required) || @@ -3175,7 +3206,7 @@ TypeReference determineTypeReferenceForFhirElementName() { // If the element has a type change, we need to use DataType, to make // sure the property can capture all the types. - if(changes.All(c => c.DeclaredType is PrimitiveTypeReference)) + if(changes.All(c => c.DeclaredTypeReference is PrimitiveTypeReference)) { return PrimitiveTypeReference.PrimitiveType; } @@ -3183,14 +3214,7 @@ TypeReference determineTypeReferenceForFhirElementName() return ComplexTypeReference.DataTypeReference; } - // if (element.Path is "Element.id" or "Extension.url") - // { - // /* these two properties formally use a CQL primitive (at least, - // * that's how they are encoded in the StructureDefinition. */ - // return CqlTypeReference.SystemString; - // } - - var initialTypeName = getTypeNameFromElement(); + 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 @@ -3318,47 +3342,69 @@ private void WriteElementGettersAndSetters(ElementDefinition element, WrittenEle _writer.WriteLine(string.Empty); } - bool needsPrimitiveProperty = ei.PropertyType is + bool needsHelperProperty = ei.PropertyType is PrimitiveTypeReference or ListTypeReference { Element: PrimitiveTypeReference }; - if (needsPrimitiveProperty) + if (needsHelperProperty) { + // If the property has had multiple types over time, we need to generate a helper property for each type. if(_elementTypeChanges.TryGetValue(element.Path, out ElementTypeChange[]? changes)) { - var newest = changes.Select(c => c.Since).OrderDescending().First(); + var lastChange = changes.Last(); foreach(ElementTypeChange change in changes) { - WritePrimitiveHelperProperty(element, ei, change.DeclaredType, change.Since, usePrimaryName: change.Since == newest); + // The DeclaredType given by the maintainer is the type of the element, even if it repeats, + // so let's wrap that type in a list if applicable. + TypeReference propType = ei.PropertyType is ListTypeReference ? + new ListTypeReference(change.DeclaredTypeReference) : change.DeclaredTypeReference; + string helperName = change == lastChange + ? ei.PrimitiveHelperName! + : $"{ei.PrimitiveHelperName}{change.DeclaredTypeReference.Name.ToPascalCase()}"; + string versionsRemark = $"Use this property {VersionChangeMessage(changes, change, false)}."; + WritePrimitiveHelperProperty(element.Short, ei, propType, helperName, versionsRemark); } } else { - WritePrimitiveHelperProperty(element, ei); + WritePrimitiveHelperProperty(element.Short, ei, ei.PropertyType, ei.PrimitiveHelperName!); } } } - private void WritePrimitiveHelperProperty(ElementDefinition element, WrittenElementInfo ei, - TypeReference? propTypeOverride = null, FhirReleases.FhirSequenceCodes? since = null, bool usePrimaryName = true) + private void WritePrimitiveHelperProperty(string description, WrittenElementInfo ei, + TypeReference? propType, string helperPropName, string? versionsRemark = null) { - WriteIndentedComment(element.Short); - _writer.WriteLineIndented($"/// This uses the native .NET datatype, rather than the FHIR equivalent"); + string descriptionText = versionsRemark is null + ? description + : $"{description}. {versionsRemark}"; + WriteIndentedComment(descriptionText); + _writer.WriteLineIndented("/// This uses the native .NET datatype, rather than the FHIR equivalent"); _writer.WriteLineIndented("[IgnoreDataMember]"); - var helperPropName = (usePrimaryName || propTypeOverride is null) - ? ei.PrimitiveHelperName : $"{ei.PrimitiveHelperName}_{since}"; - - var propType = propTypeOverride ?? ei.PropertyType; switch (propType) { case PrimitiveTypeReference ptr: _writer.WriteLineIndented($"public {ptr.ConveniencePropertyTypeString} {helperPropName}"); OpenScope(); - _writer.WriteLineIndented($"get {{ return {ei.PropertyName} != null ? {ei.PropertyName}.Value : null; }}"); + _writer.WriteIndented($"get {{ return {ei.PropertyName} != null ? "); + string propAccess = versionsRemark is not null + ? $"(({mostGeneralValueAccessorType(ptr)}){ei.PropertyName})" + : ei.PropertyName; + + static string mostGeneralValueAccessorType(PrimitiveTypeReference ptr) + { + return ptr.ConveniencePropertyTypeString switch + { + "string" => "IValue", + _ => ptr.PropertyTypeString + }; + } + + _writer.WriteLine($"{propAccess}.Value : null; }}"); _writer.WriteLineIndented("set"); OpenScope(); @@ -3379,7 +3425,12 @@ private void WritePrimitiveHelperProperty(ElementDefinition element, WrittenElem _writer.WriteLineIndented($"public IEnumerable<{lptr.ConveniencePropertyTypeString}> {helperPropName}"); OpenScope(); - _writer.WriteLineIndented($"get {{ return {ei.PropertyName} != null ? {ei.PropertyName}.Select(elem => elem.Value) : null; }}"); + + _writer.WriteIndented($"get {{ return {ei.PropertyName} != null ? {ei.PropertyName}"); + if(versionsRemark is not null) + _writer.Write($".Cast<{lptr.PropertyTypeString}>()"); + _writer.WriteLine($".Select(elem => elem.Value) : null; }}"); + _writer.WriteLineIndented("set"); OpenScope(); @@ -3565,7 +3616,7 @@ internal static void BuildElementOptionalFlags( sb.Append("[AllowedTypes("); bool needsSep = false; - foreach ((string etName, ElementDefinition.TypeRefComponent elementType) in elementTypes) + foreach ((string etName, ElementDefinition.TypeRefComponent _) in elementTypes) { if (needsSep) { diff --git a/src/Microsoft.Health.Fhir.CodeGenCommon/Extensions/FhirNameConventionExtensions.cs b/src/Microsoft.Health.Fhir.CodeGenCommon/Extensions/FhirNameConventionExtensions.cs index 893967a04..54b7c976b 100644 --- a/src/Microsoft.Health.Fhir.CodeGenCommon/Extensions/FhirNameConventionExtensions.cs +++ b/src/Microsoft.Health.Fhir.CodeGenCommon/Extensions/FhirNameConventionExtensions.cs @@ -95,7 +95,7 @@ public static string ToPascalCase( return string.Join(joinDelimiter, word.Split(delimitersToRemove, _wordSplitOptions).Select(w => w.ToPascalCase(false))); } - return string.Concat(word.Substring(0, 1).ToUpperInvariant(), word.Substring(1)); + return string.Concat(word[..1].ToUpperInvariant(), word[1..]); } /// An extension method that converts an array of words each to PascalCase. @@ -163,10 +163,10 @@ public static string ToCamelCase( { // converting to pascal and changing the initial letter is faster than accumulating here string pc = word.ToPascalCase(removeDelimiters, joinDelimiter, delimitersToRemove); - return string.Concat(pc.Substring(0, 1).ToLowerInvariant(), pc.Substring(1)); + return string.Concat(pc[..1].ToLowerInvariant(), pc[1..]); } - return string.Concat(word.Substring(0, 1).ToLowerInvariant(), word.Substring(1)); + return string.Concat(word[..1].ToLowerInvariant(), word[1..]); } /// An extension method that converts an array of words each to camelCase. From ca8c52dd5361def09690465d93dc5867e4061a73 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Tue, 26 Nov 2024 17:58:58 +0100 Subject: [PATCH 11/52] Small omission (not relevant for the current set of shared POCOs) --- .../Language/Firely/CSharpFirely2.cs | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 9aadf8acc..db1ca69c5 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -3392,18 +3392,9 @@ private void WritePrimitiveHelperProperty(string description, WrittenElementInfo OpenScope(); _writer.WriteIndented($"get {{ return {ei.PropertyName} != null ? "); string propAccess = versionsRemark is not null - ? $"(({mostGeneralValueAccessorType(ptr)}){ei.PropertyName})" + ? $"(({MostGeneralValueAccessorType(ptr)}){ei.PropertyName})" : ei.PropertyName; - static string mostGeneralValueAccessorType(PrimitiveTypeReference ptr) - { - return ptr.ConveniencePropertyTypeString switch - { - "string" => "IValue", - _ => ptr.PropertyTypeString - }; - } - _writer.WriteLine($"{propAccess}.Value : null; }}"); _writer.WriteLineIndented("set"); OpenScope(); @@ -3428,7 +3419,7 @@ static string mostGeneralValueAccessorType(PrimitiveTypeReference ptr) _writer.WriteIndented($"get {{ return {ei.PropertyName} != null ? {ei.PropertyName}"); if(versionsRemark is not null) - _writer.Write($".Cast<{lptr.PropertyTypeString}>()"); + _writer.Write($".Cast<{MostGeneralValueAccessorType(lptr)}>()"); _writer.WriteLine($".Select(elem => elem.Value) : null; }}"); _writer.WriteLineIndented("set"); @@ -3453,6 +3444,16 @@ static string mostGeneralValueAccessorType(PrimitiveTypeReference ptr) } } + private static string MostGeneralValueAccessorType(PrimitiveTypeReference ptr) + { + return ptr.ConveniencePropertyTypeString switch + { + "string" => "IValue", + _ => ptr.PropertyTypeString + }; + } + + /// /// Determine the type name for an element that has child elements, based on the definition and /// the declared type. From fc30f6f3f3c70cdbbd61f731a0e6eb4ca81f01d3 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Wed, 27 Nov 2024 17:34:07 +0000 Subject: [PATCH 12/52] Refactored so we now have a function to generate AllowedType attributes --- .../Language/Firely/CSharpFirely2.cs | 120 +++++------------- .../Language/Firely/CSharpFirelyCommon.cs | 19 +++ .../Language/Firely/FirelyNetIG.cs | 2 +- .../Language/Firely/TypeReference.cs | 15 ++- 4 files changed, 68 insertions(+), 88 deletions(-) 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)) { From fd0cf74bacd473eb4069ee82e831ab0b1fb2418b Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Tue, 3 Dec 2024 18:20:20 +0100 Subject: [PATCH 13/52] Removed Bw-compat code that's not needed anymore now we do a major new release. --- .../Language/Firely/CSharpFirely2.cs | 395 +++--------------- .../Language/Firely/FirelyNetIG.cs | 4 +- 2 files changed, 55 insertions(+), 344 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index d262148b9..1c472bf8b 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -43,9 +43,6 @@ bool IFileHashTestable.GenerateHashesInsteadOfOutput public Type ConfigType => typeof(FirelyGenOptions); - /// Gets the FHIR primitive type map. - public Dictionary FhirPrimitiveTypeMap => CSharpFirelyCommon.PrimitiveTypeMap; - /// Gets a value indicating whether this language is idempotent. public bool IsIdempotent => false; @@ -74,7 +71,7 @@ bool IFileHashTestable.GenerateHashesInsteadOfOutput private string _exportDirectory = string.Empty; /// Structures to skip generating. - internal static readonly HashSet _exclusionSet = + internal static readonly HashSet ExclusionSet = [ /* These two types are generated as interfaces, so require special handling and * are not to be treated as normal resources. */ @@ -103,12 +100,12 @@ bool IFileHashTestable.GenerateHashesInsteadOfOutput /// /// List of types introduced in R5 that are retrospectively introduced in R3 and R4. /// - internal static readonly List _sharedR5DataTypes = + internal static readonly List SharedR5DataTypes = [ - new WrittenModelInfo { CsName = "BackboneType", FhirName = "BackboneType", IsAbstract = true }, - new WrittenModelInfo { CsName = "Base", FhirName = "Base", IsAbstract = true }, - new WrittenModelInfo { CsName = "DataType", FhirName = "DataType", IsAbstract = true }, - new WrittenModelInfo { CsName = "PrimitiveType", FhirName = "PrimitiveType", IsAbstract = true }, + new("BackboneType", "BackboneType", true), + new("Base", "Base",true), + new("DataType", "DataType", true), + new("PrimitiveType", "PrimitiveType", true), ]; /// @@ -212,38 +209,10 @@ bool IFileHashTestable.GenerateHashesInsteadOfOutput "http://hl7.org/fhir/ValueSet/codesystem-content-mode" ]; - /// - /// Information about special handling for specific value sets (for backwards compatibility of - /// generated code) - /// - internal record class ValueSetBehaviorOverrides - { - public required bool AllowShared { get; init; } - public required bool AllowInClasses { get; init; } - public HashSet ForceInClasses { get; init; } = []; - } - - /// - /// (Immutable) ValueSets that need special handling for backwards compatibility - /// - /// - /// Plan to remove in SDK v6.0 - /// - private static readonly Dictionary _valueSetBehaviorOverrides = new() - { - { "http://hl7.org/fhir/ValueSet/consent-data-meaning", new ValueSetBehaviorOverrides() { AllowShared = true, AllowInClasses = true, } }, - { "http://hl7.org/fhir/ValueSet/consent-provision-type", new ValueSetBehaviorOverrides() { AllowShared = true, AllowInClasses = true, }}, - { "http://hl7.org/fhir/ValueSet/encounter-status", new ValueSetBehaviorOverrides() { AllowShared = true, AllowInClasses = true, }}, - { "http://hl7.org/fhir/ValueSet/list-mode", new ValueSetBehaviorOverrides() { AllowShared = true, AllowInClasses = true, }}, - { "http://hl7.org/fhir/ValueSet/color-codes", new ValueSetBehaviorOverrides() { AllowShared = false, AllowInClasses = false, }}, - //{ "http://hl7.org/fhir/ValueSet/timezones", new ValueSetBehaviorOverrides() { AllowShared = true, ForceInClasses = ["Appointment"] }}, - //{ "http://hl7.org/fhir/ValueSet/timezones", new ValueSetBehaviorOverrides() { AllowShared = true, ForceInClasses = ["Appointment"] }}, - }; - /// /// List of all valuesets that we should publish as a shared Enum although there is only 1 reference to it. /// - internal static readonly List<(string, string)> _explicitSharedValueSets = + internal static readonly List<(string, string)> ExplicitSharedValueSets = [ // This enum should go to Template-binding.cs because otherwise it will introduce a breaking change. ("R4", "http://hl7.org/fhir/ValueSet/messageheader-response-request"), @@ -253,14 +222,13 @@ internal record class ValueSetBehaviorOverrides ("R5", "http://hl7.org/fhir/ValueSet/constraint-severity"), ]; - /// Gets the reserved words. /// The reserved words. private static readonly HashSet _reservedWords = []; - private static readonly Func SupportedResourcesFilter = wmi => !wmi.IsAbstract; - private static readonly Func FhirToCsFilter = wmi => !_excludeFromCsToFhir!.Contains(wmi.FhirName); - private static readonly Func CsToStringFilter = FhirToCsFilter; + private static readonly Func _supportedResourcesFilter = wmi => !wmi.IsAbstract; + private static readonly Func _fhirToCsFilter = wmi => !_excludeFromCsToFhir!.Contains(wmi.FhirName); + private static readonly Func _csToStringFilter = _fhirToCsFilter; private static readonly string[] _excludeFromCsToFhir = [ @@ -281,7 +249,7 @@ internal record class ValueSetBehaviorOverrides /// Some valuesets have names that are the same as element names or are just not nice - use this collection /// to change the name of the generated enum as required. /// - internal static readonly Dictionary _enumNamesOverride = new() + internal static readonly Dictionary EnumNamesOverride = new() { ["http://hl7.org/fhir/ValueSet/characteristic-combination"] = "CharacteristicCombinationCode", ["http://hl7.org/fhir/ValueSet/claim-use"] = "ClaimUseCode", @@ -298,7 +266,7 @@ internal record class ValueSetBehaviorOverrides private record ElementTypeChange(FhirReleases.FhirSequenceCodes Since, TypeReference DeclaredTypeReference); - private static readonly ElementTypeChange[] stringToMarkdown = + private static readonly ElementTypeChange[] _stringToMarkdown = [ new(FhirReleases.FhirSequenceCodes.STU3, PrimitiveTypeReference.String), new(FhirReleases.FhirSequenceCodes.R5, PrimitiveTypeReference.Markdown) @@ -358,10 +326,10 @@ static string replaceLastOccurrence(string source, string find, string replace) new(FhirReleases.FhirSequenceCodes.R5, PrimitiveTypeReference.Code) ], - ["ElementDefinition.constraint.requirements"] = stringToMarkdown, - ["ElementDefinition.binding.description"] = stringToMarkdown, - ["ElementDefinition.mapping.comment"] = stringToMarkdown, - ["CapabilityStatement.implementation.description"] = stringToMarkdown, + ["ElementDefinition.constraint.requirements"] = _stringToMarkdown, + ["ElementDefinition.binding.description"] = _stringToMarkdown, + ["ElementDefinition.mapping.comment"] = _stringToMarkdown, + ["CapabilityStatement.implementation.description"] = _stringToMarkdown, }; // ReSharper restore ArrangeObjectCreationWhenTypeNotEvident @@ -468,10 +436,8 @@ static string replaceLastOccurrence(string source, string find, string replace) private IDictionary? _cqlModelClassInfo = null; /// Export the passed FHIR version into the specified directory. + /// /// The information. - /// Information describing the server. - /// Options for controlling the operation. - /// Directory to write files. public void Export(object untypedOptions, DefinitionCollection info) { if (untypedOptions is not FirelyGenOptions options) @@ -578,7 +544,7 @@ public void Export(object untypedOptions, DefinitionCollection info) } AddModels(allComplexTypes, _info.ComplexTypesByName.Values); - AddModels(allComplexTypes, _sharedR5DataTypes); + AddModels(allComplexTypes, SharedR5DataTypes); if (options.ExportStructures.Contains(FhirArtifactClassEnum.Resource)) { @@ -632,16 +598,10 @@ private void ModifyDefinitionsForConsistency() // update the copied element to be the content element edContent.ElementId = "Binary.content"; edContent.Path = "Binary.content"; - edContent.Base = new() { Path = "Binary.content", Min = 0, Max = "1" }; + edContent.Base = new ElementDefinition.BaseComponent { Path = "Binary.content", Min = 0, Max = "1" }; edContent.cgSetFieldOrder(edData.cgFieldOrder(), edData.cgComponentFieldOrder()); - //edContent.RemoveExtension(CommonDefinitions.ExtUrlEdFieldOrder); - //edContent.RemoveExtension(CommonDefinitions.ExtUrlEdComponentFieldOrder); - - //edContent.AddExtension(CommonDefinitions.ExtUrlEdFieldOrder, new Integer(edData.cgFieldOrder())); - //edContent.AddExtension(CommonDefinitions.ExtUrlEdComponentFieldOrder, new Integer(edData.cgComponentFieldOrder())); - // add our element and track info, note that we are not increasing // the orders since they are duplicate elements from different versions _ = _info.TryInsertElement(sdBinary, edContent, false); @@ -714,12 +674,6 @@ private void ModifyDefinitionsForConsistency() edContentType.cgSetFieldOrder(7, 6); - //edContentType.RemoveExtension(CommonDefinitions.ExtUrlEdFieldOrder); - //edContentType.RemoveExtension(CommonDefinitions.ExtUrlEdComponentFieldOrder); - - //edContentType.AddExtension(CommonDefinitions.ExtUrlEdFieldOrder, new Integer(6), true); - //edContentType.AddExtension(CommonDefinitions.ExtUrlEdComponentFieldOrder, new Integer(6), true); - // add our element and track info _ = _info.TryInsertElement(sdSignature, edContentType, false); } @@ -787,12 +741,6 @@ private void ModifyDefinitionsForConsistency() edFocus.cgSetFieldOrder(123, 3); - //edFocus.RemoveExtension(CommonDefinitions.ExtUrlEdFieldOrder); - //edFocus.RemoveExtension(CommonDefinitions.ExtUrlEdComponentFieldOrder); - - //edFocus.AddExtension(CommonDefinitions.ExtUrlEdFieldOrder, new Integer(123), true); - //edFocus.AddExtension(CommonDefinitions.ExtUrlEdComponentFieldOrder, new Integer(3), true); - // TODO(ginoc): This insertion is currently pushing exclusionCriteria to Order=60 in file (componentOrder 5) - it should not // add our element and track info _ = _info.TryInsertElement(sdValueSet, edFocus, true); @@ -838,12 +786,6 @@ private void ModifyDefinitionsForConsistency() edXPath.cgSetFieldOrder(66, 8); } - //edXPath.RemoveExtension(CommonDefinitions.ExtUrlEdFieldOrder); - //edXPath.RemoveExtension(CommonDefinitions.ExtUrlEdComponentFieldOrder); - - //edXPath.AddExtension(CommonDefinitions.ExtUrlEdFieldOrder, new Integer(65), true); - //edXPath.AddExtension(CommonDefinitions.ExtUrlEdComponentFieldOrder, new Integer(7), true); - // add our element and track info _ = _info.TryInsertElement(sdElementDefinition, edXPath, true); } @@ -874,20 +816,6 @@ private void ModifyDefinitionsForConsistency() _ = _info.TryInsertElement(sdRelatedArtifact, edUrl, true); } - // need to modify the summary status of narrative elements - remove this for SDK 6.0 - if (_info.ComplexTypesByName.TryGetValue("Narrative", out StructureDefinition? sdNarrative)) - { - if (sdNarrative.cgTryGetElementById("Narrative.status", out ElementDefinition? edStatus)) - { - edStatus.IsSummary = true; - } - - if (sdNarrative.cgTryGetElementById("Narrative.div", out ElementDefinition? edDiv)) - { - edDiv.IsSummary = true; - } - } - // correct issues in FHIR 6.0.0-ballot2 if ((_info.MainPackageId == "hl7.fhir.r6.core") && (_info.MainPackageVersion == "6.0.0-ballot2")) { @@ -958,12 +886,12 @@ private void WriteModelInfo( // open class OpenScope(); - WriteSupportedResources(writtenResources.Values.Where(SupportedResourcesFilter)); + WriteSupportedResources(writtenResources.Values.Where(_supportedResourcesFilter)); WriteFhirVersion(); - WriteFhirToCs(writtenPrimitives.Values.Where(FhirToCsFilter), writtenComplexTypes.Values.Where(FhirToCsFilter), writtenResources.Values.Where(FhirToCsFilter)); - WriteCsToString(writtenPrimitives.Values.Where(CsToStringFilter), writtenComplexTypes.Values.Where(CsToStringFilter), writtenResources.Values.Where(CsToStringFilter)); + WriteFhirToCs(writtenPrimitives.Values.Where(_fhirToCsFilter), writtenComplexTypes.Values.Where(_fhirToCsFilter), writtenResources.Values.Where(_fhirToCsFilter)); + WriteCsToString(writtenPrimitives.Values.Where(_csToStringFilter), writtenComplexTypes.Values.Where(_csToStringFilter), writtenResources.Values.Where(_csToStringFilter)); WriteSearchParameters(); @@ -1231,8 +1159,7 @@ private void WriteSharedValueSets(GenSubset subset) // traverse all versions of all value sets foreach ((string unversionedUrl, string[] versions) in _info.ValueSetVersions.OrderBy(kvp => kvp.Key)) { - if (_exclusionSet.Contains(unversionedUrl) || - (_valueSetBehaviorOverrides.TryGetValue(unversionedUrl, out ValueSetBehaviorOverrides? oddInfo) && (oddInfo.AllowShared == false))) + if (ExclusionSet.Contains(unversionedUrl)) { continue; } @@ -1245,13 +1172,7 @@ private void WriteSharedValueSets(GenSubset subset) continue; } - // we never want to write limited expansions - //if (vs.IsLimitedExpansion()) - //{ - // continue; - //} - - IEnumerable coreBindings = _info.CoreBindingsForVs(vs.Url); + IEnumerable coreBindings = _info.CoreBindingsForVs(vs.Url).ToList(); Hl7.Fhir.Model.BindingStrength? strongestBinding = _info.StrongestBinding(coreBindings); if (strongestBinding != Hl7.Fhir.Model.BindingStrength.Required) @@ -1266,7 +1187,7 @@ required bindings anywhere in the data model. */ IEnumerable referencedBy = coreBindings.cgExtractBaseTypes(_info); - if ((referencedBy.Count() < 2) && !_explicitSharedValueSets.Contains((_info.FhirSequence.ToString(), vs.Url))) + if ((referencedBy.Count() < 2) && !ExplicitSharedValueSets.Contains((_info.FhirSequence.ToString(), vs.Url))) { /* ValueSets that are used in a single POCO are generated as a nested enum inside that * POCO, not here in the shared valuesets */ @@ -1417,7 +1338,7 @@ private void WriteResources( { foreach (StructureDefinition complex in complexes.OrderBy(c => c.Name)) { - if (_exclusionSet.Contains(complex.Name)) + if (ExclusionSet.Contains(complex.Name)) { continue; } @@ -1444,12 +1365,8 @@ private void WriteResource( writtenModels.Add( complex.Name, - new WrittenModelInfo() - { - FhirName = complex.Name, - CsName = $"{Namespace}.{exportName}", - IsAbstract = complex.Abstract == true, - }); + new WrittenModelInfo(FhirName: complex.Name, CsName: $"{Namespace}.{exportName}", + IsAbstract: complex.Abstract == true)); string filename = Path.Combine(_exportDirectory, "Generated", $"{exportName}.cs"); @@ -1505,7 +1422,7 @@ private void WriteComplexDataTypes( { foreach (StructureDefinition complex in complexes.OrderBy(c => c.Name)) { - if (_exclusionSet.Contains(complex.Name)) + if (ExclusionSet.Contains(complex.Name)) { continue; } @@ -1537,12 +1454,8 @@ private void WriteComplexDataType( writtenModels.Add( complex.Name, - new WrittenModelInfo() - { - FhirName = complex.Name, - CsName = $"{Namespace}.{exportName}", - IsAbstract = complex.Abstract == true, - }); + new WrittenModelInfo(FhirName: complex.Name, CsName: $"{Namespace}.{exportName}", + IsAbstract: complex.Abstract == true)); string filename = Path.Combine(_exportDirectory, "Generated", $"{exportName}.cs"); @@ -2522,39 +2435,6 @@ private void WriteBackboneComponent( WriteComponentComment(complex); - string explicitName = complex.cgExplicitName(); - - /* TODO(ginoc): 2024.06.28 - Special cases to remove in SDK 6.0 - * - Evidence.statistic.attributeEstimate.attributeEstimate the explicit name is duplicative and was not passed through. - * - Citation.citedArtifact.contributorship.summary had a generator prefix. - */ - switch (explicitName) - { - case "AttributeEstimateAttributeEstimate": - explicitName = "AttributeEstimate"; - break; - case "ContributorshipSummary": - explicitName = "CitedArtifactContributorshipSummary"; - break; - } - - // 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 (complex.Id.StartsWith("Citation") || complex.Id.StartsWith("Statistic") || complex.Id.StartsWith("DeviceDefinition")) - { - string parentName = complex.Id.Substring(0, complex.Id.IndexOf('.')); - var sillyBackboneName = complex.Id.Substring(parentName.Length); - explicitName = capitalizeThoseSillyBackboneNames(sillyBackboneName); - exportName = explicitName + "Component"; - } - // end of repair - */ - bool useConcatenationInName = complex.Structure.Name == "Citation"; string componentName = complex.Element.Path; @@ -2585,8 +2465,6 @@ private void WriteBackboneComponent( { WriteMatches(exportName, exportedElements); WriteIsExactly(exportName, exportedElements); - //WriteChildren(exportName, exportedElements); - //WriteNamedChildren(exportName, exportedElements); WriteDictionarySupport(exportName, exportedElements); } @@ -2596,55 +2474,10 @@ private void WriteBackboneComponent( // check for nested components foreach (ComponentDefinition component in complex.cgChildComponents(_info)) { - string componentExportName; string componentExplicitName = component.cgExplicitName(); - - if (string.IsNullOrEmpty(componentExplicitName)) - { - componentExportName = - $"{component.cgName(NamingConvention.PascalCase, useConcatenationInName, useConcatenationInName)}Component"; - } - else - { - /* TODO(ginoc): 2024.06.28 - Special cases to remove in SDK 6.0 - * - Evidence.statistic.attributeEstimate.attributeEstimate the explicit name is duplicative and was not passed through. - * - Citation.citedArtifact.contributorship.summary had a generator prefix. - */ - - switch (componentExplicitName) - { - case "AttributeEstimateAttributeEstimate": - componentExportName = "AttributeEstimateComponent"; - break; - case "ContributorshipSummary": - componentExportName = "CitedArtifactContributorshipSummaryComponent"; - break; - default: - // Consent.provisionActorComponent is explicit lower case... - componentExportName = $"{component.cgExplicitName()}Component"; - break; - } - - ///* TODO(ginoc): 2024.06.28 - Special cases to remove in SDK 6.0 - // * - Consent.provision is explicit lower case in R4B and earlier - // * - Consent.provision.actor is explicit lower case in R4B and earlier - // */ - //if (_info.FhirSequence < FhirReleases.FhirSequenceCodes.R5) - //{ - // switch (complex.Element.Path) - // { - // case "Consent.provision": - // componentExportName = "provisionComponent"; - // break; - // case "Consent.provision.actor": - // componentExportName = "provisionActorComponent"; - // break; - // case "Consent.provision.data": - // componentExportName = "provisionDataComponent"; - // break; - // } - //} - } + string componentExportName = string.IsNullOrEmpty(componentExplicitName) ? + $"{component.cgName(NamingConvention.PascalCase, useConcatenationInName, useConcatenationInName)}Component" + : $"{component.cgExplicitName()}Component"; WriteBackboneComponent( component, @@ -2689,16 +2522,6 @@ private void WriteEnums( WriteEnums(component, className, usedEnumNames, processedValueSets); } - // after processing, we need to look for value sets we are forcing in - foreach ((string url, ValueSetBehaviorOverrides behaviors) in _valueSetBehaviorOverrides) - { - if (behaviors.ForceInClasses.Contains(className) && - _info.TryExpandVs(url, out ValueSet? vs) && - !processedValueSets.Contains(vs.Url)) - { - WriteEnum(vs, className, usedEnumNames); - } - } } /// Writes a value set as an enum. @@ -2712,27 +2535,12 @@ private bool WriteEnum( HashSet usedEnumNames, bool silent = false) { - bool passes = false; - - if (_valueSetBehaviorOverrides.TryGetValue(vs.Url, out ValueSetBehaviorOverrides? behaviors)) - { - if (behaviors.ForceInClasses.Contains(className)) - { - // skip other checks - passes = true; - } - else if (behaviors.AllowInClasses == false) - { - return false; - } - } - - if (passes || _writtenValueSets.ContainsKey(vs.Url)) + if (_writtenValueSets.ContainsKey(vs.Url)) { return true; } - if (passes || _exclusionSet.Contains(vs.Url)) + if (ExclusionSet.Contains(vs.Url)) { return false; } @@ -2754,7 +2562,7 @@ private bool WriteEnum( // Enums and their containing classes cannot have the same name, // so we have to correct these here - if (_enumNamesOverride.TryGetValue(vs.Url, out var replacementName)) + if (EnumNamesOverride.TryGetValue(vs.Url, out var replacementName)) { nameSanitized = replacementName; } @@ -2770,16 +2578,12 @@ private bool WriteEnum( { _writtenValueSets.Add( vs.Url, - new WrittenValueSetInfo() - { - ClassName = className, - ValueSetName = nameSanitized, - }); + new WrittenValueSetInfo(className, nameSanitized)); return true; } - IEnumerable referencedCodeSystems = vs.cgReferencedCodeSystems(); + IEnumerable referencedCodeSystems = vs.cgReferencedCodeSystems().ToList(); if (referencedCodeSystems.Count() == 1) { @@ -2796,56 +2600,7 @@ private bool WriteEnum( $"(systems: {referencedCodeSystems.Count()})"); } - /* TODO(ginoc): 2024.07.01 - Special cases to remove in SDK 6.0 - * - ValueSet http://hl7.org/fhir/ValueSet/item-type used to enumerate non-selectable: 'question' - * - ValueSet http://hl7.org/fhir/ValueSet/v3-ActInvoiceGroupCode in STU3 used to enumerate non-selectable: '_ActInvoiceInterGroupCode' and '_ActInvoiceRootGroupCode' - */ - switch (vs.Url) - { - case "http://hl7.org/fhir/ValueSet/item-type": - { - if (!vs.Expansion.Contains.Any(vsContains => vsContains.Code == "question")) - { - vs.Expansion.Contains.Insert(2, new ValueSet.ContainsComponent() - { - System = "http://hl7.org/fhir/item-type", - Code = "question", - Display = "Question", - }); - } - } - break; - - case "http://hl7.org/fhir/ValueSet/v3-ActInvoiceGroupCode": - { - // only care about the version present in STU3 - if (vs.Version == "2014-03-26") - { - if (!vs.Expansion.Contains.Any(vsContains => vsContains.Code == "_ActInvoiceInterGroupCode")) - { - vs.Expansion.Contains.Insert(0, new ValueSet.ContainsComponent() - { - System = "http://hl7.org/fhir/v3/ActCode", - Code = "_ActInvoiceInterGroupCode", - Display = "ActInvoiceInterGroupCode", - }); - } - - if (!vs.Expansion.Contains.Any(vsContains => vsContains.Code == "_ActInvoiceRootGroupCode")) - { - vs.Expansion.Contains.Insert(8, new ValueSet.ContainsComponent() - { - System = "http://hl7.org/fhir/v3/ActCode", - Code = "_ActInvoiceRootGroupCode", - Display = "ActInvoiceRootGroupCode", - }); - } - } - } - break; - } - - var defaultSystem = GetDefaultCodeSystem(concepts); + string defaultSystem = GetDefaultCodeSystem(concepts); _writer.WriteLineIndented($"[FhirEnumeration(\"{name}\", \"{vs.Url}\", \"{defaultSystem}\")]"); @@ -2905,11 +2660,7 @@ private bool WriteEnum( _writtenValueSets.Add( vs.Url, - new WrittenValueSetInfo() - { - ClassName = className, - ValueSetName = nameSanitized, - }); + new WrittenValueSetInfo(className, nameSanitized)); return true; } @@ -3161,23 +2912,20 @@ private void WriteElement( return baseDescription; } - var changedDescription = $"The type of this element has changed over time. Make sure to use " - + string.Join(", ", - changes.Select(change => $"{change.DeclaredTypeReference.PropertyTypeString} {VersionChangeMessage(changes, change, false)}")) + "."; + string changedDescription = $"The type of this element has changed over time. Make sure to use " + + string.Join(", ", + changes.Select(change => $"{change.DeclaredTypeReference.PropertyTypeString} {VersionChangeMessage(changes, change, false)}")) + "."; - if (baseDescription is null) - return changedDescription; - - return $"{baseDescription}. {changedDescription}"; + return baseDescription is null ? changedDescription : $"{baseDescription}. {changedDescription}"; } 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)) || - _exclusionSet.Contains(vs.Url) || + ExclusionSet.Contains(vs.Url) || (_codedElementOverrides.Contains(element.Path) && info.FhirSequence >= FhirReleases.FhirSequenceCodes.R4) || - !writtenValueSets.TryGetValue(vs.Url, out WrittenValueSetInfo vsInfo)) + !writtenValueSets.TryGetValue(vs.Url, out WrittenValueSetInfo? vsInfo)) { return (null, null); } @@ -3462,21 +3210,6 @@ private static string BuildTypeNameForNestedComplexType(ElementDefinition ed, st if (!string.IsNullOrEmpty(explicitTypeName)) { - /* TODO(ginoc): 2024.06.28 - Special cases to remove in SDK 6.0 - * - Evidence.statistic.attributeEstimate.attributeEstimate the explicit name is duplicative and was not passed through. - * - Citation.citedArtifact.contributorship.summary had a generator prefix. - */ - - switch (explicitTypeName) - { - case "AttributeEstimateAttributeEstimate": - explicitTypeName = "AttributeEstimate"; - break; - case "ContributorshipSummary": - explicitTypeName = "CitedArtifactContributorshipSummary"; - break; - } - string parentName = type.Substring(0, type.IndexOf('.')); return $"{parentName}" + $".{explicitTypeName}" + @@ -3635,7 +3368,7 @@ private void WritePrimitiveTypes( foreach (StructureDefinition primitive in primitives.OrderBy(sd => sd.Name)) { - if (_exclusionSet.Contains(primitive.Name)) + if (ExclusionSet.Contains(primitive.Name)) { continue; } @@ -3674,12 +3407,7 @@ private void WritePrimitiveType( writtenModels.Add( primitive.Name, - new WrittenModelInfo() - { - FhirName = primitive.Name, - CsName = $"{Namespace}.{exportName}", - IsAbstract = false, // no abstract primitives - }); + new WrittenModelInfo(FhirName: primitive.Name, CsName: $"{Namespace}.{exportName}", IsAbstract: false)); string filename = Path.Combine(_exportDirectory, "Generated", $"{exportName}.cs"); @@ -4028,21 +3756,12 @@ WrittenModelInfo CreateWMI(StructureDefinition t) exportName = t.Name.ToPascalCase(); } - return new WrittenModelInfo() - { - FhirName = t.Name, - CsName = $"{Namespace}.{exportName}", - IsAbstract = t.Abstract == true, - }; + return new WrittenModelInfo(FhirName: t.Name, CsName: $"{Namespace}.{exportName}", IsAbstract: t.Abstract == true); } } /// Information about a written value set. - internal struct WrittenValueSetInfo - { - internal string ClassName; - internal string ValueSetName; - } + internal record WrittenValueSetInfo(string ClassName, string ValueSetName); /// Information about the written element. internal record WrittenElementInfo( @@ -4050,16 +3769,8 @@ internal record WrittenElementInfo( string FhirElementPath, string PropertyName, TypeReference PropertyType, - string? PrimitiveHelperName) - { - //public string FhirElementName => FhirElementPath.Split('.').Last(); - } + string? PrimitiveHelperName); /// Information about the written model. - internal struct WrittenModelInfo - { - internal string FhirName; - internal string CsName; - internal bool IsAbstract; - } + internal record WrittenModelInfo(string FhirName, string CsName, bool IsAbstract); } diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/FirelyNetIG.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/FirelyNetIG.cs index 5083edfc8..1fbf6c5f6 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/FirelyNetIG.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/FirelyNetIG.cs @@ -724,7 +724,7 @@ private void ProcessAndWriteValueSets() foreach ((string unversionedUrl, string[] versions) in _info.ValueSetVersions.OrderBy(kvp => kvp.Key)) { // check for exclusions - if (CSharpFirely2._exclusionSet.Contains(unversionedUrl)) + if (CSharpFirely2.ExclusionSet.Contains(unversionedUrl)) { continue; } @@ -808,7 +808,7 @@ private void ProcessAndWriteValueSet( // Enums and their containing classes cannot have the same name, // so we have to correct these here - if (CSharpFirely2._enumNamesOverride.TryGetValue(vs.Url, out var replacementName)) + if (CSharpFirely2.EnumNamesOverride.TryGetValue(vs.Url, out var replacementName)) { nameSanitized = replacementName; } From 51998fb4e6332809a7806655e075a26d109b3458 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Tue, 3 Dec 2024 19:29:34 +0100 Subject: [PATCH 14/52] Changes to how we generate SearchParamDefinition since it moved to Base. --- .../Language/Firely/CSharpFirely2.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index d262148b9..5b6233ca2 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -182,10 +182,9 @@ bool IFileHashTestable.GenerateHashesInsteadOfOutput [ "http://hl7.org/fhir/ValueSet/publication-status", "http://hl7.org/fhir/ValueSet/FHIR-version", - - // Doesn't strictly need to be in base (but in conformance), - // but we used to generate it for base, so I am keeping it that way. + "http://hl7.org/fhir/ValueSet/search-param-type", "http://hl7.org/fhir/ValueSet/filter-operator", + "http://hl7.org/fhir/ValueSet/version-independent-all-resource-types" ]; /// @@ -195,7 +194,6 @@ bool IFileHashTestable.GenerateHashesInsteadOfOutput [ "http://hl7.org/fhir/ValueSet/capability-statement-kind", "http://hl7.org/fhir/ValueSet/binding-strength", - "http://hl7.org/fhir/ValueSet/search-param-type", // These are necessary for CapabilityStatement/CapabilityStatement2 // CapStat2 has disappeared in ballot, if that becomes final, @@ -1039,7 +1037,7 @@ private void WriteSearchParameters() .Split(_splitChars, StringSplitOptions.RemoveEmptyEntries) .Where(s => s.StartsWith(complex.Name + ".", StringComparison.Ordinal)); - path = "\"" + string.Join("\", \"", split) + "\", "; + path = "\"" + string.Join("\", \"", split) + "\""; } string target; @@ -1054,7 +1052,7 @@ private void WriteSearchParameters() foreach (string t in sp.Target.Select(t => t.GetLiteral()!)) { - sc.Add("ResourceType." + t); + sc.Add("VersionIndependentResourceTypesAll." + t); } // HACK: for http://hl7.org/fhir/SearchParameter/clinical-encounter, @@ -1083,7 +1081,7 @@ private void WriteSearchParameters() } } - target = ", Target = new ResourceType[] { " + string.Join(", ", sc) + ", }"; + target = $", Target = [{string.Join(", ", sc)}]"; } string xpath = string.IsNullOrEmpty(sp.cgXPath()) ? string.Empty : ", XPath = \"" + sp.cgXPath() + "\""; @@ -1103,7 +1101,7 @@ private void WriteSearchParameters() $" Description = @\"{SanitizeForMarkdown(description)}\"," : $" Description = new Markdown(@\"{SanitizeForMarkdown(description)}\"),") + $" Type = SearchParamType.{searchType}," + - $" Path = new string[] {{ {path}}}" + + $" Path = [{path}]" + target + xpath + expression + From f7836c49f47b987a05afff95662a9d50c2fdc073 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Wed, 4 Dec 2024 14:07:36 +0100 Subject: [PATCH 15/52] Added code to remove certain searparam targets. No longer needed to manually correct these. --- .../Language/Firely/CSharpFirely2.cs | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 5b6233ca2..d041e1417 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -100,6 +100,21 @@ bool IFileHashTestable.GenerateHashesInsteadOfOutput "http://hl7.org/fhir/ValueSet/mimetypes", ]; + private static readonly Dictionary<(string,string), VersionIndependentResourceTypesAll> _searchParamDefsTargetRemovals = new() + { + [("DiagnosticReport", "encounter")] = VersionIndependentResourceTypesAll.EpisodeOfCare, + [("RiskAssessment", "encounter")] = VersionIndependentResourceTypesAll.EpisodeOfCare, + [("List", "encounter")] = VersionIndependentResourceTypesAll.EpisodeOfCare, + [("VisionPrescription", "encounter")] = VersionIndependentResourceTypesAll.EpisodeOfCare, + [("ServiceRequest", "encounter")] = VersionIndependentResourceTypesAll.EpisodeOfCare, + [("Flag", "encounter")] = VersionIndependentResourceTypesAll.EpisodeOfCare, + [("Observation", "encounter")] = VersionIndependentResourceTypesAll.EpisodeOfCare, + [("NutritionOrder", "encounter")] = VersionIndependentResourceTypesAll.EpisodeOfCare, + [("Composition", "encounter")] = VersionIndependentResourceTypesAll.EpisodeOfCare, + [("DeviceRequest", "encounter")] = VersionIndependentResourceTypesAll.EpisodeOfCare, + [("Procedure", "encounter")] = VersionIndependentResourceTypesAll.EpisodeOfCare, + }; + /// /// List of types introduced in R5 that are retrospectively introduced in R3 and R4. /// @@ -1048,12 +1063,8 @@ private void WriteSearchParameters() } else { - SortedSet sc = []; - - foreach (string t in sp.Target.Select(t => t.GetLiteral()!)) - { - sc.Add("VersionIndependentResourceTypesAll." + t); - } + List sc = + [..sp.Target.Where(t => t != null).Cast()]; // HACK: for http://hl7.org/fhir/SearchParameter/clinical-encounter, // none of the base resources have EpisodeOfCare as target, except @@ -1069,19 +1080,29 @@ private void WriteSearchParameters() { if (complex.Name != "Procedure" && complex.Name != "DeviceRequest") { - sc.Remove("ResourceType.EpisodeOfCare"); + sc.Remove(VersionIndependentResourceTypesAll.EpisodeOfCare); } } else { if (complex.Name != "DocumentReference") { - sc.Remove("ResourceType.EpisodeOfCare"); + sc.Remove(VersionIndependentResourceTypesAll.EpisodeOfCare); } } } - target = $", Target = [{string.Join(", ", sc)}]"; + if (_info.FhirSequence != FhirReleases.FhirSequenceCodes.STU3) + { + if(_searchParamDefsTargetRemovals.TryGetValue((complex.Name,sp.Name), out var targetRemoval)) + { + sc.Remove(targetRemoval); + } + } + + var targetStrings = sc.Select(s => $"VersionIndependentResourceTypesAll.{s.GetLiteral()}") + .Order(); + target = $", Target = [{string.Join(", ", targetStrings)}]"; } string xpath = string.IsNullOrEmpty(sp.cgXPath()) ? string.Empty : ", XPath = \"" + sp.cgXPath() + "\""; From 6d004b32074b20cacabce84d7a0d8622cc042617 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Thu, 5 Dec 2024 11:15:03 +0100 Subject: [PATCH 16/52] Introduction of CompareChildren instead of Matches/IsExactly. Also made dictionary-like operations public. --- .../Language/Firely/CSharpFirely2.cs | 92 +++++-------------- 1 file changed, 25 insertions(+), 67 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index d262148b9..9b82fed26 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2069,8 +2069,7 @@ private void WriteComponent( WriteDeepCopy(exportName); } - WriteMatches(exportName, exportedElements); - WriteIsExactly(exportName, exportedElements); + WriteCompareChildren(exportName, exportedElements); WriteDictionarySupport(exportName, exportedElements); // close class @@ -2135,7 +2134,7 @@ private void WriteDictionarySupport(string exportName, List WriteDictionaryPairs(exportName, exportedElements); } - private string NullCheck(string propertyName, bool isList) => + private static string NullCheck(string propertyName, bool isList) => propertyName + (isList ? "?.Any() == true" : " is not null"); private void WriteDictionaryPairs(string exportName, List exportedElements) @@ -2151,9 +2150,9 @@ private void WriteDictionaryPairs(string exportName, List ex return; } - _writer.WriteLineIndented("internal protected override IEnumerable> GetElementPairs()"); + _writer.WriteLineIndented("public override IEnumerable> EnumerateElements()"); OpenScope(); - _writer.WriteLineIndented("foreach (var kvp in base.GetElementPairs()) yield return kvp;"); + _writer.WriteLineIndented("foreach (var kvp in base.EnumerateElements()) yield return kvp;"); foreach (WrittenElementInfo info in exportedElements) { @@ -2179,7 +2178,7 @@ private void WriteDictionaryTryGetValue(string exportName, List _writer.WriteLineIndented("return base.SetValue(key, value);"); } - /// Writes the matches. - /// Name of the exported class. + /// Writes the PairwiseEquality. + /// Name of the export. /// The exported elements. - private void WriteMatches( + private void WriteCompareChildren( string exportName, List exportedElements) { - _writer.WriteLineIndented("///"); - - // Base implementation differs from subclasses. + // Base implementation is hand-written code in a separate partial class. if (exportName == "Base") { - _writer.WriteLineIndented("public virtual bool Matches(IDeepComparable other) => other is Base;"); - _writer.WriteLine(string.Empty); return; } - if (exportName == "PrimitiveType") - { - _writer.WriteLineIndented("public override bool Matches(IDeepComparable other) => IsExactly(other);"); - _writer.WriteLine(string.Empty); - return; - } + _writer.WriteLineIndented("public override bool CompareChildren(Base other, IEqualityComparer comparer)"); - _writer.WriteLineIndented("public override bool Matches(IDeepComparable other)"); OpenScope(); _writer.WriteLineIndented($"var otherT = other as {exportName};"); _writer.WriteLineIndented("if(otherT == null) return false;"); _writer.WriteLine(string.Empty); - _writer.WriteLineIndented("if(!base.Matches(otherT)) return false;"); + + _writer.WriteLineIndented("if(!base.CompareChildren(otherT, comparer)) return false;"); foreach (WrittenElementInfo info in exportedElements) { - if (info.PropertyType is CqlTypeReference) + if(info.PropertyType is CqlTypeReference) { _writer.WriteLineIndented( $"if( {info.PropertyName} != otherT.{info.PropertyName} )" + $" return false;"); } + else if (info.PropertyType is ListTypeReference) + { + _writer.WriteLineIndented( + $"if(!comparer.ListEquals({info.PropertyName}, otherT.{info.PropertyName}))" + + $" return false;"); + } else + { _writer.WriteLineIndented( - $"if( !DeepComparable.Matches({info.PropertyName}, otherT.{info.PropertyName}))" + - $" return false;"); - } - - _writer.WriteLine(string.Empty); - _writer.WriteLineIndented("return true;"); - - CloseScope(); - } - - /// Writes the is exactly. - /// Name of the export. - /// The exported elements. - private void WriteIsExactly( - string exportName, - List exportedElements) - { - // Base implementation differs from subclasses. - if (exportName == "Base") - { - _writer.WriteLineIndented("public virtual bool IsExactly(IDeepComparable other) => other is Base;"); - _writer.WriteLine(string.Empty); - return; - } - - _writer.WriteLineIndented("public override bool IsExactly(IDeepComparable other)"); - - OpenScope(); - _writer.WriteLineIndented($"var otherT = other as {exportName};"); - _writer.WriteLineIndented("if(otherT == null) return false;"); - _writer.WriteLine(string.Empty); - - _writer.WriteLineIndented("if(!base.IsExactly(otherT)) return false;"); - - foreach (WrittenElementInfo info in exportedElements) - { - _writer.WriteLineIndented( - info.PropertyType is CqlTypeReference - ? $"if({info.PropertyName} != otherT.{info.PropertyName}) return false;" - : $"if( !DeepComparable.IsExactly({info.PropertyName}, otherT.{info.PropertyName}))" + - $" return false;"); + $"if(!comparer.Equals({info.PropertyName}, otherT.{info.PropertyName}))" + + $" return false;"); + } } _writer.WriteLine(string.Empty); @@ -2583,8 +2542,7 @@ private void WriteBackboneComponent( if (exportedElements.Count > 0) { - WriteMatches(exportName, exportedElements); - WriteIsExactly(exportName, exportedElements); + WriteCompareChildren(exportName, exportedElements); //WriteChildren(exportName, exportedElements); //WriteNamedChildren(exportName, exportedElements); WriteDictionarySupport(exportName, exportedElements); From 01f4b9fdfab091815c0cea7efe0bdb6715408adc Mon Sep 17 00:00:00 2001 From: Kasdejong Date: Tue, 17 Dec 2024 15:54:36 +0100 Subject: [PATCH 17/52] Made changes to the deep copy signatures --- .run/firely-all.run.xml | 11 ++++ .../Language/Firely/CSharpFirely2.cs | 33 ++++++---- .../Properties/launchSettings.json | 64 +++++++++---------- 3 files changed, 62 insertions(+), 46 deletions(-) create mode 100644 .run/firely-all.run.xml diff --git a/.run/firely-all.run.xml b/.run/firely-all.run.xml new file mode 100644 index 000000000..a2c5c4a33 --- /dev/null +++ b/.run/firely-all.run.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index de5d94099..54ffa2121 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2290,7 +2290,7 @@ private void WriteCopyTo( List exportedElements) { var specifier = exportName == "Base" ? "virtual" : "override"; - _writer.WriteLineIndented($"public {specifier} IDeepCopyable CopyTo(IDeepCopyable other)"); + _writer.WriteLineIndented($"protected internal {specifier} void CopyToInternal(Base other)"); OpenScope(); _writer.WriteLineIndented($"var dest = other as {exportName};"); _writer.WriteLine(string.Empty); @@ -2307,10 +2307,14 @@ private void WriteCopyTo( _writer.WriteLineIndented("dest.annotations.AddRange(annotations);"); _writer.DecreaseIndent(); _writer.WriteLine(string.Empty); + _writer.WriteLineIndented("if (Overflow.Any())"); + _writer.IncreaseIndent(); + _writer.WriteLineIndented("Overflow.CopyToInternal(dest.Overflow);"); + _writer.DecreaseIndent(); } else { - _writer.WriteLineIndented("base.CopyTo(dest);"); + _writer.WriteLineIndented("base.CopyToInternal(dest);"); } foreach (WrittenElementInfo info in exportedElements) @@ -2319,7 +2323,7 @@ private void WriteCopyTo( { _writer.WriteLineIndented( $"if({info.PropertyName}.Any())" + - $" dest.{info.PropertyName} = new {info.PropertyType.PropertyTypeString}({info.PropertyName}.DeepCopy());"); + $" dest.{info.PropertyName} = new {info.PropertyType.PropertyTypeString}({info.PropertyName}.DeepCopyInternal());"); } else { @@ -2327,15 +2331,13 @@ private void WriteCopyTo( $"if({info.PropertyName} != null) dest.{info.PropertyName} = " + (info.PropertyType is CqlTypeReference ? $"{info.PropertyName};" : - $"({info.PropertyType.PropertyTypeString}){info.PropertyName}.DeepCopy();")); + $"({info.PropertyType.PropertyTypeString}){info.PropertyName}.DeepCopyInternal();")); } } if (exportName == "PrimitiveType") _writer.WriteLineIndented("if (ObjectValue != null) dest.ObjectValue = ObjectValue;"); - _writer.WriteLineIndented("return dest;"); - CloseScope(); } @@ -2347,17 +2349,16 @@ private void WriteDeepCopy( // Base implementation differs from subclasses. if (exportName == "Base") { - _writer.WriteLineIndented("public virtual IDeepCopyable DeepCopy() =>"); - _writer.IncreaseIndent(); - _writer.WriteLineIndented("CopyTo((IDeepCopyable)Activator.CreateInstance(GetType())!);"); - _writer.DecreaseIndent(); + _writer.WriteLineIndented("protected internal abstract Base DeepCopyInternal();"); _writer.WriteLine(string.Empty); return; } - _writer.WriteLineIndented("public override IDeepCopyable DeepCopy()"); + _writer.WriteLineIndented("protected internal override Base DeepCopyInternal()"); OpenScope(); - _writer.WriteLineIndented($"return CopyTo(new {exportName}());"); + _writer.WriteLineIndented($"var instance = new {exportName}();"); + _writer.WriteLineIndented("CopyToInternal(instance);"); + _writer.WriteLineIndented("return instance;"); CloseScope(); } @@ -2379,9 +2380,11 @@ private void WriteConstrainedQuantity( if(complex.Structure.Abstract != true) WritePropertyTypeName(complex.Structure.Name); - _writer.WriteLineIndented("public override IDeepCopyable DeepCopy()"); + _writer.WriteLineIndented("protected internal override Base DeepCopyInternal()"); OpenScope(); - _writer.WriteLineIndented($"return CopyTo(new {exportName}());"); + _writer.WriteLineIndented($"var instance = new {exportName}();"); + _writer.WriteLineIndented("CopyToInternal(instance);"); + _writer.WriteLineIndented("return instance;"); CloseScope(); //_writer.WriteLineIndented("// TODO: Add code to enforce these constraints:"); @@ -3495,6 +3498,8 @@ private void WritePrimitiveType( _writer.WriteLineIndented("set { ObjectValue = value; OnPropertyChanged(\"Value\"); }"); CloseScope(); + WriteDeepCopy(exportName); + // close class CloseScope(); diff --git a/src/fhir-codegen/Properties/launchSettings.json b/src/fhir-codegen/Properties/launchSettings.json index 0ad39befd..7093e8b35 100644 --- a/src/fhir-codegen/Properties/launchSettings.json +++ b/src/fhir-codegen/Properties/launchSettings.json @@ -7,157 +7,157 @@ }, "Firely 5.x Base": { "commandName": "Project", - "commandLineArgs": "generate CSharpFirely2 --output-path ..\\..\\..\\firely-net-sdk\\src\\Hl7.Fhir.Base\\Model -p hl7.fhir.r5.core#5.0.0 -p hl7.fhir.r5.expansions#5.0.0 --subset base", + "commandLineArgs": "generate CSharpFirely2 --output-path ../../../firely-net-sdk/src/Hl7.Fhir.Base/Model -p hl7.fhir.r5.core#5.0.0 -p hl7.fhir.r5.expansions#5.0.0 --subset base", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Firely 5.x Conformance": { "commandName": "Project", - "commandLineArgs": "generate CSharpFirely2 --output-path ..\\..\\..\\firely-net-sdk\\src\\Hl7.Fhir.Conformance\\Model -p hl7.fhir.r5.core#5.0.0 -p hl7.fhir.r5.expansions#5.0.0 --subset conformance", + "commandLineArgs": "generate CSharpFirely2 --output-path ../../../firely-net-sdk/src/Hl7.Fhir.Conformance/Model -p hl7.fhir.r5.core#5.0.0 -p hl7.fhir.r5.expansions#5.0.0 --subset conformance", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Firely 5.x STU3": { "commandName": "Project", - "commandLineArgs": "generate CSharpFirely2 --output-path ..\\..\\..\\firely-net-sdk\\src\\Hl7.Fhir.STU3\\Model -p hl7.fhir.r3.core#3.0.2 -p hl7.fhir.r3.expansions#3.0.2 --subset satellite", + "commandLineArgs": "generate CSharpFirely2 --output-path ../../../firely-net-sdk/src/Hl7.Fhir.STU3/Model -p hl7.fhir.r3.core#3.0.2 -p hl7.fhir.r3.expansions#3.0.2 --subset satellite", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Firely 5.x R4": { "commandName": "Project", - "commandLineArgs": "generate CSharpFirely2 --output-path ..\\..\\..\\firely-net-sdk\\src\\Hl7.Fhir.R4\\Model -p hl7.fhir.r4.core#4.0.1 -p hl7.fhir.r4.expansions#4.0.1 --subset satellite --cql-model Fhir401", + "commandLineArgs": "generate CSharpFirely2 --output-path ../../../firely-net-sdk/src/Hl7.Fhir.R4/Model -p hl7.fhir.r4.core#4.0.1 -p hl7.fhir.r4.expansions#4.0.1 --subset satellite --cql-model Fhir401", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Firely 5.x R4B": { "commandName": "Project", - "commandLineArgs": "generate CSharpFirely2 --output-path ..\\..\\..\\firely-net-sdk\\src\\Hl7.Fhir.R4B\\Model -p hl7.fhir.r4b.core#4.3.0 -p hl7.fhir.r4b.expansions#4.3.0 --subset satellite", + "commandLineArgs": "generate CSharpFirely2 --output-path ../../../firely-net-sdk/src/Hl7.Fhir.R4B/Model -p hl7.fhir.r4b.core#4.3.0 -p hl7.fhir.r4b.expansions#4.3.0 --subset satellite", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Firely 5.x R5": { "commandName": "Project", - "commandLineArgs": "generate CSharpFirely2 --output-path ..\\..\\..\\firely-net-sdk\\src\\Hl7.Fhir.R5\\Model -p hl7.fhir.r5.core#5.0.0 -p hl7.fhir.r5.expansions#5.0.0 --subset satellite", + "commandLineArgs": "generate CSharpFirely2 --output-path ../../../firely-net-sdk/src/Hl7.Fhir.R5/Model -p hl7.fhir.r5.core#5.0.0 -p hl7.fhir.r5.expansions#5.0.0 --subset satellite", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Firely 5.x R6": { "commandName": "Project", - "commandLineArgs": "generate CSharpFirely2 --output-path ..\\..\\..\\firely-net-sdk\\src\\Hl7.Fhir.R6\\Model -p hl7.fhir.r6.core#6.0.0-ballot2 -p hl7.fhir.r6.expansions#6.0.0-ballot2 --subset satellite", + "commandLineArgs": "generate CSharpFirely2 --output-path ../../../firely-net-sdk/src/Hl7.Fhir.R6/Model -p hl7.fhir.r6.core#6.0.0-ballot2 -p hl7.fhir.r6.expansions#6.0.0-ballot2 --subset satellite", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Firely IG Backport": { "commandName": "Project", - "commandLineArgs": "generate FirelyNetIg --output-path ..\\..\\temp\\firely-ig -p hl7.fhir.uv.subscriptions-backport#1.1.0 --fhir-version 4.0.1 --auto-load-expansions --resolve-dependencies true --include-experimental", + "commandLineArgs": "generate FirelyNetIg --output-path ../../temp/firely-ig -p hl7.fhir.uv.subscriptions-backport#1.1.0 --fhir-version 4.0.1 --auto-load-expansions --resolve-dependencies true --include-experimental", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Firely IG US Core": { "commandName": "Project", - "commandLineArgs": "generate FirelyNetIg --output-path ..\\..\\temp\\firely-ig -p hl7.fhir.us.core#6.1.0 --auto-load-expansions --resolve-dependencies true --include-experimental", + "commandLineArgs": "generate FirelyNetIg --output-path ../../temp/firely-ig -p hl7.fhir.us.core#6.1.0 --auto-load-expansions --resolve-dependencies true --include-experimental", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Firely IG Extensions Pack": { "commandName": "Project", - "commandLineArgs": "generate FirelyNetIg --output-path ..\\..\\temp\\firely-ig -p hl7.fhir.uv.extensions.r4#latest --auto-load-expansions --resolve-dependencies true --include-experimental", + "commandLineArgs": "generate FirelyNetIg --output-path ../../temp/firely-ig -p hl7.fhir.uv.extensions.r4#latest --auto-load-expansions --resolve-dependencies true --include-experimental", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Firely IG NDH": { "commandName": "Project", - "commandLineArgs": "generate FirelyNetIg --output-path ..\\..\\temp\\firely-ig -p hl7.fhir.us.ndh#1.0.0-ballot --auto-load-expansions --resolve-dependencies true --include-experimental", + "commandLineArgs": "generate FirelyNetIg --output-path ../../temp/firely-ig -p hl7.fhir.us.ndh#1.0.0-ballot --auto-load-expansions --resolve-dependencies true --include-experimental", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Firely IG SDC": { "commandName": "Project", - "commandLineArgs": "generate FirelyNetIg --output-path ..\\..\\temp\\firely-ig -p hl7.fhir.uv.sdc#3.0.0 --auto-load-expansions --resolve-dependencies true --include-experimental", + "commandLineArgs": "generate FirelyNetIg --output-path ../../temp/firely-ig -p hl7.fhir.uv.sdc#3.0.0 --auto-load-expansions --resolve-dependencies true --include-experimental", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Firely OpenApi": { "commandName": "Project", - "commandLineArgs": "generate OpenApi --fhir-server-url https://secure.server.fire.ly/ -p hl7.fhir.r4.core#4.0.1 --output-path ..\\..\\generated\\FS-OpenApi-R4 --include-experimental --schema-level names --metadata true --multi-file true --single-responses false --resolve-server-canonicals false --resolve-external-canonicals false --basic-scopes-only true", + "commandLineArgs": "generate OpenApi --fhir-server-url https://secure.server.fire.ly/ -p hl7.fhir.r4.core#4.0.1 --output-path ../../generated/FS-OpenApi-R4 --include-experimental --schema-level names --metadata true --multi-file true --single-responses false --resolve-server-canonicals false --resolve-external-canonicals false --basic-scopes-only true", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Info R2": { "commandName": "Project", - "commandLineArgs": "generate Info --output-path ..\\..\\generated --output-filename Info_R2.txt -p hl7.fhir.r2.core@1.0.2 -p hl7.fhir.r2.expansions#1.0.2", + "commandLineArgs": "generate Info --output-path ../../generated --output-filename Info_R2.txt -p hl7.fhir.r2.core@1.0.2 -p hl7.fhir.r2.expansions#1.0.2", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Info R3": { "commandName": "Project", - "commandLineArgs": "generate Info --output-path ..\\..\\generated --output-filename Info_R3.txt -p hl7.fhir.r3.core@3.0.2 -p hl7.fhir.r3.expansions#3.0.2", + "commandLineArgs": "generate Info --output-path ../../generated --output-filename Info_R3.txt -p hl7.fhir.r3.core@3.0.2 -p hl7.fhir.r3.expansions#3.0.2", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Info R4": { "commandName": "Project", - "commandLineArgs": "generate Info --output-path ..\\..\\generated --output-filename Info_R4.txt -p hl7.fhir.r4.core@latest -p hl7.fhir.r4.expansions#4.0.1", + "commandLineArgs": "generate Info --output-path ../../generated --output-filename Info_R4.txt -p hl7.fhir.r4.core@latest -p hl7.fhir.r4.expansions#4.0.1", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Info R4B": { "commandName": "Project", - "commandLineArgs": "generate Info --output-path ..\\..\\generated --output-filename Info_R4B.txt -p hl7.fhir.r4b.core@4.3.0 -p hl7.fhir.r4b.expansions#4.3.0", + "commandLineArgs": "generate Info --output-path ../../generated --output-filename Info_R4B.txt -p hl7.fhir.r4b.core@4.3.0 -p hl7.fhir.r4b.expansions#4.3.0", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Info R5": { "commandName": "Project", - "commandLineArgs": "generate Info --output-path ..\\..\\generated --output-filename Info_R5.txt -p hl7.fhir.r5.core@5.0.0 -p hl7.fhir.r5.expansions#5.0.0", + "commandLineArgs": "generate Info --output-path ../../generated --output-filename Info_R5.txt -p hl7.fhir.r5.core@5.0.0 -p hl7.fhir.r5.expansions#5.0.0", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Shorthand IG US Core": { "commandName": "Project", - "commandLineArgs": "generate ShorthandIg --output-path ..\\..\\temp\\fsh-ig -p hl7.fhir.us.core#6.1.0 --auto-load-expansions --resolve-dependencies true --include-experimental", + "commandLineArgs": "generate ShorthandIg --output-path ../../temp/fsh-ig -p hl7.fhir.us.core#6.1.0 --auto-load-expansions --resolve-dependencies true --include-experimental", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Shorthand IG Extensions Pack": { "commandName": "Project", - "commandLineArgs": "generate ShorthandIg --output-path ..\\..\\temp\\fsh-ig -p hl7.fhir.uv.extensions.r4#latest --auto-load-expansions --resolve-dependencies true --include-experimental", + "commandLineArgs": "generate ShorthandIg --output-path ../../temp/fsh-ig -p hl7.fhir.uv.extensions.r4#latest --auto-load-expansions --resolve-dependencies true --include-experimental", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Shorthand IG NDH": { "commandName": "Project", - "commandLineArgs": "generate ShorthandIg --output-path ..\\..\\temp\\fsh-ig -p hl7.fhir.us.ndh#1.0.0-ballot --auto-load-expansions --resolve-dependencies true --include-experimental", + "commandLineArgs": "generate ShorthandIg --output-path ../../temp/fsh-ig -p hl7.fhir.us.ndh#1.0.0-ballot --auto-load-expansions --resolve-dependencies true --include-experimental", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Shorthand IG SDC": { "commandName": "Project", - "commandLineArgs": "generate ShorthandIg --output-path ..\\..\\temp\\fsh-ig -p hl7.fhir.uv.sdc#3.0.0 --auto-load-expansions --resolve-dependencies true --include-experimental", + "commandLineArgs": "generate ShorthandIg --output-path ../../temp/fsh-ig -p hl7.fhir.uv.sdc#3.0.0 --auto-load-expansions --resolve-dependencies true --include-experimental", "workingDirectory": "$(MSBuildProjectDirectory)" }, "TypeScript R2": { "commandName": "Project", - "commandLineArgs": "generate TypeScript --output-path ..\\..\\generated --output-filename TypeScript_R2.ts -p hl7.fhir.r2.core#1.0.2 -p hl7.fhir.r2.expansions#1.0.2", + "commandLineArgs": "generate TypeScript --output-path ../../generated --output-filename TypeScript_R2.ts -p hl7.fhir.r2.core#1.0.2 -p hl7.fhir.r2.expansions#1.0.2", "workingDirectory": "$(MSBuildProjectDirectory)" }, "TypeScript R3": { "commandName": "Project", - "commandLineArgs": "generate TypeScript --output-path ..\\..\\generated --output-filename TypeScript_R3.ts -p hl7.fhir.r3.core#3.0.2 -p hl7.fhir.r3.expansions#3.0.2", + "commandLineArgs": "generate TypeScript --output-path ../../generated --output-filename TypeScript_R3.ts -p hl7.fhir.r3.core#3.0.2 -p hl7.fhir.r3.expansions#3.0.2", "workingDirectory": "$(MSBuildProjectDirectory)" }, "TypeScript R4": { "commandName": "Project", - "commandLineArgs": "generate TypeScript --output-path ..\\..\\generated --output-filename TypeScript_R4.ts -p hl7.fhir.r4.core#4.0.1 -p hl7.fhir.r4.expansions#4.0.1", + "commandLineArgs": "generate TypeScript --output-path ../../generated --output-filename TypeScript_R4.ts -p hl7.fhir.r4.core#4.0.1 -p hl7.fhir.r4.expansions#4.0.1", "workingDirectory": "$(MSBuildProjectDirectory)" }, "TypeScript R4B": { "commandName": "Project", - "commandLineArgs": "generate TypeScript --output-path ..\\..\\generated --output-filename TypeScript_R4B.ts -p hl7.fhir.r4b.core#4.3.0 -p hl7.fhir.r4b.expansions#4.3.0", + "commandLineArgs": "generate TypeScript --output-path ../../generated --output-filename TypeScript_R4B.ts -p hl7.fhir.r4b.core#4.3.0 -p hl7.fhir.r4b.expansions#4.3.0", "workingDirectory": "$(MSBuildProjectDirectory)" }, "TypeScript R5": { "commandName": "Project", - "commandLineArgs": "generate TypeScript --output-path ..\\..\\generated --output-filename TypeScript_R5.ts -p hl7.fhir.r5.core#5.0.0 -p hl7.fhir.r5.expansions#5.0.0", + "commandLineArgs": "generate TypeScript --output-path ../../generated --output-filename TypeScript_R5.ts -p hl7.fhir.r5.core#5.0.0 -p hl7.fhir.r5.expansions#5.0.0", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Ruby R4": { "commandName": "Project", - "commandLineArgs": "generate Ruby --output-path ..\\..\\generated\\Ruby_r4 -p hl7.fhir.r4.core#4.0.1 -p hl7.fhir.r4.expansions#4.0.1", + "commandLineArgs": "generate Ruby --output-path ../../generated/Ruby_r4 -p hl7.fhir.r4.core#4.0.1 -p hl7.fhir.r4.expansions#4.0.1", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Compare x-y": { "commandName": "Project", - "commandLineArgs": "compare -c hl7.fhir.r4.core#4.0.1 -p hl7.fhir.r5.core#5.0.0 --auto-load-expansions --resolve-dependencies true --map-source-path ..\\..\\..\\fhir-cross-version --map-destination-path ..\\..\\..\\fhir-cross-version-source --map-save-style Source", + "commandLineArgs": "compare -c hl7.fhir.r4.core#4.0.1 -p hl7.fhir.r5.core#5.0.0 --auto-load-expansions --resolve-dependencies true --map-source-path ../../../fhir-cross-version --map-destination-path ../../../fhir-cross-version-source --map-save-style Source", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Compare 43-50": { "commandName": "Project", - "commandLineArgs": "compare -p hl7.fhir.r4b.core#4.3.0 -c hl7.fhir.r5.core#5.0.0 --auto-load-expansions --resolve-dependencies true --map-source-path ..\\..\\..\\fhir-cross-version --map-destination-path ..\\..\\..\\fhir-cross-version-source --map-save-style Source", + "commandLineArgs": "compare -p hl7.fhir.r4b.core#4.3.0 -c hl7.fhir.r5.core#5.0.0 --auto-load-expansions --resolve-dependencies true --map-source-path ../../../fhir-cross-version --map-destination-path ../../../fhir-cross-version-source --map-save-style Source", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Compare 50-43": { "commandName": "Project", - "commandLineArgs": "compare -p hl7.fhir.r5.core#5.0.0 -c hl7.fhir.r4b.core#4.3.0 --auto-load-expansions --resolve-dependencies true --map-source-path ..\\..\\..\\fhir-cross-version --map-destination-path ..\\..\\..\\fhir-cross-version-source --map-save-style Source", + "commandLineArgs": "compare -p hl7.fhir.r5.core#5.0.0 -c hl7.fhir.r4b.core#4.3.0 --auto-load-expansions --resolve-dependencies true --map-source-path ../../../fhir-cross-version --map-destination-path ../../../fhir-cross-version-source --map-save-style Source", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Gui": { @@ -167,7 +167,7 @@ }, "Cross Version Review": { "commandName": "Project", - "commandLineArgs": "cross-version --output-path ..\\..\\temp\\cross-version", + "commandLineArgs": "cross-version --output-path ../../temp/cross-version", "workingDirectory": "$(MSBuildProjectDirectory)" }, "http": { From 30b21dfe14aaaedd59babcd98cbd31d06c9094a5 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Wed, 19 Feb 2025 18:09:42 +0100 Subject: [PATCH 18/52] Skip Value prop generation for specific types. --- .../Language/Firely/CSharpFirely2.cs | 71 +++++++------------ 1 file changed, 25 insertions(+), 46 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 54ffa2121..698394796 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2262,16 +2262,7 @@ private void WriteCompareChildren( if (exportName == "PrimitiveType") { - _writer.WriteLineIndented("var otherValue = otherT.ObjectValue;"); - - _writer.WriteLineIndented("if (ObjectValue is byte[] bytes && otherValue is byte[] bytesOther)"); - _writer.IncreaseIndent(); - _writer.WriteLineIndented("return Enumerable.SequenceEqual(bytes, bytesOther);"); - _writer.DecreaseIndent(); - _writer.WriteLineIndented("else"); - _writer.IncreaseIndent(); _writer.WriteLineIndented("return Equals(ObjectValue, otherT.ObjectValue);"); - _writer.DecreaseIndent(); _writer.WriteLine(string.Empty); } else @@ -3431,14 +3422,14 @@ private void WritePrimitiveType( _writer.WriteLineIndented( $"public partial class" + - $" {exportName}" + - $" : PrimitiveType, " + - PrimitiveValueInterface(typeName)); + $" {exportName}" + + $" : PrimitiveType, " + + PrimitiveValueInterface(typeName)); // open class OpenScope(); - if(primitive.Abstract != true) + if (primitive.Abstract != true) WritePropertyTypeName(primitive.Name); if (!string.IsNullOrEmpty(primitive.cgpValidationRegEx())) @@ -3459,44 +3450,31 @@ private void WritePrimitiveType( _writer.WriteLineIndented($"public {exportName}(): this(({typeName})null) {{}}"); _writer.WriteLine(string.Empty); - WriteIndentedComment("Primitive value of the element"); + // For some primitive pocos, we need a hand-written value propery since we are using + // a different value type for JsonValue and Value. + if (exportName is not ("Integer64" or "Base64Binary" or "Instant")) + { + WriteIndentedComment("Primitive value of the element"); - _writer.WriteLineIndented("[FhirElement(\"value\", IsPrimitiveValue=true, XmlSerialization=XmlRepresentation.XmlAttr, InSummary=true, Order=30)]"); - _writer.WriteLineIndented($"[DeclaredType(Type = typeof({getSystemTypeForFhirType(primitive.Name)}))]"); + _writer.WriteLineIndented( + "[FhirElement(\"value\", IsPrimitiveValue=true, XmlSerialization=XmlRepresentation.XmlAttr, InSummary=true, Order=30)]"); + _writer.WriteLineIndented($"[DeclaredType(Type = typeof({getSystemTypeForFhirType(primitive.Name)}))]"); - if (PrimitiveValidationPatterns.TryGetValue(primitive.Name, out string? primitivePattern)) - { - _writer.WriteLineIndented($"[{primitivePattern}]"); - } + if (PrimitiveValidationPatterns.TryGetValue(primitive.Name, out string? primitivePattern)) + { + _writer.WriteLineIndented($"[{primitivePattern}]"); + } - _writer.WriteLineIndented("[DataMember]"); - _writer.WriteLineIndented($"public {typeName} Value"); - OpenScope(); + _writer.WriteLineIndented("[DataMember]"); - // A bit of a hack until we have a proper way to handle the primitives - // in https://github.com/FirelyTeam/firely-net-sdk/issues/2781 - if (typeName == "long?") - { - _writer.WriteLineIndented( - """ - get - { - return ObjectValue switch - { - null => null, - long l => l, - _ => Convert.ToInt64(ObjectValue) - }; - } - """); - } - else - { - _writer.WriteLineIndented($"get {{ return ({typeName})ObjectValue; }}"); - } + _writer.WriteLineIndented($"public {typeName} Value"); + OpenScope(); - _writer.WriteLineIndented("set { ObjectValue = value; OnPropertyChanged(\"Value\"); }"); - CloseScope(); + var typeNameInSwitch = typeName.EndsWith("?") ? typeName[..^1] : typeName; + _writer.WriteLineIndented($"get {{ return ObjectValue is {typeNameInSwitch} or null ? ({typeName})ObjectValue : throw COVE.INCORRECT_LITERAL_VALUE_TYPE(null, ObjectValue, this.TypeName); }}"); + _writer.WriteLineIndented("set { ObjectValue = value; OnPropertyChanged(\"Value\"); }"); + CloseScope(); + } WriteDeepCopy(exportName); @@ -3624,6 +3602,7 @@ private void WriteHeaderPrimitive() _writer.WriteLineIndented("using Hl7.Fhir.Specification;"); _writer.WriteLineIndented("using Hl7.Fhir.Validation;"); _writer.WriteLineIndented("using SystemPrimitive = Hl7.Fhir.ElementModel.Types;"); + _writer.WriteLineIndented("using COVE=Hl7.Fhir.Validation.CodedValidationException;"); _writer.WriteLine(string.Empty); WriteCopyright(); From ad3e60cc00354e05e4b33b9ff5aa40fc465074a3 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Sat, 22 Feb 2025 11:44:49 +0100 Subject: [PATCH 19/52] Mmmm.....need to split out nullability generation here --- .../Language/Firely/CSharpFirely2.cs | 114 +++++++++--------- 1 file changed, 60 insertions(+), 54 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 698394796..e2f0ddf51 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -1971,7 +1971,7 @@ private void WriteComponent( if (identifierElement.cgIsArray()) _writer.WriteLineIndented("List IIdentifiable>.Identifier { get => Identifier; set => Identifier = value; }"); else - _writer.WriteLineIndented("Identifier IIdentifiable.Identifier { get => Identifier; set => Identifier = value; }"); + _writer.WriteLineIndented("Identifier? IIdentifiable.Identifier { get => Identifier; set => Identifier = value; }"); _writer.WriteLine(string.Empty); } @@ -2089,8 +2089,8 @@ private void WriteDictionaryPairs(string exportName, List ex foreach (WrittenElementInfo info in exportedElements) { string elementProp = $"\"{info.FhirElementName}\""; - _writer.WriteLineIndented($"if ({NullCheck(info.PropertyName, info.PropertyType is ListTypeReference)}) yield return new " + - $"KeyValuePair({elementProp},{info.PropertyName});"); + _writer.WriteLineIndented($"if ({NullCheck("_"+info.PropertyName, info.PropertyType is ListTypeReference)}) yield return new " + + $"KeyValuePair({elementProp},_{info.PropertyName});"); } CloseScope(); @@ -2110,7 +2110,7 @@ private void WriteDictionaryTryGetValue(string exportName, List comparer)"); OpenScope(); - _writer.WriteLineIndented($"var otherT = other as {exportName};"); - _writer.WriteLineIndented("if(otherT == null) return false;"); + _writer.WriteLineIndented($"if(other is not {exportName} otherT) return false;"); _writer.WriteLine(string.Empty); _writer.WriteLineIndented("if(!base.CompareChildren(otherT, comparer)) return false;"); + _writer.WriteIndented( + "#pragma warning disable CS8604 // Possible null reference argument - netstd2.1 has a wrong nullable signature here"); + foreach (WrittenElementInfo info in exportedElements) { if(info.PropertyType is CqlTypeReference) { _writer.WriteLineIndented( - $"if( {info.PropertyName} != otherT.{info.PropertyName} )" + + $"if( _{info.PropertyName} != otherT._{info.PropertyName} )" + $" return false;"); } else if (info.PropertyType is ListTypeReference) { _writer.WriteLineIndented( - $"if(!comparer.ListEquals({info.PropertyName}, otherT.{info.PropertyName}))" + + $"if(!comparer.ListEquals(_{info.PropertyName}, otherT._{info.PropertyName}))" + $" return false;"); } else { _writer.WriteLineIndented( - $"if(!comparer.Equals({info.PropertyName}, otherT.{info.PropertyName}))" + + $"if(!comparer.Equals(_{info.PropertyName}, otherT._{info.PropertyName}))" + $" return false;"); } } + _writer.WriteLine("#pragma warning restore CS8604 // Possible null reference argument."); _writer.WriteLine(string.Empty); if (exportName == "PrimitiveType") @@ -2283,13 +2290,11 @@ private void WriteCopyTo( var specifier = exportName == "Base" ? "virtual" : "override"; _writer.WriteLineIndented($"protected internal {specifier} void CopyToInternal(Base other)"); OpenScope(); - _writer.WriteLineIndented($"var dest = other as {exportName};"); - _writer.WriteLine(string.Empty); - - _writer.WriteLineIndented("if (dest == null)"); - OpenScope(); + _writer.WriteLineIndented($"if(other is not {exportName} dest)"); + _writer.IncreaseIndent(); _writer.WriteLineIndented("throw new ArgumentException(\"Can only copy to an object of the same type\", \"other\");"); - CloseScope(); + _writer.DecreaseIndent(); + _writer.WriteLine(); if (exportName == "Base") { @@ -2313,16 +2318,16 @@ private void WriteCopyTo( if (info.PropertyType is ListTypeReference) { _writer.WriteLineIndented( - $"if({info.PropertyName}.Any())" + - $" dest.{info.PropertyName} = new {info.PropertyType.PropertyTypeString}({info.PropertyName}.DeepCopyInternal());"); + $"if(_{info.PropertyName} is not null)" + + $" dest.{info.PropertyName} = new {info.PropertyType.PropertyTypeString}(_{info.PropertyName}.DeepCopyInternal());"); } else { _writer.WriteLineIndented( - $"if({info.PropertyName} != null) dest.{info.PropertyName} = " + + $"if(_{info.PropertyName} is not null) dest.{info.PropertyName} = " + (info.PropertyType is CqlTypeReference ? - $"{info.PropertyName};" : - $"({info.PropertyType.PropertyTypeString}){info.PropertyName}.DeepCopyInternal();")); + $"_{info.PropertyName};" : + $"({info.PropertyType.PropertyTypeString})_{info.PropertyName}.DeepCopyInternal();")); } } @@ -3032,14 +3037,14 @@ private void WriteElementGettersAndSetters(ElementDefinition element, WrittenEle if (ei.PropertyType is not ListTypeReference) { - _writer.WriteLineIndented($"public {ei.PropertyType.PropertyTypeString} {ei.PropertyName}"); + _writer.WriteLineIndented($"public {ei.PropertyType.PropertyTypeString}? {ei.PropertyName}"); OpenScope(); _writer.WriteLineIndented($"get {{ return _{ei.PropertyName}; }}"); _writer.WriteLineIndented($"set {{ _{ei.PropertyName} = value; OnPropertyChanged(\"{ei.PropertyName}\"); }}"); CloseScope(); - _writer.WriteLineIndented($"private {ei.PropertyType.PropertyTypeString} _{ei.PropertyName};"); + _writer.WriteLineIndented($"private {ei.PropertyType.PropertyTypeString}? _{ei.PropertyName};"); _writer.WriteLine(string.Empty); } else @@ -3047,12 +3052,12 @@ private void WriteElementGettersAndSetters(ElementDefinition element, WrittenEle _writer.WriteLineIndented($"public {ei.PropertyType.PropertyTypeString} {ei.PropertyName}"); OpenScope(); - _writer.WriteLineIndented($"get {{ if(_{ei.PropertyName}==null) _{ei.PropertyName} =" + - $" new {ei.PropertyType.PropertyTypeString}(); return _{ei.PropertyName}; }}"); + _writer.WriteLineIndented($"get => _{ei.PropertyName} ??" + + $" new {ei.PropertyType.PropertyTypeString}();"); _writer.WriteLineIndented($"set {{ _{ei.PropertyName} = value; OnPropertyChanged(\"{ei.PropertyName}\"); }}"); CloseScope(); - _writer.WriteLineIndented($"private {ei.PropertyType.PropertyTypeString} _{ei.PropertyName};"); + _writer.WriteLineIndented($"private {ei.PropertyType.PropertyTypeString}? _{ei.PropertyName};"); _writer.WriteLine(string.Empty); } @@ -3101,40 +3106,31 @@ private void WritePrimitiveHelperProperty(string description, WrittenElementInfo switch (propType) { case PrimitiveTypeReference ptr: - _writer.WriteLineIndented($"public {ptr.ConveniencePropertyTypeString} {helperPropName}"); + var nullableType = ptr.ConveniencePropertyTypeString.EndsWith('?') ? ptr.ConveniencePropertyTypeString : ptr.ConveniencePropertyTypeString + '?'; + _writer.WriteLineIndented($"public {nullableType} {helperPropName}"); OpenScope(); - _writer.WriteIndented($"get {{ return {ei.PropertyName} != null ? "); string propAccess = versionsRemark is not null - ? $"(({MostGeneralValueAccessorType(ptr)}){ei.PropertyName})" - : ei.PropertyName; + ? $"(({MostGeneralValueAccessorType(ptr)}?)_{ei.PropertyName})" + : $"_{ei.PropertyName}"; + _writer.WriteLineIndented($"get => {propAccess}?.Value;"); - _writer.WriteLine($"{propAccess}.Value : null; }}"); _writer.WriteLineIndented("set"); OpenScope(); - - _writer.WriteLineIndented($"if (value == null)"); - - _writer.IncreaseIndent(); - _writer.WriteLineIndented($"{ei.PropertyName} = null;"); - _writer.DecreaseIndent(); - _writer.WriteLineIndented("else"); - _writer.IncreaseIndent(); - _writer.WriteLineIndented($"{ei.PropertyName} = new {ptr.PropertyTypeString}(value);"); - _writer.DecreaseIndent(); + _writer.WriteLineIndented($"{ei.PropertyName} = value is null ? null : new {ptr.PropertyTypeString}(value);"); _writer.WriteLineIndented($"OnPropertyChanged(\"{helperPropName}\");"); CloseScope(suppressNewline: true); CloseScope(); break; case ListTypeReference { Element: PrimitiveTypeReference lptr }: - _writer.WriteLineIndented($"public IEnumerable<{lptr.ConveniencePropertyTypeString}> {helperPropName}"); + _writer.WriteLineIndented($"public IEnumerable<{lptr.ConveniencePropertyTypeString}?>? {helperPropName}"); OpenScope(); - _writer.WriteIndented($"get {{ return {ei.PropertyName} != null ? {ei.PropertyName}"); + _writer.WriteIndented($"get => _{ei.PropertyName}"); if(versionsRemark is not null) - _writer.Write($".Cast<{MostGeneralValueAccessorType(lptr)}>()"); - _writer.WriteLine($".Select(elem => elem.Value) : null; }}"); + _writer.Write($"?.Cast<{MostGeneralValueAccessorType(lptr)}>()"); + _writer.WriteLine($"?.Select(elem => elem.Value);"); _writer.WriteLineIndented("set"); OpenScope(); @@ -3142,7 +3138,7 @@ private void WritePrimitiveHelperProperty(string description, WrittenElementInfo _writer.WriteLineIndented($"if (value == null)"); _writer.IncreaseIndent(); - _writer.WriteLineIndented($"{ei.PropertyName} = null;"); + _writer.WriteLineIndented($"{ei.PropertyName} = null!;"); _writer.DecreaseIndent(); _writer.WriteLineIndented("else"); _writer.IncreaseIndent(); @@ -3442,12 +3438,14 @@ private void WritePrimitiveType( _writer.WriteLine(string.Empty); } - _writer.WriteLineIndented($"public {exportName}({typeName} value)"); + var nullableTypeName = typeName.EndsWith('?') ? typeName : typeName + '?'; + + _writer.WriteLineIndented($"public {exportName}({nullableTypeName} value)"); OpenScope(); _writer.WriteLineIndented("Value = value;"); CloseScope(); - _writer.WriteLineIndented($"public {exportName}(): this(({typeName})null) {{}}"); + _writer.WriteLineIndented($"public {exportName}(): this(({nullableTypeName})null) {{}}"); _writer.WriteLine(string.Empty); // For some primitive pocos, we need a hand-written value propery since we are using @@ -3467,11 +3465,11 @@ private void WritePrimitiveType( _writer.WriteLineIndented("[DataMember]"); - _writer.WriteLineIndented($"public {typeName} Value"); + _writer.WriteLineIndented($"public {nullableTypeName} Value"); OpenScope(); var typeNameInSwitch = typeName.EndsWith("?") ? typeName[..^1] : typeName; - _writer.WriteLineIndented($"get {{ return ObjectValue is {typeNameInSwitch} or null ? ({typeName})ObjectValue : throw COVE.INCORRECT_LITERAL_VALUE_TYPE(null, ObjectValue, this.TypeName); }}"); + _writer.WriteLineIndented($"get {{ return ObjectValue is {typeNameInSwitch} or null ? ({nullableTypeName})ObjectValue : throw COVE.INCORRECT_LITERAL_VALUE_TYPE(null, ObjectValue, this.TypeName); }}"); _writer.WriteLineIndented("set { ObjectValue = value; OnPropertyChanged(\"Value\"); }"); CloseScope(); } @@ -3579,8 +3577,12 @@ private void WriteHeaderComplexDataType() _writer.WriteLineIndented("using Hl7.Fhir.Specification;"); _writer.WriteLineIndented("using Hl7.Fhir.Utility;"); _writer.WriteLineIndented("using Hl7.Fhir.Validation;"); + _writer.WriteLineIndented("using System.Diagnostics.CodeAnalysis;"); _writer.WriteLineIndented("using SystemPrimitive = Hl7.Fhir.ElementModel.Types;"); - _writer.WriteLine(string.Empty); + _writer.WriteLine(); + + _writer.WriteLineIndented("#nullable enable"); + _writer.WriteLine(); WriteCopyright(); @@ -3601,10 +3603,14 @@ private void WriteHeaderPrimitive() _writer.WriteLineIndented("using Hl7.Fhir.Introspection;"); _writer.WriteLineIndented("using Hl7.Fhir.Specification;"); _writer.WriteLineIndented("using Hl7.Fhir.Validation;"); + _writer.WriteLineIndented("using System.Diagnostics.CodeAnalysis;"); _writer.WriteLineIndented("using SystemPrimitive = Hl7.Fhir.ElementModel.Types;"); _writer.WriteLineIndented("using COVE=Hl7.Fhir.Validation.CodedValidationException;"); _writer.WriteLine(string.Empty); + _writer.WriteLineIndented("#nullable enable"); + _writer.WriteLine(); + WriteCopyright(); } From c3d52366c9488d9fab88eaf7cd98b55c311a0b1a Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Mon, 24 Feb 2025 15:45:04 +0100 Subject: [PATCH 20/52] Made interface/pattern generation nullable aware. --- .../Language/Firely/CSharpFirely2.cs | 204 +++++++++--------- 1 file changed, 102 insertions(+), 102 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index e2f0ddf51..451bab3e3 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -1607,7 +1607,7 @@ private void WriteInterfaceComponent( string pn = exportName + "." + interfaceEi.PropertyName; WrittenElementInfo? resourceEi = null; - if (resourceElements.TryGetValue(interfaceEi.FhirElementName ?? string.Empty, out ElementDefinition? resourceEd)) + if (resourceElements.TryGetValue(interfaceEi.FhirElementName, out ElementDefinition? resourceEd)) { resourceEi = BuildElementInfo(resourceExportName, resourceEd); } @@ -1632,67 +1632,27 @@ private void WriteInterfaceElementGettersAndSetters( string interfaceExportName, WrittenElementInfo interfaceEi) { - string pn = interfaceExportName + "." + interfaceEi.PropertyName; - string rt = resourceEi?.PropertyType.PropertyTypeString ?? string.Empty; - string it = interfaceEi.PropertyType.PropertyTypeString; + string pn = interfaceEi.PropertyName; + string it = interfaceEi.PropertyType is ListTypeReference ? interfaceEi.PropertyType.PropertyTypeString : WithNullabilityMarking(interfaceEi.PropertyType.PropertyTypeString); if ((resourceEd == null) || (resourceEi == null)) { - _writer.WriteLineIndented("[IgnoreDataMember]"); - _writer.WriteLineIndented($"{it} {pn}"); - OpenScope(); - _writer.WriteLineIndented($"get {{ return null; }}"); - _writer.WriteLineIndented($"set {{ throw new NotImplementedException(\"Resource {resourceExportName} does not implement {interfaceExportName}.{interfaceEi.FhirElementName}\");}}"); - CloseScope(); + writeEmptyGetterAndSetter(it, pn); } else if (interfaceEi.PropertyType.PropertyTypeString == resourceEi.PropertyType.PropertyTypeString) { - _writer.WriteLineIndented("[IgnoreDataMember]"); - _writer.WriteLineIndented($"{it} {pn}" + - $" {{" + - $" get => {resourceEi.PropertyName};" + - $" set {{ {resourceEi.PropertyName} = value; }}" + - $" }}"); - _writer.WriteLine(); + writeOneOnOneGetterAndSetter(it, pn); } + // a resource is allowed to have a scalar in place of a list - else if ((interfaceEi.PropertyType is ListTypeReference interfaceLTR) && - (interfaceLTR.Element.PropertyTypeString == resourceEi.PropertyType.PropertyTypeString)) + else if ((interfaceEi.PropertyType is ListTypeReference interfaceLtr) && + (interfaceLtr.Element.PropertyTypeString == resourceEi.PropertyType.PropertyTypeString)) { - _writer.WriteLineIndented("[IgnoreDataMember]"); - _writer.WriteLineIndented($"{it} {pn}"); - OpenScope(); - //_writer.WriteLineIndented($"get {{ return new {it}() {{ {resourceEi.PropertyName} }}; }}"); - _writer.WriteLineIndented("get"); - OpenScope(); // getter - _writer.WriteLineIndented($"if ({resourceEi.PropertyName} == null) return new {it}();"); - _writer.WriteLineIndented($"return new {it}() {{ {resourceEi.PropertyName} }};"); - CloseScope(); // getter - - _writer.WriteLineIndented("set"); - OpenScope(); - _writer.WriteLineIndented($"if (value.Count == 0) {{ {resourceEi.PropertyName} = null; }}"); - _writer.WriteLineIndented($"else if (value.Count == 1) {{ {resourceEi.PropertyName} = value.First(); }}"); - _writer.WriteLineIndented($"else {{ throw new NotImplementedException(\"Resource {resourceExportName} can only have a single {pn} value\"); }}"); - CloseScope(); - - CloseScope(); + writeSingleToListGetterAndSetter(it, pn); } else { - WriteIndentedComment( - $"{resourceExportName}.{resourceEi.PropertyName} ({resourceEi.PropertyType.PropertyTypeString}) is incompatible with\n" + - $"{interfaceExportName}.{interfaceEi.FhirElementName} ({interfaceEi.PropertyType.PropertyTypeString})", - isSummary: false, - isRemarks: true); - _writer.WriteLineIndented("[IgnoreDataMember]"); - _writer.WriteLineIndented($"{it} {pn}"); - OpenScope(); - _writer.WriteLineIndented($"get {{ return null; }}"); - _writer.WriteLineIndented($"set {{ throw new NotImplementedException(\"{resourceExportName}.{resourceEi.PropertyName} " + - $"({resourceEi.PropertyType.PropertyTypeString}) is incompatible with" + - $" {interfaceExportName}.{interfaceEi.FhirElementName} ({interfaceEi.PropertyType.PropertyTypeString})\");}}"); - CloseScope(); + writeIncompatibleGetterSetter(it, pn); } if (!TryGetPrimitiveType(interfaceEi.PropertyType, out PrimitiveTypeReference? interfacePtr)) @@ -1700,59 +1660,88 @@ private void WriteInterfaceElementGettersAndSetters( return; } - string ppn = interfaceExportName + "." + interfaceEi.PrimitiveHelperName; - string prt = (resourceEi?.PropertyType is PrimitiveTypeReference rPTR) ? rPTR.ConveniencePropertyTypeString : string.Empty; - string pit = interfacePtr.ConveniencePropertyTypeString; + string ppn = interfaceEi.PrimitiveHelperName!; + string pit = interfaceEi.PropertyType is ListTypeReference ? interfacePtr.ConveniencePropertyTypeString : WithNullabilityMarking(interfacePtr.ConveniencePropertyTypeString); if ((resourceEd == null) || (resourceEi == null)) { - _writer.WriteLineIndented("[IgnoreDataMember]"); - _writer.WriteLineIndented($"{pit} {ppn}"); - OpenScope(); - _writer.WriteLineIndented($"get {{ return null; }}"); - _writer.WriteLineIndented($"set {{ throw new NotImplementedException(\"Resource {resourceExportName}" + - $" does not implement {interfaceExportName}.{interfaceEi.FhirElementName}\");}}"); - CloseScope(); + writeEmptyGetterAndSetter(pit, ppn); } else if (interfaceEi.PropertyType == resourceEi.PropertyType) + { + writeOneOnOneGetterAndSetter(pit, ppn); + } + else + { + writeIncompatibleGetterSetter(pit, ppn); + } + + return; + + void writeOneOnOneGetterAndSetter(string propertyType, string propertyName) { _writer.WriteLineIndented("[IgnoreDataMember]"); - _writer.WriteLineIndented($"{pit} {ppn}" + - $" {{" + - $" get => {resourceEi.PrimitiveHelperName};" + - $" set {{ {resourceEi.PrimitiveHelperName} = value; }}" + - $" }}"); - _writer.WriteLine(); + _writer.WriteLineIndented($"{propertyType} {interfaceExportName}.{propertyName}"); + OpenScope(); + _writer.WriteLineIndented($"get => {propertyName};"); + _writer.WriteLineIndented($"set => {propertyName} = value;"); + CloseScope(); } - // a resource is allowed to have a scalar in place of a list - //else if (interfaceEi.PropertyType == "List<" + resourceEi.PropertyType + ">") - else if (interfaceEi.PropertyType is ListTypeReference) + + void writeSingleToListGetterAndSetter(string propertyType, string propertyName) { _writer.WriteLineIndented("[IgnoreDataMember]"); - _writer.WriteLineIndented($"{pit} {ppn}"); + _writer.WriteLineIndented($"{propertyType} {interfaceExportName}.{propertyName}"); OpenScope(); - _writer.WriteLineIndented($"get {{ return new {pit}() {{ {resourceEi.PropertyType.PropertyTypeString} }}; }}"); + + _writer.WriteLineIndented($"get => {propertyName} is null ? [] : [{propertyName}];"); _writer.WriteLineIndented("set"); OpenScope(); - _writer.WriteLineIndented($"if (value.Count == 1) {{ {resourceEi.PrimitiveHelperName} = value.First(); }}"); - _writer.WriteLineIndented($"else {{ throw new NotImplementedException(\"Resource {resourceExportName} can only have a single {ppn} value\"); }}"); + _writer.WriteLineIndented($"{propertyName} = value switch"); + + OpenScope(); + _writer.WriteLineIndented($"{{ Count: 0 }} => null,"); + _writer.WriteLineIndented($"{{ Count: 1 }} => value.First(),"); + _writer.WriteLineIndented($"_ => throw new NotImplementedException(\"Resource {resourceExportName} can only have a single {propertyName} value\")"); + CloseScope(includeSemicolon: true, suppressNewline: true); + + CloseScope(suppressNewline: true); CloseScope(); + } + void writeIncompatibleGetterSetter(string propertyType, string propertyName) + { + string message = $"{resourceExportName}.{resourceEi.PropertyName} is incompatible with " + + $"{interfaceExportName}.{interfaceEi.FhirElementName}."; + WriteIndentedComment(message, isSummary: false, isRemarks: true); + + _writer.WriteLineIndented("[IgnoreDataMember]"); + _writer.WriteLineIndented($"{propertyType} {interfaceExportName}.{propertyName}"); + + OpenScope(); + _writer.WriteLineIndented($"get => {emptyInterfaceType()};"); + _writer.WriteLineIndented($"set => throw new NotImplementedException(\"{message}\");"); CloseScope(); } - else + + string emptyInterfaceType() => interfaceEi.PropertyType is ListTypeReference ? "[]" : "null"; + + void writeEmptyGetterAndSetter(string propertyType, string propertyName) { - _writer.WriteLineIndented($"// {resourceExportName}.{resourceEi.PropertyName} ({prt}) is incompatible with {interfaceExportName}.{interfaceEi.FhirElementName} ({pit})"); _writer.WriteLineIndented("[IgnoreDataMember]"); - _writer.WriteLineIndented($" {pit} {ppn}"); + _writer.WriteLineIndented($"{propertyType} {interfaceExportName}.{propertyName}"); OpenScope(); - _writer.WriteLineIndented($"get {{ return null; }}"); - _writer.WriteLineIndented($"set {{ throw new NotImplementedException(\"{resourceExportName}.{resourceEi.PropertyName} ({resourceEi.PropertyType.PropertyTypeString}) is incompatible with {interfaceExportName}.{interfaceEi.FhirElementName} ({interfaceEi.PropertyType.PropertyTypeString})\");}}"); + _writer.WriteLineIndented($"get => {emptyInterfaceType()};"); + _writer.WriteLineIndented($"set => throw new NotImplementedException(\"Resource {resourceExportName}" + + $" does not implement {interfaceExportName}.{interfaceEi.FhirElementName}\");"); CloseScope(); } } + private static string WithNullabilityMarking(string type) => type.EndsWith("?") ? type : type + "?"; + + private void WriteInterfaceElements( ComponentDefinition complex, string exportedComplexName, @@ -1779,12 +1768,16 @@ private void WriteInterfaceElements( { WriteIndentedComment(element.Short.Replace("{{title}}", structureName)); _writer.WriteLineIndented($"/// This uses the native .NET datatype, rather than the FHIR equivalent"); - _writer.WriteLineIndented($"{eiPtr.ConveniencePropertyTypeString} {ei.PrimitiveHelperName} {{ get; set; }}"); + + _writer.WriteLineIndented($"{WithNullabilityMarking(eiPtr.ConveniencePropertyTypeString)} {ei.PrimitiveHelperName} {{ get; set; }}"); _writer.WriteLine(); } if (description != null) WriteIndentedComment(description); - _writer.WriteLineIndented($"{ei.PropertyType.PropertyTypeString ?? string.Empty} {ei.PropertyName} {{ get; set; }}"); + var typ = ei.PropertyType is ListTypeReference + ? ei.PropertyType.PropertyTypeString + : WithNullabilityMarking(ei.PropertyType.PropertyTypeString); + _writer.WriteLineIndented($"{typ} {ei.PropertyName} {{ get; set; }}"); _writer.WriteLine(); } } @@ -1888,18 +1881,19 @@ private void WriteComponent( if (identifierElement.cgIsArray()) interfaces.Add("IIdentifiable>"); else - interfaces.Add("IIdentifiable"); + interfaces.Add("IIdentifiable"); } } - var primaryCodeElementInfo = isResource ? getPrimaryCodedElementInfo(complex, exportName) : null; + WrittenElementInfo? primaryCodeElementInfo = isResource ? getPrimaryCodedElementInfo(complex, exportName) : null; if (primaryCodeElementInfo != null) { - interfaces.Add($"ICoded<{primaryCodeElementInfo.PropertyType.PropertyTypeString}>"); + string nullable = primaryCodeElementInfo.PropertyType is ListTypeReference ? "" : "?"; + interfaces.Add($"ICoded<{primaryCodeElementInfo.PropertyType.PropertyTypeString}{nullable}>"); } - var modifierElement = complex.cgGetChild("modifierExtension"); + ElementDefinition? modifierElement = complex.cgGetChild("modifierExtension"); if (modifierElement != null) { if (!modifierElement.cgIsInherited(complex.Structure)) @@ -1971,15 +1965,22 @@ private void WriteComponent( if (identifierElement.cgIsArray()) _writer.WriteLineIndented("List IIdentifiable>.Identifier { get => Identifier; set => Identifier = value; }"); else - _writer.WriteLineIndented("Identifier? IIdentifiable.Identifier { get => Identifier; set => Identifier = value; }"); + _writer.WriteLineIndented("Identifier? IIdentifiable.Identifier { get => Identifier; set => Identifier = value; }"); _writer.WriteLine(string.Empty); } if (primaryCodeElementInfo != null) { - _writer.WriteLineIndented($"{primaryCodeElementInfo.PropertyType.PropertyTypeString} ICoded<{primaryCodeElementInfo.PropertyType.PropertyTypeString}>.Code {{ get => {primaryCodeElementInfo.PropertyName}; set => {primaryCodeElementInfo.PropertyName} = value; }}"); - _writer.WriteLineIndented($"IEnumerable ICoded.ToCodings() => {primaryCodeElementInfo.PropertyName}.ToCodings();"); + var (codedType, bang) = primaryCodeElementInfo.PropertyType switch + { + ListTypeReference { PropertyTypeString: { } n } => (n, string.Empty), + { PropertyTypeString: {} n } => (WithNullabilityMarking(n), "!") + }; + + _writer.WriteLineIndented($"{codedType} ICoded<{codedType}>.Code {{ get => {primaryCodeElementInfo.PropertyName}; " + + $"set => {primaryCodeElementInfo.PropertyName} = value{bang}; }}"); + _writer.WriteLineIndented($"IEnumerable ICoded.ToCodings() => {primaryCodeElementInfo.PropertyName}?.ToCodings() ?? [];"); _writer.WriteLine(string.Empty); } @@ -1989,7 +1990,7 @@ private void WriteComponent( if (birthdayProperty != null) { - _writer.WriteLineIndented($"Hl7.Fhir.Model.Date {Namespace}.IPatient.BirthDate => {birthdayProperty.PropertyName};"); + _writer.WriteLineIndented($"Hl7.Fhir.Model.Date? {Namespace}.IPatient.BirthDate => {birthdayProperty.PropertyName};"); _writer.WriteLine(string.Empty); } } @@ -2982,20 +2983,18 @@ string getTypeNameFromElement() internal static bool TryGetPrimitiveType(TypeReference tr, [NotNullWhen(true)] out PrimitiveTypeReference? ptr) { - if (tr is PrimitiveTypeReference p) + switch (tr) { - ptr = p; - return true; + case PrimitiveTypeReference p: + ptr = p; + return true; + case ListTypeReference { Element: PrimitiveTypeReference pltr }: + ptr = pltr; + return true; + default: + ptr = null; + return false; } - - if (tr is ListTypeReference { Element: PrimitiveTypeReference pltr }) - { - ptr = pltr; - return true; - } - - ptr = null; - return false; } internal WrittenElementInfo BuildElementInfo( @@ -3106,7 +3105,7 @@ private void WritePrimitiveHelperProperty(string description, WrittenElementInfo switch (propType) { case PrimitiveTypeReference ptr: - var nullableType = ptr.ConveniencePropertyTypeString.EndsWith('?') ? ptr.ConveniencePropertyTypeString : ptr.ConveniencePropertyTypeString + '?'; + string nullableType = WithNullabilityMarking(ptr.ConveniencePropertyTypeString); _writer.WriteLineIndented($"public {nullableType} {helperPropName}"); OpenScope(); @@ -3123,7 +3122,8 @@ private void WritePrimitiveHelperProperty(string description, WrittenElementInfo CloseScope(); break; case ListTypeReference { Element: PrimitiveTypeReference lptr }: - _writer.WriteLineIndented($"public IEnumerable<{lptr.ConveniencePropertyTypeString}?>? {helperPropName}"); + string nullableTypeList = WithNullabilityMarking(lptr.ConveniencePropertyTypeString); + _writer.WriteLineIndented($"public IEnumerable<{nullableTypeList}>? {helperPropName}"); OpenScope(); From e00c714d1e6fd275f7ff3566931eeb4a2f436175 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Mon, 24 Feb 2025 21:35:00 +0100 Subject: [PATCH 21/52] Missed the fact that a comment was commenting out an important line --- .../Language/Firely/CSharpFirely2.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 451bab3e3..5d5edce9a 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -1980,7 +1980,7 @@ private void WriteComponent( _writer.WriteLineIndented($"{codedType} ICoded<{codedType}>.Code {{ get => {primaryCodeElementInfo.PropertyName}; " + $"set => {primaryCodeElementInfo.PropertyName} = value{bang}; }}"); - _writer.WriteLineIndented($"IEnumerable ICoded.ToCodings() => {primaryCodeElementInfo.PropertyName}?.ToCodings() ?? [];"); + _writer.WriteLineIndented($"IReadOnlyCollection ICoded.ToCodings() => {primaryCodeElementInfo.PropertyName}?.ToCodings() ?? [];"); _writer.WriteLine(string.Empty); } @@ -2240,7 +2240,7 @@ private void WriteCompareChildren( _writer.WriteLineIndented("if(!base.CompareChildren(otherT, comparer)) return false;"); - _writer.WriteIndented( + _writer.WriteLineIndented( "#pragma warning disable CS8604 // Possible null reference argument - netstd2.1 has a wrong nullable signature here"); foreach (WrittenElementInfo info in exportedElements) @@ -2265,7 +2265,7 @@ private void WriteCompareChildren( } } - _writer.WriteLine("#pragma warning restore CS8604 // Possible null reference argument."); + _writer.WriteLineIndented("#pragma warning restore CS8604 // Possible null reference argument."); _writer.WriteLine(string.Empty); if (exportName == "PrimitiveType") @@ -3051,8 +3051,7 @@ private void WriteElementGettersAndSetters(ElementDefinition element, WrittenEle _writer.WriteLineIndented($"public {ei.PropertyType.PropertyTypeString} {ei.PropertyName}"); OpenScope(); - _writer.WriteLineIndented($"get => _{ei.PropertyName} ??" + - $" new {ei.PropertyType.PropertyTypeString}();"); + _writer.WriteLineIndented($"get => _{ei.PropertyName} ??= [];"); _writer.WriteLineIndented($"set {{ _{ei.PropertyName} = value; OnPropertyChanged(\"{ei.PropertyName}\"); }}"); CloseScope(); @@ -3069,7 +3068,7 @@ PrimitiveTypeReference or // If the property has had multiple types over time, we need to generate a helper property for each type. if(_elementTypeChanges.TryGetValue(element.Path, out ElementTypeChange[]? changes)) { - var lastChange = changes.Last(); + ElementTypeChange lastChange = changes.Last(); foreach(ElementTypeChange change in changes) { @@ -3123,14 +3122,14 @@ private void WritePrimitiveHelperProperty(string description, WrittenElementInfo break; case ListTypeReference { Element: PrimitiveTypeReference lptr }: string nullableTypeList = WithNullabilityMarking(lptr.ConveniencePropertyTypeString); - _writer.WriteLineIndented($"public IEnumerable<{nullableTypeList}>? {helperPropName}"); + _writer.WriteLineIndented($"public IEnumerable<{nullableTypeList}> {helperPropName}"); OpenScope(); _writer.WriteIndented($"get => _{ei.PropertyName}"); if(versionsRemark is not null) _writer.Write($"?.Cast<{MostGeneralValueAccessorType(lptr)}>()"); - _writer.WriteLine($"?.Select(elem => elem.Value);"); + _writer.WriteLine($"?.Select(elem => elem.Value) ?? [];"); _writer.WriteLineIndented("set"); OpenScope(); From 36170b2a6986c23d6af79d10d4c8bbd05aa7dd4b Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Mon, 10 Mar 2025 17:29:17 +0100 Subject: [PATCH 22/52] Moved Address, Duration, HumanName and Ratio to base. --- .run/firely-all.run.xml | 12 ++++++------ .../Language/Firely/CSharpFirely2.cs | 4 ++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.run/firely-all.run.xml b/.run/firely-all.run.xml index a2c5c4a33..7f737b051 100644 --- a/.run/firely-all.run.xml +++ b/.run/firely-all.run.xml @@ -1,11 +1,11 @@ - - - - - - + + + + + + \ No newline at end of file diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 54ffa2121..e70143d6e 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -128,6 +128,7 @@ bool IFileHashTestable.GenerateHashesInsteadOfOutput /// private static readonly List _baseSubsetComplexTypes = [ + "Address", "Attachment", "BackboneElement", "BackboneType", @@ -137,8 +138,10 @@ bool IFileHashTestable.GenerateHashesInsteadOfOutput "ContactPoint", "ContactDetail", "DataType", + "Duration", "Element", "Extension", + "HumanName", "Identifier", "Meta", "Narrative", @@ -146,6 +149,7 @@ bool IFileHashTestable.GenerateHashesInsteadOfOutput "PrimitiveType", "Quantity", "Range", + "Ratio", "Reference", "Signature", "UsageContext", From b4c55aaf791807caa8a2df423b0c842de3151906 Mon Sep 17 00:00:00 2001 From: Kasdejong Date: Thu, 13 Mar 2025 11:29:35 +0100 Subject: [PATCH 23/52] adjusted codegen for overflow errors on pocos --- .../Language/Firely/CSharpFirely2.cs | 95 ++++++++++--------- 1 file changed, 52 insertions(+), 43 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 9c29fe302..604190502 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2170,41 +2170,39 @@ private void WriteDictionaryTrySetValue(string exportName, List)) throw new ArgumentException(\"Value must be a Base or IEnumerable\", nameof(value));"); + // switch _writer.WriteLineIndented("switch (key)"); OpenScope(); foreach (WrittenElementInfo info in exportedElements) { - // Because our list properties are never null when you get them, but can be set to null, - // we need a bang after the assignment here for lists, but not for other elements. - var listTrick = info.PropertyType is ListTypeReference ? "!" : ""; - - writeSetValueCase(info.FhirElementName, null, - $"{info.PropertyName} = ({info.PropertyType.PropertyTypeString}?)value{listTrick};"); - - // if (info.PropertyType is ListTypeReference ltr) - // { - // writeSetValueCase(info.FhirElementName, $"value is IEnumerable<{ltr.Element.PropertyTypeString}> v", - // $"{info.PropertyName} = new {info.PropertyType.PropertyTypeString}(v);"); - // } - // else - // { - // writeSetValueCase(info.FhirElementName, $"value is {info.PropertyType.PropertyTypeString} v", - // $"{info.PropertyName} = v;"); - // } - // - // writeSetValueCase(info.FhirElementName, "value is null", - // $"{info.PropertyName} = null;"); + writeSetValueCase(info.FhirElementName, null, info.PropertyType, info.PropertyName); } - void writeSetValueCase(string name, string? when, string statement) + void writeSetValueCase(string fhirName, string? when, TypeReference type, string propName) { - _writer.WriteLineIndented(when is not null ? $"case \"{name}\" when {when}:" : $"case \"{name}\":"); + string overflowTypeName = type.PropertyTypeString switch + { + "Hl7.Fhir.Model.Resource" => "DynamicResource", + "Hl7.Fhir.Model.DataType" => "DynamicDataType", + "Hl7.Fhir.Model.PrimitiveType" => "DynamicPrimitive", + { } s => s + }; + + _writer.WriteLineIndented(when is not null ? $"case \"{fhirName}\" when {when}:" : $"case \"{fhirName}\":"); _writer.IncreaseIndent(); + _writer.WriteLineIndented($"if (value is not ({type.PropertyTypeString} or null))"); + _writer.OpenScope(); + _writer.WriteLineIndented($"{propName} = OverflowNull<{overflowTypeName}>.INSTANCE;"); + _writer.WriteLineIndented($"Overflow[\"{fhirName}\"] = value;"); + _writer.CloseScope(); - _writer.WriteLineIndented(statement); + // Because our list properties are never null when you get them, but can be set to null, + // we need a bang after the assignment here for lists, but not for other elements. + _writer.WriteLineIndented($"else {propName} = ({type.PropertyTypeString}?)value{(type is ListTypeReference ? "!" : "")};"); _writer.WriteLineIndented($"return this;"); _writer.DecreaseIndent(); } @@ -3038,30 +3036,41 @@ private void WriteElementGettersAndSetters(ElementDefinition element, WrittenEle { _writer.WriteLineIndented("[DataMember]"); - if (ei.PropertyType is not ListTypeReference) + string overflowTypeName = ei.PropertyType.PropertyTypeString switch { - _writer.WriteLineIndented($"public {ei.PropertyType.PropertyTypeString}? {ei.PropertyName}"); + "Hl7.Fhir.Model.Resource" => "DynamicResource", + "Hl7.Fhir.Model.DataType" => "DynamicDataType", + "Hl7.Fhir.Model.PrimitiveType" => "DynamicPrimitive", + { } s => s + }; - OpenScope(); - _writer.WriteLineIndented($"get {{ return _{ei.PropertyName}; }}"); - _writer.WriteLineIndented($"set {{ _{ei.PropertyName} = value; OnPropertyChanged(\"{ei.PropertyName}\"); }}"); - CloseScope(); + _writer.WriteLineIndented($"public {(ei.PropertyType is ListTypeReference ltr ? ltr.PropertyTypeString : $"{ei.PropertyType.PropertyTypeString}?")} {ei.PropertyName}"); - _writer.WriteLineIndented($"private {ei.PropertyType.PropertyTypeString}? _{ei.PropertyName};"); - _writer.WriteLine(string.Empty); - } - else - { - _writer.WriteLineIndented($"public {ei.PropertyType.PropertyTypeString} {ei.PropertyName}"); + OpenScope(); - OpenScope(); - _writer.WriteLineIndented($"get => _{ei.PropertyName} ??= [];"); - _writer.WriteLineIndented($"set {{ _{ei.PropertyName} = value; OnPropertyChanged(\"{ei.PropertyName}\"); }}"); - CloseScope(); + _writer.WriteLineIndented("get"); + OpenScope(); + _writer.WriteLineIndented($"if(OverflowNull<{overflowTypeName}>.InOverflow(_{ei.PropertyName}))"); + _writer.IncreaseIndent(); + _writer.WriteLineIndented($"throw CodedValidationException.FromTypes(typeof({ei.PropertyType.PropertyTypeString}), Overflow[\"{ei.FhirElementName}\"]);"); + _writer.DecreaseIndent(); + _writer.WriteLineIndented(ei.PropertyType is not ListTypeReference ? $"return _{ei.PropertyName};" : $"return _{ei.PropertyName} ??= [];"); - _writer.WriteLineIndented($"private {ei.PropertyType.PropertyTypeString}? _{ei.PropertyName};"); - _writer.WriteLine(string.Empty); - } + CloseScope(); + + _writer.WriteLineIndented("set"); + OpenScope(); + _writer.WriteLineIndented($"if (OverflowNull<{overflowTypeName}>.InOverflow(_{ei.PropertyName}))"); + _writer.IncreaseIndent(); + _writer.WriteLineIndented($"Overflow.Remove(\"{ei.FhirElementName}\");"); + _writer.DecreaseIndent(); + _writer.WriteLineIndented($"_{ei.PropertyName} = value;"); + _writer.WriteLineIndented($"OnPropertyChanged(\"{ei.PropertyName}\");"); + CloseScope(); + + CloseScope(); + _writer.WriteLineIndented($"private {ei.PropertyType.PropertyTypeString}? _{ei.PropertyName};"); + _writer.WriteLine(string.Empty); bool needsHelperProperty = ei.PropertyType is PrimitiveTypeReference or @@ -3472,7 +3481,7 @@ private void WritePrimitiveType( OpenScope(); var typeNameInSwitch = typeName.EndsWith("?") ? typeName[..^1] : typeName; - _writer.WriteLineIndented($"get {{ return ObjectValue is {typeNameInSwitch} or null ? ({nullableTypeName})ObjectValue : throw COVE.INCORRECT_LITERAL_VALUE_TYPE(null, ObjectValue, this.TypeName); }}"); + _writer.WriteLineIndented($"get {{ return ObjectValue is {typeNameInSwitch} or null ? ({nullableTypeName})ObjectValue : throw COVE.FromTypes(typeof({exportName}), ObjectValue); }}"); _writer.WriteLineIndented("set { ObjectValue = value; OnPropertyChanged(\"Value\"); }"); CloseScope(); } From 4d2618c3d1a229a2bc4d3147648554da8aff5959 Mon Sep 17 00:00:00 2001 From: Kasdejong Date: Thu, 13 Mar 2025 16:17:20 +0100 Subject: [PATCH 24/52] more changes to codegen for invalid overflow usage --- .../Language/Firely/CSharpFirely2.cs | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 604190502..c2ed8a23f 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2064,6 +2064,15 @@ private string DetermineExportedBaseTypeName(string baseTypeName) return baseTypeName; } + private string getDynamicTypeForAbstractTypeName(string abstractTypeName) => + abstractTypeName switch + { + "Hl7.Fhir.Model.Resource" => "DynamicResource", + "Hl7.Fhir.Model.DataType" => "DynamicDataType", + "Hl7.Fhir.Model.PrimitiveType" => "DynamicPrimitive", + { } s => s + }; + private void WriteDictionarySupport(string exportName, List exportedElements) { WriteDictionaryTryGetValue(exportName, exportedElements); @@ -2071,9 +2080,6 @@ private void WriteDictionarySupport(string exportName, List WriteDictionaryPairs(exportName, exportedElements); } - private static string NullCheck(string propertyName, bool isList) => - propertyName + (isList ? "?.Any() == true" : " is not null"); - private void WriteDictionaryPairs(string exportName, List exportedElements) { // Base implementation differs from subclasses and is hand-written code in a separate partical class @@ -2094,7 +2100,7 @@ private void WriteDictionaryPairs(string exportName, List ex foreach (WrittenElementInfo info in exportedElements) { string elementProp = $"\"{info.FhirElementName}\""; - _writer.WriteLineIndented($"if ({NullCheck("_"+info.PropertyName, info.PropertyType is ListTypeReference)}) yield return new " + + _writer.WriteLineIndented($"if (!_{info.PropertyName}.InOverflow<{getDynamicTypeForAbstractTypeName(info.PropertyType.PropertyTypeString)}>() && _{info.PropertyName}{(info.PropertyType is ListTypeReference ? "?.Any() is true" : " is not null")}) yield return new " + $"KeyValuePair({elementProp},_{info.PropertyName});"); } @@ -2124,16 +2130,23 @@ private void WriteDictionaryTryGetValue(string exportName, List())"); + _writer.OpenScope(); + _writer.WriteLineIndented($"value = Overflow[\"{key}\"];"); + _writer.WriteLineIndented("return true;"); + _writer.CloseScope(); _writer.WriteLineIndented($"value = _{propName};"); - _writer.WriteLineIndented($"return {NullCheck("_"+propName, isList)};"); + _writer.WriteLineIndented($"return (value as {type.PropertyTypeString}){(type is ListTypeReference ? "?.Any() is true" : " is not null")};"); _writer.DecreaseIndent(); } @@ -2170,7 +2183,7 @@ private void WriteDictionaryTrySetValue(string exportName, List)) throw new ArgumentException(\"Value must be a Base or IEnumerable\", nameof(value));"); + _writer.WriteLineIndented("if(value is not (null or Hl7.Fhir.Model.Base or IEnumerable)) throw new ArgumentException(\"Value must be a Base or IEnumerable\", nameof(value));"); // switch _writer.WriteLineIndented("switch (key)"); @@ -2183,13 +2196,7 @@ private void WriteDictionaryTrySetValue(string exportName, List "DynamicResource", - "Hl7.Fhir.Model.DataType" => "DynamicDataType", - "Hl7.Fhir.Model.PrimitiveType" => "DynamicPrimitive", - { } s => s - }; + string overflowTypeName = getDynamicTypeForAbstractTypeName(type.PropertyTypeString); _writer.WriteLineIndented(when is not null ? $"case \"{fhirName}\" when {when}:" : $"case \"{fhirName}\":"); @@ -3050,7 +3057,7 @@ private void WriteElementGettersAndSetters(ElementDefinition element, WrittenEle _writer.WriteLineIndented("get"); OpenScope(); - _writer.WriteLineIndented($"if(OverflowNull<{overflowTypeName}>.InOverflow(_{ei.PropertyName}))"); + _writer.WriteLineIndented($"if(_{ei.PropertyName}.InOverflow<{overflowTypeName}>())"); _writer.IncreaseIndent(); _writer.WriteLineIndented($"throw CodedValidationException.FromTypes(typeof({ei.PropertyType.PropertyTypeString}), Overflow[\"{ei.FhirElementName}\"]);"); _writer.DecreaseIndent(); @@ -3060,7 +3067,7 @@ private void WriteElementGettersAndSetters(ElementDefinition element, WrittenEle _writer.WriteLineIndented("set"); OpenScope(); - _writer.WriteLineIndented($"if (OverflowNull<{overflowTypeName}>.InOverflow(_{ei.PropertyName}))"); + _writer.WriteLineIndented($"if (_{ei.PropertyName}.InOverflow<{overflowTypeName}>())"); _writer.IncreaseIndent(); _writer.WriteLineIndented($"Overflow.Remove(\"{ei.FhirElementName}\");"); _writer.DecreaseIndent(); From 7246f83ef65939fd9694925011f36f4e7830df0d Mon Sep 17 00:00:00 2001 From: Kasdejong Date: Thu, 13 Mar 2025 16:41:33 +0100 Subject: [PATCH 25/52] switched around conditions for EnumerateElements --- .../Language/Firely/CSharpFirely2.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index c2ed8a23f..c4310b417 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2100,7 +2100,7 @@ private void WriteDictionaryPairs(string exportName, List ex foreach (WrittenElementInfo info in exportedElements) { string elementProp = $"\"{info.FhirElementName}\""; - _writer.WriteLineIndented($"if (!_{info.PropertyName}.InOverflow<{getDynamicTypeForAbstractTypeName(info.PropertyType.PropertyTypeString)}>() && _{info.PropertyName}{(info.PropertyType is ListTypeReference ? "?.Any() is true" : " is not null")}) yield return new " + + _writer.WriteLineIndented($"if (_{info.PropertyName}{(info.PropertyType is ListTypeReference ? "?.Any() is true" : " is not null")} && !_{info.PropertyName}.InOverflow<{getDynamicTypeForAbstractTypeName(info.PropertyType.PropertyTypeString)}>()) yield return new " + $"KeyValuePair({elementProp},_{info.PropertyName});"); } From 812709391b26a26628ab3d43afc608fde541fcae Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Thu, 20 Mar 2025 13:01:50 +0100 Subject: [PATCH 26/52] wip --- .../Language/Firely/CSharpFirely2.cs | 15 +++++---------- .../Language/Firely/CSharpFirelyCommon.cs | 16 ---------------- 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index c4310b417..11aecbba7 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2788,7 +2788,7 @@ private void WriteElement( { 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)]"); + _writer.WriteLineIndented($"[DeclaredType(typeof(ResourceReference), Since = FhirRelease.R4)]"); } else { @@ -2797,12 +2797,12 @@ private void WriteElement( if (ei.PropertyType is CqlTypeReference ctr) { - _writer.WriteLineIndented($"[DeclaredType(Type = typeof({ctr.DeclaredTypeString}))]"); + _writer.WriteLineIndented($"[DeclaredType(typeof({ctr.DeclaredTypeString}))]"); } if (TryGetPrimitiveType(ei.PropertyType, out PrimitiveTypeReference? ptr) && ptr is CodedTypeReference) { - _writer.WriteLineIndented("[DeclaredType(Type = typeof(Code))]"); + _writer.WriteLineIndented("[DeclaredType(typeof(Code))]"); } if (!string.IsNullOrEmpty(element.cgBindingName())) @@ -2830,7 +2830,7 @@ private void WriteElement( // the metadata for the property is correct for each version. foreach(ElementTypeChange change in changes) { - _writer.WriteIndented($"[DeclaredType(Type = typeof({change.DeclaredTypeReference.PropertyTypeString})"); + _writer.WriteIndented($"[DeclaredType(typeof({change.DeclaredTypeReference.PropertyTypeString})"); _writer.Write($", Since = FhirRelease.{change.Since}"); _writer.WriteLine(")]"); } @@ -3475,12 +3475,7 @@ private void WritePrimitiveType( _writer.WriteLineIndented( "[FhirElement(\"value\", IsPrimitiveValue=true, XmlSerialization=XmlRepresentation.XmlAttr, InSummary=true, Order=30)]"); - _writer.WriteLineIndented($"[DeclaredType(Type = typeof({getSystemTypeForFhirType(primitive.Name)}))]"); - - if (PrimitiveValidationPatterns.TryGetValue(primitive.Name, out string? primitivePattern)) - { - _writer.WriteLineIndented($"[{primitivePattern}]"); - } + _writer.WriteLineIndented($"[DeclaredType(typeof({getSystemTypeForFhirType(primitive.Name)}))]"); _writer.WriteLineIndented("[DataMember]"); diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirelyCommon.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirelyCommon.cs index e967e2a4d..0b5e83c48 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirelyCommon.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirelyCommon.cs @@ -65,22 +65,6 @@ public static class CSharpFirelyCommon { "Resource", "DomainResource" }, }; - /// Primitive types that have a specific validation attribute on their Value property. - public static readonly Dictionary PrimitiveValidationPatterns = new() - { - ["uri"] = "UriPattern", - ["uuid"] = "UuidPattern", - ["id"] = "IdPattern", - ["date"] = "DatePattern", - ["dateTime"] = "DateTimePattern", - ["oid"] = "OidPattern", - ["code"] = "CodePattern", - ["time"] = "TimePattern", - ["string"] = "StringPattern", - ["markdown"] = "StringPattern", - ["xhtml"] = "NarrativeXhtmlPattern" - }; - /// /// Determines the subset of code to generate. /// From 6e88fe381904d0b5f077b4c65ad6c269e99cba59 Mon Sep 17 00:00:00 2001 From: Kasdejong Date: Thu, 20 Mar 2025 14:09:07 +0100 Subject: [PATCH 27/52] Made helper properties access getters instead of backing fields --- .../Language/Firely/CSharpFirely2.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 11aecbba7..7141898cc 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -3129,8 +3129,8 @@ private void WritePrimitiveHelperProperty(string description, WrittenElementInfo OpenScope(); string propAccess = versionsRemark is not null - ? $"(({MostGeneralValueAccessorType(ptr)}?)_{ei.PropertyName})" - : $"_{ei.PropertyName}"; + ? $"(({MostGeneralValueAccessorType(ptr)}?){ei.PropertyName})" + : $"{ei.PropertyName}"; _writer.WriteLineIndented($"get => {propAccess}?.Value;"); _writer.WriteLineIndented("set"); From b4921d9879536d5921c557e3eab1ed0baf238bfa Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Tue, 25 Mar 2025 16:17:27 +0100 Subject: [PATCH 28/52] Revert "wip" This reverts commit 812709391b26a26628ab3d43afc608fde541fcae. --- .../Language/Firely/CSharpFirely2.cs | 15 ++++++++++----- .../Language/Firely/CSharpFirelyCommon.cs | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 11aecbba7..c4310b417 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2788,7 +2788,7 @@ private void WriteElement( { WriteFhirElementAttribute(name, summary, isModifier, element, orderOffset, ", Choice = ChoiceType.DatatypeChoice", fiveWs); WriteFhirElementAttribute(name, summary, isModifier, element, orderOffset, "", fiveWs, since: since); - _writer.WriteLineIndented($"[DeclaredType(typeof(ResourceReference), Since = FhirRelease.R4)]"); + _writer.WriteLineIndented($"[DeclaredType(Type = typeof(ResourceReference), Since = FhirRelease.R4)]"); } else { @@ -2797,12 +2797,12 @@ private void WriteElement( if (ei.PropertyType is CqlTypeReference ctr) { - _writer.WriteLineIndented($"[DeclaredType(typeof({ctr.DeclaredTypeString}))]"); + _writer.WriteLineIndented($"[DeclaredType(Type = typeof({ctr.DeclaredTypeString}))]"); } if (TryGetPrimitiveType(ei.PropertyType, out PrimitiveTypeReference? ptr) && ptr is CodedTypeReference) { - _writer.WriteLineIndented("[DeclaredType(typeof(Code))]"); + _writer.WriteLineIndented("[DeclaredType(Type = typeof(Code))]"); } if (!string.IsNullOrEmpty(element.cgBindingName())) @@ -2830,7 +2830,7 @@ private void WriteElement( // the metadata for the property is correct for each version. foreach(ElementTypeChange change in changes) { - _writer.WriteIndented($"[DeclaredType(typeof({change.DeclaredTypeReference.PropertyTypeString})"); + _writer.WriteIndented($"[DeclaredType(Type = typeof({change.DeclaredTypeReference.PropertyTypeString})"); _writer.Write($", Since = FhirRelease.{change.Since}"); _writer.WriteLine(")]"); } @@ -3475,7 +3475,12 @@ private void WritePrimitiveType( _writer.WriteLineIndented( "[FhirElement(\"value\", IsPrimitiveValue=true, XmlSerialization=XmlRepresentation.XmlAttr, InSummary=true, Order=30)]"); - _writer.WriteLineIndented($"[DeclaredType(typeof({getSystemTypeForFhirType(primitive.Name)}))]"); + _writer.WriteLineIndented($"[DeclaredType(Type = typeof({getSystemTypeForFhirType(primitive.Name)}))]"); + + if (PrimitiveValidationPatterns.TryGetValue(primitive.Name, out string? primitivePattern)) + { + _writer.WriteLineIndented($"[{primitivePattern}]"); + } _writer.WriteLineIndented("[DataMember]"); diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirelyCommon.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirelyCommon.cs index 0b5e83c48..e967e2a4d 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirelyCommon.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirelyCommon.cs @@ -65,6 +65,22 @@ public static class CSharpFirelyCommon { "Resource", "DomainResource" }, }; + /// Primitive types that have a specific validation attribute on their Value property. + public static readonly Dictionary PrimitiveValidationPatterns = new() + { + ["uri"] = "UriPattern", + ["uuid"] = "UuidPattern", + ["id"] = "IdPattern", + ["date"] = "DatePattern", + ["dateTime"] = "DateTimePattern", + ["oid"] = "OidPattern", + ["code"] = "CodePattern", + ["time"] = "TimePattern", + ["string"] = "StringPattern", + ["markdown"] = "StringPattern", + ["xhtml"] = "NarrativeXhtmlPattern" + }; + /// /// Determines the subset of code to generate. /// From e6055f6448ba676927d2d8019e1bddb516b3f6b0 Mon Sep 17 00:00:00 2001 From: Kasdejong Date: Tue, 25 Mar 2025 16:25:27 +0100 Subject: [PATCH 29/52] Changed type check at start of SetValue from enumerable to list --- .../Language/Firely/CSharpFirely2.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 586672573..6bfcf4821 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2183,7 +2183,7 @@ private void WriteDictionaryTrySetValue(string exportName, List)) throw new ArgumentException(\"Value must be a Base or IEnumerable\", nameof(value));"); + _writer.WriteLineIndented("if(value is not (null or Hl7.Fhir.Model.Base or List)) throw new ArgumentException(\"Value must be a Base or IEnumerable\", nameof(value));"); // switch _writer.WriteLineIndented("switch (key)"); From 1d31db6beef27e42a3c10cb752785e22d7bd3afc Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Thu, 20 Mar 2025 13:01:50 +0100 Subject: [PATCH 30/52] wip --- .../Language/Firely/CSharpFirely2.cs | 15 +++++---------- .../Language/Firely/CSharpFirelyCommon.cs | 16 ---------------- 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 6bfcf4821..946226402 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2788,7 +2788,7 @@ private void WriteElement( { 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)]"); + _writer.WriteLineIndented($"[DeclaredType(typeof(ResourceReference), Since = FhirRelease.R4)]"); } else { @@ -2797,12 +2797,12 @@ private void WriteElement( if (ei.PropertyType is CqlTypeReference ctr) { - _writer.WriteLineIndented($"[DeclaredType(Type = typeof({ctr.DeclaredTypeString}))]"); + _writer.WriteLineIndented($"[DeclaredType(typeof({ctr.DeclaredTypeString}))]"); } if (TryGetPrimitiveType(ei.PropertyType, out PrimitiveTypeReference? ptr) && ptr is CodedTypeReference) { - _writer.WriteLineIndented("[DeclaredType(Type = typeof(Code))]"); + _writer.WriteLineIndented("[DeclaredType(typeof(Code))]"); } if (!string.IsNullOrEmpty(element.cgBindingName())) @@ -2830,7 +2830,7 @@ private void WriteElement( // the metadata for the property is correct for each version. foreach(ElementTypeChange change in changes) { - _writer.WriteIndented($"[DeclaredType(Type = typeof({change.DeclaredTypeReference.PropertyTypeString})"); + _writer.WriteIndented($"[DeclaredType(typeof({change.DeclaredTypeReference.PropertyTypeString})"); _writer.Write($", Since = FhirRelease.{change.Since}"); _writer.WriteLine(")]"); } @@ -3475,12 +3475,7 @@ private void WritePrimitiveType( _writer.WriteLineIndented( "[FhirElement(\"value\", IsPrimitiveValue=true, XmlSerialization=XmlRepresentation.XmlAttr, InSummary=true, Order=30)]"); - _writer.WriteLineIndented($"[DeclaredType(Type = typeof({getSystemTypeForFhirType(primitive.Name)}))]"); - - if (PrimitiveValidationPatterns.TryGetValue(primitive.Name, out string? primitivePattern)) - { - _writer.WriteLineIndented($"[{primitivePattern}]"); - } + _writer.WriteLineIndented($"[DeclaredType(typeof({getSystemTypeForFhirType(primitive.Name)}))]"); _writer.WriteLineIndented("[DataMember]"); diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirelyCommon.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirelyCommon.cs index e967e2a4d..0b5e83c48 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirelyCommon.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirelyCommon.cs @@ -65,22 +65,6 @@ public static class CSharpFirelyCommon { "Resource", "DomainResource" }, }; - /// Primitive types that have a specific validation attribute on their Value property. - public static readonly Dictionary PrimitiveValidationPatterns = new() - { - ["uri"] = "UriPattern", - ["uuid"] = "UuidPattern", - ["id"] = "IdPattern", - ["date"] = "DatePattern", - ["dateTime"] = "DateTimePattern", - ["oid"] = "OidPattern", - ["code"] = "CodePattern", - ["time"] = "TimePattern", - ["string"] = "StringPattern", - ["markdown"] = "StringPattern", - ["xhtml"] = "NarrativeXhtmlPattern" - }; - /// /// Determines the subset of code to generate. /// From bd952d9b404968b1ec3da79186d73973bcae43cc Mon Sep 17 00:00:00 2001 From: Kasdejong Date: Tue, 25 Mar 2025 17:09:59 +0100 Subject: [PATCH 31/52] changed check type to IList since List is invariant --- .../Language/Firely/CSharpFirely2.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 6bfcf4821..a80535f52 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2183,7 +2183,7 @@ private void WriteDictionaryTrySetValue(string exportName, List)) throw new ArgumentException(\"Value must be a Base or IEnumerable\", nameof(value));"); + _writer.WriteLineIndented("if(value is not (null or Hl7.Fhir.Model.Base or IList)) throw new ArgumentException(\"Value must be a Base or IEnumerable\", nameof(value));"); // switch _writer.WriteLineIndented("switch (key)"); @@ -3588,6 +3588,7 @@ private void WriteHeaderComplexDataType() WriteGenerationComment(); _writer.WriteLineIndented("using System;"); + _writer.WriteLineIndented("using System.Collections;"); _writer.WriteLineIndented("using System.Collections.Generic;"); _writer.WriteLineIndented("using System.Linq;"); _writer.WriteLineIndented("using System.Runtime.Serialization;"); From b222fc5843dbe9075d2b86720b73272c437b3a27 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Fri, 4 Apr 2025 12:48:19 +0200 Subject: [PATCH 32/52] Rename DeclaredType to AllowedTypes. --- .../Language/Firely/CSharpFirely2.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 946226402..122f1fca0 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2183,7 +2183,7 @@ private void WriteDictionaryTrySetValue(string exportName, List)) throw new ArgumentException(\"Value must be a Base or IEnumerable\", nameof(value));"); + _writer.WriteLineIndented("if(value is not (null or Hl7.Fhir.Model.Base or IList)) throw new ArgumentException(\"Value must be a Base or a list of Base\", nameof(value));"); // switch _writer.WriteLineIndented("switch (key)"); @@ -2788,7 +2788,7 @@ private void WriteElement( { WriteFhirElementAttribute(name, summary, isModifier, element, orderOffset, ", Choice = ChoiceType.DatatypeChoice", fiveWs); WriteFhirElementAttribute(name, summary, isModifier, element, orderOffset, "", fiveWs, since: since); - _writer.WriteLineIndented($"[DeclaredType(typeof(ResourceReference), Since = FhirRelease.R4)]"); + _writer.WriteLineIndented($"[AllowedTypes(typeof(ResourceReference), Since = FhirRelease.R4)]"); } else { @@ -2797,12 +2797,12 @@ private void WriteElement( if (ei.PropertyType is CqlTypeReference ctr) { - _writer.WriteLineIndented($"[DeclaredType(typeof({ctr.DeclaredTypeString}))]"); + _writer.WriteLineIndented($"[AllowedTypes(typeof({ctr.DeclaredTypeString}))]"); } if (TryGetPrimitiveType(ei.PropertyType, out PrimitiveTypeReference? ptr) && ptr is CodedTypeReference) { - _writer.WriteLineIndented("[DeclaredType(typeof(Code))]"); + _writer.WriteLineIndented("[AllowedTypes(typeof(Code))]"); } if (!string.IsNullOrEmpty(element.cgBindingName())) @@ -2830,7 +2830,7 @@ private void WriteElement( // the metadata for the property is correct for each version. foreach(ElementTypeChange change in changes) { - _writer.WriteIndented($"[DeclaredType(typeof({change.DeclaredTypeReference.PropertyTypeString})"); + _writer.WriteIndented($"[AllowedTypes(typeof({change.DeclaredTypeReference.PropertyTypeString})"); _writer.Write($", Since = FhirRelease.{change.Since}"); _writer.WriteLine(")]"); } @@ -3475,7 +3475,7 @@ private void WritePrimitiveType( _writer.WriteLineIndented( "[FhirElement(\"value\", IsPrimitiveValue=true, XmlSerialization=XmlRepresentation.XmlAttr, InSummary=true, Order=30)]"); - _writer.WriteLineIndented($"[DeclaredType(typeof({getSystemTypeForFhirType(primitive.Name)}))]"); + _writer.WriteLineIndented($"[AllowedTypes(typeof({getSystemTypeForFhirType(primitive.Name)}))]"); _writer.WriteLineIndented("[DataMember]"); From ad1e376e074aa9ff3c6e8bd06ae9f7a2f2c55160 Mon Sep 17 00:00:00 2001 From: Kasdejong Date: Tue, 8 Apr 2025 10:39:24 +0200 Subject: [PATCH 33/52] Removed obsolete comment --- .../Language/Firely/CSharpFirely2.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 9937638ef..fcbfb4d36 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2816,10 +2816,6 @@ private void WriteElement( _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); From 99c4f483b1a99073725c97f9aaf59fd2b709f464 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Tue, 8 Apr 2025 10:41:40 +0200 Subject: [PATCH 34/52] Add Collections to usings. --- .../Language/Firely/CSharpFirely2.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 122f1fca0..48f2d2484 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -883,6 +883,7 @@ private void WriteModelInfo( WriteGenerationComment(); _writer.WriteLineIndented("using System;"); + _writer.WriteLineIndented("using System.Collections;"); _writer.WriteLineIndented("using System.Collections.Generic;"); _writer.WriteLineIndented("using Hl7.Fhir.Introspection;"); _writer.WriteLineIndented("using Hl7.Fhir.Validation;"); @@ -3583,6 +3584,7 @@ private void WriteHeaderComplexDataType() WriteGenerationComment(); _writer.WriteLineIndented("using System;"); + _writer.WriteLineIndented("using System.Collections;"); _writer.WriteLineIndented("using System.Collections.Generic;"); _writer.WriteLineIndented("using System.Linq;"); _writer.WriteLineIndented("using System.Runtime.Serialization;"); From 03a5351634ee404647a91e583e7444d3821813a8 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Tue, 8 Apr 2025 10:51:55 +0200 Subject: [PATCH 35/52] Cleanup of Attachment.size allowedattributes --- .../Language/Firely/CSharpFirely2.cs | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index dd0b20834..c666fb352 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2813,23 +2813,9 @@ 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)); - - 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($"[AllowedTypes(typeof({change.DeclaredTypeReference.PropertyTypeString})"); - _writer.Write($", Since = FhirRelease.{change.Since}"); - _writer.WriteLine(")]"); + _writer.WriteIndented(BuildAllowedTypesAttribute([change.DeclaredTypeReference], change.Since)); } } From 065312f6b1ffbb34d4d2a611588436768623faea Mon Sep 17 00:00:00 2001 From: Kasdejong Date: Tue, 8 Apr 2025 11:25:20 +0200 Subject: [PATCH 36/52] this should be a writeline --- .../Language/Firely/CSharpFirely2.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index c666fb352..7269efd5d 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2815,7 +2815,7 @@ private void WriteElement( { foreach(ElementTypeChange change in changes) { - _writer.WriteIndented(BuildAllowedTypesAttribute([change.DeclaredTypeReference], change.Since)); + _writer.WriteLineIndented(BuildAllowedTypesAttribute([change.DeclaredTypeReference], change.Since)); } } From 3109429d3439ab2faa353137b65a12f3cfedfa9f Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Tue, 8 Apr 2025 16:44:35 +0200 Subject: [PATCH 37/52] Only used [AllowedTypes] where applicable. --- .../Language/Firely/CSharpFirely2.cs | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 7269efd5d..1b047c431 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2704,15 +2704,22 @@ private void WriteElements( orderOffset); } } - 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) + private void WriteFhirElementAttribute(string name, string summary, string? isModifier, ElementDefinition element, + string choice, string fiveWs, string? since = null, (string, string)? until = null, + string? xmlSerialization = null, string? declaredType = null) { var xmlser = xmlSerialization is null ? null : $", XmlSerialization = XmlRepresentation.{xmlSerialization}"; string attributeText = $"[FhirElement(\"{name}\"{xmlser}{summary}{isModifier}, Order={GetOrder(element)}{choice}{fiveWs}"; - if (since is { }) + if (since is not null) { attributeText += $", Since=FhirRelease.{since}"; } + if (declaredType is not null) + { + attributeText += $", DeclaredType={declaredType}"; + } + attributeText += ")]"; _writer.WriteLineIndented(attributeText); @@ -2782,28 +2789,18 @@ private void WriteElement( if (path == "OperationOutcome.issue.severity") { - WriteFhirElementAttribute(name, summary, ", IsModifier=true", element, orderOffset, choice, fiveWs); - WriteFhirElementAttribute(name, summary, null, element, orderOffset, choice, fiveWs, since: "R4"); + WriteFhirElementAttribute(name, summary, ", IsModifier=true", element, choice, fiveWs); + WriteFhirElementAttribute(name, summary, null, element, choice, fiveWs, since: "R4"); } else if (path is "Signature.who" or "Signature.onBehalfOf") { - WriteFhirElementAttribute(name, summary, isModifier, element, orderOffset, ", Choice = ChoiceType.DatatypeChoice", fiveWs); - WriteFhirElementAttribute(name, summary, isModifier, element, orderOffset, "", fiveWs, since: since); + WriteFhirElementAttribute(name, summary, isModifier, element, ", Choice = ChoiceType.DatatypeChoice", fiveWs); + WriteFhirElementAttribute(name, summary, isModifier, element, "", fiveWs, since: since); _writer.WriteLineIndented($"[AllowedTypes(typeof(ResourceReference), Since = FhirRelease.R4)]"); } else { - WriteFhirElementAttribute(name, summary, isModifier, element, orderOffset, choice, fiveWs, since, until, xmlSerialization); - } - - if (ei.PropertyType is CqlTypeReference ctr) - { - _writer.WriteLineIndented($"[AllowedTypes(typeof({ctr.DeclaredTypeString}))]"); - } - - if (TryGetPrimitiveType(ei.PropertyType, out PrimitiveTypeReference? ptr) && ptr is CodedTypeReference) - { - _writer.WriteLineIndented("[AllowedTypes(typeof(Code))]"); + WriteFhirElementAttribute(name, summary, isModifier, element, choice, fiveWs, since, until, xmlSerialization); } if (!string.IsNullOrEmpty(element.cgBindingName())) @@ -3260,7 +3257,6 @@ internal static void BuildElementOptionalFlags( if (elementType == "Resource") { choice = ", Choice=ChoiceType.ResourceChoice"; - allowedTypes = $"[AllowedTypes(typeof({Namespace}.Resource))]"; } } else @@ -3458,7 +3454,6 @@ private void WritePrimitiveType( _writer.WriteLineIndented( "[FhirElement(\"value\", IsPrimitiveValue=true, XmlSerialization=XmlRepresentation.XmlAttr, InSummary=true, Order=30)]"); - _writer.WriteLineIndented($"[AllowedTypes(typeof({getSystemTypeForFhirType(primitive.Name)}))]"); _writer.WriteLineIndented("[DataMember]"); From 1f7755ecf9051d46a4b78a0cd56722f1026296e8 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Wed, 23 Apr 2025 13:25:33 +0200 Subject: [PATCH 38/52] No longer touches the overflow when copying pocos when its empty. --- .../Language/Firely/CSharpFirely2.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 1b047c431..44c5c2a15 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2314,7 +2314,7 @@ private void WriteCopyTo( _writer.WriteLineIndented("dest.annotations.AddRange(annotations);"); _writer.DecreaseIndent(); _writer.WriteLine(string.Empty); - _writer.WriteLineIndented("if (Overflow.Any())"); + _writer.WriteLineIndented("if (HasOverflow)"); _writer.IncreaseIndent(); _writer.WriteLineIndented("Overflow.CopyToInternal(dest.Overflow);"); _writer.DecreaseIndent(); From 3d263d071d6a41e70bc71d46a1e548ab913c29ad Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Thu, 24 Apr 2025 13:25:28 +0200 Subject: [PATCH 39/52] Renamed ObjectValue to JsonValue --- .../Language/Firely/CSharpFirely2.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 44c5c2a15..a5b317797 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2280,7 +2280,7 @@ private void WriteCompareChildren( if (exportName == "PrimitiveType") { - _writer.WriteLineIndented("return Equals(ObjectValue, otherT.ObjectValue);"); + _writer.WriteLineIndented("return Equals(JsonValue, otherT.JsonValue);"); _writer.WriteLine(string.Empty); } else @@ -2343,7 +2343,7 @@ private void WriteCopyTo( } if (exportName == "PrimitiveType") - _writer.WriteLineIndented("if (ObjectValue != null) dest.ObjectValue = ObjectValue;"); + _writer.WriteLineIndented("if (JsonValue != null) dest.JsonValue = JsonValue;"); CloseScope(); } @@ -3461,8 +3461,8 @@ private void WritePrimitiveType( OpenScope(); var typeNameInSwitch = typeName.EndsWith("?") ? typeName[..^1] : typeName; - _writer.WriteLineIndented($"get {{ return ObjectValue is {typeNameInSwitch} or null ? ({nullableTypeName})ObjectValue : throw COVE.FromTypes(typeof({exportName}), ObjectValue); }}"); - _writer.WriteLineIndented("set { ObjectValue = value; OnPropertyChanged(\"Value\"); }"); + _writer.WriteLineIndented($"get {{ return JsonValue is {typeNameInSwitch} or null ? ({nullableTypeName})JsonValue : throw COVE.FromTypes(typeof({exportName}), JsonValue); }}"); + _writer.WriteLineIndented("set { JsonValue = value; OnPropertyChanged(\"Value\"); }"); CloseScope(); } From 06a3aa194a7d40be6c46c687e9dda4deec402acb Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Thu, 8 May 2025 10:28:24 +0200 Subject: [PATCH 40/52] Generated [AllowNull] attributes. --- .../Language/Firely/CSharpFirely2.cs | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index a5b317797..d499eb2a6 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -1137,10 +1137,8 @@ private void WriteFhirToCs( /// Writes the FHIR version. private void WriteFhirVersion() { - _writer.WriteLineIndented("public static string Version"); - OpenScope(); - _writer.WriteLineIndented($"get {{ return \"{_info.FhirVersionLiteral}\"; }}"); - CloseScope(); + _writer.WriteLineIndented($"""public static string Version => "{_info.FhirVersionLiteral}";"""); + _writer.WriteLine(); } /// Writes the supported resources dictionary. @@ -1638,26 +1636,27 @@ private void WriteInterfaceElementGettersAndSetters( WrittenElementInfo interfaceEi) { string pn = interfaceEi.PropertyName; - string it = interfaceEi.PropertyType is ListTypeReference ? interfaceEi.PropertyType.PropertyTypeString : WithNullabilityMarking(interfaceEi.PropertyType.PropertyTypeString); + bool isList = interfaceEi.PropertyType is ListTypeReference; + string it = isList ? interfaceEi.PropertyType.PropertyTypeString : WithNullabilityMarking(interfaceEi.PropertyType.PropertyTypeString); if ((resourceEd == null) || (resourceEi == null)) { - writeEmptyGetterAndSetter(it, pn); + writeEmptyGetterAndSetter(it, pn, isList); } else if (interfaceEi.PropertyType.PropertyTypeString == resourceEi.PropertyType.PropertyTypeString) { - writeOneOnOneGetterAndSetter(it, pn); + writeOneOnOneGetterAndSetter(it, pn, isList); } // a resource is allowed to have a scalar in place of a list else if ((interfaceEi.PropertyType is ListTypeReference interfaceLtr) && (interfaceLtr.Element.PropertyTypeString == resourceEi.PropertyType.PropertyTypeString)) { - writeSingleToListGetterAndSetter(it, pn); + writeSingleToListGetterAndSetter(it, pn, isList); } else { - writeIncompatibleGetterSetter(it, pn); + writeIncompatibleGetterSetter(it, pn, isList); } if (!TryGetPrimitiveType(interfaceEi.PropertyType, out PrimitiveTypeReference? interfacePtr)) @@ -1666,25 +1665,27 @@ private void WriteInterfaceElementGettersAndSetters( } string ppn = interfaceEi.PrimitiveHelperName!; - string pit = interfaceEi.PropertyType is ListTypeReference ? interfacePtr.ConveniencePropertyTypeString : WithNullabilityMarking(interfacePtr.ConveniencePropertyTypeString); + string pit = isList ? interfacePtr.ConveniencePropertyTypeString : WithNullabilityMarking(interfacePtr.ConveniencePropertyTypeString); if ((resourceEd == null) || (resourceEi == null)) { - writeEmptyGetterAndSetter(pit, ppn); + writeEmptyGetterAndSetter(pit, ppn, isList); } else if (interfaceEi.PropertyType == resourceEi.PropertyType) { - writeOneOnOneGetterAndSetter(pit, ppn); + writeOneOnOneGetterAndSetter(pit, ppn, isList); } else { - writeIncompatibleGetterSetter(pit, ppn); + writeIncompatibleGetterSetter(pit, ppn, isList); } return; - void writeOneOnOneGetterAndSetter(string propertyType, string propertyName) + void writeOneOnOneGetterAndSetter(string propertyType, string propertyName, bool allowNull) { + if(allowNull) + _writer.WriteLineIndented("[AllowNull]"); _writer.WriteLineIndented("[IgnoreDataMember]"); _writer.WriteLineIndented($"{propertyType} {interfaceExportName}.{propertyName}"); OpenScope(); @@ -1693,8 +1694,10 @@ void writeOneOnOneGetterAndSetter(string propertyType, string propertyName) CloseScope(); } - void writeSingleToListGetterAndSetter(string propertyType, string propertyName) + void writeSingleToListGetterAndSetter(string propertyType, string propertyName, bool allowNull) { + if (allowNull) + _writer.WriteLineIndented("[AllowNull]"); _writer.WriteLineIndented("[IgnoreDataMember]"); _writer.WriteLineIndented($"{propertyType} {interfaceExportName}.{propertyName}"); OpenScope(); @@ -1715,12 +1718,14 @@ void writeSingleToListGetterAndSetter(string propertyType, string propertyName) CloseScope(); } - void writeIncompatibleGetterSetter(string propertyType, string propertyName) + void writeIncompatibleGetterSetter(string propertyType, string propertyName, bool allowNull) { string message = $"{resourceExportName}.{resourceEi.PropertyName} is incompatible with " + $"{interfaceExportName}.{interfaceEi.FhirElementName}."; WriteIndentedComment(message, isSummary: false, isRemarks: true); + if (allowNull) + _writer.WriteLineIndented("[AllowNull]"); _writer.WriteLineIndented("[IgnoreDataMember]"); _writer.WriteLineIndented($"{propertyType} {interfaceExportName}.{propertyName}"); @@ -1732,8 +1737,10 @@ void writeIncompatibleGetterSetter(string propertyType, string propertyName) string emptyInterfaceType() => interfaceEi.PropertyType is ListTypeReference ? "[]" : "null"; - void writeEmptyGetterAndSetter(string propertyType, string propertyName) + void writeEmptyGetterAndSetter(string propertyType, string propertyName, bool allowNull) { + if (allowNull) + _writer.WriteLineIndented("[AllowNull]"); _writer.WriteLineIndented("[IgnoreDataMember]"); _writer.WriteLineIndented($"{propertyType} {interfaceExportName}.{propertyName}"); OpenScope(); @@ -1782,6 +1789,7 @@ private void WriteInterfaceElements( var typ = ei.PropertyType is ListTypeReference ? ei.PropertyType.PropertyTypeString : WithNullabilityMarking(ei.PropertyType.PropertyTypeString); + _writer.WriteLineIndented("[AllowNull]"); _writer.WriteLineIndented($"{typ} {ei.PropertyName} {{ get; set; }}"); _writer.WriteLine(); } @@ -3031,6 +3039,8 @@ private void WriteElementGettersAndSetters(ElementDefinition element, WrittenEle { } s => s }; + if(ei.PropertyType is ListTypeReference) + _writer.WriteLineIndented("[AllowNull]"); _writer.WriteLineIndented($"public {(ei.PropertyType is ListTypeReference ltr ? ltr.PropertyTypeString : $"{ei.PropertyType.PropertyTypeString}?")} {ei.PropertyName}"); OpenScope(); @@ -3314,7 +3324,7 @@ private void WritePropertyTypeName(string name) { WriteIndentedComment("FHIR Type Name"); - _writer.WriteLineIndented($"public override string TypeName {{ get {{ return \"{name}\"; }} }}"); + _writer.WriteLineIndented($"""public override string TypeName => "{name}";"""); _writer.WriteLine(string.Empty); } From 7db97d89f6c87533516256bb1579e16ff5eadf44 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Fri, 23 May 2025 10:50:29 +0200 Subject: [PATCH 41/52] Changes to be able to generate FHIR R6. --- .../Language/Firely/CSharpFirely2.cs | 19 ++++++++++++++----- .../Loader/PackageLoader.cs | 2 +- .../Models/DefinitionCollection.cs | 8 +++++++- .../Packaging/FhirReleases.cs | 2 +- .../Properties/launchSettings.json | 14 ++++++++++++-- 5 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index c665a4141..7e1aa3fd8 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -420,18 +420,20 @@ public void Export(object untypedOptions, DefinitionCollection info) } // only generate base definitions for R5 - if (subset.HasFlag(GenSubset.Base) && info.FhirSequence != FhirReleases.FhirSequenceCodes.R5) + if (subset.HasFlag(GenSubset.Base) && + info.FhirSequence is not (FhirReleases.FhirSequenceCodes.R6 or FhirReleases.FhirSequenceCodes.R5)) { - Console.WriteLine($"Aborting {LanguageName} for {info.FhirSequence}: code generation for the 'base' subset should be run on R5 only."); + Console.WriteLine($"Aborting {LanguageName} for {info.FhirSequence}: code generation for the 'base' subset should be run on R5/R6 only."); return; } // conformance subset is only valid for STU3 and R5 if (subset.HasFlag(GenSubset.Conformance) && - (info.FhirSequence != FhirReleases.FhirSequenceCodes.STU3 && - info.FhirSequence != FhirReleases.FhirSequenceCodes.R5)) + info.FhirSequence is not (FhirReleases.FhirSequenceCodes.STU3 or + FhirReleases.FhirSequenceCodes.R5 or FhirReleases.FhirSequenceCodes.R6)) + { - Console.WriteLine($"Aborting {LanguageName} for {info.FhirSequence}: code generation for the 'conformance' subset should be run on STU3 or R5 only."); + Console.WriteLine($"Aborting {LanguageName} for {info.FhirSequence}: code generation for the 'conformance' subset should be run on STU3 or R5/R6 only."); return; } @@ -936,6 +938,13 @@ private void WriteSearchParameters() foreach (SearchParameter sp in resourceSearchParams.Values.OrderBy(s => s.Name)) { + // TODO:R6: Add support for the 'resource' search parameter type + if ((sp.TypeElement.ObjectValue is "resource")) + { + Console.WriteLine($"Skipping SearchParameter {sp.Id} ({sp.Url}) because it is target type of 'resource'!!!"); + continue; + } + if (sp.Experimental == true) { continue; diff --git a/src/Microsoft.Health.Fhir.CodeGen/Loader/PackageLoader.cs b/src/Microsoft.Health.Fhir.CodeGen/Loader/PackageLoader.cs index 2ee8e348e..75126bda7 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Loader/PackageLoader.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Loader/PackageLoader.cs @@ -126,7 +126,7 @@ public PackageLoader(ConfigRoot? config = null, LoaderOptions? opts = null) _defaultFhirVersion = FhirReleases.FhirVersionToSequence(_rootConfiguration.FhirVersion); - _jsonOptions = opts.FhirJsonOptions; + _jsonOptions = opts.FhirJsonOptions.UsingMode(DeserializerModes.Ostrich); _jsonParser = new(opts.FhirJsonSettings); #if !DISABLE_XML diff --git a/src/Microsoft.Health.Fhir.CodeGen/Models/DefinitionCollection.cs b/src/Microsoft.Health.Fhir.CodeGen/Models/DefinitionCollection.cs index c6baa3412..ce2c877b2 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Models/DefinitionCollection.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Models/DefinitionCollection.cs @@ -854,8 +854,14 @@ private void ProcessParameters(OperationDefinition op) fo = outFieldOrder.Count + 1; outFieldOrder.Add(pc.Name, fo); } - else + else { + if (inFieldOrder.ContainsKey(pc.Name)) + { + Console.WriteLine($"Operation: {op.Id} ({op.Url}) defines the parameter {pc.Name} more than once!!!"); + continue; + } + fo = inFieldOrder.Count + 1; inFieldOrder.Add(pc.Name, fo); } diff --git a/src/Microsoft.Health.Fhir.CodeGenCommon/Packaging/FhirReleases.cs b/src/Microsoft.Health.Fhir.CodeGenCommon/Packaging/FhirReleases.cs index b6617fc12..f2ee611a3 100644 --- a/src/Microsoft.Health.Fhir.CodeGenCommon/Packaging/FhirReleases.cs +++ b/src/Microsoft.Health.Fhir.CodeGenCommon/Packaging/FhirReleases.cs @@ -37,7 +37,7 @@ public enum FhirSequenceCodes : int /// FHIR R5. R5 = 5, - /// FHIR R5. + /// FHIR R6. R6 = 6, } diff --git a/src/fhir-codegen/Properties/launchSettings.json b/src/fhir-codegen/Properties/launchSettings.json index 0ad39befd..5af0191f3 100644 --- a/src/fhir-codegen/Properties/launchSettings.json +++ b/src/fhir-codegen/Properties/launchSettings.json @@ -35,9 +35,19 @@ "commandLineArgs": "generate CSharpFirely2 --output-path ..\\..\\..\\firely-net-sdk\\src\\Hl7.Fhir.R5\\Model -p hl7.fhir.r5.core#5.0.0 -p hl7.fhir.r5.expansions#5.0.0 --subset satellite", "workingDirectory": "$(MSBuildProjectDirectory)" }, - "Firely 5.x R6": { + "NEW Firely 5.x R6": { "commandName": "Project", - "commandLineArgs": "generate CSharpFirely2 --output-path ..\\..\\..\\firely-net-sdk\\src\\Hl7.Fhir.R6\\Model -p hl7.fhir.r6.core#6.0.0-ballot2 -p hl7.fhir.r6.expansions#6.0.0-ballot2 --subset satellite", + "commandLineArgs": "generate CSharpFirely2 --output-path ..\\..\\..\\firely-net-sdk\\src\\Hl7.Fhir.R6\\Model -p hl7.fhir.r6.core#6.0.0-ballot3 -p hl7.fhir.r6.expansions#6.0.0-ballot3 --subset satellite", + "workingDirectory": "$(MSBuildProjectDirectory)" + }, + "NEW Firely 6.x Base": { + "commandName": "Project", + "commandLineArgs": "generate CSharpFirely2 --output-path ..\\..\\..\\firely-net-sdk\\src\\Hl7.Fhir.Base\\Model -p hl7.fhir.r6.core#6.0.0-ballot3 -p hl7.fhir.r6.expansions#6.0.0-ballot3 --subset base", + "workingDirectory": "$(MSBuildProjectDirectory)" + }, + "NEW Firely 6.x Conformance": { + "commandName": "Project", + "commandLineArgs": "generate CSharpFirely2 --output-path ..\\..\\..\\firely-net-sdk\\src\\Hl7.Fhir.Conformance\\Model -p hl7.fhir.r6.core#6.0.0-ballot3 -p hl7.fhir.r6.expansions#6.0.0-ballot3 --subset conformance", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Firely IG Backport": { From 149d65bad93b96a86205d32dc32bf4447d300387 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Fri, 23 May 2025 13:50:42 +0200 Subject: [PATCH 42/52] Update launchSettings.json Small correction in name of one of the launchsettings --- src/fhir-codegen/Properties/launchSettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fhir-codegen/Properties/launchSettings.json b/src/fhir-codegen/Properties/launchSettings.json index 5af0191f3..47f2a4532 100644 --- a/src/fhir-codegen/Properties/launchSettings.json +++ b/src/fhir-codegen/Properties/launchSettings.json @@ -35,7 +35,7 @@ "commandLineArgs": "generate CSharpFirely2 --output-path ..\\..\\..\\firely-net-sdk\\src\\Hl7.Fhir.R5\\Model -p hl7.fhir.r5.core#5.0.0 -p hl7.fhir.r5.expansions#5.0.0 --subset satellite", "workingDirectory": "$(MSBuildProjectDirectory)" }, - "NEW Firely 5.x R6": { + "NEW Firely 6.x R6": { "commandName": "Project", "commandLineArgs": "generate CSharpFirely2 --output-path ..\\..\\..\\firely-net-sdk\\src\\Hl7.Fhir.R6\\Model -p hl7.fhir.r6.core#6.0.0-ballot3 -p hl7.fhir.r6.expansions#6.0.0-ballot3 --subset satellite", "workingDirectory": "$(MSBuildProjectDirectory)" From 69f44d3e16c6581d9698ae16bf7828336c46e965 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Fri, 20 Jun 2025 17:10:47 +0200 Subject: [PATCH 43/52] Changes needed to get R6 to compile. --- .../ElementDefinitionExtensions.cs | 8 +- .../Language/Firely/CSharpFirely2.cs | 223 +++++++----------- .../Models/ComponentDefinition.cs | 2 +- .../Properties/launchSettings.json | 6 +- 4 files changed, 88 insertions(+), 151 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/FhirExtensions/ElementDefinitionExtensions.cs b/src/Microsoft.Health.Fhir.CodeGen/FhirExtensions/ElementDefinitionExtensions.cs index 2bba61f4e..aba2ec179 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/FhirExtensions/ElementDefinitionExtensions.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/FhirExtensions/ElementDefinitionExtensions.cs @@ -78,7 +78,7 @@ public static int cgComponentFieldOrder(this ElementDefinition ed) public static string cgBasePath(this ElementDefinition ed) => ed.Base?.Path ?? string.Empty; /// Gets the explicit name of this element if set, or returns an empty string. - public static string cgExplicitName(this ElementDefinition ed) => ed.GetExtensionValue(CommonDefinitions.ExtUrlExplicitTypeName)?.ToString() ?? string.Empty; + public static string? cgExplicitName(this ElementDefinition ed) => ed.GetExtensionValue(CommonDefinitions.ExtUrlExplicitTypeName)?.ToString(); /// Gets the short name of this element, or explicit name if there is one. /// The ed to act on. @@ -89,12 +89,10 @@ public static string cgName(this ElementDefinition ed, bool allowExplicitName = { if (allowExplicitName) { - string en = ed.cgExplicitName(); + string? en = ed.cgExplicitName(); - if (!string.IsNullOrEmpty(en)) - { + if (en is not null) return en; - } } if (removeChoiceMarker && ed.Path.EndsWith("[x]", StringComparison.Ordinal)) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 7e1aa3fd8..9f9dc0d69 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -387,6 +387,32 @@ private record SinceVersion(FhirReleases.FhirSequenceCodes Since); ["Signature.contentType"] = ("R4", "") }; + private static string? GetExplicitName(ElementDefinition ed, FhirReleases.FhirSequenceCodes sequence) => + (ed.Path, sequence) switch + { + ("Evidence.statistic.attributeEstimate.attributeEstimate", _) => "AttributeEstimate", + ("Citation.citedArtifact.contributorship.summary", _) => "CitedArtifactContributorshipSummary", + ("Measure.group", FhirReleases.FhirSequenceCodes.R6) => "GroupBackboneComponent", + ("Measure.group.component", FhirReleases.FhirSequenceCodes.R6) => "GroupComponent", + ("Measure.group.stratifier.component", FhirReleases.FhirSequenceCodes.R6) => "GroupStratifierComponent", + ("MolecularDefinition.location.sequenceLocation.coordinateInterval", FhirReleases.FhirSequenceCodes.R6) => + "LocationSequenceLocationCoordinateIntervalComponent", + ("MolecularDefinition.location.sequenceLocation.coordinateInterval.coordinateSystem", FhirReleases.FhirSequenceCodes.R6) => + "LocationSequenceLocationCoordinateIntervalCoordinateSystemComponent", + ("MolecularDefinition.representation.extracted.coordinateInterval", FhirReleases.FhirSequenceCodes.R6) => + "RepresentationExtractedCoordinateIntervalComponent", + ("MolecularDefinition.representation.extracted.coordinateInterval.coordinateSystem", FhirReleases.FhirSequenceCodes.R6) => + "RepresentationExtractedCoordinateIntervalCoordinateSystemComponent", + ("MolecularDefinition.representation.relative.edit.coordinateInterval", FhirReleases.FhirSequenceCodes.R6) => + "RepresentationRelativeEditCoordinateIntervalComponent", + ("MolecularDefinition.representation.relative.edit.coordinateInterval.coordinateSystem", FhirReleases.FhirSequenceCodes.R6) => + "RepresentationRelativeEditCoordinateIntervalCoordinateSystemComponent", + + ("TestPlan.scope", FhirReleases.FhirSequenceCodes.R6) => "ScopeComponent", + ("TestPlan.testCase.scope", FhirReleases.FhirSequenceCodes.R6) => "TestCaseScopeComponent", + _ => ed.cgExplicitName() + }; + /// True to export five ws. private bool _exportFiveWs = true; @@ -1575,29 +1601,6 @@ private void WriteInterfaceComponent( // open class OpenScope(); - // right now, no interfaces have components - TODO: determine naming convention if this comes up - //foreach (ComponentDefinition component in complex.cgChildComponents(_info)) - //{ - // string componentExportName; - // if (string.IsNullOrEmpty(component.cgExplicitName())) - // { - // componentExportName = - // $"{component.cgName(NamingConvention.PascalCase)}Component"; - // } - // else - // { - // // Consent.provisionActorComponent is explicit lower case... - // componentExportName = - // $"{component.cgExplicitName()}" + - // $"Component"; - // } - // WriteBackboneComponent( - // component, - // componentExportName, - // exportName, - // subset); - //} - WriteInterfaceElements(complex, exportName, ref exportedElements); // close class @@ -1983,16 +1986,14 @@ private void WriteComponent( { string componentExportName; - if (string.IsNullOrEmpty(component.cgExplicitName())) + if (GetExplicitName(component.Element, _info.FhirSequence) is {} explicitName) { - componentExportName = - $"{component.cgName(NamingConvention.PascalCase)}Component"; + componentExportName = $"{explicitName}Component"; } else { componentExportName = - $"{component.cgExplicitName()}" + - $"Component"; + $"{component.cgName(NamingConvention.PascalCase)}Component"; } WriteBackboneComponent( @@ -2567,44 +2568,12 @@ private void WriteBackboneComponent( WriteComponentComment(complex); - string explicitName = complex.cgExplicitName(); - - /* TODO(ginoc): 2024.06.28 - Special cases to remove in SDK 6.0 - * - Evidence.statistic.attributeEstimate.attributeEstimate the explicit name is duplicative and was not passed through. - * - Citation.citedArtifact.contributorship.summary had a generator prefix. - */ - switch (explicitName) - { - case "AttributeEstimateAttributeEstimate": - explicitName = "AttributeEstimate"; - break; - case "ContributorshipSummary": - explicitName = "CitedArtifactContributorshipSummary"; - break; - } - - // 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 (complex.Id.StartsWith("Citation") || complex.Id.StartsWith("Statistic") || complex.Id.StartsWith("DeviceDefinition")) - { - string parentName = complex.Id.Substring(0, complex.Id.IndexOf('.')); - var sillyBackboneName = complex.Id.Substring(parentName.Length); - explicitName = capitalizeThoseSillyBackboneNames(sillyBackboneName); - exportName = explicitName + "Component"; - } - // end of repair - */ + string? explicitName = GetExplicitName(complex.Element, _info.FhirSequence); bool useConcatenationInName = complex.Structure.Name == "Citation"; - string explicitNamePart = string.IsNullOrEmpty(explicitName) - ? complex.cgName(NamingConvention.PascalCase, useConcatenationInName, useConcatenationInName) - : explicitName; + string explicitNamePart = explicitName ?? + complex.cgName(NamingConvention.PascalCase, useConcatenationInName, useConcatenationInName); string componentName = parentExportName + "#" + explicitNamePart; WriteSerializable(); @@ -2647,53 +2616,15 @@ private void WriteBackboneComponent( foreach (ComponentDefinition component in complex.cgChildComponents(_info)) { string componentExportName; - string componentExplicitName = component.cgExplicitName(); - if (string.IsNullOrEmpty(componentExplicitName)) + if (GetExplicitName(component.Element, _info.FhirSequence) is {} componentExplicitName) { - componentExportName = - $"{component.cgName(NamingConvention.PascalCase, useConcatenationInName, useConcatenationInName)}Component"; + componentExportName = $"{componentExplicitName}Component"; } else { - /* TODO(ginoc): 2024.06.28 - Special cases to remove in SDK 6.0 - * - Evidence.statistic.attributeEstimate.attributeEstimate the explicit name is duplicative and was not passed through. - * - Citation.citedArtifact.contributorship.summary had a generator prefix. - */ - - switch (componentExplicitName) - { - case "AttributeEstimateAttributeEstimate": - componentExportName = "AttributeEstimateComponent"; - break; - case "ContributorshipSummary": - componentExportName = "CitedArtifactContributorshipSummaryComponent"; - break; - default: - // Consent.provisionActorComponent is explicit lower case... - componentExportName = $"{component.cgExplicitName()}Component"; - break; - } - - ///* TODO(ginoc): 2024.06.28 - Special cases to remove in SDK 6.0 - // * - Consent.provision is explicit lower case in R4B and earlier - // * - Consent.provision.actor is explicit lower case in R4B and earlier - // */ - //if (_info.FhirSequence < FhirReleases.FhirSequenceCodes.R5) - //{ - // switch (complex.Element.Path) - // { - // case "Consent.provision": - // componentExportName = "provisionComponent"; - // break; - // case "Consent.provision.actor": - // componentExportName = "provisionActorComponent"; - // break; - // case "Consent.provision.data": - // componentExportName = "provisionDataComponent"; - // break; - // } - //} + componentExportName = + $"{component.cgName(NamingConvention.PascalCase, useConcatenationInName, useConcatenationInName)}Component"; } WriteBackboneComponent( @@ -2951,6 +2882,47 @@ private bool WriteEnum( _writer.WriteLineIndented($"{codeName},"); } + // HACK for short-term R6 support.... + // We need to add the FHIR version enum literals for R6, as they are not part of the + // R5 distribution, which we are using at this moment to generate Base/Conformance. + if (vs.Url == "http://hl7.org/fhir/ValueSet/FHIR-version") + { + _writer.WriteIndented( + """ + /// + /// R6 Versions. + /// (system: http://hl7.org/fhir/FHIR-version) + /// + [EnumLiteral("6.0"), Description("6.0")] + N6_0, + /// + /// R6 Final Version. + /// (system: http://hl7.org/fhir/FHIR-version) + /// + [EnumLiteral("6.0.0"), Description("6.0.0")] + N6_0_0, + /// + /// R6 1st Draft Ballot. + /// (system: http://hl7.org/fhir/FHIR-version) + /// + [EnumLiteral("6.0.0-ballo1"), Description("6.0.0-ballot1")] + N6_0_0Ballo1, + /// + /// R6 2nd Draft Ballot. + /// (system: http://hl7.org/fhir/FHIR-version) + /// + [EnumLiteral("6.0.0-ballot2"), Description("6.0.0-ballot2")] + N6_0_0Ballot2, + /// + /// R6 3rd Draft Ballot. + /// (system: http://hl7.org/fhir/FHIR-version) + /// + [EnumLiteral("6.0.0-ballot3"), Description("6.0.0-ballot3")] + N6_0_0Ballot3, + """); + } + + CloseScope(); _writtenValueSets.Add( @@ -3294,7 +3266,7 @@ string getTypeNameFromElement() // check to see if the referenced element has an explicit name if (info.TryFindElementByPath(btn, out StructureDefinition? targetSd, out ElementDefinition? targetEd)) { - return BuildTypeNameForNestedComplexType(targetEd, btn); + return BuildTypeNameForNestedComplexType(targetEd, btn, info.FhirSequence); } return btn; @@ -3308,7 +3280,7 @@ string getTypeNameFromElement() string getPocoNameForComplexTypeReference(string name) { return name.Contains('.') - ? BuildTypeNameForNestedComplexType(element, name) + ? BuildTypeNameForNestedComplexType(element, name, info.FhirSequence) : TypeReference.MapTypeName(name); } } @@ -3465,43 +3437,10 @@ PrimitiveTypeReference or /// The ed. /// The type. /// A string. - private static string BuildTypeNameForNestedComplexType(ElementDefinition ed, string type) + private static string BuildTypeNameForNestedComplexType(ElementDefinition ed, string type, FhirReleases.FhirSequenceCodes sequence) { - // 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")) + if (GetExplicitName(ed, sequence) is {} explicitTypeName) { - 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)) - { - /* TODO(ginoc): 2024.06.28 - Special cases to remove in SDK 6.0 - * - Evidence.statistic.attributeEstimate.attributeEstimate the explicit name is duplicative and was not passed through. - * - Citation.citedArtifact.contributorship.summary had a generator prefix. - */ - - switch (explicitTypeName) - { - case "AttributeEstimateAttributeEstimate": - explicitTypeName = "AttributeEstimate"; - break; - case "ContributorshipSummary": - explicitTypeName = "CitedArtifactContributorshipSummary"; - break; - } - string parentName = type.Substring(0, type.IndexOf('.')); return $"{parentName}" + $".{explicitTypeName}" + diff --git a/src/Microsoft.Health.Fhir.CodeGen/Models/ComponentDefinition.cs b/src/Microsoft.Health.Fhir.CodeGen/Models/ComponentDefinition.cs index c477ed497..932b79295 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Models/ComponentDefinition.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Models/ComponentDefinition.cs @@ -60,7 +60,7 @@ public ComponentDefinition(StructureDefinition sd) /// Cg explicit name. /// A string. - public string cgExplicitName() => Element?.cgExplicitName() ?? string.Empty; + public string? cgExplicitName() => Element.cgExplicitName(); /// Gets the code generation name. /// Note: Firely generation uses this version. diff --git a/src/fhir-codegen/Properties/launchSettings.json b/src/fhir-codegen/Properties/launchSettings.json index 5af0191f3..443c70b1e 100644 --- a/src/fhir-codegen/Properties/launchSettings.json +++ b/src/fhir-codegen/Properties/launchSettings.json @@ -35,17 +35,17 @@ "commandLineArgs": "generate CSharpFirely2 --output-path ..\\..\\..\\firely-net-sdk\\src\\Hl7.Fhir.R5\\Model -p hl7.fhir.r5.core#5.0.0 -p hl7.fhir.r5.expansions#5.0.0 --subset satellite", "workingDirectory": "$(MSBuildProjectDirectory)" }, - "NEW Firely 5.x R6": { + "Firely 5.x R6": { "commandName": "Project", "commandLineArgs": "generate CSharpFirely2 --output-path ..\\..\\..\\firely-net-sdk\\src\\Hl7.Fhir.R6\\Model -p hl7.fhir.r6.core#6.0.0-ballot3 -p hl7.fhir.r6.expansions#6.0.0-ballot3 --subset satellite", "workingDirectory": "$(MSBuildProjectDirectory)" }, - "NEW Firely 6.x Base": { + "DONT USE YET Firely 5.x Base (R6)": { "commandName": "Project", "commandLineArgs": "generate CSharpFirely2 --output-path ..\\..\\..\\firely-net-sdk\\src\\Hl7.Fhir.Base\\Model -p hl7.fhir.r6.core#6.0.0-ballot3 -p hl7.fhir.r6.expansions#6.0.0-ballot3 --subset base", "workingDirectory": "$(MSBuildProjectDirectory)" }, - "NEW Firely 6.x Conformance": { + "DONT USE YET Firely 5.x Conformance (R6)": { "commandName": "Project", "commandLineArgs": "generate CSharpFirely2 --output-path ..\\..\\..\\firely-net-sdk\\src\\Hl7.Fhir.Conformance\\Model -p hl7.fhir.r6.core#6.0.0-ballot3 -p hl7.fhir.r6.expansions#6.0.0-ballot3 --subset conformance", "workingDirectory": "$(MSBuildProjectDirectory)" From 679110a1d206a9b627ac05c126cb06315dbba793 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Mon, 23 Jun 2025 16:45:57 +0200 Subject: [PATCH 44/52] Caught a bug that actually activated a repair for the questionnaire enum. --- .../Language/Firely/CSharpFirely2.cs | 65 +++++-------------- 1 file changed, 17 insertions(+), 48 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 7e1aa3fd8..d4b13a7ef 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2787,15 +2787,6 @@ private bool WriteEnum( return false; } - FhirConcept[] concepts = vs.cgGetFlatConcepts(_info).ToArray(); - - if (concepts.Length == 0) - { - // TODO(ginoc): 2024.09.19 - do we want to start using a Terminology server to expand these? - // value set that cannot be expanded and does not have an expansion provided - return false; - } - string name = (vs.Name ?? vs.Id) .Replace(" ", string.Empty, StringComparison.Ordinal) .Replace("_", string.Empty, StringComparison.Ordinal); @@ -2849,50 +2840,28 @@ private bool WriteEnum( /* TODO(ginoc): 2024.07.01 - Special cases to remove in SDK 6.0 * - ValueSet http://hl7.org/fhir/ValueSet/item-type used to enumerate non-selectable: 'question' * - ValueSet http://hl7.org/fhir/ValueSet/v3-ActInvoiceGroupCode in STU3 used to enumerate non-selectable: '_ActInvoiceInterGroupCode' and '_ActInvoiceRootGroupCode' + * ewout: 20250623 - These invoice codes seem to have disappeared before, so we don't need to add them back in. */ - switch (vs.Url) + if(vs.Url == "http://hl7.org/fhir/ValueSet/item-type") { - case "http://hl7.org/fhir/ValueSet/item-type": + if (!vs.Expansion.Contains.Any(vsContains => vsContains.Code == "question")) + { + vs.Expansion.Contains.Insert(2, new ValueSet.ContainsComponent() { - if (!vs.Expansion.Contains.Any(vsContains => vsContains.Code == "question")) - { - vs.Expansion.Contains.Insert(2, new ValueSet.ContainsComponent() - { - System = "http://hl7.org/fhir/item-type", - Code = "question", - Display = "Question", - }); - } - } - break; + System = "http://hl7.org/fhir/item-type", + Code = "question", + Display = "Question", + }); + } + } - case "http://hl7.org/fhir/ValueSet/v3-ActInvoiceGroupCode": - { - // only care about the version present in STU3 - if (vs.Version == "2014-03-26") - { - if (!vs.Expansion.Contains.Any(vsContains => vsContains.Code == "_ActInvoiceInterGroupCode")) - { - vs.Expansion.Contains.Insert(0, new ValueSet.ContainsComponent() - { - System = "http://hl7.org/fhir/v3/ActCode", - Code = "_ActInvoiceInterGroupCode", - Display = "ActInvoiceInterGroupCode", - }); - } + FhirConcept[] concepts = vs.cgGetFlatConcepts(_info).ToArray(); - if (!vs.Expansion.Contains.Any(vsContains => vsContains.Code == "_ActInvoiceRootGroupCode")) - { - vs.Expansion.Contains.Insert(8, new ValueSet.ContainsComponent() - { - System = "http://hl7.org/fhir/v3/ActCode", - Code = "_ActInvoiceRootGroupCode", - Display = "ActInvoiceRootGroupCode", - }); - } - } - } - break; + if (concepts.Length == 0) + { + // TODO(ginoc): 2024.09.19 - do we want to start using a Terminology server to expand these? + // value set that cannot be expanded and does not have an expansion provided + return false; } var defaultSystem = GetDefaultCodeSystem(concepts); From e060257f601dbb0d58e5f87362ed747cd1d6b5de Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Mon, 23 Jun 2025 17:10:54 +0200 Subject: [PATCH 45/52] Shuffled the fix around again to make sure no header comments get generated. --- .../Language/Firely/CSharpFirely2.cs | 97 ++++++++++--------- 1 file changed, 49 insertions(+), 48 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index cefc8a59d..7a9c33e26 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2751,23 +2751,6 @@ private bool WriteEnum( return true; } - IEnumerable referencedCodeSystems = vs.cgReferencedCodeSystems(); - - if (referencedCodeSystems.Count() == 1) - { - WriteIndentedComment( - $"{vs.Description}\n" + - $"(url: {vs.Url})\n" + - $"(system: {referencedCodeSystems.First()})"); - } - else - { - WriteIndentedComment( - $"{vs.Description}\n" + - $"(url: {vs.Url})\n" + - $"(systems: {referencedCodeSystems.Count()})"); - } - /* TODO(ginoc): 2024.07.01 - Special cases to remove in SDK 6.0 * - ValueSet http://hl7.org/fhir/ValueSet/item-type used to enumerate non-selectable: 'question' * - ValueSet http://hl7.org/fhir/ValueSet/v3-ActInvoiceGroupCode in STU3 used to enumerate non-selectable: '_ActInvoiceInterGroupCode' and '_ActInvoiceRootGroupCode' @@ -2775,7 +2758,7 @@ private bool WriteEnum( */ if(vs.Url == "http://hl7.org/fhir/ValueSet/item-type") { - if (!vs.Expansion.Contains.Any(vsContains => vsContains.Code == "question")) + if (vs.Expansion.Contains.Find(vsContains => vsContains.Code == "question") == null) { vs.Expansion.Contains.Insert(2, new ValueSet.ContainsComponent() { @@ -2795,6 +2778,24 @@ private bool WriteEnum( return false; } + + IEnumerable referencedCodeSystems = vs.cgReferencedCodeSystems(); + + if (referencedCodeSystems.Count() == 1) + { + WriteIndentedComment( + $"{vs.Description}\n" + + $"(url: {vs.Url})\n" + + $"(system: {referencedCodeSystems.First()})"); + } + else + { + WriteIndentedComment( + $"{vs.Description}\n" + + $"(url: {vs.Url})\n" + + $"(systems: {referencedCodeSystems.Count()})"); + } + var defaultSystem = GetDefaultCodeSystem(concepts); _writer.WriteLineIndented($"[FhirEnumeration(\"{name}\", \"{vs.Url}\", \"{defaultSystem}\")]"); @@ -2858,36 +2859,36 @@ private bool WriteEnum( { _writer.WriteIndented( """ - /// - /// R6 Versions. - /// (system: http://hl7.org/fhir/FHIR-version) - /// - [EnumLiteral("6.0"), Description("6.0")] - N6_0, - /// - /// R6 Final Version. - /// (system: http://hl7.org/fhir/FHIR-version) - /// - [EnumLiteral("6.0.0"), Description("6.0.0")] - N6_0_0, - /// - /// R6 1st Draft Ballot. - /// (system: http://hl7.org/fhir/FHIR-version) - /// - [EnumLiteral("6.0.0-ballo1"), Description("6.0.0-ballot1")] - N6_0_0Ballo1, - /// - /// R6 2nd Draft Ballot. - /// (system: http://hl7.org/fhir/FHIR-version) - /// - [EnumLiteral("6.0.0-ballot2"), Description("6.0.0-ballot2")] - N6_0_0Ballot2, - /// - /// R6 3rd Draft Ballot. - /// (system: http://hl7.org/fhir/FHIR-version) - /// - [EnumLiteral("6.0.0-ballot3"), Description("6.0.0-ballot3")] - N6_0_0Ballot3, + /// + /// R6 Versions. + /// (system: http://hl7.org/fhir/FHIR-version) + /// + [EnumLiteral("6.0"), Description("6.0")] + N6_0, + /// + /// R6 Final Version. + /// (system: http://hl7.org/fhir/FHIR-version) + /// + [EnumLiteral("6.0.0"), Description("6.0.0")] + N6_0_0, + /// + /// R6 1st Draft Ballot. + /// (system: http://hl7.org/fhir/FHIR-version) + /// + [EnumLiteral("6.0.0-ballo1"), Description("6.0.0-ballot1")] + N6_0_0Ballo1, + /// + /// R6 2nd Draft Ballot. + /// (system: http://hl7.org/fhir/FHIR-version) + /// + [EnumLiteral("6.0.0-ballot2"), Description("6.0.0-ballot2")] + N6_0_0Ballot2, + /// + /// R6 3rd Draft Ballot. + /// (system: http://hl7.org/fhir/FHIR-version) + /// + [EnumLiteral("6.0.0-ballot3"), Description("6.0.0-ballot3")] + N6_0_0Ballot3, """); } From 08e0213145d79ceb7709a1eb57559c577dd7e8e7 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Mon, 23 Jun 2025 17:25:37 +0200 Subject: [PATCH 46/52] Corrected more lost enum values. --- .../Language/Firely/CSharpFirely2.cs | 52 +++++++++++++++---- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 7a9c33e26..94d3ab2dd 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2754,19 +2754,51 @@ private bool WriteEnum( /* TODO(ginoc): 2024.07.01 - Special cases to remove in SDK 6.0 * - ValueSet http://hl7.org/fhir/ValueSet/item-type used to enumerate non-selectable: 'question' * - ValueSet http://hl7.org/fhir/ValueSet/v3-ActInvoiceGroupCode in STU3 used to enumerate non-selectable: '_ActInvoiceInterGroupCode' and '_ActInvoiceRootGroupCode' - * ewout: 20250623 - These invoice codes seem to have disappeared before, so we don't need to add them back in. */ - if(vs.Url == "http://hl7.org/fhir/ValueSet/item-type") + switch (vs.Url) { - if (vs.Expansion.Contains.Find(vsContains => vsContains.Code == "question") == null) - { - vs.Expansion.Contains.Insert(2, new ValueSet.ContainsComponent() + case "http://hl7.org/fhir/ValueSet/item-type": { - System = "http://hl7.org/fhir/item-type", - Code = "question", - Display = "Question", - }); - } + if (vs.Expansion.Contains.Find(vsContains => vsContains.Code == "question") == null) + { + vs.Expansion.Contains.Insert(2, new ValueSet.ContainsComponent() + { + System = "http://hl7.org/fhir/item-type", + Code = "question", + Display = "Question", + }); + } + + break; + } + case "http://hl7.org/fhir/ValueSet/v3-ActInvoiceGroupCode": + { + // only care about the version present in STU3 + if (vs.Version == "2014-03-26") + { + if (vs.Expansion.Contains.Find(vsContains => vsContains.Code == "_ActInvoiceInterGroupCode") == null) + { + vs.Expansion.Contains.Insert(0, new ValueSet.ContainsComponent() + { + System = "http://hl7.org/fhir/v3/ActCode", + Code = "_ActInvoiceInterGroupCode", + Display = "ActInvoiceInterGroupCode", + }); + } + + if (vs.Expansion.Contains.Find(vsContains => vsContains.Code == "_ActInvoiceRootGroupCode") == null) + { + vs.Expansion.Contains.Insert(8, new ValueSet.ContainsComponent() + { + System = "http://hl7.org/fhir/v3/ActCode", + Code = "_ActInvoiceRootGroupCode", + Display = "ActInvoiceRootGroupCode", + }); + } + } + + break; + } } FhirConcept[] concepts = vs.cgGetFlatConcepts(_info).ToArray(); From 89dab1ca199fa5f03b88e1806bea1e6f4c78c9d4 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Mon, 30 Jun 2025 15:21:36 +0200 Subject: [PATCH 47/52] Uses ballot3, removed code that needs to go out in SDK6 --- .../Language/Firely/CSharpFirely2.cs | 50 ------------------- .../Properties/launchSettings.json | 2 +- 2 files changed, 1 insertion(+), 51 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 556ce158b..6ee04c601 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2591,56 +2591,6 @@ private bool WriteEnum( return true; } - /* TODO(ginoc): 2024.07.01 - Special cases to remove in SDK 6.0 - * - ValueSet http://hl7.org/fhir/ValueSet/item-type used to enumerate non-selectable: 'question' - * - ValueSet http://hl7.org/fhir/ValueSet/v3-ActInvoiceGroupCode in STU3 used to enumerate non-selectable: '_ActInvoiceInterGroupCode' and '_ActInvoiceRootGroupCode' - */ - switch (vs.Url) - { - case "http://hl7.org/fhir/ValueSet/item-type": - { - if (vs.Expansion.Contains.Find(vsContains => vsContains.Code == "question") == null) - { - vs.Expansion.Contains.Insert(2, new ValueSet.ContainsComponent() - { - System = "http://hl7.org/fhir/item-type", - Code = "question", - Display = "Question", - }); - } - - break; - } - case "http://hl7.org/fhir/ValueSet/v3-ActInvoiceGroupCode": - { - // only care about the version present in STU3 - if (vs.Version == "2014-03-26") - { - if (vs.Expansion.Contains.Find(vsContains => vsContains.Code == "_ActInvoiceInterGroupCode") == null) - { - vs.Expansion.Contains.Insert(0, new ValueSet.ContainsComponent() - { - System = "http://hl7.org/fhir/v3/ActCode", - Code = "_ActInvoiceInterGroupCode", - Display = "ActInvoiceInterGroupCode", - }); - } - - if (vs.Expansion.Contains.Find(vsContains => vsContains.Code == "_ActInvoiceRootGroupCode") == null) - { - vs.Expansion.Contains.Insert(8, new ValueSet.ContainsComponent() - { - System = "http://hl7.org/fhir/v3/ActCode", - Code = "_ActInvoiceRootGroupCode", - Display = "ActInvoiceRootGroupCode", - }); - } - } - - break; - } - } - FhirConcept[] concepts = vs.cgGetFlatConcepts(_info).ToArray(); if (concepts.Length == 0) diff --git a/src/fhir-codegen/Properties/launchSettings.json b/src/fhir-codegen/Properties/launchSettings.json index 7093e8b35..baded558a 100644 --- a/src/fhir-codegen/Properties/launchSettings.json +++ b/src/fhir-codegen/Properties/launchSettings.json @@ -37,7 +37,7 @@ }, "Firely 5.x R6": { "commandName": "Project", - "commandLineArgs": "generate CSharpFirely2 --output-path ../../../firely-net-sdk/src/Hl7.Fhir.R6/Model -p hl7.fhir.r6.core#6.0.0-ballot2 -p hl7.fhir.r6.expansions#6.0.0-ballot2 --subset satellite", + "commandLineArgs": "generate CSharpFirely2 --output-path ../../../firely-net-sdk/src/Hl7.Fhir.R6/Model -p hl7.fhir.r6.core#6.0.0-ballot3 -p hl7.fhir.r6.expansions#6.0.0-ballot3 --subset satellite", "workingDirectory": "$(MSBuildProjectDirectory)" }, "Firely IG Backport": { From 570d493125348292cf8ac51cae99ba99227d5681 Mon Sep 17 00:00:00 2001 From: Kasdejong Date: Tue, 5 Aug 2025 14:00:02 +0200 Subject: [PATCH 48/52] made required elements not nullable in the model --- .../Language/Firely/CSharpFirely2.cs | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 6ee04c601..01bea8370 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -1702,7 +1702,7 @@ void writeOneOnOneGetterAndSetter(string propertyType, string propertyName, bool _writer.WriteLineIndented($"{propertyType} {interfaceExportName}.{propertyName}"); OpenScope(); _writer.WriteLineIndented($"get => {propertyName};"); - _writer.WriteLineIndented($"set => {propertyName} = value;"); + _writer.WriteLineIndented($"set => {propertyName} = value!;"); CloseScope(); } @@ -1986,9 +1986,9 @@ private void WriteComponent( if (identifierElement != null) { if (identifierElement.cgIsArray()) - _writer.WriteLineIndented("List IIdentifiable>.Identifier { get => Identifier; set => Identifier = value; }"); + _writer.WriteLineIndented("List IIdentifiable>.Identifier { get => Identifier; set => Identifier = value!; }"); else - _writer.WriteLineIndented("Identifier? IIdentifiable.Identifier { get => Identifier; set => Identifier = value; }"); + _writer.WriteLineIndented("Identifier? IIdentifiable.Identifier { get => Identifier; set => Identifier = value!; }"); _writer.WriteLine(string.Empty); } @@ -2210,10 +2210,10 @@ private void WriteDictionaryTrySetValue(string exportName, List 0 ); } @@ -3103,9 +3104,9 @@ private void WriteElementGettersAndSetters(ElementDefinition element, WrittenEle { } s => s }; - if(ei.PropertyType is ListTypeReference) + if (ei.PropertyType is ListTypeReference) _writer.WriteLineIndented("[AllowNull]"); - _writer.WriteLineIndented($"public {(ei.PropertyType is ListTypeReference ltr ? ltr.PropertyTypeString : $"{ei.PropertyType.PropertyTypeString}?")} {ei.PropertyName}"); + _writer.WriteLineIndented($"public {(ei.PropertyType is ListTypeReference || ei.Required ? ei.PropertyType.PropertyTypeString : $"{ei.PropertyType.PropertyTypeString}?")} {ei.PropertyName}"); OpenScope(); @@ -3115,7 +3116,7 @@ private void WriteElementGettersAndSetters(ElementDefinition element, WrittenEle _writer.IncreaseIndent(); _writer.WriteLineIndented($"throw CodedValidationException.FromTypes(typeof({ei.PropertyType.PropertyTypeString}), Overflow[\"{ei.FhirElementName}\"]);"); _writer.DecreaseIndent(); - _writer.WriteLineIndented(ei.PropertyType is not ListTypeReference ? $"return _{ei.PropertyName};" : $"return _{ei.PropertyName} ??= [];"); + _writer.WriteLineIndented(ei.PropertyType is not ListTypeReference ? $"return _{ei.PropertyName}{(ei.Required ? "!" : "")};" : $"return _{ei.PropertyName} ??= [];"); CloseScope(); @@ -3178,8 +3179,8 @@ private void WritePrimitiveHelperProperty(string description, WrittenElementInfo switch (propType) { case PrimitiveTypeReference ptr: - string nullableType = WithNullabilityMarking(ptr.ConveniencePropertyTypeString); - _writer.WriteLineIndented($"public {nullableType} {helperPropName}"); + string typeString = WithNullabilityMarking(ptr.ConveniencePropertyTypeString); + _writer.WriteLineIndented($"public {typeString} {helperPropName}"); OpenScope(); string propAccess = versionsRemark is not null @@ -3189,7 +3190,7 @@ private void WritePrimitiveHelperProperty(string description, WrittenElementInfo _writer.WriteLineIndented("set"); OpenScope(); - _writer.WriteLineIndented($"{ei.PropertyName} = value is null ? null : new {ptr.PropertyTypeString}(value);"); + _writer.WriteLineIndented($"{ei.PropertyName} = value is null ? null! : new {ptr.PropertyTypeString}(value);"); _writer.WriteLineIndented($"OnPropertyChanged(\"{helperPropName}\");"); CloseScope(suppressNewline: true); CloseScope(); @@ -3801,7 +3802,8 @@ internal record WrittenElementInfo( string FhirElementPath, string PropertyName, TypeReference PropertyType, - string? PrimitiveHelperName); + string? PrimitiveHelperName, + bool Required = false); /// Information about the written model. internal record WrittenModelInfo(string FhirName, string CsName, bool IsAbstract); From 007d7ef059575665f31c9994b32fd99b9ff251c4 Mon Sep 17 00:00:00 2001 From: Kasdejong Date: Wed, 13 Aug 2025 17:51:33 +0200 Subject: [PATCH 49/52] Create special allowed types attributes for open choice types --- .../Language/Firely/CSharpFirely2.cs | 5 +++++ .../Language/Firely/CSharpFirelyCommon.cs | 2 ++ 2 files changed, 7 insertions(+) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 01bea8370..00868f915 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -3366,6 +3366,11 @@ internal static void BuildElementOptionalFlags( IEnumerable typeRefs = elementTypes.Values.Select(v => TypeReference.BuildFromFhirTypeName(v.Code)); allowedTypes = BuildAllowedTypesAttribute(typeRefs, null); } + + if (elementTypes.Count > 30) + { + allowedTypes = BuildOpenAllowedTypesAttribute(); + } } } diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirelyCommon.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirelyCommon.cs index 0b5e83c48..fc93f9863 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirelyCommon.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirelyCommon.cs @@ -240,6 +240,8 @@ public static int GetOrder(int relativeOrder) return (relativeOrder * 10) + 10; } + public static string BuildOpenAllowedTypesAttribute() => "[AllowedTypes(OpenChoice = true)]"; + public static string BuildAllowedTypesAttribute(IEnumerable types, FhirReleases.FhirSequenceCodes? since) { StringBuilder sb = new(); From 812c658153a06d8aeb22c9df2f7ca7ce88a56dd8 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Fri, 15 Aug 2025 10:24:28 +0200 Subject: [PATCH 50/52] Added the capability to generate OpenTypes in ModelInfo --- .../Language/Firely/CSharpFirely2.cs | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 00868f915..42f73804e 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -940,6 +940,8 @@ private void WriteModelInfo( WriteCsToString(writtenPrimitives.Values.Where(_csToStringFilter), writtenComplexTypes.Values.Where(_csToStringFilter), writtenResources.Values.Where(_csToStringFilter)); WriteSearchParameters(); + var dataTypes = writtenPrimitives.Concat(writtenComplexTypes).ToDictionary(we => we.Key, we => we.Value); + WriteOpenTypes(dataTypes); // close class CloseScope(); @@ -963,6 +965,30 @@ private void WriteModelInfo( } } + + private void WriteOpenTypes(Dictionary types) + { + _writer.WriteIndentedComment("The open types that are used in the FHIR model. These are types that can be used in the 'value' field of an Extension.", isSummary: true); + _writer.WriteIndentedComment("This list differs from the one in the written documentation (https://www.hl7.org/fhir/datatypes.html),\n" + + "but we assume the list in Extension.value[x] is the more authorative.", + isRemarks: true, isSummary: false); + _writer.WriteLineIndented("public static readonly Type[] OpenTypes ="); + OpenScope(); + + var extensionValue = _info.TryFindElementByPath("Extension.value[x]", + out StructureDefinition? _, + out ElementDefinition? edExtensionValue) + ? edExtensionValue.Type + : throw new InvalidOperationException("Could not find Extension.value[x] element in definitions"); + + foreach (var typeName in extensionValue.Select(ev => ev.Code).OrderBy(c => c)) + { + _writer.WriteLineIndented($"typeof({types[typeName].CsName}),"); + } + + CloseScope(includeSemicolon: true); + } + /// Writes the search parameters. private void WriteSearchParameters() { @@ -3366,11 +3392,14 @@ internal static void BuildElementOptionalFlags( IEnumerable typeRefs = elementTypes.Values.Select(v => TypeReference.BuildFromFhirTypeName(v.Code)); allowedTypes = BuildAllowedTypesAttribute(typeRefs, null); } - - if (elementTypes.Count > 30) + else if (elementTypes.Count > 30) { allowedTypes = BuildOpenAllowedTypesAttribute(); } + else + throw new InvalidOperationException("Cannot generate AllowedTypes attribute for element " + + $"{element.Path} with types {string.Join(", ", elementTypes.Keys)} because " + + $"not all types in the choice not available in the current subset ({subset})."); } } From 0fb45f06d66240e4585360dabc469ef0d60ff2d0 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Fri, 15 Aug 2025 11:33:16 +0200 Subject: [PATCH 51/52] Update src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs --- .../Language/Firely/CSharpFirely2.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 42f73804e..89c02e3c1 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -3399,7 +3399,7 @@ internal static void BuildElementOptionalFlags( else throw new InvalidOperationException("Cannot generate AllowedTypes attribute for element " + $"{element.Path} with types {string.Join(", ", elementTypes.Keys)} because " + - $"not all types in the choice not available in the current subset ({subset})."); + $"not all types in the choice are available in the current subset ({subset})."); } } From 9cc19668af36498651fe728caf1852e986f5a96f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Skowro=C5=84ski?= Date: Fri, 15 Aug 2025 11:41:30 +0200 Subject: [PATCH 52/52] Skip generation of abstract enum members --- .../Language/Firely/CSharpFirely2.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs index 89c02e3c1..6bdca6cae 100644 --- a/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs +++ b/src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs @@ -2656,6 +2656,9 @@ private bool WriteEnum( foreach (FhirConcept concept in concepts) { + if (concept.IsAbstract is true) + continue; + string codeName = ConvertEnumValue(concept.Code); string codeValue = FhirSanitizationUtils.SanitizeForValue(concept.Code); string description = string.IsNullOrEmpty(concept.Definition)