From e7da3b09bb5c42f597cc71c9993a623bf307e6d3 Mon Sep 17 00:00:00 2001 From: jods Date: Thu, 18 Jul 2024 04:00:59 +0200 Subject: [PATCH] Attribute with fixed enum value + default/fixed values for elements (#69) * Fixes #68 and add support for element default/fixed values * Fix various issues in Elements new implementation * Improve fixed value handling in elements * Fix List regular expression to support IList * Updated version. --------- Co-authored-by: Muhammad Miftah --- LinqToXsd/Properties/launchSettings.json | 18 ++ Version.props | 2 +- XObjectsCode/Src/ClrBasePropertyInfo.cs | 14 +- XObjectsCode/Src/ClrPropertyInfo.cs | 242 ++++++++++++-------- XObjectsCode/Src/SimpleTypeCodeDomHelper.cs | 37 +-- XObjectsCode/Src/XsdToTypesConverter.cs | 37 ++- XObjectsCore/API/XTypedServices.cs | 3 + 7 files changed, 233 insertions(+), 120 deletions(-) diff --git a/LinqToXsd/Properties/launchSettings.json b/LinqToXsd/Properties/launchSettings.json index a5ec9c8b..efa9bbdb 100644 --- a/LinqToXsd/Properties/launchSettings.json +++ b/LinqToXsd/Properties/launchSettings.json @@ -32,6 +32,24 @@ "commandLineArgs": "gen \"Microsoft.Search.Query.xsd\" -a", "workingDirectory": "..\\GeneratedSchemaLibraries\\Microsoft Search", "hotReloadEnabled": false + }, + "AIXM": { + "commandName": "Project", + "commandLineArgs": "gen \"AIXM_Features.xsd\" -a", + "workingDirectory": "..\\GeneratedSchemaLibraries\\AIXM\\aixm-5.1.1", + "hotReloadEnabled": false + }, + "Pubmed": { + "commandName": "Project", + "commandLineArgs": "gen \"efetch-pubmed.xsd\" -a", + "workingDirectory": "..\\GeneratedSchemaLibraries\\Pubmed", + "hotReloadEnabled": false + }, + "Windows": { + "commandName": "Project", + "commandLineArgs": "gen \"windowsTaskSched.xsd\" -a", + "workingDirectory": "..\\GeneratedSchemaLibraries\\Windows", + "hotReloadEnabled": false } } } \ No newline at end of file diff --git a/Version.props b/Version.props index 57b55ef3..da2e0af2 100644 --- a/Version.props +++ b/Version.props @@ -1,6 +1,6 @@ - 3.4.6 + 3.4.7 $(VersionSuffix) $(Version)-$(VersionSuffix) diff --git a/XObjectsCode/Src/ClrBasePropertyInfo.cs b/XObjectsCode/Src/ClrBasePropertyInfo.cs index 79eda1d0..168c68c5 100644 --- a/XObjectsCode/Src/ClrBasePropertyInfo.cs +++ b/XObjectsCode/Src/ClrBasePropertyInfo.cs @@ -12,7 +12,6 @@ public abstract class ClrBasePropertyInfo : ContentInfo protected bool hasSet; protected XCodeTypeReference returnType; - protected XCodeTypeReference defaultValueType; protected bool isVirtual; protected bool isOverride; @@ -21,10 +20,9 @@ public abstract class ClrBasePropertyInfo : ContentInfo public ClrBasePropertyInfo() { - this.IsVirtual = false; - this.isOverride = false; - this.returnType = null; - this.defaultValueType = null; + IsVirtual = false; + isOverride = false; + returnType = null; annotations = new List(); } @@ -94,12 +92,6 @@ public virtual XCodeTypeReference ReturnType set { returnType = value; } } - public virtual XCodeTypeReference DefaultValueType - { - get { return defaultValueType; } - set { defaultValueType = value; } - } - public virtual string ClrTypeName { get { return null; } diff --git a/XObjectsCode/Src/ClrPropertyInfo.cs b/XObjectsCode/Src/ClrPropertyInfo.cs index cbd986b8..57ac5f24 100644 --- a/XObjectsCode/Src/ClrPropertyInfo.cs +++ b/XObjectsCode/Src/ClrPropertyInfo.cs @@ -37,7 +37,6 @@ public ClrPropertyInfo(string propertyName, string propertyNs, string schemaName this.schemaName = schemaName; this.hasSet = true; this.returnType = null; - this.defaultValueType = null; this.clrTypeName = null; this.occursInSchema = occursInSchema; if (this.occursInSchema > Occurs.ZeroOrOne) @@ -160,7 +159,14 @@ public override bool IsList public override bool IsNullable { - get { return (CanBeAbsent && fixedDefaultValue == null) || IsNillable; } + get + { + return + // Elements can be absent and must be read as null even when they have a default value + // Absent attributes will take their default value when they have one + (CanBeAbsent && (propertyOrigin != SchemaOrigin.Attribute || fixedDefaultValue == null)) + || IsNillable; + } } public bool CanBeAbsent @@ -282,9 +288,6 @@ public override bool VerifyRequired public override XCodeTypeReference ReturnType => returnType ??= CreateReturnType(IsEnum ? typeRef.ClrFullTypeName : clrTypeName); - public override XCodeTypeReference DefaultValueType - => defaultValueType ??= CreateReturnType(clrTypeName); - private string QualifiedType => typeRef.IsLocalType && !typeRef.IsSimpleType ? parentTypeFullName + "." + clrTypeName : clrTypeName; @@ -802,27 +805,30 @@ private void AddGetStatements(CodeStatementCollection getStatements) return; } - CodeExpression returnExp = null; - - if (FixedValue != null) + // Fixed attributes always have the same value, whether they're present or not. + if (FixedValue != null && propertyOrigin == SchemaOrigin.Attribute) { - getStatements.Add( - new CodeMethodReturnStatement( - new CodeFieldReferenceExpression(null, - NameGenerator.ChangeClrName(this.propertyName, NameOptions.MakeFixedValueField) - )) - ); + ReturnFixedValue(getStatements); return; } getStatements.Add(GetValueMethodCall()); CheckOccurrence(getStatements); CheckNillable(getStatements); + // Fixed elements always have the same value, _when they're present._ + // If they're optional they can still be absent and read as null, which is handled in CheckOccurence + if (FixedValue != null && propertyOrigin != SchemaOrigin.Attribute) + { + ReturnFixedValue(getStatements); + return; + } + GetElementDefaultValue(getStatements); // Attribute default value is handled in CheckOccurence CodeVariableReferenceExpression returnValueExp = new CodeVariableReferenceExpression("x"); + CodeExpression returnExp; if (!IsRef && typeRef.IsSimpleType) { //for referencing properties, directly create the object of referenced type - CodeTypeReference parseType = DefaultValueType; + CodeTypeReference parseType = ReturnType; if (typeRef.IsValueType && IsNullable) { parseType = new CodeTypeReference(clrTypeName); @@ -862,15 +868,7 @@ private void AddGetStatements(CodeStatementCollection getStatements) returnValueExp, simpleTypeExpression); - if (DefaultValue != null) - { - ((CodeMethodInvokeExpression) returnExp).Parameters.Add( - new CodeFieldReferenceExpression(null, - NameGenerator.ChangeClrName(this.propertyName, - NameOptions.MakeDefaultValueField))); - } - - if (this.IsEnum) + if (IsEnum) { // (EnumType) Enum.Parse(typeof(EnumType), returnExp) returnExp = CodeDomHelper.CreateParseEnumCall(this.TypeReference.ClrFullTypeName, returnExp); @@ -889,11 +887,25 @@ private void CheckOccurrence(CodeStatementCollection getStatements) { Debug.Assert(!this.IsList); CodeStatement returnStatement = null; - if (CanBeAbsent && DefaultValue == null) + if (CanBeAbsent) { - // For value types, this is needed to return T?, since ParseValue return T. - // It's not mandatory for ref types but it's more consistent and performant to do it always. - returnStatement = new CodeMethodReturnStatement(new CodePrimitiveExpression(null)); + // Absent attributes return their default value (if any). + // Note that absent elements return null, only empty elements return their default value (per xsd specs). + if (DefaultValue != null && propertyOrigin == SchemaOrigin.Attribute) + { + returnStatement = new CodeMethodReturnStatement( + new CodeFieldReferenceExpression( + null, + NameGenerator.ChangeClrName(propertyName, NameOptions.MakeDefaultValueField) + ) + ); + } + else + { + // For value types, this is needed to return T?, since ParseValue return T. + // It's not mandatory for ref types but it's more consistent and performant to do it always. + returnStatement = new CodeMethodReturnStatement(new CodePrimitiveExpression(null)); + } } else if (VerifyRequired) { @@ -931,6 +943,49 @@ private void CheckNillable(CodeStatementCollection getStatements) } } + private void ReturnFixedValue(CodeStatementCollection getStatements) + { + getStatements.Add( + new CodeMethodReturnStatement( + new CodeFieldReferenceExpression( + null, + NameGenerator.ChangeClrName(propertyName, NameOptions.MakeFixedValueField) + ) + ) + ); + } + + private void GetElementDefaultValue(CodeStatementCollection getStatements) + { + // Default values of attributes are already handled previously based on attribute presence + if (propertyOrigin == SchemaOrigin.Attribute || DefaultValue == null) return; + + var x = new CodeVariableReferenceExpression("x"); + + // Default values apply when element is present and empty. + // Technically at this point x should be != null thanks to CheckOccurence but let's not crash with NRE if we're reading malformed XML. + // `if (x != null && x.IsEmpty) return defaultValue;` + getStatements.Add( + new CodeConditionStatement( + new CodeBinaryOperatorExpression( + new CodeBinaryOperatorExpression( + x, + CodeBinaryOperatorType.IdentityInequality, + new CodePrimitiveExpression(null) + ), + CodeBinaryOperatorType.BooleanAnd, + new CodeFieldReferenceExpression(x, "IsEmpty") + ), + new CodeMethodReturnStatement( + new CodeFieldReferenceExpression( + null, + NameGenerator.ChangeClrName(propertyName, NameOptions.MakeDefaultValueField) + ) + ) + ) + ); + } + private void AddSetStatements(CodeStatementCollection setStatements) { AddFixedValueChecking(setStatements); @@ -1149,83 +1204,82 @@ public void SetFixedDefaultValue(ClrWrapperTypeInfo typeInfo) protected void CreateFixedDefaultValue(CodeTypeDeclaration typeDecl) { - if (fixedDefaultValue != null) - { - //Add Fixed/Default value wrapping field - CodeMemberField fixedOrDefaultField = null; - CodeTypeReference returnType = DefaultValueType; - if (this.unionDefaultType != null) - { - returnType = new CodeTypeReference(unionDefaultType.ToString()); - } + if (fixedDefaultValue == null) return; + // Add Fixed/Default value wrapping field - if (FixedValue != null) - { - fixedOrDefaultField = new CodeMemberField(returnType, - NameGenerator.ChangeClrName(PropertyName, NameOptions.MakeFixedValueField)); - } - else // if (DefaultValue != null) - { - fixedOrDefaultField = new CodeMemberField(returnType, - NameGenerator.ChangeClrName(PropertyName, NameOptions.MakeDefaultValueField)); - } + CodeTypeReference returnType = unionDefaultType != null + ? new CodeTypeReference(unionDefaultType.ToString()) + : ReturnType; - CodeDomHelper.AddBrowseNever(fixedOrDefaultField); - fixedOrDefaultField.Attributes = (fixedOrDefaultField.Attributes & ~MemberAttributes.AccessMask & - ~MemberAttributes.ScopeMask) - | MemberAttributes.Private | MemberAttributes.Static; + var fieldName = NameGenerator.ChangeClrName( + PropertyName, + FixedValue != null ? NameOptions.MakeFixedValueField : NameOptions.MakeDefaultValueField /* DefaultValue != null */); - fixedOrDefaultField.InitExpression = - SimpleTypeCodeDomHelper.CreateFixedDefaultValueExpression(returnType, fixedDefaultValue); - typeDecl.Members.Add(fixedOrDefaultField); - } + var fixedOrDefaultField = new CodeMemberField(returnType, fieldName); + CodeDomHelper.AddBrowseNever(fixedOrDefaultField); + + fixedOrDefaultField.Attributes = + (fixedOrDefaultField.Attributes & ~MemberAttributes.AccessMask & ~MemberAttributes.ScopeMask) + | MemberAttributes.Private + | MemberAttributes.Static; + + fixedOrDefaultField.InitExpression = + SimpleTypeCodeDomHelper.CreateFixedDefaultValueExpression(returnType, fixedDefaultValue, IsEnum); + + typeDecl.Members.Add(fixedOrDefaultField); } protected void AddFixedValueChecking(CodeStatementCollection setStatements) { - if (FixedValue != null) - { - CodeExpression fixedValueExpr = - new CodeFieldReferenceExpression(null, - NameGenerator.ChangeClrName(this.propertyName, NameOptions.MakeFixedValueField)); - setStatements.Add( - new CodeConditionStatement( - CodeDomHelper.CreateMethodCall( - new CodePropertySetValueReferenceExpression(), - Constants.EqualityCheck, - fixedValueExpr - ), - new CodeStatement[] { }, - new CodeStatement[] - { - new CodeThrowExceptionStatement( - new CodeObjectCreateExpression(typeof(LinqToXsdFixedValueException), - new CodePropertySetValueReferenceExpression(), - fixedValueExpr)) - } + if (FixedValue == null) return; + + // The set value must match the property fixed value. + + CodeExpression fixedValueExpr = + new CodeFieldReferenceExpression(null, + NameGenerator.ChangeClrName(propertyName, NameOptions.MakeFixedValueField)); + + var valueExpr = new CodePropertySetValueReferenceExpression(); + + // Note the condition is opposite, because CodeDOM doesn't have unary negation... + // so we're doing `if (value == fixed) { /* ok */ } else throw` + CodeExpression condition = CodeDomHelper.CreateMethodCall( + fixedValueExpr, + Constants.EqualityCheck, + valueExpr); + + // If the property is an optional element, setting it to null is also acceptable: it removes the element from document. + // So we're checking `if (value == fixed || value == null) ...` + // On principle, setting attributes to null can also make sense as it would remove the XAttribute from document, + // but that doesn't mesh well with the type system (attributes with default/fixed values are never nullable, + // which would still be workable on Ref types with [AllowNull] on property, but we can't assign null to a value type setter. + // This is really an edge case and can be achieved by removing the XAttribute from Untyped. + if (IsNullable) + { + condition = new CodeBinaryOperatorExpression( + condition, + CodeBinaryOperatorType.BooleanOr, + new CodeBinaryOperatorExpression( + valueExpr, + CodeBinaryOperatorType.IdentityEquality, + new CodePrimitiveExpression(null) ) ); } - } - public virtual void InsertDefaultFixedValueInDefaultCtor(CodeConstructor ctor) - { - if (this.FixedValue != null) - { - ctor.Statements.Add( - new CodeAssignStatement( - CodeDomHelper.CreateFieldReference(null, propertyName), - CodeDomHelper.CreateFieldReference(null, - NameGenerator.ChangeClrName(propertyName, NameOptions.MakeFixedValueField)))); - } - else if (DefaultValue != null) - { - ctor.Statements.Add( - new CodeAssignStatement( - CodeDomHelper.CreateFieldReference(null, propertyName), - CodeDomHelper.CreateFieldReference(null, - NameGenerator.ChangeClrName(propertyName, NameOptions.MakeDefaultValueField)))); - } + setStatements.Add( + new CodeConditionStatement( + condition, + new CodeStatement[] { }, + new CodeStatement[] + { + new CodeThrowExceptionStatement( + new CodeObjectCreateExpression(typeof(LinqToXsdFixedValueException), + new CodePropertySetValueReferenceExpression(), + fixedValueExpr)) + } + ) + ); } } } \ No newline at end of file diff --git a/XObjectsCode/Src/SimpleTypeCodeDomHelper.cs b/XObjectsCode/Src/SimpleTypeCodeDomHelper.cs index bb356a80..49c7b82a 100644 --- a/XObjectsCode/Src/SimpleTypeCodeDomHelper.cs +++ b/XObjectsCode/Src/SimpleTypeCodeDomHelper.cs @@ -8,6 +8,7 @@ using System.Reflection; using System.Diagnostics; using System.Globalization; +using System.Text.RegularExpressions; namespace Xml.Schema.Linq.CodeGen { @@ -405,17 +406,22 @@ private static CodeExpression CreateTypeConversionExpr(string typeName, object v new CodePrimitiveExpression(value.ToString())); } - internal static CodeExpression CreateValueExpression(string builtInType, string strValue) + internal static CodeExpression CreateValueExpression(string builtInType, string strValue, bool isEnum) { int dot = builtInType.LastIndexOf('.'); - Debug.Assert(dot != -1); - string localType = builtInType.Substring(dot + 1); + string localType = dot < 0 ? builtInType : builtInType.Substring(dot + 1); + + Debug.Assert(dot != -1 || isEnum); // Enums are local types that may be simple names if (localType == "String" || localType == "Object") { return new CodePrimitiveExpression(strValue); } + else if (isEnum) + { + return new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(builtInType), strValue); + } else if (localType == "Uri") { return new CodeObjectCreateExpression("Uri", new CodePrimitiveExpression(strValue)); @@ -426,41 +432,46 @@ internal static CodeExpression CreateValueExpression(string builtInType, string } } - internal static CodeArrayCreateExpression CreateFixedDefaultArrayValueInit(string baseType, string value) + internal static CodeArrayCreateExpression CreateFixedDefaultArrayValueInit(string baseType, string value, bool isEnum) { - CodeArrayCreateExpression array = new CodeArrayCreateExpression(baseType); + var array = new CodeArrayCreateExpression(baseType); foreach (string s in value.Split(' ')) { - array.Initializers.Add(CreateValueExpression(baseType, s)); + array.Initializers.Add(CreateValueExpression(baseType, s, isEnum)); } return array; } - internal static CodeExpression CreateFixedDefaultValueExpression(CodeTypeReference type, string value) + internal static CodeExpression CreateFixedDefaultValueExpression(CodeTypeReference type, string value, bool isEnum) { string baseType = type.BaseType; - if (baseType.Contains("Nullable")) + if (Regex.IsMatch(baseType, @"\bNullable`1")) { Debug.Assert(type.TypeArguments.Count == 1); baseType = type.TypeArguments[0].BaseType; - return CreateValueExpression(baseType, value); + return CreateValueExpression(baseType, value, isEnum); + } + else if (baseType.EndsWith("?")) + { + baseType = baseType.Substring(0, baseType.Length - 1); + return CreateValueExpression(baseType, value, isEnum); } else if (type.ArrayRank != 0) { baseType = type.ArrayElementType.BaseType; - return CreateFixedDefaultArrayValueInit(baseType, value); + return CreateFixedDefaultArrayValueInit(baseType, value, isEnum); } - else if (baseType.Contains("List")) + else if (Regex.IsMatch(baseType, @"\bI?List`1")) { //Create sth like: new List(new string[] { }); Debug.Assert(type.TypeArguments.Count == 1); baseType = type.TypeArguments[0].BaseType; - return CreateFixedDefaultArrayValueInit(baseType, value); + return CreateFixedDefaultArrayValueInit(baseType, value, isEnum); } - return CreateValueExpression(baseType, value); + return CreateValueExpression(baseType, value, isEnum); } } } \ No newline at end of file diff --git a/XObjectsCode/Src/XsdToTypesConverter.cs b/XObjectsCode/Src/XsdToTypesConverter.cs index 2aecf9ce..98b3f7ee 100644 --- a/XObjectsCode/Src/XsdToTypesConverter.cs +++ b/XObjectsCode/Src/XsdToTypesConverter.cs @@ -985,7 +985,7 @@ private ClrPropertyInfo BuildPropertyForElement(XmlSchemaElement elem, bool from propertyInfo.ClrNamespace = clrNs; propertyInfo.IsNillable = elem.IsNillable; - //SetFixedDefaultValue(elem, propertyInfo); + SetFixedDefaultValue(elem, propertyInfo); if (substitutionMembers != null) { @@ -1191,5 +1191,40 @@ private void SetFixedDefaultValue(XmlSchemaAttribute attribute, ClrPropertyInfo } } } + + private void SetFixedDefaultValue(XmlSchemaElement element, ClrPropertyInfo propertyInfo) + { + //saves fixed/default value in the corresponding property + + if ((element.FixedValue ?? element.DefaultValue) == null) return; + //Currently only consider fixed/default values for simple types + if (element.ElementSchemaType is XmlSchemaComplexType) return; + + if (element.RefName != null && !element.RefName.IsEmpty) + { + var globalEl = (XmlSchemaElement)schemas.GlobalElements[element.RefName]; + propertyInfo.FixedValue = globalEl.FixedValue; + propertyInfo.DefaultValue = globalEl.DefaultValue; + } + else + { + propertyInfo.FixedValue = element.FixedValue; + propertyInfo.DefaultValue = element.DefaultValue; + } + + if (element.ElementSchemaType.DerivedBy == XmlSchemaDerivationMethod.Union) + { + string value = propertyInfo.FixedValue; + if (value == null) + value = propertyInfo.DefaultValue; + if (value != null) + { + propertyInfo.unionDefaultType = element + .ElementSchemaType.Datatype + .ParseValue(value, new NameTable(), null) + .GetType(); + } + } + } } } \ No newline at end of file diff --git a/XObjectsCore/API/XTypedServices.cs b/XObjectsCore/API/XTypedServices.cs index a4956bff..27c6403b 100644 --- a/XObjectsCore/API/XTypedServices.cs +++ b/XObjectsCore/API/XTypedServices.cs @@ -399,6 +399,9 @@ public static T ParseValue(XAttribute attribute, XmlSchemaDatatype datatype) return ParseValue(attribute.Value, attribute.Parent, datatype); } + // Kept for backward compatibility with code generated in previous versions. + // Current generator does not use this method anymore, as attributes with default properties + // have `if (attr == null) return defaultValue` directly in getter to workaround enum parsing. public static T ParseValue(XAttribute attribute, XmlSchemaDatatype datatype, T defaultValue) { if (attribute == null)