diff --git a/.github/workflows/nuget-reference-check.yml b/.github/workflows/nuget-reference-check.yml index f66ddc315..344a61813 100644 --- a/.github/workflows/nuget-reference-check.yml +++ b/.github/workflows/nuget-reference-check.yml @@ -35,7 +35,7 @@ jobs: # declared in Directory.Packages.props - keep the two in step. They are by design # permanently "outdated", so they are excluded from the outdated signal only - the # --deprecated and --vulnerable steps below do not use this filter. - IGNORE_PACKAGES="Microsoft\.NETCore\.Platforms|Microsoft\.NETCore\.Targets|Microsoft\.Extensions\.Logging\.Abstractions|Microsoft\.Extensions\.DependencyInjection\.Abstractions|System\.Collections\.Immutable|System\.IO\.Pipelines" + IGNORE_PACKAGES="Microsoft\.NETCore\.Platforms|Microsoft\.NETCore\.Targets|Microsoft\.Extensions\.Logging\.Abstractions|Microsoft\.Extensions\.DependencyInjection\.Abstractions|Microsoft\.Extensions\.Http|System\.Collections\.Immutable|System\.IO\.Pipelines" # Full report for the workflow logs only, including transitive packages dotnet list SysML2.NET.sln package --outdated --include-transitive > outdated-full-raw.log diff --git a/Directory.Packages.props b/Directory.Packages.props index b584267de..4e6ffa01f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,6 +14,7 @@ IGNORE_PACKAGES in .github/workflows/nuget-reference-check.yml (outdated check only): Microsoft.Extensions.DependencyInjection.Abstractions + Microsoft.Extensions.Http Microsoft.Extensions.Logging.Abstractions System.Collections.Immutable System.IO.Pipelines @@ -24,6 +25,7 @@ --> + @@ -55,6 +57,7 @@ + diff --git a/SySML2.NET.REST.Tests/Exceptions/MergeConflictExceptionTestFixture.cs b/SySML2.NET.REST.Tests/Exceptions/MergeConflictExceptionTestFixture.cs new file mode 100644 index 000000000..464a3f06f --- /dev/null +++ b/SySML2.NET.REST.Tests/Exceptions/MergeConflictExceptionTestFixture.cs @@ -0,0 +1,74 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST.Tests.Exceptions +{ + using System; + using System.Net; + + using NUnit.Framework; + + using SysML2.NET.PSM.DTO; + using SysML2.NET.REST.Exceptions; + + [TestFixture] + public class MergeConflictExceptionTestFixture + { + [Test] + public void VerifyConstructor() + { + var exception = new MergeConflictException([]); + + using (Assert.EnterMultipleScope()) + { + Assert.That(exception.Conflicts, Has.Count.EqualTo(0)); + Assert.That(exception.StatusCode, Is.EqualTo(HttpStatusCode.Conflict)); + Assert.That(exception.Error, Is.Null); + } + + var conflict = new DataIdentity { Id = Guid.Parse("2f1a0e1c-6b2a-4d5e-9c3f-8a7b6d5e4c3b") }; + + exception = new MergeConflictException([conflict]); + + using (Assert.EnterMultipleScope()) + { + Assert.That(exception.Conflicts, Has.Count.EqualTo(1)); + Assert.That(exception.Conflicts[0], Is.SameAs(conflict)); + Assert.That(exception, Is.InstanceOf()); + } + + exception = new MergeConflictException(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(exception.Conflicts, Has.Count.EqualTo(0)); + Assert.That(exception.StatusCode, Is.EqualTo(HttpStatusCode.Conflict)); + } + + exception = new MergeConflictException("the message"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(exception.Message, Is.EqualTo("the message")); + Assert.That(exception.Conflicts, Has.Count.EqualTo(0)); + } + + var innerException = new InvalidOperationException(); + + exception = new MergeConflictException("the message", innerException); + + using (Assert.EnterMultipleScope()) + { + Assert.That(exception.InnerException, Is.SameAs(innerException)); + Assert.That(exception.StatusCode, Is.EqualTo(HttpStatusCode.Conflict)); + Assert.That(exception.Error, Is.Null); + } + } + } +} diff --git a/SySML2.NET.REST.Tests/Exceptions/PsmApiExceptionTestFixture.cs b/SySML2.NET.REST.Tests/Exceptions/PsmApiExceptionTestFixture.cs new file mode 100644 index 000000000..a40362939 --- /dev/null +++ b/SySML2.NET.REST.Tests/Exceptions/PsmApiExceptionTestFixture.cs @@ -0,0 +1,73 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST.Tests.Exceptions +{ + using System; + using System.Net; + + using NUnit.Framework; + + using SysML2.NET.PSM.DTO; + using SysML2.NET.REST.Exceptions; + + [TestFixture] + public class PsmApiExceptionTestFixture + { + [Test] + public void VerifyConstructor() + { + var error = new Error { Description = "the error description" }; + + var exception = new PsmApiException(HttpStatusCode.NotFound, error); + + using (Assert.EnterMultipleScope()) + { + Assert.That(exception.StatusCode, Is.EqualTo(HttpStatusCode.NotFound)); + Assert.That(exception.Error, Is.SameAs(error)); + Assert.That(exception.Message, Is.EqualTo("the error description")); + } + + exception = new PsmApiException(HttpStatusCode.BadRequest, new Error { Description = " " }); + + Assert.That(exception.Message, Is.EqualTo("The server responded with status code 400 (BadRequest)")); + + exception = new PsmApiException(HttpStatusCode.InternalServerError, null); + + using (Assert.EnterMultipleScope()) + { + Assert.That(exception.Error, Is.Null); + Assert.That(exception.Message, Is.EqualTo("The server responded with status code 500 (InternalServerError)")); + } + + exception = new PsmApiException(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(exception.StatusCode, Is.EqualTo((HttpStatusCode)0)); + Assert.That(exception.Error, Is.Null); + } + + exception = new PsmApiException("the message"); + + Assert.That(exception.Message, Is.EqualTo("the message")); + + var innerException = new InvalidOperationException(); + + exception = new PsmApiException("the message", innerException); + + using (Assert.EnterMultipleScope()) + { + Assert.That(exception.Message, Is.EqualTo("the message")); + Assert.That(exception.InnerException, Is.SameAs(innerException)); + Assert.That(exception.Error, Is.Null); + } + } + } +} diff --git a/SySML2.NET.REST.Tests/PsmApiClientTestFixture.cs b/SySML2.NET.REST.Tests/PsmApiClientTestFixture.cs new file mode 100644 index 000000000..2c8ff879a --- /dev/null +++ b/SySML2.NET.REST.Tests/PsmApiClientTestFixture.cs @@ -0,0 +1,506 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST.Tests +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Net; + using System.Net.Http; + using System.Text.Json; + using System.Threading.Tasks; + + using NUnit.Framework; + + using RichardSzalay.MockHttp; + + using SysML2.NET.Core.DTO.Root.Namespaces; + using SysML2.NET.PSM; + using SysML2.NET.PSM.DTO; + using SysML2.NET.PSM.Enumerations; + using SysML2.NET.REST.Exceptions; + using SysML2.NET.Serializer.Json; + + [TestFixture] + public class PsmApiClientTestFixture + { + private const string JsonMediaType = "application/json"; + + private const string ErrorPayload = "{\"@type\":\"Error\",\"description\":\"the error description\"}"; + + private static readonly Uri BaseAddress = new("http://localhost/api/"); + + private static readonly Guid ProjectId = Guid.Parse("000e9890-6935-43e6-a5d7-5d7cac601f4c"); + + private static readonly Guid BranchId = Guid.Parse("c294a463-6c9c-47a8-b592-01252c5ab2a7"); + + private static readonly Guid CommitId = Guid.Parse("6d7ad9fd-6520-4ff2-885b-8c5c129e6c27"); + + private static readonly Guid OtherCommitId = Guid.Parse("2f1a0e1c-6b2a-4d5e-9c3f-8a7b6d5e4c3b"); + + private static readonly Guid RootNamespaceId = Guid.Parse("864d5403-c87b-4679-9d3a-b09c7fde1dd9"); + + private MockHttpMessageHandler mockHttp; + + private PsmApiClient client; + + [SetUp] + public void SetUp() + { + this.mockHttp = new MockHttpMessageHandler(); + + var httpClient = this.mockHttp.ToHttpClient(); + httpClient.BaseAddress = BaseAddress; + + this.client = new PsmApiClient(httpClient, new DeSerializer(), new Serializer()); + } + + [TearDown] + public void TearDown() + { + this.mockHttp.Dispose(); + } + + private static string ReadPayload(string fileName) + { + return File.ReadAllText(Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", fileName)); + } + + private static KeyValuePair[] CreateLinkHeader(string link) + { + return [new KeyValuePair("Link", link)]; + } + + [Test] + public void VerifyConstructor() + { + using var httpClient = new HttpClient(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(() => new PsmApiClient(null, new DeSerializer(), new Serializer()), Throws.TypeOf()); + Assert.That(() => new PsmApiClient(httpClient, null, new Serializer()), Throws.TypeOf()); + Assert.That(() => new PsmApiClient(httpClient, new DeSerializer(), null), Throws.TypeOf()); + Assert.That(() => new PsmApiClient(httpClient, new DeSerializer(), new Serializer()), Throws.Nothing); + } + } + + [Test] + public async Task VerifyGetBranchesByProjectAsync() + { + var branchesUri = $"{BaseAddress}projects/{ProjectId}/branches"; + var previousPage = Page.FromCursor(1618861496632, OtherCommitId); + var nextPage = Page.FromCursor(1618861496632, Guid.Parse("20109f40-4825-416a-86f6-7508ac42a021")); + + this.mockHttp.Expect(HttpMethod.Get, branchesUri) + .WithExactQueryString($"page[size]=2&page[after]={Uri.EscapeDataString(previousPage.ToString())}") + .Respond(HttpStatusCode.OK, + CreateLinkHeader($"<{branchesUri}?page[after]=MTYxODg2MTQ5NjYzMnwyMDEwOWY0MC00ODI1LTQxNmEtODZmNi03NTA4YWM0MmEwMjE&page[size]=2>; rel=\"next\", <{branchesUri}?page[before]={Uri.EscapeDataString(previousPage.ToString())}>; rel=\"prev\""), + JsonMediaType, ReadPayload("branches.json")); + + var branches = await this.client.GetBranchesByProjectAsync(ProjectId, new QueryParameters { PageSize = 2, PageAfter = previousPage }); + + using (Assert.EnterMultipleScope()) + { + Assert.That(branches.Records, Has.Count.EqualTo(1)); + Assert.That(branches.Records[0].Id, Is.EqualTo(BranchId)); + Assert.That(branches.Records[0].Name, Is.EqualTo("main")); + Assert.That(branches.Next, Is.EqualTo(nextPage)); + Assert.That(branches.Previous, Is.EqualTo(previousPage)); + } + + this.mockHttp.Expect(HttpMethod.Get, branchesUri).WithExactQueryString(string.Empty).Respond(JsonMediaType, "[]"); + + branches = await this.client.GetBranchesByProjectAsync(ProjectId); + + using (Assert.EnterMultipleScope()) + { + Assert.That(branches.Records, Has.Count.EqualTo(0)); + Assert.That(branches.Next, Is.Null); + Assert.That(branches.Previous, Is.Null); + } + + this.mockHttp.Expect(HttpMethod.Get, branchesUri) + .Respond(HttpStatusCode.OK, CreateLinkHeader($"<{branchesUri}?page[after]=not-a-page>; rel=\"next\""), JsonMediaType, "[]"); + + branches = await this.client.GetBranchesByProjectAsync(ProjectId); + + Assert.That(branches.Next, Is.EqualTo(new Page("not-a-page"))); + + this.mockHttp.Expect(HttpMethod.Get, branchesUri) + .WithExactQueryString("page[after]=not-a-page") + .Respond(HttpStatusCode.OK, CreateLinkHeader($"<{branchesUri}?page[after]=>; rel=\"next\""), JsonMediaType, "[]"); + + branches = await this.client.GetBranchesByProjectAsync(ProjectId, new QueryParameters { PageAfter = branches.Next }); + + Assert.That(branches.Next, Is.Null); + + this.mockHttp.Expect(HttpMethod.Get, branchesUri).Respond(HttpStatusCode.NotFound, JsonMediaType, ErrorPayload); + + await Assert.ThatAsync(() => this.client.GetBranchesByProjectAsync(ProjectId), + Throws.TypeOf() + .With.Property(nameof(PsmApiException.StatusCode)).EqualTo(HttpStatusCode.NotFound) + .And.Property(nameof(PsmApiException.Error)).Property(nameof(Error.Description)).EqualTo("the error description")); + + this.mockHttp.Expect(HttpMethod.Get, branchesUri).Respond(HttpStatusCode.NotFound, JsonMediaType, "{}"); + + await Assert.ThatAsync(() => this.client.GetBranchesByProjectAsync(ProjectId), + Throws.TypeOf().With.Property(nameof(PsmApiException.Error)).Null); + + this.mockHttp.Expect(HttpMethod.Get, branchesUri).Respond(HttpStatusCode.NotFound, JsonMediaType, "{\"@type\":\"Unknown\"}"); + + await Assert.ThatAsync(() => this.client.GetBranchesByProjectAsync(ProjectId), + Throws.TypeOf().With.Property(nameof(PsmApiException.Error)).Null); + + this.mockHttp.Expect(HttpMethod.Get, branchesUri).Respond(HttpStatusCode.BadGateway, "text/plain", "the gateway is down"); + + await Assert.ThatAsync(() => this.client.GetBranchesByProjectAsync(ProjectId), + Throws.TypeOf() + .With.Property(nameof(PsmApiException.StatusCode)).EqualTo(HttpStatusCode.BadGateway) + .And.Property(nameof(PsmApiException.Error)).Null); + + this.mockHttp.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task VerifyDeleteBranchByProjectAndIdAsync() + { + var branchUri = $"{BaseAddress}projects/{ProjectId}/branches/{BranchId}"; + + this.mockHttp.Expect(HttpMethod.Delete, branchUri).Respond(JsonMediaType, ReadPayload("branch.json")); + + var branch = await this.client.DeleteBranchByProjectAndIdAsync(ProjectId, BranchId); + + Assert.That(branch.Id, Is.EqualTo(BranchId)); + + this.mockHttp.Expect(HttpMethod.Delete, branchUri).Respond(HttpStatusCode.NoContent); + + Assert.That(await this.client.DeleteBranchByProjectAndIdAsync(ProjectId, BranchId), Is.Null); + + this.mockHttp.Expect(HttpMethod.Delete, branchUri).Respond(HttpStatusCode.NotFound, JsonMediaType, ErrorPayload); + + await Assert.ThatAsync(() => this.client.DeleteBranchByProjectAndIdAsync(ProjectId, BranchId), + Throws.TypeOf().With.Property(nameof(PsmApiException.StatusCode)).EqualTo(HttpStatusCode.NotFound)); + + this.mockHttp.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task VerifyPostCommitByProjectAsync() + { + var commitsUri = $"{BaseAddress}projects/{ProjectId}/commits"; + var commitRequest = new CommitRequest { Description = "the commit description" }; + + this.mockHttp.Expect(HttpMethod.Post, commitsUri) + .WithExactQueryString($"branchId={BranchId}") + .With(request => request.Content.Headers.ContentType.MediaType == JsonMediaType + && request.Content.ReadAsStringAsync().GetAwaiter().GetResult().Contains("the commit description")) + .Respond(HttpStatusCode.Created, JsonMediaType, ReadPayload("commit.json")); + + var commit = await this.client.PostCommitByProjectAsync(ProjectId, commitRequest, BranchId); + + Assert.That(commit.Id, Is.EqualTo(CommitId)); + + this.mockHttp.Expect(HttpMethod.Post, commitsUri) + .With(request => string.IsNullOrWhiteSpace(request.RequestUri.Query)) + .Respond(HttpStatusCode.BadRequest, JsonMediaType, ErrorPayload); + + await Assert.ThatAsync(() => this.client.PostCommitByProjectAsync(ProjectId, commitRequest), + Throws.TypeOf().With.Property(nameof(PsmApiException.StatusCode)).EqualTo(HttpStatusCode.BadRequest)); + + await Assert.ThatAsync(() => this.client.PostCommitByProjectAsync(ProjectId, null), Throws.ArgumentNullException); + + this.mockHttp.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task VerifyGetRootsByProjectCommitAsync() + { + var rootsUri = $"{BaseAddress}projects/{ProjectId}/commits/{CommitId}/roots"; + + this.mockHttp.Expect(HttpMethod.Get, rootsUri) + .WithExactQueryString("excludeUsed=true") + .Respond(JsonMediaType, ReadPayload("roots.json")); + + var roots = await this.client.GetRootsByProjectCommitAsync(ProjectId, CommitId, true); + + using (Assert.EnterMultipleScope()) + { + Assert.That(roots.Records, Has.Count.EqualTo(2)); + Assert.That(roots.Records[0].Id, Is.EqualTo(RootNamespaceId)); + } + + this.mockHttp.Expect(HttpMethod.Get, rootsUri).Respond(HttpStatusCode.NotFound, JsonMediaType, ErrorPayload); + + await Assert.ThatAsync(() => this.client.GetRootsByProjectCommitAsync(ProjectId, CommitId), + Throws.TypeOf().With.Property(nameof(PsmApiException.StatusCode)).EqualTo(HttpStatusCode.NotFound)); + + this.mockHttp.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task VerifyGetRelationshipsByProjectCommitRelatedElementAsync() + { + var relationshipsUri = $"{BaseAddress}projects/{ProjectId}/commits/{CommitId}/elements/{RootNamespaceId}/relationships"; + + this.mockHttp.Expect(HttpMethod.Get, relationshipsUri).WithExactQueryString("direction=in").Respond(JsonMediaType, "[]"); + + var relationships = await this.client.GetRelationshipsByProjectCommitRelatedElementAsync(ProjectId, CommitId, RootNamespaceId, Direction.@in); + + Assert.That(relationships.Records, Has.Count.EqualTo(0)); + + this.mockHttp.Expect(HttpMethod.Get, relationshipsUri).WithExactQueryString("direction=both&excludeUsed=true").Respond(JsonMediaType, "[]"); + + relationships = await this.client.GetRelationshipsByProjectCommitRelatedElementAsync(ProjectId, CommitId, RootNamespaceId, Direction.both, true); + + Assert.That(relationships.Records, Has.Count.EqualTo(0)); + + this.mockHttp.Expect(HttpMethod.Get, relationshipsUri).WithExactQueryString(string.Empty).Respond(JsonMediaType, "[]"); + + relationships = await this.client.GetRelationshipsByProjectCommitRelatedElementAsync(ProjectId, CommitId, RootNamespaceId); + + Assert.That(relationships.Next, Is.Null); + + this.mockHttp.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task VerifyGetElementByProjectCommitIdAsync() + { + var elementUri = $"{BaseAddress}projects/{ProjectId}/commits/{CommitId}/elements/{RootNamespaceId}"; + var roots = ReadPayload("roots.json"); + + using (var rootsDocument = JsonDocument.Parse(roots)) + { + this.mockHttp.Expect(HttpMethod.Get, elementUri) + .WithExactQueryString("excludeUsed=false") + .Respond(JsonMediaType, rootsDocument.RootElement[0].GetRawText()); + } + + var element = await this.client.GetElementByProjectCommitIdAsync(ProjectId, CommitId, RootNamespaceId, false); + + Assert.That(element, Is.TypeOf().With.Property(nameof(Namespace.Id)).EqualTo(RootNamespaceId)); + + this.mockHttp.Expect(HttpMethod.Get, elementUri).Respond(JsonMediaType, roots); + + await Assert.ThatAsync(() => this.client.GetElementByProjectCommitIdAsync(ProjectId, CommitId, RootNamespaceId), Throws.TypeOf()); + + this.mockHttp.Expect(HttpMethod.Get, elementUri).Respond(JsonMediaType, "[]"); + + await Assert.ThatAsync(() => this.client.GetElementByProjectCommitIdAsync(ProjectId, CommitId, RootNamespaceId), Throws.TypeOf()); + + this.mockHttp.Expect(HttpMethod.Get, elementUri).Respond(HttpStatusCode.NotFound, JsonMediaType, ErrorPayload); + + await Assert.ThatAsync(() => this.client.GetElementByProjectCommitIdAsync(ProjectId, CommitId, RootNamespaceId), + Throws.TypeOf().With.Property(nameof(PsmApiException.StatusCode)).EqualTo(HttpStatusCode.NotFound)); + + this.mockHttp.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task VerifyGetQueryResultsByProjectIdQueryPostAsync() + { + var queryResultsUri = $"{BaseAddress}projects/{ProjectId}/query-results"; + var queryRequest = new QueryRequest { Name = "the query name" }; + + this.mockHttp.Expect(HttpMethod.Post, queryResultsUri) + .WithExactQueryString($"page[size]=2&commitId={CommitId}") + .With(request => request.Content.ReadAsStringAsync().GetAwaiter().GetResult().Contains("the query name")) + .Respond(JsonMediaType, ReadPayload("roots.json")); + + var results = await this.client.GetQueryResultsByProjectIdQueryPostAsync(ProjectId, queryRequest, CommitId, new QueryParameters { PageSize = 2 }); + + using (Assert.EnterMultipleScope()) + { + Assert.That(results.Records, Has.Count.EqualTo(2)); + Assert.That(results.Records[0], Is.TypeOf()); + Assert.That(results.Next, Is.Null); + } + + this.mockHttp.Expect(HttpMethod.Post, queryResultsUri) + .With(request => string.IsNullOrWhiteSpace(request.RequestUri.Query)) + .Respond(HttpStatusCode.BadRequest, JsonMediaType, ErrorPayload); + + await Assert.ThatAsync(() => this.client.GetQueryResultsByProjectIdQueryPostAsync(ProjectId, queryRequest), + Throws.TypeOf().With.Property(nameof(PsmApiException.StatusCode)).EqualTo(HttpStatusCode.BadRequest)); + + this.mockHttp.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task VerifyPutProjectByIdAsync() + { + var projectUri = $"{BaseAddress}projects/{ProjectId}"; + var projectRequest = new ProjectRequest { Name = "the project name" }; + + this.mockHttp.Expect(HttpMethod.Put, projectUri) + .With(request => request.Content.ReadAsStringAsync().GetAwaiter().GetResult().Contains("the project name")) + .Respond(JsonMediaType, ReadPayload("project.json")); + + var project = await this.client.PutProjectByIdAsync(ProjectId, projectRequest); + + using (Assert.EnterMultipleScope()) + { + Assert.That(project.Id, Is.EqualTo(ProjectId)); + Assert.That(project.DefaultBranch, Is.EqualTo(BranchId)); + } + + this.mockHttp.Expect(HttpMethod.Put, projectUri) + .With(request => request.Content is null) + .Respond(JsonMediaType, ReadPayload("project.json")); + + project = await this.client.PutProjectByIdAsync(ProjectId); + + Assert.That(project.Id, Is.EqualTo(ProjectId)); + + this.mockHttp.Expect(HttpMethod.Put, projectUri).Respond(HttpStatusCode.BadRequest, JsonMediaType, ErrorPayload); + + await Assert.ThatAsync(() => this.client.PutProjectByIdAsync(ProjectId, projectRequest), + Throws.TypeOf() + .With.Property(nameof(PsmApiException.StatusCode)).EqualTo(HttpStatusCode.BadRequest) + .And.Message.EqualTo("the error description")); + + this.mockHttp.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task VerifyMergeAsync() + { + var mergeUri = $"{BaseAddress}projects/{ProjectId}/branches/{BranchId}/merge"; + + this.mockHttp.Expect(HttpMethod.Post, mergeUri) + .WithExactQueryString($"sourceCommitId={CommitId},{OtherCommitId}&description=the%20merge%20description") + .With(request => request.Content.ReadAsStringAsync().GetAwaiter().GetResult().Contains(RootNamespaceId.ToString())) + .Respond(HttpStatusCode.Created, JsonMediaType, ReadPayload("commit.json")); + + var mergeCommit = await this.client.MergeAsync(ProjectId, [CommitId, OtherCommitId], BranchId, [new Namespace { Id = RootNamespaceId }], "the merge description"); + + Assert.That(mergeCommit.Id, Is.EqualTo(CommitId)); + + await Assert.ThatAsync(() => this.client.MergeAsync(ProjectId, null, BranchId), Throws.TypeOf()); + + await Assert.ThatAsync(() => this.client.MergeAsync(ProjectId, [], BranchId), Throws.TypeOf()); + + this.mockHttp.Expect(HttpMethod.Post, mergeUri) + .WithExactQueryString($"sourceCommitId={CommitId}") + .With(request => request.Content is null) + .Respond(HttpStatusCode.Created, JsonMediaType, ReadPayload("commit.json")); + + mergeCommit = await this.client.MergeAsync(ProjectId, [CommitId], BranchId); + + Assert.That(mergeCommit.Id, Is.EqualTo(CommitId)); + + this.mockHttp.Expect(HttpMethod.Post, mergeUri).Respond(HttpStatusCode.Conflict, "text/plain", "the branch has moved"); + + await Assert.ThatAsync(() => this.client.MergeAsync(ProjectId, [CommitId], BranchId, null), + Throws.TypeOf().With.Property(nameof(PsmApiException.StatusCode)).EqualTo(HttpStatusCode.Conflict)); + + this.mockHttp.Expect(HttpMethod.Post, mergeUri) + .Respond(HttpStatusCode.Conflict, JsonMediaType, $"[{{\"@id\":\"{OtherCommitId}\",\"@type\":\"DataIdentity\"}}]"); + + await Assert.ThatAsync(() => this.client.MergeAsync(ProjectId, [CommitId], BranchId, null), + Throws.TypeOf() + .With.Property(nameof(PsmApiException.StatusCode)).EqualTo(HttpStatusCode.Conflict) + .And.Property(nameof(MergeConflictException.Conflicts)).With.Count.EqualTo(1)); + + this.mockHttp.Expect(HttpMethod.Post, mergeUri).Respond(HttpStatusCode.Conflict, JsonMediaType, ErrorPayload); + + await Assert.ThatAsync(() => this.client.MergeAsync(ProjectId, [CommitId], BranchId, null), + Throws.TypeOf() + .With.Property(nameof(PsmApiException.StatusCode)).EqualTo(HttpStatusCode.Conflict) + .And.Property(nameof(PsmApiException.Error)).Property(nameof(Error.Description)).EqualTo("the error description")); + + this.mockHttp.Expect(HttpMethod.Post, mergeUri).Respond(HttpStatusCode.Conflict, JsonMediaType, "{ not json"); + + await Assert.ThatAsync(() => this.client.MergeAsync(ProjectId, [CommitId], BranchId, null), + Throws.TypeOf().With.Property(nameof(PsmApiException.StatusCode)).EqualTo(HttpStatusCode.Conflict)); + + this.mockHttp.Expect(HttpMethod.Post, mergeUri).Respond(HttpStatusCode.NotFound, JsonMediaType, ErrorPayload); + + await Assert.ThatAsync(() => this.client.MergeAsync(ProjectId, [CommitId], BranchId, null), + Throws.TypeOf().With.Property(nameof(PsmApiException.StatusCode)).EqualTo(HttpStatusCode.NotFound)); + + this.mockHttp.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task VerifyGetDatatypesAsync() + { + var datatypesUri = $"{BaseAddress}meta/datatypes"; + const string datatypesPayload = "{\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"$defs\":{\"Boolean\":{\"type\":\"boolean\"}}}"; + var nextPage = Page.FromCursor(1618861496632, CommitId); + + this.mockHttp.Expect(HttpMethod.Get, datatypesUri) + .WithExactQueryString("page[size]=1") + .Respond(HttpStatusCode.OK, CreateLinkHeader($"<{datatypesUri}?page[after]={nextPage}&page[size]=1>; rel=\"next\""), JsonMediaType, datatypesPayload); + + var datatypes = await this.client.GetDatatypesAsync(new QueryParameters { PageSize = 1 }); + + using (datatypes.Records[0]) + { + using (Assert.EnterMultipleScope()) + { + Assert.That(datatypes.Records, Has.Count.EqualTo(1)); + Assert.That(datatypes.Records[0].RootElement.GetProperty("$defs").GetProperty("Boolean").GetProperty("type").GetString(), Is.EqualTo("boolean")); + Assert.That(datatypes.Next, Is.EqualTo(nextPage)); + Assert.That(datatypes.Previous, Is.Null); + } + } + + this.mockHttp.Expect(HttpMethod.Get, datatypesUri).WithExactQueryString(string.Empty).Respond(JsonMediaType, datatypesPayload); + + datatypes = await this.client.GetDatatypesAsync(); + + using (datatypes.Records[0]) + { + Assert.That(datatypes.Next, Is.Null); + } + + this.mockHttp.Expect(HttpMethod.Get, datatypesUri).Respond(HttpStatusCode.NotFound, JsonMediaType, ErrorPayload); + + await Assert.ThatAsync(() => this.client.GetDatatypesAsync(), + Throws.TypeOf().With.Property(nameof(PsmApiException.StatusCode)).EqualTo(HttpStatusCode.NotFound)); + + this.mockHttp.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task VerifyGetDatatypeByIdAsync() + { + const string datatypeId = "https://www.omg.org/spec/SysML/the/datatype"; + var datatypeUri = $"{BaseAddress}meta/datatypes/{Uri.EscapeDataString(datatypeId)}"; + + this.mockHttp.Expect(HttpMethod.Get, datatypeUri) + .Respond(JsonMediaType, "{\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"type\":\"boolean\"}"); + + using (var datatype = await this.client.GetDatatypeByIdAsync(datatypeId)) + { + Assert.That(datatype.RootElement.GetProperty("type").GetString(), Is.EqualTo("boolean")); + } + + this.mockHttp.Expect(HttpMethod.Get, datatypeUri).Respond(HttpStatusCode.NotFound, JsonMediaType, ErrorPayload); + + await Assert.ThatAsync(() => this.client.GetDatatypeByIdAsync(datatypeId), + Throws.TypeOf().With.Property(nameof(PsmApiException.StatusCode)).EqualTo(HttpStatusCode.NotFound)); + + this.mockHttp.Expect(HttpMethod.Get, datatypeUri).Respond(JsonMediaType, "not json"); + + await Assert.ThatAsync(() => this.client.GetDatatypeByIdAsync(datatypeId), Throws.InstanceOf()); + + using (Assert.EnterMultipleScope()) + { + await Assert.ThatAsync(() => this.client.GetDatatypeByIdAsync(null), Throws.ArgumentNullException); + await Assert.ThatAsync(() => this.client.GetDatatypeByIdAsync(" "), Throws.ArgumentException); + } + + this.mockHttp.VerifyNoOutstandingExpectation(); + } + } +} diff --git a/SySML2.NET.REST.Tests/QueryStringBuilderTestFixture.cs b/SySML2.NET.REST.Tests/QueryStringBuilderTestFixture.cs new file mode 100644 index 000000000..755d8c4af --- /dev/null +++ b/SySML2.NET.REST.Tests/QueryStringBuilderTestFixture.cs @@ -0,0 +1,77 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST.Tests +{ + using System; + + using NUnit.Framework; + + using SysML2.NET.PSM; + + [TestFixture] + public class QueryStringBuilderTestFixture + { + private static readonly Guid FirstCommitId = Guid.Parse("6d7ad9fd-6520-4ff2-885b-8c5c129e6c27"); + + private static readonly Guid SecondCommitId = Guid.Parse("2f1a0e1c-6b2a-4d5e-9c3f-8a7b6d5e4c3b"); + + private static readonly Guid[] NoCommitIds = []; + + private static readonly Guid[] TwoCommitIds = [FirstCommitId, SecondCommitId]; + + private static readonly string[] ValuesContainingTheDelimiter = ["first, value", "second&value"]; + + [Test] + public void VerifyConstructor() + { + Assert.That(new QueryStringBuilder().ToString(), Is.Empty); + + Assert.That(new QueryStringBuilder(new QueryParameters()).ToString(), Is.Empty); + + var page = Page.FromCursor(1618861496632, FirstCommitId); + + var builder = new QueryStringBuilder(new QueryParameters { PageSize = 2, PageAfter = page, PageBefore = page }); + + Assert.That(builder.ToString(), Is.EqualTo($"?page[size]=2&page[after]={Uri.EscapeDataString(page.ToString())}&page[before]={Uri.EscapeDataString(page.ToString())}")); + } + + [Test] + public void VerifyAdd() + { + var builder = new QueryStringBuilder(); + + builder.Add("commitId", null); + builder.Add("sourceCommitId", NoCommitIds); + builder.Add("page[after]", default(Page)); + + Assert.That(builder.ToString(), Is.Empty); + + builder.Add("excludeUsed", false); + builder.Add("sourceCommitId", TwoCommitIds); + builder.Add("description", "the merge, & its description"); + builder.Add("pageSize", 1.5); + builder.Add("name", ValuesContainingTheDelimiter); + + Assert.That(builder.ToString(), Is.EqualTo($"?excludeUsed=false&sourceCommitId={FirstCommitId},{SecondCommitId}&description=the%20merge%2C%20%26%20its%20description&pageSize=1.5&name=first%2C%20value,second%26value")); + } + + [Test] + public void VerifyToString() + { + var builder = new QueryStringBuilder(); + + Assert.That(builder.ToString(), Is.Empty); + + builder.Add("excludeUsed", true); + + Assert.That(builder.ToString(), Is.EqualTo("?excludeUsed=true")); + } + } +} diff --git a/SySML2.NET.REST.Tests/RestClientTestFixture.cs b/SySML2.NET.REST.Tests/RestClientTestFixture.cs deleted file mode 100644 index 1a7da74a5..000000000 --- a/SySML2.NET.REST.Tests/RestClientTestFixture.cs +++ /dev/null @@ -1,101 +0,0 @@ -// ------------------------------------------------------------------------------------------------ -// -// -// Copyright (C) 2022-2026 Starion Group S.A. -// SPDX-License-Identifier: Apache-2.0 -// -// -// ------------------------------------------------------------------------------------------------ - -namespace SySML2.NET.REST.Tests -{ - using System.Linq; - using System.Net.Http; - using System.Threading; - using System.Threading.Tasks; - - using SysML2.NET.Serializer.Json; - - using NUnit.Framework; - - /// - /// Suite of tests for the class. - /// - [TestFixture] - [Explicit("Host not reachable ATM")] - public class RestClientTestFixture - { - private string baseUri; - - private CancellationTokenSource cancellationTokenSource; - - private RestClient restClient; - - [SetUp] - public void Setup() - { - this.baseUri = "http://sysml2-sst.intercax.com:9000"; - - this.cancellationTokenSource = new CancellationTokenSource(); - - var httpClient = new HttpClient(); - var deserializer = new DeSerializer(); - var serializer = new Serializer(); - - this.restClient = new RestClient(httpClient, deserializer, serializer); - } - - [Test] - public async Task Verify_that_RestClient_can_query_projects_branches_tags_commits_elemets_can_be_requested() - { - Assert.DoesNotThrowAsync(async () => await this.restClient.Open("john", "doe", this.baseUri, this.cancellationTokenSource.Token)); - - var projects = await this.restClient.RequestProjects(null, null, this.cancellationTokenSource.Token); - - var projectId = projects.First().Id; - - var p = (await this.restClient.RequestProjects(projectId, null, this.cancellationTokenSource.Token)).Single(); - - Assert.That(p.Id, Is.EqualTo(projectId)); - - foreach (var project in projects) - { - Assert.That(project.Name, Is.Not.Null.Or.Empty); - Assert.That(project.DefaultBranch, Is.Not.Null.Or.Empty); - - var branches = await this.restClient.RequestBranches(project.Id, null, null, this.cancellationTokenSource.Token); - - Assert.That(branches, Is.Not.Empty); - - foreach (var branch in branches) - { - Assert.That(branch.Name, Is.Not.Null.Or.Empty); - Assert.That(branch.Head, Is.Not.Null.Or.Empty); - Assert.That(branch.OwningProject, Is.EqualTo(project.Id)); - } - - var tags = await this.restClient.RequestTags(project.Id, null, null, this.cancellationTokenSource.Token); - - foreach (var tag in tags) - { - Assert.That(tag.Name, Is.Not.Null.Or.Empty); - Assert.That(tag.OwningProject, Is.EqualTo(project.Id)); - } - - var commits = await this.restClient.RequestCommits(project.Id, null, null, this.cancellationTokenSource.Token); - - foreach (var commit in commits) - { - Assert.That(commit.OwningProject, Is.EqualTo(project.Id)); - - // var elements = await this.restClient.RequestElements(project.Id, commit.Id, null, null, this.cancellationTokenSource.Token); - - //foreach (var element in elements) - //{ - // Assert.That(element.Id, Is.Not.Null.Or.Empty); - //} - } - } - } - } -} diff --git a/SySML2.NET.REST.Tests/SessionTestFixture.cs b/SySML2.NET.REST.Tests/SessionTestFixture.cs new file mode 100644 index 000000000..99663b86e --- /dev/null +++ b/SySML2.NET.REST.Tests/SessionTestFixture.cs @@ -0,0 +1,118 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST.Tests +{ + using System; + using System.Net; + using System.Net.Http; + using System.Threading; + using System.Threading.Tasks; + + using Moq; + + using NUnit.Framework; + + using RichardSzalay.MockHttp; + + using SysML2.NET.REST.Exceptions; + using SysML2.NET.Serializer.Json; + + [TestFixture] + public class SessionTestFixture + { + private const string JsonMediaType = "application/json"; + + private MockHttpMessageHandler mockHttp; + + private Mock httpClientFactory; + + private Session session; + + [SetUp] + public void SetUp() + { + this.mockHttp = new MockHttpMessageHandler(); + this.httpClientFactory = new Mock(); + this.httpClientFactory.Setup(factory => factory.CreateClient(nameof(Session))).Returns(() => this.mockHttp.ToHttpClient()); + + this.session = new Session(this.httpClientFactory.Object, new DeSerializer(), new Serializer()); + } + + [TearDown] + public void TearDown() + { + this.mockHttp.Dispose(); + } + + [Test] + public void VerifyConstructor() + { + using (Assert.EnterMultipleScope()) + { + Assert.That(() => new Session(null, new DeSerializer(), new Serializer()), Throws.TypeOf()); + Assert.That(() => new Session(this.httpClientFactory.Object, null, new Serializer()), Throws.TypeOf()); + Assert.That(() => new Session(this.httpClientFactory.Object, new DeSerializer(), null), Throws.TypeOf()); + Assert.That(this.session.Client, Is.Null); + } + } + + [Test] + public async Task VerifyOpen() + { + await Assert.ThatAsync(() => this.session.Open(null, CancellationToken.None), Throws.TypeOf()); + await Assert.ThatAsync(() => this.session.Open(new Uri("api", UriKind.Relative), CancellationToken.None), Throws.TypeOf()); + + this.mockHttp.Expect(HttpMethod.Get, "http://localhost/api/projects").Respond(JsonMediaType, "[]"); + + var projects = await this.session.Open(new Uri("http://localhost/api"), CancellationToken.None); + + using (Assert.EnterMultipleScope()) + { + Assert.That(projects.Records, Has.Count.EqualTo(0)); + Assert.That(this.session.Client, Is.Not.Null); + } + + var firstClient = this.session.Client; + + this.mockHttp.Expect(HttpMethod.Get, "http://otherhost/projects").Respond(JsonMediaType, "[]"); + + await this.session.Open(new Uri("http://otherhost/"), CancellationToken.None); + + Assert.That(this.session.Client, Is.Not.SameAs(firstClient)); + + this.mockHttp.Expect(HttpMethod.Get, "http://localhost/api/projects").Respond(HttpStatusCode.InternalServerError); + + await Assert.ThatAsync(() => this.session.Open(new Uri("http://localhost/api/"), CancellationToken.None), Throws.TypeOf()); + + Assert.That(this.session.Client, Is.Null); + + this.mockHttp.VerifyNoOutstandingExpectation(); + this.httpClientFactory.Verify(factory => factory.CreateClient(nameof(Session)), Times.Exactly(3)); + } + + [Test] + public async Task VerifyClose() + { + this.session.Close(); + + Assert.That(this.session.Client, Is.Null); + + this.mockHttp.Expect(HttpMethod.Get, "http://localhost/api/projects").Respond(JsonMediaType, "[]"); + + await this.session.Open(new Uri("http://localhost/api/"), CancellationToken.None); + + Assert.That(this.session.Client, Is.Not.Null); + + this.session.Close(); + + Assert.That(this.session.Client, Is.Null); + } + } +} diff --git a/SySML2.NET.REST.Tests/SySML2.NET.REST.Tests.csproj b/SySML2.NET.REST.Tests/SySML2.NET.REST.Tests.csproj index 50a379adb..3ae9fbe84 100644 --- a/SySML2.NET.REST.Tests/SySML2.NET.REST.Tests.csproj +++ b/SySML2.NET.REST.Tests/SySML2.NET.REST.Tests.csproj @@ -1,35 +1,45 @@ - - - - net10.0 - 14.0 - Starion Group S.A. - Sam Gerene - Nunit test suite for the SysML2.NET.REST project - Copyright © Starion Group S.A. - Apache-2.0 - https://github.com/STARIONGROUP/SysML2.NET.git - Git - false - disable - false - true - en-US - - - - - - - - - - - - - - - - - - + + + + net10.0 + 14.0 + Starion Group S.A. + Sam Gerene + Nunit test suite for the SysML2.NET.REST project + Copyright © Starion Group S.A. + Apache-2.0 + https://github.com/STARIONGROUP/SysML2.NET.git + Git + false + disable + false + true + en-US + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SySML2.NET.REST/AuthenticationStatusKind.cs b/SySML2.NET.REST/AuthenticationStatusKind.cs deleted file mode 100644 index 18b17245c..000000000 --- a/SySML2.NET.REST/AuthenticationStatusKind.cs +++ /dev/null @@ -1,37 +0,0 @@ -// ------------------------------------------------------------------------------------------------ -// -// -// Copyright (C) 2022-2026 Starion Group S.A. -// SPDX-License-Identifier: Apache-2.0 -// -// -// ------------------------------------------------------------------------------------------------ - -namespace SySML2.NET.REST -{ - /// - /// Enumeration that defines possible status for the authentication process - /// - public enum AuthenticationStatusKind - { - /// - /// Defaut status, when no authentication process has been performed - /// - None, - - /// - /// Status when the authentication process is in progress - /// - Authenticating, - - /// - /// Status when the authentication process ends succesfully - /// - Success, - - /// - /// Status when the authentication process failed - /// - Fail - } -} \ No newline at end of file diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/BranchApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/BranchApi.cs new file mode 100644 index 000000000..ea48225b1 --- /dev/null +++ b/SySML2.NET.REST/AutoGenPsmApiClient/BranchApi.cs @@ -0,0 +1,161 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + + using SysML2.NET.Common; + using SysML2.NET.Core.DTO.Root.Elements; + using SysML2.NET.PSM; + using SysML2.NET.PSM.DTO; + using SysML2.NET.REST.Exceptions; + + /// + /// Provides the Branch operations of the Systems Modeling API and Services + /// + public partial class PsmApiClient + { + /// + /// Get branches by project + /// + /// + /// ID of the project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getBranchesByProject operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> GetBranchesByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/branches{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadPagedResponseAsync(response, cancellationToken); + } + + /// + /// Create branch by project + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the postBranchByProject operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task PostBranchByProjectAsync(Guid projectId, BranchRequest requestBody, CancellationToken cancellationToken = default) + { + ThrowIfNull(requestBody, nameof(requestBody)); + + var query = new QueryStringBuilder(); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects/{Escape(projectId)}/branches{query}", content, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Get branch by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the branch + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getBranchesByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task GetBranchesByProjectAndIdAsync(Guid projectId, Guid branchId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/branches/{Escape(branchId)}{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Delete branch by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the branch + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the deleteBranchByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task DeleteBranchByProjectAndIdAsync(Guid projectId, Guid branchId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(HttpMethod.Delete, $"projects/{Escape(projectId)}/branches/{Escape(branchId)}{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/CommitApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/CommitApi.cs new file mode 100644 index 000000000..a6b228774 --- /dev/null +++ b/SySML2.NET.REST/AutoGenPsmApiClient/CommitApi.cs @@ -0,0 +1,200 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + + using SysML2.NET.Common; + using SysML2.NET.Core.DTO.Root.Elements; + using SysML2.NET.PSM; + using SysML2.NET.PSM.DTO; + using SysML2.NET.REST.Exceptions; + + /// + /// Provides the Commit operations of the Systems Modeling API and Services + /// + public partial class PsmApiClient + { + /// + /// Get commits by project + /// + /// + /// ID of the project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getCommitsByProject operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> GetCommitsByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadPagedResponseAsync(response, cancellationToken); + } + + /// + /// Create commit by project + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// ID of the branch - project's default branch if unspecified + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the postCommitByProject operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task PostCommitByProjectAsync(Guid projectId, CommitRequest requestBody, Guid? branchId = null, CancellationToken cancellationToken = default) + { + ThrowIfNull(requestBody, nameof(requestBody)); + + var query = new QueryStringBuilder(); + query.Add("branchId", branchId); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects/{Escape(projectId)}/commits{query}", content, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Get commit by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getCommitByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task GetCommitByProjectAndIdAsync(Guid projectId, Guid commitId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(commitId)}{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Get changes by project and commit + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getChangesByProjectCommit operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> GetChangesByProjectCommitAsync(Guid projectId, Guid commitId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/changes{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadPagedResponseAsync(response, cancellationToken); + } + + /// + /// Get change by project, commit and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// ID of the change (DataVersion) + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getChangeByProjectCommitId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task GetChangeByProjectCommitIdAsync(Guid projectId, Guid commitId, Guid changeId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/changes/{Escape(changeId)}{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/DiffMergeApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/DiffMergeApi.cs new file mode 100644 index 000000000..4ff7175ec --- /dev/null +++ b/SySML2.NET.REST/AutoGenPsmApiClient/DiffMergeApi.cs @@ -0,0 +1,132 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + + using SysML2.NET.Common; + using SysML2.NET.Core.DTO.Root.Elements; + using SysML2.NET.PSM; + using SysML2.NET.PSM.DTO; + using SysML2.NET.REST.Exceptions; + + /// + /// Provides the Diff & Merge operations of the Systems Modeling API and Services + /// + public partial class PsmApiClient + { + /// + /// Diff a base commit and compare commit + /// + /// + /// ID of the project + /// + /// + /// ID of the base commit + /// + /// + /// ID of the compare commit + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the diff operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> DiffAsync(Guid projectId, Guid baseCommitId, Guid compareCommitId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + query.Add("baseCommitId", baseCommitId); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(compareCommitId)}/diff{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadPagedResponseAsync(response, cancellationToken); + } + + /// + /// Merge source commit(s) into a target branch + /// + /// + /// ID of the project + /// + /// + /// ID(s) of the source commit(s) - comma separated + /// + /// + /// ID of the target branch + /// + /// + /// The payload sent as the body of the request + /// + /// + /// Description of merge commit - in case one is created + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the merge operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when is empty + /// + /// + /// Thrown when the server responds with an error status code + /// + /// + /// Thrown when the server reports conflicting data + /// + public async Task MergeAsync(Guid projectId, IEnumerable sourceCommitId, Guid targetBranchId, IEnumerable requestBody = null, string description = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + ThrowIfNullOrEmpty(sourceCommitId, nameof(sourceCommitId)); + + var query = new QueryStringBuilder(queryParameters); + query.Add("sourceCommitId", sourceCommitId); + query.Add("description", description); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects/{Escape(projectId)}/branches/{Escape(targetBranchId)}/merge{query}", content, cancellationToken); + + await this.ThrowOnConflictAsync(response, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/ElementApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/ElementApi.cs new file mode 100644 index 000000000..3b3d1a3e6 --- /dev/null +++ b/SySML2.NET.REST/AutoGenPsmApiClient/ElementApi.cs @@ -0,0 +1,179 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + + using SysML2.NET.Common; + using SysML2.NET.Core.DTO.Root.Elements; + using SysML2.NET.PSM; + using SysML2.NET.PSM.DTO; + using SysML2.NET.REST.Exceptions; + + /// + /// Provides the Element operations of the Systems Modeling API and Services + /// + public partial class PsmApiClient + { + /// + /// Get elements by project and commit + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// Exclude Data from ProjectUsages + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getElementsByProjectCommit operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> GetElementsByProjectCommitAsync(Guid projectId, Guid commitId, bool? excludeUsed = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + query.Add("excludeUsed", excludeUsed); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/elements{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadPagedDataAsync(response, cancellationToken); + } + + /// + /// Get element by project, commit and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// ID of the element + /// + /// + /// Exclude Data from ProjectUsages + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getElementByProjectCommitId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task GetElementByProjectCommitIdAsync(Guid projectId, Guid commitId, Guid elementId, bool? excludeUsed = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + query.Add("excludeUsed", excludeUsed); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/elements/{Escape(elementId)}{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadDataAsync(response, cancellationToken); + } + + /// + /// Get ProjectUsage that originates the provided element + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// ID of the element + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getProjectUsageByProjectCommitElement operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task GetProjectUsageByProjectCommitElementAsync(Guid projectId, Guid commitId, Guid elementId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/elements/{Escape(elementId)}/projectUsage{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Get root elements by project and commit + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// Exclude Data from ProjectUsages + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getRootsByProjectCommit operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> GetRootsByProjectCommitAsync(Guid projectId, Guid commitId, bool? excludeUsed = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + query.Add("excludeUsed", excludeUsed); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/roots{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadPagedDataAsync(response, cancellationToken); + } + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/IPsmApiClient.cs b/SySML2.NET.REST/AutoGenPsmApiClient/IPsmApiClient.cs new file mode 100644 index 000000000..86b051b8d --- /dev/null +++ b/SySML2.NET.REST/AutoGenPsmApiClient/IPsmApiClient.cs @@ -0,0 +1,840 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + using System; + using System.CodeDom.Compiler; + using System.Collections.Generic; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + + using SysML2.NET.Common; + using SysML2.NET.Core.DTO.Root.Elements; + using SysML2.NET.PSM; + using SysML2.NET.PSM.DTO; + using SysML2.NET.PSM.Enumerations; + using SysML2.NET.REST.Exceptions; + + /// + /// The client of the REST/HTTP platform specific model of the Systems Modeling API and Services + /// + [GeneratedCode("SysML2.NET", "latest")] + public partial interface IPsmApiClient + { + /// + /// Get projects + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getProjects operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> GetProjectsAsync(QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Create project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the postProject operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task PostProjectAsync(ProjectRequest requestBody = null, CancellationToken cancellationToken = default); + + /// + /// Get project by ID + /// + /// + /// ID of the project + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getProjectById operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task GetProjectByIdAsync(Guid projectId, CancellationToken cancellationToken = default); + + /// + /// Update project by ID + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the putProjectById operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task PutProjectByIdAsync(Guid projectId, ProjectRequest requestBody = null, CancellationToken cancellationToken = default); + + /// + /// Delete project by ID + /// + /// + /// ID of the project + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the deleteProjectById operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task DeleteProjectByIdAsync(Guid projectId, CancellationToken cancellationToken = default); + + /// + /// Get branches by project + /// + /// + /// ID of the project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getBranchesByProject operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> GetBranchesByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Create branch by project + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the postBranchByProject operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + Task PostBranchByProjectAsync(Guid projectId, BranchRequest requestBody, CancellationToken cancellationToken = default); + + /// + /// Get branch by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the branch + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getBranchesByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task GetBranchesByProjectAndIdAsync(Guid projectId, Guid branchId, CancellationToken cancellationToken = default); + + /// + /// Delete branch by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the branch + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the deleteBranchByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task DeleteBranchByProjectAndIdAsync(Guid projectId, Guid branchId, CancellationToken cancellationToken = default); + + /// + /// Get tags by project + /// + /// + /// ID of the project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getTagsByProject operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> GetTagsByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Create tag by project + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the postTagByProject operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + Task PostTagByProjectAsync(Guid projectId, TagRequest requestBody, CancellationToken cancellationToken = default); + + /// + /// Get tag by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the tag + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getTagByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task GetTagByProjectAndIdAsync(Guid projectId, Guid tagId, CancellationToken cancellationToken = default); + + /// + /// Delete tag by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the tag + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the deleteTagByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task DeleteTagByProjectAndIdAsync(Guid projectId, Guid tagId, CancellationToken cancellationToken = default); + + /// + /// Get commits by project + /// + /// + /// ID of the project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getCommitsByProject operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> GetCommitsByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Create commit by project + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// ID of the branch - project's default branch if unspecified + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the postCommitByProject operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + Task PostCommitByProjectAsync(Guid projectId, CommitRequest requestBody, Guid? branchId = null, CancellationToken cancellationToken = default); + + /// + /// Get commit by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getCommitByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task GetCommitByProjectAndIdAsync(Guid projectId, Guid commitId, CancellationToken cancellationToken = default); + + /// + /// Get changes by project and commit + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getChangesByProjectCommit operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> GetChangesByProjectCommitAsync(Guid projectId, Guid commitId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Get change by project, commit and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// ID of the change (DataVersion) + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getChangeByProjectCommitId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task GetChangeByProjectCommitIdAsync(Guid projectId, Guid commitId, Guid changeId, CancellationToken cancellationToken = default); + + /// + /// Get elements by project and commit + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// Exclude Data from ProjectUsages + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getElementsByProjectCommit operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> GetElementsByProjectCommitAsync(Guid projectId, Guid commitId, bool? excludeUsed = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Get element by project, commit and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// ID of the element + /// + /// + /// Exclude Data from ProjectUsages + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getElementByProjectCommitId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task GetElementByProjectCommitIdAsync(Guid projectId, Guid commitId, Guid elementId, bool? excludeUsed = null, CancellationToken cancellationToken = default); + + /// + /// Get ProjectUsage that originates the provided element + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// ID of the element + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getProjectUsageByProjectCommitElement operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task GetProjectUsageByProjectCommitElementAsync(Guid projectId, Guid commitId, Guid elementId, CancellationToken cancellationToken = default); + + /// + /// Get root elements by project and commit + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// Exclude Data from ProjectUsages + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getRootsByProjectCommit operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> GetRootsByProjectCommitAsync(Guid projectId, Guid commitId, bool? excludeUsed = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Get relationships by project, commit, and related element + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// ID of the related element + /// + /// + /// Filter for relationships that are incoming (in), outgoing (out), or both relative to the related element + /// + /// + /// Exclude Data from ProjectUsages + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getRelationshipsByProjectCommitRelatedElement operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> GetRelationshipsByProjectCommitRelatedElementAsync(Guid projectId, Guid commitId, Guid relatedElementId, Direction? direction = null, bool? excludeUsed = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Diff a base commit and compare commit + /// + /// + /// ID of the project + /// + /// + /// ID of the base commit + /// + /// + /// ID of the compare commit + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the diff operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> DiffAsync(Guid projectId, Guid baseCommitId, Guid compareCommitId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Merge source commit(s) into a target branch + /// + /// + /// ID of the project + /// + /// + /// ID(s) of the source commit(s) - comma separated + /// + /// + /// ID of the target branch + /// + /// + /// The payload sent as the body of the request + /// + /// + /// Description of merge commit - in case one is created + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the merge operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when is empty + /// + /// + /// Thrown when the server responds with an error status code + /// + /// + /// Thrown when the server reports conflicting data + /// + Task MergeAsync(Guid projectId, IEnumerable sourceCommitId, Guid targetBranchId, IEnumerable requestBody = null, string description = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Get queries by project + /// + /// + /// ID of the project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getQueriesByProject operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> GetQueriesByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Create query by project + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the postQueryByProject operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + Task PostQueryByProjectAsync(Guid projectId, QueryRequest requestBody, CancellationToken cancellationToken = default); + + /// + /// Get query by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the query + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getQueryByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task GetQueryByProjectAndIdAsync(Guid projectId, Guid queryId, CancellationToken cancellationToken = default); + + /// + /// Update project by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the query + /// + /// + /// The payload sent as the body of the request + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the putQueryByProjectAndId operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + Task PutQueryByProjectAndIdAsync(Guid projectId, Guid queryId, QueryRequest requestBody, CancellationToken cancellationToken = default); + + /// + /// Delete query by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the query + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the deleteQueryByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task DeleteQueryByProjectAndIdAsync(Guid projectId, Guid queryId, CancellationToken cancellationToken = default); + + /// + /// Get query results by project and query + /// + /// + /// ID of the project + /// + /// + /// ID of the query + /// + /// + /// ID of the commit - defaults to head of project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getQueryResultsByProjectIdQueryId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> GetQueryResultsByProjectIdQueryIdAsync(Guid projectId, Guid queryId, Guid? commitId = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Get query results by project and query definition + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// ID of the commit - defaults to head of project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getQueryResultsByProjectIdQuery operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> GetQueryResultsByProjectIdQueryAsync(Guid projectId, QueryRequest requestBody, Guid? commitId = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Get query results by project and query definition via POST + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// ID of the commit - defaults to head of project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getQueryResultsByProjectIdQueryPost operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> GetQueryResultsByProjectIdQueryPostAsync(Guid projectId, QueryRequest requestBody, Guid? commitId = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Get datatypes + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getDatatypes operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> GetDatatypesAsync(QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Get datatype by ID + /// + /// + /// ID of the datatype + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getDatatypeById operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when is empty or whitespace + /// + /// + /// Thrown when the server responds with an error status code + /// + Task GetDatatypeByIdAsync(string datatypeId, CancellationToken cancellationToken = default); + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/MetaApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/MetaApi.cs new file mode 100644 index 000000000..b0540e7ab --- /dev/null +++ b/SySML2.NET.REST/AutoGenPsmApiClient/MetaApi.cs @@ -0,0 +1,99 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + + using SysML2.NET.Common; + using SysML2.NET.Core.DTO.Root.Elements; + using SysML2.NET.PSM; + using SysML2.NET.PSM.DTO; + using SysML2.NET.REST.Exceptions; + + /// + /// Provides the Meta operations of the Systems Modeling API and Services + /// + public partial class PsmApiClient + { + /// + /// Get datatypes + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getDatatypes operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> GetDatatypesAsync(QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + + using var response = await this.SendAsync(HttpMethod.Get, $"meta/datatypes{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await ReadPagedJsonAsync(response, cancellationToken); + } + + /// + /// Get datatype by ID + /// + /// + /// ID of the datatype + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getDatatypeById operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when is empty or whitespace + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task GetDatatypeByIdAsync(string datatypeId, CancellationToken cancellationToken = default) + { + ThrowIfNullOrWhiteSpace(datatypeId, nameof(datatypeId)); + + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(HttpMethod.Get, $"meta/datatypes/{Escape(datatypeId)}{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await ReadJsonAsync(response, cancellationToken); + } + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/ProjectApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/ProjectApi.cs new file mode 100644 index 000000000..cef1b1f45 --- /dev/null +++ b/SySML2.NET.REST/AutoGenPsmApiClient/ProjectApi.cs @@ -0,0 +1,176 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + using System; + using System.CodeDom.Compiler; + using System.Collections.Generic; + using System.Net.Http; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + + using SysML2.NET.Common; + using SysML2.NET.Core.DTO.Root.Elements; + using SysML2.NET.PSM; + using SysML2.NET.PSM.DTO; + using SysML2.NET.REST.Exceptions; + + /// + /// Provides the Project operations of the Systems Modeling API and Services + /// + [GeneratedCode("SysML2.NET", "latest")] + public partial class PsmApiClient + { + /// + /// Get projects + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getProjects operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> GetProjectsAsync(QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadPagedResponseAsync(response, cancellationToken); + } + + /// + /// Create project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the postProject operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task PostProjectAsync(ProjectRequest requestBody = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects{query}", content, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Get project by ID + /// + /// + /// ID of the project + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getProjectById operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task GetProjectByIdAsync(Guid projectId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Update project by ID + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the putProjectById operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task PutProjectByIdAsync(Guid projectId, ProjectRequest requestBody = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Put, $"projects/{Escape(projectId)}{query}", content, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Delete project by ID + /// + /// + /// ID of the project + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the deleteProjectById operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task DeleteProjectByIdAsync(Guid projectId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(HttpMethod.Delete, $"projects/{Escape(projectId)}{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/QueryApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/QueryApi.cs new file mode 100644 index 000000000..6eaeb4303 --- /dev/null +++ b/SySML2.NET.REST/AutoGenPsmApiClient/QueryApi.cs @@ -0,0 +1,319 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + + using SysML2.NET.Common; + using SysML2.NET.Core.DTO.Root.Elements; + using SysML2.NET.PSM; + using SysML2.NET.PSM.DTO; + using SysML2.NET.REST.Exceptions; + + /// + /// Provides the Query operations of the Systems Modeling API and Services + /// + public partial class PsmApiClient + { + /// + /// Get queries by project + /// + /// + /// ID of the project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getQueriesByProject operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> GetQueriesByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/queries{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadPagedResponseAsync(response, cancellationToken); + } + + /// + /// Create query by project + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the postQueryByProject operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task PostQueryByProjectAsync(Guid projectId, QueryRequest requestBody, CancellationToken cancellationToken = default) + { + ThrowIfNull(requestBody, nameof(requestBody)); + + var query = new QueryStringBuilder(); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects/{Escape(projectId)}/queries{query}", content, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Get query by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the query + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getQueryByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task GetQueryByProjectAndIdAsync(Guid projectId, Guid queryId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/queries/{Escape(queryId)}{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Update project by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the query + /// + /// + /// The payload sent as the body of the request + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the putQueryByProjectAndId operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task PutQueryByProjectAndIdAsync(Guid projectId, Guid queryId, QueryRequest requestBody, CancellationToken cancellationToken = default) + { + ThrowIfNull(requestBody, nameof(requestBody)); + + var query = new QueryStringBuilder(); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Put, $"projects/{Escape(projectId)}/queries/{Escape(queryId)}{query}", content, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Delete query by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the query + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the deleteQueryByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task DeleteQueryByProjectAndIdAsync(Guid projectId, Guid queryId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(HttpMethod.Delete, $"projects/{Escape(projectId)}/queries/{Escape(queryId)}{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Get query results by project and query + /// + /// + /// ID of the project + /// + /// + /// ID of the query + /// + /// + /// ID of the commit - defaults to head of project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getQueryResultsByProjectIdQueryId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> GetQueryResultsByProjectIdQueryIdAsync(Guid projectId, Guid queryId, Guid? commitId = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + query.Add("commitId", commitId); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/queries/{Escape(queryId)}/results{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadPagedDataAsync(response, cancellationToken); + } + + /// + /// Get query results by project and query definition + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// ID of the commit - defaults to head of project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getQueryResultsByProjectIdQuery operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> GetQueryResultsByProjectIdQueryAsync(Guid projectId, QueryRequest requestBody, Guid? commitId = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + ThrowIfNull(requestBody, nameof(requestBody)); + + var query = new QueryStringBuilder(queryParameters); + query.Add("commitId", commitId); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/query-results{query}", content, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadPagedDataAsync(response, cancellationToken); + } + + /// + /// Get query results by project and query definition via POST + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// ID of the commit - defaults to head of project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getQueryResultsByProjectIdQueryPost operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> GetQueryResultsByProjectIdQueryPostAsync(Guid projectId, QueryRequest requestBody, Guid? commitId = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + ThrowIfNull(requestBody, nameof(requestBody)); + + var query = new QueryStringBuilder(queryParameters); + query.Add("commitId", commitId); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects/{Escape(projectId)}/query-results{query}", content, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadPagedDataAsync(response, cancellationToken); + } + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/RelationshipApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/RelationshipApi.cs new file mode 100644 index 000000000..f45bcd3ec --- /dev/null +++ b/SySML2.NET.REST/AutoGenPsmApiClient/RelationshipApi.cs @@ -0,0 +1,84 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + + using SysML2.NET.Common; + using SysML2.NET.Core.DTO.Root.Elements; + using SysML2.NET.Extensions.PSM; + using SysML2.NET.PSM; + using SysML2.NET.PSM.DTO; + using SysML2.NET.PSM.Enumerations; + using SysML2.NET.REST.Exceptions; + + /// + /// Provides the Relationship operations of the Systems Modeling API and Services + /// + public partial class PsmApiClient + { + /// + /// Get relationships by project, commit, and related element + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// ID of the related element + /// + /// + /// Filter for relationships that are incoming (in), outgoing (out), or both relative to the related element + /// + /// + /// Exclude Data from ProjectUsages + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getRelationshipsByProjectCommitRelatedElement operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> GetRelationshipsByProjectCommitRelatedElementAsync(Guid projectId, Guid commitId, Guid relatedElementId, Direction? direction = null, bool? excludeUsed = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + query.Add("direction", direction is { } directionValue ? ToQueryValue(DirectionProvider.ToUtf8Bytes(directionValue)) : null); + query.Add("excludeUsed", excludeUsed); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/elements/{Escape(relatedElementId)}/relationships{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadPagedDataAsync(response, cancellationToken); + } + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/TagApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/TagApi.cs new file mode 100644 index 000000000..90c4d7795 --- /dev/null +++ b/SySML2.NET.REST/AutoGenPsmApiClient/TagApi.cs @@ -0,0 +1,161 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + + using SysML2.NET.Common; + using SysML2.NET.Core.DTO.Root.Elements; + using SysML2.NET.PSM; + using SysML2.NET.PSM.DTO; + using SysML2.NET.REST.Exceptions; + + /// + /// Provides the Tag operations of the Systems Modeling API and Services + /// + public partial class PsmApiClient + { + /// + /// Get tags by project + /// + /// + /// ID of the project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getTagsByProject operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> GetTagsByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/tags{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadPagedResponseAsync(response, cancellationToken); + } + + /// + /// Create tag by project + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the postTagByProject operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task PostTagByProjectAsync(Guid projectId, TagRequest requestBody, CancellationToken cancellationToken = default) + { + ThrowIfNull(requestBody, nameof(requestBody)); + + var query = new QueryStringBuilder(); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects/{Escape(projectId)}/tags{query}", content, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Get tag by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the tag + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getTagByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task GetTagByProjectAndIdAsync(Guid projectId, Guid tagId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/tags/{Escape(tagId)}{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Delete tag by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the tag + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the deleteTagByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task DeleteTagByProjectAndIdAsync(Guid projectId, Guid tagId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(HttpMethod.Delete, $"projects/{Escape(projectId)}/tags/{Escape(tagId)}{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SySML2.NET.REST/Exceptions/MergeConflictException.cs b/SySML2.NET.REST/Exceptions/MergeConflictException.cs new file mode 100644 index 000000000..427942832 --- /dev/null +++ b/SySML2.NET.REST/Exceptions/MergeConflictException.cs @@ -0,0 +1,71 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.REST.Exceptions +{ + using System; + using System.Collections.Generic; + using System.Net; + + using SysML2.NET.PSM.DTO; + + /// + /// The exception thrown when a merge is rejected because the source and target hold conflicting data + /// + public class MergeConflictException : PsmApiException + { + /// + /// Initializes a new instance of the class + /// + public MergeConflictException() : this(null, null) + { + } + + /// + /// Initializes a new instance of the class with a specified error message + /// + /// + /// The message that describes the error + /// + public MergeConflictException(string message) : this(message, null) + { + } + + /// + /// Initializes a new instance of the class with a specified error message and the exception that caused it + /// + /// + /// The message that describes the error + /// + /// + /// The exception that is the cause of this exception, or null when none is specified + /// + public MergeConflictException(string message, Exception innerException) : base(HttpStatusCode.Conflict, message, innerException) + { + this.Conflicts = []; + } + + /// + /// Initializes a new instance of the class + /// + /// + /// The of each conflicting data item + /// + public MergeConflictException(IReadOnlyList conflicts) + : base(HttpStatusCode.Conflict, null) + { + this.Conflicts = conflicts; + } + + /// + /// Gets the of each conflicting data item + /// + public IReadOnlyList Conflicts { get; } + } +} diff --git a/SySML2.NET.REST/Exceptions/PsmApiException.cs b/SySML2.NET.REST/Exceptions/PsmApiException.cs new file mode 100644 index 000000000..08ed4890c --- /dev/null +++ b/SySML2.NET.REST/Exceptions/PsmApiException.cs @@ -0,0 +1,95 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.REST.Exceptions +{ + using System; + using System.Net; + + using SysML2.NET.PSM.DTO; + + /// + /// The exception thrown when the Systems Modeling API and Services responds with an error status code + /// + public class PsmApiException : Exception + { + /// + /// Initializes a new instance of the class + /// + public PsmApiException() + { + } + + /// + /// Initializes a new instance of the class with a specified error message + /// + /// + /// The message that describes the error + /// + public PsmApiException(string message) : base(message) + { + } + + /// + /// Initializes a new instance of the class with a specified error message and the exception that caused it + /// + /// + /// The message that describes the error + /// + /// + /// The exception that is the cause of this exception, or null when none is specified + /// + public PsmApiException(string message, Exception innerException) : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class for a response with a specified status code + /// + /// + /// The of the response + /// + /// + /// The message that describes the error + /// + /// + /// The exception that is the cause of this exception, or null when none is specified + /// + protected PsmApiException(HttpStatusCode statusCode, string message, Exception innerException) : base(message, innerException) + { + this.StatusCode = statusCode; + } + + /// + /// Initializes a new instance of the class + /// + /// + /// The of the response + /// + /// + /// The carried by the response, or null when the response carries none + /// + public PsmApiException(HttpStatusCode statusCode, Error error) + : base(string.IsNullOrWhiteSpace(error?.Description) ? $"The server responded with status code {(int)statusCode} ({statusCode})" : error.Description) + { + this.StatusCode = statusCode; + this.Error = error; + } + + /// + /// Gets the of the response + /// + public HttpStatusCode StatusCode { get; } + + /// + /// Gets the carried by the response, or null when the response carries none + /// + public Error Error { get; } + } +} diff --git a/SySML2.NET.REST/IRestClient.cs b/SySML2.NET.REST/IRestClient.cs deleted file mode 100644 index eb0ad5081..000000000 --- a/SySML2.NET.REST/IRestClient.cs +++ /dev/null @@ -1,166 +0,0 @@ -// ------------------------------------------------------------------------------------------------ -// -// -// Copyright (C) 2022-2026 Starion Group S.A. -// SPDX-License-Identifier: Apache-2.0 -// -// -// ------------------------------------------------------------------------------------------------ - -namespace SySML2.NET.REST -{ - using System; - using System.Collections.Generic; - using System.Threading.Tasks; - using System.Threading; - - using SysML2.NET.PSM; - using SysML2.NET.PSM.DTO; - using SysML2.NET.Core.DTO.Root.Elements; - - /// - /// The purpose of the is to provide an abstraction over the SysML REST/HTTP PSM protocol - /// - public interface IRestClient - { - /// - /// Opens a connection to the SysML2 model server REST API - /// - /// - /// The username used to login a SysML2 model server - /// - /// - /// The password (secret) used to login a SysML2 model server - /// - /// - /// The uri of the model-server - /// - /// - /// The used to cancel the operation - /// - Task> Open(string username, string password, string uri, CancellationToken cancellationToken); - - /// - /// Closes a connection to the SysML2 model server REST API - /// - void Close(); - - /// - /// Requests the s from the SysML2 model server REST API - /// - /// - /// The unique identifier of the to read. In case this is null all s are read. - /// - /// - /// The used to filter the response on the SysML2 model server REST API - /// - /// - /// The used to cancel the operation - /// - /// - /// The list of s that are available on the SysML2 model server REST API - /// - Task> RequestProjects(Guid? project, QueryParameters queryParameters, CancellationToken cancellationToken); - - /// - /// Requests the es in a from the SysML2 model server REST API - /// - /// - /// The unique identifier of the that contains the requested - /// - /// - /// The unique identifier of the to read. In case this is null all s are read. - /// - /// - /// The used to filter the response on the SysML2 model server REST API - /// - /// - /// The used to cancel the operation - /// - /// - /// A list of es that are contained by the specified , or a single if so requested - /// - Task> RequestBranches(Guid project, Guid? branch, QueryParameters queryParameters, CancellationToken cancellationToken); - - - /// - /// Requests the s in a from the SysML2 model server REST API - /// - /// - /// The unique identifier of the that contains the requested - /// - /// - /// - /// The used to filter the response on the SysML2 model server REST API - /// - /// - /// The used to cancel the operation - /// - /// - /// A list of s that are contained by the specified , or a single if so requested - /// - Task> RequestCommits(Guid project, Guid? commit, QueryParameters queryParameters, CancellationToken cancellationToken); - - /// - /// Requests the s in a from the SysML2 model server REST API - /// - /// - /// The unique identifier of the that contains the requested - /// - /// - /// - /// The used to filter the response on the SysML2 model server REST API - /// - /// - /// The used to cancel the operation - /// - /// - /// A list of s that are contained by the specified , or a single if so requested - /// - /// - Task> RequestTags(Guid project, Guid? tag, QueryParameters queryParameters, CancellationToken cancellationToken); - - /// - /// Requests the s in a of a from the SysML2 model server REST API - /// - /// - /// The unique identifier of the that contains the requested s - /// - /// - /// The unique identifier of the that contains the requested s - /// - /// - /// The unique identifier of the to read. In case this is null all s are read. - /// - /// - /// The used to filter the response on the SysML2 model server REST API - /// - /// - /// The used to cancel the operation - /// - /// - /// A list of s that are contained by the specified and , or a single if so requested - /// - Task> RequestElements(Guid project, Guid commit, Guid? element, QueryParameters queryParameters, CancellationToken cancellationToken); - - /// - /// Requests the root s in a of a from the SysML2 model server REST API - /// - /// - /// The unique identifier of the that contains the requested s - /// - /// - /// The unique identifier of the that contains the requested s - /// - /// - /// The used to filter the response on the SysML2 model server REST API - /// - /// - /// The used to cancel the operation - /// - /// - /// A list of s that are contained by the specified and , or a single if so requested - /// - Task> RequestRootElements(Guid project, Guid commit, QueryParameters queryParameters, CancellationToken cancellationToken); - } -} diff --git a/SySML2.NET.REST/ISession.cs b/SySML2.NET.REST/ISession.cs index 376d6c230..f39253834 100644 --- a/SySML2.NET.REST/ISession.cs +++ b/SySML2.NET.REST/ISession.cs @@ -1,38 +1,48 @@ -// ------------------------------------------------------------------------------------------------ -// -// -// Copyright (C) 2022-2026 Starion Group S.A. -// SPDX-License-Identifier: Apache-2.0 -// -// -// ------------------------------------------------------------------------------------------------ - -namespace SySML2.NET.REST -{ - using System.Threading; - using System.Threading.Tasks; - - /// - /// Definition of the used to provide an abstraction over the SysML2 model server REST API - /// - public interface ISession - { - /// - /// Opens a connection to the SysML2 model server REST API - /// - /// The username used to login a SysML2 model server - /// The password (secret) used to login a SysML2 model server - /// - /// The uri of the model-server - /// - /// - /// The used to cancel the operation - /// - Task Open(string username, string password, string uri, CancellationToken cancellationToken); - - /// - /// Closes a connection to the SysML2 model server REST API - /// - void Close(); - } -} +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + using System; + using System.Threading; + using System.Threading.Tasks; + + using SysML2.NET.PSM; + using SysML2.NET.PSM.DTO; + + /// + /// Definition of the that holds the connection to a server of the Systems Modeling API and Services + /// + public interface ISession + { + /// + /// Gets the of the open connection, or null when the session is closed + /// + IPsmApiClient Client { get; } + + /// + /// Opens a connection to the server, replacing any open connection, and reads the first page of its projects + /// + /// + /// The absolute base address of the server + /// + /// + /// The that cancels the operation + /// + /// + /// The first page of the projects of the server + /// + Task> Open(Uri baseAddress, CancellationToken cancellationToken); + + /// + /// Closes the connection to the server + /// + void Close(); + } +} diff --git a/SySML2.NET.REST/PsmApiClient.cs b/SySML2.NET.REST/PsmApiClient.cs new file mode 100644 index 000000000..199224f9e --- /dev/null +++ b/SySML2.NET.REST/PsmApiClient.cs @@ -0,0 +1,690 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Net; + using System.Net.Http; + using System.Net.Http.Headers; + using System.Runtime.Serialization; + using System.Text; + using System.Text.Json; + using System.Text.RegularExpressions; + using System.Threading; + using System.Threading.Tasks; + + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Logging.Abstractions; + + using SysML2.NET.Common; + using SysML2.NET.PSM; + using SysML2.NET.PSM.DTO; + using SysML2.NET.REST.Exceptions; + using SysML2.NET.Serializer.Json; + + /// + /// The client of the REST/HTTP platform specific model of the Systems Modeling API and Services + /// + public partial class PsmApiClient : IPsmApiClient + { + /// + /// The media type of every payload exchanged with the Systems Modeling API and Services + /// + private const string JsonMediaType = "application/json"; + + /// + /// The pattern of one link-value of an RFC 8288 Link header, capturing its target and its relation type + /// + private const string LinkValueExpression = "<(?[^>]*)>[^,]*?;\\s*rel=\"?(?[^\";,]+)\"?"; + + /// + /// The time, in milliseconds, after which matching a Link header value times out + /// + private const int LinkValueMatchTimeoutMilliseconds = 1000; + +#if NET7_0_OR_GREATER + /// + /// Gets the that matches one link-value of an RFC 8288 Link header + /// + /// + /// The + /// + [GeneratedRegex(LinkValueExpression, RegexOptions.None, LinkValueMatchTimeoutMilliseconds)] + private static partial Regex LinkValuePattern(); +#else + /// + /// The that matches one link-value of an RFC 8288 Link header + /// + private static readonly Regex LinkValueRegex = new(LinkValueExpression, RegexOptions.Compiled, TimeSpan.FromMilliseconds(LinkValueMatchTimeoutMilliseconds)); + + /// + /// Gets the that matches one link-value of an RFC 8288 Link header + /// + /// + /// The + /// + private static Regex LinkValuePattern() => LinkValueRegex; +#endif + + /// + /// The used to write every request payload + /// + private static readonly JsonWriterOptions WriterOptions = new(); + + /// + /// The whose addresses the server + /// + private readonly HttpClient httpClient; + + /// + /// The that reads the response payloads + /// + private readonly IDeSerializer deSerializer; + + /// + /// The that writes the request payloads + /// + private readonly ISerializer serializer; + + /// + /// The used to log + /// + private readonly ILogger logger; + + /// + /// Initializes a new instance of the class + /// + /// + /// The whose addresses the server, ending with a slash + /// + /// + /// The that reads the response payloads + /// + /// + /// The that writes the request payloads + /// + /// + /// The used to set up logging, or null to disable logging + /// + /// + /// Thrown when , or is null + /// + public PsmApiClient(HttpClient httpClient, IDeSerializer deSerializer, ISerializer serializer, ILoggerFactory loggerFactory = null) + { + this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + this.deSerializer = deSerializer ?? throw new ArgumentNullException(nameof(deSerializer)); + this.serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); + this.logger = loggerFactory == null ? NullLogger.Instance : loggerFactory.CreateLogger(); + } + + /// + /// Escapes a path parameter so that it forms a single path segment + /// + /// + /// The value of the path parameter + /// + /// + /// The escaped value + /// + private static string Escape(object value) + { + return Uri.EscapeDataString(Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty); + } + + /// + /// Converts the UTF-8 representation of an enumeration literal into the value of a query parameter + /// + /// + /// The UTF-8 encoded representation of the literal in the Systems Modeling API and Services + /// + /// + /// The value of the query parameter + /// + private static string ToQueryValue(ReadOnlySpan utf8Value) + { + return Encoding.UTF8.GetString(utf8Value); + } + + /// + /// Throws when a required sequence parameter is null or empty + /// + /// + /// The type of the items of the sequence + /// + /// + /// The value of the parameter + /// + /// + /// The name of the parameter + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when is empty + /// + private static void ThrowIfNullOrEmpty(IEnumerable value, string parameterName) + { + if (value is null) + { + throw new ArgumentNullException(parameterName); + } + + if (!value.Any()) + { + throw new ArgumentException("At least one value is required", parameterName); + } + } + + /// + /// Throws when a required text parameter is null, empty or whitespace + /// + /// + /// The value of the parameter + /// + /// + /// The name of the parameter + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when is empty or whitespace + /// + private static void ThrowIfNullOrWhiteSpace(string value, string parameterName) + { + if (value is null) + { + throw new ArgumentNullException(parameterName); + } + + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("A non-whitespace value is required", parameterName); + } + } + + /// + /// Throws when a required request body is null + /// + /// + /// The request body + /// + /// + /// The name of the parameter + /// + /// + /// Thrown when is null + /// + private static void ThrowIfNull(object value, string parameterName) + { + if (value is null) + { + throw new ArgumentNullException(parameterName); + } + } + + /// + /// Queries the page that the link with the specified relation type of the Link header points to + /// + /// + /// The whose Link header is read + /// + /// + /// The relation types, any of which selects the link + /// + /// + /// The carrying the token exactly as the service returned it, or null when the header has no such link or the link carries no page token + /// + private static Page? QueryLinkedPage(HttpResponseMessage response, params string[] relationTypes) + { + if (!response.Headers.TryGetValues("Link", out var headerValues)) + { + return null; + } + + var target = headerValues + .SelectMany(headerValue => LinkValuePattern().Matches(headerValue).Cast()) + .Where(match => relationTypes.Contains(match.Groups["relation"].Value, StringComparer.OrdinalIgnoreCase)) + .Select(match => match.Groups["target"].Value) + .FirstOrDefault(); + + if (target is null) + { + return null; + } + + var queryStart = target.IndexOf('?'); + + var token = queryStart < 0 + ? null + : target.Substring(queryStart + 1) + .Split('&') + .Select(pair => pair.Split(['='], 2)) + .Where(pair => pair.Length == 2) + .Where(pair => Uri.UnescapeDataString(pair[0]) is "page[after]" or "page[before]") + .Select(pair => Uri.UnescapeDataString(pair[1])) + .FirstOrDefault(); + + return string.IsNullOrWhiteSpace(token) ? null : new Page(token); + } + + /// + /// Creates the JSON body of a request from a request payload + /// + /// + /// The to send, or null to send no body + /// + /// + /// The that cancels the serialization + /// + /// + /// The , or null when is null + /// + private async Task CreateContentAsync(IRequest requestBody, CancellationToken cancellationToken) + { + if (requestBody is null) + { + return null; + } + + var stream = new MemoryStream(); + + await this.serializer.SerializeRequestAsync(requestBody, SerializationModeKind.JSON, false, stream, WriterOptions, cancellationToken); + + return CreateContent(stream); + } + + /// + /// Creates the JSON body of a request from a sequence of data items + /// + /// + /// The items to send, or null to send no body + /// + /// + /// The that cancels the serialization + /// + /// + /// The , or null when is null + /// + private async Task CreateContentAsync(IEnumerable requestBody, CancellationToken cancellationToken) + { + if (requestBody is null) + { + return null; + } + + var stream = new MemoryStream(); + + await this.serializer.SerializeAsync(requestBody, SerializationModeKind.JSON, false, stream, WriterOptions, cancellationToken); + + return CreateContent(stream); + } + + /// + /// Wraps a serialized payload in an of the JSON media type + /// + /// + /// The that holds the serialized payload + /// + /// + /// The + /// + private static StreamContent CreateContent(MemoryStream stream) + { + stream.Position = 0; + + var content = new StreamContent(stream); + content.Headers.ContentType = new MediaTypeHeaderValue(JsonMediaType); + + return content; + } + + /// + /// Reads the body of a response as a + /// + /// + /// The to read + /// + /// + /// The that cancels the read + /// + /// + /// The , which the caller disposes + /// + private static Task ReadContentStreamAsync(HttpContent content, CancellationToken cancellationToken) + { +#if NET5_0_OR_GREATER + return content.ReadAsStreamAsync(cancellationToken); +#else + cancellationToken.ThrowIfCancellationRequested(); + + return content.ReadAsStreamAsync(); +#endif + } + + /// + /// Throws a carrying the deserialized when the response is not successful + /// + /// + /// The to check + /// + /// + /// The that cancels the deserialization + /// + /// + /// An awaitable + /// + /// + /// Thrown when the response has an error status code + /// + private async Task EnsureSuccessAsync(HttpResponseMessage response, CancellationToken cancellationToken) + { + if (response.IsSuccessStatusCode) + { + return; + } + + Error error = null; + + if (response.Content?.Headers.ContentType?.MediaType == JsonMediaType) + { + await using var stream = await ReadContentStreamAsync(response.Content, cancellationToken); + + error = await this.ReadErrorAsync(stream, response.StatusCode, cancellationToken); + } + + throw new PsmApiException(response.StatusCode, error); + } + + /// + /// Reads the carried by the body of an error response + /// + /// + /// The that holds the body of the response + /// + /// + /// The of the response, used to log an unreadable body + /// + /// + /// The that cancels the deserialization + /// + /// + /// The , or null when the body is not a readable + /// + private async Task ReadErrorAsync(Stream stream, HttpStatusCode statusCode, CancellationToken cancellationToken) + { + try + { + return await this.deSerializer.DeSerializeResponseAsync(stream, SerializationModeKind.JSON, false, cancellationToken); + } + catch (Exception exception) when (exception is JsonException or SerializationException or InvalidOperationException) + { + this.logger.LogWarning(exception, "The {StatusCode} response does not carry a readable Error", statusCode); + return null; + } + } + + /// + /// Throws a carrying the conflicting data when the response reports a conflict with a JSON body + /// + /// + /// The to check + /// + /// + /// The that cancels the deserialization + /// + /// + /// An awaitable + /// + /// + /// Thrown when the response has the status code and a JSON body listing the conflicting data + /// + /// + /// Thrown when the response has the status code and a JSON body that does not list the conflicting data + /// + private async Task ThrowOnConflictAsync(HttpResponseMessage response, CancellationToken cancellationToken) + { + if (response.StatusCode != HttpStatusCode.Conflict || response.Content?.Headers.ContentType?.MediaType != JsonMediaType) + { + return; + } + + await using var buffer = new MemoryStream(); + + await using (var stream = await ReadContentStreamAsync(response.Content, cancellationToken)) + { + await stream.CopyToAsync(buffer, cancellationToken); + } + + buffer.Position = 0; + + IEnumerable conflicts; + + try + { + conflicts = await this.deSerializer.DeSerializeResponsesAsync(buffer, SerializationModeKind.JSON, false, cancellationToken); + } + catch (Exception exception) when (exception is JsonException or SerializationException or InvalidOperationException) + { + this.logger.LogWarning(exception, "The {StatusCode} response does not carry readable conflicting data", response.StatusCode); + + buffer.Position = 0; + + throw new PsmApiException(response.StatusCode, await this.ReadErrorAsync(buffer, response.StatusCode, cancellationToken)); + } + + throw new MergeConflictException(conflicts.ToList()); + } + + /// + /// Reads a single response payload + /// + /// + /// The type of the payload + /// + /// + /// The successful + /// + /// + /// The that cancels the deserialization + /// + /// + /// The payload, or null when the response has the status code + /// + private async Task ReadResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken) where T : IResponse + { + if (response.StatusCode == HttpStatusCode.NoContent) + { + return default; + } + + await using var stream = await ReadContentStreamAsync(response.Content, cancellationToken); + + return await this.deSerializer.DeSerializeResponseAsync(stream, SerializationModeKind.JSON, false, cancellationToken); + } + + /// + /// Reads one page of response payloads together with the pages that the Link header points to + /// + /// + /// The type of the payloads + /// + /// + /// The successful + /// + /// + /// The that cancels the deserialization + /// + /// + /// The + /// + private async Task> ReadPagedResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken) where T : IResponse + { + await using var stream = await ReadContentStreamAsync(response.Content, cancellationToken); + + var records = await this.deSerializer.DeSerializeResponsesAsync(stream, SerializationModeKind.JSON, false, cancellationToken); + + return CreatePagedResult(response, records.ToList()); + } + + /// + /// Reads a single data payload, such as an element + /// + /// + /// The type of the payload + /// + /// + /// The successful + /// + /// + /// The that cancels the deserialization + /// + /// + /// The payload + /// + /// + /// Thrown when the response does not carry exactly one data item + /// + private async Task ReadDataAsync(HttpResponseMessage response, CancellationToken cancellationToken) where T : IData + { + await using var stream = await ReadContentStreamAsync(response.Content, cancellationToken); + + var data = await this.deSerializer.DeSerializeAsync(stream, SerializationModeKind.JSON, SerializationTargetKind.PSM, false, cancellationToken); + + return data.Single(); + } + + /// + /// Reads one page of data payloads, such as elements, together with the pages that the Link header points to + /// + /// + /// The type of the payloads + /// + /// + /// The successful + /// + /// + /// The that cancels the deserialization + /// + /// + /// The + /// + private async Task> ReadPagedDataAsync(HttpResponseMessage response, CancellationToken cancellationToken) where T : IData + { + await using var stream = await ReadContentStreamAsync(response.Content, cancellationToken); + + var data = await this.deSerializer.DeSerializeAsync(stream, SerializationModeKind.JSON, SerializationTargetKind.PSM, false, cancellationToken); + + return CreatePagedResult(response, data.ToList()); + } + + /// + /// Reads a payload that has no data transfer object, such as a JSON schema + /// + /// + /// The successful + /// + /// + /// The that cancels the parsing + /// + /// + /// The parsed , which the caller disposes + /// + private static async Task ReadJsonAsync(HttpResponseMessage response, CancellationToken cancellationToken) + { + await using var stream = await ReadContentStreamAsync(response.Content, cancellationToken); + + return await JsonDocument.ParseAsync(stream, default, cancellationToken); + } + + /// + /// Reads a payload that has no data transfer object, such as a JSON schema, together with the pages that the Link header points to + /// + /// + /// The successful + /// + /// + /// The that cancels the parsing + /// + /// + /// The whose single record is the parsed , which the caller disposes + /// + private static async Task> ReadPagedJsonAsync(HttpResponseMessage response, CancellationToken cancellationToken) + { + var document = await ReadJsonAsync(response, cancellationToken); + + return CreatePagedResult(response, [document]); + } + + /// + /// Creates a from the records of a response and the pages that its Link header points to + /// + /// + /// The type of the records + /// + /// + /// The successful + /// + /// + /// The records of the page + /// + /// + /// The + /// + private static PagedResult CreatePagedResult(HttpResponseMessage response, IReadOnlyList records) + { + return new PagedResult + { + Records = records, + Next = QueryLinkedPage(response, "next"), + Previous = QueryLinkedPage(response, "prev", "previous") + }; + } + + /// + /// Sends a request to the server + /// + /// + /// The of the request + /// + /// + /// The request URI, relative to the + /// + /// + /// The body of the request, or null to send no body + /// + /// + /// The that cancels the request + /// + /// + /// The , which the caller disposes + /// + private async Task SendAsync(HttpMethod method, string requestUri, HttpContent content, CancellationToken cancellationToken) + { + using var requestMessage = new HttpRequestMessage(method, requestUri); + requestMessage.Content = content; + requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(JsonMediaType)); + + var response = await this.httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + + if (this.logger.IsEnabled(LogLevel.Debug)) + { + this.logger.LogDebug("{Method} {RequestUri} responded {StatusCode}", method, requestUri, response.StatusCode); + } + + return response; + } + } +} diff --git a/SySML2.NET.REST/QueryStringBuilder.cs b/SySML2.NET.REST/QueryStringBuilder.cs new file mode 100644 index 000000000..5b061785d --- /dev/null +++ b/SySML2.NET.REST/QueryStringBuilder.cs @@ -0,0 +1,121 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + + using SysML2.NET.PSM; + + /// + /// Builds the query string of a request to the Systems Modeling API and Services, skipping parameters without a value + /// + internal sealed class QueryStringBuilder + { + /// + /// The encoded name/value pairs of the query string, in the order they were added + /// + private readonly List parameters = []; + + /// + /// Initializes a new instance of the class + /// + /// + /// The that select the page of the result, or null when the request is not paged + /// + public QueryStringBuilder(QueryParameters queryParameters = null) + { + if (queryParameters is null) + { + return; + } + + if (queryParameters.PageSize > 0) + { + this.Add("page[size]", queryParameters.PageSize); + } + + this.Add("page[after]", queryParameters.PageAfter); + this.Add("page[before]", queryParameters.PageBefore); + } + + /// + /// Adds a query parameter, writing a sequence as a comma separated list in the OpenAPI form style without explode + /// + /// + /// The name of the query parameter as declared in the OpenAPI document + /// + /// + /// The value of the query parameter, or null or an empty sequence to omit it + /// + public void Add(string name, object value) + { + var text = value switch + { + null => null, + string stringValue => Uri.EscapeDataString(stringValue), + bool booleanValue => booleanValue ? "true" : "false", + IEnumerable sequence => FormatSequence(sequence), + _ => FormatValue(value) + }; + + if (text is not null) + { + this.parameters.Add($"{name}={text}"); + } + } + + /// + /// Returns the query string, including its leading question mark + /// + /// + /// The query string, or an empty string when no parameter has a value + /// + public override string ToString() + { + return this.parameters.Count == 0 ? string.Empty : $"?{string.Join("&", this.parameters)}"; + } + + /// + /// Formats a sequence as its escaped values joined by a literal comma + /// + /// + /// The sequence to format + /// + /// + /// The formatted list, or null when the sequence is empty + /// + private static string FormatSequence(IEnumerable sequence) + { + var values = sequence.Cast().Select(FormatValue).ToList(); + + return values.Count == 0 ? null : string.Join(',', values); + } + + /// + /// Formats a scalar value using the invariant culture and escapes it + /// + /// + /// The value to format + /// + /// + /// The escaped value, or null when the value formats as null + /// + private static string FormatValue(object value) + { + var text = Convert.ToString(value, CultureInfo.InvariantCulture); + + return text is null ? null : Uri.EscapeDataString(text); + } + } +} diff --git a/SySML2.NET.REST/RestClient.cs b/SySML2.NET.REST/RestClient.cs deleted file mode 100644 index 74d7d9848..000000000 --- a/SySML2.NET.REST/RestClient.cs +++ /dev/null @@ -1,410 +0,0 @@ -// ------------------------------------------------------------------------------------------------ -// -// -// Copyright (C) 2022-2026 Starion Group S.A. -// SPDX-License-Identifier: Apache-2.0 -// -// -// ------------------------------------------------------------------------------------------------ - -namespace SySML2.NET.REST -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Net.Http; - using System.Threading.Tasks; - using System.Threading; - - using Microsoft.Extensions.Logging; - using Microsoft.Extensions.Logging.Abstractions; - - using SysML2.NET.PSM; - using SysML2.NET.Common; - using SysML2.NET.Core.DTO.Root.Elements; - using SysML2.NET.PSM.DTO; - using SysML2.NET.Serializer.Json; - - /// - /// The purpose of the is to provide an abstraction over the SysML REST/HTTP PSM protocol - /// - public class RestClient : IRestClient - { - /// - /// Dependency injected used to perform HTTP requests on the REST API - /// - private readonly HttpClient httpClient; - - /// - /// Dependency injected used to deserialize from JSON - /// - private readonly IDeSerializer deserializer; - - /// - /// Dependency injected used to serialize to JSON - /// - private readonly ISerializer serializer; - - /// - /// The base uri or hostname of the model server REST API - /// - private string baseUri; - - /// - /// The used to log - /// - private readonly ILogger logger; - - /// - /// Initializes a new instance of the class - /// - /// - /// Dependency injected used to perform HTTP requests - /// - /// - /// Dependency injected used to deserialize from JSON - /// - /// - /// Dependency injected used to serialize to JSON - /// - /// /// - /// The (injected) used to setup logging - /// - public RestClient(HttpClient httpClient, IDeSerializer deserializer, ISerializer serializer, ILoggerFactory loggerFactory = null) - { - this.httpClient = httpClient; - this.deserializer = deserializer; - this.serializer = serializer; - - this.logger = loggerFactory == null ? NullLogger.Instance : loggerFactory.CreateLogger(); - } - - /// - /// Opens a connection to the SysML2 model server REST API - /// - /// - /// The username used to login a SysML2 model server - /// - /// - /// The password (secret) used to login a SysML2 model server - /// - /// - /// The uri of the model-server - /// - /// - /// The used to cancel the operation - /// - /// - /// The list of s that the user has access to - /// - public async Task> Open(string username, string password, string uri, CancellationToken cancellationToken) - { - try - { - this.baseUri = uri; - - var projects = await this.RequestProjects(null, null, cancellationToken); - return projects; - } - catch (Exception) - { - this.baseUri = null; - throw; - } - } - - /// - /// Closes a connection to the SysML2 model server REST API - /// - public void Close() - { - this.baseUri = null; - } - - /// - /// Requests the s from the SysML2 model server REST API - /// - /// - /// The unique identifier of the to read. In case this is null all s are read. - /// - /// - /// The used to filter the response on the SysML2 model server REST API - /// - /// - /// The used to cancel the operation - /// - /// - /// The list of s that are available on the SysML2 model server REST API - /// - public async Task> RequestProjects(Guid? project, QueryParameters queryParameters, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(this.baseUri)) - { - throw new InvalidOperationException("prior to executing a REST request the client must be opened. Please use the Open method to do so"); - } - - var requestUri = project == null ? new Uri($"{this.baseUri}/projects{queryParameters}") : new Uri($"{this.baseUri}/projects/{project}{queryParameters}"); - - var data = await this.RequestData(requestUri, cancellationToken); - - if (data != null && data.Any()) - { - return data.OfType(); - } - - return Enumerable.Empty(); - } - - /// - /// Requests the es in a from the SysML2 model server REST API - /// - /// - /// The unique identifier of the that contains the requested - /// - /// - /// The unique identifier of the to read. In case this is null all s are read. - /// - /// - /// The used to filter the response on the SysML2 model server REST API - /// - /// - /// The used to cancel the operation - /// - /// - /// A list of es that are contained by the specified , or a single if so requested - /// - public async Task> RequestBranches(Guid project, Guid? branch, QueryParameters queryParameters, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(this.baseUri)) - { - throw new InvalidOperationException("prior to executing a REST request the client must be opened. Please use the Open method to do so"); - } - - if (project == Guid.Empty) - { - throw new ArgumentException("The project unique identifier may not be the empty Guid", nameof(project)); - } - - var requestUri = branch == null ? new Uri($"{this.baseUri}/projects/{project}/branches{queryParameters}") : new Uri($"{this.baseUri}/projects/{project}/branches/{branch}{queryParameters}"); - - this.logger.LogDebug(requestUri.ToString()); - - var data = await this.RequestData(requestUri, cancellationToken); - - if (data != null && data.Any()) - { - return data.OfType(); - } - - return Enumerable.Empty(); - } - - /// - /// Requests the s in a from the SysML2 model server REST API - /// - /// - /// The unique identifier of the that contains the requested - /// - /// - /// - /// The used to filter the response on the SysML2 model server REST API - /// - /// - /// The used to cancel the operation - /// - /// - /// A list of s that are contained by the specified , or a single if so requested - /// - public async Task> RequestCommits(Guid project, Guid? commit, QueryParameters queryParameters, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(this.baseUri)) - { - throw new InvalidOperationException("prior to executing a REST request the client must be opened. Please use the Open method to do so"); - } - - if (project == Guid.Empty) - { - throw new ArgumentException("The project unique identifier may not be the empty Guid", nameof(project)); - } - - var requestUri = commit == null ? new Uri($"{this.baseUri}/projects/{project}/commits{queryParameters}") : new Uri($"{this.baseUri}/projects/{project}/commits/{commit}{queryParameters}"); - var data = await this.RequestData(requestUri, cancellationToken); - - if (data != null && data.Any()) - { - return data.OfType(); - } - - return Enumerable.Empty(); - } - - /// - /// Requests the s in a from the SysML2 model server REST API - /// - /// - /// The unique identifier of the that contains the requested - /// - /// - /// - /// The used to filter the response on the SysML2 model server REST API - /// - /// - /// The used to cancel the operation - /// - /// - /// A list of s that are contained by the specified , or a single if so requested - /// - /// - public async Task> RequestTags(Guid project, Guid? tag, QueryParameters queryParameters, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(this.baseUri)) - { - throw new InvalidOperationException("prior to executing a REST request the client must be opened. Please use the Open method to do so"); - } - - if (project == Guid.Empty) - { - throw new ArgumentException("The project unique identifier may not be the empty Guid", nameof(project)); - } - - var requestUri = tag == null ? new Uri($"{this.baseUri}/projects/{project}/tags{queryParameters}") : new Uri($"{this.baseUri}/projects/{project}/tags/{tag}{queryParameters}"); - var data = await this.RequestData(requestUri, cancellationToken); - - if (data != null && data.Any()) - { - return data.OfType(); - } - - return Enumerable.Empty(); - } - - /// - /// Requests the s in a of a from the SysML2 model server REST API - /// - /// - /// The unique identifier of the that contains the requested s - /// - /// - /// The unique identifier of the that contains the requested s - /// - /// - /// The unique identifier of the to read. In case this is null all s are read. - /// - /// - /// The used to filter the response on the SysML2 model server REST API - /// - /// - /// The used to cancel the operation - /// - /// - /// A list of s that are contained by the specified and , or a single if so requested - /// - public async Task> RequestElements(Guid project, Guid commit, Guid? element, QueryParameters queryParameters, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(this.baseUri)) - { - throw new InvalidOperationException("prior to executing a REST request the client must be opened. Please use the Open method to do so"); - } - - if (project == Guid.Empty) - { - throw new ArgumentException("The project unique identifier may not be the empty Guid", nameof(project)); - } - - if (commit == Guid.Empty) - { - throw new ArgumentException("The commit unique identifier may not be the empty Guid", nameof(commit)); - } - - var requestUri = element == null ? new Uri($"{this.baseUri}/projects/{project}/commits/{commit}/elements{queryParameters}") : new Uri($"{this.baseUri}/projects/{project}/commits/{commit}/elements/{element}{queryParameters}"); - var data = await this.RequestData(requestUri, cancellationToken); - - if (data != null && data.Any()) - { - return data.OfType(); - } - - return Enumerable.Empty(); - - } - - /// - /// Requests the root s in a of a from the SysML2 model server REST API - /// - /// - /// The unique identifier of the that contains the requested s - /// - /// - /// The unique identifier of the that contains the requested s - /// - /// - /// The used to filter the response on the SysML2 model server REST API - /// - /// - /// The used to cancel the operation - /// - /// - /// A list of s that are contained by the specified and , or a single if so requested - /// - public async Task> RequestRootElements(Guid project, Guid commit, QueryParameters queryParameters, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(this.baseUri)) - { - throw new InvalidOperationException("prior to executing a REST request the client must be opened. Please use the Open method to do so"); - } - - if (project == Guid.Empty) - { - throw new ArgumentException("The project unique identifier may not be the empty Guid", nameof(project)); - } - - if (commit == Guid.Empty) - { - throw new ArgumentException("The commit unique identifier may not be the empty Guid", nameof(commit)); - } - - var requestUri = new Uri($"{this.baseUri}/projects/{project}/commits/{commit}/roots{queryParameters}"); - var data = await this.RequestData(requestUri, cancellationToken); - if (data != null && data.Any()) - { - return data.OfType(); - } - - return Enumerable.Empty(); - } - - /// - /// Requests an from the SysML2 model server REST API - /// - /// - /// The request including the query parameters - /// - /// - /// The used to cancel the operation - /// - /// - private async Task> RequestData(Uri requestUri, CancellationToken cancellationToken) - { - if (this.logger.IsEnabled(LogLevel.Debug)) - { - this.logger.LogDebug("request data from: {0}", requestUri); - } - - var requestMessage = new HttpRequestMessage() - { - Method = HttpMethod.Get, - RequestUri = requestUri, - }; - - using var response = await this.httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken); - - response.EnsureSuccessStatusCode(); - - using var stream = await response.Content.ReadAsStreamAsync(); - - var result = await this.deserializer.DeSerializeAsync(stream, SerializationModeKind.JSON, SerializationTargetKind.PSM, false, cancellationToken); - - return result; - } - } -} diff --git a/SySML2.NET.REST/Session.cs b/SySML2.NET.REST/Session.cs index afd2c5a3d..0b44bc269 100644 --- a/SySML2.NET.REST/Session.cs +++ b/SySML2.NET.REST/Session.cs @@ -1,78 +1,137 @@ -// ------------------------------------------------------------------------------------------------ -// -// -// Copyright (C) 2022-2026 Starion Group S.A. -// SPDX-License-Identifier: Apache-2.0 -// -// -// ------------------------------------------------------------------------------------------------ - -namespace SySML2.NET.REST -{ - using System; - using System.Threading; - using System.Threading.Tasks; - - using SysML2.NET.PIM; - - /// - /// The purpose of the class used to provide an abstraction over the REST API - /// - public class Session : ISession - { - /// - /// The base uri or hostname of the model server REST API - /// - private string baseUri; - - /// - /// Dependency injected used to perform HTTP requests - /// - private readonly IRestClient restClient; - - /// - /// Initializes a new instance of the class - /// - /// - /// Dependency injected used to perform HTTP requests - /// - public Session(IRestClient restClient) - { - this.restClient = restClient; - } - - /// - /// Opens a connection to the SysML2 model server REST API - /// - /// The username used to login a SysML2 model server - /// The password (secret) used to login a SysML2 model server - /// - /// The uri of the model-server - /// - /// - /// The used to cancel the operation - /// - public async Task Open(string username, string password, string uri, CancellationToken cancellationToken) - { - try - { - this.baseUri = uri; - - await this.restClient.Open(username, password, this.baseUri, cancellationToken); - } - catch (Exception) - { - this.baseUri = null; - throw; - } - } - - /// - /// Closes a connection to the SysML2 model server REST API - /// - public void Close() - { - this.baseUri = null; - } - } -} +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + using System; + using System.Net.Http; + using System.Threading; + using System.Threading.Tasks; + + using Microsoft.Extensions.Logging; + + using SysML2.NET.PSM; + using SysML2.NET.PSM.DTO; + using SysML2.NET.Serializer.Json; + + /// + /// The holds the connection to a server of the Systems Modeling API and Services + /// + public class Session : ISession + { + /// + /// The that creates a new for each opened connection + /// + private readonly IHttpClientFactory httpClientFactory; + + /// + /// The handed to each + /// + private readonly IDeSerializer deSerializer; + + /// + /// The handed to each + /// + private readonly ISerializer serializer; + + /// + /// The handed to each + /// + private readonly ILoggerFactory loggerFactory; + + /// + /// Initializes a new instance of the class + /// + /// + /// The that creates a new for each opened connection + /// + /// + /// The that reads the response payloads + /// + /// + /// The that writes the request payloads + /// + /// + /// The used to set up logging, or null to disable logging + /// + /// + /// Thrown when , or is null + /// + public Session(IHttpClientFactory httpClientFactory, IDeSerializer deSerializer, ISerializer serializer, ILoggerFactory loggerFactory = null) + { + this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); + this.deSerializer = deSerializer ?? throw new ArgumentNullException(nameof(deSerializer)); + this.serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); + this.loggerFactory = loggerFactory; + } + + /// + /// Gets the of the open connection, or null when the session is closed + /// + public IPsmApiClient Client { get; private set; } + + /// + /// Opens a connection to the server, replacing any open connection, and reads the first page of its projects + /// + /// + /// The absolute base address of the server + /// + /// + /// The that cancels the operation + /// + /// + /// The first page of the projects of the server + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when is not absolute + /// + public async Task> Open(Uri baseAddress, CancellationToken cancellationToken) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(baseAddress); +#else + if (baseAddress is null) + { + throw new ArgumentNullException(nameof(baseAddress)); + } +#endif + + if (!baseAddress.IsAbsoluteUri) + { + throw new ArgumentException("The base address must be absolute", nameof(baseAddress)); + } + + this.Close(); + + var httpClient = this.httpClientFactory.CreateClient(nameof(Session)); + + // a relative request URI drops the last path segment of a base address that does not end with a slash + httpClient.BaseAddress = baseAddress.AbsoluteUri.EndsWith('/') ? baseAddress : new Uri($"{baseAddress.AbsoluteUri}/"); + + var client = new PsmApiClient(httpClient, this.deSerializer, this.serializer, this.loggerFactory); + + var projects = await client.GetProjectsAsync(cancellationToken: cancellationToken); + + this.Client = client; + + return projects; + } + + /// + /// Closes the connection to the server + /// + public void Close() + { + this.Client = null; + } + } +} diff --git a/SySML2.NET.REST/SySML2.NET.REST.csproj b/SySML2.NET.REST/SySML2.NET.REST.csproj index e679aa30e..114fffbb2 100644 --- a/SySML2.NET.REST/SySML2.NET.REST.csproj +++ b/SySML2.NET.REST/SySML2.NET.REST.csproj @@ -22,7 +22,12 @@ + + + + + diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/PSM/AutoGenEnum/Direction.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/PSM/AutoGenEnum/Direction.cs new file mode 100644 index 000000000..3d4f67eb1 --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/PSM/AutoGenEnum/Direction.cs @@ -0,0 +1,44 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.PSM.Enumerations +{ + using System.CodeDom.Compiler; + + /// + /// Enumeration of the values allowed by the Direction property of the Systems Modeling API and Services + /// + [GeneratedCode("SysML2.NET", "latest")] + public enum Direction + { + /// + /// The "in" value + /// + @in, + + /// + /// The "out" value + /// + @out, + + /// + /// The "both" value + /// + both + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/PSM/AutoGenEnumProvider/CompositeConstraintOperatorProvider.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/PSM/AutoGenEnumProvider/CompositeConstraintOperatorProvider.cs index e0f1b7bb1..34e5fe941 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/PSM/AutoGenEnumProvider/CompositeConstraintOperatorProvider.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/PSM/AutoGenEnumProvider/CompositeConstraintOperatorProvider.cs @@ -46,30 +46,6 @@ public static CompositeConstraintOperator Parse(ReadOnlySpan value) throw new ArgumentException($"'{new string(value)}' is not a valid CompositeConstraintOperator", nameof(value)); } - /// - /// Tries to parse the to a - /// - /// The that is to be parsed. - /// The parsed literal, or default when the value denotes none. - /// True when the value was parsed. - public static bool TryParse(ReadOnlySpan value, out CompositeConstraintOperator result) - { - if (value.Length == 3 && value.Equals("and".AsSpan(), StringComparison.OrdinalIgnoreCase)) - { - result = CompositeConstraintOperator.and; - return true; - } - - if (value.Length == 2 && value.Equals("or".AsSpan(), StringComparison.OrdinalIgnoreCase)) - { - result = CompositeConstraintOperator.or; - return true; - } - - result = default; - return false; - } - /// /// Parses the UTF-8 encoded to a /// @@ -117,6 +93,30 @@ public static CompositeConstraintOperator Parse(in ReadOnlySequence value) return Parse(buffer[..(int)value.Length]); } + /// + /// Tries to parse the to a + /// + /// The that is to be parsed. + /// The parsed literal, or default when the value denotes none. + /// True when the value was parsed. + public static bool TryParse(ReadOnlySpan value, out CompositeConstraintOperator result) + { + if (value.Length == 3 && value.Equals("and".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + result = CompositeConstraintOperator.and; + return true; + } + + if (value.Length == 2 && value.Equals("or".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + result = CompositeConstraintOperator.or; + return true; + } + + result = default; + return false; + } + /// /// Converts a to its UTF-8 encoded representation on the wire /// diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/PSM/AutoGenEnumProvider/CompositeConstraintRequestOperatorProvider.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/PSM/AutoGenEnumProvider/CompositeConstraintRequestOperatorProvider.cs index 141f9769e..4da6fdc3d 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/PSM/AutoGenEnumProvider/CompositeConstraintRequestOperatorProvider.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/PSM/AutoGenEnumProvider/CompositeConstraintRequestOperatorProvider.cs @@ -46,30 +46,6 @@ public static CompositeConstraintRequestOperator Parse(ReadOnlySpan value) throw new ArgumentException($"'{new string(value)}' is not a valid CompositeConstraintRequestOperator", nameof(value)); } - /// - /// Tries to parse the to a - /// - /// The that is to be parsed. - /// The parsed literal, or default when the value denotes none. - /// True when the value was parsed. - public static bool TryParse(ReadOnlySpan value, out CompositeConstraintRequestOperator result) - { - if (value.Length == 3 && value.Equals("and".AsSpan(), StringComparison.OrdinalIgnoreCase)) - { - result = CompositeConstraintRequestOperator.and; - return true; - } - - if (value.Length == 2 && value.Equals("or".AsSpan(), StringComparison.OrdinalIgnoreCase)) - { - result = CompositeConstraintRequestOperator.or; - return true; - } - - result = default; - return false; - } - /// /// Parses the UTF-8 encoded to a /// @@ -117,6 +93,30 @@ public static CompositeConstraintRequestOperator Parse(in ReadOnlySequence return Parse(buffer[..(int)value.Length]); } + /// + /// Tries to parse the to a + /// + /// The that is to be parsed. + /// The parsed literal, or default when the value denotes none. + /// True when the value was parsed. + public static bool TryParse(ReadOnlySpan value, out CompositeConstraintRequestOperator result) + { + if (value.Length == 3 && value.Equals("and".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + result = CompositeConstraintRequestOperator.and; + return true; + } + + if (value.Length == 2 && value.Equals("or".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + result = CompositeConstraintRequestOperator.or; + return true; + } + + result = default; + return false; + } + /// /// Converts a to its UTF-8 encoded representation on the wire /// diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/PSM/AutoGenEnumProvider/DirectionProvider.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/PSM/AutoGenEnumProvider/DirectionProvider.cs new file mode 100644 index 000000000..313cbf737 --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/PSM/AutoGenEnumProvider/DirectionProvider.cs @@ -0,0 +1,147 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Extensions.PSM +{ + using System; + using System.Buffers; + using System.Text; + + using SysML2.NET.PSM.Enumerations; + + /// + /// The purpose of the is to convert a + /// to and from its representation in the Systems Modeling API and Services + /// + public static class DirectionProvider + { + /// + /// The length in bytes of the longest Direction representation + /// + private const int LongestValueByteLength = 4; + + /// + /// Parses the to a + /// + /// The that is to be parsed. + /// A enumeration literal. + /// Thrown when the value denotes no literal. + public static Direction Parse(ReadOnlySpan value) + { + if (TryParse(value, out var result)) + { + return result; + } + + throw new ArgumentException($"'{new string(value)}' is not a valid Direction", nameof(value)); + } + + /// + /// Parses the UTF-8 encoded to a + /// + /// The that is to be parsed. + /// A enumeration literal. + /// Thrown when the value denotes no literal. + public static Direction Parse(ReadOnlySpan value) + { + if (value.Length <= LongestValueByteLength) + { + Span characters = stackalloc char[LongestValueByteLength]; + + var characterCount = Encoding.UTF8.GetChars(value, characters); + + if (TryParse(characters[..characterCount], out var result)) + { + return result; + } + } + + throw new ArgumentException($"'{Encoding.UTF8.GetString(value)}' is not a valid Direction", nameof(value)); + } + + /// + /// Parses the UTF-8 encoded to a + /// + /// The that is to be parsed. + /// A enumeration literal. + /// Thrown when the value denotes no literal. + public static Direction Parse(in ReadOnlySequence value) + { + if (value.IsSingleSegment) + { + return Parse(value.FirstSpan); + } + + if (value.Length > LongestValueByteLength) + { + throw new ArgumentException($"a Direction is at most {LongestValueByteLength} bytes long", nameof(value)); + } + + Span buffer = stackalloc byte[LongestValueByteLength]; + value.CopyTo(buffer); + + return Parse(buffer[..(int)value.Length]); + } + + /// + /// Tries to parse the to a + /// + /// The that is to be parsed. + /// The parsed literal, or default when the value denotes none. + /// True when the value was parsed. + public static bool TryParse(ReadOnlySpan value, out Direction result) + { + if (value.Length == 2 && value.Equals("in".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + result = Direction.@in; + return true; + } + + if (value.Length == 3 && value.Equals("out".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + result = Direction.@out; + return true; + } + + if (value.Length == 4 && value.Equals("both".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + result = Direction.both; + return true; + } + + result = default; + return false; + } + + /// + /// Converts a to its UTF-8 encoded representation on the wire + /// + /// The that is to be converted. + /// A backed by static UTF-8 data. + /// Thrown when the literal is not known. + public static ReadOnlySpan ToUtf8Bytes(Direction value) + { + return value switch + { + Direction.@in => "in"u8, + Direction.@out => "out"u8, + Direction.both => "both"u8, + _ => throw new ArgumentOutOfRangeException(nameof(value)) + }; + } + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/PSM/AutoGenEnumProvider/PrimitiveConstraintOperatorProvider.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/PSM/AutoGenEnumProvider/PrimitiveConstraintOperatorProvider.cs index 07f7e46d9..158ca28ea 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/PSM/AutoGenEnumProvider/PrimitiveConstraintOperatorProvider.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/PSM/AutoGenEnumProvider/PrimitiveConstraintOperatorProvider.cs @@ -46,6 +46,53 @@ public static PrimitiveConstraintOperator Parse(ReadOnlySpan value) throw new ArgumentException($"'{new string(value)}' is not a valid PrimitiveConstraintOperator", nameof(value)); } + /// + /// Parses the UTF-8 encoded to a + /// + /// The that is to be parsed. + /// A enumeration literal. + /// Thrown when the value denotes no literal. + public static PrimitiveConstraintOperator Parse(ReadOnlySpan value) + { + if (value.Length <= LongestValueByteLength) + { + Span characters = stackalloc char[LongestValueByteLength]; + + var characterCount = Encoding.UTF8.GetChars(value, characters); + + if (TryParse(characters[..characterCount], out var result)) + { + return result; + } + } + + throw new ArgumentException($"'{Encoding.UTF8.GetString(value)}' is not a valid PrimitiveConstraintOperator", nameof(value)); + } + + /// + /// Parses the UTF-8 encoded to a + /// + /// The that is to be parsed. + /// A enumeration literal. + /// Thrown when the value denotes no literal. + public static PrimitiveConstraintOperator Parse(in ReadOnlySequence value) + { + if (value.IsSingleSegment) + { + return Parse(value.FirstSpan); + } + + if (value.Length > LongestValueByteLength) + { + throw new ArgumentException($"a PrimitiveConstraintOperator is at most {LongestValueByteLength} bytes long", nameof(value)); + } + + Span buffer = stackalloc byte[LongestValueByteLength]; + value.CopyTo(buffer); + + return Parse(buffer[..(int)value.Length]); + } + /// /// Tries to parse the to a /// @@ -100,53 +147,6 @@ public static bool TryParse(ReadOnlySpan value, out PrimitiveConstraintOpe return false; } - /// - /// Parses the UTF-8 encoded to a - /// - /// The that is to be parsed. - /// A enumeration literal. - /// Thrown when the value denotes no literal. - public static PrimitiveConstraintOperator Parse(ReadOnlySpan value) - { - if (value.Length <= LongestValueByteLength) - { - Span characters = stackalloc char[LongestValueByteLength]; - - var characterCount = Encoding.UTF8.GetChars(value, characters); - - if (TryParse(characters[..characterCount], out var result)) - { - return result; - } - } - - throw new ArgumentException($"'{Encoding.UTF8.GetString(value)}' is not a valid PrimitiveConstraintOperator", nameof(value)); - } - - /// - /// Parses the UTF-8 encoded to a - /// - /// The that is to be parsed. - /// A enumeration literal. - /// Thrown when the value denotes no literal. - public static PrimitiveConstraintOperator Parse(in ReadOnlySequence value) - { - if (value.IsSingleSegment) - { - return Parse(value.FirstSpan); - } - - if (value.Length > LongestValueByteLength) - { - throw new ArgumentException($"a PrimitiveConstraintOperator is at most {LongestValueByteLength} bytes long", nameof(value)); - } - - Span buffer = stackalloc byte[LongestValueByteLength]; - value.CopyTo(buffer); - - return Parse(buffer[..(int)value.Length]); - } - /// /// Converts a to its UTF-8 encoded representation on the wire /// diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/PSM/AutoGenEnumProvider/PrimitiveConstraintRequestOperatorProvider.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/PSM/AutoGenEnumProvider/PrimitiveConstraintRequestOperatorProvider.cs index ce1ea16c7..58d57078e 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/PSM/AutoGenEnumProvider/PrimitiveConstraintRequestOperatorProvider.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/PSM/AutoGenEnumProvider/PrimitiveConstraintRequestOperatorProvider.cs @@ -46,6 +46,53 @@ public static PrimitiveConstraintRequestOperator Parse(ReadOnlySpan value) throw new ArgumentException($"'{new string(value)}' is not a valid PrimitiveConstraintRequestOperator", nameof(value)); } + /// + /// Parses the UTF-8 encoded to a + /// + /// The that is to be parsed. + /// A enumeration literal. + /// Thrown when the value denotes no literal. + public static PrimitiveConstraintRequestOperator Parse(ReadOnlySpan value) + { + if (value.Length <= LongestValueByteLength) + { + Span characters = stackalloc char[LongestValueByteLength]; + + var characterCount = Encoding.UTF8.GetChars(value, characters); + + if (TryParse(characters[..characterCount], out var result)) + { + return result; + } + } + + throw new ArgumentException($"'{Encoding.UTF8.GetString(value)}' is not a valid PrimitiveConstraintRequestOperator", nameof(value)); + } + + /// + /// Parses the UTF-8 encoded to a + /// + /// The that is to be parsed. + /// A enumeration literal. + /// Thrown when the value denotes no literal. + public static PrimitiveConstraintRequestOperator Parse(in ReadOnlySequence value) + { + if (value.IsSingleSegment) + { + return Parse(value.FirstSpan); + } + + if (value.Length > LongestValueByteLength) + { + throw new ArgumentException($"a PrimitiveConstraintRequestOperator is at most {LongestValueByteLength} bytes long", nameof(value)); + } + + Span buffer = stackalloc byte[LongestValueByteLength]; + value.CopyTo(buffer); + + return Parse(buffer[..(int)value.Length]); + } + /// /// Tries to parse the to a /// @@ -100,53 +147,6 @@ public static bool TryParse(ReadOnlySpan value, out PrimitiveConstraintReq return false; } - /// - /// Parses the UTF-8 encoded to a - /// - /// The that is to be parsed. - /// A enumeration literal. - /// Thrown when the value denotes no literal. - public static PrimitiveConstraintRequestOperator Parse(ReadOnlySpan value) - { - if (value.Length <= LongestValueByteLength) - { - Span characters = stackalloc char[LongestValueByteLength]; - - var characterCount = Encoding.UTF8.GetChars(value, characters); - - if (TryParse(characters[..characterCount], out var result)) - { - return result; - } - } - - throw new ArgumentException($"'{Encoding.UTF8.GetString(value)}' is not a valid PrimitiveConstraintRequestOperator", nameof(value)); - } - - /// - /// Parses the UTF-8 encoded to a - /// - /// The that is to be parsed. - /// A enumeration literal. - /// Thrown when the value denotes no literal. - public static PrimitiveConstraintRequestOperator Parse(in ReadOnlySequence value) - { - if (value.IsSingleSegment) - { - return Parse(value.FirstSpan); - } - - if (value.Length > LongestValueByteLength) - { - throw new ArgumentException($"a PrimitiveConstraintRequestOperator is at most {LongestValueByteLength} bytes long", nameof(value)); - } - - Span buffer = stackalloc byte[LongestValueByteLength]; - value.CopyTo(buffer); - - return Parse(buffer[..(int)value.Length]); - } - /// /// Converts a to its UTF-8 encoded representation on the wire /// diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/BranchApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/BranchApi.cs new file mode 100644 index 000000000..ea48225b1 --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/BranchApi.cs @@ -0,0 +1,161 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + + using SysML2.NET.Common; + using SysML2.NET.Core.DTO.Root.Elements; + using SysML2.NET.PSM; + using SysML2.NET.PSM.DTO; + using SysML2.NET.REST.Exceptions; + + /// + /// Provides the Branch operations of the Systems Modeling API and Services + /// + public partial class PsmApiClient + { + /// + /// Get branches by project + /// + /// + /// ID of the project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getBranchesByProject operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> GetBranchesByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/branches{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadPagedResponseAsync(response, cancellationToken); + } + + /// + /// Create branch by project + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the postBranchByProject operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task PostBranchByProjectAsync(Guid projectId, BranchRequest requestBody, CancellationToken cancellationToken = default) + { + ThrowIfNull(requestBody, nameof(requestBody)); + + var query = new QueryStringBuilder(); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects/{Escape(projectId)}/branches{query}", content, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Get branch by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the branch + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getBranchesByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task GetBranchesByProjectAndIdAsync(Guid projectId, Guid branchId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/branches/{Escape(branchId)}{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Delete branch by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the branch + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the deleteBranchByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task DeleteBranchByProjectAndIdAsync(Guid projectId, Guid branchId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(HttpMethod.Delete, $"projects/{Escape(projectId)}/branches/{Escape(branchId)}{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/CommitApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/CommitApi.cs new file mode 100644 index 000000000..a6b228774 --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/CommitApi.cs @@ -0,0 +1,200 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + + using SysML2.NET.Common; + using SysML2.NET.Core.DTO.Root.Elements; + using SysML2.NET.PSM; + using SysML2.NET.PSM.DTO; + using SysML2.NET.REST.Exceptions; + + /// + /// Provides the Commit operations of the Systems Modeling API and Services + /// + public partial class PsmApiClient + { + /// + /// Get commits by project + /// + /// + /// ID of the project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getCommitsByProject operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> GetCommitsByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadPagedResponseAsync(response, cancellationToken); + } + + /// + /// Create commit by project + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// ID of the branch - project's default branch if unspecified + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the postCommitByProject operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task PostCommitByProjectAsync(Guid projectId, CommitRequest requestBody, Guid? branchId = null, CancellationToken cancellationToken = default) + { + ThrowIfNull(requestBody, nameof(requestBody)); + + var query = new QueryStringBuilder(); + query.Add("branchId", branchId); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects/{Escape(projectId)}/commits{query}", content, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Get commit by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getCommitByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task GetCommitByProjectAndIdAsync(Guid projectId, Guid commitId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(commitId)}{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Get changes by project and commit + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getChangesByProjectCommit operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> GetChangesByProjectCommitAsync(Guid projectId, Guid commitId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/changes{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadPagedResponseAsync(response, cancellationToken); + } + + /// + /// Get change by project, commit and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// ID of the change (DataVersion) + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getChangeByProjectCommitId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task GetChangeByProjectCommitIdAsync(Guid projectId, Guid commitId, Guid changeId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/changes/{Escape(changeId)}{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/DiffMergeApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/DiffMergeApi.cs new file mode 100644 index 000000000..4ff7175ec --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/DiffMergeApi.cs @@ -0,0 +1,132 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + + using SysML2.NET.Common; + using SysML2.NET.Core.DTO.Root.Elements; + using SysML2.NET.PSM; + using SysML2.NET.PSM.DTO; + using SysML2.NET.REST.Exceptions; + + /// + /// Provides the Diff & Merge operations of the Systems Modeling API and Services + /// + public partial class PsmApiClient + { + /// + /// Diff a base commit and compare commit + /// + /// + /// ID of the project + /// + /// + /// ID of the base commit + /// + /// + /// ID of the compare commit + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the diff operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> DiffAsync(Guid projectId, Guid baseCommitId, Guid compareCommitId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + query.Add("baseCommitId", baseCommitId); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(compareCommitId)}/diff{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadPagedResponseAsync(response, cancellationToken); + } + + /// + /// Merge source commit(s) into a target branch + /// + /// + /// ID of the project + /// + /// + /// ID(s) of the source commit(s) - comma separated + /// + /// + /// ID of the target branch + /// + /// + /// The payload sent as the body of the request + /// + /// + /// Description of merge commit - in case one is created + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the merge operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when is empty + /// + /// + /// Thrown when the server responds with an error status code + /// + /// + /// Thrown when the server reports conflicting data + /// + public async Task MergeAsync(Guid projectId, IEnumerable sourceCommitId, Guid targetBranchId, IEnumerable requestBody = null, string description = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + ThrowIfNullOrEmpty(sourceCommitId, nameof(sourceCommitId)); + + var query = new QueryStringBuilder(queryParameters); + query.Add("sourceCommitId", sourceCommitId); + query.Add("description", description); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects/{Escape(projectId)}/branches/{Escape(targetBranchId)}/merge{query}", content, cancellationToken); + + await this.ThrowOnConflictAsync(response, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/ElementApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/ElementApi.cs new file mode 100644 index 000000000..3b3d1a3e6 --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/ElementApi.cs @@ -0,0 +1,179 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + + using SysML2.NET.Common; + using SysML2.NET.Core.DTO.Root.Elements; + using SysML2.NET.PSM; + using SysML2.NET.PSM.DTO; + using SysML2.NET.REST.Exceptions; + + /// + /// Provides the Element operations of the Systems Modeling API and Services + /// + public partial class PsmApiClient + { + /// + /// Get elements by project and commit + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// Exclude Data from ProjectUsages + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getElementsByProjectCommit operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> GetElementsByProjectCommitAsync(Guid projectId, Guid commitId, bool? excludeUsed = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + query.Add("excludeUsed", excludeUsed); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/elements{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadPagedDataAsync(response, cancellationToken); + } + + /// + /// Get element by project, commit and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// ID of the element + /// + /// + /// Exclude Data from ProjectUsages + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getElementByProjectCommitId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task GetElementByProjectCommitIdAsync(Guid projectId, Guid commitId, Guid elementId, bool? excludeUsed = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + query.Add("excludeUsed", excludeUsed); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/elements/{Escape(elementId)}{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadDataAsync(response, cancellationToken); + } + + /// + /// Get ProjectUsage that originates the provided element + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// ID of the element + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getProjectUsageByProjectCommitElement operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task GetProjectUsageByProjectCommitElementAsync(Guid projectId, Guid commitId, Guid elementId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/elements/{Escape(elementId)}/projectUsage{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Get root elements by project and commit + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// Exclude Data from ProjectUsages + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getRootsByProjectCommit operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> GetRootsByProjectCommitAsync(Guid projectId, Guid commitId, bool? excludeUsed = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + query.Add("excludeUsed", excludeUsed); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/roots{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadPagedDataAsync(response, cancellationToken); + } + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/IPsmApiClient.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/IPsmApiClient.cs new file mode 100644 index 000000000..86b051b8d --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/IPsmApiClient.cs @@ -0,0 +1,840 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + using System; + using System.CodeDom.Compiler; + using System.Collections.Generic; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + + using SysML2.NET.Common; + using SysML2.NET.Core.DTO.Root.Elements; + using SysML2.NET.PSM; + using SysML2.NET.PSM.DTO; + using SysML2.NET.PSM.Enumerations; + using SysML2.NET.REST.Exceptions; + + /// + /// The client of the REST/HTTP platform specific model of the Systems Modeling API and Services + /// + [GeneratedCode("SysML2.NET", "latest")] + public partial interface IPsmApiClient + { + /// + /// Get projects + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getProjects operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> GetProjectsAsync(QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Create project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the postProject operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task PostProjectAsync(ProjectRequest requestBody = null, CancellationToken cancellationToken = default); + + /// + /// Get project by ID + /// + /// + /// ID of the project + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getProjectById operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task GetProjectByIdAsync(Guid projectId, CancellationToken cancellationToken = default); + + /// + /// Update project by ID + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the putProjectById operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task PutProjectByIdAsync(Guid projectId, ProjectRequest requestBody = null, CancellationToken cancellationToken = default); + + /// + /// Delete project by ID + /// + /// + /// ID of the project + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the deleteProjectById operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task DeleteProjectByIdAsync(Guid projectId, CancellationToken cancellationToken = default); + + /// + /// Get branches by project + /// + /// + /// ID of the project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getBranchesByProject operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> GetBranchesByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Create branch by project + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the postBranchByProject operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + Task PostBranchByProjectAsync(Guid projectId, BranchRequest requestBody, CancellationToken cancellationToken = default); + + /// + /// Get branch by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the branch + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getBranchesByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task GetBranchesByProjectAndIdAsync(Guid projectId, Guid branchId, CancellationToken cancellationToken = default); + + /// + /// Delete branch by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the branch + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the deleteBranchByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task DeleteBranchByProjectAndIdAsync(Guid projectId, Guid branchId, CancellationToken cancellationToken = default); + + /// + /// Get tags by project + /// + /// + /// ID of the project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getTagsByProject operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> GetTagsByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Create tag by project + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the postTagByProject operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + Task PostTagByProjectAsync(Guid projectId, TagRequest requestBody, CancellationToken cancellationToken = default); + + /// + /// Get tag by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the tag + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getTagByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task GetTagByProjectAndIdAsync(Guid projectId, Guid tagId, CancellationToken cancellationToken = default); + + /// + /// Delete tag by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the tag + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the deleteTagByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task DeleteTagByProjectAndIdAsync(Guid projectId, Guid tagId, CancellationToken cancellationToken = default); + + /// + /// Get commits by project + /// + /// + /// ID of the project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getCommitsByProject operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> GetCommitsByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Create commit by project + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// ID of the branch - project's default branch if unspecified + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the postCommitByProject operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + Task PostCommitByProjectAsync(Guid projectId, CommitRequest requestBody, Guid? branchId = null, CancellationToken cancellationToken = default); + + /// + /// Get commit by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getCommitByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task GetCommitByProjectAndIdAsync(Guid projectId, Guid commitId, CancellationToken cancellationToken = default); + + /// + /// Get changes by project and commit + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getChangesByProjectCommit operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> GetChangesByProjectCommitAsync(Guid projectId, Guid commitId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Get change by project, commit and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// ID of the change (DataVersion) + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getChangeByProjectCommitId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task GetChangeByProjectCommitIdAsync(Guid projectId, Guid commitId, Guid changeId, CancellationToken cancellationToken = default); + + /// + /// Get elements by project and commit + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// Exclude Data from ProjectUsages + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getElementsByProjectCommit operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> GetElementsByProjectCommitAsync(Guid projectId, Guid commitId, bool? excludeUsed = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Get element by project, commit and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// ID of the element + /// + /// + /// Exclude Data from ProjectUsages + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getElementByProjectCommitId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task GetElementByProjectCommitIdAsync(Guid projectId, Guid commitId, Guid elementId, bool? excludeUsed = null, CancellationToken cancellationToken = default); + + /// + /// Get ProjectUsage that originates the provided element + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// ID of the element + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getProjectUsageByProjectCommitElement operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task GetProjectUsageByProjectCommitElementAsync(Guid projectId, Guid commitId, Guid elementId, CancellationToken cancellationToken = default); + + /// + /// Get root elements by project and commit + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// Exclude Data from ProjectUsages + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getRootsByProjectCommit operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> GetRootsByProjectCommitAsync(Guid projectId, Guid commitId, bool? excludeUsed = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Get relationships by project, commit, and related element + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// ID of the related element + /// + /// + /// Filter for relationships that are incoming (in), outgoing (out), or both relative to the related element + /// + /// + /// Exclude Data from ProjectUsages + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getRelationshipsByProjectCommitRelatedElement operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> GetRelationshipsByProjectCommitRelatedElementAsync(Guid projectId, Guid commitId, Guid relatedElementId, Direction? direction = null, bool? excludeUsed = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Diff a base commit and compare commit + /// + /// + /// ID of the project + /// + /// + /// ID of the base commit + /// + /// + /// ID of the compare commit + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the diff operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> DiffAsync(Guid projectId, Guid baseCommitId, Guid compareCommitId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Merge source commit(s) into a target branch + /// + /// + /// ID of the project + /// + /// + /// ID(s) of the source commit(s) - comma separated + /// + /// + /// ID of the target branch + /// + /// + /// The payload sent as the body of the request + /// + /// + /// Description of merge commit - in case one is created + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the merge operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when is empty + /// + /// + /// Thrown when the server responds with an error status code + /// + /// + /// Thrown when the server reports conflicting data + /// + Task MergeAsync(Guid projectId, IEnumerable sourceCommitId, Guid targetBranchId, IEnumerable requestBody = null, string description = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Get queries by project + /// + /// + /// ID of the project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getQueriesByProject operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> GetQueriesByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Create query by project + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the postQueryByProject operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + Task PostQueryByProjectAsync(Guid projectId, QueryRequest requestBody, CancellationToken cancellationToken = default); + + /// + /// Get query by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the query + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getQueryByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task GetQueryByProjectAndIdAsync(Guid projectId, Guid queryId, CancellationToken cancellationToken = default); + + /// + /// Update project by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the query + /// + /// + /// The payload sent as the body of the request + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the putQueryByProjectAndId operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + Task PutQueryByProjectAndIdAsync(Guid projectId, Guid queryId, QueryRequest requestBody, CancellationToken cancellationToken = default); + + /// + /// Delete query by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the query + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the deleteQueryByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task DeleteQueryByProjectAndIdAsync(Guid projectId, Guid queryId, CancellationToken cancellationToken = default); + + /// + /// Get query results by project and query + /// + /// + /// ID of the project + /// + /// + /// ID of the query + /// + /// + /// ID of the commit - defaults to head of project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getQueryResultsByProjectIdQueryId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> GetQueryResultsByProjectIdQueryIdAsync(Guid projectId, Guid queryId, Guid? commitId = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Get query results by project and query definition + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// ID of the commit - defaults to head of project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getQueryResultsByProjectIdQuery operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> GetQueryResultsByProjectIdQueryAsync(Guid projectId, QueryRequest requestBody, Guid? commitId = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Get query results by project and query definition via POST + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// ID of the commit - defaults to head of project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getQueryResultsByProjectIdQueryPost operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> GetQueryResultsByProjectIdQueryPostAsync(Guid projectId, QueryRequest requestBody, Guid? commitId = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Get datatypes + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getDatatypes operation + /// + /// + /// Thrown when the server responds with an error status code + /// + Task> GetDatatypesAsync(QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + + /// + /// Get datatype by ID + /// + /// + /// ID of the datatype + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getDatatypeById operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when is empty or whitespace + /// + /// + /// Thrown when the server responds with an error status code + /// + Task GetDatatypeByIdAsync(string datatypeId, CancellationToken cancellationToken = default); + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/MetaApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/MetaApi.cs new file mode 100644 index 000000000..b0540e7ab --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/MetaApi.cs @@ -0,0 +1,99 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + + using SysML2.NET.Common; + using SysML2.NET.Core.DTO.Root.Elements; + using SysML2.NET.PSM; + using SysML2.NET.PSM.DTO; + using SysML2.NET.REST.Exceptions; + + /// + /// Provides the Meta operations of the Systems Modeling API and Services + /// + public partial class PsmApiClient + { + /// + /// Get datatypes + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getDatatypes operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> GetDatatypesAsync(QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + + using var response = await this.SendAsync(HttpMethod.Get, $"meta/datatypes{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await ReadPagedJsonAsync(response, cancellationToken); + } + + /// + /// Get datatype by ID + /// + /// + /// ID of the datatype + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getDatatypeById operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when is empty or whitespace + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task GetDatatypeByIdAsync(string datatypeId, CancellationToken cancellationToken = default) + { + ThrowIfNullOrWhiteSpace(datatypeId, nameof(datatypeId)); + + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(HttpMethod.Get, $"meta/datatypes/{Escape(datatypeId)}{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await ReadJsonAsync(response, cancellationToken); + } + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/ProjectApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/ProjectApi.cs new file mode 100644 index 000000000..cef1b1f45 --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/ProjectApi.cs @@ -0,0 +1,176 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + using System; + using System.CodeDom.Compiler; + using System.Collections.Generic; + using System.Net.Http; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + + using SysML2.NET.Common; + using SysML2.NET.Core.DTO.Root.Elements; + using SysML2.NET.PSM; + using SysML2.NET.PSM.DTO; + using SysML2.NET.REST.Exceptions; + + /// + /// Provides the Project operations of the Systems Modeling API and Services + /// + [GeneratedCode("SysML2.NET", "latest")] + public partial class PsmApiClient + { + /// + /// Get projects + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getProjects operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> GetProjectsAsync(QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadPagedResponseAsync(response, cancellationToken); + } + + /// + /// Create project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the postProject operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task PostProjectAsync(ProjectRequest requestBody = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects{query}", content, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Get project by ID + /// + /// + /// ID of the project + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getProjectById operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task GetProjectByIdAsync(Guid projectId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Update project by ID + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the putProjectById operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task PutProjectByIdAsync(Guid projectId, ProjectRequest requestBody = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Put, $"projects/{Escape(projectId)}{query}", content, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Delete project by ID + /// + /// + /// ID of the project + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the deleteProjectById operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task DeleteProjectByIdAsync(Guid projectId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(HttpMethod.Delete, $"projects/{Escape(projectId)}{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/QueryApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/QueryApi.cs new file mode 100644 index 000000000..6eaeb4303 --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/QueryApi.cs @@ -0,0 +1,319 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + + using SysML2.NET.Common; + using SysML2.NET.Core.DTO.Root.Elements; + using SysML2.NET.PSM; + using SysML2.NET.PSM.DTO; + using SysML2.NET.REST.Exceptions; + + /// + /// Provides the Query operations of the Systems Modeling API and Services + /// + public partial class PsmApiClient + { + /// + /// Get queries by project + /// + /// + /// ID of the project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getQueriesByProject operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> GetQueriesByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/queries{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadPagedResponseAsync(response, cancellationToken); + } + + /// + /// Create query by project + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the postQueryByProject operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task PostQueryByProjectAsync(Guid projectId, QueryRequest requestBody, CancellationToken cancellationToken = default) + { + ThrowIfNull(requestBody, nameof(requestBody)); + + var query = new QueryStringBuilder(); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects/{Escape(projectId)}/queries{query}", content, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Get query by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the query + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getQueryByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task GetQueryByProjectAndIdAsync(Guid projectId, Guid queryId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/queries/{Escape(queryId)}{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Update project by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the query + /// + /// + /// The payload sent as the body of the request + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the putQueryByProjectAndId operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task PutQueryByProjectAndIdAsync(Guid projectId, Guid queryId, QueryRequest requestBody, CancellationToken cancellationToken = default) + { + ThrowIfNull(requestBody, nameof(requestBody)); + + var query = new QueryStringBuilder(); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Put, $"projects/{Escape(projectId)}/queries/{Escape(queryId)}{query}", content, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Delete query by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the query + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the deleteQueryByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task DeleteQueryByProjectAndIdAsync(Guid projectId, Guid queryId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(HttpMethod.Delete, $"projects/{Escape(projectId)}/queries/{Escape(queryId)}{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Get query results by project and query + /// + /// + /// ID of the project + /// + /// + /// ID of the query + /// + /// + /// ID of the commit - defaults to head of project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getQueryResultsByProjectIdQueryId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> GetQueryResultsByProjectIdQueryIdAsync(Guid projectId, Guid queryId, Guid? commitId = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + query.Add("commitId", commitId); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/queries/{Escape(queryId)}/results{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadPagedDataAsync(response, cancellationToken); + } + + /// + /// Get query results by project and query definition + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// ID of the commit - defaults to head of project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getQueryResultsByProjectIdQuery operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> GetQueryResultsByProjectIdQueryAsync(Guid projectId, QueryRequest requestBody, Guid? commitId = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + ThrowIfNull(requestBody, nameof(requestBody)); + + var query = new QueryStringBuilder(queryParameters); + query.Add("commitId", commitId); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/query-results{query}", content, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadPagedDataAsync(response, cancellationToken); + } + + /// + /// Get query results by project and query definition via POST + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// ID of the commit - defaults to head of project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getQueryResultsByProjectIdQueryPost operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> GetQueryResultsByProjectIdQueryPostAsync(Guid projectId, QueryRequest requestBody, Guid? commitId = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + ThrowIfNull(requestBody, nameof(requestBody)); + + var query = new QueryStringBuilder(queryParameters); + query.Add("commitId", commitId); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects/{Escape(projectId)}/query-results{query}", content, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadPagedDataAsync(response, cancellationToken); + } + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/RelationshipApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/RelationshipApi.cs new file mode 100644 index 000000000..f45bcd3ec --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/RelationshipApi.cs @@ -0,0 +1,84 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + + using SysML2.NET.Common; + using SysML2.NET.Core.DTO.Root.Elements; + using SysML2.NET.Extensions.PSM; + using SysML2.NET.PSM; + using SysML2.NET.PSM.DTO; + using SysML2.NET.PSM.Enumerations; + using SysML2.NET.REST.Exceptions; + + /// + /// Provides the Relationship operations of the Systems Modeling API and Services + /// + public partial class PsmApiClient + { + /// + /// Get relationships by project, commit, and related element + /// + /// + /// ID of the project + /// + /// + /// ID of the commit + /// + /// + /// ID of the related element + /// + /// + /// Filter for relationships that are incoming (in), outgoing (out), or both relative to the related element + /// + /// + /// Exclude Data from ProjectUsages + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getRelationshipsByProjectCommitRelatedElement operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> GetRelationshipsByProjectCommitRelatedElementAsync(Guid projectId, Guid commitId, Guid relatedElementId, Direction? direction = null, bool? excludeUsed = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + query.Add("direction", direction is { } directionValue ? ToQueryValue(DirectionProvider.ToUtf8Bytes(directionValue)) : null); + query.Add("excludeUsed", excludeUsed); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/elements/{Escape(relatedElementId)}/relationships{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadPagedDataAsync(response, cancellationToken); + } + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/TagApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/TagApi.cs new file mode 100644 index 000000000..90c4d7795 --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/TagApi.cs @@ -0,0 +1,161 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + + using SysML2.NET.Common; + using SysML2.NET.Core.DTO.Root.Elements; + using SysML2.NET.PSM; + using SysML2.NET.PSM.DTO; + using SysML2.NET.REST.Exceptions; + + /// + /// Provides the Tag operations of the Systems Modeling API and Services + /// + public partial class PsmApiClient + { + /// + /// Get tags by project + /// + /// + /// ID of the project + /// + /// + /// The page of the result to request, or null to let the server choose + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getTagsByProject operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task> GetTagsByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/tags{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadPagedResponseAsync(response, cancellationToken); + } + + /// + /// Create tag by project + /// + /// + /// ID of the project + /// + /// + /// The payload sent as the body of the request + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the postTagByProject operation + /// + /// + /// Thrown when is null + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task PostTagByProjectAsync(Guid projectId, TagRequest requestBody, CancellationToken cancellationToken = default) + { + ThrowIfNull(requestBody, nameof(requestBody)); + + var query = new QueryStringBuilder(); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects/{Escape(projectId)}/tags{query}", content, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Get tag by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the tag + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getTagByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task GetTagByProjectAndIdAsync(Guid projectId, Guid tagId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/tags/{Escape(tagId)}{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + + /// + /// Delete tag by project and ID + /// + /// + /// ID of the project + /// + /// + /// ID of the tag + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the deleteTagByProjectAndId operation + /// + /// + /// Thrown when the server responds with an error status code + /// + public async Task DeleteTagByProjectAndIdAsync(Guid projectId, Guid tagId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(HttpMethod.Delete, $"projects/{Escape(projectId)}/tags/{Escape(tagId)}{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadResponseAsync(response, cancellationToken); + } + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SysML2.NET.CodeGenerator.Tests/Generators/OpenApiHandleBarsGenerators/OpenApiApiClientGeneratorTestFixture.cs b/SysML2.NET.CodeGenerator.Tests/Generators/OpenApiHandleBarsGenerators/OpenApiApiClientGeneratorTestFixture.cs new file mode 100644 index 000000000..282989474 --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Generators/OpenApiHandleBarsGenerators/OpenApiApiClientGeneratorTestFixture.cs @@ -0,0 +1,166 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.CodeGenerator.Tests.Generators.OpenApiHandleBarsGenerators +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net.Http; + using System.Threading.Tasks; + + using Microsoft.OpenApi; + + using NUnit.Framework; + + using SysML2.NET.CodeGenerator.Generators.OpenApiHandleBarsGenerators; + + [TestFixture] + public class OpenApiApiClientGeneratorTestFixture + { + private OpenApiDocument openApiDocument; + private DirectoryInfo outputDirectory; + private OpenApiApiClientGenerator openApiApiClientGenerator; + + [OneTimeSetUp] + public async Task SetUp() + { + this.openApiDocument = await OpenApiDocumentLoader.LoadAsync(); + + var directoryInfo = new DirectoryInfo(TestContext.CurrentContext.TestDirectory); + + this.outputDirectory = directoryInfo.CreateSubdirectory(Path.Combine("OpenAPI", "_SySML2.NET.REST.AutoGenPsmApiClient")); + this.openApiApiClientGenerator = new OpenApiApiClientGenerator(); + } + + private static IEnumerable Tags() + { + var openApiDocument = OpenApiDocumentLoader.LoadAsync().GetAwaiter().GetResult(); + + return OpenApiApiClientGenerator.QueryTagModels(openApiDocument).Select(tagModel => tagModel.Tag); + } + + [Test] + public async Task VerifyGenerateAsync() + { + await Assert.ThatAsync(() => this.openApiApiClientGenerator.GenerateAsync(null, this.outputDirectory), Throws.TypeOf()); + await Assert.ThatAsync(() => this.openApiApiClientGenerator.GenerateAsync(this.openApiDocument, null), Throws.TypeOf()); + + await Assert.ThatAsync(() => this.openApiApiClientGenerator.GenerateAsync(this.openApiDocument, this.outputDirectory), Throws.Nothing); + + var generatedFileNames = this.outputDirectory.GetFiles("*.cs").Select(file => file.Name); + + Assert.That(generatedFileNames, Is.EquivalentTo(["IPsmApiClient.cs", "ProjectApi.cs", "BranchApi.cs", "TagApi.cs", "CommitApi.cs", "ElementApi.cs", "RelationshipApi.cs", "DiffMergeApi.cs", "QueryApi.cs", "MetaApi.cs"])); + } + + [Test] + public void VerifyQueryTagModels() + { + Assert.That(() => OpenApiApiClientGenerator.QueryTagModels(null), Throws.TypeOf()); + + var validOperation = new OpenApiOperation + { + OperationId = "getThings", + Parameters = [CreateArrayParameter(ParameterStyle.Form, false)], + Responses = new OpenApiResponses { ["200"] = CreateJsonResponse() } + }; + + Assert.That(OpenApiApiClientGenerator.QueryTagModels(CreateDocument(validOperation)), Has.Count.EqualTo(1)); + + var twoSuccessResponses = new OpenApiOperation + { + OperationId = "getThings", + Responses = new OpenApiResponses { ["200"] = CreateJsonResponse(), ["201"] = CreateJsonResponse() } + }; + + Assert.That(() => OpenApiApiClientGenerator.QueryTagModels(CreateDocument(twoSuccessResponses)), Throws.TypeOf()); + + var explodedArray = new OpenApiOperation + { + OperationId = "getThings", + Parameters = [CreateArrayParameter(ParameterStyle.Form, true)], + Responses = new OpenApiResponses { ["200"] = CreateJsonResponse() } + }; + + Assert.That(() => OpenApiApiClientGenerator.QueryTagModels(CreateDocument(explodedArray)), Throws.TypeOf()); + + var pipeDelimitedArray = new OpenApiOperation + { + OperationId = "getThings", + Parameters = [CreateArrayParameter(ParameterStyle.PipeDelimited, false)], + Responses = new OpenApiResponses { ["200"] = CreateJsonResponse() } + }; + + Assert.That(() => OpenApiApiClientGenerator.QueryTagModels(CreateDocument(pipeDelimitedArray)), Throws.TypeOf()); + } + + private static OpenApiDocument CreateDocument(OpenApiOperation operation) + { + var document = new OpenApiDocument + { + Tags = new HashSet { new() { Name = "Things" } }, + Paths = [] + }; + + operation.Tags = new HashSet { new("Things", document) }; + document.Paths["/things"] = new OpenApiPathItem { Operations = new Dictionary { [HttpMethod.Get] = operation } }; + + return document; + } + + private static OpenApiParameter CreateArrayParameter(ParameterStyle style, bool explode) + { + return new OpenApiParameter + { + Name = "thingId", + In = ParameterLocation.Query, + Style = style, + Explode = explode, + Schema = new OpenApiSchema { Type = JsonSchemaType.Array, Items = new OpenApiSchema { Type = JsonSchemaType.String } } + }; + } + + private static OpenApiResponse CreateJsonResponse() + { + return new OpenApiResponse { Content = new Dictionary { ["application/json"] = new OpenApiMediaType() } }; + } + + [Test] + [TestCaseSource(nameof(Tags))] + [Category("Expected")] + public async Task VerifyGenerateApiClientPartialAsync(string tag) + { + await Assert.ThatAsync(() => this.openApiApiClientGenerator.GenerateApiClientPartialAsync(this.openApiDocument, this.outputDirectory, "Unknown"), Throws.TypeOf()); + + var tagModel = OpenApiApiClientGenerator.QueryTagModels(this.openApiDocument).Single(candidate => candidate.Tag == tag); + + var generatedCode = await this.openApiApiClientGenerator.GenerateApiClientPartialAsync(this.openApiDocument, this.outputDirectory, tag); + + var expected = await File.ReadAllTextAsync(Path.Combine(TestContext.CurrentContext.TestDirectory, + $"Expected/OpenAPI/REST/AutoGenPsmApiClient/{tagModel.Name}Api.cs")); + + Assert.That(generatedCode, Is.EqualTo(expected)); + } + + [Test] + [Category("Expected")] + public async Task VerifyGenerateApiClientInterfaceAsync() + { + await Assert.ThatAsync(() => this.openApiApiClientGenerator.GenerateApiClientInterfaceAsync(null, this.outputDirectory), Throws.TypeOf()); + + var generatedCode = await this.openApiApiClientGenerator.GenerateApiClientInterfaceAsync(this.openApiDocument, this.outputDirectory); + + var expected = await File.ReadAllTextAsync(Path.Combine(TestContext.CurrentContext.TestDirectory, + "Expected/OpenAPI/REST/AutoGenPsmApiClient/IPsmApiClient.cs")); + + Assert.That(generatedCode, Is.EqualTo(expected)); + } + } +} diff --git a/SysML2.NET.CodeGenerator.Tests/Generators/OpenApiHandleBarsGenerators/OpenApiEnumGeneratorTestFixture.cs b/SysML2.NET.CodeGenerator.Tests/Generators/OpenApiHandleBarsGenerators/OpenApiEnumGeneratorTestFixture.cs index 59b544a69..9ec958a75 100644 --- a/SysML2.NET.CodeGenerator.Tests/Generators/OpenApiHandleBarsGenerators/OpenApiEnumGeneratorTestFixture.cs +++ b/SysML2.NET.CodeGenerator.Tests/Generators/OpenApiHandleBarsGenerators/OpenApiEnumGeneratorTestFixture.cs @@ -10,14 +10,18 @@ namespace SysML2.NET.CodeGenerator.Tests.Generators.OpenApiHandleBarsGenerators { using System; + using System.Collections.Generic; using System.IO; using System.Linq; + using System.Net.Http; + using System.Text.Json.Nodes; using System.Threading.Tasks; using Microsoft.OpenApi; using NUnit.Framework; + using SysML2.NET.CodeGenerator.Extensions; using SysML2.NET.CodeGenerator.Generators.OpenApiHandleBarsGenerators; [TestFixture] @@ -41,8 +45,8 @@ public async Task SetUp() [Test] public async Task VerifyGenerateAsync() { - Assert.That(() => this.openApiEnumGenerator.GenerateAsync(null, this.outputDirectory), Throws.TypeOf()); - Assert.That(() => this.openApiEnumGenerator.GenerateAsync(this.openApiDocument, null), Throws.TypeOf()); + await Assert.ThatAsync(() => this.openApiEnumGenerator.GenerateAsync(null, this.outputDirectory), Throws.TypeOf()); + await Assert.ThatAsync(() => this.openApiEnumGenerator.GenerateAsync(this.openApiDocument, null), Throws.TypeOf()); Assert.That(() => OpenApiEnumGenerator.QueryEnumerations(null), Throws.TypeOf()); await Assert.ThatAsync(() => this.openApiEnumGenerator.GenerateAsync(this.openApiDocument, this.outputDirectory), Throws.Nothing); @@ -52,15 +56,66 @@ public async Task VerifyGenerateAsync() Assert.That(enumerationNames, Is.EqualTo([ "CompositeConstraintOperator", "CompositeConstraintRequestOperator", + "Direction", "PrimitiveConstraintOperator", "PrimitiveConstraintRequestOperator" ])); } + [Test] + public void VerifyQueryParameterEnumerations() + { + Assert.That(() => OpenApiEnumGenerator.QueryParameterEnumerations(null), Throws.TypeOf()); + + var enumerations = OpenApiEnumGenerator.QueryParameterEnumerations(this.openApiDocument); + + using (Assert.EnterMultipleScope()) + { + Assert.That(enumerations, Has.Count.EqualTo(1)); + Assert.That(enumerations[0].Key, Is.EqualTo("Direction")); + Assert.That(enumerations[0].Value.QueryEnumerationValues(), Is.EqualTo(["in", "out", "both"])); + } + + Assert.That(OpenApiEnumGenerator.QueryParameterEnumerations(new OpenApiDocument { Paths = [] }), Has.Count.EqualTo(0)); + + var clashingDocument = new OpenApiDocument + { + Paths = new OpenApiPaths + { + ["/first"] = CreatePathItem("in", "out"), + ["/second"] = CreatePathItem("up", "down") + } + }; + + Assert.That(() => OpenApiEnumGenerator.QueryParameterEnumerations(clashingDocument), Throws.TypeOf()); + } + + private static OpenApiPathItem CreatePathItem(params string[] allowedValues) + { + return new OpenApiPathItem + { + Operations = new Dictionary + { + [HttpMethod.Get] = new() + { + Parameters = + [ + new OpenApiParameter + { + Name = "direction", + In = ParameterLocation.Query, + Schema = new OpenApiSchema { Type = JsonSchemaType.String, Enum = [..allowedValues.Select(value => (JsonNode)JsonValue.Create(value))] } + } + ] + } + } + }; + } + [Test] [Category("Expected")] public async Task VerifyGenerateEnumerationAsync([Values("CompositeConstraintOperator", "CompositeConstraintRequestOperator", - "PrimitiveConstraintOperator", "PrimitiveConstraintRequestOperator")] string enumerationName) + "Direction", "PrimitiveConstraintOperator", "PrimitiveConstraintRequestOperator")] string enumerationName) { var generatedCode = await this.openApiEnumGenerator.GenerateEnumerationAsync(this.openApiDocument, this.outputDirectory, enumerationName); diff --git a/SysML2.NET.CodeGenerator.Tests/Generators/OpenApiHandleBarsGenerators/OpenApiEnumProviderGeneratorTestFixture.cs b/SysML2.NET.CodeGenerator.Tests/Generators/OpenApiHandleBarsGenerators/OpenApiEnumProviderGeneratorTestFixture.cs index 759420f18..cb9d4a2f3 100644 --- a/SysML2.NET.CodeGenerator.Tests/Generators/OpenApiHandleBarsGenerators/OpenApiEnumProviderGeneratorTestFixture.cs +++ b/SysML2.NET.CodeGenerator.Tests/Generators/OpenApiHandleBarsGenerators/OpenApiEnumProviderGeneratorTestFixture.cs @@ -40,25 +40,25 @@ public async Task SetUp() [Test] public async Task VerifyGenerateAsync() { - Assert.That(() => this.openApiEnumProviderGenerator.GenerateAsync(null, this.outputDirectory), Throws.TypeOf()); - Assert.That(() => this.openApiEnumProviderGenerator.GenerateAsync(this.openApiDocument, null), Throws.TypeOf()); + await Assert.ThatAsync(() => this.openApiEnumProviderGenerator.GenerateAsync(null, this.outputDirectory), Throws.TypeOf()); + await Assert.ThatAsync(() => this.openApiEnumProviderGenerator.GenerateAsync(this.openApiDocument, null), Throws.TypeOf()); using (Assert.EnterMultipleScope()) { - Assert.That(() => this.openApiEnumProviderGenerator.GenerateEnumerationProviderAsync(null, this.outputDirectory, "CompositeConstraintOperator"), Throws.TypeOf()); - Assert.That(() => this.openApiEnumProviderGenerator.GenerateEnumerationProviderAsync(this.openApiDocument, null, "CompositeConstraintOperator"), Throws.TypeOf()); - Assert.That(() => this.openApiEnumProviderGenerator.GenerateEnumerationProviderAsync(this.openApiDocument, this.outputDirectory, " "), Throws.TypeOf()); + await Assert.ThatAsync(() => this.openApiEnumProviderGenerator.GenerateEnumerationProviderAsync(null, this.outputDirectory, "CompositeConstraintOperator"), Throws.TypeOf()); + await Assert.ThatAsync(() => this.openApiEnumProviderGenerator.GenerateEnumerationProviderAsync(this.openApiDocument, null, "CompositeConstraintOperator"), Throws.TypeOf()); + await Assert.ThatAsync(() => this.openApiEnumProviderGenerator.GenerateEnumerationProviderAsync(this.openApiDocument, this.outputDirectory, " "), Throws.TypeOf()); } await Assert.ThatAsync(() => this.openApiEnumProviderGenerator.GenerateAsync(this.openApiDocument, this.outputDirectory), Throws.Nothing); - Assert.That(this.outputDirectory.GetFiles("*Provider.cs"), Has.Length.EqualTo(4)); + Assert.That(this.outputDirectory.GetFiles("*Provider.cs"), Has.Length.EqualTo(5)); } [Test] [Category("Expected")] public async Task VerifyGenerateEnumerationProviderAsync([Values("CompositeConstraintOperator", "CompositeConstraintRequestOperator", - "PrimitiveConstraintOperator", "PrimitiveConstraintRequestOperator")] string enumerationName) + "Direction", "PrimitiveConstraintOperator", "PrimitiveConstraintRequestOperator")] string enumerationName) { var generatedCode = await this.openApiEnumProviderGenerator.GenerateEnumerationProviderAsync(this.openApiDocument, this.outputDirectory, enumerationName); diff --git a/SysML2.NET.CodeGenerator.Tests/OpenApiHandleBarHelpers/HttpMethodHelperTestFixture.cs b/SysML2.NET.CodeGenerator.Tests/OpenApiHandleBarHelpers/HttpMethodHelperTestFixture.cs new file mode 100644 index 000000000..3aa3f1da0 --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/OpenApiHandleBarHelpers/HttpMethodHelperTestFixture.cs @@ -0,0 +1,51 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.CodeGenerator.Tests.OpenApiHandleBarHelpers +{ + using System; + + using HandlebarsDotNet; + using HandlebarsDotNet.Helpers; + + using NUnit.Framework; + + using SysML2.NET.CodeGenerator.OpenApiHandleBarHelpers; + + [TestFixture] + public class HttpMethodHelperTestFixture + { + [Test] + public void VerifyRegisterHttpMethodHelper() + { + Assert.That(() => ((IHandlebars)null).RegisterHttpMethodHelper(), Throws.TypeOf()); + + var handlebars = Handlebars.CreateSharedEnvironment(); + HandlebarsHelpers.Register(handlebars); + handlebars.RegisterHttpMethodHelper(); + + var template = handlebars.Compile("{{OpenApi.HttpMethod Method}}"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(template(new { Method = "GET" }), Is.EqualTo("Get")); + Assert.That(template(new { Method = "POST" }), Is.EqualTo("Post")); + Assert.That(template(new { Method = "PUT" }), Is.EqualTo("Put")); + Assert.That(template(new { Method = "DELETE" }), Is.EqualTo("Delete")); + Assert.That(template(new { Method = "HEAD" }), Is.EqualTo("Head")); + Assert.That(template(new { Method = "OPTIONS" }), Is.EqualTo("Options")); + Assert.That(template(new { Method = "TRACE" }), Is.EqualTo("Trace")); + Assert.That(template(new { Method = "get" }), Is.EqualTo("Get")); + Assert.That(() => template(new { Method = "PATCH" }), Throws.TypeOf()); + Assert.That(() => template(new { Method = (string)null }), Throws.TypeOf()); + Assert.That(() => handlebars.Compile("{{OpenApi.HttpMethod}}")(new { }), Throws.TypeOf()); + } + } + } +} diff --git a/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/ApiClientOperationModel.cs b/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/ApiClientOperationModel.cs new file mode 100644 index 000000000..903dc35fe --- /dev/null +++ b/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/ApiClientOperationModel.cs @@ -0,0 +1,59 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.CodeGenerator.Generators.OpenApiHandleBarsGenerators +{ + using System.Collections.Generic; + using System.Linq; + + using Microsoft.OpenApi; + + /// + /// The template model of one operation of the Systems Modeling API and Services, bound to its path and HTTP method + /// + public sealed class ApiClientOperationModel + { + /// + /// The names of the cursor-based pagination query parameters, which the generated client groups into one QueryParameters argument + /// + private static readonly string[] PageParameterNames = ["page[after]", "page[before]", "page[size]"]; + + /// + /// Gets the path template of the operation, relative to the server root + /// + public required string Path { get; init; } + + /// + /// Gets the HTTP method of the operation, in upper case + /// + public required string HttpMethod { get; init; } + + /// + /// Gets the as declared in the OpenAPI document + /// + public required OpenApiOperation Operation { get; init; } + + /// + /// Gets the parameters of the operation other than the cursor-based pagination query parameters, in declaration order + /// + public IEnumerable Parameters => this.Operation.Parameters?.Where(parameter => !PageParameterNames.Contains(parameter.Name)) ?? []; + + /// + /// Gets a value indicating whether the operation declares one of the cursor-based pagination query parameters + /// + public bool IsPaged => this.Operation.Parameters?.Any(parameter => PageParameterNames.Contains(parameter.Name)) == true; + + /// + /// Gets a value indicating whether the 409 response of the operation lists the conflicting data as DataIdentity items + /// + public bool ThrowsMergeConflict => this.Operation.Responses?.TryGetValue("409", out var conflictResponse) == true + && conflictResponse.Content?.TryGetValue("application/json", out var mediaType) == true + && mediaType.Schema?.Items is OpenApiSchemaReference { Reference.Id: "DataIdentity" }; + } +} diff --git a/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/ApiClientTagModel.cs b/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/ApiClientTagModel.cs new file mode 100644 index 000000000..33d5b35b3 --- /dev/null +++ b/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/ApiClientTagModel.cs @@ -0,0 +1,45 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.CodeGenerator.Generators.OpenApiHandleBarsGenerators +{ + using System.Collections.Generic; + using System.Linq; + + /// + /// The template model of the operations that share one tag of the Systems Modeling API and Services + /// + public sealed class ApiClientTagModel + { + /// + /// Gets the tag as declared in the OpenAPI document + /// + public required string Tag { get; init; } + + /// + /// Gets the identifier-safe name of the tag, which names the generated file + /// + public required string Name { get; init; } + + /// + /// Gets a value indicating whether the partial generated for this tag carries the class-level attributes, which a class declares once + /// + public required bool IsPrimary { get; init; } + + /// + /// Gets the operations of the tag, in document order + /// + public required IReadOnlyList Operations { get; init; } + + /// + /// Gets a value indicating whether an operation of the tag declares a query parameter with an inline set of allowed values + /// + public bool UsesEnumerations => this.Operations.Any(operation => operation.Operation.Parameters?.Any(parameter => parameter.Schema?.Enum?.Count > 0) == true); + } +} diff --git a/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/OpenApiApiClientGenerator.cs b/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/OpenApiApiClientGenerator.cs new file mode 100644 index 000000000..e866e3d87 --- /dev/null +++ b/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/OpenApiApiClientGenerator.cs @@ -0,0 +1,291 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.CodeGenerator.Generators.OpenApiHandleBarsGenerators +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Threading.Tasks; + + using Microsoft.OpenApi; + + using SysML2.NET.CodeGenerator.OpenApiHandleBarHelpers; + + /// + /// OpenAPI based handlebars generator for the client of the Systems Modeling API and Services, one partial per tag + /// + public class OpenApiApiClientGenerator : OpenApiHandleBarsGenerator + { + /// + /// The name of the template that generates the partial of the client for one tag + /// + private const string ClassTemplateName = "psm-apiclient-openapi-template"; + + /// + /// The name of the template that generates the interface of the client + /// + private const string InterfaceTemplateName = "psm-apiclient-interface-openapi-template"; + + /// + /// The names of the partial templates shared by the class and the interface templates + /// + private static readonly string[] PartialTemplateNames = + [ + "psm-apiclient-typename-openapi-partial", + "psm-apiclient-documentation-openapi-partial", + "psm-apiclient-signature-openapi-partial" + ]; + + /// + /// The name of the generated interface of the client + /// + private const string InterfaceName = "IPsmApiClient"; + + /// + /// Generates the interface of the client and one partial of the client per tag of the OpenAPI document + /// + /// + /// the that contains the OpenAPI model to generate from + /// + /// + /// The target + /// + /// + /// an awaitable + /// + public override Task GenerateAsync(OpenApiDocument openApiDocument, DirectoryInfo outputDirectory) + { + ArgumentNullException.ThrowIfNull(openApiDocument); + ArgumentNullException.ThrowIfNull(outputDirectory); + + return this.GenerateInternalAsync(openApiDocument, outputDirectory); + } + + /// + /// Generates the partial of the client for the specified tag + /// + /// + /// the that contains the OpenAPI model to generate from + /// + /// + /// The target + /// + /// + /// The tag, as declared in the OpenAPI document, to generate the partial for + /// + /// + /// The generated code + /// + public Task GenerateApiClientPartialAsync(OpenApiDocument openApiDocument, DirectoryInfo outputDirectory, string tag) + { + ArgumentNullException.ThrowIfNull(openApiDocument); + ArgumentNullException.ThrowIfNull(outputDirectory); + ArgumentException.ThrowIfNullOrWhiteSpace(tag); + + return this.WritePartialAsync(QueryTagModels(openApiDocument).Single(model => model.Tag == tag), outputDirectory); + } + + /// + /// Generates the interface of the client + /// + /// + /// the that contains the OpenAPI model to generate from + /// + /// + /// The target + /// + /// + /// The generated code + /// + public Task GenerateApiClientInterfaceAsync(OpenApiDocument openApiDocument, DirectoryInfo outputDirectory) + { + ArgumentNullException.ThrowIfNull(openApiDocument); + ArgumentNullException.ThrowIfNull(outputDirectory); + + return this.WriteInterfaceAsync(QueryTagModels(openApiDocument), outputDirectory); + } + + /// + /// Queries the operations of the OpenAPI document grouped by their first tag, in the tag order of the document + /// + /// + /// the that contains the OpenAPI model to generate from + /// + /// + /// One per tag that has at least one operation + /// + /// + /// Thrown when an operation declares a shape that the generated client cannot express + /// + public static IReadOnlyList QueryTagModels(OpenApiDocument openApiDocument) + { + ArgumentNullException.ThrowIfNull(openApiDocument); + + var operationsByTag = openApiDocument.Paths + .SelectMany(path => path.Value.Operations.Select(operation => (Tag: operation.Value.Tags.First().Name, Model: new ApiClientOperationModel + { + Path = path.Key, + HttpMethod = operation.Key.Method, + Operation = ValidateOperation(operation.Value) + }))) + .ToLookup(entry => entry.Tag, entry => entry.Model, StringComparer.Ordinal); + + return openApiDocument.Tags + .Where(tag => operationsByTag.Contains(tag.Name)) + .Select((tag, tagIndex) => new ApiClientTagModel + { + Tag = tag.Name, + Name = QueryTagName(tag.Name), + IsPrimary = tagIndex == 0, + Operations = operationsByTag[tag.Name].ToList() + }) + .ToList(); + } + + /// + /// Verifies that the generated client can express the operation + /// + /// + /// The to verify + /// + /// + /// The verified + /// + /// + /// Thrown when the operation has more than one successful JSON response, or an array query parameter not written as a comma separated list + /// + private static OpenApiOperation ValidateOperation(OpenApiOperation operation) + { + var successfulJsonResponseCount = operation.Responses? + .Count(response => response.Key.StartsWith('2') && response.Value.Content?.ContainsKey("application/json") == true) ?? 0; + + if (successfulJsonResponseCount > 1) + { + throw new InvalidOperationException($"The operation {operation.OperationId} declares {successfulJsonResponseCount} successful JSON responses; the generated client returns one"); + } + + var unsupportedArrayParameter = operation.Parameters? + .FirstOrDefault(parameter => parameter.In == ParameterLocation.Query + && parameter.Schema?.Type == JsonSchemaType.Array + && (parameter.Style is not (null or ParameterStyle.Form) || parameter.Explode)); + + if (unsupportedArrayParameter is not null) + { + throw new InvalidOperationException($"The array query parameter {unsupportedArrayParameter.Name} of the operation {operation.OperationId} is not declared with style form and explode false"); + } + + return operation; + } + + /// + /// Register the custom helpers + /// + protected override void RegisterHelpers() + { + this.Handlebars.RegisterTextHelper(); + this.Handlebars.RegisterHttpMethodHelper(); + } + + /// + /// Register the code templates + /// + protected override void RegisterTemplates() + { + foreach (var partialTemplateName in PartialTemplateNames) + { + this.RegisterPartialTemplate(partialTemplateName); + } + + this.RegisterTemplate(ClassTemplateName); + this.RegisterTemplate(InterfaceTemplateName); + } + + /// + /// Queries the identifier-safe name of a tag by keeping only its letters and digits + /// + /// + /// The tag as declared in the OpenAPI document + /// + /// + /// The identifier-safe name + /// + private static string QueryTagName(string tag) + { + return string.Concat(tag.Where(char.IsLetterOrDigit)); + } + + /// + /// Generates the interface of the client and one partial of the client per tag of the OpenAPI document + /// + /// + /// the that contains the OpenAPI model to generate from + /// + /// + /// The target + /// + /// + /// an awaitable + /// + private async Task GenerateInternalAsync(OpenApiDocument openApiDocument, DirectoryInfo outputDirectory) + { + var tagModels = QueryTagModels(openApiDocument); + + await this.WriteInterfaceAsync(tagModels, outputDirectory); + + foreach (var tagModel in tagModels) + { + await this.WritePartialAsync(tagModel, outputDirectory); + } + } + + /// + /// Applies the class template to the tag and writes the result to disk + /// + /// + /// The to generate the partial for + /// + /// + /// The target + /// + /// + /// The generated code + /// + private async Task WritePartialAsync(ApiClientTagModel tagModel, DirectoryInfo outputDirectory) + { + var generatedCode = this.CodeCleanup(this.Templates[ClassTemplateName](tagModel)); + + await WriteAsync(generatedCode, outputDirectory, $"{tagModel.Name}Api.cs"); + + return generatedCode; + } + + /// + /// Applies the interface template to all tags and writes the result to disk + /// + /// + /// The s whose operations the interface declares + /// + /// + /// The target + /// + /// + /// The generated code + /// + private async Task WriteInterfaceAsync(IReadOnlyList tagModels, DirectoryInfo outputDirectory) + { + var generatedCode = this.CodeCleanup(this.Templates[InterfaceTemplateName](tagModels)); + + await WriteAsync(generatedCode, outputDirectory, $"{InterfaceName}.cs"); + + return generatedCode; + } + } +} diff --git a/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/OpenApiEnumGenerator.cs b/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/OpenApiEnumGenerator.cs index 3db2f8ff4..930b60b9d 100644 --- a/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/OpenApiEnumGenerator.cs +++ b/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/OpenApiEnumGenerator.cs @@ -75,13 +75,13 @@ public Task GenerateEnumerationAsync(OpenApiDocument openApiDocument, Di } /// - /// Queries the enumerations declared inline by the schemas of the OpenAPI document + /// Queries the enumerations declared inline by the schemas and by the query parameters of the OpenAPI document /// /// /// the that contains the OpenAPI model to generate from /// /// - /// The generated enumeration name paired with the property schema that declares it, ordered by name + /// The generated enumeration name paired with the schema that declares it, ordered by name /// public static IReadOnlyList> QueryEnumerations(OpenApiDocument openApiDocument) { @@ -89,10 +89,40 @@ public static IReadOnlyList> QueryEnumerati return QuerySchemas(openApiDocument) .SelectMany(schema => schema.Value.QueryEnumerations(schema.Key)) + .Concat(QueryParameterEnumerations(openApiDocument)) .OrderBy(enumeration => enumeration.Key, StringComparer.Ordinal) .ToList(); } + /// + /// Queries the enumerations declared inline by the query parameters of the operations of the OpenAPI document + /// + /// + /// the that contains the OpenAPI model to generate from + /// + /// + /// The enumeration name, derived from the parameter name, paired with the parameter schema that declares it + /// + /// + /// Thrown when parameters with the same name declare different sets of allowed values + /// + public static IReadOnlyList> QueryParameterEnumerations(OpenApiDocument openApiDocument) + { + ArgumentNullException.ThrowIfNull(openApiDocument); + + return openApiDocument.Paths + .SelectMany(path => path.Value.Operations.Values) + .SelectMany(operation => operation.Parameters ?? []) + .Where(parameter => parameter.In == ParameterLocation.Query && parameter.Schema?.Enum?.Count > 0) + .GroupBy(parameter => OpenApiSchemaExtensions.QueryPropertyName(parameter.Name), StringComparer.Ordinal) + .Select(group => group.Select(parameter => parameter.Schema).DistinctBy(schema => string.Join("|", schema.QueryEnumerationValues())).ToList() switch + { + [var schema] => new KeyValuePair(group.Key, schema), + _ => throw new InvalidOperationException($"The query parameters named {group.Key} declare different sets of allowed values") + }) + .ToList(); + } + /// /// Register the custom helpers /// diff --git a/SysML2.NET.CodeGenerator/OpenApiHandleBarHelpers/HttpMethodHelper.cs b/SysML2.NET.CodeGenerator/OpenApiHandleBarHelpers/HttpMethodHelper.cs new file mode 100644 index 000000000..701b26e21 --- /dev/null +++ b/SysML2.NET.CodeGenerator/OpenApiHandleBarHelpers/HttpMethodHelper.cs @@ -0,0 +1,55 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.CodeGenerator.OpenApiHandleBarHelpers +{ + using System; + using System.Globalization; + + using HandlebarsDotNet; + + /// + /// A handlebars helper that converts an HTTP method name to the name of the matching static member + /// + public static class HttpMethodHelper + { + /// + /// Registers the + /// + /// + /// The context with which the helper needs to be registered + /// + public static void RegisterHttpMethodHelper(this IHandlebars handlebars) + { + ArgumentNullException.ThrowIfNull(handlebars); + + handlebars.RegisterHelper("OpenApi.HttpMethod", (_, arguments) => + { + if (arguments.Length != 1) + { + throw new HandlebarsException("{{OpenApi.HttpMethod}} helper must have exactly one argument"); + } + + var method = Convert.ToString(arguments[0], CultureInfo.InvariantCulture)?.ToUpperInvariant(); + + return method switch + { + "GET" => "Get", + "POST" => "Post", + "PUT" => "Put", + "DELETE" => "Delete", + "HEAD" => "Head", + "OPTIONS" => "Options", + "TRACE" => "Trace", + _ => throw new HandlebarsException($"{{{{OpenApi.HttpMethod}}}} helper does not support the HTTP method '{method}'") + }; + }); + } + } +} diff --git a/SysML2.NET.CodeGenerator/OpenApiHandleBarHelpers/TextHelper.cs b/SysML2.NET.CodeGenerator/OpenApiHandleBarHelpers/TextHelper.cs new file mode 100644 index 000000000..438a5d8c8 --- /dev/null +++ b/SysML2.NET.CodeGenerator/OpenApiHandleBarHelpers/TextHelper.cs @@ -0,0 +1,43 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.CodeGenerator.OpenApiHandleBarHelpers +{ + using System; + using System.Globalization; + + using HandlebarsDotNet; + + /// + /// A handlebars helper that converts a value to its invariant text so that templates can compare enumeration values + /// + public static class TextHelper + { + /// + /// Registers the + /// + /// + /// The context with which the helper needs to be registered + /// + public static void RegisterTextHelper(this IHandlebars handlebars) + { + ArgumentNullException.ThrowIfNull(handlebars); + + handlebars.RegisterHelper("OpenApi.Text", (_, arguments) => + { + if (arguments.Length != 1) + { + throw new HandlebarsException("{{OpenApi.Text}} helper must have exactly one argument"); + } + + return Convert.ToString(arguments[0], CultureInfo.InvariantCulture) ?? string.Empty; + }); + } + } +} diff --git a/SysML2.NET.CodeGenerator/Resources/SysML.ecore b/SysML2.NET.CodeGenerator/Resources/SysML.ecore deleted file mode 100644 index a6463a006..000000000 --- a/SysML2.NET.CodeGenerator/Resources/SysML.ecore +++ /dev/null @@ -1,4528 +0,0 @@ - - - - -
- - - -
- - - - - -
- - - - - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - - - -
- - -
- - - - - - -
- - - -
- - - - - -
- - - - - -
- - - - -
- - - - -
- - - - -
- - - - - -
- - - - - -
- - - - - -
- - - - - -
- - - - -
- - - - -
- - -
- - - - -
- - -
- - - - - -
- - - - - -
- - - - -
- - - - -
- - - - - -
- - -
- - - - -
- - - - -
- - -
- - - - -
- - - - - -
- - - - - -
- - - - -
- - - - -
- - - - - - -
- - - -
- - - - - -
- - - - - -
- - - - -
- - - - - -
- - - - - -
- - -
- - - - - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - - - -
- - -
- - - - - -
- - -
- - - - - -
- - - - - -
- - - - -
- - - - - -
- - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - - - - -
- - -
- - - - - -
- - - -
- - - - - -
- - - - - -
- - - - - - - -
- - - - - -
- - - - - -
- - - - - -
- - -
- - - - - -
- - - - - -
- - -
- - - - -
- - -
- - - - - - -
- - - -
- - - - -
- - - - -
- - - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - - -
- - - - - -
- - - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - - -
- - - -
- - - - - -
- - - - - -
- - - - - -
- - - - - - - -
- - - -
- - - - - -
- - - - -
- - - - - - -
- - - - -
- - -
- - - - - -
- - - - -
- - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - - - - -
- - - - - - -
- - - -
- - - - -
- - - - -
- - - - - -
- - - -
- - - - - - - -
- - - -
- - - - -
- - - - - -
- - - -
- - - - -
- - -
- - - - - -
- - - -
- - -
- - - - - -
- - - - - -
- - - - - - -
- - - -
- - - - -
- - - - -
- - - - - - - -
- - - -
- - - - - -
- - -
- - - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - -
- - - - - - -
- - - -
- - - - - - -
- - - - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - -
- - - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - -
- - - - - - -
- - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - -
- - - - - - -
- - - -
- - - - -
- - - - -
- - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - -
- - - - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - -
- - - - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - -
- - - - - - -
- - - -
- - -
- - - - - -
- - - - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - - - - - -
- - - -
- - - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - -
- - - - - - -
- - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - - - -
- - - - - -
- - - - -
- - - -
- - - - - -
- - - -
- - - - -
- - - - - -
- - - -
- - -
- - - - - -
- - - - - -
- - - - -
- - - -
- - - - - - -
- - - - -
- - - -
- - - - - -
- - - - - - -
- - - -
- - -
- - - - - -
- - - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - -
- - -
- - - - -
- - -
- - - - -
- - - - - -
- - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - - -
- - - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - -
- - - - - -
- - - - -
- - - -
- - -
- - - - -
- - -
- - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - - -
- - - -
- - -
- - - - -
- - -
- - - - - -
- - -
- - - - - -
- - - - -
- - - -
- - -
- - - - - -
- - -
- - - - -
- - -
- - - - -
- - -
- - - - - -
- - - -
- - -
- - - - - - -
- - -
- - - - - -
- - - - -
- - -
- - - - - -
- - -
- - - - - - -
- - -
- - - - - - - -
- - - -
- - -
- - - - - - -
- - -
- - - - - - -
- - -
- - - - - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - -
- - - - - -
- - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - - -
- - - -
- - - - - -
- - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - - -
- - - - -
- - - -
- - -
- - - - - - -
- - - - -
- - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - - -
- - - - -
- - - - -
- - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - - -
- - - - -
- - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - - - -
- - - - -
- - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - - -
- - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - -
- - - -
- - -
- - - - - -
- - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - -
- - - -
- - -
- - - - - -
- - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - - -
- - - - -
- - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - -
- - - -
- - -
- - - - - -
- - - - -
- - - -
- - -
- - - - -
- - -
- - - - -
- - -
- - - - -
- - - - - -
- - - - - -
- - - - - - - - -
- - - -
- - -
- - - - - -
- - - -
- - - - - -
- - - -
- - -
- - - - - - -
- - - -
- - - - -
- - - - - -
- - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - - -
- - - -
- - - - -
- - -
- - - - - -
- - -
- - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - -
- - - -
- - -
- - - - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - -
- - -
- - - - -
- - -
- - - - -
- - - - - -
- - - -
- - - - -
- - -
- - - - - - -
- - - -
- - - - -
- - - - -
- - - - - -
- - - -
- - - - -
- - -
- - - - - - -
- - - -
- - - - -
- - - - -
- - - - - -
- - - -
- - -
- - - - - - -
- - - -
- - -
- - - - -
- - -
- - - - - -
- - - -
- - -
- - - - - -
- - - -
- - -
- - - - -
- - -
- - - - -
- - -
- - - - - -
- - - -
- - - - - -
- - - -
- - -
- - - - - - -
- - - -
- - - - -
- - - - -
- - - - - -
- - - -
- - -
- - - - -
- - -
- - - - - -
- - - - -
- - - - -
- - - -
- - -
- - - - -
- - -
- - - - - -
- - - - -
- - - -
- - -
- - - - -
- - -
- - - - -
- - -
- - - - - - -
- - - - -
- - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - - -
- - - -
- - - - - - -
- - - -
- - -
- - - - - - -
- - - - -
- - - - -
- - - - -
- - - -
- - - - - -
- - -
- - - - - -
- - - - - - -
- - - - -
- - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - -
- - -
- - - - - - - -
- - - -
- - -
- - - - - -
- - -
- - - - - -
- - - - -
- - - - - -
- - - - -
- - - - -
- - - -
- - - - -
- - - - - -
- - - - -
- - - -
- - - - - -
- - - - -
- - - -
- - - - - -
- - - -
- - - - - -
- - - -
- - -
- - - - - - -
- - - -
- - -
- - - - - - -
- - - -
- - - - - -
- - - - -
- - - - -
- - - -
- - -
- - - - - - -
- - - -
- - - - - -
- - - - - - -
- - -
- - - - - diff --git a/SysML2.NET.CodeGenerator/SysML2.NET.CodeGenerator.csproj b/SysML2.NET.CodeGenerator/SysML2.NET.CodeGenerator.csproj index 6dbf6fbff..e627ac8a7 100644 --- a/SysML2.NET.CodeGenerator/SysML2.NET.CodeGenerator.csproj +++ b/SysML2.NET.CodeGenerator/SysML2.NET.CodeGenerator.csproj @@ -293,6 +293,21 @@ Always + + Always + + + Always + + + Always + + + Always + + + Always + diff --git a/SysML2.NET.CodeGenerator/Templates/OpenAPI/Partials/psm-apiclient-documentation-openapi-partial.hbs b/SysML2.NET.CodeGenerator/Templates/OpenAPI/Partials/psm-apiclient-documentation-openapi-partial.hbs new file mode 100644 index 000000000..f6b220ddf --- /dev/null +++ b/SysML2.NET.CodeGenerator/Templates/OpenAPI/Partials/psm-apiclient-documentation-openapi-partial.hbs @@ -0,0 +1,69 @@ +/// +/// {{Operation.Summary}} +/// +{{#each Parameters}} +{{#if Required}} +/// +/// {{Description}} +/// +{{/if}} +{{/each}} +{{#if Operation.RequestBody}} +/// +/// The payload sent as the body of the request +/// +{{/if}} +{{#each Parameters}} +{{#unless Required}} +/// +/// {{Description}} +/// +{{/unless}} +{{/each}} +{{#if IsPaged}} +/// +/// The page of the result to request, or null to let the server choose +/// +{{/if}} +/// +/// The that cancels the request +/// +/// +/// The deserialized response of the {{Operation.OperationId}} operation +/// +{{#each Parameters}} +{{#if Required}} +{{#if (String.Equal (OpenApi.Text Schema.Type) 'Array')}} +/// +/// Thrown when is null +/// +/// +/// Thrown when is empty +/// +{{else if (String.Equal (OpenApi.Text Schema.Type) 'String')}} +{{#unless Schema.Enum}} +{{#unless (String.Equal Schema.Format 'uuid')}} +/// +/// Thrown when is null +/// +/// +/// Thrown when is empty or whitespace +/// +{{/unless}} +{{/unless}} +{{/if}} +{{/if}} +{{/each}} +{{#if Operation.RequestBody.Required}} +/// +/// Thrown when is null +/// +{{/if}} +/// +/// Thrown when the server responds with an error status code +/// +{{#if ThrowsMergeConflict}} +/// +/// Thrown when the server reports conflicting data +/// +{{/if}} diff --git a/SysML2.NET.CodeGenerator/Templates/OpenAPI/Partials/psm-apiclient-signature-openapi-partial.hbs b/SysML2.NET.CodeGenerator/Templates/OpenAPI/Partials/psm-apiclient-signature-openapi-partial.hbs new file mode 100644 index 000000000..6fa877dec --- /dev/null +++ b/SysML2.NET.CodeGenerator/Templates/OpenAPI/Partials/psm-apiclient-signature-openapi-partial.hbs @@ -0,0 +1,34 @@ +{{~#*inline "scalar"~}} +{{~#if (String.Equal Format 'uuid')}}Guid{{else if (String.Equal (OpenApi.Text Type) 'Integer')}}int{{else if (String.Equal (OpenApi.Text Type) 'Boolean')}}bool{{else}}string{{/if~}} +{{~/inline~}} +{{~#*inline "nullable"~}} +{{~#if (String.Equal Format 'uuid')}}?{{else if (String.Equal (OpenApi.Text Type) 'Integer')}}?{{else if (String.Equal (OpenApi.Text Type) 'Boolean')}}?{{/if~}} +{{~/inline~}} +{{~#*inline "parametertype"~}} +{{~#if Schema.Enum}}{{String.Pascalcase Name}}{{#unless Required}}?{{/unless}}{{else if (String.Equal (OpenApi.Text Schema.Type) 'Array')}}IEnumerable<{{> scalar Schema.Items}}>{{else}}{{> scalar Schema}}{{#unless Required}}{{> nullable Schema}}{{/unless}}{{/if~}} +{{~/inline~}} +Task< +{{~#each Operation.Responses~}} +{{~#if (String.StartsWith @key '2')~}} +{{~#each Content~}} +{{~#if (String.Equal @key 'application/json')~}} +{{~#if Schema.Items}}PagedResult<{{> psm-apiclient-typename-openapi-partial Schema.Items}}>{{else if Schema.Reference}}{{> psm-apiclient-typename-openapi-partial Schema}}{{else if ../../IsPaged}}PagedResult{{else}}JsonDocument{{/if~}} +{{~/if~}} +{{~/each~}} +{{~/if~}} +{{~/each~}} +> {{String.Pascalcase Operation.OperationId}}Async( +{{~#each Parameters~}} +{{~#if Required}}{{> parametertype}} {{Name}}, {{/if~}} +{{~/each~}} +{{~#with Operation.RequestBody~}} +{{~#each Content~}} +{{~#if Schema.Reference}}{{Schema.Reference.Id}} requestBody{{#unless ../Required}} = null{{/unless}}, {{/if~}} +{{~#each Schema.OneOf~}}{{~#if Items}}IEnumerable<{{> psm-apiclient-typename-openapi-partial Items}}> requestBody{{#unless ../../Required}} = null{{/unless}}, {{/if~}}{{~/each~}} +{{~/each~}} +{{~/with~}} +{{~#each Parameters~}} +{{~#unless Required}}{{> parametertype}} {{Name}} = null, {{/unless~}} +{{~/each~}} +{{~#if IsPaged}}QueryParameters queryParameters = null, {{/if~}} +CancellationToken cancellationToken = default) \ No newline at end of file diff --git a/SysML2.NET.CodeGenerator/Templates/OpenAPI/Partials/psm-apiclient-typename-openapi-partial.hbs b/SysML2.NET.CodeGenerator/Templates/OpenAPI/Partials/psm-apiclient-typename-openapi-partial.hbs new file mode 100644 index 000000000..f002c4bdb --- /dev/null +++ b/SysML2.NET.CodeGenerator/Templates/OpenAPI/Partials/psm-apiclient-typename-openapi-partial.hbs @@ -0,0 +1 @@ +{{~#if (String.Equal Reference.Id 'Element')}}IElement{{else if (String.Equal Reference.Id 'Relationship')}}IRelationship{{else if (String.Equal Reference.Id 'Data')}}IData{{else}}{{Reference.Id}}{{/if~}} \ No newline at end of file diff --git a/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-interface-openapi-template.hbs b/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-interface-openapi-template.hbs new file mode 100644 index 000000000..68915cb34 --- /dev/null +++ b/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-interface-openapi-template.hbs @@ -0,0 +1,51 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + using System; + using System.CodeDom.Compiler; + using System.Collections.Generic; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + + using SysML2.NET.Common; + using SysML2.NET.Core.DTO.Root.Elements; + using SysML2.NET.PSM; + using SysML2.NET.PSM.DTO; + using SysML2.NET.PSM.Enumerations; + using SysML2.NET.REST.Exceptions; + + /// + /// The client of the REST/HTTP platform specific model of the Systems Modeling API and Services + /// + [GeneratedCode("SysML2.NET", "latest")] + public partial interface IPsmApiClient + { + {{#each this}} + {{#each Operations}} + {{#unless (Boolean.And @../first @first)}} + + {{/unless}} + {{> psm-apiclient-documentation-openapi-partial}} + {{> psm-apiclient-signature-openapi-partial}}; + {{/each}} + {{/each}} + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-openapi-template.hbs b/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-openapi-template.hbs new file mode 100644 index 000000000..984fc4529 --- /dev/null +++ b/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-openapi-template.hbs @@ -0,0 +1,124 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +{{#*inline "family"~}} +{{~#if (String.Equal Reference.Id 'Element')}}Data{{else if (String.Equal Reference.Id 'Relationship')}}Data{{else if (String.Equal Reference.Id 'Data')}}Data{{else}}Response{{/if~}} +{{~/inline}} +namespace SySML2.NET.REST +{ + using System; + {{#if IsPrimary}} + using System.CodeDom.Compiler; + {{/if}} + using System.Collections.Generic; + using System.Net.Http; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + + using SysML2.NET.Common; + using SysML2.NET.Core.DTO.Root.Elements; + {{#if UsesEnumerations}} + using SysML2.NET.Extensions.PSM; + {{/if}} + using SysML2.NET.PSM; + using SysML2.NET.PSM.DTO; + {{#if UsesEnumerations}} + using SysML2.NET.PSM.Enumerations; + {{/if}} + using SysML2.NET.REST.Exceptions; + + /// + /// Provides the {{Tag}} operations of the Systems Modeling API and Services + /// + {{#if IsPrimary}} + [GeneratedCode("SysML2.NET", "latest")] + {{/if}} + public partial class PsmApiClient + { + {{#each Operations}} + {{> psm-apiclient-documentation-openapi-partial}} + public async {{> psm-apiclient-signature-openapi-partial}} + { + {{#each Parameters}} + {{#if Required}} + {{#if (String.Equal (OpenApi.Text Schema.Type) 'Array')}} + ThrowIfNullOrEmpty({{Name}}, nameof({{Name}})); + + {{else if (String.Equal (OpenApi.Text Schema.Type) 'String')}} + {{#unless Schema.Enum}} + {{#unless (String.Equal Schema.Format 'uuid')}} + ThrowIfNullOrWhiteSpace({{Name}}, nameof({{Name}})); + + {{/unless}} + {{/unless}} + {{/if}} + {{/if}} + {{/each}} + {{#if Operation.RequestBody.Required}} + ThrowIfNull(requestBody, nameof(requestBody)); + + {{/if}} + var query = new QueryStringBuilder({{#if IsPaged}}queryParameters{{/if}}); + {{#each Parameters}} + {{#if (String.Equal (OpenApi.Text In) 'Query')}} + {{#if Schema.Enum}} + query.Add("{{Name}}", {{Name}} is { } {{Name}}Value ? ToQueryValue({{String.Pascalcase Name}}Provider.ToUtf8Bytes({{Name}}Value)) : null); + {{else}} + query.Add("{{Name}}", {{Name}}); + {{/if}} + {{/if}} + {{/each}} + + {{#if Operation.RequestBody}} + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(HttpMethod.{{OpenApi.HttpMethod HttpMethod}}, $"{{{String.Replace (String.Replace (String.Substring Path 1) '{' '{Escape(') '}' ')}'}}}{query}", content, cancellationToken); + {{else}} + using var response = await this.SendAsync(HttpMethod.{{OpenApi.HttpMethod HttpMethod}}, $"{{{String.Replace (String.Replace (String.Substring Path 1) '{' '{Escape(') '}' ')}'}}}{query}", null, cancellationToken); + {{/if}} + {{#if ThrowsMergeConflict}} + + await this.ThrowOnConflictAsync(response, cancellationToken); + {{/if}} + + await this.EnsureSuccessAsync(response, cancellationToken); + + {{#each Operation.Responses}} + {{#if (String.StartsWith @key '2')}} + {{#each Content}} + {{#if (String.Equal @key 'application/json')}} + {{#if Schema.Items}} + return await this.ReadPaged{{> family Schema.Items}}Async<{{> psm-apiclient-typename-openapi-partial Schema.Items}}>(response, cancellationToken); + {{else if Schema.Reference}} + return await this.Read{{> family Schema}}Async<{{> psm-apiclient-typename-openapi-partial Schema}}>(response, cancellationToken); + {{else if ../../IsPaged}} + return await ReadPagedJsonAsync(response, cancellationToken); + {{else}} + return await ReadJsonAsync(response, cancellationToken); + {{/if}} + {{/if}} + {{/each}} + {{/if}} + {{/each}} + } + {{#unless @last}} + + {{/unless}} + {{/each}} + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-enumprovider-openapi-template.hbs b/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-enumprovider-openapi-template.hbs index 78083185c..c44605e4f 100644 --- a/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-enumprovider-openapi-template.hbs +++ b/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-enumprovider-openapi-template.hbs @@ -46,26 +46,6 @@ namespace SysML2.NET.Extensions.PSM throw new ArgumentException($"'{new string(value)}' is not a valid {{this.Key}}", nameof(value)); } - /// - /// Tries to parse the to a - /// - /// The that is to be parsed. - /// The parsed literal, or default when the value denotes none. - /// True when the value was parsed. - public static bool TryParse(ReadOnlySpan value, out {{this.Key}} result) - { - {{#each (Enumeration.QueryValues this.Value) as | enumerationValue |}} - if (value.Length == {{#Enumeration.WriteValueLength enumerationValue}} && value.Equals("{{{enumerationValue}}}".AsSpan(), StringComparison.OrdinalIgnoreCase)) - { - result = {{../this.Key}}.{{#Enumeration.WriteLiteralName enumerationValue}}; - return true; - } - - {{/each}} - result = default; - return false; - } - /// /// Parses the UTF-8 encoded to a /// @@ -113,6 +93,26 @@ namespace SysML2.NET.Extensions.PSM return Parse(buffer[..(int)value.Length]); } + /// + /// Tries to parse the to a + /// + /// The that is to be parsed. + /// The parsed literal, or default when the value denotes none. + /// True when the value was parsed. + public static bool TryParse(ReadOnlySpan value, out {{this.Key}} result) + { + {{#each (Enumeration.QueryValues this.Value) as | enumerationValue |}} + if (value.Length == {{#Enumeration.WriteValueLength enumerationValue}} && value.Equals("{{{enumerationValue}}}".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + result = {{../this.Key}}.{{#Enumeration.WriteLiteralName enumerationValue}}; + return true; + } + + {{/each}} + result = default; + return false; + } + /// /// Converts a to its UTF-8 encoded representation on the wire /// diff --git a/SysML2.NET.Extensions/PSM/AutoGenEnumProvider/CompositeConstraintOperatorProvider.cs b/SysML2.NET.Extensions/PSM/AutoGenEnumProvider/CompositeConstraintOperatorProvider.cs index e0f1b7bb1..34e5fe941 100644 --- a/SysML2.NET.Extensions/PSM/AutoGenEnumProvider/CompositeConstraintOperatorProvider.cs +++ b/SysML2.NET.Extensions/PSM/AutoGenEnumProvider/CompositeConstraintOperatorProvider.cs @@ -46,30 +46,6 @@ public static CompositeConstraintOperator Parse(ReadOnlySpan value) throw new ArgumentException($"'{new string(value)}' is not a valid CompositeConstraintOperator", nameof(value)); } - /// - /// Tries to parse the to a - /// - /// The that is to be parsed. - /// The parsed literal, or default when the value denotes none. - /// True when the value was parsed. - public static bool TryParse(ReadOnlySpan value, out CompositeConstraintOperator result) - { - if (value.Length == 3 && value.Equals("and".AsSpan(), StringComparison.OrdinalIgnoreCase)) - { - result = CompositeConstraintOperator.and; - return true; - } - - if (value.Length == 2 && value.Equals("or".AsSpan(), StringComparison.OrdinalIgnoreCase)) - { - result = CompositeConstraintOperator.or; - return true; - } - - result = default; - return false; - } - /// /// Parses the UTF-8 encoded to a /// @@ -117,6 +93,30 @@ public static CompositeConstraintOperator Parse(in ReadOnlySequence value) return Parse(buffer[..(int)value.Length]); } + /// + /// Tries to parse the to a + /// + /// The that is to be parsed. + /// The parsed literal, or default when the value denotes none. + /// True when the value was parsed. + public static bool TryParse(ReadOnlySpan value, out CompositeConstraintOperator result) + { + if (value.Length == 3 && value.Equals("and".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + result = CompositeConstraintOperator.and; + return true; + } + + if (value.Length == 2 && value.Equals("or".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + result = CompositeConstraintOperator.or; + return true; + } + + result = default; + return false; + } + /// /// Converts a to its UTF-8 encoded representation on the wire /// diff --git a/SysML2.NET.Extensions/PSM/AutoGenEnumProvider/CompositeConstraintRequestOperatorProvider.cs b/SysML2.NET.Extensions/PSM/AutoGenEnumProvider/CompositeConstraintRequestOperatorProvider.cs index 141f9769e..4da6fdc3d 100644 --- a/SysML2.NET.Extensions/PSM/AutoGenEnumProvider/CompositeConstraintRequestOperatorProvider.cs +++ b/SysML2.NET.Extensions/PSM/AutoGenEnumProvider/CompositeConstraintRequestOperatorProvider.cs @@ -46,30 +46,6 @@ public static CompositeConstraintRequestOperator Parse(ReadOnlySpan value) throw new ArgumentException($"'{new string(value)}' is not a valid CompositeConstraintRequestOperator", nameof(value)); } - /// - /// Tries to parse the to a - /// - /// The that is to be parsed. - /// The parsed literal, or default when the value denotes none. - /// True when the value was parsed. - public static bool TryParse(ReadOnlySpan value, out CompositeConstraintRequestOperator result) - { - if (value.Length == 3 && value.Equals("and".AsSpan(), StringComparison.OrdinalIgnoreCase)) - { - result = CompositeConstraintRequestOperator.and; - return true; - } - - if (value.Length == 2 && value.Equals("or".AsSpan(), StringComparison.OrdinalIgnoreCase)) - { - result = CompositeConstraintRequestOperator.or; - return true; - } - - result = default; - return false; - } - /// /// Parses the UTF-8 encoded to a /// @@ -117,6 +93,30 @@ public static CompositeConstraintRequestOperator Parse(in ReadOnlySequence return Parse(buffer[..(int)value.Length]); } + /// + /// Tries to parse the to a + /// + /// The that is to be parsed. + /// The parsed literal, or default when the value denotes none. + /// True when the value was parsed. + public static bool TryParse(ReadOnlySpan value, out CompositeConstraintRequestOperator result) + { + if (value.Length == 3 && value.Equals("and".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + result = CompositeConstraintRequestOperator.and; + return true; + } + + if (value.Length == 2 && value.Equals("or".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + result = CompositeConstraintRequestOperator.or; + return true; + } + + result = default; + return false; + } + /// /// Converts a to its UTF-8 encoded representation on the wire /// diff --git a/SysML2.NET.Extensions/PSM/AutoGenEnumProvider/DirectionProvider.cs b/SysML2.NET.Extensions/PSM/AutoGenEnumProvider/DirectionProvider.cs new file mode 100644 index 000000000..313cbf737 --- /dev/null +++ b/SysML2.NET.Extensions/PSM/AutoGenEnumProvider/DirectionProvider.cs @@ -0,0 +1,147 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Extensions.PSM +{ + using System; + using System.Buffers; + using System.Text; + + using SysML2.NET.PSM.Enumerations; + + /// + /// The purpose of the is to convert a + /// to and from its representation in the Systems Modeling API and Services + /// + public static class DirectionProvider + { + /// + /// The length in bytes of the longest Direction representation + /// + private const int LongestValueByteLength = 4; + + /// + /// Parses the to a + /// + /// The that is to be parsed. + /// A enumeration literal. + /// Thrown when the value denotes no literal. + public static Direction Parse(ReadOnlySpan value) + { + if (TryParse(value, out var result)) + { + return result; + } + + throw new ArgumentException($"'{new string(value)}' is not a valid Direction", nameof(value)); + } + + /// + /// Parses the UTF-8 encoded to a + /// + /// The that is to be parsed. + /// A enumeration literal. + /// Thrown when the value denotes no literal. + public static Direction Parse(ReadOnlySpan value) + { + if (value.Length <= LongestValueByteLength) + { + Span characters = stackalloc char[LongestValueByteLength]; + + var characterCount = Encoding.UTF8.GetChars(value, characters); + + if (TryParse(characters[..characterCount], out var result)) + { + return result; + } + } + + throw new ArgumentException($"'{Encoding.UTF8.GetString(value)}' is not a valid Direction", nameof(value)); + } + + /// + /// Parses the UTF-8 encoded to a + /// + /// The that is to be parsed. + /// A enumeration literal. + /// Thrown when the value denotes no literal. + public static Direction Parse(in ReadOnlySequence value) + { + if (value.IsSingleSegment) + { + return Parse(value.FirstSpan); + } + + if (value.Length > LongestValueByteLength) + { + throw new ArgumentException($"a Direction is at most {LongestValueByteLength} bytes long", nameof(value)); + } + + Span buffer = stackalloc byte[LongestValueByteLength]; + value.CopyTo(buffer); + + return Parse(buffer[..(int)value.Length]); + } + + /// + /// Tries to parse the to a + /// + /// The that is to be parsed. + /// The parsed literal, or default when the value denotes none. + /// True when the value was parsed. + public static bool TryParse(ReadOnlySpan value, out Direction result) + { + if (value.Length == 2 && value.Equals("in".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + result = Direction.@in; + return true; + } + + if (value.Length == 3 && value.Equals("out".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + result = Direction.@out; + return true; + } + + if (value.Length == 4 && value.Equals("both".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + result = Direction.both; + return true; + } + + result = default; + return false; + } + + /// + /// Converts a to its UTF-8 encoded representation on the wire + /// + /// The that is to be converted. + /// A backed by static UTF-8 data. + /// Thrown when the literal is not known. + public static ReadOnlySpan ToUtf8Bytes(Direction value) + { + return value switch + { + Direction.@in => "in"u8, + Direction.@out => "out"u8, + Direction.both => "both"u8, + _ => throw new ArgumentOutOfRangeException(nameof(value)) + }; + } + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SysML2.NET.Extensions/PSM/AutoGenEnumProvider/PrimitiveConstraintOperatorProvider.cs b/SysML2.NET.Extensions/PSM/AutoGenEnumProvider/PrimitiveConstraintOperatorProvider.cs index 07f7e46d9..158ca28ea 100644 --- a/SysML2.NET.Extensions/PSM/AutoGenEnumProvider/PrimitiveConstraintOperatorProvider.cs +++ b/SysML2.NET.Extensions/PSM/AutoGenEnumProvider/PrimitiveConstraintOperatorProvider.cs @@ -46,6 +46,53 @@ public static PrimitiveConstraintOperator Parse(ReadOnlySpan value) throw new ArgumentException($"'{new string(value)}' is not a valid PrimitiveConstraintOperator", nameof(value)); } + /// + /// Parses the UTF-8 encoded to a + /// + /// The that is to be parsed. + /// A enumeration literal. + /// Thrown when the value denotes no literal. + public static PrimitiveConstraintOperator Parse(ReadOnlySpan value) + { + if (value.Length <= LongestValueByteLength) + { + Span characters = stackalloc char[LongestValueByteLength]; + + var characterCount = Encoding.UTF8.GetChars(value, characters); + + if (TryParse(characters[..characterCount], out var result)) + { + return result; + } + } + + throw new ArgumentException($"'{Encoding.UTF8.GetString(value)}' is not a valid PrimitiveConstraintOperator", nameof(value)); + } + + /// + /// Parses the UTF-8 encoded to a + /// + /// The that is to be parsed. + /// A enumeration literal. + /// Thrown when the value denotes no literal. + public static PrimitiveConstraintOperator Parse(in ReadOnlySequence value) + { + if (value.IsSingleSegment) + { + return Parse(value.FirstSpan); + } + + if (value.Length > LongestValueByteLength) + { + throw new ArgumentException($"a PrimitiveConstraintOperator is at most {LongestValueByteLength} bytes long", nameof(value)); + } + + Span buffer = stackalloc byte[LongestValueByteLength]; + value.CopyTo(buffer); + + return Parse(buffer[..(int)value.Length]); + } + /// /// Tries to parse the to a /// @@ -100,53 +147,6 @@ public static bool TryParse(ReadOnlySpan value, out PrimitiveConstraintOpe return false; } - /// - /// Parses the UTF-8 encoded to a - /// - /// The that is to be parsed. - /// A enumeration literal. - /// Thrown when the value denotes no literal. - public static PrimitiveConstraintOperator Parse(ReadOnlySpan value) - { - if (value.Length <= LongestValueByteLength) - { - Span characters = stackalloc char[LongestValueByteLength]; - - var characterCount = Encoding.UTF8.GetChars(value, characters); - - if (TryParse(characters[..characterCount], out var result)) - { - return result; - } - } - - throw new ArgumentException($"'{Encoding.UTF8.GetString(value)}' is not a valid PrimitiveConstraintOperator", nameof(value)); - } - - /// - /// Parses the UTF-8 encoded to a - /// - /// The that is to be parsed. - /// A enumeration literal. - /// Thrown when the value denotes no literal. - public static PrimitiveConstraintOperator Parse(in ReadOnlySequence value) - { - if (value.IsSingleSegment) - { - return Parse(value.FirstSpan); - } - - if (value.Length > LongestValueByteLength) - { - throw new ArgumentException($"a PrimitiveConstraintOperator is at most {LongestValueByteLength} bytes long", nameof(value)); - } - - Span buffer = stackalloc byte[LongestValueByteLength]; - value.CopyTo(buffer); - - return Parse(buffer[..(int)value.Length]); - } - /// /// Converts a to its UTF-8 encoded representation on the wire /// diff --git a/SysML2.NET.Extensions/PSM/AutoGenEnumProvider/PrimitiveConstraintRequestOperatorProvider.cs b/SysML2.NET.Extensions/PSM/AutoGenEnumProvider/PrimitiveConstraintRequestOperatorProvider.cs index ce1ea16c7..58d57078e 100644 --- a/SysML2.NET.Extensions/PSM/AutoGenEnumProvider/PrimitiveConstraintRequestOperatorProvider.cs +++ b/SysML2.NET.Extensions/PSM/AutoGenEnumProvider/PrimitiveConstraintRequestOperatorProvider.cs @@ -46,6 +46,53 @@ public static PrimitiveConstraintRequestOperator Parse(ReadOnlySpan value) throw new ArgumentException($"'{new string(value)}' is not a valid PrimitiveConstraintRequestOperator", nameof(value)); } + /// + /// Parses the UTF-8 encoded to a + /// + /// The that is to be parsed. + /// A enumeration literal. + /// Thrown when the value denotes no literal. + public static PrimitiveConstraintRequestOperator Parse(ReadOnlySpan value) + { + if (value.Length <= LongestValueByteLength) + { + Span characters = stackalloc char[LongestValueByteLength]; + + var characterCount = Encoding.UTF8.GetChars(value, characters); + + if (TryParse(characters[..characterCount], out var result)) + { + return result; + } + } + + throw new ArgumentException($"'{Encoding.UTF8.GetString(value)}' is not a valid PrimitiveConstraintRequestOperator", nameof(value)); + } + + /// + /// Parses the UTF-8 encoded to a + /// + /// The that is to be parsed. + /// A enumeration literal. + /// Thrown when the value denotes no literal. + public static PrimitiveConstraintRequestOperator Parse(in ReadOnlySequence value) + { + if (value.IsSingleSegment) + { + return Parse(value.FirstSpan); + } + + if (value.Length > LongestValueByteLength) + { + throw new ArgumentException($"a PrimitiveConstraintRequestOperator is at most {LongestValueByteLength} bytes long", nameof(value)); + } + + Span buffer = stackalloc byte[LongestValueByteLength]; + value.CopyTo(buffer); + + return Parse(buffer[..(int)value.Length]); + } + /// /// Tries to parse the to a /// @@ -100,53 +147,6 @@ public static bool TryParse(ReadOnlySpan value, out PrimitiveConstraintReq return false; } - /// - /// Parses the UTF-8 encoded to a - /// - /// The that is to be parsed. - /// A enumeration literal. - /// Thrown when the value denotes no literal. - public static PrimitiveConstraintRequestOperator Parse(ReadOnlySpan value) - { - if (value.Length <= LongestValueByteLength) - { - Span characters = stackalloc char[LongestValueByteLength]; - - var characterCount = Encoding.UTF8.GetChars(value, characters); - - if (TryParse(characters[..characterCount], out var result)) - { - return result; - } - } - - throw new ArgumentException($"'{Encoding.UTF8.GetString(value)}' is not a valid PrimitiveConstraintRequestOperator", nameof(value)); - } - - /// - /// Parses the UTF-8 encoded to a - /// - /// The that is to be parsed. - /// A enumeration literal. - /// Thrown when the value denotes no literal. - public static PrimitiveConstraintRequestOperator Parse(in ReadOnlySequence value) - { - if (value.IsSingleSegment) - { - return Parse(value.FirstSpan); - } - - if (value.Length > LongestValueByteLength) - { - throw new ArgumentException($"a PrimitiveConstraintRequestOperator is at most {LongestValueByteLength} bytes long", nameof(value)); - } - - Span buffer = stackalloc byte[LongestValueByteLength]; - value.CopyTo(buffer); - - return Parse(buffer[..(int)value.Length]); - } - /// /// Converts a to its UTF-8 encoded representation on the wire /// diff --git a/SysML2.NET.Serializer.Json.Tests/DeSerializerTestFixture.cs b/SysML2.NET.Serializer.Json.Tests/DeSerializerTestFixture.cs index 463da6e79..803458338 100644 --- a/SysML2.NET.Serializer.Json.Tests/DeSerializerTestFixture.cs +++ b/SysML2.NET.Serializer.Json.Tests/DeSerializerTestFixture.cs @@ -25,6 +25,7 @@ namespace SysML2.NET.Serializer.Json.Tests using SysML2.NET.Common; using SysML2.NET.Core.Core.Types; using SysML2.NET.Core.DTO.Core.Features; + using SysML2.NET.Core.DTO.Root.Elements; using SysML2.NET.PIM.DTO; using SysML2.NET.Serializer.Json; @@ -351,6 +352,23 @@ public async Task VerifyDeSerializeAsync() Assert.That(grown, Has.Count.EqualTo(LargePayloadElementCount), "payload larger than the initial buffer"); Assert.That(grown[^1].Id, Is.EqualTo(LargePayloadIdentifier(LargePayloadElementCount - 1)), "the last element survives the buffer growth"); } + + await using (var stream = CreateStream($"[{Element}]", false, 1)) + { + await Assert.ThatAsync(() => this.deSerializer.DeSerializeAsync(stream, SerializationModeKind.JSON, SerializationTargetKind.CORE, false, CancellationToken.None), + Throws.TypeOf()); + } + + await using (var stream = CreateStream($"[{Element}]", false, 1)) + { + var features = (await this.deSerializer.DeSerializeAsync(stream, SerializationModeKind.JSON, SerializationTargetKind.CORE, false, CancellationToken.None)).ToList(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(features, Has.Count.EqualTo(1)); + Assert.That(features[0].Id, Is.EqualTo(ElementIdentifier)); + } + } } /// diff --git a/SysML2.NET.Serializer.Json/DeSerializer.cs b/SysML2.NET.Serializer.Json/DeSerializer.cs index a1ef0a2b1..12dfa8cc1 100644 --- a/SysML2.NET.Serializer.Json/DeSerializer.cs +++ b/SysML2.NET.Serializer.Json/DeSerializer.cs @@ -14,6 +14,7 @@ namespace SysML2.NET.Serializer.Json using System.Collections.Generic; using System.Diagnostics; using System.IO; + using System.Linq; using System.Runtime.Serialization; using System.Text.Json; using System.Threading; @@ -155,6 +156,24 @@ public async Task> DeSerializeAsync(Stream stream, Seri } } + /// + /// Asynchronously deserializes the JSON stream to an + /// + /// The expected type of every deserialized item. + /// the JSON input stream + /// The to use + /// The to use + /// Asserts that the deserializer should deserialize derived properties if present or if they are ignored + /// The used to cancel the operation + /// an + /// Thrown when the payload is not valid JSON, declares an unknown @type, or holds an item that is not a + public async Task> DeSerializeAsync(Stream stream, SerializationModeKind serializationModeKind, SerializationTargetKind serializationTargetKind, bool deserializeDerivedProperties, CancellationToken cancellationToken) where T : IIdentified + { + var data = await this.DeSerializeAsync(stream, serializationModeKind, serializationTargetKind, deserializeDerivedProperties, cancellationToken); + + return data.Select(item => item is T typedItem ? typedItem : throw new JsonException($"The payload holds a {item.GetType().Name}, which is not a {typeof(T).Name}.")).ToList(); + } + /// /// Deserializes the JSON stream to a single request /// diff --git a/SysML2.NET.Serializer.Json/IDeSerializer.cs b/SysML2.NET.Serializer.Json/IDeSerializer.cs index cd3d39abe..dcfa3552f 100644 --- a/SysML2.NET.Serializer.Json/IDeSerializer.cs +++ b/SysML2.NET.Serializer.Json/IDeSerializer.cs @@ -66,6 +66,19 @@ public interface IDeSerializer /// Task> DeSerializeAsync(Stream stream, SerializationModeKind serializationModeKind, SerializationTargetKind serializationTargetKind, bool deserializeDerivedProperties, CancellationToken cancellationToken); + /// + /// Asynchronously deserializes the JSON stream to an + /// + /// The expected type of every deserialized item. + /// the JSON input stream + /// The to use + /// The to use + /// Asserts that the deserializer should deserialize derived properties if present or if they are ignored + /// The used to cancel the operation + /// an + /// Thrown when the payload is not valid JSON, declares an unknown @type, or holds an item that is not a + Task> DeSerializeAsync(Stream stream, SerializationModeKind serializationModeKind, SerializationTargetKind serializationTargetKind, bool deserializeDerivedProperties, CancellationToken cancellationToken) where T : IIdentified; + /// /// Deserializes the JSON stream to a single request /// diff --git a/SysML2.NET.Tests/PSM/PageTestFixture.cs b/SysML2.NET.Tests/PSM/PageTestFixture.cs new file mode 100644 index 000000000..5f6c5804e --- /dev/null +++ b/SysML2.NET.Tests/PSM/PageTestFixture.cs @@ -0,0 +1,100 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Tests.PSM +{ + using System; + using System.Text; + + using NUnit.Framework; + + using SysML2.NET.PSM; + + [TestFixture] + public class PageTestFixture + { + private const string SpecificationToken = "MTYxODg2MTQ5NjYzMnwyMDEwOWY0MC00ODI1LTQxNmEtODZmNi03NTA4YWM0MmEwMjE"; + + private static readonly Guid SpecificationIdentifier = Guid.Parse("20109f40-4825-416a-86f6-7508ac42a021"); + + private static string Encode(string text) + { + return Convert.ToBase64String(Encoding.UTF8.GetBytes(text)); + } + + [Test] + public void VerifyConstructor() + { + using (Assert.EnterMultipleScope()) + { + Assert.That(() => new Page(null), Throws.TypeOf()); + Assert.That(() => new Page(string.Empty), Throws.TypeOf()); + Assert.That(() => new Page(" "), Throws.TypeOf()); + Assert.That(new Page("not a token!").Token, Is.EqualTo("not a token!")); + Assert.That(new Page(SpecificationToken).Token, Is.EqualTo(SpecificationToken)); + } + + Assert.That(default(Page).Token, Is.Null); + } + + [Test] + public void VerifyFromCursor() + { + using (Assert.EnterMultipleScope()) + { + Assert.That(Page.FromCursor(1618861496632, SpecificationIdentifier).Token, Is.EqualTo(SpecificationToken)); + Assert.That(Page.FromCursor(0, Guid.Empty).Token, Is.EqualTo(Encode($"0|{Guid.Empty}").TrimEnd('='))); + Assert.That(Page.FromCursor(1618861496632, Guid.Empty), Is.Not.EqualTo(Page.FromCursor(1618861496632, SpecificationIdentifier))); + } + } + + [Test] + public void VerifyToString() + { + using (Assert.EnterMultipleScope()) + { + Assert.That(new Page("not a token!").ToString(), Is.EqualTo("not a token!")); + Assert.That(Page.FromCursor(1618861496632, SpecificationIdentifier).ToString(), Is.EqualTo(SpecificationToken)); + Assert.That(default(Page).ToString(), Is.Null); + } + } + + [Test] + public void VerifyTryDecode() + { + using (Assert.EnterMultipleScope()) + { + Assert.That(default(Page).TryDecode(out _, out _), Is.False); + Assert.That(new Page("not a token!").TryDecode(out _, out _), Is.False); + Assert.That(new Page(Encode($"1618861496632{SpecificationIdentifier}")).TryDecode(out _, out _), Is.False); + Assert.That(new Page(Encode($"epoch|{SpecificationIdentifier}")).TryDecode(out _, out _), Is.False); + Assert.That(new Page(Encode($"-1|{SpecificationIdentifier}")).TryDecode(out _, out _), Is.False); + Assert.That(new Page(Encode($"1618861496632|{SpecificationIdentifier}?").Replace('/', '_').Replace('+', '-')).TryDecode(out _, out _), Is.False); + Assert.That(new Page(Encode("1618861496632|not-a-guid")).TryDecode(out var rejectedEpoch, out var rejectedIdentifier), Is.False); + Assert.That(rejectedEpoch, Is.Zero); + Assert.That(rejectedIdentifier, Is.EqualTo(Guid.Empty)); + } + + Assert.That(new Page(SpecificationToken).TryDecode(out var epoch, out var identifier), Is.True); + + using (Assert.EnterMultipleScope()) + { + Assert.That(epoch, Is.EqualTo(1618861496632)); + Assert.That(identifier, Is.EqualTo(SpecificationIdentifier)); + Assert.That(new Page(Encode($"1618861496632|{SpecificationIdentifier}")).TryDecode(out var paddedEpoch, out var paddedIdentifier), Is.True); + Assert.That(paddedEpoch, Is.EqualTo(epoch)); + Assert.That(paddedIdentifier, Is.EqualTo(identifier)); + } + + var roundTripIdentifier = Guid.Parse("08f433db-b6d4-41b8-9020-e820ebc417be"); + + Assert.That(Page.FromCursor(1618862165315, roundTripIdentifier).TryDecode(out var roundTripEpoch, out var decodedIdentifier) && roundTripEpoch == 1618862165315 && decodedIdentifier == roundTripIdentifier, Is.True); + } + } +} diff --git a/SysML2.NET.Tests/PSM/PagedResultTestFixture.cs b/SysML2.NET.Tests/PSM/PagedResultTestFixture.cs index 45705cd39..6d36c4274 100644 --- a/SysML2.NET.Tests/PSM/PagedResultTestFixture.cs +++ b/SysML2.NET.Tests/PSM/PagedResultTestFixture.cs @@ -43,13 +43,14 @@ public void VerifyPagedResult() Assert.That(pagedResult.Records[^1], Is.EqualTo("the last record")); } - pagedResult.Previous = new Page { Epoch = 1700000000, Identifier = FirstIdentifier }; - pagedResult.Next = new Page { Epoch = 1700000600, Identifier = LastIdentifier }; + pagedResult.Previous = Page.FromCursor(1618861496632, FirstIdentifier); + pagedResult.Next = new Page("the next page token"); using (Assert.EnterMultipleScope()) { - Assert.That(pagedResult.Previous.Value.ToString(), Is.EqualTo($"1700000000|{FirstIdentifier}")); - Assert.That(pagedResult.Next.Value.ToString(), Is.EqualTo($"1700000600|{LastIdentifier}")); + Assert.That(pagedResult.Previous, Is.EqualTo(Page.FromCursor(1618861496632, FirstIdentifier))); + Assert.That(pagedResult.Previous, Is.Not.EqualTo(Page.FromCursor(1618861496632, LastIdentifier))); + Assert.That(pagedResult.Next.Value.Token, Is.EqualTo("the next page token")); } } } diff --git a/SysML2.NET/PSM/AutoGenEnum/Direction.cs b/SysML2.NET/PSM/AutoGenEnum/Direction.cs new file mode 100644 index 000000000..3d4f67eb1 --- /dev/null +++ b/SysML2.NET/PSM/AutoGenEnum/Direction.cs @@ -0,0 +1,44 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.PSM.Enumerations +{ + using System.CodeDom.Compiler; + + /// + /// Enumeration of the values allowed by the Direction property of the Systems Modeling API and Services + /// + [GeneratedCode("SysML2.NET", "latest")] + public enum Direction + { + /// + /// The "in" value + /// + @in, + + /// + /// The "out" value + /// + @out, + + /// + /// The "both" value + /// + both + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SysML2.NET/PSM/Page.cs b/SysML2.NET/PSM/Page.cs index 5ae2b7d96..16e9f5c3e 100644 --- a/SysML2.NET/PSM/Page.cs +++ b/SysML2.NET/PSM/Page.cs @@ -10,31 +10,111 @@ namespace SysML2.NET.PSM { using System; + using System.Globalization; + using System.Text; /// - /// Defines the structure of the page for the pagination process + /// Defines the opaque page token of the cursor-based pagination (Systems Modeling API and Services, Clause 8.1.3) /// - public struct Page + public readonly struct Page { /// - /// the epoch time + /// Initializes a new instance of the struct /// - public int Epoch { get; set; } + /// + /// The page token, kept exactly as the service returned it + /// + /// + /// Thrown when is null, empty or white space + /// + public Page(string token) + { + if (string.IsNullOrWhiteSpace(token)) + { + throw new ArgumentException("The page token is required", nameof(token)); + } + + this.Token = token; + } /// - /// The page identifier + /// The page token, sent back to the service unchanged /// - public Guid Identifier { get; set; } + public string Token { get; } /// - /// Returns a string that represents the + /// Creates a whose token is the unpadded Base64 encoding of an epoch and an identifier separated by a vertical bar /// + /// + /// The time, in milliseconds since the Unix epoch, that positions the page + /// + /// + /// The identifier that positions the page + /// /// - /// a string representation + /// The in the token format shown by the examples of Clause 8.1.3 + /// + public static Page FromCursor(long epoch, Guid identifier) + { + var text = $"{epoch.ToString(CultureInfo.InvariantCulture)}|{identifier}"; + + return new Page(Convert.ToBase64String(Encoding.UTF8.GetBytes(text)).TrimEnd('=')); + } + + /// + /// Returns the page token + /// + /// + /// The /// public override string ToString() { - return $"{this.Epoch}|{this.Identifier}"; + return this.Token; + } + + /// + /// Decodes the token as the unpadded Base64 encoding of an epoch and an identifier separated by a vertical bar + /// + /// + /// The epoch, in milliseconds since the Unix epoch, when the decoding succeeds, or 0 otherwise + /// + /// + /// The identifier when the decoding succeeds, or otherwise + /// + /// + /// true when the token has the format shown by the examples of Clause 8.1.3, false otherwise + /// + public bool TryDecode(out long epoch, out Guid identifier) + { + epoch = 0; + identifier = Guid.Empty; + + if (string.IsNullOrWhiteSpace(this.Token)) + { + return false; + } + + var base64 = this.Token.Replace('-', '+').Replace('_', '/'); + base64 = base64.PadRight(base64.Length + (4 - base64.Length % 4) % 4, '='); + + var bytes = new byte[base64.Length * 3 / 4]; + + if (!Convert.TryFromBase64String(base64, bytes, out var byteCount)) + { + return false; + } + + var parts = Encoding.UTF8.GetString(bytes, 0, byteCount).Split('|'); + + if (parts.Length != 2 || !long.TryParse(parts[0], NumberStyles.None, CultureInfo.InvariantCulture, out var decodedEpoch) || !Guid.TryParse(parts[1], out var decodedIdentifier)) + { + return false; + } + + epoch = decodedEpoch; + identifier = decodedIdentifier; + + return true; } } } diff --git a/SysML2.NET/PSM/QueryParameters.cs b/SysML2.NET/PSM/QueryParameters.cs index 5a3344bd6..bcc7528a9 100644 --- a/SysML2.NET/PSM/QueryParameters.cs +++ b/SysML2.NET/PSM/QueryParameters.cs @@ -27,7 +27,7 @@ public class QueryParameters /// specifies the URL of the page succeeding the page being requested /// /// - /// format: epoch|GUID, where epoch is Unix epoch: DateTime(1970, 1, 1, 0, 0, 0, 0) + /// the opaque page token returned by the service in the Link header (Systems Modeling API and Services, Clause 8.1.3) /// public Page? PageAfter { get; set; } = null; @@ -35,7 +35,7 @@ public class QueryParameters /// specifies the URL of a page preceding the page being requested /// /// - /// format: epoch|GUID, where epoch is Unix epoch: DateTime(1970, 1, 1, 0, 0, 0, 0) + /// the opaque page token returned by the service in the Link header (Systems Modeling API and Services, Clause 8.1.3) /// public Page? PageBefore { get; set; } = null;