From ebe166e2f6d2fc7c7c742eac7aeb43b4b44fe241 Mon Sep 17 00:00:00 2001 From: samatstarion Date: Thu, 22 Oct 2026 08:30:12 +0200 Subject: [PATCH 1/2] [Fix] keep an explicit IS-EDITABLE="false" when a document is read and written again; fixes #86 - add AccessControlledElement.IsEditableSpecified, set when IS-EDITABLE is read or IsEditable is set; IS-EDITABLE is written when it is specified or true - write the attributes of AccessControlledElement and SpecHierarchy before the ALTERNATIVE-ID element, through a new Identifiable.WriteXmlAttributes(Async); an element with an ALTERNATIVE-ID that was editable or table-internal threw an InvalidOperationException - add IsEditableTestFixture Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 + .../Decorators/ModelMetadataTestFixture.cs | 1 + ReqIFSharp.Tests/IsEditableTestFixture.cs | 427 ++++++++++++++++++ ReqIFSharp/AccessControlledElement.cs | 47 +- ReqIFSharp/Identifiable.cs | 32 ++ .../SpecHierarchy.cs | 45 +- 6 files changed, 535 insertions(+), 21 deletions(-) create mode 100644 ReqIFSharp.Tests/IsEditableTestFixture.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 28eed81..d934bce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,11 @@ The repository ships two NuGet packages that version independently: **ReqIFSharp ### Added - `AttributeValueXHTML.ExtractNormalizedTextFromValue(bool keepLineBreaks = false)` returns the text of an XHTML value as plain text: runs of spaces, tabs and line breaks are collapsed to one space (non-breaking spaces are kept), no space is added around inline elements, and block elements such as paragraphs and list items are separated by a space or, with `keepLineBreaks`, by a line feed. The text of a `pre` element is kept as written, and HTML named entities such as ` ` are turned into their characters. `ExtractUnformattedTextFromValue` is unchanged ([#92](https://github.com/STARIONGROUP/ReqIFSharp/issues/92)). +- `AccessControlledElement.IsEditableSpecified` tells whether the `IS-EDITABLE` attribute is written. It is true when the attribute was present in the document that was read or when `IsEditable` was set; set it to false to leave out an `IS-EDITABLE` that is false ([#86](https://github.com/STARIONGROUP/ReqIFSharp/issues/86)). ### Changed +- Setting `IsEditable` to false on a spec hierarchy or attribute definition now writes `IS-EDITABLE="false"`; before, the attribute was left out. Both mean that the element is read-only. Set `IsEditableSpecified` to false to leave the attribute out ([#86](https://github.com/STARIONGROUP/ReqIFSharp/issues/86)). - The source files carry the licence in short SPDX form (`SPDX-FileCopyrightText: 2017-2026 Starion Group S.A.`, `SPDX-License-Identifier: Apache-2.0`) instead of the full Apache-2.0 notice; the licence itself is unchanged, and `NOTICE` now gives the same years, 2017-2026 ([#103](https://github.com/STARIONGROUP/ReqIFSharp/issues/103)). - A `REQ-IF` root element in a namespace other than the ReqIF namespace (`http://www.omg.org/spec/ReqIF/20110401/reqif.xsd`) is no longer read as a ReqIF document; it is skipped and a warning is logged. Before, such an element was read when it had no prefix. A root in no namespace is still read ([#87](https://github.com/STARIONGROUP/ReqIFSharp/issues/87)). - An element other than `SPEC-RELATION-REF` inside the `SPEC-RELATIONS` of a relation group is now skipped with a warning, and the references after it are read. Before, it caused an `XmlException` ([#94](https://github.com/STARIONGROUP/ReqIFSharp/issues/94)). @@ -25,6 +27,8 @@ The repository ships two NuGet packages that version independently: **ReqIFSharp - A document without whitespace between its elements (minified XML) lost data without an error: every other header field (such as `CREATION-TIME` and `REQ-IF-TOOL-ID`) and every other enumeration value of an attribute value was skipped, and the spec relations of a relation group were not read or caused an `XmlException` ([#94](https://github.com/STARIONGROUP/ReqIFSharp/issues/94)). - `AttributeValueXHTML.TheOriginalValue` was not read from `THE-ORIGINAL-VALUE`, so it was always `null` after reading, and a document that was read and written again lost the original values of its simplified XHTML attribute values ([#95](https://github.com/STARIONGROUP/ReqIFSharp/issues/95)). - Attribute values did not log anything, also when an `ILoggerFactory` was passed to the deserializer: the `AttributeValue` constructors did not keep the logger factory. An enumeration attribute value no longer reports its own element and its `DEFINITION` and `VALUES` elements as not supported ([#97](https://github.com/STARIONGROUP/ReqIFSharp/issues/97)). +- An explicit `IS-EDITABLE="false"` on a spec hierarchy or attribute definition was dropped when a document was read and written again, so the receiving tool fell back to its own default. It is now written whenever it was read or `IsEditable` was set, also when it is false; an element without the attribute is still written without it ([#86](https://github.com/STARIONGROUP/ReqIFSharp/issues/86)). +- Writing a spec hierarchy or attribute definition that has an `ALTERNATIVE-ID` threw an `InvalidOperationException` when it was editable or, for a spec hierarchy, `IS-TABLE-INTERNAL`: those attributes were written after the `ALTERNATIVE-ID` element ([#86](https://github.com/STARIONGROUP/ReqIFSharp/issues/86)). - `new AttributeValueXHTML(loggerFactory)` left `ExternalObjects` `null`, so `QueryExternalObjects` and `QueryBase64PayloadsAsync` of ReqIFSharp.Extensions threw for a spec element that held such a value. `new AttributeValueXHTML()` left its logger `null`, so reading into it threw ([#98](https://github.com/STARIONGROUP/ReqIFSharp/issues/98)). ## [10.1.2] - 2026-10-18 diff --git a/ReqIFSharp.Tests/Decorators/ModelMetadataTestFixture.cs b/ReqIFSharp.Tests/Decorators/ModelMetadataTestFixture.cs index af4e19a..7304d5a 100644 --- a/ReqIFSharp.Tests/Decorators/ModelMetadataTestFixture.cs +++ b/ReqIFSharp.Tests/Decorators/ModelMetadataTestFixture.cs @@ -39,6 +39,7 @@ public class ModelMetadataTestFixture "DatatypeDefinition", // AttributeDefinition base accessor that aliases the concrete Type "AttributeDefinition", // AttributeValue base accessor that aliases the concrete Definition "ExternalObjects", // derived from the parsed XHTML content + "IsEditableSpecified", // records whether IS-EDITABLE is written, not a metamodel feature "OwningDefinition", // back-reference to the owning AttributeDefinition "Definition", // see note below - kept, handled explicitly (NOT excluded) "SpecElAt", // back-reference to the owning SpecElementWithAttributes diff --git a/ReqIFSharp.Tests/IsEditableTestFixture.cs b/ReqIFSharp.Tests/IsEditableTestFixture.cs new file mode 100644 index 0000000..71c6d5e --- /dev/null +++ b/ReqIFSharp.Tests/IsEditableTestFixture.cs @@ -0,0 +1,427 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// SPDX-FileCopyrightText: 2017-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace ReqIFSharp.Tests +{ + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using System.Xml; + using System.Xml.Linq; + + using NUnit.Framework; + + using ReqIFSharp; + + /// + /// Verifies that the IS-EDITABLE attribute of an (a spec hierarchy + /// or an attribute definition) is written as it was read, including an explicit IS-EDITABLE="false". + /// + [TestFixture] + public class IsEditableTestFixture + { + private const string FileName = "reqifsharpgenerated.reqif"; + + /// + /// Returns the test document with IS-EDITABLE="false" on its first spec hierarchy and on its first + /// attribute definition, IS-EDITABLE="true" on its second spec hierarchy and no IS-EDITABLE on its third + /// + private static string CreateDocument(out string[] identifiers) + { + var ns = TestDocuments.ReqIFNamespace; + var document = XDocument.Parse(TestDocuments.Read(FileName)); + + var specHierarchies = document.Descendants(ns + "SPEC-HIERARCHY").ToList(); + Assert.That(specHierarchies, Has.Count.GreaterThanOrEqualTo(3)); + + var attributeDefinition = document.Descendants(ns + "SPEC-ATTRIBUTES").Elements().First(); + + specHierarchies[0].SetAttributeValue("IS-EDITABLE", "false"); + specHierarchies[1].SetAttributeValue("IS-EDITABLE", "true"); + specHierarchies[2].SetAttributeValue("IS-EDITABLE", null); + attributeDefinition.SetAttributeValue("IS-EDITABLE", "false"); + + identifiers = new[] + { + specHierarchies[0].Attribute("IDENTIFIER")!.Value, + specHierarchies[1].Attribute("IDENTIFIER")!.Value, + specHierarchies[2].Attribute("IDENTIFIER")!.Value, + attributeDefinition.Attribute("IDENTIFIER")!.Value + }; + + return document.ToString(); + } + + /// + /// Returns the spec hierarchy with the identifier + /// + private static SpecHierarchy SpecHierarchyOf(ReqIF reqif, string identifier) + { + IEnumerable All(IEnumerable hierarchies) => + hierarchies.SelectMany(x => new[] { x }.Concat(All(x.Children))); + + return All(reqif.CoreContent.Specifications.SelectMany(x => x.Children)).Single(x => x.Identifier == identifier); + } + + /// + /// Returns the access controlled element (spec hierarchy or attribute definition) with the identifier + /// + private static AccessControlledElement AccessControlledElementOf(ReqIF reqif, string identifier) + { + return reqif.CoreContent.SpecTypes.SelectMany(x => x.SpecAttributes).SingleOrDefault(x => x.Identifier == identifier) + ?? (AccessControlledElement)SpecHierarchyOf(reqif, identifier); + } + + /// + /// Returns the IS-EDITABLE attribute of the element with the identifier, or null when it has none + /// + private static string IsEditableOf(XDocument document, string identifier) + { + return document.Descendants() + .Single(x => (string)x.Attribute("IDENTIFIER") == identifier) + .Attribute("IS-EDITABLE")?.Value; + } + + private static void AssertRoundTrip(Stream output, string[] identifiers) + { + output.Position = 0; + var document = XDocument.Load(output); + + Assert.Multiple(() => + { + Assert.That(IsEditableOf(document, identifiers[0]), Is.EqualTo("false")); + Assert.That(IsEditableOf(document, identifiers[1]), Is.EqualTo("true")); + Assert.That(IsEditableOf(document, identifiers[2]), Is.Null); + Assert.That(IsEditableOf(document, identifiers[3]), Is.EqualTo("false")); + }); + } + + [TestCaseSource(typeof(TestDocuments), nameof(TestDocuments.Kinds))] + public void Verify_that_IS_EDITABLE_is_written_as_it_was_read(SupportedFileExtensionKind kind) + { + var xml = CreateDocument(out var identifiers); + + var reqifs = new ReqIFDeserializer().Deserialize(TestDocuments.ToStream(xml, kind), kind).ToList(); + + var reqif = reqifs.Single(); + + Assert.Multiple(() => + { + Assert.That(SpecHierarchyOf(reqif, identifiers[0]).IsEditableSpecified, Is.True); + Assert.That(SpecHierarchyOf(reqif, identifiers[0]).IsEditable, Is.False); + Assert.That(SpecHierarchyOf(reqif, identifiers[1]).IsEditableSpecified, Is.True); + Assert.That(SpecHierarchyOf(reqif, identifiers[1]).IsEditable, Is.True); + Assert.That(SpecHierarchyOf(reqif, identifiers[2]).IsEditableSpecified, Is.False); + Assert.That(SpecHierarchyOf(reqif, identifiers[2]).IsEditable, Is.False); + Assert.That(AccessControlledElementOf(reqif, identifiers[3]).IsEditableSpecified, Is.True); + Assert.That(AccessControlledElementOf(reqif, identifiers[3]).IsEditable, Is.False); + }); + + using var output = new MemoryStream(); + new ReqIFSerializer().Serialize(reqifs, output, SupportedFileExtensionKind.Reqif); + + AssertRoundTrip(output, identifiers); + } + + [TestCaseSource(typeof(TestDocuments), nameof(TestDocuments.Kinds))] + public async Task Verify_that_IS_EDITABLE_is_written_as_it_was_read_async(SupportedFileExtensionKind kind) + { + var xml = CreateDocument(out var identifiers); + + var reqifs = (await new ReqIFDeserializer().DeserializeAsync(TestDocuments.ToStream(xml, kind), kind, CancellationToken.None)).ToList(); + + using var output = new MemoryStream(); + await new ReqIFSerializer().SerializeAsync(reqifs, output, SupportedFileExtensionKind.Reqif, CancellationToken.None); + + AssertRoundTrip(output, identifiers); + } + + [Test] + public void Verify_that_IsEditableSpecified_is_false_for_a_new_element_and_set_by_IsEditable() + { + var specHierarchy = new SpecHierarchy(); + + Assert.That(specHierarchy.IsEditableSpecified, Is.False); + + specHierarchy.IsEditable = false; + + Assert.Multiple(() => + { + Assert.That(specHierarchy.IsEditableSpecified, Is.True); + Assert.That(specHierarchy.IsEditable, Is.False); + }); + } + + [TestCase(0, Description = "spec hierarchy")] + [TestCase(3, Description = "attribute definition")] + public async Task Verify_that_a_false_IS_EDITABLE_that_was_read_is_left_out_when_IsEditableSpecified_is_set_to_false(int index) + { + var xml = CreateDocument(out var identifiers); + var reqifs = new ReqIFDeserializer().Deserialize(TestDocuments.ToStream(xml, SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif).ToList(); + + var element = AccessControlledElementOf(reqifs.Single(), identifiers[index]); + Assert.That(element.IsEditableSpecified, Is.True, "the element should have been read with IS-EDITABLE=\"false\""); + + element.IsEditableSpecified = false; + + foreach (var document in await SerializeBothWays(reqifs)) + { + Assert.That(IsEditableOf(document, identifiers[index]), Is.Null); + } + } + + [Test] + public async Task Verify_that_a_false_IS_EDITABLE_is_written_when_IsEditableSpecified_is_set_to_true() + { + var xml = CreateDocument(out var identifiers); + var reqifs = new ReqIFDeserializer().Deserialize(TestDocuments.ToStream(xml, SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif).ToList(); + + SpecHierarchyOf(reqifs.Single(), identifiers[2]).IsEditableSpecified = true; + + foreach (var document in await SerializeBothWays(reqifs)) + { + Assert.That(IsEditableOf(document, identifiers[2]), Is.EqualTo("false")); + } + } + + [TestCase(false, "false")] + [TestCase(true, "true")] + public async Task Verify_that_IsEditable_set_in_code_is_written(bool isEditable, string expected) + { + var xml = CreateDocument(out var identifiers); + var reqifs = new ReqIFDeserializer().Deserialize(TestDocuments.ToStream(xml, SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif).ToList(); + + // identifiers[2] was read without IS-EDITABLE; set it in code and leave the others as read + SpecHierarchyOf(reqifs.Single(), identifiers[2]).IsEditable = isEditable; + + foreach (var document in await SerializeBothWays(reqifs)) + { + Assert.Multiple(() => + { + Assert.That(IsEditableOf(document, identifiers[2]), Is.EqualTo(expected)); + Assert.That(IsEditableOf(document, identifiers[0]), Is.EqualTo("false")); + }); + } + } + + [TestCase(false, "false")] + [TestCase(true, "true")] + public async Task Verify_that_IsEditable_set_in_code_on_an_attribute_definition_is_written(bool isEditable, string expected) + { + var xml = CreateDocument(out var identifiers); + var reqifs = new ReqIFDeserializer().Deserialize(TestDocuments.ToStream(xml, SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif).ToList(); + + // an attribute definition that was read without IS-EDITABLE + var attributeDefinition = reqifs.Single().CoreContent.SpecTypes + .SelectMany(x => x.SpecAttributes) + .First(x => x.Identifier != identifiers[3]); + + Assert.That(attributeDefinition.IsEditableSpecified, Is.False); + + attributeDefinition.IsEditable = isEditable; + + foreach (var document in await SerializeBothWays(reqifs)) + { + Assert.That(IsEditableOf(document, attributeDefinition.Identifier), Is.EqualTo(expected)); + } + } + + [Test] + public async Task Verify_that_a_true_IS_EDITABLE_is_written_when_IsEditableSpecified_is_false() + { + var xml = CreateDocument(out var identifiers); + var reqifs = new ReqIFDeserializer().Deserialize(TestDocuments.ToStream(xml, SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif).ToList(); + + SpecHierarchyOf(reqifs.Single(), identifiers[1]).IsEditableSpecified = false; + + foreach (var document in await SerializeBothWays(reqifs)) + { + Assert.That(IsEditableOf(document, identifiers[1]), Is.EqualTo("true")); + } + } + + [TestCase("true", null)] + [TestCase("false", "true")] + public async Task Verify_that_the_attributes_of_a_spec_hierarchy_with_an_alternative_id_are_written(string isEditable, string isTableInternal) + { + var ns = TestDocuments.ReqIFNamespace; + var source = XDocument.Parse(TestDocuments.Read(FileName)); + + var specHierarchy = source.Descendants(ns + "SPEC-HIERARCHY").First(); + var identifier = specHierarchy.Attribute("IDENTIFIER")!.Value; + specHierarchy.SetAttributeValue("IS-EDITABLE", isEditable); + specHierarchy.SetAttributeValue("IS-TABLE-INTERNAL", isTableInternal); + specHierarchy.Elements(ns + "ALTERNATIVE-ID").Remove(); + specHierarchy.AddFirst(new XElement(ns + "ALTERNATIVE-ID", new XElement(ns + "ALTERNATIVE-ID", new XAttribute("IDENTIFIER", "hierarchy_alternative")))); + + var reqifs = new ReqIFDeserializer().Deserialize(TestDocuments.ToStream(source.ToString(), SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif).ToList(); + + foreach (var document in await SerializeBothWays(reqifs)) + { + var element = document.Descendants(ns + "SPEC-HIERARCHY").Single(x => (string)x.Attribute("IDENTIFIER") == identifier); + + Assert.Multiple(() => + { + Assert.That((string)element.Attribute("IS-EDITABLE"), Is.EqualTo(isEditable)); + Assert.That((string)element.Attribute("IS-TABLE-INTERNAL"), Is.EqualTo(isTableInternal)); + Assert.That((string)element.Element(ns + "ALTERNATIVE-ID")?.Element(ns + "ALTERNATIVE-ID")?.Attribute("IDENTIFIER"), Is.EqualTo("hierarchy_alternative")); + }); + } + } + + [TestCase("true")] + [TestCase("false")] + public async Task Verify_that_the_attributes_of_an_attribute_definition_with_an_alternative_id_are_written(string isEditable) + { + var ns = TestDocuments.ReqIFNamespace; + var source = XDocument.Parse(TestDocuments.Read(FileName)); + + var attributeDefinition = source.Descendants(ns + "ATTRIBUTE-DEFINITION-ENUMERATION").First(); + var identifier = attributeDefinition.Attribute("IDENTIFIER")!.Value; + attributeDefinition.SetAttributeValue("IS-EDITABLE", isEditable); + attributeDefinition.SetAttributeValue("MULTI-VALUED", "true"); + attributeDefinition.Elements(ns + "ALTERNATIVE-ID").Remove(); + attributeDefinition.AddFirst(new XElement(ns + "ALTERNATIVE-ID", new XElement(ns + "ALTERNATIVE-ID", new XAttribute("IDENTIFIER", "definition_alternative")))); + + var reqifs = new ReqIFDeserializer().Deserialize(TestDocuments.ToStream(source.ToString(), SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif).ToList(); + + foreach (var document in await SerializeBothWays(reqifs)) + { + var element = document.Descendants(ns + "ATTRIBUTE-DEFINITION-ENUMERATION").Single(x => (string)x.Attribute("IDENTIFIER") == identifier); + + Assert.Multiple(() => + { + Assert.That((string)element.Attribute("IS-EDITABLE"), Is.EqualTo(isEditable)); + Assert.That((string)element.Attribute("MULTI-VALUED"), Is.EqualTo("true")); + Assert.That((string)element.Element(ns + "ALTERNATIVE-ID")?.Element(ns + "ALTERNATIVE-ID")?.Attribute("IDENTIFIER"), Is.EqualTo("definition_alternative")); + }); + } + } + + [Test] + public async Task Verify_that_the_attributes_of_datatypes_and_enumeration_definitions_are_written() + { + var ns = TestDocuments.ReqIFNamespace; + var numericAttributes = new[] { "MIN", "MAX", "ACCURACY", "MAX-LENGTH" }; + + var xml = TestDocuments.Read("Datatype-Demo.reqif"); + var source = XDocument.Parse(xml); + + var elements = source.Descendants() + .Where(x => numericAttributes.Any(a => x.Attribute(a) != null) || x.Attribute("MULTI-VALUED") != null) + .ToList(); + + var localNames = elements.Select(x => x.Name.LocalName).Distinct().ToList(); + Assert.That(localNames, Is.SupersetOf(new[] { "DATATYPE-DEFINITION-INTEGER", "DATATYPE-DEFINITION-REAL", "DATATYPE-DEFINITION-STRING", "ATTRIBUTE-DEFINITION-ENUMERATION" })); + Assert.That(elements.Where(x => x.Element(ns + "ALTERNATIVE-ID") != null), Is.Not.Empty, "the test document should have a datatype with an alternative id"); + + var reqifs = new ReqIFDeserializer().Deserialize(TestDocuments.ToStream(xml, SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif).ToList(); + + foreach (var document in await SerializeBothWays(reqifs)) + { + foreach (var expected in elements) + { + var identifier = expected.Attribute("IDENTIFIER")!.Value; + var actual = document.Descendants(expected.Name).Single(x => (string)x.Attribute("IDENTIFIER") == identifier); + + Assert.Multiple(() => + { + foreach (var name in numericAttributes.Where(a => expected.Attribute(a) != null)) + { + Assert.That(actual.Attribute(name), Is.Not.Null, $"{name} of {identifier} is not written"); + Assert.That(XmlConvert.ToDouble(actual.Attribute(name)!.Value), Is.EqualTo(XmlConvert.ToDouble(expected.Attribute(name)!.Value)), $"{name} of {identifier}"); + } + + if (expected.Attribute("MULTI-VALUED") != null) + { + Assert.That(actual.Attribute("MULTI-VALUED"), Is.Not.Null, $"MULTI-VALUED of {identifier} is not written"); + Assert.That(XmlConvert.ToBoolean(actual.Attribute("MULTI-VALUED")!.Value), Is.EqualTo(XmlConvert.ToBoolean(expected.Attribute("MULTI-VALUED")!.Value)), $"MULTI-VALUED of {identifier}"); + } + + Assert.That(actual.Element(ns + "ALTERNATIVE-ID") != null, Is.EqualTo(expected.Element(ns + "ALTERNATIVE-ID") != null), $"ALTERNATIVE-ID of {identifier}"); + }); + } + } + } + + [TestCase("")] + [TestCase(" ")] + public async Task Verify_that_an_empty_IS_EDITABLE_is_read_as_not_specified_and_left_out(string isEditable) + { + var ns = TestDocuments.ReqIFNamespace; + var source = XDocument.Parse(TestDocuments.Read(FileName)); + + var specHierarchy = source.Descendants(ns + "SPEC-HIERARCHY").First(); + var identifier = specHierarchy.Attribute("IDENTIFIER")!.Value; + specHierarchy.SetAttributeValue("IS-EDITABLE", isEditable); + + var reqifs = new ReqIFDeserializer().Deserialize(TestDocuments.ToStream(source.ToString(), SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif).ToList(); + + var element = SpecHierarchyOf(reqifs.Single(), identifier); + + Assert.Multiple(() => + { + Assert.That(element.IsEditableSpecified, Is.False); + Assert.That(element.IsEditable, Is.False); + }); + + foreach (var document in await SerializeBothWays(reqifs)) + { + Assert.That(IsEditableOf(document, identifier), Is.Null); + } + } + + [TestCase("0", false, "false")] + [TestCase("1", true, "true")] + public async Task Verify_that_a_numeric_IS_EDITABLE_is_read_and_written_as_a_boolean(string isEditable, bool expectedValue, string expectedText) + { + var ns = TestDocuments.ReqIFNamespace; + var source = XDocument.Parse(TestDocuments.Read(FileName)); + + var specHierarchy = source.Descendants(ns + "SPEC-HIERARCHY").First(); + var identifier = specHierarchy.Attribute("IDENTIFIER")!.Value; + specHierarchy.SetAttributeValue("IS-EDITABLE", isEditable); + + var reqifs = new ReqIFDeserializer().Deserialize(TestDocuments.ToStream(source.ToString(), SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif).ToList(); + + var element = SpecHierarchyOf(reqifs.Single(), identifier); + + Assert.Multiple(() => + { + Assert.That(element.IsEditableSpecified, Is.True); + Assert.That(element.IsEditable, Is.EqualTo(expectedValue)); + }); + + foreach (var document in await SerializeBothWays(reqifs)) + { + Assert.That(IsEditableOf(document, identifier), Is.EqualTo(expectedText)); + } + } + + /// + /// Writes the documents with the sync and with the async serializer + /// + private static async Task> SerializeBothWays(IEnumerable reqifs) + { + var serializer = new ReqIFSerializer(); + + using var syncOutput = new MemoryStream(); + serializer.Serialize(reqifs, syncOutput, SupportedFileExtensionKind.Reqif); + syncOutput.Position = 0; + + using var asyncOutput = new MemoryStream(); + await serializer.SerializeAsync(reqifs, asyncOutput, SupportedFileExtensionKind.Reqif, CancellationToken.None); + asyncOutput.Position = 0; + + return new[] { XDocument.Load(syncOutput), XDocument.Load(asyncOutput) }; + } + } +} diff --git a/ReqIFSharp/AccessControlledElement.cs b/ReqIFSharp/AccessControlledElement.cs index 33eee14..2514892 100644 --- a/ReqIFSharp/AccessControlledElement.cs +++ b/ReqIFSharp/AccessControlledElement.cs @@ -49,15 +49,40 @@ protected AccessControlledElement(ILoggerFactory loggerFactory) this.logger = this.loggerFactory == null ? NullLogger.Instance : this.loggerFactory.CreateLogger(); } + /// + /// Backing field for the property + /// + private bool isEditable; + /// /// Gets or sets a value indicating whether this is editable /// /// /// True means that the element’s contents may be modified by the user of a tool containing the element. /// False or leaving isEditable out means that the element is read-only to the user of a tool containing the element. + /// Setting this property also sets to true, so that the value is written. /// [ReqIfProperty(aggregation: AggregationKind.None, lowerValue: 0, upperValue: 1, isOrdered: false, isReadOnly: false, isDerived: false, isDerivedUnion: false, isUnique: true, defaultValue: null)] - public bool IsEditable { get; set; } + public bool IsEditable + { + get => this.isEditable; + set + { + this.isEditable = value; + this.IsEditableSpecified = true; + } + } + + /// + /// Gets or sets a value indicating whether the IS-EDITABLE attribute is written + /// + /// + /// This is true when the IS-EDITABLE attribute was present in the document that was read, or when + /// was set. When this and are both false, the attribute is + /// left out, which means the element is read-only; an of true is always written. + /// Set it to false to leave out an IS-EDITABLE attribute that is false. + /// + public bool IsEditableSpecified { get; set; } /// /// Generates a object from its XML representation. @@ -88,23 +113,23 @@ internal override void ReadXml(XmlReader reader) } /// - /// Converts a object into its XML representation. + /// Writes the IS-EDITABLE attribute when or is true /// /// /// an instance of /// - internal override void WriteXml(XmlWriter writer) + internal override void WriteXmlAttributes(XmlWriter writer) { - base.WriteXml(writer); + base.WriteXmlAttributes(writer); - if (this.IsEditable) + if (this.IsEditableSpecified || this.IsEditable) { - writer.WriteAttributeString("IS-EDITABLE", "true"); + writer.WriteAttributeString("IS-EDITABLE", XmlConvert.ToString(this.IsEditable)); } } /// - /// Asynchronously converts a object into its XML representation. + /// Asynchronously writes the IS-EDITABLE attribute when or is true /// /// /// an instance of @@ -112,13 +137,13 @@ internal override void WriteXml(XmlWriter writer) /// /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. /// - internal override async Task WriteXmlAsync(XmlWriter writer, CancellationToken token) + internal override async Task WriteXmlAttributesAsync(XmlWriter writer, CancellationToken token) { - await base.WriteXmlAsync(writer, token); + await base.WriteXmlAttributesAsync(writer, token); - if (this.IsEditable) + if (this.IsEditableSpecified || this.IsEditable) { - await writer.WriteAttributeStringAsync(null, "IS-EDITABLE", null, "true"); + await writer.WriteAttributeStringAsync(null, "IS-EDITABLE", null, XmlConvert.ToString(this.IsEditable)); } } } diff --git a/ReqIFSharp/Identifiable.cs b/ReqIFSharp/Identifiable.cs index 1b60d46..af4b4e7 100644 --- a/ReqIFSharp/Identifiable.cs +++ b/ReqIFSharp/Identifiable.cs @@ -163,6 +163,8 @@ internal virtual void WriteXml(XmlWriter writer) writer.WriteAttributeString("LONG-NAME", this.LongName); } + this.WriteXmlAttributes(writer); + if (this.AlternativeId != null) { writer.WriteStartElement("ALTERNATIVE-ID"); @@ -206,6 +208,8 @@ internal virtual async Task WriteXmlAsync(XmlWriter writer, CancellationToken to await writer.WriteAttributeStringAsync(null, "LONG-NAME", null, this.LongName); } + await this.WriteXmlAttributesAsync(writer, token); + if (this.AlternativeId != null) { await writer.WriteStartElementAsync(null, "ALTERNATIVE-ID", null); @@ -214,6 +218,34 @@ internal virtual async Task WriteXmlAsync(XmlWriter writer, CancellationToken to } } + /// + /// Writes the XML attributes that a subclass adds to the element. It is called by + /// after the attributes of the and before its ALTERNATIVE-ID element, since an + /// attribute cannot be written after a child element. + /// + /// + /// an instance of + /// + internal virtual void WriteXmlAttributes(XmlWriter writer) + { + } + + /// + /// Asynchronously writes the XML attributes that a subclass adds to the element. It is called by + /// after the attributes of the and before its + /// ALTERNATIVE-ID element, since an attribute cannot be written after a child element. + /// + /// + /// an instance of + /// + /// + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + internal virtual Task WriteXmlAttributesAsync(XmlWriter writer, CancellationToken token) + { + return Task.CompletedTask; + } + /// /// Reads the ALTERNATIVE-ID /// diff --git a/ReqIFSharp/SpecElementWithAttributes/SpecHierarchy.cs b/ReqIFSharp/SpecElementWithAttributes/SpecHierarchy.cs index 1de41b5..1a6c593 100644 --- a/ReqIFSharp/SpecElementWithAttributes/SpecHierarchy.cs +++ b/ReqIFSharp/SpecElementWithAttributes/SpecHierarchy.cs @@ -308,11 +308,6 @@ internal override void WriteXml(XmlWriter writer) base.WriteXml(writer); - if (this.IsTableInternal) - { - writer.WriteAttributeString("IS-TABLE-INTERNAL", "true"); - } - this.WriteObject(writer); this.WriteEditableAtts(writer); @@ -341,11 +336,6 @@ internal override async Task WriteXmlAsync(XmlWriter writer, CancellationToken t await base.WriteXmlAsync(writer, token); - if (this.IsTableInternal) - { - await writer.WriteAttributeStringAsync(null, "IS-TABLE-INTERNAL",null, "true"); - } - await this.WriteObjectAsync(writer, token); await this.WriteEditableAttsAsync(writer, token); @@ -353,6 +343,41 @@ internal override async Task WriteXmlAsync(XmlWriter writer, CancellationToken t await this.WriteChildrenAsync(writer, token); } + /// + /// Writes the IS-EDITABLE and IS-TABLE-INTERNAL attributes + /// + /// + /// an instance of + /// + internal override void WriteXmlAttributes(XmlWriter writer) + { + base.WriteXmlAttributes(writer); + + if (this.IsTableInternal) + { + writer.WriteAttributeString("IS-TABLE-INTERNAL", "true"); + } + } + + /// + /// Asynchronously writes the IS-EDITABLE and IS-TABLE-INTERNAL attributes + /// + /// + /// an instance of + /// + /// + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + internal override async Task WriteXmlAttributesAsync(XmlWriter writer, CancellationToken token) + { + await base.WriteXmlAttributesAsync(writer, token); + + if (this.IsTableInternal) + { + await writer.WriteAttributeStringAsync(null, "IS-TABLE-INTERNAL", null, "true"); + } + } + /// /// Initializes the properties of the current instance /// From d02d331fed26910c3f4cf6e934878dbd68a47662 Mon Sep 17 00:00:00 2001 From: samatstarion Date: Thu, 22 Oct 2026 08:40:11 +0200 Subject: [PATCH 2/2] [Fix] resolve SonarCloud findings on the IS-EDITABLE change - rename the local in AccessControlledElement.ReadXml that hid the new isEditable field (S1117) - let AssertRoundTrip take a MemoryStream (CA1859) - keep the expected element names in a static readonly field (CA1861) Co-Authored-By: Claude Opus 5.5 --- ReqIFSharp.Tests/IsEditableTestFixture.cs | 12 ++++++++++-- ReqIFSharp/AccessControlledElement.cs | 8 ++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/ReqIFSharp.Tests/IsEditableTestFixture.cs b/ReqIFSharp.Tests/IsEditableTestFixture.cs index 71c6d5e..c8af9cd 100644 --- a/ReqIFSharp.Tests/IsEditableTestFixture.cs +++ b/ReqIFSharp.Tests/IsEditableTestFixture.cs @@ -30,6 +30,14 @@ public class IsEditableTestFixture { private const string FileName = "reqifsharpgenerated.reqif"; + /// + /// The elements that write attributes of their own next to those of an + /// + private static readonly string[] ElementsWithOwnAttributes = + { + "DATATYPE-DEFINITION-INTEGER", "DATATYPE-DEFINITION-REAL", "DATATYPE-DEFINITION-STRING", "ATTRIBUTE-DEFINITION-ENUMERATION" + }; + /// /// Returns the test document with IS-EDITABLE="false" on its first spec hierarchy and on its first /// attribute definition, IS-EDITABLE="true" on its second spec hierarchy and no IS-EDITABLE on its third @@ -90,7 +98,7 @@ private static string IsEditableOf(XDocument document, string identifier) .Attribute("IS-EDITABLE")?.Value; } - private static void AssertRoundTrip(Stream output, string[] identifiers) + private static void AssertRoundTrip(MemoryStream output, string[] identifiers) { output.Position = 0; var document = XDocument.Load(output); @@ -320,7 +328,7 @@ public async Task Verify_that_the_attributes_of_datatypes_and_enumeration_defini .ToList(); var localNames = elements.Select(x => x.Name.LocalName).Distinct().ToList(); - Assert.That(localNames, Is.SupersetOf(new[] { "DATATYPE-DEFINITION-INTEGER", "DATATYPE-DEFINITION-REAL", "DATATYPE-DEFINITION-STRING", "ATTRIBUTE-DEFINITION-ENUMERATION" })); + Assert.That(localNames, Is.SupersetOf(ElementsWithOwnAttributes)); Assert.That(elements.Where(x => x.Element(ns + "ALTERNATIVE-ID") != null), Is.Not.Empty, "the test document should have a datatype with an alternative id"); var reqifs = new ReqIFDeserializer().Deserialize(TestDocuments.ToStream(xml, SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif).ToList(); diff --git a/ReqIFSharp/AccessControlledElement.cs b/ReqIFSharp/AccessControlledElement.cs index 2514892..7c2ffc2 100644 --- a/ReqIFSharp/AccessControlledElement.cs +++ b/ReqIFSharp/AccessControlledElement.cs @@ -98,16 +98,16 @@ internal override void ReadXml(XmlReader reader) this.logger.LogTrace("reading IS-EDITABLE at line:position {LineNumber}:{LinePosition}", xmlLineInfo?.LineNumber, xmlLineInfo?.LinePosition); - var isEditable = reader.GetAttribute("IS-EDITABLE"); - if (!string.IsNullOrWhiteSpace(isEditable)) + var isEditableValue = reader.GetAttribute("IS-EDITABLE"); + if (!string.IsNullOrWhiteSpace(isEditableValue)) { try { - this.IsEditable = XmlConvert.ToBoolean(isEditable); + this.IsEditable = XmlConvert.ToBoolean(isEditableValue); } catch (Exception e) { - throw new SerializationException($"The AccessControlledElement.IS-EDITABLE {isEditable} at line:position {xmlLineInfo?.LineNumber}:{xmlLineInfo?.LinePosition} could not be converted to a BOOLEAN", e); + throw new SerializationException($"The AccessControlledElement.IS-EDITABLE {isEditableValue} at line:position {xmlLineInfo?.LineNumber}:{xmlLineInfo?.LinePosition} could not be converted to a BOOLEAN", e); } } }