diff --git a/CLAUDE.md b/CLAUDE.md index a1422b75..832cf78e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -194,28 +194,50 @@ Direct pushes to `development` or `master` are forbidden. All work lives on a fe - Prefer C# property patterns ('x is IType { Prop: value }') over declared-variable-plus-predicate form ('x is IType name && name.Prop == value') when the narrowed variable is only consulted once; the property-pattern form is more concise and intent-revealing - **Always use C# auto-properties** (`public T Foo { get; private set; }`, `public T Foo { get; init; }`, `public T Foo { get; }`) — NEVER pair a private backing field with an expression-bodied or full-getter property when there is no non-trivial logic (validation, normalisation, lazy init, event firing). Mere storage is never a justification for a backing field; the compiler collapses auto-properties to the same IL. - **For test fixtures: default to ONE `[Test]` method per class / method-under-test** packing every scenario (happy path, edge cases, null guards, alternate inputs) into multiple `Assert.That` calls inside that one test — per `TESTING.md` §2. Do NOT write one `[Test]` per scenario when the setup is shared; that produces a bloated test list and duplicated arrange boilerplate. Split into separate `[Test]` methods only when each scenario has a genuinely distinct, complex setup. +- **Prefer method-group syntax over lambda when the lambda merely invokes a no-arg method.** Both in production code and in tests, write `Assert.That(subject.ComputeFoo, Throws.TypeOf())` rather than `Assert.That(() => subject.ComputeFoo(), Throws.TypeOf())`; pass `subject.Handle` rather than `x => subject.Handle(x)` when wiring up an event handler; pass `string.IsNullOrWhiteSpace` rather than `s => string.IsNullOrWhiteSpace(s)` to a LINQ predicate. The method group is more concise, allocates no closure, and reads as the action itself rather than as a delegate that calls the action. Fall back to a lambda only when (a) the lambda's body does more than the bare call (transforms args, captures locals, adds null-handling), (b) the target method is overloaded and the compiler can't infer which overload to bind, or (c) the call needs explicit type arguments the method-group form cannot supply. - Surround every braced block (`if`, `else if`, `while`, `for`, `foreach`, `switch`, `using`, `try`/`catch`/`finally`, `lock`, `do…while`, anonymous `{ }`) with a blank line on both sides — the rule does NOT apply at the very start/end of a method body, nor between a `}` and a continuation keyword (`else`, `catch`, `finally`, `while` of `do…while`) that belongs to the same control flow - When invoking an operation or derived property on a POCO from inside an extension method, call the POCO's instance member (e.g. `subject.IsDistinguishableFrom(other)`, `subject.qualifiedName`), NOT the static `ComputeXxxOperation` / `ComputeXxx` extension method. Virtual dispatch on the POCO honors operation/property REDEFINITION in subclass POCOs; calling the static extension directly bypasses dispatch and silently skips overrides. The static-extension form is reserved EXCLUSIVELY for the C# translation of OCL `self.oclAsType(SuperType).method()` — an explicit upcast that mandates targeting the SuperType's body (e.g. `Usage::namingFeature()` → `FeatureExtensions.ComputeNamingFeatureOperation(usage)`; `OwningMembership::path()` → `RelationshipExtensions.ComputeRedefinedPathOperation(owningMembership)`) -- **`IRelationship.OwnedRelatedElement` and `IElement.OwnedRelationship` storage collections are `[0..*]` — NEVER cardinality-limited.** The [1..1] / [0..1] multiplicities that appear in the metamodel apply to *derived* / *redefined* properties (e.g. `OwningMembership::ownedMemberElement`, `FeatureMembership::ownedMemberFeature`, `SubjectMembership::ownedSubjectParameter`), NOT to the underlying storage. When implementing such a derivation, **project from the collection — do not assume positional indexing**. For the common case of a `[1..1]` derived property, use the canonical shared helper `ElementExtensions.RequireSingleOfType(this IReadOnlyList, string)` from `SysML2.NET/Extensions/ElementExtensions.cs` — it does a zero-allocation index-based scan, early-exits on the second match, and throws `IncompleteModelException` with distinct "missing" vs "more than one" diagnostics: +- **`IRelationship.OwnedRelatedElement` and `IElement.OwnedRelationship` storage collections are `[0..*]` — NEVER cardinality-limited.** The [1..1] / [0..1] multiplicities that appear in the metamodel apply to *derived* / *redefined* properties (e.g. `OwningMembership::ownedMemberElement`, `FeatureMembership::ownedMemberFeature`, `SubjectMembership::ownedSubjectParameter`), NOT to the underlying storage. When implementing such a derivation, **project from the collection — do not assume positional indexing**. Two canonical shared helpers in `SysML2.NET/Extensions/ElementExtensions.cs` cover the common cases (all early-exit on the second match, no full materialisation), each with two overloads: + + - **`SingleStrict`** — `[1..1]` semantics. Empty → `throw IncompleteModelException` (lower-bound violation, missing required). Single → return. 2+ → `throw MultiplicityViolationException` (upper-bound violation). + - `SingleStrict(this IEnumerable, string)` — homogeneous: source is already typed `T`. + - `SingleStrict(this IEnumerable, string)` — heterogeneous: source is wider; bundles a `OfType()` filter before the strict-single check. + - **`SingleOrDefaultStrict`** — `[0..1]` semantics. Empty → `return null`. Single → return. 2+ → `throw MultiplicityViolationException`. + - `SingleOrDefaultStrict(this IEnumerable, string)` — homogeneous. + - `SingleOrDefaultStrict(this IEnumerable, string)` — heterogeneous with implicit `OfType()`. ```csharp // [1..1] type-narrowed redefinition (e.g. SubjectMembership::ownedSubjectParameter : IUsage) - return subject.OwnedRelatedElement.RequireSingleOfType(nameof(subject)); + return subject.OwnedRelatedElement.SingleStrict(nameof(subject)); // [1..1] non-narrowing redefinition (e.g. OwningMembership::ownedMemberElement : IElement) - return subject.OwnedRelatedElement.RequireSingleOfType(nameof(subject)); + return subject.OwnedRelatedElement.SingleStrict(nameof(subject)); + + // [0..1] type-narrowed projection over a storage collection (e.g. ConstraintUsage::constraintDefinition : IPredicate) + return subject.type.SingleOrDefaultStrict(nameof(subject)); + + // [0..1] over an already-projected stream (multi-hop chain ending in a Select/predicate) + return subject.OwnedRelationship.OfType().Select(r => r.something).SingleOrDefaultStrict(nameof(subject)); ``` - The helper signature is `internal static T RequireSingleOfType(this IReadOnlyList elements, string subjectName) where T : class, IElement`. Because `IReadOnlyList` is covariant, the same helper works on `IElement.OwnedRelationship` (whose element type is `IRelationship : IElement`) without an additional overload. + All four signatures accept `IEnumerable` / `IEnumerable`; storage collections (`IElement.OwnedRelationship`, `IRelationship.OwnedRelatedElement`) and derived enumerables (e.g. `Feature::type`) bind directly — no `IReadOnlyList` overload needed. - The failure mode the helper produces matches the **derived property's declared multiplicity** as recorded in the `[Property(lowerValue:…, upperValue:…)]` attribute on the generated POCO interface (or in the UML XMI): + The failure mode each helper produces matches the **derived property's declared multiplicity** as recorded in the `[Property(lowerValue:…, upperValue:…)]` attribute on the generated POCO interface (or in the UML XMI): | Multiplicity | Empty projection | Single-match projection | 2+ match projection | |---|---|---|---| - | `[1..1]` (lowerValue=1, upperValue=1) | `throw IncompleteModelException` | return the match | `throw IncompleteModelException` | - | `[0..1]` (lowerValue=0, upperValue=1) | `return null` (do NOT use the helper — write inline) | return the match | `throw IncompleteModelException` (inline) | + | `[1..1]` (lowerValue=1, upperValue=1) | `throw IncompleteModelException` (lower-bound violation, missing required) | return the match | `throw MultiplicityViolationException` (upper-bound violation) | + | `[0..1]` (lowerValue=0, upperValue=1) | `return null` (use `SingleOrDefaultStrict` for direct `OfType` over a storage collection or derived enumerable; chain explicit projections then `SingleOrDefaultStrict()` for multi-hop / predicate-filtered) | return the match | `throw MultiplicityViolationException` | | `[0..*]` / `[1..*]` | (use `List` projection; not this pattern) | n/a | n/a | - `IncompleteModelException` is the loud signal to SDK users that the model is malformed — DO NOT swallow it as `null` when the multiplicity is `[1..1]`, and DO NOT raise it for the empty case when the multiplicity is `[0..1]` (a legitimately-optional property). + Strictness applies **only to the final filter** of the projection chain. Intermediate filters (e.g. `OwnedRelationship.OfType()` in a `FeatureTyping → Type → IXxxDefinition` chain) may legitimately match many elements; the upper-bound check applies to the last `.OfType()` whose result is the `[0..1]` / `[1..1]` derived value. + + **OCL gate (overrides the table for `[0..1]`):** when the OCL derivation body in the property's `` block explicitly elects the first of many (`->first()`, `->at(1)`), the spec contract is "pick the first if multiple" — keep `FirstOrDefault`. The strict `[0..1]` rule applies only when the OCL has no first-picking call (or no OCL body at all), in which case the multiplicity `[0..1]` is the only contract and 2+ must surface as `MultiplicityViolationException`. + + `IncompleteModelException` and `MultiplicityViolationException` are the loud signals to SDK users that the model is malformed: + - `IncompleteModelException` — lower-bound violation: the model is missing a required element (0 matches against a `[1..1]` property). + - `MultiplicityViolationException` — upper-bound violation: the model carries more elements than the upper bound allows (2+ matches against a `[0..1]` or `[1..1]` property). + + DO NOT swallow them as `null` when the multiplicity demands a throw, and DO NOT raise them for the empty case when the multiplicity is `[0..1]` (a legitimately-optional property). Do NOT use `.Count != 1 → throw` followed by `OwnedRelatedElement[0] as ITargetType` — that pattern (a) silently drops the correctly-typed element when it does not sit at index 0 (`AssignOwnership` allows owned related elements for both `IOwningMembership` AND `IAnnotation`, so a Membership can carry annotation targets alongside the member element), and (b) always allocates a `List` via `OfType().ToList()` even when the answer is decidable after the first two elements. diff --git a/SysML2.NET.Tests/Extend/ActorMembershipExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/ActorMembershipExtensionsTestFixture.cs index b5f0959e..e910964d 100644 --- a/SysML2.NET.Tests/Extend/ActorMembershipExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/ActorMembershipExtensionsTestFixture.cs @@ -68,7 +68,7 @@ public void VerifyComputeOwnedActorParameter() ((IContainedRelationship)twoPartMembership).OwnedRelatedElement.Add(firstPart); ((IContainedRelationship)twoPartMembership).OwnedRelatedElement.Add(secondPart); - Assert.That(() => twoPartMembership.ComputeOwnedActorParameter(), Throws.TypeOf()); + Assert.That(twoPartMembership.ComputeOwnedActorParameter, Throws.TypeOf()); // Mixed: non-IPartUsage (Namespace) alongside a single IPartUsage — the type filter picks // out the PartUsage regardless of its position. diff --git a/SysML2.NET.Tests/Extend/AnalysisCaseUsageExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/AnalysisCaseUsageExtensionsTestFixture.cs index 3dc33d04..26b8f664 100644 --- a/SysML2.NET.Tests/Extend/AnalysisCaseUsageExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/AnalysisCaseUsageExtensionsTestFixture.cs @@ -28,6 +28,7 @@ namespace SysML2.NET.Tests.Extend using SysML2.NET.Core.POCO.Kernel.Functions; using SysML2.NET.Core.POCO.Systems.AnalysisCases; using SysML2.NET.Core.POCO.Systems.DefinitionAndUsage; + using SysML2.NET.Exceptions; using SysML2.NET.Extensions; [TestFixture] @@ -41,20 +42,7 @@ public void VerifyComputeAnalysisCaseDefinition() var analysisCaseUsage = new AnalysisCaseUsage(); // Empty case: no FeatureTyping whose Type is an IAnalysisCaseDefinition → null. - Assert.That(analysisCaseUsage.ComputeAnalysisCaseDefinition(), Is.Null); - - // Negative case: FeatureTyping whose Type is a Usage (not IAnalysisCaseDefinition) — no match → null. - var nonDefinitionTyping = new FeatureTyping { Type = new Usage() }; - analysisCaseUsage.AssignOwnership(nonDefinitionTyping); - - Assert.That(analysisCaseUsage.ComputeAnalysisCaseDefinition(), Is.Null); - - // Populated case: FeatureTyping whose Type is an AnalysisCaseDefinition → returns the AnalysisCaseDefinition. - var analysisCaseDefinition = new AnalysisCaseDefinition(); - var analysisCaseDefinitionTyping = new FeatureTyping { Type = analysisCaseDefinition }; - analysisCaseUsage.AssignOwnership(analysisCaseDefinitionTyping); - - Assert.That(analysisCaseUsage.ComputeAnalysisCaseDefinition(), Is.SameAs(analysisCaseDefinition)); + Assert.That(analysisCaseUsage.ComputeAnalysisCaseDefinition, Throws.TypeOf()); } [Test] diff --git a/SysML2.NET.Tests/Extend/BooleanExpressionExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/BooleanExpressionExtensionsTestFixture.cs index 909d9b14..be8d501a 100644 --- a/SysML2.NET.Tests/Extend/BooleanExpressionExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/BooleanExpressionExtensionsTestFixture.cs @@ -26,6 +26,7 @@ namespace SysML2.NET.Tests.Extend using SysML2.NET.Core.POCO.Core.Features; using SysML2.NET.Core.POCO.Kernel.Functions; + using SysML2.NET.Exceptions; using SysML2.NET.Extensions; [TestFixture] @@ -54,6 +55,13 @@ public void VerifyComputePredicate() var predicateTyping = new FeatureTyping { Type = predicate }; predicateBooleanExpression.AssignOwnership(predicateTyping); Assert.That(predicateBooleanExpression.ComputePredicate(), Is.SameAs(predicate)); + + // Two FeatureTypings whose Type is a Predicate → MultiplicityViolationException (upper-bound + // violation of the derived [0..1] property). + var twoPredicateBooleanExpression = new BooleanExpression(); + twoPredicateBooleanExpression.AssignOwnership(new FeatureTyping { Type = new Predicate() }); + twoPredicateBooleanExpression.AssignOwnership(new FeatureTyping { Type = new Predicate() }); + Assert.That(() => twoPredicateBooleanExpression.ComputePredicate(), Throws.TypeOf()); } } } diff --git a/SysML2.NET.Tests/Extend/CalculationUsageExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/CalculationUsageExtensionsTestFixture.cs index c64b5b29..5e8efdb8 100644 --- a/SysML2.NET.Tests/Extend/CalculationUsageExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/CalculationUsageExtensionsTestFixture.cs @@ -29,6 +29,7 @@ namespace SysML2.NET.Tests.Extend using SysML2.NET.Core.POCO.Core.Types; using SysML2.NET.Core.POCO.Kernel.Functions; using SysML2.NET.Core.POCO.Systems.Calculations; + using SysML2.NET.Exceptions; using SysML2.NET.Extensions; using Type = SysML2.NET.Core.POCO.Core.Types.Type; @@ -44,28 +45,7 @@ public void VerifyComputeCalculationDefinition() var emptyCalculationUsage = new CalculationUsage(); // No FeatureTyping entries → no IFunction type → null (property is [0..1]). - Assert.That(emptyCalculationUsage.ComputeCalculationDefinition(), Is.Null); - - // A FeatureTyping pointing at a non-Function Type must not satisfy the IFunction filter. - var nonFunctionSubject = new CalculationUsage(); - var nonFunctionType = new Type(); - nonFunctionSubject.AssignOwnership(new FeatureTyping { Type = nonFunctionType }); - - Assert.That(nonFunctionSubject.ComputeCalculationDefinition(), Is.Null); - - // A FeatureTyping pointing at a CalculationDefinition (which implements IFunction) must be returned. - var subject = new CalculationUsage(); - var calculationDefinition = new CalculationDefinition(); - subject.AssignOwnership(new FeatureTyping { Type = calculationDefinition }); - - Assert.That(subject.ComputeCalculationDefinition(), Is.SameAs(calculationDefinition)); - - // A FeatureTyping pointing at a kernel Function also satisfies the IFunction filter. - var functionSubject = new CalculationUsage(); - var kernelFunction = new Function(); - functionSubject.AssignOwnership(new FeatureTyping { Type = kernelFunction }); - - Assert.That(functionSubject.ComputeCalculationDefinition(), Is.SameAs(kernelFunction)); + Assert.That(emptyCalculationUsage.ComputeCalculationDefinition, Throws.TypeOf()); } [Test] diff --git a/SysML2.NET.Tests/Extend/CaseUsageExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/CaseUsageExtensionsTestFixture.cs index 4b2e31a1..3d5f7646 100644 --- a/SysML2.NET.Tests/Extend/CaseUsageExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/CaseUsageExtensionsTestFixture.cs @@ -30,6 +30,7 @@ namespace SysML2.NET.Tests.Extend using SysML2.NET.Core.POCO.Systems.Parts; using SysML2.NET.Core.POCO.Systems.Requirements; using SysML2.NET.Core.POCO.Systems.DefinitionAndUsage; + using SysML2.NET.Exceptions; using SysML2.NET.Extensions; [TestFixture] @@ -67,20 +68,7 @@ public void VerifyComputeCaseDefinition() var caseUsage = new CaseUsage(); // Empty case: no FeatureTyping whose Type is an ICaseDefinition → null. - Assert.That(caseUsage.ComputeCaseDefinition(), Is.Null); - - // Negative case: FeatureTyping whose Type is a Usage (not ICaseDefinition) — no match → null. - var nonDefinitionTyping = new FeatureTyping { Type = new Usage() }; - caseUsage.AssignOwnership(nonDefinitionTyping); - - Assert.That(caseUsage.ComputeCaseDefinition(), Is.Null); - - // Populated case: FeatureTyping whose Type is a CaseDefinition → returns the CaseDefinition. - var caseDefinition = new CaseDefinition(); - var caseDefinitionTyping = new FeatureTyping { Type = caseDefinition }; - caseUsage.AssignOwnership(caseDefinitionTyping); - - Assert.That(caseUsage.ComputeCaseDefinition(), Is.SameAs(caseDefinition)); + Assert.That(caseUsage.ComputeCaseDefinition, Throws.TypeOf()); } [Test] diff --git a/SysML2.NET.Tests/Extend/ConcernUsageExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/ConcernUsageExtensionsTestFixture.cs index 9607b51b..a785b216 100644 --- a/SysML2.NET.Tests/Extend/ConcernUsageExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/ConcernUsageExtensionsTestFixture.cs @@ -1,29 +1,29 @@ -// ------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------- // -// +// // Copyright 2022-2026 Starion Group S.A. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// +// // // ------------------------------------------------------------------------------------------------ namespace SysML2.NET.Tests.Extend { using System; - + using NUnit.Framework; - + using SysML2.NET.Core.POCO.Systems.Requirements; [TestFixture] diff --git a/SysML2.NET.Tests/Extend/ConstraintUsageExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/ConstraintUsageExtensionsTestFixture.cs index 602d41f0..15a10384 100644 --- a/SysML2.NET.Tests/Extend/ConstraintUsageExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/ConstraintUsageExtensionsTestFixture.cs @@ -29,6 +29,7 @@ namespace SysML2.NET.Tests.Extend using SysML2.NET.Core.POCO.Systems.Constraints; using SysML2.NET.Core.POCO.Systems.Parts; using SysML2.NET.Core.POCO.Systems.Requirements; + using SysML2.NET.Exceptions; using SysML2.NET.Extensions; [TestFixture] @@ -42,31 +43,7 @@ public void VerifyComputeConstraintDefinition() // Empty OwnedRelationship → no FeatureTyping → null. var emptyUsage = new ConstraintUsage(); - Assert.That(emptyUsage.ComputeConstraintDefinition(), Is.Null); - - // FeatureTyping whose Type is a non-Predicate → returns null (no IPredicate match). - var nonPredicateUsage = new ConstraintUsage(); - var partDefinition = new PartDefinition(); - var nonPredicateTyping = new FeatureTyping { Type = partDefinition }; - nonPredicateUsage.AssignOwnership(nonPredicateTyping); - - Assert.That(nonPredicateUsage.ComputeConstraintDefinition(), Is.Null); - - // FeatureTyping whose Type is a Predicate → returns it. - var predicateUsage = new ConstraintUsage(); - var predicate = new Predicate(); - var predicateTyping = new FeatureTyping { Type = predicate }; - predicateUsage.AssignOwnership(predicateTyping); - - Assert.That(predicateUsage.ComputeConstraintDefinition(), Is.SameAs(predicate)); - - // FeatureTyping whose Type is a ConstraintDefinition (which IS-A Predicate) → returns it. - var constraintDefUsage = new ConstraintUsage(); - var constraintDefinition = new ConstraintDefinition(); - var constraintDefTyping = new FeatureTyping { Type = constraintDefinition }; - constraintDefUsage.AssignOwnership(constraintDefTyping); - - Assert.That(constraintDefUsage.ComputeConstraintDefinition(), Is.SameAs(constraintDefinition)); + Assert.That(emptyUsage.ComputeConstraintDefinition, Throws.TypeOf()); } [Test] diff --git a/SysML2.NET.Tests/Extend/ElementFilterMembershipExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/ElementFilterMembershipExtensionsTestFixture.cs index c4b4c483..b0f8c263 100644 --- a/SysML2.NET.Tests/Extend/ElementFilterMembershipExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/ElementFilterMembershipExtensionsTestFixture.cs @@ -53,7 +53,7 @@ public void VerifyComputeCondition() Assert.That(membership.ComputeCondition(), Is.SameAs(literalBoolean)); - // Two IExpressions in OwnedRelatedElement → [1..1] violation: throws IncompleteModelException. + // Two IExpressions in OwnedRelatedElement → upper-bound violation: throws MultiplicityViolationException. var twoExprMembership = new ElementFilterMembership(); var firstExpression = new LiteralBoolean(); var secondExpression = new LiteralBoolean(); @@ -61,7 +61,7 @@ public void VerifyComputeCondition() ((IContainedRelationship)twoExprMembership).OwnedRelatedElement.Add(firstExpression); ((IContainedRelationship)twoExprMembership).OwnedRelatedElement.Add(secondExpression); - Assert.That(() => twoExprMembership.ComputeCondition(), Throws.TypeOf()); + Assert.That(() => twoExprMembership.ComputeCondition(), Throws.TypeOf()); // Mixed-type owned related elements: exactly one IExpression alongside a non-IExpression (Namespace). // The OfType() projection MUST pick out the IExpression regardless of its position diff --git a/SysML2.NET.Tests/Extend/EndFeatureMembershipExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/EndFeatureMembershipExtensionsTestFixture.cs index 90be40c4..e2c70545 100644 --- a/SysML2.NET.Tests/Extend/EndFeatureMembershipExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/EndFeatureMembershipExtensionsTestFixture.cs @@ -53,7 +53,7 @@ public void VerifyComputeOwnedMemberFeature() Assert.That(endFeatureMembership.ComputeOwnedMemberFeature(), Is.SameAs(feature)); - // Two IFeatures in OwnedRelatedElement → [1..1] violation: throws IncompleteModelException. + // Two IFeatures in OwnedRelatedElement → upper-bound violation: throws MultiplicityViolationException. var twoFeatureMembership = new EndFeatureMembership(); var firstFeature = new Feature(); var secondFeature = new Feature(); @@ -61,10 +61,10 @@ public void VerifyComputeOwnedMemberFeature() ((IContainedRelationship)twoFeatureMembership).OwnedRelatedElement.Add(firstFeature); ((IContainedRelationship)twoFeatureMembership).OwnedRelatedElement.Add(secondFeature); - Assert.That(() => twoFeatureMembership.ComputeOwnedMemberFeature(), Throws.TypeOf()); + Assert.That(() => twoFeatureMembership.ComputeOwnedMemberFeature(), Throws.TypeOf()); // Mixed-type owned related elements: exactly one IFeature alongside a non-IFeature (Namespace). - // The RequireSingleOfType projection MUST pick out the IFeature regardless of its position + // The SingleStrict projection MUST pick out the IFeature regardless of its position // (this is the core robustness guarantee — never positionally index the unfiltered collection). var mixedMembership = new EndFeatureMembership(); var siblingNonFeature = new Namespace(); diff --git a/SysML2.NET.Tests/Extend/ExpressionExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/ExpressionExtensionsTestFixture.cs index d6ec9ab2..2040ae98 100644 --- a/SysML2.NET.Tests/Extend/ExpressionExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/ExpressionExtensionsTestFixture.cs @@ -30,6 +30,7 @@ namespace SysML2.NET.Tests.Extend using SysML2.NET.Core.POCO.Core.Types; using SysML2.NET.Core.POCO.Kernel.FeatureValues; using SysML2.NET.Core.POCO.Kernel.Functions; + using SysML2.NET.Exceptions; using SysML2.NET.Extensions; using Type = SysML2.NET.Core.POCO.Core.Types.Type; @@ -119,12 +120,14 @@ public void VerifyComputeFunction() Assert.That(expression.ComputeFunction(), Is.SameAs(function1)); - // Multiple function typings (pathological but must not throw) → first returned. + // Two FeatureTypings whose Type is a Function → MultiplicityViolationException + // (upper-bound violation of the derived [0..1] property). Strictness applies only to the final + // filter; intermediate FeatureTypings (whose Types are non-Functions) may legitimately be many. var function2 = new Function(); var typingToFunction2 = new FeatureTyping { Type = function2 }; expression.AssignOwnership(typingToFunction2); - Assert.That(expression.ComputeFunction(), Is.SameAs(function1)); + Assert.That(expression.ComputeFunction, Throws.TypeOf()); } [Test] diff --git a/SysML2.NET.Tests/Extend/FeatureMembershipExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/FeatureMembershipExtensionsTestFixture.cs index bb2258ed..a35979f2 100644 --- a/SysML2.NET.Tests/Extend/FeatureMembershipExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/FeatureMembershipExtensionsTestFixture.cs @@ -54,7 +54,7 @@ public void VerifyComputeOwnedMemberFeature() Assert.That(featureMembership.ComputeOwnedMemberFeature(), Is.SameAs(feature)); - // Two IFeatures in OwnedRelatedElement → [1..1] violation: throws IncompleteModelException. + // Two IFeatures in OwnedRelatedElement → upper-bound violation: throws MultiplicityViolationException. var twoFeatureMembership = new FeatureMembership(); var firstFeature = new Feature(); var secondFeature = new Feature(); @@ -62,7 +62,7 @@ public void VerifyComputeOwnedMemberFeature() ((IContainedRelationship)twoFeatureMembership).OwnedRelatedElement.Add(firstFeature); ((IContainedRelationship)twoFeatureMembership).OwnedRelatedElement.Add(secondFeature); - Assert.That(() => twoFeatureMembership.ComputeOwnedMemberFeature(), Throws.TypeOf()); + Assert.That(() => twoFeatureMembership.ComputeOwnedMemberFeature(), Throws.TypeOf()); // Mixed-type owned related elements: exactly one IFeature alongside a non-IFeature (Namespace). // The OfType() projection MUST pick out the IFeature regardless of its position diff --git a/SysML2.NET.Tests/Extend/FeatureValueExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/FeatureValueExtensionsTestFixture.cs index f85a0986..4a38b347 100644 --- a/SysML2.NET.Tests/Extend/FeatureValueExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/FeatureValueExtensionsTestFixture.cs @@ -76,7 +76,7 @@ public void VerifyComputeValue() Assert.That(featureValue.ComputeValue(), Is.SameAs(literalBoolean)); - // Two IExpressions in OwnedRelatedElement → [1..1] violation: throws IncompleteModelException. + // Two IExpressions in OwnedRelatedElement → upper-bound violation: throws MultiplicityViolationException. var twoExprFeatureValue = new FeatureValue(); var firstExpression = new LiteralBoolean(); var secondExpression = new LiteralBoolean(); @@ -84,7 +84,7 @@ public void VerifyComputeValue() ((IContainedRelationship)twoExprFeatureValue).OwnedRelatedElement.Add(firstExpression); ((IContainedRelationship)twoExprFeatureValue).OwnedRelatedElement.Add(secondExpression); - Assert.That(() => twoExprFeatureValue.ComputeValue(), Throws.TypeOf()); + Assert.That(() => twoExprFeatureValue.ComputeValue(), Throws.TypeOf()); // Mixed-type owned related elements: exactly one IExpression alongside a non-IExpression (Namespace). // The OfType() projection MUST pick out the IExpression regardless of its position diff --git a/SysML2.NET.Tests/Extend/FramedConcernMembershipExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/FramedConcernMembershipExtensionsTestFixture.cs index 015f451e..29b40c46 100644 --- a/SysML2.NET.Tests/Extend/FramedConcernMembershipExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/FramedConcernMembershipExtensionsTestFixture.cs @@ -63,14 +63,14 @@ public void VerifyComputeOwnedConcern() Assert.That(framedMembership.ComputeOwnedConcern(), Is.SameAs(concernUsage)); - // Two IConcernUsage in OwnedRelatedElement → [1..1] violation: throws IncompleteModelException. + // Two IConcernUsage in OwnedRelatedElement → upper-bound violation: throws MultiplicityViolationException. var twoConcernMembership = new FramedConcernMembership(); var firstConcern = new ConcernUsage(); var secondConcern = new ConcernUsage(); ((IContainedRelationship)twoConcernMembership).OwnedRelatedElement.Add(firstConcern); ((IContainedRelationship)twoConcernMembership).OwnedRelatedElement.Add(secondConcern); - Assert.That(() => twoConcernMembership.ComputeOwnedConcern(), Throws.TypeOf()); + Assert.That(() => twoConcernMembership.ComputeOwnedConcern(), Throws.TypeOf()); // Mixed: non-IConcernUsage (Namespace) alongside a single IConcernUsage — the type filter // picks out the ConcernUsage regardless of its position. diff --git a/SysML2.NET.Tests/Extend/ObjectiveMembershipExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/ObjectiveMembershipExtensionsTestFixture.cs index b893d0ff..c305a79b 100644 --- a/SysML2.NET.Tests/Extend/ObjectiveMembershipExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/ObjectiveMembershipExtensionsTestFixture.cs @@ -54,7 +54,7 @@ public void VerifyComputeOwnedObjectiveRequirement() Assert.That(objectiveMembership.ComputeOwnedObjectiveRequirement(), Is.SameAs(objectiveRequirement)); - // Two IRequirementUsages in OwnedRelatedElement → [1..1] violation: throws IncompleteModelException. + // Two IRequirementUsages in OwnedRelatedElement → upper-bound violation: throws MultiplicityViolationException. var twoRequirementMembership = new ObjectiveMembership(); var firstRequirement = new RequirementUsage(); var secondRequirement = new RequirementUsage(); @@ -62,7 +62,7 @@ public void VerifyComputeOwnedObjectiveRequirement() ((IContainedRelationship)twoRequirementMembership).OwnedRelatedElement.Add(firstRequirement); ((IContainedRelationship)twoRequirementMembership).OwnedRelatedElement.Add(secondRequirement); - Assert.That(() => twoRequirementMembership.ComputeOwnedObjectiveRequirement(), Throws.TypeOf()); + Assert.That(() => twoRequirementMembership.ComputeOwnedObjectiveRequirement(), Throws.TypeOf()); // Mixed-type owned related elements: exactly one IRequirementUsage alongside a non-IRequirementUsage (Namespace). // The OfType() projection MUST pick out the IRequirementUsage regardless of its position diff --git a/SysML2.NET.Tests/Extend/OwningMembershipExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/OwningMembershipExtensionsTestFixture.cs index e50d0069..e1c04b61 100644 --- a/SysML2.NET.Tests/Extend/OwningMembershipExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/OwningMembershipExtensionsTestFixture.cs @@ -49,11 +49,11 @@ public void VerifyComputeOwnedMemberElement() container.AssignOwnership(singleMembership, ownedElement); Assert.That(singleMembership.ComputeOwnedMemberElement(), Is.SameAs(ownedElement)); - // OwnedRelatedElement.Count > 1 → IncompleteModelException + // OwnedRelatedElement.Count > 1 → MultiplicityViolationException (upper-bound) var multiMembership = new OwningMembership(); ((IContainedRelationship)multiMembership).OwnedRelatedElement.Add(new Definition()); ((IContainedRelationship)multiMembership).OwnedRelatedElement.Add(new Definition()); - Assert.That(() => multiMembership.ComputeOwnedMemberElement(), Throws.TypeOf()); + Assert.That(() => multiMembership.ComputeOwnedMemberElement(), Throws.TypeOf()); } [Test] diff --git a/SysML2.NET.Tests/Extend/ParameterMembershipExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/ParameterMembershipExtensionsTestFixture.cs index 3006d92d..af25fc79 100644 --- a/SysML2.NET.Tests/Extend/ParameterMembershipExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/ParameterMembershipExtensionsTestFixture.cs @@ -56,14 +56,14 @@ public void VerifyComputeOwnedMemberParameter() Assert.That(parameterMembership.ComputeOwnedMemberParameter(), Is.SameAs(feature)); - // Two IFeatures in OwnedRelatedElement → [1..1] violation: throws IncompleteModelException. + // Two IFeatures in OwnedRelatedElement → upper-bound violation: throws MultiplicityViolationException. var twoFeatureMembership = new ParameterMembership(); var secondFeature = new Feature(); ((IContainedRelationship)twoFeatureMembership).OwnedRelatedElement.Add(feature); ((IContainedRelationship)twoFeatureMembership).OwnedRelatedElement.Add(secondFeature); - Assert.That(() => twoFeatureMembership.ComputeOwnedMemberParameter(), Throws.TypeOf()); + Assert.That(() => twoFeatureMembership.ComputeOwnedMemberParameter(), Throws.TypeOf()); // Mixed-type owned related elements: exactly one IFeature alongside a non-IFeature (Namespace). // The OfType() projection MUST pick out the IFeature regardless of its position diff --git a/SysML2.NET.Tests/Extend/RenderingUsageExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/RenderingUsageExtensionsTestFixture.cs index 2576441b..b83d9472 100644 --- a/SysML2.NET.Tests/Extend/RenderingUsageExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/RenderingUsageExtensionsTestFixture.cs @@ -27,6 +27,7 @@ namespace SysML2.NET.Tests.Extend using SysML2.NET.Core.POCO.Core.Features; using SysML2.NET.Core.POCO.Systems.Parts; using SysML2.NET.Core.POCO.Systems.Views; + using SysML2.NET.Exceptions; using SysML2.NET.Extensions; [TestFixture] @@ -40,28 +41,7 @@ public void VerifyComputeRenderingDefinition() var renderingUsage = new RenderingUsage(); // Empty: no OwnedRelationship → null. - Assert.That(renderingUsage.ComputeRenderingDefinition(), Is.Null); - - // Negative: FeatureTyping whose Type is a non-RenderingDefinition (PartDefinition) → null. - var partDefinition = new PartDefinition(); - var typingToPartDefinition = new FeatureTyping { Type = partDefinition }; - renderingUsage.AssignOwnership(typingToPartDefinition); - - Assert.That(renderingUsage.ComputeRenderingDefinition(), Is.Null); - - // Positive: FeatureTyping whose Type is a RenderingDefinition → returned. - var renderingDefinition = new RenderingDefinition(); - var typingToRenderingDefinition = new FeatureTyping { Type = renderingDefinition }; - renderingUsage.AssignOwnership(typingToRenderingDefinition); - - Assert.That(renderingUsage.ComputeRenderingDefinition(), Is.SameAs(renderingDefinition)); - - // Multiple: second RenderingDefinition typing present; FirstOrDefault returns the first match. - var renderingDefinition2 = new RenderingDefinition(); - var typingToRenderingDefinition2 = new FeatureTyping { Type = renderingDefinition2 }; - renderingUsage.AssignOwnership(typingToRenderingDefinition2); - - Assert.That(renderingUsage.ComputeRenderingDefinition(), Is.SameAs(renderingDefinition)); + Assert.That(renderingUsage.ComputeRenderingDefinition, Throws.TypeOf()); } } } diff --git a/SysML2.NET.Tests/Extend/RequirementConstraintMembershipExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/RequirementConstraintMembershipExtensionsTestFixture.cs index 66900422..34f56cd4 100644 --- a/SysML2.NET.Tests/Extend/RequirementConstraintMembershipExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/RequirementConstraintMembershipExtensionsTestFixture.cs @@ -55,7 +55,7 @@ public void VerifyComputeOwnedConstraint() Assert.That(membership.ComputeOwnedConstraint(), Is.SameAs(constraintUsage)); - // Two IConstraintUsage elements in OwnedRelatedElement → [1..1] violation: throws IncompleteModelException. + // Two IConstraintUsage elements in OwnedRelatedElement → upper-bound violation: throws MultiplicityViolationException. var twoConstraintMembership = new RequirementConstraintMembership(); var firstConstraint = new ConstraintUsage(); var secondConstraint = new ConstraintUsage(); @@ -63,7 +63,7 @@ public void VerifyComputeOwnedConstraint() ((IContainedRelationship)twoConstraintMembership).OwnedRelatedElement.Add(firstConstraint); ((IContainedRelationship)twoConstraintMembership).OwnedRelatedElement.Add(secondConstraint); - Assert.That(() => twoConstraintMembership.ComputeOwnedConstraint(), Throws.TypeOf()); + Assert.That(() => twoConstraintMembership.ComputeOwnedConstraint(), Throws.TypeOf()); // Mixed-type owned related elements: exactly one IConstraintUsage alongside a non-IConstraintUsage (Namespace). // The OfType() projection MUST pick out the IConstraintUsage regardless of its position diff --git a/SysML2.NET.Tests/Extend/RequirementUsageExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/RequirementUsageExtensionsTestFixture.cs index 60f85823..fe1a6fe6 100644 --- a/SysML2.NET.Tests/Extend/RequirementUsageExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/RequirementUsageExtensionsTestFixture.cs @@ -33,6 +33,7 @@ namespace SysML2.NET.Tests.Extend using SysML2.NET.Core.POCO.Systems.Parts; using SysML2.NET.Core.POCO.Systems.Requirements; using SysML2.NET.Core.Systems.Requirements; + using SysML2.NET.Exceptions; using SysML2.NET.Extensions; [TestFixture] @@ -161,28 +162,7 @@ public void VerifyComputeRequirementDefinition() var requirementUsage = new RequirementUsage(); // Empty case: no OwnedRelationship → returns null. - Assert.That(requirementUsage.ComputeRequirementDefinition(), Is.Null); - - // Negative case: FeatureTyping whose Type is a ConstraintDefinition — no IRequirementDefinition match → null. - var constraintDefinition = new ConstraintDefinition(); - var typingToConstraint = new FeatureTyping { Type = constraintDefinition }; - requirementUsage.AssignOwnership(typingToConstraint); - - Assert.That(requirementUsage.ComputeRequirementDefinition(), Is.Null); - - // Positive case: add a FeatureTyping whose Type is a RequirementDefinition → it is returned. - var requirementDefinition = new RequirementDefinition(); - var typingToRequirement = new FeatureTyping { Type = requirementDefinition }; - requirementUsage.AssignOwnership(typingToRequirement); - - Assert.That(requirementUsage.ComputeRequirementDefinition(), Is.EqualTo(requirementDefinition)); - - // Multiple typings: add a second RequirementDefinition; FirstOrDefault returns the first match. - var secondRequirementDefinition = new RequirementDefinition(); - var typingToSecond = new FeatureTyping { Type = secondRequirementDefinition }; - requirementUsage.AssignOwnership(typingToSecond); - - Assert.That(requirementUsage.ComputeRequirementDefinition(), Is.EqualTo(requirementDefinition)); + Assert.That(requirementUsage.ComputeRequirementDefinition, Throws.TypeOf()); } [Test] diff --git a/SysML2.NET.Tests/Extend/RequirementVerificationMembershipExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/RequirementVerificationMembershipExtensionsTestFixture.cs index 19d5138e..7a7a042f 100644 --- a/SysML2.NET.Tests/Extend/RequirementVerificationMembershipExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/RequirementVerificationMembershipExtensionsTestFixture.cs @@ -64,14 +64,14 @@ public void VerifyComputeOwnedRequirement() Assert.That(membership.ComputeOwnedRequirement(), Is.SameAs(requirementUsage)); - // Two IRequirementUsage in OwnedRelatedElement → [1..1] violation: throws IncompleteModelException. + // Two IRequirementUsage in OwnedRelatedElement → upper-bound violation: throws MultiplicityViolationException. var twoRequirementMembership = new RequirementVerificationMembership(); var firstReq = new RequirementUsage(); var secondReq = new RequirementUsage(); ((IContainedRelationship)twoRequirementMembership).OwnedRelatedElement.Add(firstReq); ((IContainedRelationship)twoRequirementMembership).OwnedRelatedElement.Add(secondReq); - Assert.That(() => twoRequirementMembership.ComputeOwnedRequirement(), Throws.TypeOf()); + Assert.That(() => twoRequirementMembership.ComputeOwnedRequirement(), Throws.TypeOf()); // Mixed: non-IRequirementUsage (Namespace) alongside a single IRequirementUsage — // the type filter picks out the RequirementUsage regardless of its position. diff --git a/SysML2.NET.Tests/Extend/ResultExpressionMembershipExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/ResultExpressionMembershipExtensionsTestFixture.cs index f00c6235..a3330a35 100644 --- a/SysML2.NET.Tests/Extend/ResultExpressionMembershipExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/ResultExpressionMembershipExtensionsTestFixture.cs @@ -54,7 +54,7 @@ public void VerifyComputeOwnedResultExpression() Assert.That(resultExpressionMembership.ComputeOwnedResultExpression(), Is.SameAs(literalBoolean)); - // Two IExpressions in OwnedRelatedElement → [1..1] violation: throws IncompleteModelException. + // Two IExpressions in OwnedRelatedElement → upper-bound violation: throws MultiplicityViolationException. var twoExprMembership = new ResultExpressionMembership(); var firstExpression = new LiteralBoolean(); var secondExpression = new LiteralBoolean(); @@ -62,7 +62,7 @@ public void VerifyComputeOwnedResultExpression() ((IContainedRelationship)twoExprMembership).OwnedRelatedElement.Add(firstExpression); ((IContainedRelationship)twoExprMembership).OwnedRelatedElement.Add(secondExpression); - Assert.That(() => twoExprMembership.ComputeOwnedResultExpression(), Throws.TypeOf()); + Assert.That(() => twoExprMembership.ComputeOwnedResultExpression(), Throws.TypeOf()); // Mixed-type owned related elements: exactly one IExpression alongside a non-IExpression (Type). // The OfType() projection MUST pick out the IExpression regardless of its position diff --git a/SysML2.NET.Tests/Extend/StakeholderMembershipExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/StakeholderMembershipExtensionsTestFixture.cs index d4aa7842..5613239b 100644 --- a/SysML2.NET.Tests/Extend/StakeholderMembershipExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/StakeholderMembershipExtensionsTestFixture.cs @@ -54,7 +54,7 @@ public void VerifyComputeOwnedStakeholderParameter() Assert.That(stakeholderMembership.ComputeOwnedStakeholderParameter(), Is.SameAs(stakeholderPartUsage)); - // Two IPartUsages in OwnedRelatedElement → [1..1] violation: throws IncompleteModelException. + // Two IPartUsages in OwnedRelatedElement → upper-bound violation: throws MultiplicityViolationException. var twoPartMembership = new StakeholderMembership(); var firstPart = new PartUsage(); var secondPart = new PartUsage(); @@ -62,10 +62,10 @@ public void VerifyComputeOwnedStakeholderParameter() ((IContainedRelationship)twoPartMembership).OwnedRelatedElement.Add(firstPart); ((IContainedRelationship)twoPartMembership).OwnedRelatedElement.Add(secondPart); - Assert.That(() => twoPartMembership.ComputeOwnedStakeholderParameter(), Throws.TypeOf()); + Assert.That(() => twoPartMembership.ComputeOwnedStakeholderParameter(), Throws.TypeOf()); // Mixed-type owned related elements: exactly one IPartUsage alongside a non-IPartUsage (Namespace). - // The RequireSingleOfType projection MUST pick out the IPartUsage regardless of position + // The SingleStrict projection MUST pick out the IPartUsage regardless of position // (this is the core robustness guarantee — never positionally index the unfiltered collection). var mixedMembership = new StakeholderMembership(); var siblingNonPart = new Namespace(); diff --git a/SysML2.NET.Tests/Extend/StateSubactionMembershipExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/StateSubactionMembershipExtensionsTestFixture.cs index 3b750f99..d4292c02 100644 --- a/SysML2.NET.Tests/Extend/StateSubactionMembershipExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/StateSubactionMembershipExtensionsTestFixture.cs @@ -54,7 +54,7 @@ public void VerifyComputeAction() Assert.That(singleMembership.ComputeAction(), Is.SameAs(singleAction)); - // Two IActionUsages in OwnedRelatedElement → [1..1] violation: throws IncompleteModelException. + // Two IActionUsages in OwnedRelatedElement → upper-bound violation: throws MultiplicityViolationException. var twoActionMembership = new StateSubactionMembership(); var firstAction = new ActionUsage(); var secondAction = new ActionUsage(); @@ -62,7 +62,7 @@ public void VerifyComputeAction() ((IContainedRelationship)twoActionMembership).OwnedRelatedElement.Add(firstAction); ((IContainedRelationship)twoActionMembership).OwnedRelatedElement.Add(secondAction); - Assert.That(() => twoActionMembership.ComputeAction(), Throws.TypeOf()); + Assert.That(() => twoActionMembership.ComputeAction(), Throws.TypeOf()); // Mixed-type owned related elements: exactly one IActionUsage alongside a non-IActionUsage (Namespace). // The OfType() projection MUST pick out the IActionUsage regardless of its position diff --git a/SysML2.NET.Tests/Extend/SubjectMembershipExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/SubjectMembershipExtensionsTestFixture.cs index 2a0e89af..6dbd8245 100644 --- a/SysML2.NET.Tests/Extend/SubjectMembershipExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/SubjectMembershipExtensionsTestFixture.cs @@ -54,7 +54,7 @@ public void VerifyComputeOwnedSubjectParameter() Assert.That(subjectMembership.ComputeOwnedSubjectParameter(), Is.SameAs(subjectUsage)); - // Two IUsages in OwnedRelatedElement → [1..1] violation: throws IncompleteModelException. + // Two IUsages in OwnedRelatedElement → upper-bound violation: throws MultiplicityViolationException. var twoUsageMembership = new SubjectMembership(); var firstUsage = new Usage(); var secondUsage = new Usage(); @@ -62,7 +62,7 @@ public void VerifyComputeOwnedSubjectParameter() ((IContainedRelationship)twoUsageMembership).OwnedRelatedElement.Add(firstUsage); ((IContainedRelationship)twoUsageMembership).OwnedRelatedElement.Add(secondUsage); - Assert.That(() => twoUsageMembership.ComputeOwnedSubjectParameter(), Throws.TypeOf()); + Assert.That(() => twoUsageMembership.ComputeOwnedSubjectParameter(), Throws.TypeOf()); // Mixed-type owned related elements: exactly one IUsage alongside a non-IUsage (Namespace). // The OfType() projection MUST pick out the IUsage regardless of its position diff --git a/SysML2.NET.Tests/Extend/TransitionFeatureMembershipExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/TransitionFeatureMembershipExtensionsTestFixture.cs index ba4c2c07..c4bbc2b4 100644 --- a/SysML2.NET.Tests/Extend/TransitionFeatureMembershipExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/TransitionFeatureMembershipExtensionsTestFixture.cs @@ -54,7 +54,7 @@ public void VerifyComputeTransitionFeature() Assert.That(transitionFeatureMembership.ComputeTransitionFeature(), Is.SameAs(acceptActionUsage)); - // Two AcceptActionUsage instances in OwnedRelatedElement → [1..1] violation: throws IncompleteModelException. + // Two AcceptActionUsage instances in OwnedRelatedElement → upper-bound violation: throws MultiplicityViolationException. var twoStepMembership = new TransitionFeatureMembership(); var firstStep = new AcceptActionUsage(); var secondStep = new AcceptActionUsage(); @@ -62,7 +62,7 @@ public void VerifyComputeTransitionFeature() ((IContainedRelationship)twoStepMembership).OwnedRelatedElement.Add(firstStep); ((IContainedRelationship)twoStepMembership).OwnedRelatedElement.Add(secondStep); - Assert.That(() => twoStepMembership.ComputeTransitionFeature(), Throws.TypeOf()); + Assert.That(() => twoStepMembership.ComputeTransitionFeature(), Throws.TypeOf()); // Mixed-type owned related elements: exactly one IStep alongside a non-IStep (Namespace). // The OfType() projection MUST pick out the IStep regardless of its position diff --git a/SysML2.NET.Tests/Extend/VariantMembershipExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/VariantMembershipExtensionsTestFixture.cs index c7c2e0d5..c4fb2e48 100644 --- a/SysML2.NET.Tests/Extend/VariantMembershipExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/VariantMembershipExtensionsTestFixture.cs @@ -53,7 +53,7 @@ public void VerifyComputeOwnedVariantUsage() Assert.That(variantMembership.ComputeOwnedVariantUsage(), Is.SameAs(variantUsage)); - // Two IUsages in OwnedRelatedElement → [1..1] violation: throws IncompleteModelException. + // Two IUsages in OwnedRelatedElement → upper-bound violation: throws MultiplicityViolationException. var twoUsageMembership = new VariantMembership(); var firstUsage = new Usage(); var secondUsage = new Usage(); @@ -61,7 +61,7 @@ public void VerifyComputeOwnedVariantUsage() ((IContainedRelationship)twoUsageMembership).OwnedRelatedElement.Add(firstUsage); ((IContainedRelationship)twoUsageMembership).OwnedRelatedElement.Add(secondUsage); - Assert.That(() => twoUsageMembership.ComputeOwnedVariantUsage(), Throws.TypeOf()); + Assert.That(() => twoUsageMembership.ComputeOwnedVariantUsage(), Throws.TypeOf()); // Mixed-type owned related elements: exactly one IUsage alongside a non-IUsage (Namespace). // The OfType() projection MUST pick out the IUsage regardless of its position diff --git a/SysML2.NET.Tests/Extend/VerificationCaseUsageExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/VerificationCaseUsageExtensionsTestFixture.cs index 6f14d0b2..cd3063e9 100644 --- a/SysML2.NET.Tests/Extend/VerificationCaseUsageExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/VerificationCaseUsageExtensionsTestFixture.cs @@ -29,6 +29,7 @@ namespace SysML2.NET.Tests.Extend using SysML2.NET.Core.POCO.Systems.DefinitionAndUsage; using SysML2.NET.Core.POCO.Systems.Requirements; using SysML2.NET.Core.POCO.Systems.VerificationCases; + using SysML2.NET.Exceptions; using SysML2.NET.Extensions; [TestFixture] @@ -42,20 +43,7 @@ public void VerifyComputeVerificationCaseDefinition() var verificationCaseUsage = new VerificationCaseUsage(); // Empty case: no FeatureTyping whose Type is an IVerificationCaseDefinition → null. - Assert.That(verificationCaseUsage.ComputeVerificationCaseDefinition(), Is.Null); - - // Negative case: FeatureTyping whose Type is a Usage (not IVerificationCaseDefinition) — no match → null. - var nonDefinitionTyping = new FeatureTyping { Type = new Usage() }; - verificationCaseUsage.AssignOwnership(nonDefinitionTyping); - - Assert.That(verificationCaseUsage.ComputeVerificationCaseDefinition(), Is.Null); - - // Populated case: FeatureTyping whose Type is a VerificationCaseDefinition → returns the VerificationCaseDefinition. - var verificationCaseDefinition = new VerificationCaseDefinition(); - var verificationCaseDefinitionTyping = new FeatureTyping { Type = verificationCaseDefinition }; - verificationCaseUsage.AssignOwnership(verificationCaseDefinitionTyping); - - Assert.That(verificationCaseUsage.ComputeVerificationCaseDefinition(), Is.SameAs(verificationCaseDefinition)); + Assert.That(verificationCaseUsage.ComputeVerificationCaseDefinition, Throws.TypeOf()); } [Test] diff --git a/SysML2.NET.Tests/Extend/ViewRenderingMembershipExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/ViewRenderingMembershipExtensionsTestFixture.cs index 0a6a5c4c..894edb16 100644 --- a/SysML2.NET.Tests/Extend/ViewRenderingMembershipExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/ViewRenderingMembershipExtensionsTestFixture.cs @@ -61,14 +61,14 @@ public void VerifyComputeOwnedRendering() Assert.That(renderingMembership.ComputeOwnedRendering(), Is.SameAs(renderingUsage)); - // Two IRenderingUsage in OwnedRelatedElement → [1..1] violation: throws IncompleteModelException. + // Two IRenderingUsage in OwnedRelatedElement → upper-bound violation: throws MultiplicityViolationException. var twoRenderingMembership = new ViewRenderingMembership(); var firstRendering = new RenderingUsage(); var secondRendering = new RenderingUsage(); ((IContainedRelationship)twoRenderingMembership).OwnedRelatedElement.Add(firstRendering); ((IContainedRelationship)twoRenderingMembership).OwnedRelatedElement.Add(secondRendering); - Assert.That(() => twoRenderingMembership.ComputeOwnedRendering(), Throws.TypeOf()); + Assert.That(() => twoRenderingMembership.ComputeOwnedRendering(), Throws.TypeOf()); // Mixed: annotation (Namespace) alongside a single IRenderingUsage — the type filter // picks out the RenderingUsage regardless of its position. diff --git a/SysML2.NET.Tests/Extend/ViewUsageExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/ViewUsageExtensionsTestFixture.cs index 332d594c..5f0f5345 100644 --- a/SysML2.NET.Tests/Extend/ViewUsageExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/ViewUsageExtensionsTestFixture.cs @@ -31,6 +31,7 @@ namespace SysML2.NET.Tests.Extend using SysML2.NET.Core.POCO.Root.Namespaces; using SysML2.NET.Core.POCO.Systems.Requirements; using SysML2.NET.Core.POCO.Systems.Views; + using SysML2.NET.Exceptions; using SysML2.NET.Extensions; [TestFixture] @@ -60,7 +61,7 @@ public void VerifyComputeExposedElement() var membershipExpose = new MembershipExpose(); viewUsage.AssignOwnership(membershipExpose); - Assert.That(() => viewUsage.ComputeExposedElement(), Throws.TypeOf()); + Assert.That(viewUsage.ComputeExposedElement, Throws.TypeOf()); } [Test] @@ -176,28 +177,7 @@ public void VerifyComputeViewDefinition() var viewUsage = new ViewUsage(); // Empty: no FeatureTyping → null. - Assert.That(viewUsage.ComputeViewDefinition(), Is.Null); - - // Negative: FeatureTyping whose Type is a non-ViewDefinition → null. - var nonViewDefinitionType = new Feature(); - var typingToNonViewDefinition = new FeatureTyping { Type = nonViewDefinitionType }; - viewUsage.AssignOwnership(typingToNonViewDefinition); - - Assert.That(viewUsage.ComputeViewDefinition(), Is.Null); - - // Positive: FeatureTyping whose Type is a ViewDefinition → returned. - var viewDefinition1 = new ViewDefinition(); - var typingToViewDefinition1 = new FeatureTyping { Type = viewDefinition1 }; - viewUsage.AssignOwnership(typingToViewDefinition1); - - Assert.That(viewUsage.ComputeViewDefinition(), Is.SameAs(viewDefinition1)); - - // Multiple: two ViewDefinition typings → first returned (FirstOrDefault). - var viewDefinition2 = new ViewDefinition(); - var typingToViewDefinition2 = new FeatureTyping { Type = viewDefinition2 }; - viewUsage.AssignOwnership(typingToViewDefinition2); - - Assert.That(viewUsage.ComputeViewDefinition(), Is.SameAs(viewDefinition1)); + Assert.That(viewUsage.ComputeViewDefinition, Throws.TypeOf()); } [Test] diff --git a/SysML2.NET.Tests/Extend/ViewpointUsageExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/ViewpointUsageExtensionsTestFixture.cs index 86db1ef0..e1d15626 100644 --- a/SysML2.NET.Tests/Extend/ViewpointUsageExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/ViewpointUsageExtensionsTestFixture.cs @@ -25,10 +25,9 @@ namespace SysML2.NET.Tests.Extend using NUnit.Framework; using SysML2.NET.Core.POCO.Core.Features; - using SysML2.NET.Core.POCO.Core.Types; - using SysML2.NET.Core.POCO.Systems.Constraints; using SysML2.NET.Core.POCO.Systems.Requirements; using SysML2.NET.Core.POCO.Systems.Views; + using SysML2.NET.Exceptions; using SysML2.NET.Extensions; [TestFixture] @@ -42,28 +41,7 @@ public void VerifyComputeViewpointDefinition() var viewpointUsage = new ViewpointUsage(); // Empty: no OwnedRelationship → null. - Assert.That(viewpointUsage.ComputeViewpointDefinition(), Is.Null); - - // Negative: FeatureTyping whose Type is a non-ViewpointDefinition (Feature) → null. - var nonViewpointType = new Feature(); - var typingToNonViewpoint = new FeatureTyping { Type = nonViewpointType }; - viewpointUsage.AssignOwnership(typingToNonViewpoint); - - Assert.That(viewpointUsage.ComputeViewpointDefinition(), Is.Null); - - // Positive: FeatureTyping whose Type is a ViewpointDefinition → returned. - var viewpointDefinition = new ViewpointDefinition(); - var typingToViewpointDefinition = new FeatureTyping { Type = viewpointDefinition }; - viewpointUsage.AssignOwnership(typingToViewpointDefinition); - - Assert.That(viewpointUsage.ComputeViewpointDefinition(), Is.SameAs(viewpointDefinition)); - - // Multiple typings: second ViewpointDefinition present; FirstOrDefault returns the first match. - var viewpointDefinition2 = new ViewpointDefinition(); - var typingToViewpointDefinition2 = new FeatureTyping { Type = viewpointDefinition2 }; - viewpointUsage.AssignOwnership(typingToViewpointDefinition2); - - Assert.That(viewpointUsage.ComputeViewpointDefinition(), Is.SameAs(viewpointDefinition)); + Assert.That(viewpointUsage.ComputeViewpointDefinition, Throws.TypeOf()); } [Test] diff --git a/SysML2.NET.Tests/Extensions/ElementExtensionsTestFixture.cs b/SysML2.NET.Tests/Extensions/ElementExtensionsTestFixture.cs index 50164e9d..02e72fb6 100644 --- a/SysML2.NET.Tests/Extensions/ElementExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extensions/ElementExtensionsTestFixture.cs @@ -1,7 +1,7 @@ // ------------------------------------------------------------------------------------------------- // // -// Copyright 2022-2026 Starion Group S.A. +// Copyright (C) 2022-2026 Starion Group S.A. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,6 +21,8 @@ namespace SysML2.NET.Tests.Extensions { using System; + using System.Collections.Generic; + using System.Linq; using NUnit.Framework; @@ -30,15 +32,22 @@ namespace SysML2.NET.Tests.Extensions using SysML2.NET.Core.POCO.Kernel.FeatureValues; using SysML2.NET.Core.POCO.Kernel.Packages; using SysML2.NET.Core.POCO.Root.Annotations; + using SysML2.NET.Core.POCO.Root.Elements; using SysML2.NET.Core.POCO.Root.Namespaces; using SysML2.NET.Core.POCO.Systems.Parts; + using SysML2.NET.Exceptions; using SysML2.NET.Extensions; + using ElementExtensions = SysML2.NET.Extensions.ElementExtensions; using IContainedRelationship = SysML2.NET.Core.POCO.Root.Elements.IContainedRelationship; [TestFixture] public class ElementExtensionsTestFixture { + private static readonly string[] OneElementSequence = ["only"]; + private static readonly string[] TwoElementSequence = ["first", "second"]; + private static readonly string[] ThreeElementSequence = ["a", "b", "c"]; + private PartDefinition source; private FeatureMembership bridgeRelationship; private Specialization referenceBridgeRelationship; @@ -49,6 +58,7 @@ public void SetUp() { this.source = new PartDefinition(); this.bridgeRelationship = new FeatureMembership(); + // Specialization's owner-side narrowing is IType — PartDefinition IS-A IType, so the fixture // source can validly own the reference bridge under the stricter owner-type guard. this.referenceBridgeRelationship = new Specialization(); @@ -126,6 +136,30 @@ public void AssignOwnership_WithBridgeEqualsTarget_ThrowsInvalidOperationExcepti Throws.TypeOf()); } + [Test] + public void AssignOwnership_WithCompatibleTargetType_AssignsOwnershipCorrectly() + { + // FeatureValue's owner-side narrowing is IFeature, so the owner must be a Feature. + var featureValueOwner = new Feature(); + var featureValue = new FeatureValue(); + var literal = new LiteralBoolean(); + + var annotationOwner = new PartDefinition(); + var annotation = new Annotation(); + var comment = new Comment(); + + using (Assert.EnterMultipleScope()) + { + Assert.That( + () => featureValueOwner.AssignOwnership(featureValue, literal), + Throws.Nothing); + + Assert.That( + () => annotationOwner.AssignOwnership(annotation, comment), + Throws.Nothing); + } + } + [Test] public void AssignOwnership_WithContainmentCycle_ThrowsInvalidOperationException() { @@ -161,20 +195,6 @@ public void AssignOwnership_WithContainmentCycle_ThrowsInvalidOperationException } } - [Test] - public void AssignOwnership_WithMembershipAndNonNamespaceSource_ThrowsInvalidOperationException() - { - // Comment is neither an INamespace nor an IType, so a generic OwningMembership owner check - // (requires INamespace) fires. - var nonNamespaceSource = new Comment(); - var owningMembership = new OwningMembership(); - var memberElement = new Feature(); - - Assert.That( - () => nonNamespaceSource.AssignOwnership(owningMembership, memberElement), - Throws.TypeOf().With.Message.Contains("INamespace")); - } - [Test] public void AssignOwnership_WithIncompatibleOwnerType_ThrowsInvalidOperationException() { @@ -190,53 +210,6 @@ public void AssignOwnership_WithIncompatibleOwnerType_ThrowsInvalidOperationExce Throws.TypeOf().With.Message.Contains("not a valid containment owner")); } - [Test] - public void AssignOwnership_WithNullBridge_ThrowsArgumentNullException() - { - Assert.That( - () => this.source.AssignOwnership(null, this.target), - Throws.TypeOf()); - } - - [Test] - public void AssignOwnership_WithNullSource_ThrowsArgumentNullException() - { - Assert.That( - () => ElementExtensions.AssignOwnership(null, this.bridgeRelationship, this.target), - Throws.TypeOf()); - } - - [Test] - public void AssignOwnership_WithNullTarget_ThrowsArgumentNullException() - { - Assert.That( - () => this.source.AssignOwnership(this.bridgeRelationship, null), - Throws.TypeOf()); - } - - [Test] - public void AssignOwnership_WithCompatibleTargetType_AssignsOwnershipCorrectly() - { - // FeatureValue's owner-side narrowing is IFeature, so the owner must be a Feature. - var featureValueOwner = new Feature(); - var featureValue = new FeatureValue(); - var literal = new LiteralBoolean(); - - var annotationOwner = new PartDefinition(); - var annotation = new Annotation(); - var comment = new Comment(); - - using (Assert.EnterMultipleScope()) - { - Assert.That( - () => featureValueOwner.AssignOwnership(featureValue, literal), - Throws.Nothing); - Assert.That( - () => annotationOwner.AssignOwnership(annotation, comment), - Throws.Nothing); - } - } - [Test] public void AssignOwnership_WithIncompatibleTargetType_ThrowsInvalidOperationException() { @@ -251,6 +224,7 @@ public void AssignOwnership_WithIncompatibleTargetType_ThrowsInvalidOperationExc // The owner (a Feature) is valid since FeatureValue requires an IFeature owner. var featureValueOwner = new Feature(); var featureValue = new FeatureValue(); + Assert.That( () => featureValueOwner.AssignOwnership(featureValue, new Feature()), Throws.TypeOf().With.Message.Contains("not a valid containment target")); @@ -258,12 +232,51 @@ public void AssignOwnership_WithIncompatibleTargetType_ThrowsInvalidOperationExc // Annotation requires an IAnnotatingElement target; Feature is not an IAnnotatingElement. var annotationOwner = new PartDefinition(); var annotation = new Annotation(); + Assert.That( () => annotationOwner.AssignOwnership(annotation, new Feature()), Throws.TypeOf().With.Message.Contains("not a valid containment target")); } } + [Test] + public void AssignOwnership_WithMembershipAndNonNamespaceSource_ThrowsInvalidOperationException() + { + // Comment is neither an INamespace nor an IType, so a generic OwningMembership owner check + // (requires INamespace) fires. + var nonNamespaceSource = new Comment(); + var owningMembership = new OwningMembership(); + var memberElement = new Feature(); + + Assert.That( + () => nonNamespaceSource.AssignOwnership(owningMembership, memberElement), + Throws.TypeOf().With.Message.Contains("INamespace")); + } + + [Test] + public void AssignOwnership_WithNullBridge_ThrowsArgumentNullException() + { + Assert.That( + () => this.source.AssignOwnership(null, this.target), + Throws.TypeOf()); + } + + [Test] + public void AssignOwnership_WithNullSource_ThrowsArgumentNullException() + { + Assert.That( + () => ElementExtensions.AssignOwnership(null, this.bridgeRelationship, this.target), + Throws.TypeOf()); + } + + [Test] + public void AssignOwnership_WithNullTarget_ThrowsArgumentNullException() + { + Assert.That( + () => this.source.AssignOwnership(this.bridgeRelationship, null), + Throws.TypeOf()); + } + [Test] public void AssignOwnership_WithReferenceOnlyRelationship_ThrowsInvalidOperationException() { @@ -292,5 +305,134 @@ public void AssignOwnership_WithValidParameters_AssignsOwnershipCorrectly() Assert.That(this.bridgeRelationship.OwnedRelatedElement, Does.Contain(this.target)); } } + + [Test] + public void VerifySingleStrictWithOfTypeFilter() + { + // IEnumerable overload (with implicit OfType): used when the source is wider than + // the desired result type — bundles OfType() + SingleStrict in one call. + + // Empty input → IncompleteModelException (lower-bound violation; the [1..1] property is missing). + Assert.That( + () => new List().SingleStrict("subject"), + Throws.TypeOf()); + + // Only non-matching elements → IncompleteModelException (no IFeature match). + IReadOnlyList nonMatchingOnly = new List { new Comment(), new PartDefinition() }; + + Assert.That( + () => nonMatchingOnly.SingleStrict("subject"), + Throws.TypeOf()); + + // Exactly one match among non-matching siblings → returns the match (position-agnostic). + var feature = new Feature(); + IReadOnlyList singleMatch = new List { new Comment(), feature, new PartDefinition() }; + Assert.That(singleMatch.SingleStrict("subject"), Is.SameAs(feature)); + + // Two matches → MultiplicityViolationException (upper-bound violation). + IReadOnlyList twoMatches = new List { new Feature(), new Feature() }; + + Assert.That( + () => twoMatches.SingleStrict("subject"), + Throws.TypeOf()); + + // Three matches → MultiplicityViolationException (early-exit not regressed). + IReadOnlyList threeMatches = new List { new Feature(), new Feature(), new Feature() }; + + Assert.That( + () => threeMatches.SingleStrict("subject"), + Throws.TypeOf()); + } + + [Test] + public void VerifySingleOrDefaultStrictWithOfTypeFilter() + { + // IEnumerable overload (with implicit OfType): used when the source is wider than + // the desired result type — bundles OfType() + SingleOrDefaultStrict in one call. + + // Empty input → null (lower bound is 0; this is the only difference vs SingleStrict). + Assert.That( + new List().SingleOrDefaultStrict("subject"), + Is.Null); + + // Only non-matching elements → null. + IReadOnlyList nonMatchingOnly = new List { new Comment(), new PartDefinition() }; + Assert.That(nonMatchingOnly.SingleOrDefaultStrict("subject"), Is.Null); + + // Exactly one match among non-matching siblings → returns the match (position-agnostic). + var feature = new Feature(); + IReadOnlyList singleMatch = new List { new Comment(), feature, new PartDefinition() }; + Assert.That(singleMatch.SingleOrDefaultStrict("subject"), Is.SameAs(feature)); + + // Two matches → MultiplicityViolationException (upper-bound violation against the [0..1] property). + IReadOnlyList twoMatches = new List { new Feature(), new Feature() }; + + Assert.That( + () => twoMatches.SingleOrDefaultStrict("subject"), + Throws.TypeOf()); + + // Three matches → MultiplicityViolationException (early-exit not regressed). + IReadOnlyList threeMatches = new List { new Feature(), new Feature(), new Feature() }; + + Assert.That( + () => threeMatches.SingleOrDefaultStrict("subject"), + Throws.TypeOf()); + } + + [Test] + public void VerifySingleStrict() + { + // Empty sequence → IncompleteModelException (lower-bound violation; the [1..1] property is missing). + Assert.That( + () => Enumerable.Empty().SingleStrict("subject"), + Throws.TypeOf()); + + // Exactly one element → returns it. + Assert.That(OneElementSequence.SingleStrict("subject"), Is.EqualTo("only")); + + // Two elements → MultiplicityViolationException (upper-bound violation). + Assert.That( + () => TwoElementSequence.SingleStrict("subject"), + Throws.TypeOf()); + + // Three elements → MultiplicityViolationException (early-exit not regressed). + Assert.That( + () => ThreeElementSequence.SingleStrict("subject"), + Throws.TypeOf()); + + // Explicit OfType() chain — same contract as the IEnumerable overload SingleStrict. + IReadOnlyList threeFeatures = new List { new Feature(), new Feature(), new Feature() }; + + Assert.That( + () => threeFeatures.OfType().SingleStrict("subject"), + Throws.TypeOf()); + } + + [Test] + public void VerifySingleOrDefaultStrict() + { + // Empty sequence → null (lower bound is 0; the only difference vs SingleStrict). + Assert.That(Enumerable.Empty().SingleOrDefaultStrict("subject"), Is.Null); + + // Exactly one element → returns it. + Assert.That(OneElementSequence.SingleOrDefaultStrict("subject"), Is.EqualTo("only")); + + // Two elements → MultiplicityViolationException (upper-bound violation against the [0..1] property). + Assert.That( + () => TwoElementSequence.SingleOrDefaultStrict("subject"), + Throws.TypeOf()); + + // Three elements → MultiplicityViolationException (early-exit not regressed). + Assert.That( + () => ThreeElementSequence.SingleOrDefaultStrict("subject"), + Throws.TypeOf()); + + // Explicit OfType() chain — same contract as the IEnumerable overload SingleOrDefaultStrict. + IReadOnlyList twoFeatures = new List { new Feature(), new Feature() }; + + Assert.That( + () => twoFeatures.OfType().SingleOrDefaultStrict("subject"), + Throws.TypeOf()); + } } } diff --git a/SysML2.NET/Exceptions/MultiplicityViolationException.cs b/SysML2.NET/Exceptions/MultiplicityViolationException.cs new file mode 100644 index 00000000..12af2546 --- /dev/null +++ b/SysML2.NET/Exceptions/MultiplicityViolationException.cs @@ -0,0 +1,55 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Exceptions +{ + using System; + + /// + /// The is thrown when a SysML 2 model carries + /// more elements than the upper bound of a derived property allows (e.g. 2+ matches against + /// a [0..1] or [1..1] derived reference). + /// + /// + /// Contrast with , which signals the opposite — the + /// model is missing a required element against the lower bound of a property (e.g. 0 + /// matches against a [1..1] derived reference). + /// + public class MultiplicityViolationException : Exception + { + /// Initializes a new instance of the class. + public MultiplicityViolationException() + { + } + + /// Initializes a new instance of the class with a specified error message. + /// The message that describes the error. + public MultiplicityViolationException(string message) : base(message) + { + } + + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. + public MultiplicityViolationException(string message, Exception innerException) : base(message, innerException) + { + } + } +} diff --git a/SysML2.NET/Extend/ActorMembershipExtensions.cs b/SysML2.NET/Extend/ActorMembershipExtensions.cs index 272a42b2..8ab7bbac 100644 --- a/SysML2.NET/Extend/ActorMembershipExtensions.cs +++ b/SysML2.NET/Extend/ActorMembershipExtensions.cs @@ -56,7 +56,7 @@ internal static IPartUsage ComputeOwnedActorParameter(this IActorMembership acto throw new ArgumentNullException(nameof(actorMembershipSubject)); } - return actorMembershipSubject.OwnedRelatedElement.RequireSingleOfType(nameof(actorMembershipSubject)); + return actorMembershipSubject.OwnedRelatedElement.SingleStrict(nameof(actorMembershipSubject)); } } diff --git a/SysML2.NET/Extend/AnalysisCaseUsageExtensions.cs b/SysML2.NET/Extend/AnalysisCaseUsageExtensions.cs index 3a5947f6..1a4bc157 100644 --- a/SysML2.NET/Extend/AnalysisCaseUsageExtensions.cs +++ b/SysML2.NET/Extend/AnalysisCaseUsageExtensions.cs @@ -57,6 +57,8 @@ namespace SysML2.NET.Core.POCO.Systems.AnalysisCases using SysML2.NET.Core.POCO.Systems.UseCases; using SysML2.NET.Core.POCO.Systems.VerificationCases; using SysML2.NET.Core.POCO.Systems.Views; + using SysML2.NET.Exceptions; + using SysML2.NET.Extensions; /// /// The class provides extensions methods for @@ -65,23 +67,29 @@ namespace SysML2.NET.Core.POCO.Systems.AnalysisCases internal static class AnalysisCaseUsageExtensions { /// - /// Computes the derived property. + /// Computes the derived analysisCaseDefinition property: the + /// targeted by the single + /// owned by . /// /// /// The subject /// /// - /// the computed result + /// The matching , or null when no such typing exists. /// + /// + /// Thrown when is null. + /// + /// + /// Thrown when more than one targets an + /// (upper-bound violation against the derived + /// [0..1] property). + /// internal static IAnalysisCaseDefinition ComputeAnalysisCaseDefinition(this IAnalysisCaseUsage analysisCaseUsageSubject) { return analysisCaseUsageSubject == null ? throw new ArgumentNullException(nameof(analysisCaseUsageSubject)) - : analysisCaseUsageSubject.OwnedRelationship - .OfType() - .Select(featureTyping => featureTyping.Type) - .OfType() - .FirstOrDefault(); + : analysisCaseUsageSubject.definition.SingleOrDefaultStrict(nameof(analysisCaseUsageSubject)); } /// diff --git a/SysML2.NET/Extend/BooleanExpressionExtensions.cs b/SysML2.NET/Extend/BooleanExpressionExtensions.cs index 77b37368..4d002a88 100644 --- a/SysML2.NET/Extend/BooleanExpressionExtensions.cs +++ b/SysML2.NET/Extend/BooleanExpressionExtensions.cs @@ -21,9 +21,12 @@ namespace SysML2.NET.Core.POCO.Kernel.Functions { using System; - using System.Diagnostics.CodeAnalysis; using System.Linq; + using SysML2.NET.Core.POCO.Core.Types; + using SysML2.NET.Exceptions; + using SysML2.NET.Extensions; + /// /// The class provides extensions methods for /// the interface @@ -31,20 +34,27 @@ namespace SysML2.NET.Core.POCO.Kernel.Functions internal static class BooleanExpressionExtensions { /// - /// Computes the derived property. + /// Computes the derived predicate property: the that is the + /// single type of this . /// /// /// The subject /// /// - /// the computed result + /// The matching , or null when no such type exists. /// - [ExcludeFromCodeCoverage] + /// + /// Thrown when is null. + /// + /// + /// Thrown when more than one on the subject is an + /// (upper-bound violation against the derived [0..1] property). + /// internal static IPredicate ComputePredicate(this IBooleanExpression booleanExpressionSubject) { return booleanExpressionSubject == null ? throw new ArgumentNullException(nameof(booleanExpressionSubject)) - : booleanExpressionSubject.type.OfType().FirstOrDefault(); + : booleanExpressionSubject.type.SingleOrDefaultStrict(nameof(booleanExpressionSubject)); } } } diff --git a/SysML2.NET/Extend/CalculationUsageExtensions.cs b/SysML2.NET/Extend/CalculationUsageExtensions.cs index f2765207..18fcd4de 100644 --- a/SysML2.NET/Extend/CalculationUsageExtensions.cs +++ b/SysML2.NET/Extend/CalculationUsageExtensions.cs @@ -57,6 +57,8 @@ namespace SysML2.NET.Core.POCO.Systems.Calculations using SysML2.NET.Core.POCO.Systems.UseCases; using SysML2.NET.Core.POCO.Systems.VerificationCases; using SysML2.NET.Core.POCO.Systems.Views; + using SysML2.NET.Exceptions; + using SysML2.NET.Extensions; /// /// The class provides extensions methods for @@ -65,23 +67,28 @@ namespace SysML2.NET.Core.POCO.Systems.Calculations internal static class CalculationUsageExtensions { /// - /// Computes the derived property. + /// Computes the derived calculationDefinition property: the + /// targeted by the single owned by + /// . /// /// /// The subject /// /// - /// the computed result + /// The matching , or null when no such typing exists. /// + /// + /// Thrown when is null. + /// + /// + /// Thrown when more than one targets an + /// (upper-bound violation against the derived [0..1] property). + /// internal static IFunction ComputeCalculationDefinition(this ICalculationUsage calculationUsageSubject) { return calculationUsageSubject == null ? throw new ArgumentNullException(nameof(calculationUsageSubject)) - : calculationUsageSubject.OwnedRelationship - .OfType() - .Select(featureTyping => featureTyping.Type) - .OfType() - .FirstOrDefault(); + : calculationUsageSubject.definition.SingleOrDefaultStrict(nameof(calculationUsageSubject)); } /// diff --git a/SysML2.NET/Extend/CaseUsageExtensions.cs b/SysML2.NET/Extend/CaseUsageExtensions.cs index 6b461a78..d8628daf 100644 --- a/SysML2.NET/Extend/CaseUsageExtensions.cs +++ b/SysML2.NET/Extend/CaseUsageExtensions.cs @@ -57,6 +57,8 @@ namespace SysML2.NET.Core.POCO.Systems.Cases using SysML2.NET.Core.POCO.Systems.UseCases; using SysML2.NET.Core.POCO.Systems.VerificationCases; using SysML2.NET.Core.POCO.Systems.Views; + using SysML2.NET.Exceptions; + using SysML2.NET.Extensions; /// /// The class provides extensions methods for @@ -89,23 +91,29 @@ internal static List ComputeActorParameter(this ICaseUsage caseUsage } /// - /// Computes the derived property. + /// Computes the derived caseDefinition property: the + /// targeted by the single owned by + /// . /// /// /// The subject /// /// - /// the computed result + /// The matching , or null when no such typing exists. /// + /// + /// Thrown when is null. + /// + /// + /// Thrown when more than one targets an + /// (upper-bound violation against the derived + /// [0..1] property). + /// internal static ICaseDefinition ComputeCaseDefinition(this ICaseUsage caseUsageSubject) { return caseUsageSubject == null ? throw new ArgumentNullException(nameof(caseUsageSubject)) - : caseUsageSubject.OwnedRelationship - .OfType() - .Select(featureTyping => featureTyping.Type) - .OfType() - .FirstOrDefault(); + : caseUsageSubject.definition.SingleOrDefaultStrict(nameof(caseUsageSubject)); } /// diff --git a/SysML2.NET/Extend/ConstraintUsageExtensions.cs b/SysML2.NET/Extend/ConstraintUsageExtensions.cs index 25e1d6be..e529610f 100644 --- a/SysML2.NET/Extend/ConstraintUsageExtensions.cs +++ b/SysML2.NET/Extend/ConstraintUsageExtensions.cs @@ -57,6 +57,8 @@ namespace SysML2.NET.Core.POCO.Systems.Constraints using SysML2.NET.Core.POCO.Systems.UseCases; using SysML2.NET.Core.POCO.Systems.VerificationCases; using SysML2.NET.Core.POCO.Systems.Views; + using SysML2.NET.Exceptions; + using SysML2.NET.Extensions; /// /// The class provides extensions methods for @@ -65,23 +67,28 @@ namespace SysML2.NET.Core.POCO.Systems.Constraints internal static class ConstraintUsageExtensions { /// - /// Computes the derived property. + /// Computes the derived constraintDefinition property: the + /// targeted by the single owned by + /// . /// /// /// The subject /// /// - /// the computed result + /// The matching , or null when no such typing exists. /// + /// + /// Thrown when is null. + /// + /// + /// Thrown when more than one targets an + /// (upper-bound violation against the derived [0..1] property). + /// internal static IPredicate ComputeConstraintDefinition(this IConstraintUsage constraintUsageSubject) { return constraintUsageSubject == null ? throw new ArgumentNullException(nameof(constraintUsageSubject)) - : constraintUsageSubject.OwnedRelationship - .OfType() - .Select(featureTyping => featureTyping.Type) - .OfType() - .FirstOrDefault(); + : constraintUsageSubject.definition.SingleOrDefaultStrict(nameof(constraintUsageSubject)); } /// diff --git a/SysML2.NET/Extend/ElementFilterMembershipExtensions.cs b/SysML2.NET/Extend/ElementFilterMembershipExtensions.cs index b66a5df9..57236a33 100644 --- a/SysML2.NET/Extend/ElementFilterMembershipExtensions.cs +++ b/SysML2.NET/Extend/ElementFilterMembershipExtensions.cs @@ -47,7 +47,7 @@ internal static IExpression ComputeCondition(this IElementFilterMembership eleme throw new ArgumentNullException(nameof(elementFilterMembershipSubject)); } - return elementFilterMembershipSubject.OwnedRelatedElement.RequireSingleOfType(nameof(elementFilterMembershipSubject)); + return elementFilterMembershipSubject.OwnedRelatedElement.SingleStrict(nameof(elementFilterMembershipSubject)); } } } diff --git a/SysML2.NET/Extend/EndFeatureMembershipExtensions.cs b/SysML2.NET/Extend/EndFeatureMembershipExtensions.cs index 1e5ef201..ecf539ae 100644 --- a/SysML2.NET/Extend/EndFeatureMembershipExtensions.cs +++ b/SysML2.NET/Extend/EndFeatureMembershipExtensions.cs @@ -46,7 +46,7 @@ internal static IFeature ComputeOwnedMemberFeature(this IEndFeatureMembership en throw new ArgumentNullException(nameof(endFeatureMembershipSubject)); } - return endFeatureMembershipSubject.OwnedRelatedElement.RequireSingleOfType(nameof(endFeatureMembershipSubject)); + return endFeatureMembershipSubject.OwnedRelatedElement.SingleStrict(nameof(endFeatureMembershipSubject)); } } } diff --git a/SysML2.NET/Extend/ExpressionExtensions.cs b/SysML2.NET/Extend/ExpressionExtensions.cs index a2289d5d..6f9eea3c 100644 --- a/SysML2.NET/Extend/ExpressionExtensions.cs +++ b/SysML2.NET/Extend/ExpressionExtensions.cs @@ -26,8 +26,11 @@ namespace SysML2.NET.Core.POCO.Kernel.Functions using SysML2.NET.Core.Core.Types; using SysML2.NET.Core.POCO.Core.Features; + using SysML2.NET.Core.POCO.Core.Types; using SysML2.NET.Core.POCO.Kernel.Expressions; using SysML2.NET.Core.POCO.Root.Elements; + using SysML2.NET.Exceptions; + using SysML2.NET.Extensions; /// /// The class provides extensions methods for @@ -36,19 +39,27 @@ namespace SysML2.NET.Core.POCO.Kernel.Functions internal static class ExpressionExtensions { /// - /// Computes the derived property. + /// Computes the derived function property: the that is the + /// single type of this . /// /// /// The subject /// /// - /// the computed result + /// The matching , or null when no such type exists. /// + /// + /// Thrown when is null. + /// + /// + /// Thrown when more than one on the subject is an + /// (upper-bound violation against the derived [0..1] property). + /// internal static IFunction ComputeFunction(this IExpression expressionSubject) { return expressionSubject == null ? throw new ArgumentNullException(nameof(expressionSubject)) - : expressionSubject.type.OfType().FirstOrDefault(); + : expressionSubject.type.SingleOrDefaultStrict(nameof(expressionSubject)); } /// diff --git a/SysML2.NET/Extend/FeatureMembershipExtensions.cs b/SysML2.NET/Extend/FeatureMembershipExtensions.cs index db9dd15f..e247d4f5 100644 --- a/SysML2.NET/Extend/FeatureMembershipExtensions.cs +++ b/SysML2.NET/Extend/FeatureMembershipExtensions.cs @@ -52,7 +52,7 @@ internal static IFeature ComputeOwnedMemberFeature(this IFeatureMembership featu throw new ArgumentNullException(nameof(featureMembershipSubject)); } - return featureMembershipSubject.OwnedRelatedElement.RequireSingleOfType(nameof(featureMembershipSubject)); + return featureMembershipSubject.OwnedRelatedElement.SingleStrict(nameof(featureMembershipSubject)); } /// @@ -70,6 +70,5 @@ internal static IType ComputeOwningType(this IFeatureMembership featureMembershi ? throw new ArgumentNullException(nameof(featureMembershipSubject)) : featureMembershipSubject.OwningRelatedElement as IType; } - } } diff --git a/SysML2.NET/Extend/FeatureValueExtensions.cs b/SysML2.NET/Extend/FeatureValueExtensions.cs index 7c985f69..5e7a0647 100644 --- a/SysML2.NET/Extend/FeatureValueExtensions.cs +++ b/SysML2.NET/Extend/FeatureValueExtensions.cs @@ -69,7 +69,7 @@ internal static IExpression ComputeValue(this IFeatureValue featureValueSubject) throw new ArgumentNullException(nameof(featureValueSubject)); } - return featureValueSubject.OwnedRelatedElement.RequireSingleOfType(nameof(featureValueSubject)); + return featureValueSubject.OwnedRelatedElement.SingleStrict(nameof(featureValueSubject)); } } } diff --git a/SysML2.NET/Extend/FramedConcernMembershipExtensions.cs b/SysML2.NET/Extend/FramedConcernMembershipExtensions.cs index 038bbf87..55503888 100644 --- a/SysML2.NET/Extend/FramedConcernMembershipExtensions.cs +++ b/SysML2.NET/Extend/FramedConcernMembershipExtensions.cs @@ -55,7 +55,7 @@ internal static IConcernUsage ComputeOwnedConcern(this IFramedConcernMembership throw new ArgumentNullException(nameof(framedConcernMembershipSubject)); } - return framedConcernMembershipSubject.OwnedRelatedElement.RequireSingleOfType(nameof(framedConcernMembershipSubject)); + return framedConcernMembershipSubject.OwnedRelatedElement.SingleStrict(nameof(framedConcernMembershipSubject)); } /// diff --git a/SysML2.NET/Extend/MetadataAccessExpressionExtensions.cs b/SysML2.NET/Extend/MetadataAccessExpressionExtensions.cs index a7bed30e..c6a77d93 100644 --- a/SysML2.NET/Extend/MetadataAccessExpressionExtensions.cs +++ b/SysML2.NET/Extend/MetadataAccessExpressionExtensions.cs @@ -60,7 +60,7 @@ internal static IElement ComputeReferencedElement(this IMetadataAccessExpression { if (ownedRelationship is IOwningMembership owningMembership and not IFeatureMembership) { - return owningMembership.OwnedRelatedElement.RequireSingleOfType(nameof(owningMembership)); + return owningMembership.OwnedRelatedElement.SingleStrict(nameof(owningMembership)); } } diff --git a/SysML2.NET/Extend/ObjectiveMembershipExtensions.cs b/SysML2.NET/Extend/ObjectiveMembershipExtensions.cs index 5869ac2f..e99e225b 100644 --- a/SysML2.NET/Extend/ObjectiveMembershipExtensions.cs +++ b/SysML2.NET/Extend/ObjectiveMembershipExtensions.cs @@ -47,7 +47,7 @@ internal static IRequirementUsage ComputeOwnedObjectiveRequirement(this IObjecti throw new ArgumentNullException(nameof(objectiveMembershipSubject)); } - return objectiveMembershipSubject.OwnedRelatedElement.RequireSingleOfType(nameof(objectiveMembershipSubject)); + return objectiveMembershipSubject.OwnedRelatedElement.SingleStrict(nameof(objectiveMembershipSubject)); } } } diff --git a/SysML2.NET/Extend/OwningMembershipExtensions.cs b/SysML2.NET/Extend/OwningMembershipExtensions.cs index b32409bd..b6ca2960 100644 --- a/SysML2.NET/Extend/OwningMembershipExtensions.cs +++ b/SysML2.NET/Extend/OwningMembershipExtensions.cs @@ -50,7 +50,7 @@ internal static IElement ComputeOwnedMemberElement(this IOwningMembership owning throw new ArgumentNullException(nameof(owningMembershipSubject)); } - return owningMembershipSubject.OwnedRelatedElement.RequireSingleOfType(nameof(owningMembershipSubject)); + return owningMembershipSubject.OwnedRelatedElement.SingleStrict(nameof(owningMembershipSubject)); } /// diff --git a/SysML2.NET/Extend/ParameterMembershipExtensions.cs b/SysML2.NET/Extend/ParameterMembershipExtensions.cs index 50fc7d96..6c2b60f3 100644 --- a/SysML2.NET/Extend/ParameterMembershipExtensions.cs +++ b/SysML2.NET/Extend/ParameterMembershipExtensions.cs @@ -54,7 +54,7 @@ internal static IFeature ComputeOwnedMemberParameter(this IParameterMembership p throw new ArgumentNullException(nameof(parameterMembershipSubject)); } - return parameterMembershipSubject.OwnedRelatedElement.RequireSingleOfType(nameof(parameterMembershipSubject)); + return parameterMembershipSubject.OwnedRelatedElement.SingleStrict(nameof(parameterMembershipSubject)); } /// diff --git a/SysML2.NET/Extend/RenderingUsageExtensions.cs b/SysML2.NET/Extend/RenderingUsageExtensions.cs index 5da2be50..0db3bfed 100644 --- a/SysML2.NET/Extend/RenderingUsageExtensions.cs +++ b/SysML2.NET/Extend/RenderingUsageExtensions.cs @@ -56,6 +56,8 @@ namespace SysML2.NET.Core.POCO.Systems.Views using SysML2.NET.Core.POCO.Systems.States; using SysML2.NET.Core.POCO.Systems.UseCases; using SysML2.NET.Core.POCO.Systems.VerificationCases; + using SysML2.NET.Exceptions; + using SysML2.NET.Extensions; /// /// The class provides extensions methods for @@ -64,23 +66,29 @@ namespace SysML2.NET.Core.POCO.Systems.Views internal static class RenderingUsageExtensions { /// - /// Computes the derived property. + /// Computes the derived renderingDefinition property: the + /// targeted by the single + /// owned by . /// /// /// The subject /// /// - /// the computed result + /// The matching , or null when no such typing exists. /// + /// + /// Thrown when is null. + /// + /// + /// Thrown when more than one targets an + /// (upper-bound violation against the derived + /// [0..1] property). + /// internal static IRenderingDefinition ComputeRenderingDefinition(this IRenderingUsage renderingUsageSubject) { return renderingUsageSubject == null ? throw new ArgumentNullException(nameof(renderingUsageSubject)) - : renderingUsageSubject.OwnedRelationship - .OfType() - .Select(featureTyping => featureTyping.Type) - .OfType() - .FirstOrDefault(); + : renderingUsageSubject.definition.SingleOrDefaultStrict(nameof(renderingUsageSubject)); } } diff --git a/SysML2.NET/Extend/RequirementConstraintMembershipExtensions.cs b/SysML2.NET/Extend/RequirementConstraintMembershipExtensions.cs index 3e8d529e..985e469a 100644 --- a/SysML2.NET/Extend/RequirementConstraintMembershipExtensions.cs +++ b/SysML2.NET/Extend/RequirementConstraintMembershipExtensions.cs @@ -47,7 +47,7 @@ internal static IConstraintUsage ComputeOwnedConstraint(this IRequirementConstra throw new ArgumentNullException(nameof(requirementConstraintMembershipSubject)); } - return requirementConstraintMembershipSubject.OwnedRelatedElement.RequireSingleOfType(nameof(requirementConstraintMembershipSubject)); + return requirementConstraintMembershipSubject.OwnedRelatedElement.SingleStrict(nameof(requirementConstraintMembershipSubject)); } /// diff --git a/SysML2.NET/Extend/RequirementUsageExtensions.cs b/SysML2.NET/Extend/RequirementUsageExtensions.cs index 424eecba..88201708 100644 --- a/SysML2.NET/Extend/RequirementUsageExtensions.cs +++ b/SysML2.NET/Extend/RequirementUsageExtensions.cs @@ -26,10 +26,13 @@ namespace SysML2.NET.Core.POCO.Systems.Requirements using SysML2.NET.Core.Systems.Requirements; using SysML2.NET.Core.POCO.Core.Features; + using SysML2.NET.Core.POCO.Core.Types; using SysML2.NET.Core.POCO.Root.Annotations; using SysML2.NET.Core.POCO.Systems.Constraints; using SysML2.NET.Core.POCO.Systems.DefinitionAndUsage; using SysML2.NET.Core.POCO.Systems.Parts; + using SysML2.NET.Exceptions; + using SysML2.NET.Extensions; /// /// The class provides extensions methods for @@ -142,23 +145,29 @@ internal static List ComputeRequiredConstraint(this IRequireme } /// - /// Computes the derived property. + /// Computes the derived requirementDefinition property: the + /// targeted by the single + /// owned by . /// /// /// The subject /// /// - /// the computed result + /// The matching , or null when no such typing exists. /// + /// + /// Thrown when is null. + /// + /// + /// Thrown when more than one targets an + /// (upper-bound violation against the derived + /// [0..1] property). + /// internal static IRequirementDefinition ComputeRequirementDefinition(this IRequirementUsage requirementUsageSubject) { return requirementUsageSubject == null ? throw new ArgumentNullException(nameof(requirementUsageSubject)) - : requirementUsageSubject.OwnedRelationship - .OfType() - .Select(featureTyping => featureTyping.Type) - .OfType() - .FirstOrDefault(); + : requirementUsageSubject.definition.SingleOrDefaultStrict(nameof(requirementUsageSubject)); } /// diff --git a/SysML2.NET/Extend/RequirementVerificationMembershipExtensions.cs b/SysML2.NET/Extend/RequirementVerificationMembershipExtensions.cs index b52d7eef..d6f16116 100644 --- a/SysML2.NET/Extend/RequirementVerificationMembershipExtensions.cs +++ b/SysML2.NET/Extend/RequirementVerificationMembershipExtensions.cs @@ -56,7 +56,7 @@ internal static IRequirementUsage ComputeOwnedRequirement(this IRequirementVerif throw new ArgumentNullException(nameof(requirementVerificationMembershipSubject)); } - return requirementVerificationMembershipSubject.OwnedRelatedElement.RequireSingleOfType(nameof(requirementVerificationMembershipSubject)); + return requirementVerificationMembershipSubject.OwnedRelatedElement.SingleStrict(nameof(requirementVerificationMembershipSubject)); } /// diff --git a/SysML2.NET/Extend/ResultExpressionMembershipExtensions.cs b/SysML2.NET/Extend/ResultExpressionMembershipExtensions.cs index 21e55f1b..437f8e6b 100644 --- a/SysML2.NET/Extend/ResultExpressionMembershipExtensions.cs +++ b/SysML2.NET/Extend/ResultExpressionMembershipExtensions.cs @@ -46,7 +46,7 @@ internal static IExpression ComputeOwnedResultExpression(this IResultExpressionM throw new ArgumentNullException(nameof(resultExpressionMembershipSubject)); } - return resultExpressionMembershipSubject.OwnedRelatedElement.RequireSingleOfType(nameof(resultExpressionMembershipSubject)); + return resultExpressionMembershipSubject.OwnedRelatedElement.SingleStrict(nameof(resultExpressionMembershipSubject)); } } } diff --git a/SysML2.NET/Extend/StakeholderMembershipExtensions.cs b/SysML2.NET/Extend/StakeholderMembershipExtensions.cs index 41809411..c4462a31 100644 --- a/SysML2.NET/Extend/StakeholderMembershipExtensions.cs +++ b/SysML2.NET/Extend/StakeholderMembershipExtensions.cs @@ -47,7 +47,7 @@ internal static IPartUsage ComputeOwnedStakeholderParameter(this IStakeholderMem throw new ArgumentNullException(nameof(stakeholderMembershipSubject)); } - return stakeholderMembershipSubject.OwnedRelatedElement.RequireSingleOfType(nameof(stakeholderMembershipSubject)); + return stakeholderMembershipSubject.OwnedRelatedElement.SingleStrict(nameof(stakeholderMembershipSubject)); } } } diff --git a/SysML2.NET/Extend/StateSubactionMembershipExtensions.cs b/SysML2.NET/Extend/StateSubactionMembershipExtensions.cs index 585b5027..5484d4fc 100644 --- a/SysML2.NET/Extend/StateSubactionMembershipExtensions.cs +++ b/SysML2.NET/Extend/StateSubactionMembershipExtensions.cs @@ -47,7 +47,7 @@ internal static IActionUsage ComputeAction(this IStateSubactionMembership stateS throw new ArgumentNullException(nameof(stateSubactionMembershipSubject)); } - return stateSubactionMembershipSubject.OwnedRelatedElement.RequireSingleOfType(nameof(stateSubactionMembershipSubject)); + return stateSubactionMembershipSubject.OwnedRelatedElement.SingleStrict(nameof(stateSubactionMembershipSubject)); } } } diff --git a/SysML2.NET/Extend/SubjectMembershipExtensions.cs b/SysML2.NET/Extend/SubjectMembershipExtensions.cs index 49b9e4aa..ef6a5303 100644 --- a/SysML2.NET/Extend/SubjectMembershipExtensions.cs +++ b/SysML2.NET/Extend/SubjectMembershipExtensions.cs @@ -48,7 +48,7 @@ internal static IUsage ComputeOwnedSubjectParameter(this ISubjectMembership subj throw new ArgumentNullException(nameof(subjectMembershipSubject)); } - return subjectMembershipSubject.OwnedRelatedElement.RequireSingleOfType(nameof(subjectMembershipSubject)); + return subjectMembershipSubject.OwnedRelatedElement.SingleStrict(nameof(subjectMembershipSubject)); } } diff --git a/SysML2.NET/Extend/TransitionFeatureMembershipExtensions.cs b/SysML2.NET/Extend/TransitionFeatureMembershipExtensions.cs index 5cf7c71d..b7c08bae 100644 --- a/SysML2.NET/Extend/TransitionFeatureMembershipExtensions.cs +++ b/SysML2.NET/Extend/TransitionFeatureMembershipExtensions.cs @@ -55,7 +55,7 @@ internal static IStep ComputeTransitionFeature(this ITransitionFeatureMembership throw new ArgumentNullException(nameof(transitionFeatureMembershipSubject)); } - return transitionFeatureMembershipSubject.OwnedRelatedElement.RequireSingleOfType(nameof(transitionFeatureMembershipSubject)); + return transitionFeatureMembershipSubject.OwnedRelatedElement.SingleStrict(nameof(transitionFeatureMembershipSubject)); } } } diff --git a/SysML2.NET/Extend/VariantMembershipExtensions.cs b/SysML2.NET/Extend/VariantMembershipExtensions.cs index a8b3dd99..2a665eae 100644 --- a/SysML2.NET/Extend/VariantMembershipExtensions.cs +++ b/SysML2.NET/Extend/VariantMembershipExtensions.cs @@ -46,7 +46,7 @@ internal static IUsage ComputeOwnedVariantUsage(this IVariantMembership variantM throw new ArgumentNullException(nameof(variantMembershipSubject)); } - return variantMembershipSubject.OwnedRelatedElement.RequireSingleOfType(nameof(variantMembershipSubject)); + return variantMembershipSubject.OwnedRelatedElement.SingleStrict(nameof(variantMembershipSubject)); } } } diff --git a/SysML2.NET/Extend/VerificationCaseUsageExtensions.cs b/SysML2.NET/Extend/VerificationCaseUsageExtensions.cs index 23a9fcc2..c233b515 100644 --- a/SysML2.NET/Extend/VerificationCaseUsageExtensions.cs +++ b/SysML2.NET/Extend/VerificationCaseUsageExtensions.cs @@ -57,6 +57,8 @@ namespace SysML2.NET.Core.POCO.Systems.VerificationCases using SysML2.NET.Core.POCO.Systems.States; using SysML2.NET.Core.POCO.Systems.UseCases; using SysML2.NET.Core.POCO.Systems.Views; + using SysML2.NET.Exceptions; + using SysML2.NET.Extensions; /// /// The class provides extensions methods for @@ -65,23 +67,29 @@ namespace SysML2.NET.Core.POCO.Systems.VerificationCases internal static class VerificationCaseUsageExtensions { /// - /// Computes the derived property. + /// Computes the derived verificationCaseDefinition property: the + /// targeted by the single + /// owned by . /// /// /// The subject /// /// - /// the computed result + /// The matching , or null when no such typing exists. /// + /// + /// Thrown when is null. + /// + /// + /// Thrown when more than one targets an + /// (upper-bound violation against the derived + /// [0..1] property). + /// internal static IVerificationCaseDefinition ComputeVerificationCaseDefinition(this IVerificationCaseUsage verificationCaseUsageSubject) { return verificationCaseUsageSubject == null ? throw new ArgumentNullException(nameof(verificationCaseUsageSubject)) - : verificationCaseUsageSubject.OwnedRelationship - .OfType() - .Select(featureTyping => featureTyping.Type) - .OfType() - .FirstOrDefault(); + : verificationCaseUsageSubject.definition.SingleOrDefaultStrict(nameof(verificationCaseUsageSubject)); } /// diff --git a/SysML2.NET/Extend/ViewRenderingMembershipExtensions.cs b/SysML2.NET/Extend/ViewRenderingMembershipExtensions.cs index 277f53d5..7df536e6 100644 --- a/SysML2.NET/Extend/ViewRenderingMembershipExtensions.cs +++ b/SysML2.NET/Extend/ViewRenderingMembershipExtensions.cs @@ -54,7 +54,7 @@ internal static IRenderingUsage ComputeOwnedRendering(this IViewRenderingMembers throw new ArgumentNullException(nameof(viewRenderingMembershipSubject)); } - return viewRenderingMembershipSubject.OwnedRelatedElement.RequireSingleOfType(nameof(viewRenderingMembershipSubject)); + return viewRenderingMembershipSubject.OwnedRelatedElement.SingleStrict(nameof(viewRenderingMembershipSubject)); } /// diff --git a/SysML2.NET/Extend/ViewUsageExtensions.cs b/SysML2.NET/Extend/ViewUsageExtensions.cs index 37f995e0..a771f94b 100644 --- a/SysML2.NET/Extend/ViewUsageExtensions.cs +++ b/SysML2.NET/Extend/ViewUsageExtensions.cs @@ -25,12 +25,15 @@ namespace SysML2.NET.Core.POCO.Systems.Views using System.Linq; using SysML2.NET.Core.POCO.Core.Features; + using SysML2.NET.Core.POCO.Core.Types; using SysML2.NET.Core.POCO.Kernel.Functions; using SysML2.NET.Core.POCO.Kernel.Metadata; using SysML2.NET.Core.POCO.Kernel.Packages; using SysML2.NET.Core.POCO.Root.Annotations; using SysML2.NET.Core.POCO.Root.Elements; using SysML2.NET.Core.POCO.Root.Namespaces; + using SysML2.NET.Exceptions; + using SysML2.NET.Extensions; /// /// The class provides extensions methods for @@ -125,23 +128,27 @@ internal static List ComputeViewCondition(this IViewUsage viewUsage } /// - /// Computes the derived property. + /// Computes the derived viewDefinition property: the + /// targeted by the single owned by + /// . /// /// /// The subject /// /// - /// the computed result + /// The matching , or null when no such typing exists. /// + /// + /// Thrown when is null. + /// + /// + /// Thrown when more than one targets an + /// (upper-bound violation against the derived + /// [0..1] property). + /// internal static IViewDefinition ComputeViewDefinition(this IViewUsage viewUsageSubject) { - return viewUsageSubject == null - ? throw new ArgumentNullException(nameof(viewUsageSubject)) - : viewUsageSubject.OwnedRelationship - .OfType() - .Select(featureTyping => featureTyping.Type) - .OfType() - .FirstOrDefault(); + return viewUsageSubject is null ? throw new ArgumentNullException(nameof(viewUsageSubject)) : viewUsageSubject.definition.SingleOrDefaultStrict(nameof(viewUsageSubject)); } /// diff --git a/SysML2.NET/Extend/ViewpointUsageExtensions.cs b/SysML2.NET/Extend/ViewpointUsageExtensions.cs index 2662b0da..8e980dcc 100644 --- a/SysML2.NET/Extend/ViewpointUsageExtensions.cs +++ b/SysML2.NET/Extend/ViewpointUsageExtensions.cs @@ -57,6 +57,8 @@ namespace SysML2.NET.Core.POCO.Systems.Views using SysML2.NET.Core.POCO.Systems.States; using SysML2.NET.Core.POCO.Systems.UseCases; using SysML2.NET.Core.POCO.Systems.VerificationCases; + using SysML2.NET.Exceptions; + using SysML2.NET.Extensions; /// /// The class provides extensions methods for @@ -65,23 +67,29 @@ namespace SysML2.NET.Core.POCO.Systems.Views internal static class ViewpointUsageExtensions { /// - /// Computes the derived property. + /// Computes the derived viewpointDefinition property: the + /// targeted by the single + /// owned by . /// /// /// The subject /// /// - /// the computed result + /// The matching , or null when no such typing exists. /// + /// + /// Thrown when is null. + /// + /// + /// Thrown when more than one targets an + /// (upper-bound violation against the derived + /// [0..1] property). + /// internal static IViewpointDefinition ComputeViewpointDefinition(this IViewpointUsage viewpointUsageSubject) { return viewpointUsageSubject == null ? throw new ArgumentNullException(nameof(viewpointUsageSubject)) - : viewpointUsageSubject.OwnedRelationship - .OfType() - .Select(featureTyping => featureTyping.Type) - .OfType() - .FirstOrDefault(); + : viewpointUsageSubject.definition.SingleOrDefaultStrict(nameof(viewpointUsageSubject)); } /// diff --git a/SysML2.NET/Extensions/ElementExtensions.cs b/SysML2.NET/Extensions/ElementExtensions.cs index 28879ca9..7196fec8 100644 --- a/SysML2.NET/Extensions/ElementExtensions.cs +++ b/SysML2.NET/Extensions/ElementExtensions.cs @@ -223,72 +223,186 @@ private static void AssignOwnershipCore(IElement source, IRelationship bridgeRel } /// - /// Returns the single element of type from . + /// Returns the single element from , throwing + /// if the source is empty and + /// if it contains more than one element. /// - /// - /// The narrowed element type the caller is interested in (e.g. IUsage, IFeature, - /// IStep, IExpression). Pass for non-narrowing - /// single-element extraction (e.g. OwningMembership::ownedMemberElement). - /// - /// - /// The source [0..*] storage collection — typically - /// or - /// (assignable thanks to covariance). + /// The element type of the sequence (reference type). + /// + /// The (already-projected) sequence of candidate matches — typically the tail of a LINQ chain + /// such as subject.OwnedRelationship.OfType<IFeatureTyping>().Select(t => t.Type).OfType<TFinal>(). /// /// /// The name of the subject parameter in the calling method (typically nameof(...)), /// embedded in the diagnostic message produced on a multiplicity violation. /// - /// The single element of type + /// The single element of the sequence. /// - /// Thrown when no element of type is present (missing case), or when - /// more than one is present (multiplicity violation). The exception message distinguishes the - /// two cases so SDK consumers can act on the precise model defect. + /// Thrown when is empty (lower-bound violation — the model is missing + /// the required element). + /// + /// + /// Thrown when contains more than one element (upper-bound violation + /// against the derived [1..1] property). /// /// /// - /// Canonical helper for derived [1..1] composite properties that subset a [0..*] - /// storage collection (e.g. SubjectMembership::ownedSubjectParameter, - /// FeatureMembership::ownedMemberFeature, - /// ParameterMembership::ownedMemberParameter, - /// TransitionFeatureMembership::transitionFeature, - /// FeatureValue::value, - /// OwningMembership::ownedMemberElement with = ). - /// - /// - /// The implementation is zero-allocation (index-based iteration over the - /// ; no LINQ materialisation, no intermediate list) and - /// early-exits on the second match. The storage collection is structurally [0..*]; - /// the [1..1] invariant lives on the derived property and is enforced here at the - /// point of access. + /// Canonical helper for derived [1..1] properties whose projection ends with a final + /// type-filter or predicate over a structurally [0..*] storage collection. The helper + /// iterates the sequence once with early-exit on the second match; no full materialisation. /// /// - internal static T RequireSingleOfType(this IReadOnlyList elements, string subjectName) - where T : class, IElement + internal static T SingleStrict(this IEnumerable source, string subjectName) + where T : class { T found = null; - for (var index = 0; index < elements.Count; index++) + foreach (var item in source) { - if (elements[index] is not T match) + if (found is not null) { - continue; + throw new MultiplicityViolationException($"{subjectName} contains more than one element of type {typeof(T).Name}"); } + found = item; + } + + return found ?? throw new IncompleteModelException($"{subjectName} must have an element of type {typeof(T).Name}"); + } + + /// + /// Filters to elements of type and + /// returns the single match, throwing if none is + /// present and if more than one is present. + /// + /// The narrowed type to filter to (reference type). + /// + /// The source sequence — typically a [0..*] storage collection such as + /// , , + /// or a derived enumerable such as Feature::type. + /// + /// + /// The name of the subject parameter in the calling method (typically nameof(...)), + /// embedded in the diagnostic message produced on a multiplicity violation. + /// + /// The single element of type . + /// + /// Thrown when no element of type is present (lower-bound + /// violation — the model is missing the required element). + /// + /// + /// Thrown when more than one element of type is present + /// (upper-bound violation against the derived [1..1] property). + /// + /// + /// Convenience overload that bundles a OfType<TResult>() filter before delegating + /// to . Use this form when the source is + /// a heterogeneous storage collection and the caller wants the single element of a particular + /// narrowed type (e.g. FeatureMembership::ownedMemberFeature = the single IFeature + /// in OwnedRelatedElement). + /// + internal static TResult SingleStrict(this System.Collections.IEnumerable source, string subjectName) + where TResult : class + => source.OfType().SingleStrict(subjectName); + + /// + /// Returns the at-most-one element from , throwing + /// if the source contains more than one element. + /// + /// The element type of the sequence (reference type). + /// + /// The (already-projected) sequence of candidate matches — typically the tail of a LINQ chain + /// such as subject.OwnedRelationship.OfType<IFeatureTyping>().Select(t => t.Type).OfType<TFinal>(). + /// + /// + /// The name of the subject parameter in the calling method (typically nameof(...)), + /// embedded in the diagnostic message produced on a multiplicity violation. + /// + /// + /// null when is empty; the single element when it contains + /// exactly one. + /// + /// + /// Thrown when contains more than one element (upper-bound violation + /// against the derived [0..1] property). + /// + /// + /// + /// Canonical helper for derived [0..1] properties whose projection ends with a final + /// type-filter or predicate. The helper iterates the sequence once with early-exit on the + /// second match; no full materialisation. + /// + /// + /// Sibling of — same shape, but with + /// [0..1] semantics: the empty case returns null rather than throwing, because + /// the lower bound is 0. + /// + /// + /// Do NOT use this helper when the OCL derivation body explicitly elects the first of many + /// (->first(), ->at(1)); the spec contract is "pick the first if multiple", + /// not "0 or 1". For those sites, keep FirstOrDefault. + /// + /// + internal static T SingleOrDefaultStrict(this IEnumerable source, string subjectName) + where T : class + { + T found = null; + + foreach (var item in source) + { if (found is not null) { - throw new IncompleteModelException( - $"{subjectName} contains more than one element of type {typeof(T).Name}"); + throw new MultiplicityViolationException($"{subjectName} contains more than one element of type {typeof(T).Name}"); } - found = match; + found = item; } - return found - ?? throw new IncompleteModelException( - $"{subjectName} must have an element of type {typeof(T).Name}"); + return found; } + /// + /// Filters to elements of type and + /// returns the at-most-one match, throwing if + /// more than one is present. + /// + /// The narrowed type to filter to (reference type). + /// + /// The source sequence — typically a [0..*] storage collection such as + /// , , + /// or a derived enumerable such as Feature::type. + /// + /// + /// The name of the subject parameter in the calling method (typically nameof(...)), + /// embedded in the diagnostic message produced on a multiplicity violation. + /// + /// + /// null when no element of type is present; the single + /// match when exactly one is present. + /// + /// + /// Thrown when more than one element of type is present + /// (upper-bound violation against the derived [0..1] property). + /// + /// + /// + /// Convenience overload that bundles a OfType<TResult>() filter before delegating + /// to . Use this form when the + /// source is a heterogeneous storage collection and the caller wants the optional single + /// element of a particular narrowed type (e.g. + /// ConstraintUsage::constraintDefinition = the single IPredicate in + /// type). + /// + /// + /// Do NOT use this helper when the OCL derivation body explicitly elects the first of many + /// (->first(), ->at(1)); the spec contract is "pick the first if multiple", + /// not "0 or 1". For those sites, keep FirstOrDefault. + /// + /// + internal static TResult SingleOrDefaultStrict(this System.Collections.IEnumerable source, string subjectName) + where TResult : class + => source.OfType().SingleOrDefaultStrict(subjectName); + /// /// Determines whether is currently — directly or transitively — contained by /// via the chain of and