From 3170cf3fd41b72a6ec7c826d6c78f971412728af Mon Sep 17 00:00:00 2001 From: atheate Date: Wed, 28 Oct 2026 15:11:09 +0100 Subject: [PATCH] Fix missing PSM/PIM data mapping --- .../PSM/DTO/DataMapperTestFixture.cs | 67 +++++++++ .../PSM/DTO/PimMappingTestFixture.cs | 128 +++++++++++++++++- SysML2.NET/PSM/DTO/DataMapper.cs | 63 +++++++++ SysML2.NET/PSM/DTO/IRequestOfTRecord.cs | 3 - .../PSM/DTO/Mapping/CommitRequest.Mapping.cs | 108 +++++++++++++++ .../DTO/Mapping/DataVersionRequest.Mapping.cs | 59 ++++++++ 6 files changed, 423 insertions(+), 5 deletions(-) create mode 100644 SysML2.NET.Tests/PSM/DTO/DataMapperTestFixture.cs create mode 100644 SysML2.NET/PSM/DTO/DataMapper.cs create mode 100644 SysML2.NET/PSM/DTO/Mapping/CommitRequest.Mapping.cs create mode 100644 SysML2.NET/PSM/DTO/Mapping/DataVersionRequest.Mapping.cs diff --git a/SysML2.NET.Tests/PSM/DTO/DataMapperTestFixture.cs b/SysML2.NET.Tests/PSM/DTO/DataMapperTestFixture.cs new file mode 100644 index 000000000..9137050e3 --- /dev/null +++ b/SysML2.NET.Tests/PSM/DTO/DataMapperTestFixture.cs @@ -0,0 +1,67 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Tests.PSM.DTO +{ + using System; + + using NUnit.Framework; + + using SysML2.NET.Common; + using SysML2.NET.Core.DTO.Root.Namespaces; + using SysML2.NET.PSM.DTO; + + [TestFixture] + public class DataMapperTestFixture + { + [Test] + public void VerifyToPim() + { + Assert.That(DataMapper.ToPim(null), Is.Null); + Assert.That(() => DataMapper.ToPim(new UnmodelledDataRequest()), Throws.TypeOf()); + + var element = new Namespace { Id = Guid.NewGuid() }; + var externalData = new ExternalDataRequest { Id = Guid.NewGuid() }; + var externalRelationship = new ExternalRelationshipRequest { Id = Guid.NewGuid(), Language = "the language" }; + var projectUsage = new ProjectUsageRequest { Id = Guid.NewGuid(), UsedProject = Guid.NewGuid() }; + + using (Assert.EnterMultipleScope()) + { + Assert.That(DataMapper.ToPim(element), Is.SameAs(element)); + Assert.That(DataMapper.ToPim(externalData), Is.TypeOf().With.Property("Id").EqualTo(externalData.Id)); + Assert.That(DataMapper.ToPim(externalRelationship), Is.TypeOf().With.Property("Language").EqualTo("the language")); + Assert.That(DataMapper.ToPim(projectUsage), Is.TypeOf().With.Property("Id").EqualTo(projectUsage.Id)); + } + } + + [Test] + public void VerifyToRequest() + { + Assert.That(DataMapper.ToRequest(null), Is.Null); + Assert.That(() => DataMapper.ToRequest(new ExternalData { Id = Guid.NewGuid() }), Throws.TypeOf()); + + var element = new Namespace { Id = Guid.NewGuid() }; + var externalData = new SysML2.NET.PIM.DTO.ExternalData { Id = Guid.NewGuid() }; + var externalRelationship = new SysML2.NET.PIM.DTO.ExternalRelationship { Id = Guid.NewGuid(), Language = "the language" }; + var projectUsage = new SysML2.NET.PIM.DTO.ProjectUsage { Id = Guid.NewGuid() }; + + using (Assert.EnterMultipleScope()) + { + Assert.That(DataMapper.ToRequest(element), Is.SameAs(element)); + Assert.That(DataMapper.ToRequest(externalData), Is.TypeOf().With.Property("Id").EqualTo(externalData.Id)); + Assert.That(DataMapper.ToRequest(externalRelationship), Is.TypeOf().With.Property("Language").EqualTo("the language")); + Assert.That(DataMapper.ToRequest(projectUsage), Is.TypeOf().With.Property("Id").EqualTo(projectUsage.Id)); + } + } + + private sealed class UnmodelledDataRequest : IDataRequest + { + } + } +} diff --git a/SysML2.NET.Tests/PSM/DTO/PimMappingTestFixture.cs b/SysML2.NET.Tests/PSM/DTO/PimMappingTestFixture.cs index d1a3aebe9..d60fdc6ce 100644 --- a/SysML2.NET.Tests/PSM/DTO/PimMappingTestFixture.cs +++ b/SysML2.NET.Tests/PSM/DTO/PimMappingTestFixture.cs @@ -445,6 +445,130 @@ public void VerifyDataIdentityRoundTrip() } } + [Test] + public void VerifyDataVersionRequestRoundTrip() + { + Assert.That(() => new DataVersionRequest().FromPim(null), Throws.TypeOf()); + + var record = new PIM.DTO.DataVersion + { + Id = Guid.NewGuid(), + Alias = [.. TwoAliases], + Description = "the description", + Name = "the version", + Commit = Guid.NewGuid() + }; + + var empty = RoundTrip(record); + + using (Assert.EnterMultipleScope()) + { + Assert.That(empty.Identity, Is.Null); + Assert.That(empty.Payload, Is.Null); + Assert.That(empty.Id, Is.EqualTo(Guid.Empty)); + Assert.That(empty.Commit, Is.EqualTo(Guid.Empty)); + } + + var identity = new PIM.DTO.DataIdentity { Id = Guid.NewGuid(), Name = "the identity" }; + var payload = new PIM.DTO.ExternalData { Id = Guid.NewGuid(), ResourceIdentifier = "https://example.org/the-resource" }; + record.Identity = identity; + record.Payload = payload; + + var result = RoundTrip(record); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result.Alias, Is.EqualTo(TwoAliases)); + Assert.That(result.Description, Is.EqualTo(record.Description)); + Assert.That(result.Name, Is.EqualTo(record.Name)); + Assert.That(result.Identity.Id, Is.EqualTo(identity.Id)); + Assert.That(result.Payload, Is.TypeOf()); + Assert.That(result.Payload.Id, Is.EqualTo(payload.Id)); + Assert.That(((PIM.DTO.ExternalData)result.Payload).ResourceIdentifier, Is.EqualTo(payload.ResourceIdentifier)); + } + } + + [Test] + public void VerifyCommitRequestRoundTrip() + { + var commitRequest = new CommitRequest(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(() => commitRequest.FromPim(null), Throws.TypeOf()); + Assert.That(() => commitRequest.FromPim(new PIM.DTO.Commit(), null), Throws.TypeOf()); + Assert.That(() => commitRequest.FromPim(null, []), Throws.TypeOf()); + } + + var record = new PIM.DTO.Commit + { + Id = Guid.NewGuid(), + Alias = [.. TwoAliases], + Description = "the description", + Name = "the commit", + Change = [Guid.NewGuid()] + }; + + commitRequest.FromPim(record); + + using (Assert.EnterMultipleScope()) + { + Assert.That(commitRequest.Name, Is.EqualTo(record.Name)); + Assert.That(commitRequest.Change, Has.Count.EqualTo(0)); + Assert.That(() => commitRequest.FromPim(record, []), Throws.TypeOf()); + } + + var (emptyCommit, emptyChange) = new CommitRequest().ToPimWithChange(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(emptyCommit.Change, Has.Count.EqualTo(0)); + Assert.That(emptyChange, Has.Count.EqualTo(0)); + } + + var request = new CommitRequest + { + Alias = [.. TwoAliases], + Description = "the description", + Name = "the commit", + Change = + [ + new DataVersionRequest { Name = "the first version", Identity = new DataIdentityRequest { Id = Guid.NewGuid() } }, + new DataVersionRequest { Name = "the second version", Payload = new ExternalDataRequest { Id = Guid.NewGuid() } } + ] + }; + + var (commit, change) = request.ToPimWithChange(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(commit.Id, Is.Not.EqualTo(Guid.Empty)); + Assert.That(commit.Alias, Is.EqualTo(TwoAliases)); + Assert.That(commit.Name, Is.EqualTo(request.Name)); + Assert.That(change, Has.Count.EqualTo(2)); + Assert.That(commit.Change, Is.EqualTo(change.Select(version => version.Id))); + Assert.That(change.Select(version => version.Commit), Is.All.EqualTo(commit.Id)); + Assert.That(change[0].Name, Is.EqualTo("the first version")); + Assert.That(change[0].Identity.Id, Is.EqualTo(request.Change[0].Identity.Id)); + Assert.That(change[1].Payload, Is.TypeOf()); + Assert.That(request.ToPim().Change, Has.Count.EqualTo(2)); + } + + var restored = new CommitRequest(); + restored.FromPim(commit, [.. change.Reverse()]); + + using (Assert.EnterMultipleScope()) + { + Assert.That(restored.Alias, Is.EqualTo(TwoAliases)); + Assert.That(restored.Description, Is.EqualTo(request.Description)); + Assert.That(restored.Name, Is.EqualTo(request.Name)); + Assert.That(restored.Change, Has.Count.EqualTo(2)); + Assert.That(restored.Change[0].Name, Is.EqualTo("the first version")); + Assert.That(restored.Change[0].Identity.Id, Is.EqualTo(request.Change[0].Identity.Id)); + Assert.That(restored.Change[1].Payload, Is.TypeOf()); + } + } + [Test] public void VerifyContractCoverage() { @@ -463,8 +587,8 @@ public void VerifyContractCoverage() using (Assert.EnterMultipleScope()) { - Assert.That(mappable, Has.Count.EqualTo(23)); - Assert.That(unmapped, Is.EquivalentTo(["Error", "CommitRequest", "DataVersionRequest", "MergeOutcome"])); + Assert.That(mappable, Has.Count.EqualTo(25)); + Assert.That(unmapped, Is.EquivalentTo(["Error", "MergeOutcome"])); Assert.That(mappable, Does.Contain("PrimitiveConstraint")); } } diff --git a/SysML2.NET/PSM/DTO/DataMapper.cs b/SysML2.NET/PSM/DTO/DataMapper.cs new file mode 100644 index 000000000..57418ee84 --- /dev/null +++ b/SysML2.NET/PSM/DTO/DataMapper.cs @@ -0,0 +1,63 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.PSM.DTO +{ + using System; + + using SysML2.NET.Common; + + /// + /// Maps the payload of a data version between the Platform Independent Model and the REST/HTTP PSM request + /// + /// + /// The payload alternatives are reached through and , + /// which carry no Platform Independent Model counterpart of their own, so the dispatch lives here. + /// + public static class DataMapper + { + /// + /// Maps a REST/HTTP PSM payload request onto its Platform Independent Model counterpart + /// + /// The payload to map. + /// The Platform Independent Model payload, or null when the payload is null. + /// Thrown when the payload is of an alternative the Platform Independent Model does not model. + public static IData ToPim(IDataRequest request) + { + return request switch + { + null => null, + ExternalDataRequest externalData => externalData.ToPim(), + ExternalRelationshipRequest externalRelationship => externalRelationship.ToPim(), + ProjectUsageRequest projectUsage => projectUsage.ToPim(), + IData element => element, + _ => throw new NotSupportedException($"A {request.GetType().Name} has no Platform Independent Model counterpart.") + }; + } + + /// + /// Maps a Platform Independent Model payload onto its REST/HTTP PSM request alternative + /// + /// The payload to map. + /// The request payload, or null when the payload is null. + /// Thrown when the payload is of a type the REST/HTTP PSM request does not model. + public static IDataRequest ToRequest(IData data) + { + return data switch + { + null => null, + PIM.DTO.ExternalData externalData => PimMapper.Map(externalData), + PIM.DTO.ExternalRelationship externalRelationship => PimMapper.Map(externalRelationship), + PIM.DTO.ProjectUsage projectUsage => PimMapper.Map(projectUsage), + IDataRequest element => element, + _ => throw new NotSupportedException($"A {data.GetType().Name} has no REST/HTTP PSM request counterpart.") + }; + } + } +} diff --git a/SysML2.NET/PSM/DTO/IRequestOfTRecord.cs b/SysML2.NET/PSM/DTO/IRequestOfTRecord.cs index db9a160a5..98f9d2b63 100644 --- a/SysML2.NET/PSM/DTO/IRequestOfTRecord.cs +++ b/SysML2.NET/PSM/DTO/IRequestOfTRecord.cs @@ -15,9 +15,6 @@ namespace SysML2.NET.PSM.DTO /// /// The Platform Independent Model type that this request model corresponds to. /// - /// - /// Requests whose payload the Platform Independent Model cannot carry implement the non generic only. - /// public interface IRequest : IRequest, IPimMappable { } diff --git a/SysML2.NET/PSM/DTO/Mapping/CommitRequest.Mapping.cs b/SysML2.NET/PSM/DTO/Mapping/CommitRequest.Mapping.cs new file mode 100644 index 000000000..6320245f6 --- /dev/null +++ b/SysML2.NET/PSM/DTO/Mapping/CommitRequest.Mapping.cs @@ -0,0 +1,108 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.PSM.DTO +{ + using System; + using System.Collections.Generic; + using System.Linq; + + /// + /// Declares the Platform Independent Model counterpart of the Commit request model + /// + /// + /// The request embeds its change while the Platform Independent Model commit references it, so the mapping yields + /// the commit together with its data versions, bound by identifiers generated at mapping time. + /// + public partial class CommitRequest : IRequest + { + /// + /// Converts this model into its Platform Independent Model record + /// + /// The commit, whose change references data versions that only returns. + /// Thrown when a change payload has no Platform Independent Model counterpart. + public PIM.DTO.Commit ToPim() + { + return this.ToPimWithChange().Commit; + } + + /// + /// Converts this model into its Platform Independent Model commit and the data versions that commit changes + /// + /// The commit, and its change in the order of the request. + /// Thrown when a change payload has no Platform Independent Model counterpart. + public (PIM.DTO.Commit Commit, IReadOnlyList Change) ToPimWithChange() + { + var commit = new PIM.DTO.Commit + { + Id = Guid.NewGuid(), + Alias = [.. this.Alias ?? []], + Description = this.Description, + Name = this.Name + }; + + var change = (this.Change ?? []).Select(request => + { + var version = request.ToPim(); + version.Id = Guid.NewGuid(); + version.Commit = commit.Id; + return version; + }).ToList(); + + commit.Change = [.. change.Select(version => version.Id)]; + + return (commit, change); + } + + /// + /// Populates this model from its Platform Independent Model record, leaving the change empty + /// + /// The record to read from. + /// Thrown when the record is null. + public void FromPim(PIM.DTO.Commit record) + { + if (record == null) + { + throw new ArgumentNullException(nameof(record)); + } + + this.Alias = [.. record.Alias ?? []]; + this.Description = record.Description; + this.Name = record.Name; + this.Change = []; + } + + /// + /// Populates this model from its Platform Independent Model record and the data versions its change references + /// + /// The record to read from. + /// The data versions that resolve the record's change identifiers. + /// Thrown when the record or the data versions are null. + /// Thrown when a change identifier resolves to no data version, or two data versions share an identifier. + /// Thrown when a change payload has no REST/HTTP PSM request counterpart. + public void FromPim(PIM.DTO.Commit record, IEnumerable change) + { + if (change == null) + { + throw new ArgumentNullException(nameof(change)); + } + + this.FromPim(record); + + var versions = change.ToDictionary(version => version.Id); + + this.Change = + [ + .. (record.Change ?? []).Select(identifier => versions.TryGetValue(identifier, out var version) + ? PimMapper.Map(version) + : throw new ArgumentException($"The change {identifier} resolves to no data version.", nameof(change))) + ]; + } + } +} diff --git a/SysML2.NET/PSM/DTO/Mapping/DataVersionRequest.Mapping.cs b/SysML2.NET/PSM/DTO/Mapping/DataVersionRequest.Mapping.cs new file mode 100644 index 000000000..be5054af2 --- /dev/null +++ b/SysML2.NET/PSM/DTO/Mapping/DataVersionRequest.Mapping.cs @@ -0,0 +1,59 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.PSM.DTO +{ + using System; + + /// + /// Declares the Platform Independent Model counterpart of the DataVersion request model + /// + /// + /// The request carries no identifier and no commit; supplies both when it maps its change. + /// + public partial class DataVersionRequest : IRequest + { + /// + /// Converts this model into its Platform Independent Model record + /// + /// The record carrying the members that the REST/HTTP PSM models. + /// Thrown when the payload has no Platform Independent Model counterpart. + public PIM.DTO.DataVersion ToPim() + { + return new PIM.DTO.DataVersion + { + Alias = [.. this.Alias ?? []], + Description = this.Description, + Identity = this.Identity?.ToPim(), + Name = this.Name, + Payload = DataMapper.ToPim(this.Payload) + }; + } + + /// + /// Populates this model from its Platform Independent Model record + /// + /// The record to read from. + /// Thrown when the record is null. + /// Thrown when the payload has no REST/HTTP PSM request counterpart. + public void FromPim(PIM.DTO.DataVersion record) + { + if (record == null) + { + throw new ArgumentNullException(nameof(record)); + } + + this.Alias = [.. record.Alias ?? []]; + this.Description = record.Description; + this.Identity = PimMapper.Map(record.Identity); + this.Name = record.Name; + this.Payload = DataMapper.ToRequest(record.Payload); + } + } +}