diff --git a/CHANGELOG.md b/CHANGELOG.md index d934bce..5ca40b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,21 @@ The repository ships two NuGet packages that version independently: **ReqIFSharp ### Added +- Methods to build a document in code. Each one adds the new element to its owner and sets its back-references; all except `CreateAttributeValue` also give it a valid identifier and a `LastChange` of now. So the written document is valid against the ReqIF schema (unless a relation group has values, see below) and the ReqIFSharp.Extensions queries work on it ([#85](https://github.com/STARIONGROUP/ReqIFSharp/issues/85)): + - `ReqIF.Create(title)` creates a document with a header and a content. + - `ReqIFContent.CreateDatatypeDefinitionBoolean`, `…Date`, `…Enumeration`, `…Integer`, `…Real`, `…String` and `…XHTML`, `CreateSpecObjectType`, `CreateSpecificationType`, `CreateSpecRelationType`, `CreateRelationGroupType`, `CreateSpecObject`, `CreateSpecification`, `CreateSpecRelation` and `CreateRelationGroup`. + - `DatatypeDefinitionEnumeration.CreateEnumValue` and `SpecType.CreateAttributeDefinition`, with an overload for each kind of datatype. + - `CreateAttributeDefinition`, `CreateEnumValue` and `CreateRelationGroup` require a long name, which the ReqIF specification makes mandatory for these elements; they throw when it is null or empty. + - `Specification.AddChild` and `SpecHierarchy.AddChild` add a spec object to the hierarchy. + - The methods throw an `ArgumentException` when a type, spec object, specification or datatype passed to them belongs to another document, so that a document only refers to its own elements. + - `SpecElementWithAttributes.CreateAttributeValue`, with an overload for each kind of attribute definition. It throws an `InvalidOperationException` when the element has no type, and an `ArgumentException` when the definition has no datatype, is not an attribute of the element's type, the element already has a value for it, several enumeration literals are passed for a definition that is not multi-valued, an enumeration literal does not belong to the definition's enumeration, an integer or real value is outside the `Min` and `Max` of its datatype (or a real is not a number), a string is longer than the `MaxLength` of its datatype, or the XHTML is not well-formed with a single `xhtml:div` or `xhtml:p` root element (an XML declaration is not allowed either). A null string throws an `ArgumentNullException`, as the schema requires a value; use an empty string for no text. When the XHTML uses the `xhtml` prefix without declaring it, `xmlns:xhtml` is added to its root element, so the value is complete on its own, as a value read from a document is. An XHTML value gets an `ExternalObject` for each `xhtml:object` it contains, as when it is read. An attribute value has no identifier or `LastChange`, and the `LastChange` of the element is left as it is. A relation group can have values, as the ReqIF metamodel allows, but the ReqIF XML schema has no `VALUES` on a `RELATION-GROUP`, so a document with them is not valid against the schema. +- `Identifiable.CreateIdentifier()` returns a new identifier that is a valid `xsd:ID`: an underscore followed by a GUID. `Identifiable.MarkAsChanged()` sets `LastChange` to now; call it after changing an element ([#85](https://github.com/STARIONGROUP/ReqIFSharp/issues/85)). - `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 +- The `TheValue` and `TheOriginalValue` of an XHTML attribute value are read as they are in the document. Before, whitespace at the start and the end was removed, so a document that was read and written again could differ ([#85](https://github.com/STARIONGROUP/ReqIFSharp/issues/85)). - 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)). @@ -23,6 +33,7 @@ The repository ships two NuGet packages that version independently: **ReqIFSharp ### Fixed +- An infinite real attribute value was written as `Infinity` or `-Infinity`, which is not a valid `xsd:double`, so the document failed schema validation and could not be read back. It is now written as `INF` or `-INF`, as the `MIN` and `MAX` of a real datatype already were ([#85](https://github.com/STARIONGROUP/ReqIFSharp/issues/85)). - A document that binds the ReqIF namespace to a prefix (``) was read as zero documents, and type, source/target and relation-group references in it were not read. Elements are now matched on their local name and namespace ([#87](https://github.com/STARIONGROUP/ReqIFSharp/issues/87)). - 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)). diff --git a/ReqIFSharp.Extensions.Tests/ReqIFExtensions/CreatedDocumentQueriesTestFixture.cs b/ReqIFSharp.Extensions.Tests/ReqIFExtensions/CreatedDocumentQueriesTestFixture.cs new file mode 100644 index 0000000..3e9ddf0 --- /dev/null +++ b/ReqIFSharp.Extensions.Tests/ReqIFExtensions/CreatedDocumentQueriesTestFixture.cs @@ -0,0 +1,186 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// SPDX-FileCopyrightText: 2017-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace ReqIFSharp.Extensions.Tests.ReqIFExtensions +{ + using System; + using System.Linq; + + using NUnit.Framework; + + using ReqIFSharp; + using ReqIFSharp.Extensions.ReqIFExtensions; + + /// + /// Verifies that the query extension methods work on a document built in code with the create methods of the + /// ReqIFSharp model, without setting any reference by hand + /// + [TestFixture] + public class CreatedDocumentQueriesTestFixture + { + private ReqIFContent content; + + private DatatypeDefinitionString stringType; + + private DatatypeDefinitionXHTML xhtmlType; + + private DatatypeDefinitionEnumeration statusType; + + private EnumValue approved; + + private SpecObjectType requirementType; + + private AttributeDefinitionString idAttribute; + + private AttributeDefinitionXHTML textAttribute; + + private AttributeDefinitionEnumeration statusAttribute; + + private SpecificationType specificationType; + + private SpecRelationType refinesType; + + private RelationGroupType traceType; + + private SpecObject brew; + + private SpecObject heat; + + private SpecObject tank; + + private Specification specification; + + private SpecHierarchy brewNode; + + private SpecRelation relation; + + private RelationGroup relationGroup; + + [SetUp] + public void SetUp() + { + this.content = ReqIF.Create("Coffee machine requirements").CoreContent; + + this.stringType = this.content.CreateDatatypeDefinitionString("String", 255); + this.xhtmlType = this.content.CreateDatatypeDefinitionXHTML("Formatted text"); + this.statusType = this.content.CreateDatatypeDefinitionEnumeration("Status"); + var draft = this.statusType.CreateEnumValue("Draft", 0, "draft"); + this.approved = this.statusType.CreateEnumValue("Approved", 1, "approved"); + + this.requirementType = this.content.CreateSpecObjectType("Requirement"); + this.idAttribute = this.requirementType.CreateAttributeDefinition(this.stringType, "ID"); + this.textAttribute = this.requirementType.CreateAttributeDefinition(this.xhtmlType, "Text"); + this.statusAttribute = this.requirementType.CreateAttributeDefinition(this.statusType, "Status"); + + this.specificationType = this.content.CreateSpecificationType("Document"); + this.refinesType = this.content.CreateSpecRelationType("refines"); + this.traceType = this.content.CreateRelationGroupType("trace"); + + SpecObject CreateRequirement(string id, EnumValue status) + { + var requirement = this.content.CreateSpecObject(this.requirementType, id); + requirement.CreateAttributeValue(this.idAttribute, id); + requirement.CreateAttributeValue(this.textAttribute, $"The text of {id}"); + requirement.CreateAttributeValue(this.statusAttribute, status); + return requirement; + } + + this.brew = CreateRequirement("REQ-1", this.approved); + this.heat = CreateRequirement("REQ-2", draft); + this.tank = CreateRequirement("REQ-3", draft); + + this.specification = this.content.CreateSpecification(this.specificationType, "Coffee machine"); + this.brewNode = this.specification.AddChild(this.brew); + this.brewNode.AddChild(this.heat); + this.specification.AddChild(this.tank); + + this.relation = this.content.CreateSpecRelation(this.refinesType, this.heat, this.brew); + + this.relationGroup = this.content.CreateRelationGroup(this.traceType, this.specification, this.specification, "Trace"); + this.relationGroup.SpecRelations.Add(this.relation); + } + + [Test] + public void Verify_that_the_type_queries_find_the_elements_of_each_type() + { + Assert.Multiple(() => + { + Assert.That(this.requirementType.QueryReferencingSpecObjects(), Is.EquivalentTo(new[] { this.brew, this.heat, this.tank })); + Assert.That(this.specificationType.QueryReferencingSpecifications(), Is.EquivalentTo(new[] { this.specification })); + Assert.That(this.refinesType.QueryReferencingSpecRelations(), Is.EquivalentTo(new[] { this.relation })); + Assert.That(this.traceType.QueryReferencingRelationGroups(), Is.EquivalentTo(new[] { this.relationGroup })); + }); + } + + [Test] + public void Verify_that_the_datatype_queries_work() + { + Assert.Multiple(() => + { + Assert.That(this.stringType.QueryReferencingAttributeDefinitions(), Is.EquivalentTo(new[] { this.idAttribute })); + Assert.That(this.statusType.QueryReferencingAttributeDefinitions(), Is.EquivalentTo(new[] { this.statusAttribute })); + Assert.That(this.stringType.QueryDatatypeName(), Is.EqualTo("String")); + Assert.That(this.idAttribute.QueryDatatypeName(), Is.EqualTo("String")); + Assert.That(this.statusAttribute.QueryDatatypeName(), Is.EqualTo("Enumeration")); + Assert.That(this.idAttribute.QueryDefaultValueAsFormattedString(), Is.EqualTo("NOT SET")); + }); + } + + [Test] + public void Verify_that_the_spec_object_queries_work() + { + Assert.Multiple(() => + { + Assert.That(this.heat.QuerySpecRelations(), Is.EquivalentTo(new[] { this.relation })); + Assert.That(this.brew.QuerySpecRelations(), Is.EquivalentTo(new[] { this.relation })); + Assert.That(this.tank.QuerySpecRelations(), Is.Empty); + Assert.That(this.heat.QueryContainerSpecifications(), Is.EquivalentTo(new[] { this.specification })); + }); + } + + [Test] + public void Verify_that_the_specification_queries_work() + { + Assert.Multiple(() => + { + Assert.That(this.specification.QueryAllContainedSpecObjects(), Is.EquivalentTo(new[] { this.brew, this.heat, this.tank })); + Assert.That(this.specification.QueryAllContainedSpecHierarchies().Select(x => x.Object), Is.EquivalentTo(new[] { this.brew, this.heat, this.tank })); + Assert.That(this.brewNode.QueryAllContainedSpecHierarchies().Select(x => x.Object), Is.EquivalentTo(new[] { this.heat })); + Assert.That(this.specification.QueryAttributeDefinitions(), Is.EquivalentTo(new AttributeDefinition[] { this.idAttribute, this.textAttribute, this.statusAttribute })); + }); + } + + [Test] + public void Verify_that_the_value_queries_work() + { + var values = this.brew.Values; + + Assert.Multiple(() => + { + Assert.That(values.Select(x => x.QueryFormattedValue()), Is.EqualTo(new[] { "REQ-1", "The text of REQ-1", "approved" })); + Assert.That(this.brew.QueryExternalObjects(), Is.Empty); + Assert.That(this.content.QueryExternalObjects(), Is.Empty); + Assert.That(this.brew.QueryTypeDisplayName(), Is.EqualTo("Spec Object")); + }); + } + + [Test] + public void Verify_that_QueryExternalObjects_finds_an_object_in_a_created_xhtml_value() + { + var pump = this.content.CreateSpecObject(this.requirementType, "REQ-4"); + pump.CreateAttributeValue(this.textAttribute, "The pump "); + + Assert.Multiple(() => + { + Assert.That(pump.QueryExternalObjects().Select(x => x.Uri), Is.EqualTo(new[] { "files/pump.png" })); + Assert.That(this.content.QueryExternalObjects().Select(x => x.Uri), Is.EqualTo(new[] { "files/pump.png" })); + }); + } + } +} diff --git a/ReqIFSharp.Tests/CreateElementsTestFixture.cs b/ReqIFSharp.Tests/CreateElementsTestFixture.cs new file mode 100644 index 0000000..65100d8 --- /dev/null +++ b/ReqIFSharp.Tests/CreateElementsTestFixture.cs @@ -0,0 +1,1357 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// SPDX-FileCopyrightText: 2017-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace ReqIFSharp.Tests +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using System.Xml; + + using NUnit.Framework; + + using ReqIFSharp; + + /// + /// Verifies the methods that create elements of a document in code: each element gets a valid + /// identifier, its and its back-references, and a document built with them is + /// valid against the ReqIF schema. + /// + [TestFixture] + public class CreateElementsTestFixture + { + private DateTime before; + + private ReqIF reqif; + + private ReqIFContent content; + + [SetUp] + public void SetUp() + { + this.before = DateTime.UtcNow; + this.reqif = ReqIF.Create("a title"); + this.content = this.reqif.CoreContent; + } + + /// + /// Asserts that the element was given a new identifier that is a valid xsd:ID, the long name and a last change + /// between the start of the test and now + /// + private void AssertCreated(Identifiable identifiable, string longName) + { + Assert.Multiple(() => + { + Assert.That(identifiable.Identifier, Does.StartWith("_")); + Assert.That(() => XmlConvert.VerifyNCName(identifiable.Identifier), Throws.Nothing); + Assert.That(identifiable.LongName, Is.EqualTo(longName)); + Assert.That(identifiable.LastChange, Is.InRange(this.before, DateTime.UtcNow)); + Assert.That(identifiable.LastChange.Kind, Is.EqualTo(DateTimeKind.Utc)); + }); + } + + [Test] + public void Verify_that_CreateIdentifier_returns_a_unique_valid_xsd_ID() + { + var identifiers = Enumerable.Range(0, 100).Select(_ => Identifiable.CreateIdentifier()).ToList(); + + Assert.Multiple(() => + { + Assert.That(identifiers, Is.Unique); + Assert.That(identifiers, Has.All.StartWith("_")); + Assert.That(() => identifiers.ForEach(x => XmlConvert.VerifyNCName(x)), Throws.Nothing); + }); + } + + [Test] + public void Verify_that_MarkAsChanged_sets_LastChange_to_now() + { + var specObject = new SpecObject { LastChange = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc) }; + + var start = DateTime.UtcNow; + specObject.MarkAsChanged(); + + Assert.That(specObject.LastChange, Is.InRange(start, DateTime.UtcNow)); + } + + [Test] + public void Verify_that_Create_returns_a_document_with_a_header_and_a_content() + { + var header = this.reqif.TheHeader; + + Assert.Multiple(() => + { + Assert.That(header.Identifier, Does.StartWith("_")); + Assert.That(header.Title, Is.EqualTo("a title")); + Assert.That(header.CreationTime, Is.InRange(this.before, DateTime.UtcNow)); + Assert.That(header.CreationTime.Kind, Is.EqualTo(DateTimeKind.Utc)); + Assert.That(header.ReqIFVersion, Is.EqualTo("1.0")); + Assert.That(header.ReqIFToolId, Is.EqualTo("ReqIFSharp")); + Assert.That(header.SourceToolId, Is.EqualTo("ReqIFSharp")); + Assert.That(header.DocumentRoot, Is.SameAs(this.reqif)); + Assert.That(this.content, Is.Not.Null); + Assert.That(this.content.DocumentRoot, Is.SameAs(this.reqif)); + }); + } + + [Test] + public void Verify_that_the_datatype_definitions_are_created_in_the_content() + { + var datatypes = new DatatypeDefinition[] + { + this.content.CreateDatatypeDefinitionBoolean("boolean"), + this.content.CreateDatatypeDefinitionDate("date"), + this.content.CreateDatatypeDefinitionEnumeration("enumeration"), + this.content.CreateDatatypeDefinitionInteger("integer", -10, 10), + this.content.CreateDatatypeDefinitionReal("real", 2, -1.5, 1.5), + this.content.CreateDatatypeDefinitionString("string", 255), + this.content.CreateDatatypeDefinitionXHTML("xhtml") + }; + + Assert.That(this.content.DataTypes, Is.EqualTo(datatypes)); + + foreach (var datatype in datatypes) + { + Assert.That(datatype.ReqIFContent, Is.SameAs(this.content)); + } + + var names = new[] { "boolean", "date", "enumeration", "integer", "real", "string", "xhtml" }; + + for (var i = 0; i < datatypes.Length; i++) + { + this.AssertCreated(datatypes[i], names[i]); + } + + var integer = (DatatypeDefinitionInteger)datatypes[3]; + var real = (DatatypeDefinitionReal)datatypes[4]; + var text = (DatatypeDefinitionString)datatypes[5]; + + Assert.Multiple(() => + { + Assert.That(integer.Min, Is.EqualTo(-10)); + Assert.That(integer.Max, Is.EqualTo(10)); + Assert.That(real.Accuracy, Is.EqualTo(2)); + Assert.That(real.Min, Is.EqualTo(-1.5)); + Assert.That(real.Max, Is.EqualTo(1.5)); + Assert.That(text.MaxLength, Is.EqualTo(255)); + }); + } + + [Test] + public void Verify_that_invalid_datatype_limits_throw() + { + Assert.Multiple(() => + { + Assert.That(() => this.content.CreateDatatypeDefinitionInteger("integer", 1, 0), Throws.ArgumentException.With.Property("ParamName").EqualTo("min")); + Assert.That(() => this.content.CreateDatatypeDefinitionReal("real", -1, 0, 1), Throws.ArgumentException.With.Property("ParamName").EqualTo("accuracy")); + Assert.That(() => this.content.CreateDatatypeDefinitionReal("real", 2, 1, 0), Throws.ArgumentException.With.Property("ParamName").EqualTo("min")); + Assert.That(() => this.content.CreateDatatypeDefinitionString("string", -1), Throws.ArgumentException.With.Property("ParamName").EqualTo("maxLength")); + Assert.That(this.content.DataTypes, Is.Empty); + }); + } + + [Test] + public void Verify_that_CreateEnumValue_adds_a_literal_with_its_properties() + { + var enumeration = this.content.CreateDatatypeDefinitionEnumeration("status"); + + var draft = enumeration.CreateEnumValue("draft", 0); + var approved = enumeration.CreateEnumValue("approved", 1, "green"); + + Assert.That(enumeration.SpecifiedValues, Is.EqualTo(new[] { draft, approved })); + this.AssertCreated(approved, "approved"); + + Assert.Multiple(() => + { + Assert.That(approved.DataTpeDefEnum, Is.SameAs(enumeration)); + Assert.That(approved.Properties.Key, Is.EqualTo(1)); + Assert.That(approved.Properties.OtherContent, Is.EqualTo("green")); + Assert.That(approved.Properties.EnumValue, Is.SameAs(approved)); + Assert.That(draft.Properties.OtherContent, Is.Empty); + }); + } + + [Test] + public void Verify_that_the_spec_types_are_created_in_the_content() + { + var specTypes = new SpecType[] + { + this.content.CreateSpecObjectType("requirement"), + this.content.CreateSpecificationType("document"), + this.content.CreateSpecRelationType("refines"), + this.content.CreateRelationGroupType("trace") + }; + + Assert.That(this.content.SpecTypes, Is.EqualTo(specTypes)); + + foreach (var specType in specTypes) + { + Assert.That(specType.ReqIFContent, Is.SameAs(this.content)); + } + + this.AssertCreated(specTypes[0], "requirement"); + this.AssertCreated(specTypes[1], "document"); + this.AssertCreated(specTypes[2], "refines"); + this.AssertCreated(specTypes[3], "trace"); + } + + [Test] + public void Verify_that_the_attribute_definitions_are_created_in_the_spec_type() + { + var specType = this.content.CreateSpecObjectType("requirement"); + + var enumeration = this.content.CreateDatatypeDefinitionEnumeration("status"); + + var datatypes = new DatatypeDefinition[] + { + this.content.CreateDatatypeDefinitionBoolean("boolean"), + this.content.CreateDatatypeDefinitionDate("date"), + enumeration, + this.content.CreateDatatypeDefinitionInteger("integer", 0, 10), + this.content.CreateDatatypeDefinitionReal("real", 2, 0, 1), + this.content.CreateDatatypeDefinitionString("string", 10), + this.content.CreateDatatypeDefinitionXHTML("xhtml") + }; + + var definitions = new AttributeDefinition[] + { + specType.CreateAttributeDefinition((DatatypeDefinitionBoolean)datatypes[0], "boolean attribute"), + specType.CreateAttributeDefinition((DatatypeDefinitionDate)datatypes[1], "date attribute"), + specType.CreateAttributeDefinition(enumeration, "enumeration attribute", true), + specType.CreateAttributeDefinition((DatatypeDefinitionInteger)datatypes[3], "integer attribute"), + specType.CreateAttributeDefinition((DatatypeDefinitionReal)datatypes[4], "real attribute"), + specType.CreateAttributeDefinition((DatatypeDefinitionString)datatypes[5], "string attribute"), + specType.CreateAttributeDefinition((DatatypeDefinitionXHTML)datatypes[6], "xhtml attribute") + }; + + Assert.That(specType.SpecAttributes, Is.EqualTo(definitions)); + + Assert.Multiple(() => + { + for (var i = 0; i < definitions.Length; i++) + { + Assert.That(definitions[i].SpecType, Is.SameAs(specType)); + Assert.That(definitions[i].DatatypeDefinition, Is.SameAs(datatypes[i])); + } + }); + + var names = new[] { "boolean attribute", "date attribute", "enumeration attribute", "integer attribute", "real attribute", "string attribute", "xhtml attribute" }; + + for (var i = 0; i < definitions.Length; i++) + { + this.AssertCreated(definitions[i], names[i]); + } + + Assert.Multiple(() => + { + Assert.That(((AttributeDefinitionEnumeration)definitions[2]).IsMultiValued, Is.True); + Assert.That(specType.CreateAttributeDefinition(enumeration, "single").IsMultiValued, Is.False); + Assert.That(() => specType.CreateAttributeDefinition((DatatypeDefinitionString)null, "string"), Throws.ArgumentNullException); + }); + } + + [Test] + public void Verify_that_the_spec_elements_are_created_in_the_content() + { + var requirement = this.content.CreateSpecObjectType("requirement"); + var document = this.content.CreateSpecificationType("document"); + var refines = this.content.CreateSpecRelationType("refines"); + var trace = this.content.CreateRelationGroupType("trace"); + + var source = this.content.CreateSpecObject(requirement, "source"); + var target = this.content.CreateSpecObject(requirement); + var specification = this.content.CreateSpecification(document, "specification"); + var relation = this.content.CreateSpecRelation(refines, source, target, "relation"); + var relationGroup = this.content.CreateRelationGroup(trace, specification, specification, "group"); + + Assert.Multiple(() => + { + Assert.That(this.content.SpecObjects, Is.EqualTo(new[] { source, target })); + Assert.That(this.content.Specifications, Is.EqualTo(new[] { specification })); + Assert.That(this.content.SpecRelations, Is.EqualTo(new[] { relation })); + Assert.That(this.content.SpecRelationGroups, Is.EqualTo(new[] { relationGroup })); + + Assert.That(source.Type, Is.SameAs(requirement)); + Assert.That(source.ReqIFContent, Is.SameAs(this.content)); + Assert.That(target.LongName, Is.Null); + Assert.That(specification.Type, Is.SameAs(document)); + Assert.That(specification.ReqIFContent, Is.SameAs(this.content)); + Assert.That(relation.Type, Is.SameAs(refines)); + Assert.That(relation.Source, Is.SameAs(source)); + Assert.That(relation.Target, Is.SameAs(target)); + Assert.That(relation.ReqIFContent, Is.SameAs(this.content)); + Assert.That(relationGroup.Type, Is.SameAs(trace)); + Assert.That(relationGroup.SourceSpecification, Is.SameAs(specification)); + Assert.That(relationGroup.TargetSpecification, Is.SameAs(specification)); + Assert.That(relationGroup.CoreContent, Is.SameAs(this.content)); + }); + + this.AssertCreated(source, "source"); + this.AssertCreated(specification, "specification"); + this.AssertCreated(relation, "relation"); + this.AssertCreated(relationGroup, "group"); + } + + [Test] + public void Verify_that_creating_a_spec_element_without_its_type_or_ends_throws() + { + var requirement = this.content.CreateSpecObjectType("requirement"); + var specObject = this.content.CreateSpecObject(requirement); + var specification = this.content.CreateSpecification(this.content.CreateSpecificationType("document")); + var refines = this.content.CreateSpecRelationType("refines"); + var trace = this.content.CreateRelationGroupType("trace"); + + Assert.Multiple(() => + { + Assert.That(() => this.content.CreateSpecObject(null), Throws.ArgumentNullException); + Assert.That(() => this.content.CreateSpecification(null), Throws.ArgumentNullException); + Assert.That(() => this.content.CreateSpecRelation(null, specObject, specObject), Throws.ArgumentNullException); + Assert.That(() => this.content.CreateSpecRelation(refines, null, specObject), Throws.ArgumentNullException); + Assert.That(() => this.content.CreateSpecRelation(refines, specObject, null), Throws.ArgumentNullException); + Assert.That(() => this.content.CreateRelationGroup(null, specification, specification, "group"), Throws.ArgumentNullException); + Assert.That(() => this.content.CreateRelationGroup(trace, null, specification, "group"), Throws.ArgumentNullException); + Assert.That(() => this.content.CreateRelationGroup(trace, specification, null, "group"), Throws.ArgumentNullException); + + Assert.That(this.content.SpecObjects, Has.Count.EqualTo(1)); + Assert.That(this.content.Specifications, Has.Count.EqualTo(1)); + Assert.That(this.content.SpecRelations, Is.Empty); + Assert.That(this.content.SpecRelationGroups, Is.Empty); + }); + } + + [Test] + public void Verify_that_AddChild_builds_the_spec_hierarchy() + { + var requirement = this.content.CreateSpecObjectType("requirement"); + var specification = this.content.CreateSpecification(this.content.CreateSpecificationType("document")); + + var parentObject = this.content.CreateSpecObject(requirement); + var childObject = this.content.CreateSpecObject(requirement); + + var parent = specification.AddChild(parentObject); + var child = parent.AddChild(childObject); + + Assert.Multiple(() => + { + Assert.That(specification.Children, Is.EqualTo(new[] { parent })); + Assert.That(parent.Children, Is.EqualTo(new[] { child })); + + Assert.That(parent.Object, Is.SameAs(parentObject)); + Assert.That(parent.Root, Is.SameAs(specification)); + Assert.That(parent.Container, Is.Null); + Assert.That(parent.ReqIfContent, Is.SameAs(this.content)); + + Assert.That(child.Object, Is.SameAs(childObject)); + Assert.That(child.Root, Is.SameAs(specification)); + Assert.That(child.Container, Is.SameAs(parent)); + Assert.That(child.ReqIfContent, Is.SameAs(this.content)); + + Assert.That(() => specification.AddChild(null), Throws.ArgumentNullException); + Assert.That(() => parent.AddChild(null), Throws.ArgumentNullException); + }); + + this.AssertCreated(parent, null); + this.AssertCreated(child, null); + } + + [Test] + public void Verify_that_the_attribute_values_are_created_in_the_spec_element() + { + var requirement = this.content.CreateSpecObjectType("requirement"); + + var enumeration = this.content.CreateDatatypeDefinitionEnumeration("status"); + var draft = enumeration.CreateEnumValue("draft", 0); + var approved = enumeration.CreateEnumValue("approved", 1); + + var booleanDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionBoolean("boolean"), "boolean"); + var dateDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionDate("date"), "date"); + var enumerationDefinition = requirement.CreateAttributeDefinition(enumeration, "status", true); + var integerDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionInteger("integer", 0, 10), "integer"); + var realDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionReal("real", 2, 0, 10), "real"); + var stringDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionString("string", 10), "string"); + var xhtmlDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionXHTML("xhtml"), "xhtml"); + + var specObject = this.content.CreateSpecObject(requirement); + var date = new DateTime(2026, 10, 22, 12, 0, 0, DateTimeKind.Utc); + + var values = new AttributeValue[] + { + specObject.CreateAttributeValue(booleanDefinition, true), + specObject.CreateAttributeValue(dateDefinition, date), + specObject.CreateAttributeValue(enumerationDefinition, draft, approved), + specObject.CreateAttributeValue(integerDefinition, 7), + specObject.CreateAttributeValue(realDefinition, 1.25), + specObject.CreateAttributeValue(stringDefinition, "text"), + specObject.CreateAttributeValue(xhtmlDefinition, "rich text") + }; + + Assert.That(specObject.Values, Is.EqualTo(values)); + + Assert.Multiple(() => + { + foreach (var value in values) + { + Assert.That(value.SpecElAt, Is.SameAs(specObject)); + Assert.That(value.AttributeDefinition, Is.SameAs(requirement.SpecAttributes[Array.IndexOf(values, value)])); + } + + Assert.That(((AttributeValueBoolean)values[0]).TheValue, Is.True); + Assert.That(((AttributeValueDate)values[1]).TheValue, Is.EqualTo(date)); + Assert.That(((AttributeValueEnumeration)values[2]).Values, Is.EqualTo(new[] { draft, approved })); + Assert.That(((AttributeValueInteger)values[3]).TheValue, Is.EqualTo(7)); + Assert.That(((AttributeValueReal)values[4]).TheValue, Is.EqualTo(1.25)); + Assert.That(((AttributeValueString)values[5]).TheValue, Is.EqualTo("text")); + Assert.That(((AttributeValueXHTML)values[6]).TheValue, Is.EqualTo("rich text")); + }); + } + + [Test] + public void Verify_that_an_attribute_value_that_does_not_fit_the_element_throws() + { + var requirement = this.content.CreateSpecObjectType("requirement"); + var other = this.content.CreateSpecObjectType("other"); + + var enumeration = this.content.CreateDatatypeDefinitionEnumeration("status"); + var draft = enumeration.CreateEnumValue("draft", 0); + var approved = enumeration.CreateEnumValue("approved", 1); + var foreign = this.content.CreateDatatypeDefinitionEnumeration("other").CreateEnumValue("foreign", 0); + + var stringType = this.content.CreateDatatypeDefinitionString("string", 10); + var stringDefinition = requirement.CreateAttributeDefinition(stringType, "string"); + var otherDefinition = other.CreateAttributeDefinition(stringType, "other string"); + var singleDefinition = requirement.CreateAttributeDefinition(enumeration, "status"); + var xhtmlDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionXHTML("xhtml"), "xhtml"); + + var specObject = this.content.CreateSpecObject(requirement); + specObject.CreateAttributeValue(stringDefinition, "first"); + + Assert.Multiple(() => + { + Assert.That(() => specObject.CreateAttributeValue((AttributeDefinitionString)null, "text"), Throws.ArgumentNullException); + Assert.That(() => specObject.CreateAttributeValue(otherDefinition, "text"), Throws.ArgumentException.With.Message.Contains("is not an attribute of the spec type")); + Assert.That(() => specObject.CreateAttributeValue(stringDefinition, "second"), Throws.ArgumentException.With.Message.Contains("already has a value")); + Assert.That(() => specObject.CreateAttributeValue(singleDefinition, draft, approved), Throws.ArgumentException.With.Message.Contains("is not multi-valued")); + Assert.That(() => specObject.CreateAttributeValue(singleDefinition, foreign), Throws.ArgumentException.With.Message.Contains("is not a literal")); + Assert.That(() => specObject.CreateAttributeValue(singleDefinition, (EnumValue)null), Throws.ArgumentException.With.Property("ParamName").EqualTo("values")); + Assert.That(() => specObject.CreateAttributeValue(singleDefinition, (EnumValue[])null), Throws.ArgumentNullException); + Assert.That(() => specObject.CreateAttributeValue(xhtmlDefinition, null), Throws.ArgumentNullException); + + Assert.That(specObject.Values, Has.Count.EqualTo(1)); + }); + + var noValue = specObject.CreateAttributeValue(singleDefinition); + Assert.That(noValue.Values, Is.Empty); + } + + [Test] + public void Verify_that_an_attribute_value_for_an_element_without_a_spec_type_throws() + { + var requirement = this.content.CreateSpecObjectType("requirement"); + var definition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionString("string", 10), "string"); + + SpecElementWithAttributes[] elements = + [ + new SpecObject { Identifier = "spec-object" }, + new Specification { Identifier = "specification" }, + new SpecRelation { Identifier = "spec-relation" }, + new RelationGroup { Identifier = "relation-group" } + ]; + + Assert.Multiple(() => + { + foreach (var element in elements) + { + Assert.That(() => element.CreateAttributeValue(definition, "text"), Throws.InvalidOperationException.With.Message.Contains("has no spec type"), element.Identifier); + Assert.That(element.Values, Is.Empty, element.Identifier); + } + }); + } + + [Test] + public void Verify_that_an_attribute_value_for_a_definition_without_a_datatype_throws_for_each_kind() + { + var requirement = this.content.CreateSpecObjectType("requirement"); + + var booleanDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionBoolean("boolean"), "boolean"); + var dateDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionDate("date"), "date"); + var enumeration = this.content.CreateDatatypeDefinitionEnumeration("status"); + var draft = enumeration.CreateEnumValue("draft", 0); + var enumerationDefinition = requirement.CreateAttributeDefinition(enumeration, "status"); + var integerDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionInteger("integer", 0, 10), "integer"); + var realDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionReal("real", 2, 0, 10), "real"); + var stringDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionString("string", 10), "string"); + var xhtmlDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionXHTML("xhtml"), "xhtml"); + + booleanDefinition.Type = null; + dateDefinition.Type = null; + enumerationDefinition.Type = null; + integerDefinition.Type = null; + realDefinition.Type = null; + stringDefinition.Type = null; + xhtmlDefinition.Type = null; + + var specObject = this.content.CreateSpecObject(requirement); + + var message = "has no datatype"; + + Assert.Multiple(() => + { + Assert.That(() => specObject.CreateAttributeValue(booleanDefinition, true), Throws.ArgumentException.With.Message.Contains(message)); + Assert.That(() => specObject.CreateAttributeValue(dateDefinition, DateTime.UtcNow), Throws.ArgumentException.With.Message.Contains(message)); + Assert.That(() => specObject.CreateAttributeValue(enumerationDefinition, draft), Throws.ArgumentException.With.Message.Contains(message)); + Assert.That(() => specObject.CreateAttributeValue(integerDefinition, 5), Throws.ArgumentException.With.Message.Contains(message)); + Assert.That(() => specObject.CreateAttributeValue(realDefinition, 5.0), Throws.ArgumentException.With.Message.Contains(message)); + Assert.That(() => specObject.CreateAttributeValue(stringDefinition, "text"), Throws.ArgumentException.With.Message.Contains(message)); + Assert.That(() => specObject.CreateAttributeValue(xhtmlDefinition, "text"), Throws.ArgumentException.With.Message.Contains(message)); + + Assert.That(specObject.Values, Is.Empty); + }); + } + + [Test] + public void Verify_that_equal_and_zero_datatype_limits_are_accepted() + { + var integer = this.content.CreateDatatypeDefinitionInteger("integer", 5, 5); + var real = this.content.CreateDatatypeDefinitionReal("real", 0, 1.5, 1.5); + var text = this.content.CreateDatatypeDefinitionString("string", 0); + + Assert.Multiple(() => + { + Assert.That(integer.Min, Is.EqualTo(5)); + Assert.That(integer.Max, Is.EqualTo(5)); + Assert.That(real.Accuracy, Is.EqualTo(0)); + Assert.That(real.Min, Is.EqualTo(1.5)); + Assert.That(real.Max, Is.EqualTo(1.5)); + Assert.That(text.MaxLength, Is.EqualTo(0)); + Assert.That(this.content.DataTypes, Is.EqualTo(new DatatypeDefinition[] { integer, real, text })); + }); + } + + [Test] + public void Verify_that_CreateAttributeDefinition_throws_when_the_datatype_is_null() + { + var specType = this.content.CreateSpecObjectType("requirement"); + + Assert.Multiple(() => + { + Assert.That(() => specType.CreateAttributeDefinition((DatatypeDefinitionBoolean)null, "boolean"), Throws.ArgumentNullException); + Assert.That(() => specType.CreateAttributeDefinition((DatatypeDefinitionDate)null, "date"), Throws.ArgumentNullException); + Assert.That(() => specType.CreateAttributeDefinition((DatatypeDefinitionEnumeration)null, "enumeration"), Throws.ArgumentNullException); + Assert.That(() => specType.CreateAttributeDefinition((DatatypeDefinitionInteger)null, "integer"), Throws.ArgumentNullException); + Assert.That(() => specType.CreateAttributeDefinition((DatatypeDefinitionReal)null, "real"), Throws.ArgumentNullException); + Assert.That(() => specType.CreateAttributeDefinition((DatatypeDefinitionString)null, "string"), Throws.ArgumentNullException); + Assert.That(() => specType.CreateAttributeDefinition((DatatypeDefinitionXHTML)null, "xhtml"), Throws.ArgumentNullException); + Assert.That(specType.SpecAttributes, Is.Empty); + }); + } + + [Test] + public void Verify_that_a_second_value_for_the_same_definition_throws_for_each_kind() + { + var requirement = this.content.CreateSpecObjectType("requirement"); + var enumeration = this.content.CreateDatatypeDefinitionEnumeration("status"); + var draft = enumeration.CreateEnumValue("draft", 0); + + var booleanDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionBoolean("boolean"), "boolean"); + var dateDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionDate("date"), "date"); + var enumerationDefinition = requirement.CreateAttributeDefinition(enumeration, "status"); + var integerDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionInteger("integer", 0, 10), "integer"); + var realDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionReal("real", 2, 0, 10), "real"); + var xhtmlDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionXHTML("xhtml"), "xhtml"); + + var specObject = this.content.CreateSpecObject(requirement); + specObject.CreateAttributeValue(booleanDefinition, true); + specObject.CreateAttributeValue(dateDefinition, DateTime.UtcNow); + specObject.CreateAttributeValue(enumerationDefinition, draft); + specObject.CreateAttributeValue(integerDefinition, 1); + specObject.CreateAttributeValue(realDefinition, 1.0); + specObject.CreateAttributeValue(xhtmlDefinition, "text"); + + Assert.Multiple(() => + { + Assert.That(() => specObject.CreateAttributeValue(booleanDefinition, false), Throws.ArgumentException.With.Message.Contains("already has a value")); + Assert.That(() => specObject.CreateAttributeValue(dateDefinition, DateTime.UtcNow), Throws.ArgumentException.With.Message.Contains("already has a value")); + Assert.That(() => specObject.CreateAttributeValue(enumerationDefinition, draft), Throws.ArgumentException.With.Message.Contains("already has a value")); + Assert.That(() => specObject.CreateAttributeValue(integerDefinition, 2), Throws.ArgumentException.With.Message.Contains("already has a value")); + Assert.That(() => specObject.CreateAttributeValue(realDefinition, 2.0), Throws.ArgumentException.With.Message.Contains("already has a value")); + Assert.That(() => specObject.CreateAttributeValue(xhtmlDefinition, "other"), Throws.ArgumentException.With.Message.Contains("already has a value")); + Assert.That(specObject.Values, Has.Count.EqualTo(6)); + }); + } + + [Test] + public void Verify_that_a_value_for_an_attribute_of_another_type_throws_on_every_kind_of_spec_element() + { + var stringType = this.content.CreateDatatypeDefinitionString("string", 10); + var foreignDefinition = this.content.CreateSpecObjectType("requirement").CreateAttributeDefinition(stringType, "foreign"); + + var specificationType = this.content.CreateSpecificationType("document"); + var relationType = this.content.CreateSpecRelationType("refines"); + var groupType = this.content.CreateRelationGroupType("trace"); + specificationType.CreateAttributeDefinition(stringType, "title"); + relationType.CreateAttributeDefinition(stringType, "rationale"); + groupType.CreateAttributeDefinition(stringType, "comment"); + + var specObject = this.content.CreateSpecObject(this.content.CreateSpecObjectType("other")); + var specification = this.content.CreateSpecification(specificationType); + var relation = this.content.CreateSpecRelation(relationType, specObject, specObject); + var group = this.content.CreateRelationGroup(groupType, specification, specification, "group"); + + Assert.Multiple(() => + { + foreach (var element in new SpecElementWithAttributes[] { specification, relation, group }) + { + Assert.That(() => element.CreateAttributeValue(foreignDefinition, "text"), Throws.ArgumentException.With.Message.Contains("is not an attribute of the spec type")); + Assert.That(element.Values, Is.Empty); + } + }); + } + + [Test] + public void Verify_that_the_logger_factory_of_the_document_is_passed_to_the_created_elements() + { + var loggerFactory = new RecordingLoggerFactory(); + var document = ReqIF.Create("a title", loggerFactory); + var documentContent = document.CoreContent; + + var stringType = documentContent.CreateDatatypeDefinitionString("string", 10); + var enumeration = documentContent.CreateDatatypeDefinitionEnumeration("status"); + var draft = enumeration.CreateEnumValue("draft", 0); + var requirement = documentContent.CreateSpecObjectType("requirement"); + var idAttribute = requirement.CreateAttributeDefinition(stringType, "id"); + var statusAttribute = requirement.CreateAttributeDefinition(enumeration, "status"); + var specObject = documentContent.CreateSpecObject(requirement); + specObject.CreateAttributeValue(idAttribute, "REQ-1"); + specObject.CreateAttributeValue(statusAttribute, draft); + var specification = documentContent.CreateSpecification(documentContent.CreateSpecificationType("document")); + specification.AddChild(specObject).AddChild(specObject); + documentContent.CreateSpecRelation(documentContent.CreateSpecRelationType("refines"), specObject, specObject); + documentContent.CreateRelationGroup(documentContent.CreateRelationGroupType("trace"), specification, specification, "group"); + + var expected = new[] + { + typeof(ReqIF), typeof(ReqIFHeader), typeof(ReqIFContent), typeof(DatatypeDefinitionString), + typeof(DatatypeDefinitionEnumeration), typeof(EnumValue), typeof(EmbeddedValue), typeof(SpecType), + typeof(AttributeDefinitionString), typeof(AttributeDefinitionEnumeration), typeof(SpecObject), + typeof(AttributeValueString), typeof(AttributeValueEnumeration), typeof(Specification), + typeof(SpecHierarchy), typeof(SpecRelation), typeof(RelationGroup) + }; + + Assert.That(loggerFactory.Categories, Is.SupersetOf(expected.Select(x => x.FullName))); + } + + /// + /// An that records the categories of the loggers it creates + /// + private sealed class RecordingLoggerFactory : Microsoft.Extensions.Logging.ILoggerFactory + { + public HashSet Categories { get; } = new HashSet(); + + public Microsoft.Extensions.Logging.ILogger CreateLogger(string categoryName) + { + this.Categories.Add(categoryName); + return Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance; + } + + public void AddProvider(Microsoft.Extensions.Logging.ILoggerProvider provider) + { + } + + public void Dispose() + { + } + } + + [Test] + public void Verify_that_a_missing_long_name_throws_where_the_specification_makes_it_mandatory() + { + var specType = this.content.CreateSpecObjectType("requirement"); + var enumeration = this.content.CreateDatatypeDefinitionEnumeration("status"); + var booleanType = this.content.CreateDatatypeDefinitionBoolean("boolean"); + var dateType = this.content.CreateDatatypeDefinitionDate("date"); + var integerType = this.content.CreateDatatypeDefinitionInteger("integer", 0, 10); + var realType = this.content.CreateDatatypeDefinitionReal("real", 2, 0, 1); + var stringType = this.content.CreateDatatypeDefinitionString("string", 10); + var xhtmlType = this.content.CreateDatatypeDefinitionXHTML("xhtml"); + var specification = this.content.CreateSpecification(this.content.CreateSpecificationType("document")); + var trace = this.content.CreateRelationGroupType("trace"); + + var createDefinitions = new Func[] + { + x => specType.CreateAttributeDefinition(booleanType, x), + x => specType.CreateAttributeDefinition(dateType, x), + x => specType.CreateAttributeDefinition(enumeration, x), + x => specType.CreateAttributeDefinition(integerType, x), + x => specType.CreateAttributeDefinition(realType, x), + x => specType.CreateAttributeDefinition(stringType, x), + x => specType.CreateAttributeDefinition(xhtmlType, x) + }; + + Assert.Multiple(() => + { + foreach (var createDefinition in createDefinitions) + { + Assert.That(() => createDefinition(null), Throws.ArgumentNullException.With.Property("ParamName").EqualTo("longName")); + Assert.That(() => createDefinition(string.Empty), Throws.ArgumentException.With.Property("ParamName").EqualTo("longName")); + } + + Assert.That(() => enumeration.CreateEnumValue(null, 0), Throws.ArgumentNullException.With.Property("ParamName").EqualTo("longName")); + Assert.That(() => enumeration.CreateEnumValue(string.Empty, 0), Throws.ArgumentException.With.Property("ParamName").EqualTo("longName")); + Assert.That(() => this.content.CreateRelationGroup(trace, specification, specification, null), Throws.ArgumentNullException.With.Property("ParamName").EqualTo("longName")); + Assert.That(() => this.content.CreateRelationGroup(trace, specification, specification, string.Empty), Throws.ArgumentException.With.Property("ParamName").EqualTo("longName")); + + Assert.That(specType.SpecAttributes, Is.Empty); + Assert.That(enumeration.SpecifiedValues, Is.Empty); + Assert.That(this.content.SpecRelationGroups, Is.Empty); + }); + } + + [Test] + public void Verify_that_a_relation_group_can_have_values_as_the_metamodel_allows() + { + this.BuildDocument(); + + var traceType = this.content.SpecTypes.OfType().Single(); + var reviewedAttribute = traceType.CreateAttributeDefinition(this.content.DataTypes.OfType().Single(), "Reviewed"); + var group = this.content.SpecRelationGroups.Single(); + + var value = group.CreateAttributeValue(reviewedAttribute, true); + + using var output = Write(this.reqif); + var readGroup = Read(output).CoreContent.SpecRelationGroups.Single(); + + Assert.Multiple(() => + { + Assert.That(group.Values, Is.EqualTo(new[] { value })); + Assert.That(value.SpecElAt, Is.SameAs(group)); + Assert.That(DescribeValues(readGroup), Is.EqualTo(DescribeValues(group))); + + // the ReqIF XML schema has no VALUES on RELATION-GROUP, unlike the metamodel + Assert.That(ValidationMessages(output), Has.Exactly(1).Contains("'RELATION-GROUP'").And.Contains("'VALUES'")); + }); + } + + [Test] + public void Verify_that_an_xhtml_value_created_in_code_has_its_external_objects() + { + var requirement = this.content.CreateSpecObjectType("requirement"); + var xhtmlDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionXHTML("xhtml"), "xhtml"); + var specObject = this.content.CreateSpecObject(requirement); + + var value = specObject.CreateAttributeValue(xhtmlDefinition, + "a picture"); + + Assert.That(value.ExternalObjects, Has.Count.EqualTo(2)); + + var picture = value.ExternalObjects[0]; + var document = value.ExternalObjects[1]; + + Assert.Multiple(() => + { + Assert.That(picture.Owner, Is.SameAs(value)); + Assert.That(picture.Uri, Is.EqualTo("files/a b.png")); + Assert.That(picture.MimeType, Is.EqualTo("image/png")); + Assert.That(picture.Height, Is.EqualTo(20)); + Assert.That(picture.Width, Is.EqualTo(30)); + Assert.That(document.Uri, Is.EqualTo("files/c.pdf")); + Assert.That(document.MimeType, Is.EqualTo("application/pdf")); + }); + } + + [Test] + public void Verify_that_xhtml_that_can_be_a_value_is_accepted() + { + var requirement = this.content.CreateSpecObjectType("requirement"); + var datatype = this.content.CreateDatatypeDefinitionXHTML("xhtml"); + + var accepted = new[] + { + "prefix without a declaration", + "prefix with a declaration", + "
default namespace
", + "a paragraph", + " whitespace and a comment around the root " + }; + + Assert.Multiple(() => + { + for (var i = 0; i < accepted.Length; i++) + { + var definition = requirement.CreateAttributeDefinition(datatype, $"xhtml {i}"); + var specObject = this.content.CreateSpecObject(requirement); + var xhtml = accepted[i]; + + Assert.That(() => specObject.CreateAttributeValue(definition, xhtml), Throws.Nothing, xhtml); + Assert.That(specObject.Values, Has.Count.EqualTo(1), xhtml); + } + }); + } + + [Test] + public void Verify_that_xhtml_that_cannot_be_a_value_throws() + { + var requirement = this.content.CreateSpecObjectType("requirement"); + var definition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionXHTML("xhtml"), "xhtml"); + var specObject = this.content.CreateSpecObject(requirement); + + var rejected = new[] + { + string.Empty, + "plain text", + "not closed", + "onetwo", + "not a block", + "wrong case", + "an XML declaration", + "CDATA before the root", + "]>&e;", + "
no namespace
", + "another namespace", + "text before the root", + "the root and text after", + "an undeclared prefix" + }; + + Assert.Multiple(() => + { + foreach (var xhtml in rejected) + { + Assert.That(() => specObject.CreateAttributeValue(definition, xhtml), Throws.ArgumentException.With.Property("ParamName").EqualTo("xhtml"), xhtml); + } + + Assert.That(specObject.Values, Is.Empty); + }); + } + + [Test] + public void Verify_that_elements_of_another_document_are_rejected() + { + var other = ReqIF.Create("another document").CoreContent; + var otherObjectType = other.CreateSpecObjectType("requirement"); + var otherSpecificationType = other.CreateSpecificationType("document"); + var otherRelationType = other.CreateSpecRelationType("refines"); + var otherGroupType = other.CreateRelationGroupType("trace"); + var otherString = other.CreateDatatypeDefinitionString("string", 10); + var otherObject = other.CreateSpecObject(otherObjectType); + var otherSpecification = other.CreateSpecification(otherSpecificationType); + + var objectType = this.content.CreateSpecObjectType("requirement"); + var specObject = this.content.CreateSpecObject(objectType); + var specification = this.content.CreateSpecification(this.content.CreateSpecificationType("document")); + var node = specification.AddChild(specObject); + var relationType = this.content.CreateSpecRelationType("refines"); + var groupType = this.content.CreateRelationGroupType("trace"); + + Assert.Multiple(() => + { + Assert.That(() => this.content.CreateSpecObject(otherObjectType), Throws.ArgumentException.With.Property("ParamName").EqualTo("type")); + Assert.That(() => this.content.CreateSpecification(otherSpecificationType), Throws.ArgumentException.With.Property("ParamName").EqualTo("type")); + Assert.That(() => this.content.CreateSpecRelation(otherRelationType, specObject, specObject), Throws.ArgumentException.With.Property("ParamName").EqualTo("type")); + Assert.That(() => this.content.CreateSpecRelation(relationType, otherObject, specObject), Throws.ArgumentException.With.Property("ParamName").EqualTo("source")); + Assert.That(() => this.content.CreateSpecRelation(relationType, specObject, otherObject), Throws.ArgumentException.With.Property("ParamName").EqualTo("target")); + Assert.That(() => this.content.CreateRelationGroup(otherGroupType, specification, specification, "group"), Throws.ArgumentException.With.Property("ParamName").EqualTo("type")); + Assert.That(() => this.content.CreateRelationGroup(groupType, otherSpecification, specification, "group"), Throws.ArgumentException.With.Property("ParamName").EqualTo("sourceSpecification")); + Assert.That(() => this.content.CreateRelationGroup(groupType, specification, otherSpecification, "group"), Throws.ArgumentException.With.Property("ParamName").EqualTo("targetSpecification")); + Assert.That(() => objectType.CreateAttributeDefinition(otherString, "string"), Throws.ArgumentException.With.Property("ParamName").EqualTo("type")); + Assert.That(() => specification.AddChild(otherObject), Throws.ArgumentException.With.Property("ParamName").EqualTo("specObject")); + Assert.That(() => node.AddChild(otherObject), Throws.ArgumentException.With.Property("ParamName").EqualTo("specObject")); + + Assert.That(this.content.SpecObjects, Is.EqualTo(new[] { specObject })); + Assert.That(this.content.Specifications, Is.EqualTo(new[] { specification })); + Assert.That(this.content.SpecRelations, Is.Empty); + Assert.That(this.content.SpecRelationGroups, Is.Empty); + Assert.That(objectType.SpecAttributes, Is.Empty); + Assert.That(specification.Children, Is.EqualTo(new[] { node })); + Assert.That(node.Children, Is.Empty); + }); + } + + [Test] + public void Verify_that_elements_that_belong_to_no_document_are_accepted() + { + var detachedType = new SpecObjectType(); + var detachedString = new DatatypeDefinitionString { Identifier = Identifiable.CreateIdentifier() }; + var specObjectType = this.content.CreateSpecObjectType("requirement"); + + Assert.Multiple(() => + { + Assert.That(() => this.content.CreateSpecObject(detachedType), Throws.Nothing); + Assert.That(() => specObjectType.CreateAttributeDefinition(detachedString, "string"), Throws.Nothing); + }); + } + + [Test] + public void Verify_that_CreateAttributeValue_leaves_the_LastChange_of_the_element_unchanged() + { + var requirement = this.content.CreateSpecObjectType("requirement"); + var stringDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionString("string", 10), "string"); + var xhtmlDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionXHTML("xhtml"), "xhtml"); + var specObject = this.content.CreateSpecObject(requirement); + + var lastChange = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc); + specObject.LastChange = lastChange; + + specObject.CreateAttributeValue(stringDefinition, "text"); + specObject.CreateAttributeValue(xhtmlDefinition, "text"); + + Assert.That(specObject.LastChange, Is.EqualTo(lastChange)); + } + + [Test] + public void Verify_that_an_empty_created_document_is_valid_against_the_schema() + { + using var output = Write(this.reqif); + + Assert.That(ValidationMessages(output), Is.Empty); + } + + [Test] + public void Verify_that_values_within_the_limits_of_their_datatype_are_accepted() + { + var requirement = this.content.CreateSpecObjectType("requirement"); + var integerDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionInteger("integer", -5, 5), "integer"); + var realDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionReal("real", 2, -1.5, 1.5), "real"); + var stringDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionString("string", 3), "string"); + + Assert.Multiple(() => + { + foreach (var value in new long[] { -5, 0, 5 }) + { + Assert.That(() => this.content.CreateSpecObject(requirement).CreateAttributeValue(integerDefinition, value), Throws.Nothing, value.ToString()); + } + + foreach (var value in new[] { -1.5, 0, 1.5 }) + { + Assert.That(() => this.content.CreateSpecObject(requirement).CreateAttributeValue(realDefinition, value), Throws.Nothing, value.ToString()); + } + + foreach (var value in new[] { string.Empty, "abc" }) + { + Assert.That(() => this.content.CreateSpecObject(requirement).CreateAttributeValue(stringDefinition, value), Throws.Nothing, value); + } + }); + } + + [Test] + public void Verify_that_values_outside_the_limits_of_their_datatype_throw() + { + var requirement = this.content.CreateSpecObjectType("requirement"); + var integerDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionInteger("integer", -5, 5), "integer"); + var realDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionReal("real", 2, -1.5, 1.5), "real"); + var stringDefinition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionString("string", 3), "string"); + var specObject = this.content.CreateSpecObject(requirement); + + Assert.Multiple(() => + { + foreach (var value in new long[] { -6, 6, long.MinValue, long.MaxValue }) + { + Assert.That(() => specObject.CreateAttributeValue(integerDefinition, value), Throws.ArgumentException.With.Property("ParamName").EqualTo("value"), value.ToString()); + } + + foreach (var value in new[] { -1.51, 1.51, double.NaN, double.NegativeInfinity, double.PositiveInfinity }) + { + Assert.That(() => specObject.CreateAttributeValue(realDefinition, value), Throws.ArgumentException.With.Property("ParamName").EqualTo("value"), value.ToString()); + } + + Assert.That(() => specObject.CreateAttributeValue(stringDefinition, "abcd"), Throws.ArgumentException.With.Property("ParamName").EqualTo("value")); + Assert.That(() => specObject.CreateAttributeValue(stringDefinition, null), Throws.ArgumentNullException.With.Property("ParamName").EqualTo("value")); + + Assert.That(specObject.Values, Is.Empty); + }); + } + + [Test] + public void Verify_that_CreateAttributeDefinition_rejects_a_datatype_of_another_document_for_each_kind() + { + var other = ReqIF.Create("another document").CoreContent; + var specType = this.content.CreateSpecObjectType("requirement"); + + var createDefinitions = new Func[] + { + () => specType.CreateAttributeDefinition(other.CreateDatatypeDefinitionBoolean("boolean"), "boolean"), + () => specType.CreateAttributeDefinition(other.CreateDatatypeDefinitionDate("date"), "date"), + () => specType.CreateAttributeDefinition(other.CreateDatatypeDefinitionEnumeration("enumeration"), "enumeration"), + () => specType.CreateAttributeDefinition(other.CreateDatatypeDefinitionInteger("integer", 0, 10), "integer"), + () => specType.CreateAttributeDefinition(other.CreateDatatypeDefinitionReal("real", 2, 0, 1), "real"), + () => specType.CreateAttributeDefinition(other.CreateDatatypeDefinitionString("string", 10), "string"), + () => specType.CreateAttributeDefinition(other.CreateDatatypeDefinitionXHTML("xhtml"), "xhtml") + }; + + Assert.Multiple(() => + { + foreach (var createDefinition in createDefinitions) + { + Assert.That(() => createDefinition(), Throws.ArgumentException.With.Property("ParamName").EqualTo("type")); + } + + Assert.That(specType.SpecAttributes, Is.Empty); + }); + } + + [Test] + public async Task Verify_that_an_xhtml_value_created_in_a_document_read_with_another_xhtml_prefix_is_written_well_formed() + { + this.BuildDocument(); + + // a document from another tool that binds the XHTML namespace to another prefix only + string xml; + + using (var original = Write(this.reqif)) + { + xml = System.Text.Encoding.UTF8.GetString(original.ToArray()) + .Replace("xmlns:xhtml=", "xmlns:reqif-xhtml=") + .Replace("new
"); + + using var output = Write(document); + + using var asyncOutput = new MemoryStream(); + await new ReqIFSerializer().SerializeAsync(new[] { document }, asyncOutput, SupportedFileExtensionKind.Reqif, CancellationToken.None); + + Assert.Multiple(() => + { + foreach (var stream in new[] { output, asyncOutput }) + { + var readBack = Read(stream).CoreContent.SpecObjects.Single(x => x.LongName == "REQ-4"); + Assert.That(readBack.Values.OfType().Single().ExtractUnformattedTextFromValue(), Is.EqualTo("new")); + Assert.That(ValidationMessages(stream), Is.Empty); + + // the root element keeps the declarations of the document that was read + var written = System.Text.Encoding.UTF8.GetString(stream.ToArray()); + var rootStart = written.IndexOf("', rootStart) - rootStart); + Assert.That(rootElement, Does.Contain("xmlns:reqif-xhtml=")); + Assert.That(rootElement, Does.Not.Contain("xmlns:xhtml=")); + } + }); + } + + [Test] + public async Task Verify_that_an_xhtml_value_is_read_with_the_whitespace_around_it() + { + var requirement = this.content.CreateSpecObjectType("requirement"); + var definition = requirement.CreateAttributeDefinition(this.content.CreateDatatypeDefinitionXHTML("xhtml"), "xhtml"); + var specObject = this.content.CreateSpecObject(requirement); + var value = specObject.CreateAttributeValue(definition, "\n text\n "); + value.TheOriginalValue = "\n original\n"; + + using var output = Write(this.reqif); + + output.Position = 0; + var asyncDocument = (await new ReqIFDeserializer().DeserializeAsync(output, SupportedFileExtensionKind.Reqif, CancellationToken.None)).Single(); + + Assert.Multiple(() => + { + foreach (var document in new[] { Read(output), asyncDocument }) + { + var readValue = document.CoreContent.SpecObjects.Single().Values.OfType().Single(); + Assert.That(readValue.TheValue, Is.EqualTo(value.TheValue)); + Assert.That(readValue.TheOriginalValue, Is.EqualTo(value.TheOriginalValue)); + } + }); + } + + [Test] + public void Verify_that_a_created_xhtml_value_declares_the_xhtml_prefix_when_it_uses_it_undeclared() + { + var requirement = this.content.CreateSpecObjectType("requirement"); + var datatype = this.content.CreateDatatypeDefinitionXHTML("xhtml"); + const string declaration = "xmlns:xhtml=\"http://www.w3.org/1999/xhtml\""; + + var cases = new[] + { + ("text", $"text"), + ("text", $"text"), + ("\r\n text", $"\r\n text"), + ("
bold
", $"
bold
"), + ($"declared", $"declared"), + ("
no prefix
", "
no prefix
") + }; + + Assert.Multiple(() => + { + for (var i = 0; i < cases.Length; i++) + { + var (xhtml, expected) = cases[i]; + var definition = requirement.CreateAttributeDefinition(datatype, $"xhtml {i}"); + var value = this.content.CreateSpecObject(requirement).CreateAttributeValue(definition, xhtml); + + Assert.That(value.TheValue, Is.EqualTo(expected), xhtml); + } + }); + } + + /// + /// Builds a document with every kind of element and attribute value, using only the create methods. The relation + /// group has no values, so that the document is valid: the ReqIF XML schema has no VALUES on a RELATION-GROUP + /// + private void BuildDocument() + { + var stringType = this.content.CreateDatatypeDefinitionString("String", 255); + var xhtmlType = this.content.CreateDatatypeDefinitionXHTML("Formatted text"); + var integerType = this.content.CreateDatatypeDefinitionInteger("Integer", 0, 100); + var realType = this.content.CreateDatatypeDefinitionReal("Real", 2, 0, 100); + var booleanType = this.content.CreateDatatypeDefinitionBoolean("Boolean"); + var dateType = this.content.CreateDatatypeDefinitionDate("Date"); + var statusType = this.content.CreateDatatypeDefinitionEnumeration("Status"); + var draft = statusType.CreateEnumValue("Draft", 0); + var approved = statusType.CreateEnumValue("Approved", 1); + + var requirementType = this.content.CreateSpecObjectType("Requirement"); + var idAttribute = requirementType.CreateAttributeDefinition(stringType, "ID"); + var textAttribute = requirementType.CreateAttributeDefinition(xhtmlType, "Text"); + var priorityAttribute = requirementType.CreateAttributeDefinition(integerType, "Priority"); + var weightAttribute = requirementType.CreateAttributeDefinition(realType, "Weight"); + var safetyAttribute = requirementType.CreateAttributeDefinition(booleanType, "Safety"); + var dueAttribute = requirementType.CreateAttributeDefinition(dateType, "Due"); + var statusAttribute = requirementType.CreateAttributeDefinition(statusType, "Status"); + var tagsAttribute = requirementType.CreateAttributeDefinition(statusType, "Tags", isMultiValued: true); + + var specificationType = this.content.CreateSpecificationType("Document"); + var titleAttribute = specificationType.CreateAttributeDefinition(stringType, "Title"); + var refinesType = this.content.CreateSpecRelationType("refines"); + var rationaleAttribute = refinesType.CreateAttributeDefinition(stringType, "Rationale"); + var traceType = this.content.CreateRelationGroupType("trace"); + + SpecObject CreateRequirement(string id, int priority, EnumValue status, params EnumValue[] tags) + { + var requirement = this.content.CreateSpecObject(requirementType, id); + requirement.CreateAttributeValue(idAttribute, id); + requirement.CreateAttributeValue(textAttribute, $"The text of {id}"); + requirement.CreateAttributeValue(priorityAttribute, priority); + requirement.CreateAttributeValue(weightAttribute, priority / 4.0); + requirement.CreateAttributeValue(safetyAttribute, priority % 2 == 0); + requirement.CreateAttributeValue(dueAttribute, new DateTime(2027, 1, priority, 0, 0, 0, DateTimeKind.Utc)); + requirement.CreateAttributeValue(statusAttribute, status); + requirement.CreateAttributeValue(tagsAttribute, tags); + return requirement; + } + + var brew = CreateRequirement("REQ-1", 1, approved, draft, approved); + var heat = CreateRequirement("REQ-2", 2, draft, approved); + var tank = CreateRequirement("REQ-3", 3, draft); + + var specification = this.content.CreateSpecification(specificationType, "Coffee machine"); + specification.CreateAttributeValue(titleAttribute, "Coffee machine"); + specification.AddChild(brew).AddChild(heat); + specification.AddChild(tank); + + var relation = this.content.CreateSpecRelation(refinesType, heat, brew, "REQ-2 refines REQ-1"); + relation.CreateAttributeValue(rationaleAttribute, "heating is part of brewing"); + + var group = this.content.CreateRelationGroup(traceType, specification, specification, "Trace"); + group.SpecRelations.Add(relation); + } + + private static List ValidationMessages(MemoryStream output) + { + output.Position = 0; + + var messages = new List(); + new ReqIFDeserializer().Deserialize(output, SupportedFileExtensionKind.Reqif, true, (_, e) => messages.Add($"{e.Severity}: {e.Message}")); + + return messages; + } + + private static MemoryStream Write(ReqIF document) + { + var output = new MemoryStream(); + new ReqIFSerializer().Serialize(new[] { document }, output, SupportedFileExtensionKind.Reqif); + return output; + } + + private static ReqIF Read(MemoryStream stream) + { + stream.Position = 0; + return new ReqIFDeserializer().Deserialize(stream, SupportedFileExtensionKind.Reqif).Single(); + } + + /// + /// Describes the values of an element by attribute definition identifier, so they can be compared between the + /// document that was built and the document that was read back + /// + private static List DescribeValues(SpecElementWithAttributes element) + { + return element.Values.Select(x => + { + var value = x switch + { + AttributeValueEnumeration enumeration => string.Join(",", enumeration.Values.Select(v => v.Identifier)), + AttributeValueDate date => date.TheValue.ToUniversalTime().ToString("o"), + AttributeValueReal real => real.TheValue.ToString("R", System.Globalization.CultureInfo.InvariantCulture), + AttributeValueXHTML xhtml => xhtml.ExtractUnformattedTextFromValue(), + _ => Convert.ToString(x.ObjectValue, System.Globalization.CultureInfo.InvariantCulture) + }; + + return $"{x.AttributeDefinition.Identifier}={value}"; + }).ToList(); + } + + /// + /// Describes a spec hierarchy as the identifiers of its spec objects, with the children in brackets + /// + private static string DescribeHierarchy(IEnumerable children) + { + return string.Join(" ", children.Select(x => x.Children.Count == 0 ? x.Object.Identifier : $"{x.Object.Identifier}[{DescribeHierarchy(x.Children)}]")); + } + + [Test] + public void Verify_that_a_document_built_with_the_create_methods_is_valid_against_the_schema() + { + this.BuildDocument(); + + using var output = Write(this.reqif); + + Assert.That(ValidationMessages(output), Is.Empty); + } + + [Test] + public async Task Verify_that_a_document_built_with_the_create_methods_is_valid_against_the_schema_async() + { + this.BuildDocument(); + + using var output = new MemoryStream(); + await new ReqIFSerializer().SerializeAsync(new[] { this.reqif }, output, SupportedFileExtensionKind.Reqif, CancellationToken.None); + + Assert.That(ValidationMessages(output), Is.Empty); + } + + [Test] + public async Task Verify_that_infinite_real_values_are_written_valid_against_the_schema_and_read_back() + { + var requirement = this.content.CreateSpecObjectType("requirement"); + var unbounded = this.content.CreateDatatypeDefinitionReal("unbounded", 2, double.NegativeInfinity, double.PositiveInfinity); + var high = requirement.CreateAttributeDefinition(unbounded, "high"); + var low = requirement.CreateAttributeDefinition(unbounded, "low"); + var finite = requirement.CreateAttributeDefinition(unbounded, "finite"); + + var specObject = this.content.CreateSpecObject(requirement); + specObject.CreateAttributeValue(high, double.PositiveInfinity); + specObject.CreateAttributeValue(low, double.NegativeInfinity); + specObject.CreateAttributeValue(finite, 0.1); + + using var syncOutput = Write(this.reqif); + + using var asyncOutput = new MemoryStream(); + await new ReqIFSerializer().SerializeAsync(new[] { this.reqif }, asyncOutput, SupportedFileExtensionKind.Reqif, CancellationToken.None); + + foreach (var (output, name) in new[] { (syncOutput, "sync"), (asyncOutput, "async") }) + { + var xml = System.Text.Encoding.UTF8.GetString(output.ToArray()); + var values = Read(output).CoreContent.SpecObjects.Single().Values.OfType().ToDictionary(x => x.Definition.LongName, x => x.TheValue); + + Assert.Multiple(() => + { + Assert.That(xml, Does.Contain("THE-VALUE=\"INF\"").And.Contain("THE-VALUE=\"-INF\"").And.Contain("THE-VALUE=\"0.1\""), name); + Assert.That(ValidationMessages(output), Is.Empty, name); + Assert.That(values["high"], Is.EqualTo(double.PositiveInfinity), name); + Assert.That(values["low"], Is.EqualTo(double.NegativeInfinity), name); + Assert.That(values["finite"], Is.EqualTo(0.1), name); + }); + } + } + + [Test] + public void Verify_that_a_document_built_with_the_create_methods_is_read_back_with_the_same_content() + { + this.BuildDocument(); + + using var output = Write(this.reqif); + var readContent = Read(output).CoreContent; + + var specification = this.content.Specifications.Single(); + var readSpecification = readContent.Specifications.Single(); + var relation = this.content.SpecRelations.Single(); + var readRelation = readContent.SpecRelations.Single(); + var group = this.content.SpecRelationGroups.Single(); + var readGroup = readContent.SpecRelationGroups.Single(); + + Assert.Multiple(() => + { + Assert.That(readContent.DataTypes.Select(x => x.Identifier), Is.EqualTo(this.content.DataTypes.Select(x => x.Identifier))); + Assert.That(readContent.SpecTypes.Select(x => x.Identifier), Is.EqualTo(this.content.SpecTypes.Select(x => x.Identifier))); + Assert.That(readContent.SpecObjects.Select(x => x.Identifier), Is.EqualTo(this.content.SpecObjects.Select(x => x.Identifier))); + + foreach (var specObject in this.content.SpecObjects) + { + var readObject = readContent.SpecObjects.Single(x => x.Identifier == specObject.Identifier); + Assert.That(readObject.LongName, Is.EqualTo(specObject.LongName)); + Assert.That(readObject.Type.Identifier, Is.EqualTo(specObject.Type.Identifier)); + Assert.That(DescribeValues(readObject), Is.EqualTo(DescribeValues(specObject))); + } + + Assert.That(readSpecification.Identifier, Is.EqualTo(specification.Identifier)); + Assert.That(DescribeValues(readSpecification), Is.EqualTo(DescribeValues(specification))); + Assert.That(DescribeHierarchy(readSpecification.Children), Is.EqualTo(DescribeHierarchy(specification.Children))); + Assert.That(readSpecification.Children.Select(x => x.Identifier), Is.EqualTo(specification.Children.Select(x => x.Identifier))); + + Assert.That(readRelation.Identifier, Is.EqualTo(relation.Identifier)); + Assert.That(readRelation.Source.Identifier, Is.EqualTo(relation.Source.Identifier)); + Assert.That(readRelation.Target.Identifier, Is.EqualTo(relation.Target.Identifier)); + Assert.That(DescribeValues(readRelation), Is.EqualTo(DescribeValues(relation))); + + Assert.That(readGroup.Identifier, Is.EqualTo(group.Identifier)); + Assert.That(readGroup.SourceSpecification.Identifier, Is.EqualTo(specification.Identifier)); + Assert.That(readGroup.TargetSpecification.Identifier, Is.EqualTo(specification.Identifier)); + Assert.That(readGroup.SpecRelations.Select(x => x.Identifier), Is.EqualTo(new[] { relation.Identifier })); + }); + } + + [Test] + public void Verify_that_the_create_methods_extend_a_document_that_was_read() + { + this.BuildDocument(); + + using var original = Write(this.reqif); + var document = Read(original); + var readContent = document.CoreContent; + + var requirementType = readContent.SpecTypes.OfType().Single(); + var idAttribute = requirementType.SpecAttributes.OfType().Single(); + var statusAttribute = requirementType.SpecAttributes.OfType().First(x => !x.IsMultiValued); + var approved = statusAttribute.Type.SpecifiedValues.Single(x => x.LongName == "Approved"); + var refinesType = readContent.SpecTypes.OfType().Single(); + var specification = readContent.Specifications.Single(); + var brew = readContent.SpecObjects.Single(x => x.LongName == "REQ-1"); + + var grind = readContent.CreateSpecObject(requirementType, "REQ-4"); + grind.CreateAttributeValue(idAttribute, "REQ-4"); + grind.CreateAttributeValue(statusAttribute, approved); + var grindNode = specification.Children.First().AddChild(grind); + var relation = readContent.CreateSpecRelation(refinesType, grind, brew); + + Assert.Multiple(() => + { + Assert.That(grind.ReqIFContent, Is.SameAs(readContent)); + Assert.That(grindNode.Root, Is.SameAs(specification)); + Assert.That(grindNode.ReqIfContent, Is.SameAs(readContent)); + Assert.That(readContent.SpecRelations, Does.Contain(relation)); + }); + + using var output = Write(document); + + Assert.That(ValidationMessages(output), Is.Empty); + } + } +} diff --git a/ReqIFSharp/AttributeValue/AttributeValueReal.cs b/ReqIFSharp/AttributeValue/AttributeValueReal.cs index 10928da..48a99a6 100644 --- a/ReqIFSharp/AttributeValue/AttributeValueReal.cs +++ b/ReqIFSharp/AttributeValue/AttributeValueReal.cs @@ -10,7 +10,6 @@ namespace ReqIFSharp { using System; - using System.Globalization; using System.Linq; using System.Runtime.Serialization; using System.Threading; @@ -249,7 +248,7 @@ internal override void WriteXml(XmlWriter writer) throw new SerializationException("The Definition property of an AttributeValueReal may not be null"); } - writer.WriteAttributeString("THE-VALUE", this.TheValue.ToString(CultureInfo.InvariantCulture)); + writer.WriteAttributeString("THE-VALUE", XmlConvert.ToString(this.TheValue)); writer.WriteStartElement("DEFINITION"); writer.WriteElementString("ATTRIBUTE-DEFINITION-REAL-REF", this.Definition.Identifier); writer.WriteEndElement(); @@ -276,7 +275,7 @@ internal override async Task WriteXmlAsync(XmlWriter writer, CancellationToken t token.ThrowIfCancellationRequested(); - await writer.WriteAttributeStringAsync(null,"THE-VALUE", null, this.TheValue.ToString(CultureInfo.InvariantCulture)); + await writer.WriteAttributeStringAsync(null,"THE-VALUE", null, XmlConvert.ToString(this.TheValue)); await writer.WriteStartElementAsync(null, "DEFINITION", null); await writer.WriteElementStringAsync(null, "ATTRIBUTE-DEFINITION-REAL-REF", null, this.Definition.Identifier); await writer.WriteEndElementAsync(); diff --git a/ReqIFSharp/AttributeValue/AttributeValueXHTML.cs b/ReqIFSharp/AttributeValue/AttributeValueXHTML.cs index 984fe2e..e3076c9 100644 --- a/ReqIFSharp/AttributeValue/AttributeValueXHTML.cs +++ b/ReqIFSharp/AttributeValue/AttributeValueXHTML.cs @@ -477,7 +477,7 @@ internal override void ReadXml(XmlReader reader) subtree.Read(); break; case "THE-VALUE": - this.TheValue = subtree.ReadInnerXml().Trim(); + this.TheValue = subtree.ReadInnerXml(); if (this.TheValue.Contains("xhtml:object") && this.TheValue.Contains("data")) { @@ -487,7 +487,7 @@ internal override void ReadXml(XmlReader reader) break; case "THE-ORIGINAL-VALUE": - this.TheOriginalValue = subtree.ReadInnerXml().Trim(); + this.TheOriginalValue = subtree.ReadInnerXml(); break; default: subtree.Skip(); @@ -548,7 +548,7 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to await subtree.ReadAsync(); break; case "THE-VALUE": - this.TheValue = (await subtree.ReadInnerXmlAsync()).Trim(); + this.TheValue = await subtree.ReadInnerXmlAsync(); if (this.TheValue.Contains("xhtml:object") && this.TheValue.Contains("data")) { @@ -558,7 +558,7 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to break; case "THE-ORIGINAL-VALUE": - this.TheOriginalValue = (await subtree.ReadInnerXmlAsync()).Trim(); + this.TheOriginalValue = await subtree.ReadInnerXmlAsync(); break; default: await subtree.SkipAsync(); @@ -652,6 +652,198 @@ private void ReadXmlAttributes(XmlReader reader) } } + /// + /// Creates an for an XHTML fragment, in which the xhtml prefix is bound to the + /// XHTML namespace when the fragment does not declare it itself + /// + /// + /// The XHTML fragment + /// + /// + /// False to read the fragment without binding the xhtml prefix, so that a fragment that uses it without + /// declaring it is not well-formed + /// + /// + /// The , which reads asynchronously as well + /// + private static XmlReader CreateFragmentReader(string xhtml, bool bindXhtmlPrefix = true) + { + var settings = new XmlReaderSettings + { + Async = true, + ConformanceLevel = ConformanceLevel.Fragment, + DtdProcessing = DtdProcessing.Prohibit, + XmlResolver = null + }; + + if (!bindXhtmlPrefix) + { + return XmlReader.Create(new StringReader(xhtml), settings); + } + + var nameTable = new NameTable(); + var namespaceManager = new XmlNamespaceManager(nameTable); + namespaceManager.AddNamespace("xhtml", DefaultXmlAttributeFactory.XHTMLNameSpaceUri); + var context = new XmlParserContext(nameTable, namespaceManager, null, XmlSpace.None); + + return XmlReader.Create(new StringReader(xhtml), settings, context); + } + + /// + /// Checks that XHTML written in code can be the value of an : it is well-formed + /// XML with one root element, a div or p in the XHTML namespace, no text outside that + /// element and no XML declaration. The xhtml prefix may be used without declaring it. + /// + /// + /// The content of the root element is not validated against the XHTML schema + /// + /// + /// The XHTML to check + /// + /// + /// The does not meet these rules + /// + internal static void ValidateXhtml(string xhtml) + { + var rootElements = 0; + + try + { + using var reader = CreateFragmentReader(xhtml); + + while (reader.Read()) + { + if (reader.Depth != 0) + { + continue; + } + + switch (reader.NodeType) + { + case XmlNodeType.Element: + rootElements++; + + if (rootElements > 1) + { + throw new ArgumentException("The XHTML must have one root element, but it has more", nameof(xhtml)); + } + + if (reader.NamespaceURI != DefaultXmlAttributeFactory.XHTMLNameSpaceUri || (reader.LocalName != "div" && reader.LocalName != "p")) + { + throw new ArgumentException($"The root element of the XHTML must be a div or p in the namespace {DefaultXmlAttributeFactory.XHTMLNameSpaceUri}, but it is {reader.Name}", nameof(xhtml)); + } + + break; + case XmlNodeType.Text: + case XmlNodeType.CDATA: + case XmlNodeType.EntityReference: + throw new ArgumentException("The XHTML may not have text outside its root element", nameof(xhtml)); + case XmlNodeType.XmlDeclaration: + throw new ArgumentException("The XHTML may not start with an XML declaration: it is written inside the ReqIF document", nameof(xhtml)); + } + } + } + catch (XmlException exception) + { + throw new ArgumentException($"The XHTML is not well-formed: {exception.Message}", nameof(xhtml), exception); + } + + if (rootElements == 0) + { + throw new ArgumentException("The XHTML must have a root element, a div or p in the XHTML namespace", nameof(xhtml)); + } + } + + /// + /// Adds the declaration of the xhtml prefix to the root element of XHTML written in code, when the XHTML + /// uses that prefix without declaring it. The value is then complete on its own, as a value read from a document is, + /// and is written well-formed whatever prefixes the document declares. + /// + /// + /// XHTML that accepts + /// + /// + /// The with xmlns:xhtml added to its root element when needed, otherwise the + /// itself + /// + internal static string DeclareXhtmlPrefix(string xhtml) + { + if (IsWellFormedWithoutXhtmlPrefix(xhtml)) + { + return xhtml; + } + + using var reader = CreateFragmentReader(xhtml); + + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element) + { + var lineInfo = (IXmlLineInfo)reader; + var offset = GetOffset(xhtml, lineInfo.LineNumber, lineInfo.LinePosition) + reader.Name.Length; + + return xhtml.Insert(offset, $" xmlns:xhtml=\"{DefaultXmlAttributeFactory.XHTMLNameSpaceUri}\""); + } + } + + return xhtml; + } + + /// + /// Tells whether an XHTML fragment is well-formed without the xhtml prefix being bound + /// + /// + /// The XHTML fragment + /// + /// + /// True when the fragment declares every prefix it uses + /// + private static bool IsWellFormedWithoutXhtmlPrefix(string xhtml) + { + try + { + using var reader = CreateFragmentReader(xhtml, false); + + while (reader.Read()) + { + } + + return true; + } + catch (XmlException) + { + return false; + } + } + + /// + /// Gets the position in a text of a line number and line position as reported by + /// + /// + /// The text + /// + /// + /// The line number, starting at 1 + /// + /// + /// The position on the line, starting at 1 + /// + /// + /// The index in + /// + private static int GetOffset(string text, int lineNumber, int linePosition) + { + var offset = 0; + + for (var line = 1; line < lineNumber; line++) + { + var lineBreak = text.IndexOfAny(new[] { '\r', '\n' }, offset); + offset = lineBreak + (text[lineBreak] == '\r' && lineBreak + 1 < text.Length && text[lineBreak + 1] == '\n' ? 2 : 1); + } + + return offset + linePosition - 1; + } + /// /// Creates s from provided XHTML /// @@ -661,19 +853,13 @@ private void ReadXmlAttributes(XmlReader reader) /// /// An which may be empty /// - private IEnumerable CreateExternalObjects(string xhtml) + internal IEnumerable CreateExternalObjects(string xhtml) { var sw = Stopwatch.StartNew(); var result = new List(); - var settings = new XmlReaderSettings - { - Async = true, - ConformanceLevel = ConformanceLevel.Fragment - }; - - var xmlReader = XmlReader.Create(new StringReader(xhtml), settings); + using var xmlReader = CreateFragmentReader(xhtml); while (xmlReader.Read()) { if (xmlReader.MoveToContent() == XmlNodeType.Element && xmlReader.LocalName == "object") @@ -727,13 +913,7 @@ private async Task> CreateExternalObjectsAsync(strin var result = new List(); - var settings = new XmlReaderSettings - { - Async = true, - ConformanceLevel = ConformanceLevel.Fragment - }; - - var xmlReader = XmlReader.Create(new StringReader(xhtml), settings); + using var xmlReader = CreateFragmentReader(xhtml); while (xmlReader.Read()) { if (await xmlReader.MoveToContentAsync() == XmlNodeType.Element && xmlReader.LocalName == "object") diff --git a/ReqIFSharp/Datatype/DatatypeDefinitionEnumeration.cs b/ReqIFSharp/Datatype/DatatypeDefinitionEnumeration.cs index cf55a5e..aa0ea1d 100644 --- a/ReqIFSharp/Datatype/DatatypeDefinitionEnumeration.cs +++ b/ReqIFSharp/Datatype/DatatypeDefinitionEnumeration.cs @@ -80,6 +80,44 @@ internal DatatypeDefinitionEnumeration(ReqIFContent reqIfContent, ILoggerFactory [ReqIfProperty(aggregation: AggregationKind.Composite, lowerValue: 0, upperValue: int.MaxValue, isOrdered: false, isReadOnly: false, isDerived: false, isDerivedUnion: false, isUnique: true, defaultValue: null)] public List SpecifiedValues => this.specifiedValues; + /// + /// Creates an , a literal of this enumeration, and adds it to + /// + /// + /// The human-readable name of the literal, mandatory for an enum value + /// + /// + /// The numeric value of the literal + /// + /// + /// Free text that tools may use, for example for a color; empty when null + /// + /// + /// The new , with a new identifier, set to now, + /// set to this enumeration and its set + /// + /// + /// is null + /// + /// + /// is empty + /// + public EnumValue CreateEnumValue(string longName, long key, string otherContent = null) + { + ValidateLongName(longName); + + var enumValue = new EnumValue(this, this.loggerFactory); + + _ = new EmbeddedValue(enumValue, this.loggerFactory) + { + Key = key, + OtherContent = otherContent ?? string.Empty + }; + + enumValue.InitializeCreated(longName); + return enumValue; + } + /// /// Generates a object from its XML representation. /// diff --git a/ReqIFSharp/Identifiable.cs b/ReqIFSharp/Identifiable.cs index af4b4e7..cb5a802 100644 --- a/ReqIFSharp/Identifiable.cs +++ b/ReqIFSharp/Identifiable.cs @@ -102,6 +102,73 @@ protected Identifiable(ILoggerFactory loggerFactory) [ReqIfProperty(aggregation: AggregationKind.Composite, lowerValue: 0, upperValue: 1, isOrdered: false, isReadOnly: false, isDerived: false, isDerivedUnion: false, isUnique: true, defaultValue: null)] public AlternativeId AlternativeId { get; set; } + /// + /// Creates a new identifier that is a valid xsd:ID: an underscore followed by a new + /// + /// + /// An xsd:ID must start with a letter or an underscore. A bare often starts with a + /// digit, which makes a written document invalid against the ReqIF schema. + /// + /// + /// a new, unique identifier + /// + public static string CreateIdentifier() + { + return "_" + Guid.NewGuid().ToString(); + } + + /// + /// Sets to the current UTC date and time + /// + /// + /// Call this method after changing the element, for example after changing one of the attribute values of a + /// . is not updated automatically. + /// + public void MarkAsChanged() + { + this.LastChange = DateTime.UtcNow; + } + + /// + /// Gives a newly created element a new , sets its to the current + /// UTC date and time and sets its + /// + /// + /// The human-readable name, or null + /// + internal void InitializeCreated(string longName) + { + this.Identifier = CreateIdentifier(); + this.LongName = longName; + this.MarkAsChanged(); + } + + /// + /// Checks the long name of an element for which the ReqIF specification makes it mandatory: attribute + /// definitions, enum values and relation groups + /// + /// + /// The long name to check + /// + /// + /// is null + /// + /// + /// is empty + /// + internal static void ValidateLongName(string longName) + { + if (longName == null) + { + throw new ArgumentNullException(nameof(longName), "The ReqIF specification makes the long name mandatory for this element"); + } + + if (longName.Length == 0) + { + throw new ArgumentException("The ReqIF specification makes the long name mandatory for this element; it may not be empty", nameof(longName)); + } + } + /// /// Generates a object from its XML representation. /// diff --git a/ReqIFSharp/ReqIF.cs b/ReqIFSharp/ReqIF.cs index 0647ab1..327ec03 100644 --- a/ReqIFSharp/ReqIF.cs +++ b/ReqIFSharp/ReqIF.cs @@ -9,6 +9,7 @@ namespace ReqIFSharp { + using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -87,6 +88,46 @@ internal ReqIF(ILoggerFactory loggerFactory) [ReqIfProperty(aggregation: AggregationKind.None, lowerValue: 0, upperValue: 1, isOrdered: false, isReadOnly: false, isDerived: false, isDerivedUnion: false, isUnique: true, defaultValue: null)] public string Lang { get; set; } + /// + /// Creates an empty document with a header and a content. Build the content with the + /// Create... methods of . + /// + /// + /// The title of the document, or null; a null title is written as an empty TITLE element + /// + /// + /// The (injected) used to set up logging, or null + /// + /// + /// The new . Its has a new identifier, the , + /// set to now, "1.0", and + /// "ReqIFSharp" as and ; change these + /// as needed. The of the header and the + /// of the are set to the new document. + /// + public static ReqIF Create(string title, ILoggerFactory loggerFactory = null) + { + var reqif = new ReqIF(loggerFactory); + + reqif.TheHeader = new ReqIFHeader(loggerFactory) + { + Identifier = Identifiable.CreateIdentifier(), + Title = title, + CreationTime = DateTime.UtcNow, + ReqIFVersion = "1.0", + ReqIFToolId = "ReqIFSharp", + SourceToolId = "ReqIFSharp", + DocumentRoot = reqif + }; + + reqif.CoreContent = new ReqIFContent(loggerFactory) + { + DocumentRoot = reqif + }; + + return reqif; + } + /// /// Generates a object from its XML representation. /// diff --git a/ReqIFSharp/ReqIFContent.cs b/ReqIFSharp/ReqIFContent.cs index e0978ae..8944495 100644 --- a/ReqIFSharp/ReqIFContent.cs +++ b/ReqIFSharp/ReqIFContent.cs @@ -126,6 +126,481 @@ internal ReqIFContent(ILoggerFactory loggerFactory) /// public ReqIF DocumentRoot { get; set; } + /// + /// Checks that an element passed to a create method does not belong to another , so + /// that the new element only refers to elements of its own document. An element that belongs to no content is + /// accepted. + /// + /// + /// The the new element is created in + /// + /// + /// The of the element that is passed + /// + /// + /// The element that is passed + /// + /// + /// The name of the parameter the element is passed in + /// + /// + /// The element belongs to another + /// + internal static void ValidateSameContent(ReqIFContent content, ReqIFContent elementContent, Identifiable element, string paramName) + { + if (content != null && elementContent != null && !ReferenceEquals(content, elementContent)) + { + throw new ArgumentException($"{element.Identifier} belongs to another ReqIF document and cannot be referenced from this one", paramName); + } + } + + /// + /// Creates a and adds it to + /// + /// + /// The human-readable name, or null + /// + /// + /// The new , with a new identifier, + /// set to now and set to this content + /// + public DatatypeDefinitionBoolean CreateDatatypeDefinitionBoolean(string longName) + { + var datatypeDefinition = new DatatypeDefinitionBoolean(this, this.loggerFactory); + datatypeDefinition.InitializeCreated(longName); + return datatypeDefinition; + } + + /// + /// Creates a and adds it to + /// + /// + /// The human-readable name, or null + /// + /// + /// The new , with a new identifier, + /// set to now and set to this content + /// + public DatatypeDefinitionDate CreateDatatypeDefinitionDate(string longName) + { + var datatypeDefinition = new DatatypeDefinitionDate(this, this.loggerFactory); + datatypeDefinition.InitializeCreated(longName); + return datatypeDefinition; + } + + /// + /// Creates a and adds it to . Add its literals + /// with . + /// + /// + /// The human-readable name, or null + /// + /// + /// The new , with a new identifier, + /// set to now and set to this content + /// + public DatatypeDefinitionEnumeration CreateDatatypeDefinitionEnumeration(string longName) + { + var datatypeDefinition = new DatatypeDefinitionEnumeration(this, this.loggerFactory); + datatypeDefinition.InitializeCreated(longName); + return datatypeDefinition; + } + + /// + /// Creates a and adds it to + /// + /// + /// The human-readable name, or null + /// + /// + /// The smallest allowed value + /// + /// + /// The largest allowed value + /// + /// + /// The new , with a new identifier, + /// set to now and set to this content + /// + /// + /// is larger than + /// + public DatatypeDefinitionInteger CreateDatatypeDefinitionInteger(string longName, long min, long max) + { + if (min > max) + { + throw new ArgumentException($"The min {min} is larger than the max {max}", nameof(min)); + } + + var datatypeDefinition = new DatatypeDefinitionInteger(this, this.loggerFactory) + { + Min = min, + Max = max + }; + + datatypeDefinition.InitializeCreated(longName); + return datatypeDefinition; + } + + /// + /// Creates a and adds it to + /// + /// + /// The human-readable name, or null + /// + /// + /// The number of digits after the decimal point + /// + /// + /// The smallest allowed value + /// + /// + /// The largest allowed value + /// + /// + /// The new , with a new identifier, + /// set to now and set to this content + /// + /// + /// is negative, or is larger than + /// + public DatatypeDefinitionReal CreateDatatypeDefinitionReal(string longName, long accuracy, double min, double max) + { + if (accuracy < 0) + { + throw new ArgumentException($"The accuracy {accuracy} is negative", nameof(accuracy)); + } + + if (min > max) + { + throw new ArgumentException($"The min {min} is larger than the max {max}", nameof(min)); + } + + var datatypeDefinition = new DatatypeDefinitionReal(this, this.loggerFactory) + { + Accuracy = accuracy, + Min = min, + Max = max + }; + + datatypeDefinition.InitializeCreated(longName); + return datatypeDefinition; + } + + /// + /// Creates a and adds it to + /// + /// + /// The human-readable name, or null + /// + /// + /// The largest allowed number of characters + /// + /// + /// The new , with a new identifier, + /// set to now and set to this content + /// + /// + /// is negative + /// + public DatatypeDefinitionString CreateDatatypeDefinitionString(string longName, long maxLength) + { + if (maxLength < 0) + { + throw new ArgumentException($"The max length {maxLength} is negative", nameof(maxLength)); + } + + var datatypeDefinition = new DatatypeDefinitionString(this, this.loggerFactory) + { + MaxLength = maxLength + }; + + datatypeDefinition.InitializeCreated(longName); + return datatypeDefinition; + } + + /// + /// Creates a and adds it to + /// + /// + /// The human-readable name, or null + /// + /// + /// The new , with a new identifier, + /// set to now and set to this content + /// + public DatatypeDefinitionXHTML CreateDatatypeDefinitionXHTML(string longName) + { + var datatypeDefinition = new DatatypeDefinitionXHTML(this, this.loggerFactory); + datatypeDefinition.InitializeCreated(longName); + return datatypeDefinition; + } + + /// + /// Creates a and adds it to . Add its attributes with the + /// overloads. + /// + /// + /// The human-readable name, or null + /// + /// + /// The new , with a new identifier, + /// set to now and set to this content + /// + public SpecObjectType CreateSpecObjectType(string longName) + { + var specType = new SpecObjectType(this, this.loggerFactory); + specType.InitializeCreated(longName); + return specType; + } + + /// + /// Creates a and adds it to + /// + /// + /// The human-readable name, or null + /// + /// + /// The new , with a new identifier, + /// set to now and set to this content + /// + public SpecificationType CreateSpecificationType(string longName) + { + var specType = new SpecificationType(this, this.loggerFactory); + specType.InitializeCreated(longName); + return specType; + } + + /// + /// Creates a and adds it to + /// + /// + /// The human-readable name, or null + /// + /// + /// The new , with a new identifier, + /// set to now and set to this content + /// + public SpecRelationType CreateSpecRelationType(string longName) + { + var specType = new SpecRelationType(this, this.loggerFactory); + specType.InitializeCreated(longName); + return specType; + } + + /// + /// Creates a and adds it to + /// + /// + /// The human-readable name, or null + /// + /// + /// The new , with a new identifier, + /// set to now and set to this content + /// + public RelationGroupType CreateRelationGroupType(string longName) + { + var specType = new RelationGroupType(this, this.loggerFactory); + specType.InitializeCreated(longName); + return specType; + } + + /// + /// Creates a of the and adds it to . + /// Add its values with the + /// overloads. + /// + /// + /// The of the new + /// + /// + /// The human-readable name, or null + /// + /// + /// The new , with a new identifier, + /// set to now and set to this content + /// + /// + /// is null + /// + /// + /// The belongs to another + /// + public SpecObject CreateSpecObject(SpecObjectType type, string longName = null) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + ValidateSameContent(this, type.ReqIFContent, type, nameof(type)); + + var specObject = new SpecObject(this, this.loggerFactory) + { + Type = type + }; + + specObject.InitializeCreated(longName); + return specObject; + } + + /// + /// Creates a of the and adds it to . + /// Add its spec objects with . + /// + /// + /// The of the new + /// + /// + /// The human-readable name, or null + /// + /// + /// The new , with a new identifier, + /// set to now and set to this content + /// + /// + /// is null + /// + /// + /// The belongs to another + /// + public Specification CreateSpecification(SpecificationType type, string longName = null) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + ValidateSameContent(this, type.ReqIFContent, type, nameof(type)); + + var specification = new Specification(this, this.loggerFactory) + { + Type = type + }; + + specification.InitializeCreated(longName); + return specification; + } + + /// + /// Creates a of the from to + /// and adds it to + /// + /// + /// The of the new + /// + /// + /// The the relation starts from + /// + /// + /// The the relation points to + /// + /// + /// The human-readable name, or null + /// + /// + /// The new , with a new identifier, + /// set to now and set to this content + /// + /// + /// , or is null + /// + /// + /// The , or belongs to another + /// + public SpecRelation CreateSpecRelation(SpecRelationType type, SpecObject source, SpecObject target, string longName = null) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + ValidateSameContent(this, type.ReqIFContent, type, nameof(type)); + ValidateSameContent(this, source.ReqIFContent, source, nameof(source)); + ValidateSameContent(this, target.ReqIFContent, target, nameof(target)); + + var specRelation = new SpecRelation(this, this.loggerFactory) + { + Type = type, + Source = source, + Target = target + }; + + specRelation.InitializeCreated(longName); + return specRelation; + } + + /// + /// Creates a of the between two specifications and adds it to + /// . Add its relations to . + /// + /// + /// The of the new + /// + /// + /// The the relations of the group start from + /// + /// + /// The the relations of the group point to + /// + /// + /// The human-readable name, mandatory for a relation group + /// + /// + /// The new , with a new identifier, + /// set to now and set to this content + /// + /// + /// , , or + /// is null + /// + /// + /// is empty, or , or + /// belongs to another + /// + public RelationGroup CreateRelationGroup(RelationGroupType type, Specification sourceSpecification, Specification targetSpecification, string longName) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + if (sourceSpecification == null) + { + throw new ArgumentNullException(nameof(sourceSpecification)); + } + + if (targetSpecification == null) + { + throw new ArgumentNullException(nameof(targetSpecification)); + } + + Identifiable.ValidateLongName(longName); + ValidateSameContent(this, type.ReqIFContent, type, nameof(type)); + ValidateSameContent(this, sourceSpecification.ReqIFContent, sourceSpecification, nameof(sourceSpecification)); + ValidateSameContent(this, targetSpecification.ReqIFContent, targetSpecification, nameof(targetSpecification)); + + var relationGroup = new RelationGroup(this, this.loggerFactory) + { + Type = type, + SourceSpecification = sourceSpecification, + TargetSpecification = targetSpecification + }; + + relationGroup.InitializeCreated(longName); + return relationGroup; + } + /// /// Generates a object from its XML representation. /// diff --git a/ReqIFSharp/SpecElementWithAttributes/RelationGroup.cs b/ReqIFSharp/SpecElementWithAttributes/RelationGroup.cs index a8e882a..08efb6b 100644 --- a/ReqIFSharp/SpecElementWithAttributes/RelationGroup.cs +++ b/ReqIFSharp/SpecElementWithAttributes/RelationGroup.cs @@ -25,6 +25,10 @@ namespace ReqIFSharp /// /// /// The class represents a group of relations between a source and a target . + /// The ReqIF metamodel lets a relation group own attribute values, and they are read and written. The ReqIF XML schema + /// (reqif.xsd) has no VALUES element on RELATION-GROUP, so a document with such values is not valid against the schema + /// and other tools may reject it. The ProSTEP iViP ReqIF Implementation Guide calls this a bug in the schema and + /// recommends giving a no attribute definitions. /// /// /// a instance may represent a set of relations between a customer requirements and a system requirements . diff --git a/ReqIFSharp/SpecElementWithAttributes/SpecElementWithAttributes.cs b/ReqIFSharp/SpecElementWithAttributes/SpecElementWithAttributes.cs index f910274..8771215 100644 --- a/ReqIFSharp/SpecElementWithAttributes/SpecElementWithAttributes.cs +++ b/ReqIFSharp/SpecElementWithAttributes/SpecElementWithAttributes.cs @@ -9,7 +9,10 @@ namespace ReqIFSharp { + using System; using System.Collections.Generic; + using System.Globalization; + using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Xml; @@ -113,6 +116,363 @@ public SpecType SpecType /// protected abstract void SetSpecType(SpecType specType); + /// + /// Creates an for the and adds it to + /// + /// + /// The the value is for. It must be an attribute of the + /// of this element. + /// + /// + /// The value + /// + /// + /// The new , with set to this element + /// + /// + /// is null + /// + /// + /// This element has no + /// + /// + /// The has no datatype or is not an attribute of the of this element, or this + /// element already has a value for it + /// + public AttributeValueBoolean CreateAttributeValue(AttributeDefinitionBoolean definition, bool value) + { + this.ValidateValueCanBeCreated(definition); + + return new AttributeValueBoolean(this, this.loggerFactory) + { + Definition = definition, + TheValue = value + }; + } + + /// + /// Creates an for the and adds it to + /// + /// + /// The the value is for. It must be an attribute of the + /// of this element. + /// + /// + /// The value + /// + /// + /// The new , with set to this element + /// + /// + /// is null + /// + /// + /// This element has no + /// + /// + /// The has no datatype or is not an attribute of the of this element, or this + /// element already has a value for it + /// + public AttributeValueDate CreateAttributeValue(AttributeDefinitionDate definition, DateTime value) + { + this.ValidateValueCanBeCreated(definition); + + return new AttributeValueDate(this, this.loggerFactory) + { + Definition = definition, + TheValue = value + }; + } + + /// + /// Creates an for the and adds it to + /// + /// + /// The the value is for. It must be an attribute of the + /// of this element. + /// + /// + /// The value + /// + /// + /// The new , with set to this element + /// + /// + /// is null + /// + /// + /// This element has no + /// + /// + /// The has no datatype or is not an attribute of the of this element, this + /// element already has a value for it, or the is outside the and + /// of its datatype + /// + public AttributeValueInteger CreateAttributeValue(AttributeDefinitionInteger definition, long value) + { + this.ValidateValueCanBeCreated(definition); + + var datatype = definition.Type; + + if (value < datatype.Min || value > datatype.Max) + { + throw new ArgumentException($"The value {value} is outside the range {datatype.Min} to {datatype.Max} of the datatype {datatype.Identifier}", nameof(value)); + } + + return new AttributeValueInteger(this, this.loggerFactory) + { + Definition = definition, + TheValue = value + }; + } + + /// + /// Creates an for the and adds it to + /// + /// + /// The the value is for. It must be an attribute of the + /// of this element. + /// + /// + /// The value + /// + /// + /// The new , with set to this element + /// + /// + /// is null + /// + /// + /// This element has no + /// + /// + /// The has no datatype or is not an attribute of the of this element, this + /// element already has a value for it, or the is outside the and + /// of its datatype, or is not a number + /// + public AttributeValueReal CreateAttributeValue(AttributeDefinitionReal definition, double value) + { + this.ValidateValueCanBeCreated(definition); + + var datatype = definition.Type; + + if (!(value >= datatype.Min && value <= datatype.Max)) + { + throw new ArgumentException($"The value {value.ToString(CultureInfo.InvariantCulture)} is outside the range {datatype.Min.ToString(CultureInfo.InvariantCulture)} to {datatype.Max.ToString(CultureInfo.InvariantCulture)} of the datatype {datatype.Identifier}", nameof(value)); + } + + return new AttributeValueReal(this, this.loggerFactory) + { + Definition = definition, + TheValue = value + }; + } + + /// + /// Creates an for the and adds it to + /// + /// + /// The the value is for. It must be an attribute of the + /// of this element. + /// + /// + /// The value; use an empty string for no text, as the ReqIF schema requires a value + /// + /// + /// The new , with set to this element + /// + /// + /// or is null + /// + /// + /// This element has no + /// + /// + /// The has no datatype or is not an attribute of the of this element, this + /// element already has a value for it, or the is longer than the + /// of its datatype + /// + public AttributeValueString CreateAttributeValue(AttributeDefinitionString definition, string value) + { + this.ValidateValueCanBeCreated(definition); + + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + var datatype = definition.Type; + + if (value.Length > datatype.MaxLength) + { + throw new ArgumentException($"The value is {value.Length} characters long, more than the max length {datatype.MaxLength} of the datatype {datatype.Identifier}", nameof(value)); + } + + return new AttributeValueString(this, this.loggerFactory) + { + Definition = definition, + TheValue = value + }; + } + + /// + /// Creates an for the and adds it to + /// + /// + /// The the value is for. It must be an attribute of the + /// of this element. + /// + /// + /// The XHTML content: well-formed XML with one root element, an xhtml:div or xhtml:p, and no text + /// outside it. The xhtml prefix may be used without declaring it. The content of the root element is not + /// validated against the XHTML schema. + /// + /// + /// The new , with set to this element and an + /// in for each xhtml:object. + /// When the uses the xhtml prefix without declaring it, its + /// has xmlns:xhtml added to the root element, so the value is + /// complete on its own, like a value read from a document, and is written well-formed in any document. + /// + /// + /// or is null + /// + /// + /// This element has no + /// + /// + /// The has no datatype or is not an attribute of the of this element, this + /// element already has a value for it, or the does not meet the rules above + /// + public AttributeValueXHTML CreateAttributeValue(AttributeDefinitionXHTML definition, string xhtml) + { + this.ValidateValueCanBeCreated(definition); + + if (xhtml == null) + { + throw new ArgumentNullException(nameof(xhtml)); + } + + AttributeValueXHTML.ValidateXhtml(xhtml); + var theValue = AttributeValueXHTML.DeclareXhtmlPrefix(xhtml); + + var attributeValue = new AttributeValueXHTML(this, this.loggerFactory) + { + Definition = definition, + TheValue = theValue + }; + + attributeValue.ExternalObjects.AddRange(attributeValue.CreateExternalObjects(theValue)); + return attributeValue; + } + + /// + /// Creates an for the and adds it to + /// + /// + /// The the value is for. It must be an attribute of the + /// of this element. + /// + /// + /// The selected literals. They must be literals of the enumeration that is the type of the + /// ; select more than one only when the definition is multi-valued. + /// + /// + /// The new , with set to this element + /// + /// + /// or is null + /// + /// + /// This element has no + /// + /// + /// The has no datatype or is not an attribute of the of this element, this element + /// already has a value for it, a selected literal is null or not a literal of its enumeration, or more than one + /// literal is selected for a definition that is not multi-valued + /// + public AttributeValueEnumeration CreateAttributeValue(AttributeDefinitionEnumeration definition, params EnumValue[] values) + { + this.ValidateValueCanBeCreated(definition); + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + if (values.Length > 1 && !definition.IsMultiValued) + { + throw new ArgumentException($"The attribute definition {definition.Identifier} is not multi-valued, but {values.Length} values are selected", nameof(values)); + } + + foreach (var enumValue in values) + { + if (enumValue == null) + { + throw new ArgumentException("A selected value is null", nameof(values)); + } + + if (!definition.Type.SpecifiedValues.Contains(enumValue)) + { + throw new ArgumentException($"The enum value {enumValue.Identifier} is not a literal of the enumeration {definition.Type.Identifier}", nameof(values)); + } + } + + var attributeValue = new AttributeValueEnumeration(this, this.loggerFactory) + { + Definition = definition + }; + + attributeValue.Values.AddRange(values); + return attributeValue; + } + + /// + /// Checks that a value for the can be added to this element + /// + /// + /// The of the value to create + /// + /// + /// is null + /// + /// + /// This element has no + /// + /// + /// The has no datatype or is not an attribute of the of this element, or this + /// element already has a value for it + /// + private void ValidateValueCanBeCreated(AttributeDefinition definition) + { + if (definition == null) + { + throw new ArgumentNullException(nameof(definition)); + } + + var specType = this.SpecType; + + if (specType == null) + { + throw new InvalidOperationException($"{this.Identifier} has no spec type, so it cannot have attribute values"); + } + + if (definition.DatatypeDefinition == null) + { + throw new ArgumentException($"The attribute definition {definition.Identifier} has no datatype", nameof(definition)); + } + + if (!specType.SpecAttributes.Contains(definition)) + { + throw new ArgumentException($"The attribute definition {definition.Identifier} is not an attribute of the spec type {specType.Identifier} of {this.Identifier}", nameof(definition)); + } + + if (this.Values.Any(x => x.AttributeDefinition == definition)) + { + throw new ArgumentException($"{this.Identifier} already has a value for the attribute definition {definition.Identifier}", nameof(definition)); + } + } + /// /// Generates a object from its XML representation. /// diff --git a/ReqIFSharp/SpecElementWithAttributes/SpecHierarchy.cs b/ReqIFSharp/SpecElementWithAttributes/SpecHierarchy.cs index 1a6c593..9b97657 100644 --- a/ReqIFSharp/SpecElementWithAttributes/SpecHierarchy.cs +++ b/ReqIFSharp/SpecElementWithAttributes/SpecHierarchy.cs @@ -157,6 +157,41 @@ internal SpecHierarchy(SpecHierarchy container, Specification root, ReqIFContent /// public SpecHierarchy Container { get; set; } + /// + /// Adds the as a child of this spec hierarchy + /// + /// + /// The to add + /// + /// + /// The new that points to the and is added to + /// , with a new identifier, set to now and its + /// , and set + /// + /// + /// is null + /// + /// + /// belongs to another + /// + public SpecHierarchy AddChild(SpecObject specObject) + { + if (specObject == null) + { + throw new ArgumentNullException(nameof(specObject)); + } + + ReqIFContent.ValidateSameContent(this.ReqIfContent, specObject.ReqIFContent, specObject, nameof(specObject)); + + var specHierarchy = new SpecHierarchy(this, this.Root, this.ReqIfContent, this.loggerFactory) + { + Object = specObject + }; + + specHierarchy.InitializeCreated(null); + return specHierarchy; + } + /// /// Generates a object from its XML representation. /// diff --git a/ReqIFSharp/SpecElementWithAttributes/Specification.cs b/ReqIFSharp/SpecElementWithAttributes/Specification.cs index 8ddf192..3507603 100644 --- a/ReqIFSharp/SpecElementWithAttributes/Specification.cs +++ b/ReqIFSharp/SpecElementWithAttributes/Specification.cs @@ -86,6 +86,41 @@ internal Specification(ReqIFContent reqIfContent, ILoggerFactory loggerFactory) [ReqIfProperty(aggregation: AggregationKind.None, lowerValue: 1, upperValue: 1, isOrdered: false, isReadOnly: false, isDerived: false, isDerivedUnion: false, isUnique: true, defaultValue: null)] public SpecificationType Type { get; set; } + /// + /// Adds the to the top level of this specification + /// + /// + /// The to add + /// + /// + /// The new that points to the and is added to + /// , with a new identifier, set to now and its + /// and set + /// + /// + /// is null + /// + /// + /// belongs to another + /// + public SpecHierarchy AddChild(SpecObject specObject) + { + if (specObject == null) + { + throw new ArgumentNullException(nameof(specObject)); + } + + ReqIFContent.ValidateSameContent(this.ReqIFContent, specObject.ReqIFContent, specObject, nameof(specObject)); + + var specHierarchy = new SpecHierarchy(this, this.ReqIFContent, this.loggerFactory) + { + Object = specObject + }; + + specHierarchy.InitializeCreated(null); + return specHierarchy; + } + /// /// Converts a object into its XML representation. /// diff --git a/ReqIFSharp/SpecType/SpecType.cs b/ReqIFSharp/SpecType/SpecType.cs index 361a448..98fc087 100644 --- a/ReqIFSharp/SpecType/SpecType.cs +++ b/ReqIFSharp/SpecType/SpecType.cs @@ -89,6 +89,276 @@ protected SpecType(ReqIFContent reqIfContent, ILoggerFactory loggerFactory) /// public ReqIFContent ReqIFContent { get; set; } + /// + /// Creates an of the and adds it to + /// + /// + /// The of the new attribute + /// + /// + /// The human-readable name, mandatory for an attribute definition + /// + /// + /// The new , with a new identifier, set to now + /// and set to this spec type + /// + /// + /// or is null + /// + /// + /// is empty, or belongs to another + /// + public AttributeDefinitionBoolean CreateAttributeDefinition(DatatypeDefinitionBoolean type, string longName) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + ValidateLongName(longName); + ReqIFContent.ValidateSameContent(this.ReqIFContent, type.ReqIFContent, type, nameof(type)); + + var attributeDefinition = new AttributeDefinitionBoolean(this, this.loggerFactory) + { + Type = type + }; + + attributeDefinition.InitializeCreated(longName); + return attributeDefinition; + } + + /// + /// Creates an of the and adds it to + /// + /// + /// The of the new attribute + /// + /// + /// The human-readable name, mandatory for an attribute definition + /// + /// + /// The new , with a new identifier, set to now + /// and set to this spec type + /// + /// + /// or is null + /// + /// + /// is empty, or belongs to another + /// + public AttributeDefinitionDate CreateAttributeDefinition(DatatypeDefinitionDate type, string longName) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + ValidateLongName(longName); + ReqIFContent.ValidateSameContent(this.ReqIFContent, type.ReqIFContent, type, nameof(type)); + + var attributeDefinition = new AttributeDefinitionDate(this, this.loggerFactory) + { + Type = type + }; + + attributeDefinition.InitializeCreated(longName); + return attributeDefinition; + } + + /// + /// Creates an of the and adds it to + /// + /// + /// The of the new attribute + /// + /// + /// The human-readable name, mandatory for an attribute definition + /// + /// + /// A value indicating whether more than one literal of the enumeration can be selected + /// + /// + /// The new , with a new identifier, set to now + /// and set to this spec type + /// + /// + /// or is null + /// + /// + /// is empty, or belongs to another + /// + public AttributeDefinitionEnumeration CreateAttributeDefinition(DatatypeDefinitionEnumeration type, string longName, bool isMultiValued = false) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + ValidateLongName(longName); + ReqIFContent.ValidateSameContent(this.ReqIFContent, type.ReqIFContent, type, nameof(type)); + + var attributeDefinition = new AttributeDefinitionEnumeration(this, this.loggerFactory) + { + Type = type, + IsMultiValued = isMultiValued + }; + + attributeDefinition.InitializeCreated(longName); + return attributeDefinition; + } + + /// + /// Creates an of the and adds it to + /// + /// + /// The of the new attribute + /// + /// + /// The human-readable name, mandatory for an attribute definition + /// + /// + /// The new , with a new identifier, set to now + /// and set to this spec type + /// + /// + /// or is null + /// + /// + /// is empty, or belongs to another + /// + public AttributeDefinitionInteger CreateAttributeDefinition(DatatypeDefinitionInteger type, string longName) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + ValidateLongName(longName); + ReqIFContent.ValidateSameContent(this.ReqIFContent, type.ReqIFContent, type, nameof(type)); + + var attributeDefinition = new AttributeDefinitionInteger(this, this.loggerFactory) + { + Type = type + }; + + attributeDefinition.InitializeCreated(longName); + return attributeDefinition; + } + + /// + /// Creates an of the and adds it to + /// + /// + /// The of the new attribute + /// + /// + /// The human-readable name, mandatory for an attribute definition + /// + /// + /// The new , with a new identifier, set to now + /// and set to this spec type + /// + /// + /// or is null + /// + /// + /// is empty, or belongs to another + /// + public AttributeDefinitionReal CreateAttributeDefinition(DatatypeDefinitionReal type, string longName) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + ValidateLongName(longName); + ReqIFContent.ValidateSameContent(this.ReqIFContent, type.ReqIFContent, type, nameof(type)); + + var attributeDefinition = new AttributeDefinitionReal(this, this.loggerFactory) + { + Type = type + }; + + attributeDefinition.InitializeCreated(longName); + return attributeDefinition; + } + + /// + /// Creates an of the and adds it to + /// + /// + /// The of the new attribute + /// + /// + /// The human-readable name, mandatory for an attribute definition + /// + /// + /// The new , with a new identifier, set to now + /// and set to this spec type + /// + /// + /// or is null + /// + /// + /// is empty, or belongs to another + /// + public AttributeDefinitionString CreateAttributeDefinition(DatatypeDefinitionString type, string longName) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + ValidateLongName(longName); + ReqIFContent.ValidateSameContent(this.ReqIFContent, type.ReqIFContent, type, nameof(type)); + + var attributeDefinition = new AttributeDefinitionString(this, this.loggerFactory) + { + Type = type + }; + + attributeDefinition.InitializeCreated(longName); + return attributeDefinition; + } + + /// + /// Creates an of the and adds it to + /// + /// + /// The of the new attribute + /// + /// + /// The human-readable name, mandatory for an attribute definition + /// + /// + /// The new , with a new identifier, set to now + /// and set to this spec type + /// + /// + /// or is null + /// + /// + /// is empty, or belongs to another + /// + public AttributeDefinitionXHTML CreateAttributeDefinition(DatatypeDefinitionXHTML type, string longName) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + ValidateLongName(longName); + ReqIFContent.ValidateSameContent(this.ReqIFContent, type.ReqIFContent, type, nameof(type)); + + var attributeDefinition = new AttributeDefinitionXHTML(this, this.loggerFactory) + { + Type = type + }; + + attributeDefinition.InitializeCreated(longName); + return attributeDefinition; + } + /// /// Generates a object from its XML representation. ///