From dfcb3745d7b03d7aa24efd30fb6ec95fad18560b Mon Sep 17 00:00:00 2001 From: Zeynep Delal Mutlu Date: Wed, 4 Nov 2026 09:58:37 +0100 Subject: [PATCH 1/6] initial commit --- .github/workflows/nuget-reference-check.yml | 2 +- Directory.Packages.props | 3 + .../MergeConflictExceptionTestFixture.cs | 46 + .../PsmApiClientTestFixture.cs | 397 ++ .../PsmApiExceptionTestFixture.cs | 48 + .../QueryStringBuilderTestFixture.cs | 73 + .../RestClientTestFixture.cs | 101 - SySML2.NET.REST.Tests/SessionTestFixture.cs | 117 + .../SySML2.NET.REST.Tests.csproj | 80 +- SySML2.NET.REST/AuthenticationStatusKind.cs | 37 - .../AutoGenPsmApiClient/BranchApi.cs | 160 + .../AutoGenPsmApiClient/CommitApi.cs | 200 + .../AutoGenPsmApiClient/DiffMergeApi.cs | 126 + .../AutoGenPsmApiClient/ElementApi.cs | 183 + .../AutoGenPsmApiClient/IPsmApiClient.cs | 805 +++ .../AutoGenPsmApiClient/MetaApi.cs | 93 + .../AutoGenPsmApiClient/ProjectApi.cs | 179 + .../AutoGenPsmApiClient/QueryApi.cs | 307 ++ .../AutoGenPsmApiClient/RelationshipApi.cs | 83 + SySML2.NET.REST/AutoGenPsmApiClient/TagApi.cs | 160 + SySML2.NET.REST/IRestClient.cs | 166 - SySML2.NET.REST/ISession.cs | 86 +- SySML2.NET.REST/MergeConflictException.cs | 39 + SySML2.NET.REST/PsmApiClient.cs | 502 ++ SySML2.NET.REST/PsmApiException.cs | 48 + SySML2.NET.REST/QueryStringBuilder.cs | 119 + SySML2.NET.REST/RestClient.cs | 410 -- SySML2.NET.REST/Session.cs | 211 +- SySML2.NET.REST/SySML2.NET.REST.csproj | 6 + .../REST/AutoGenPsmApiClient/BranchApi.cs | 160 + .../REST/AutoGenPsmApiClient/CommitApi.cs | 200 + .../REST/AutoGenPsmApiClient/DiffMergeApi.cs | 126 + .../REST/AutoGenPsmApiClient/ElementApi.cs | 183 + .../REST/AutoGenPsmApiClient/IPsmApiClient.cs | 805 +++ .../REST/AutoGenPsmApiClient/MetaApi.cs | 93 + .../REST/AutoGenPsmApiClient/ProjectApi.cs | 179 + .../REST/AutoGenPsmApiClient/QueryApi.cs | 307 ++ .../AutoGenPsmApiClient/RelationshipApi.cs | 83 + .../REST/AutoGenPsmApiClient/TagApi.cs | 160 + .../OpenApiApiClientGeneratorTestFixture.cs | 110 + .../ApiClientOperationModel.cs | 34 + .../ApiClientTagModel.cs | 34 + .../OpenApiApiClientGenerator.cs | 251 + .../OpenApiHandleBarHelpers/TextHelper.cs | 43 + .../Resources/SysML.ecore | 4528 ----------------- .../SysML2.NET.CodeGenerator.csproj | 15 + ...piclient-documentation-openapi-partial.hbs | 47 + ...sm-apiclient-signature-openapi-partial.hbs | 34 + ...psm-apiclient-typename-openapi-partial.hbs | 1 + ...m-apiclient-interface-openapi-template.hbs | 49 + .../psm-apiclient-openapi-template.hbs | 92 + 51 files changed, 6927 insertions(+), 5394 deletions(-) create mode 100644 SySML2.NET.REST.Tests/MergeConflictExceptionTestFixture.cs create mode 100644 SySML2.NET.REST.Tests/PsmApiClientTestFixture.cs create mode 100644 SySML2.NET.REST.Tests/PsmApiExceptionTestFixture.cs create mode 100644 SySML2.NET.REST.Tests/QueryStringBuilderTestFixture.cs delete mode 100644 SySML2.NET.REST.Tests/RestClientTestFixture.cs create mode 100644 SySML2.NET.REST.Tests/SessionTestFixture.cs delete mode 100644 SySML2.NET.REST/AuthenticationStatusKind.cs create mode 100644 SySML2.NET.REST/AutoGenPsmApiClient/BranchApi.cs create mode 100644 SySML2.NET.REST/AutoGenPsmApiClient/CommitApi.cs create mode 100644 SySML2.NET.REST/AutoGenPsmApiClient/DiffMergeApi.cs create mode 100644 SySML2.NET.REST/AutoGenPsmApiClient/ElementApi.cs create mode 100644 SySML2.NET.REST/AutoGenPsmApiClient/IPsmApiClient.cs create mode 100644 SySML2.NET.REST/AutoGenPsmApiClient/MetaApi.cs create mode 100644 SySML2.NET.REST/AutoGenPsmApiClient/ProjectApi.cs create mode 100644 SySML2.NET.REST/AutoGenPsmApiClient/QueryApi.cs create mode 100644 SySML2.NET.REST/AutoGenPsmApiClient/RelationshipApi.cs create mode 100644 SySML2.NET.REST/AutoGenPsmApiClient/TagApi.cs delete mode 100644 SySML2.NET.REST/IRestClient.cs create mode 100644 SySML2.NET.REST/MergeConflictException.cs create mode 100644 SySML2.NET.REST/PsmApiClient.cs create mode 100644 SySML2.NET.REST/PsmApiException.cs create mode 100644 SySML2.NET.REST/QueryStringBuilder.cs delete mode 100644 SySML2.NET.REST/RestClient.cs create mode 100644 SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/BranchApi.cs create mode 100644 SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/CommitApi.cs create mode 100644 SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/DiffMergeApi.cs create mode 100644 SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/ElementApi.cs create mode 100644 SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/IPsmApiClient.cs create mode 100644 SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/MetaApi.cs create mode 100644 SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/ProjectApi.cs create mode 100644 SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/QueryApi.cs create mode 100644 SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/RelationshipApi.cs create mode 100644 SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/TagApi.cs create mode 100644 SysML2.NET.CodeGenerator.Tests/Generators/OpenApiHandleBarsGenerators/OpenApiApiClientGeneratorTestFixture.cs create mode 100644 SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/ApiClientOperationModel.cs create mode 100644 SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/ApiClientTagModel.cs create mode 100644 SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/OpenApiApiClientGenerator.cs create mode 100644 SysML2.NET.CodeGenerator/OpenApiHandleBarHelpers/TextHelper.cs delete mode 100644 SysML2.NET.CodeGenerator/Resources/SysML.ecore create mode 100644 SysML2.NET.CodeGenerator/Templates/OpenAPI/Partials/psm-apiclient-documentation-openapi-partial.hbs create mode 100644 SysML2.NET.CodeGenerator/Templates/OpenAPI/Partials/psm-apiclient-signature-openapi-partial.hbs create mode 100644 SysML2.NET.CodeGenerator/Templates/OpenAPI/Partials/psm-apiclient-typename-openapi-partial.hbs create mode 100644 SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-interface-openapi-template.hbs create mode 100644 SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-openapi-template.hbs 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/MergeConflictExceptionTestFixture.cs b/SySML2.NET.REST.Tests/MergeConflictExceptionTestFixture.cs new file mode 100644 index 000000000..e459c9aed --- /dev/null +++ b/SySML2.NET.REST.Tests/MergeConflictExceptionTestFixture.cs @@ -0,0 +1,46 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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 NUnit.Framework; + + using SysML2.NET.PSM.DTO; + + [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()); + } + } + } +} diff --git a/SySML2.NET.REST.Tests/PsmApiClientTestFixture.cs b/SySML2.NET.REST.Tests/PsmApiClientTestFixture.cs new file mode 100644 index 000000000..766fe8fce --- /dev/null +++ b/SySML2.NET.REST.Tests/PsmApiClientTestFixture.cs @@ -0,0 +1,397 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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.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 = new Page { Epoch = 1700000000, Identifier = OtherCommitId }; + var nextPage = new Page { Epoch = 1700000600, Identifier = CommitId }; + + this.mockHttp.Expect(HttpMethod.Get, branchesUri) + .WithQueryString("page[size]", "2") + .WithQueryString("page[after]", previousPage.ToString()) + .Respond(HttpStatusCode.OK, + CreateLinkHeader($"<{branchesUri}?page[after]={Uri.EscapeDataString(nextPage.ToString())}>; 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).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, "[]"); + + await Assert.ThatAsync(() => this.client.GetBranchesByProjectAsync(ProjectId), Throws.TypeOf()); + + 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) + .WithQueryString("branchId", BranchId.ToString()) + .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 => request.Content is null && string.IsNullOrWhiteSpace(request.RequestUri.Query)) + .Respond(HttpStatusCode.BadRequest, JsonMediaType, ErrorPayload); + + await Assert.ThatAsync(() => this.client.PostCommitByProjectAsync(ProjectId, null), + Throws.TypeOf().With.Property(nameof(PsmApiException.StatusCode)).EqualTo(HttpStatusCode.BadRequest)); + + this.mockHttp.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task VerifyGetRootsByProjectCommitAsync() + { + var rootsUri = $"{BaseAddress}projects/{ProjectId}/commits/{CommitId}/roots"; + + this.mockHttp.Expect(HttpMethod.Get, rootsUri) + .WithQueryString("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 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) + .WithQueryString("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) + .WithQueryString("commitId", CommitId.ToString()) + .WithQueryString("page[size]", "2") + .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).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) + .WithQueryString("sourceCommitId", $"{CommitId},{OtherCommitId}") + .WithQueryString("description", "the merge description") + .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)); + + this.mockHttp.Expect(HttpMethod.Post, mergeUri) + .With(request => request.Content is null && string.IsNullOrWhiteSpace(request.RequestUri.Query)) + .Respond(HttpStatusCode.Created, JsonMediaType, ReadPayload("commit.json")); + + mergeCommit = await this.client.MergeAsync(ProjectId, [], BranchId, null); + + 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.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 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()); + + this.mockHttp.VerifyNoOutstandingExpectation(); + } + } +} diff --git a/SySML2.NET.REST.Tests/PsmApiExceptionTestFixture.cs b/SySML2.NET.REST.Tests/PsmApiExceptionTestFixture.cs new file mode 100644 index 000000000..f32d5baee --- /dev/null +++ b/SySML2.NET.REST.Tests/PsmApiExceptionTestFixture.cs @@ -0,0 +1,48 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST.Tests +{ + using System.Net; + + using NUnit.Framework; + + using SysML2.NET.PSM.DTO; + + [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)")); + } + } + } +} diff --git a/SySML2.NET.REST.Tests/QueryStringBuilderTestFixture.cs b/SySML2.NET.REST.Tests/QueryStringBuilderTestFixture.cs new file mode 100644 index 000000000..244a6f02f --- /dev/null +++ b/SySML2.NET.REST.Tests/QueryStringBuilderTestFixture.cs @@ -0,0 +1,73 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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]; + + [Test] + public void VerifyConstructor() + { + Assert.That(new QueryStringBuilder().ToString(), Is.Empty); + + Assert.That(new QueryStringBuilder(new QueryParameters()).ToString(), Is.Empty); + + var page = new Page { Epoch = 1700000000, Identifier = 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); + + 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); + + Assert.That(builder.ToString(), Is.EqualTo($"?excludeUsed=false&sourceCommitId={FirstCommitId}%2C{SecondCommitId}&description=the%20merge%2C%20%26%20its%20description&pageSize=1.5")); + } + + [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..51d9e1309 --- /dev/null +++ b/SySML2.NET.REST.Tests/SessionTestFixture.cs @@ -0,0 +1,117 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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.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..53f39ac27 --- /dev/null +++ b/SySML2.NET.REST/AutoGenPsmApiClient/BranchApi.cs @@ -0,0 +1,160 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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; + + /// + /// 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task> GetBranchesByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + + using var response = await this.SendAsync(new 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 the server responds with an error status code + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task PostBranchByProjectAsync(Guid projectId, BranchRequest requestBody, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task GetBranchesByProjectAndIdAsync(Guid projectId, Guid branchId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task DeleteBranchByProjectAndIdAsync(Guid projectId, Guid branchId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(new 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..c78fa5e43 --- /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.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; + + /// + /// 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task> GetCommitsByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + + using var response = await this.SendAsync(new 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 the server responds with an error status code + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task PostCommitByProjectAsync(Guid projectId, CommitRequest requestBody, Guid? branchId = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + query.Add("branchId", branchId); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task GetCommitByProjectAndIdAsync(Guid projectId, Guid commitId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + 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(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task GetChangeByProjectCommitIdAsync(Guid projectId, Guid commitId, Guid changeId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(new 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..7227a23ee --- /dev/null +++ b/SySML2.NET.REST/AutoGenPsmApiClient/DiffMergeApi.cs @@ -0,0 +1,126 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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; + + /// + /// 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + 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(new 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 the server responds with an error status code + /// + /// + /// Thrown when the server reports conflicting data + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task MergeAsync(Guid projectId, IEnumerable sourceCommitId, Guid targetBranchId, IEnumerable requestBody, string description = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + 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(new 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..551c29ce2 --- /dev/null +++ b/SySML2.NET.REST/AutoGenPsmApiClient/ElementApi.cs @@ -0,0 +1,183 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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; + + /// + /// 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + 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(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + 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(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task GetProjectUsageByProjectCommitElementAsync(Guid projectId, Guid commitId, Guid elementId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + 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(new 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..eb72e3540 --- /dev/null +++ b/SySML2.NET.REST/AutoGenPsmApiClient/IPsmApiClient.cs @@ -0,0 +1,805 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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; + + /// + /// 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, 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, 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 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 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 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, string 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 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, 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 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 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 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 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 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..cea2ea1a3 --- /dev/null +++ b/SySML2.NET.REST/AutoGenPsmApiClient/MetaApi.cs @@ -0,0 +1,93 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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; + + /// + /// 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task GetDatatypesAsync(QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + + using var response = await this.SendAsync(new HttpMethod("GET"), $"meta/datatypes{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadJsonAsync(response, cancellationToken); + } + + /// + /// Get datatype by ID + /// + /// + /// ID of the datatype + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getDatatypeById operation + /// + /// + /// Thrown when the server responds with an error status code + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task GetDatatypeByIdAsync(string datatypeId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(new HttpMethod("GET"), $"meta/datatypes/{Escape(datatypeId)}{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.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..19532c4b2 --- /dev/null +++ b/SySML2.NET.REST/AutoGenPsmApiClient/ProjectApi.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.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; + + /// + /// Provides the Project operations of the Systems Modeling API and Services + /// + 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task> GetProjectsAsync(QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task PostProjectAsync(ProjectRequest requestBody, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task GetProjectByIdAsync(Guid projectId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task PutProjectByIdAsync(Guid projectId, ProjectRequest requestBody, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task DeleteProjectByIdAsync(Guid projectId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(new 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..4841c6326 --- /dev/null +++ b/SySML2.NET.REST/AutoGenPsmApiClient/QueryApi.cs @@ -0,0 +1,307 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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; + + /// + /// 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task> GetQueriesByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + + using var response = await this.SendAsync(new 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 the server responds with an error status code + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task PostQueryByProjectAsync(Guid projectId, QueryRequest requestBody, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task GetQueryByProjectAndIdAsync(Guid projectId, Guid queryId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(new 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 the server responds with an error status code + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task PutQueryByProjectAndIdAsync(Guid projectId, Guid queryId, QueryRequest requestBody, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task DeleteQueryByProjectAndIdAsync(Guid projectId, Guid queryId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + 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(new 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 the server responds with an error status code + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task> GetQueryResultsByProjectIdQueryAsync(Guid projectId, QueryRequest requestBody, Guid? commitId = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + query.Add("commitId", commitId); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(new 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 the server responds with an error status code + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task> GetQueryResultsByProjectIdQueryPostAsync(Guid projectId, QueryRequest requestBody, Guid? commitId = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + query.Add("commitId", commitId); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(new 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..1138990d3 --- /dev/null +++ b/SySML2.NET.REST/AutoGenPsmApiClient/RelationshipApi.cs @@ -0,0 +1,83 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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; + + /// + /// 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task> GetRelationshipsByProjectCommitRelatedElementAsync(Guid projectId, Guid commitId, Guid relatedElementId, string direction = null, bool? excludeUsed = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + query.Add("direction", direction); + query.Add("excludeUsed", excludeUsed); + + using var response = await this.SendAsync(new 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..afec4dc7a --- /dev/null +++ b/SySML2.NET.REST/AutoGenPsmApiClient/TagApi.cs @@ -0,0 +1,160 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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; + + /// + /// 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task> GetTagsByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + + using var response = await this.SendAsync(new 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 the server responds with an error status code + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task PostTagByProjectAsync(Guid projectId, TagRequest requestBody, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task GetTagByProjectAndIdAsync(Guid projectId, Guid tagId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task DeleteTagByProjectAndIdAsync(Guid projectId, Guid tagId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(new 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/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/MergeConflictException.cs b/SySML2.NET.REST/MergeConflictException.cs new file mode 100644 index 000000000..bdad2595e --- /dev/null +++ b/SySML2.NET.REST/MergeConflictException.cs @@ -0,0 +1,39 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + 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 + /// + /// + /// 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/PsmApiClient.cs b/SySML2.NET.REST/PsmApiClient.cs new file mode 100644 index 000000000..2cdc06565 --- /dev/null +++ b/SySML2.NET.REST/PsmApiClient.cs @@ -0,0 +1,502 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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.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.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"; + + /// + /// Matches one link-value of an RFC 8288 Link header, capturing its target and its relation type + /// + private static readonly Regex LinkValuePattern = new("<(?[^>]*)>[^,]*?;\\s*rel=\"?(?[^\";,]+)\"?", RegexOptions.Compiled); + + /// + /// 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); + } + + /// + /// 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 , or null when the header has no such link or the link carries no page token + /// + /// + /// Thrown when the page token of the link is not an epoch and an identifier separated by a vertical bar + /// + 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 token is null ? null : ParsePage(token); + } + + /// + /// Parses a page token formatted as writes it + /// + /// + /// The page token + /// + /// + /// The + /// + /// + /// Thrown when the token is not an epoch and an identifier separated by a vertical bar + /// + private static Page ParsePage(string token) + { + var parts = token.Split('|'); + + if (parts.Length != 2 || !int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var epoch) || !Guid.TryParse(parts[1], out var identifier)) + { + throw new FormatException($"The page token '{token}' is not an epoch and an identifier separated by a vertical bar"); + } + + return new Page + { + Epoch = epoch, Identifier = identifier + }; + } + + /// + /// 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, new JsonWriterOptions(), 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, new JsonWriterOptions(), cancellationToken); + + return CreateContent(stream); + } + + /// + /// Wraps a serialized payload in an of the JSON media type + /// + /// + /// The that holds the serialized payload + /// + /// + /// The + /// + private static HttpContent CreateContent(MemoryStream stream) + { + stream.Position = 0; + + var content = new StreamContent(stream); + content.Headers.ContentType = new MediaTypeHeaderValue(JsonMediaType); + + return content; + } + + /// + /// 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) + { + using var stream = await response.Content.ReadAsStreamAsync(); + + try + { + error = 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", response.StatusCode); + } + } + + throw new PsmApiException(response.StatusCode, error); + } + + /// + /// 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 + /// + private async Task ThrowOnConflictAsync(HttpResponseMessage response, CancellationToken cancellationToken) + { + if (response.StatusCode != HttpStatusCode.Conflict || response.Content?.Headers.ContentType?.MediaType != JsonMediaType) + { + return; + } + + using var stream = await response.Content.ReadAsStreamAsync(); + + var conflicts = await this.deSerializer.DeSerializeResponsesAsync(stream, SerializationModeKind.JSON, false, 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; + } + + using var stream = await response.Content.ReadAsStreamAsync(); + + 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 + { + using var stream = await response.Content.ReadAsStreamAsync(); + + 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 + { + using var stream = await response.Content.ReadAsStreamAsync(); + + var data = await this.deSerializer.DeSerializeAsync(stream, SerializationModeKind.JSON, SerializationTargetKind.PSM, false, cancellationToken); + + return data.Cast().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 response.Content.ReadAsStreamAsync(); + + var data = await this.deSerializer.DeSerializeAsync(stream, SerializationModeKind.JSON, SerializationTargetKind.PSM, false, cancellationToken); + + return CreatePagedResult(response, data.Cast().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 async Task ReadJsonAsync(HttpResponseMessage response, CancellationToken cancellationToken) + { + await using var stream = await response.Content.ReadAsStreamAsync(); + + return await JsonDocument.ParseAsync(stream, default, cancellationToken); + } + + /// + /// 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); + + this.logger.LogDebug("{Method} {RequestUri} responded {StatusCode}", method, requestUri, response.StatusCode); + + return response; + } + } +} diff --git a/SySML2.NET.REST/PsmApiException.cs b/SySML2.NET.REST/PsmApiException.cs new file mode 100644 index 000000000..c2604c298 --- /dev/null +++ b/SySML2.NET.REST/PsmApiException.cs @@ -0,0 +1,48 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SySML2.NET.REST +{ + 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 + /// + /// + /// 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/QueryStringBuilder.cs b/SySML2.NET.REST/QueryStringBuilder.cs new file mode 100644 index 000000000..619e57e48 --- /dev/null +++ b/SySML2.NET.REST/QueryStringBuilder.cs @@ -0,0 +1,119 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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 + /// + /// + /// 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 => stringValue, + bool booleanValue => booleanValue ? "true" : "false", + IEnumerable sequence => FormatSequence(sequence), + _ => FormatValue(value) + }; + + if (text is not null) + { + this.parameters.Add($"{name}={Uri.EscapeDataString(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 a comma separated list of values formatted using the invariant culture + /// + /// + /// 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 + /// + /// + /// The value to format + /// + /// + /// The formatted value + /// + private static string FormatValue(object value) + { + return Convert.ToString(value, CultureInfo.InvariantCulture); + } + } +} 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..3b6b371b3 100644 --- a/SySML2.NET.REST/Session.cs +++ b/SySML2.NET.REST/Session.cs @@ -1,78 +1,133 @@ -// ------------------------------------------------------------------------------------------------ -// -// -// 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 (baseAddress is null) + { + throw new ArgumentNullException(nameof(baseAddress)); + } + + 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("/", StringComparison.Ordinal) ? 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..1c86c0b76 100644 --- a/SySML2.NET.REST/SySML2.NET.REST.csproj +++ b/SySML2.NET.REST/SySML2.NET.REST.csproj @@ -15,6 +15,7 @@ true [Update] Aligned with SysML2.NET 0.25.2 + [Update] Code-generated PsmApiClient covering every operation of the Systems Modeling API and Services (breaking: replaces IRestClient/RestClient; Session no longer takes credentials) true true @@ -22,7 +23,12 @@ + + + + + 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..53f39ac27 --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/BranchApi.cs @@ -0,0 +1,160 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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; + + /// + /// 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task> GetBranchesByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + + using var response = await this.SendAsync(new 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 the server responds with an error status code + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task PostBranchByProjectAsync(Guid projectId, BranchRequest requestBody, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task GetBranchesByProjectAndIdAsync(Guid projectId, Guid branchId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task DeleteBranchByProjectAndIdAsync(Guid projectId, Guid branchId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(new 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..c78fa5e43 --- /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.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; + + /// + /// 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task> GetCommitsByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + + using var response = await this.SendAsync(new 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 the server responds with an error status code + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task PostCommitByProjectAsync(Guid projectId, CommitRequest requestBody, Guid? branchId = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + query.Add("branchId", branchId); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task GetCommitByProjectAndIdAsync(Guid projectId, Guid commitId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + 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(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task GetChangeByProjectCommitIdAsync(Guid projectId, Guid commitId, Guid changeId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(new 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..7227a23ee --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/DiffMergeApi.cs @@ -0,0 +1,126 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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; + + /// + /// 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + 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(new 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 the server responds with an error status code + /// + /// + /// Thrown when the server reports conflicting data + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task MergeAsync(Guid projectId, IEnumerable sourceCommitId, Guid targetBranchId, IEnumerable requestBody, string description = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + 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(new 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..551c29ce2 --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/ElementApi.cs @@ -0,0 +1,183 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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; + + /// + /// 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + 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(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + 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(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task GetProjectUsageByProjectCommitElementAsync(Guid projectId, Guid commitId, Guid elementId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + 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(new 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..eb72e3540 --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/IPsmApiClient.cs @@ -0,0 +1,805 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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; + + /// + /// 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, 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, 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 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 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 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, string 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 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, 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 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 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 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 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 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..cea2ea1a3 --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/MetaApi.cs @@ -0,0 +1,93 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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; + + /// + /// 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task GetDatatypesAsync(QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + + using var response = await this.SendAsync(new HttpMethod("GET"), $"meta/datatypes{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.ReadJsonAsync(response, cancellationToken); + } + + /// + /// Get datatype by ID + /// + /// + /// ID of the datatype + /// + /// + /// The that cancels the request + /// + /// + /// The deserialized response of the getDatatypeById operation + /// + /// + /// Thrown when the server responds with an error status code + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task GetDatatypeByIdAsync(string datatypeId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(new HttpMethod("GET"), $"meta/datatypes/{Escape(datatypeId)}{query}", null, cancellationToken); + + await this.EnsureSuccessAsync(response, cancellationToken); + + return await this.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..19532c4b2 --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/ProjectApi.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.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; + + /// + /// Provides the Project operations of the Systems Modeling API and Services + /// + 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task> GetProjectsAsync(QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task PostProjectAsync(ProjectRequest requestBody, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task GetProjectByIdAsync(Guid projectId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task PutProjectByIdAsync(Guid projectId, ProjectRequest requestBody, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task DeleteProjectByIdAsync(Guid projectId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(new 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..4841c6326 --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/QueryApi.cs @@ -0,0 +1,307 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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; + + /// + /// 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task> GetQueriesByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + + using var response = await this.SendAsync(new 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 the server responds with an error status code + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task PostQueryByProjectAsync(Guid projectId, QueryRequest requestBody, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task GetQueryByProjectAndIdAsync(Guid projectId, Guid queryId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(new 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 the server responds with an error status code + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task PutQueryByProjectAndIdAsync(Guid projectId, Guid queryId, QueryRequest requestBody, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task DeleteQueryByProjectAndIdAsync(Guid projectId, Guid queryId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + 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(new 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 the server responds with an error status code + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task> GetQueryResultsByProjectIdQueryAsync(Guid projectId, QueryRequest requestBody, Guid? commitId = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + query.Add("commitId", commitId); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(new 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 the server responds with an error status code + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task> GetQueryResultsByProjectIdQueryPostAsync(Guid projectId, QueryRequest requestBody, Guid? commitId = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + query.Add("commitId", commitId); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(new 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..1138990d3 --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/RelationshipApi.cs @@ -0,0 +1,83 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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; + + /// + /// 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task> GetRelationshipsByProjectCommitRelatedElementAsync(Guid projectId, Guid commitId, Guid relatedElementId, string direction = null, bool? excludeUsed = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + query.Add("direction", direction); + query.Add("excludeUsed", excludeUsed); + + using var response = await this.SendAsync(new 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..afec4dc7a --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/TagApi.cs @@ -0,0 +1,160 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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; + + /// + /// 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task> GetTagsByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(queryParameters); + + using var response = await this.SendAsync(new 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 the server responds with an error status code + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task PostTagByProjectAsync(Guid projectId, TagRequest requestBody, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task GetTagByProjectAndIdAsync(Guid projectId, Guid tagId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(new 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 + /// + [GeneratedCode("SysML2.NET", "latest")] + public async Task DeleteTagByProjectAndIdAsync(Guid projectId, Guid tagId, CancellationToken cancellationToken = default) + { + var query = new QueryStringBuilder(); + + using var response = await this.SendAsync(new 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..338f988fa --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Generators/OpenApiHandleBarsGenerators/OpenApiApiClientGeneratorTestFixture.cs @@ -0,0 +1,110 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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.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 tagModels = OpenApiApiClientGenerator.QueryTagModels(this.openApiDocument); + + using (Assert.EnterMultipleScope()) + { + Assert.That(tagModels, Has.Count.EqualTo(9)); + Assert.That(tagModels.Select(tagModel => tagModel.Name), Is.EqualTo(["Project", "Branch", "Tag", "Commit", "Element", "Relationship", "DiffMerge", "Query", "Meta"])); + Assert.That(tagModels.SelectMany(tagModel => tagModel.Operations).ToList(), Has.Count.EqualTo(35)); + Assert.That(tagModels[6].Tag, Is.EqualTo("Diff & Merge")); + Assert.That(tagModels[6].Operations.Select(operation => operation.HttpMethod), Is.EqualTo(["GET", "POST"])); + } + } + + [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/Generators/OpenApiHandleBarsGenerators/ApiClientOperationModel.cs b/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/ApiClientOperationModel.cs new file mode 100644 index 000000000..a5c55e62b --- /dev/null +++ b/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/ApiClientOperationModel.cs @@ -0,0 +1,34 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.CodeGenerator.Generators.OpenApiHandleBarsGenerators +{ + 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 + { + /// + /// 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; } + } +} diff --git a/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/ApiClientTagModel.cs b/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/ApiClientTagModel.cs new file mode 100644 index 000000000..48eefdb9c --- /dev/null +++ b/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/ApiClientTagModel.cs @@ -0,0 +1,34 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.CodeGenerator.Generators.OpenApiHandleBarsGenerators +{ + using System.Collections.Generic; + + /// + /// 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 the operations of the tag, in document order + /// + public required IReadOnlyList Operations { get; init; } + } +} diff --git a/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/OpenApiApiClientGenerator.cs b/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/OpenApiApiClientGenerator.cs new file mode 100644 index 000000000..33b812b78 --- /dev/null +++ b/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/OpenApiApiClientGenerator.cs @@ -0,0 +1,251 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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 + /// + 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 = operation.Value + }))) + .ToLookup(entry => entry.Tag, entry => entry.Model, StringComparer.Ordinal); + + return openApiDocument.Tags + .Where(tag => operationsByTag.Contains(tag.Name)) + .Select(tag => new ApiClientTagModel + { + Tag = tag.Name, + Name = QueryTagName(tag.Name), + Operations = operationsByTag[tag.Name].ToList() + }) + .ToList(); + } + + /// + /// Register the custom helpers + /// + protected override void RegisterHelpers() + { + this.Handlebars.RegisterTextHelper(); + } + + /// + /// 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/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..9b2f4ba1e --- /dev/null +++ b/SysML2.NET.CodeGenerator/Templates/OpenAPI/Partials/psm-apiclient-documentation-openapi-partial.hbs @@ -0,0 +1,47 @@ +/// +/// {{Operation.Summary}} +/// +{{#each Operation.Parameters}} +{{#if Required}} +/// +/// {{Description}} +/// +{{/if}} +{{/each}} +{{#if Operation.RequestBody}} +/// +/// The payload sent as the body of the request +/// +{{/if}} +{{#each Operation.Parameters}} +{{#unless Required}} +{{#unless (String.StartsWith Name 'page')}} +/// +/// {{Description}} +/// +{{/unless}} +{{/unless}} +{{/each}} +{{#each Operation.Parameters}} +{{#if (String.Equal Name 'page[size]')}} +/// +/// The page of the result to request, or null to let the server choose +/// +{{/if}} +{{/each}} +/// +/// The that cancels the request +/// +/// +/// The deserialized response of the {{Operation.OperationId}} operation +/// +/// +/// Thrown when the server responds with an error status code +/// +{{#each Operation.Responses}} +{{#if (String.Equal @key '409')}} +/// +/// Thrown when the server reports conflicting data +/// +{{/if}} +{{/each}} 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..037f59d4f --- /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 (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}}JsonDocument{{/if~}} +{{~/if~}} +{{~/each~}} +{{~/if~}} +{{~/each~}} +> {{String.Pascalcase Operation.OperationId}}Async( +{{~#each Operation.Parameters~}} +{{~#if Required}}{{> parametertype}} {{Name}}, {{/if~}} +{{~/each~}} +{{~#each Operation.RequestBody.Content~}} +{{~#if Schema.Reference}}{{Schema.Reference.Id}} requestBody, {{/if~}} +{{~#each Schema.OneOf~}}{{~#if Items}}IEnumerable<{{> psm-apiclient-typename-openapi-partial Items}}> requestBody, {{/if~}}{{~/each~}} +{{~/each~}} +{{~#each Operation.Parameters~}} +{{~#unless Required}}{{#unless (String.StartsWith Name 'page')}}{{> parametertype}} {{Name}} = null, {{/unless}}{{/unless~}} +{{~/each~}} +{{~#each Operation.Parameters~}} +{{~#if (String.Equal Name 'page[size]')}}QueryParameters queryParameters = null, {{/if~}} +{{~/each~}} +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..66c9d567b --- /dev/null +++ b/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-interface-openapi-template.hbs @@ -0,0 +1,49 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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; + + /// + /// 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..e3245f4ef --- /dev/null +++ b/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-openapi-template.hbs @@ -0,0 +1,92 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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; + 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; + + /// + /// Provides the {{Tag}} operations of the Systems Modeling API and Services + /// + public partial class PsmApiClient + { + {{#each Operations}} + {{> psm-apiclient-documentation-openapi-partial}} + [GeneratedCode("SysML2.NET", "latest")] + public async {{> psm-apiclient-signature-openapi-partial}} + { + var query = new QueryStringBuilder({{#each Operation.Parameters}}{{#if (String.Equal Name 'page[size]')}}queryParameters{{/if}}{{/each}}); + {{#each Operation.Parameters}} + {{#if (String.Equal (OpenApi.Text In) 'Query')}} + {{#unless (String.StartsWith Name 'page')}} + query.Add("{{Name}}", {{Name}}); + {{/unless}} + {{/if}} + {{/each}} + + {{#if Operation.RequestBody}} + using var content = await this.CreateContentAsync(requestBody, cancellationToken); + using var response = await this.SendAsync(new HttpMethod("{{HttpMethod}}"), $"{{{String.Replace (String.Replace (String.Substring Path 1) '{' '{Escape(') '}' ')}'}}}{query}", content, cancellationToken); + {{else}} + using var response = await this.SendAsync(new HttpMethod("{{HttpMethod}}"), $"{{{String.Replace (String.Replace (String.Substring Path 1) '{' '{Escape(') '}' ')}'}}}{query}", null, cancellationToken); + {{/if}} + {{#each Operation.Responses}} + {{#if (String.Equal @key '409')}} + + await this.ThrowOnConflictAsync(response, cancellationToken); + {{/if}} + {{/each}} + + 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}} + return await this.ReadJsonAsync(response, cancellationToken); + {{/if}} + {{/if}} + {{/each}} + {{/if}} + {{/each}} + } + {{#unless @last}} + + {{/unless}} + {{/each}} + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ From f6331ae2e8467b2452f8c7ace7a357f8e9cc22ce Mon Sep 17 00:00:00 2001 From: Zeynep Delal Mutlu Date: Wed, 4 Nov 2026 11:33:02 +0100 Subject: [PATCH 2/6] Fix 404 - sonarqube --- Directory.Packages.props | 93 +++++++++---------- .../AutoGenPsmApiClient/MetaApi.cs | 4 +- SySML2.NET.REST/PsmApiClient.cs | 83 ++++++++++++++--- SySML2.NET.REST/Session.cs | 6 +- .../REST/AutoGenPsmApiClient/MetaApi.cs | 4 +- .../psm-apiclient-openapi-template.hbs | 2 +- 6 files changed, 122 insertions(+), 70 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 4e6ffa01f..45d800838 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,13 +1,10 @@ - - - - true - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/MetaApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/MetaApi.cs index cea2ea1a3..097384920 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/MetaApi.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/MetaApi.cs @@ -56,7 +56,7 @@ public async Task GetDatatypesAsync(QueryParameters queryParameter await this.EnsureSuccessAsync(response, cancellationToken); - return await this.ReadJsonAsync(response, cancellationToken); + return await ReadJsonAsync(response, cancellationToken); } /// @@ -83,7 +83,7 @@ public async Task GetDatatypeByIdAsync(string datatypeId, Cancella await this.EnsureSuccessAsync(response, cancellationToken); - return await this.ReadJsonAsync(response, cancellationToken); + return await ReadJsonAsync(response, cancellationToken); } } } diff --git a/SySML2.NET.REST/PsmApiClient.cs b/SySML2.NET.REST/PsmApiClient.cs index 2cdc06565..64c3565f2 100644 --- a/SySML2.NET.REST/PsmApiClient.cs +++ b/SySML2.NET.REST/PsmApiClient.cs @@ -42,9 +42,38 @@ public partial class PsmApiClient : IPsmApiClient private const string JsonMediaType = "application/json"; /// - /// Matches one link-value of an RFC 8288 Link header, capturing its target and its relation type + /// The pattern of one link-value of an RFC 8288 Link header, capturing its target and its relation type /// - private static readonly Regex LinkValuePattern = new("<(?[^>]*)>[^,]*?;\\s*rel=\"?(?[^\";,]+)\"?", RegexOptions.Compiled); + 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 whose addresses the server @@ -129,7 +158,7 @@ private static string Escape(object value) } var target = headerValues - .SelectMany(headerValue => LinkValuePattern.Matches(headerValue).Cast()) + .SelectMany(headerValue => LinkValuePattern().Matches(headerValue).Cast()) .Where(match => relationTypes.Contains(match.Groups["relation"].Value, StringComparer.OrdinalIgnoreCase)) .Select(match => match.Groups["target"].Value) .FirstOrDefault(); @@ -240,9 +269,9 @@ private async Task CreateContentAsync(IEnumerable requestBod /// The that holds the serialized payload /// /// - /// The + /// The /// - private static HttpContent CreateContent(MemoryStream stream) + private static StreamContent CreateContent(MemoryStream stream) { stream.Position = 0; @@ -252,6 +281,29 @@ private static HttpContent CreateContent(MemoryStream stream) 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 /// @@ -278,7 +330,7 @@ private async Task EnsureSuccessAsync(HttpResponseMessage response, Cancellation if (response.Content?.Headers.ContentType?.MediaType == JsonMediaType) { - using var stream = await response.Content.ReadAsStreamAsync(); + using var stream = await ReadContentStreamAsync(response.Content, cancellationToken); try { @@ -315,7 +367,7 @@ private async Task ThrowOnConflictAsync(HttpResponseMessage response, Cancellati return; } - using var stream = await response.Content.ReadAsStreamAsync(); + using var stream = await ReadContentStreamAsync(response.Content, cancellationToken); var conflicts = await this.deSerializer.DeSerializeResponsesAsync(stream, SerializationModeKind.JSON, false, cancellationToken); @@ -344,7 +396,7 @@ private async Task ReadResponseAsync(HttpResponseMessage response, Cancell return default; } - using var stream = await response.Content.ReadAsStreamAsync(); + using var stream = await ReadContentStreamAsync(response.Content, cancellationToken); return await this.deSerializer.DeSerializeResponseAsync(stream, SerializationModeKind.JSON, false, cancellationToken); } @@ -366,7 +418,7 @@ private async Task ReadResponseAsync(HttpResponseMessage response, Cancell /// private async Task> ReadPagedResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken) where T : IResponse { - using var stream = await response.Content.ReadAsStreamAsync(); + using var stream = await ReadContentStreamAsync(response.Content, cancellationToken); var records = await this.deSerializer.DeSerializeResponsesAsync(stream, SerializationModeKind.JSON, false, cancellationToken); @@ -393,7 +445,7 @@ private async Task> ReadPagedResponseAsync(HttpResponseMessage /// private async Task ReadDataAsync(HttpResponseMessage response, CancellationToken cancellationToken) where T : IData { - using var stream = await response.Content.ReadAsStreamAsync(); + using var stream = await ReadContentStreamAsync(response.Content, cancellationToken); var data = await this.deSerializer.DeSerializeAsync(stream, SerializationModeKind.JSON, SerializationTargetKind.PSM, false, cancellationToken); @@ -417,7 +469,7 @@ private async Task ReadDataAsync(HttpResponseMessage response, Cancellatio /// private async Task> ReadPagedDataAsync(HttpResponseMessage response, CancellationToken cancellationToken) where T : IData { - await using var stream = await response.Content.ReadAsStreamAsync(); + await using var stream = await ReadContentStreamAsync(response.Content, cancellationToken); var data = await this.deSerializer.DeSerializeAsync(stream, SerializationModeKind.JSON, SerializationTargetKind.PSM, false, cancellationToken); @@ -436,9 +488,9 @@ private async Task> ReadPagedDataAsync(HttpResponseMessage res /// /// The parsed , which the caller disposes /// - private async Task ReadJsonAsync(HttpResponseMessage response, CancellationToken cancellationToken) + private static async Task ReadJsonAsync(HttpResponseMessage response, CancellationToken cancellationToken) { - await using var stream = await response.Content.ReadAsStreamAsync(); + await using var stream = await ReadContentStreamAsync(response.Content, cancellationToken); return await JsonDocument.ParseAsync(stream, default, cancellationToken); } @@ -494,7 +546,10 @@ private async Task SendAsync(HttpMethod method, string requ var response = await this.httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken); - this.logger.LogDebug("{Method} {RequestUri} responded {StatusCode}", method, requestUri, response.StatusCode); + 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/Session.cs b/SySML2.NET.REST/Session.cs index 3b6b371b3..0b44bc269 100644 --- a/SySML2.NET.REST/Session.cs +++ b/SySML2.NET.REST/Session.cs @@ -96,10 +96,14 @@ public Session(IHttpClientFactory httpClientFactory, IDeSerializer deSerializer, /// 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) { @@ -111,7 +115,7 @@ public async Task> Open(Uri baseAddress, CancellationToken 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("/", StringComparison.Ordinal) ? baseAddress : new Uri($"{baseAddress.AbsoluteUri}/"); + httpClient.BaseAddress = baseAddress.AbsoluteUri.EndsWith('/') ? baseAddress : new Uri($"{baseAddress.AbsoluteUri}/"); var client = new PsmApiClient(httpClient, this.deSerializer, this.serializer, this.loggerFactory); diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/MetaApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/MetaApi.cs index cea2ea1a3..097384920 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/MetaApi.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/MetaApi.cs @@ -56,7 +56,7 @@ public async Task GetDatatypesAsync(QueryParameters queryParameter await this.EnsureSuccessAsync(response, cancellationToken); - return await this.ReadJsonAsync(response, cancellationToken); + return await ReadJsonAsync(response, cancellationToken); } /// @@ -83,7 +83,7 @@ public async Task GetDatatypeByIdAsync(string datatypeId, Cancella await this.EnsureSuccessAsync(response, cancellationToken); - return await this.ReadJsonAsync(response, cancellationToken); + return await ReadJsonAsync(response, cancellationToken); } } } diff --git a/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-openapi-template.hbs b/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-openapi-template.hbs index e3245f4ef..8382dea8b 100644 --- a/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-openapi-template.hbs +++ b/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-openapi-template.hbs @@ -73,7 +73,7 @@ namespace SySML2.NET.REST {{else if Schema.Reference}} return await this.Read{{> family Schema}}Async<{{> psm-apiclient-typename-openapi-partial Schema}}>(response, cancellationToken); {{else}} - return await this.ReadJsonAsync(response, cancellationToken); + return await ReadJsonAsync(response, cancellationToken); {{/if}} {{/if}} {{/each}} From d0bcf63557734ed4b6b6e19b81dcea0e32c7ac1c Mon Sep 17 00:00:00 2001 From: Zeynep Delal Mutlu Date: Wed, 4 Nov 2026 16:57:03 +0100 Subject: [PATCH 3/6] PR review check --- Directory.Packages.props | 93 ++++---- .../MergeConflictExceptionTestFixture.cs | 27 +++ .../PsmApiClientTestFixture.cs | 124 +++++++++-- .../PsmApiExceptionTestFixture.cs | 24 ++ .../QueryStringBuilderTestFixture.cs | 2 +- .../AutoGenPsmApiClient/BranchApi.cs | 13 +- .../AutoGenPsmApiClient/CommitApi.cs | 16 +- .../AutoGenPsmApiClient/DiffMergeApi.cs | 17 +- .../AutoGenPsmApiClient/ElementApi.cs | 13 +- .../AutoGenPsmApiClient/IPsmApiClient.cs | 17 +- .../AutoGenPsmApiClient/MetaApi.cs | 11 +- .../AutoGenPsmApiClient/ProjectApi.cs | 20 +- .../AutoGenPsmApiClient/QueryApi.cs | 25 +-- .../AutoGenPsmApiClient/RelationshipApi.cs | 10 +- SySML2.NET.REST/AutoGenPsmApiClient/TagApi.cs | 13 +- SySML2.NET.REST/MergeConflictException.cs | 32 +++ SySML2.NET.REST/PsmApiClient.cs | 206 +++++++++++++----- SySML2.NET.REST/PsmApiException.cs | 47 ++++ SySML2.NET.REST/SySML2.NET.REST.csproj | 1 - .../OpenAPI/PSM/AutoGenEnum/Direction.cs | 44 ++++ .../CompositeConstraintOperatorProvider.cs | 48 ++-- ...positeConstraintRequestOperatorProvider.cs | 48 ++-- .../AutoGenEnumProvider/DirectionProvider.cs | 147 +++++++++++++ .../PrimitiveConstraintOperatorProvider.cs | 94 ++++---- ...mitiveConstraintRequestOperatorProvider.cs | 94 ++++---- .../REST/AutoGenPsmApiClient/BranchApi.cs | 13 +- .../REST/AutoGenPsmApiClient/CommitApi.cs | 16 +- .../REST/AutoGenPsmApiClient/DiffMergeApi.cs | 17 +- .../REST/AutoGenPsmApiClient/ElementApi.cs | 13 +- .../REST/AutoGenPsmApiClient/IPsmApiClient.cs | 17 +- .../REST/AutoGenPsmApiClient/MetaApi.cs | 11 +- .../REST/AutoGenPsmApiClient/ProjectApi.cs | 20 +- .../REST/AutoGenPsmApiClient/QueryApi.cs | 25 +-- .../AutoGenPsmApiClient/RelationshipApi.cs | 10 +- .../REST/AutoGenPsmApiClient/TagApi.cs | 13 +- .../OpenApiApiClientGeneratorTestFixture.cs | 72 +++++- .../OpenApiEnumGeneratorTestFixture.cs | 61 +++++- ...OpenApiEnumProviderGeneratorTestFixture.cs | 14 +- .../HttpMethodHelperTestFixture.cs | 51 +++++ .../ApiClientOperationModel.cs | 25 +++ .../ApiClientTagModel.cs | 11 + .../OpenApiApiClientGenerator.cs | 44 +++- .../OpenApiEnumGenerator.cs | 34 ++- .../HttpMethodHelper.cs | 55 +++++ ...piclient-documentation-openapi-partial.hbs | 26 ++- ...sm-apiclient-signature-openapi-partial.hbs | 22 +- ...m-apiclient-interface-openapi-template.hbs | 1 + .../psm-apiclient-openapi-template.hbs | 40 +++- .../psm-enumprovider-openapi-template.hbs | 40 ++-- .../CompositeConstraintOperatorProvider.cs | 48 ++-- ...positeConstraintRequestOperatorProvider.cs | 48 ++-- .../AutoGenEnumProvider/DirectionProvider.cs | 147 +++++++++++++ .../PrimitiveConstraintOperatorProvider.cs | 94 ++++---- ...mitiveConstraintRequestOperatorProvider.cs | 94 ++++---- .../DeSerializerTestFixture.cs | 18 ++ SysML2.NET.Serializer.Json/DeSerializer.cs | 19 ++ SysML2.NET.Serializer.Json/IDeSerializer.cs | 13 ++ SysML2.NET.Tests/PSM/PageTestFixture.cs | 70 ++++++ .../PSM/PagedResultTestFixture.cs | 10 +- SysML2.NET/PSM/AutoGenEnum/Direction.cs | 44 ++++ SysML2.NET/PSM/Page.cs | 61 +++++- SysML2.NET/PSM/QueryParameters.cs | 4 +- 62 files changed, 1843 insertions(+), 664 deletions(-) create mode 100644 SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/PSM/AutoGenEnum/Direction.cs create mode 100644 SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/PSM/AutoGenEnumProvider/DirectionProvider.cs create mode 100644 SysML2.NET.CodeGenerator.Tests/OpenApiHandleBarHelpers/HttpMethodHelperTestFixture.cs create mode 100644 SysML2.NET.CodeGenerator/OpenApiHandleBarHelpers/HttpMethodHelper.cs create mode 100644 SysML2.NET.Extensions/PSM/AutoGenEnumProvider/DirectionProvider.cs create mode 100644 SysML2.NET.Tests/PSM/PageTestFixture.cs create mode 100644 SysML2.NET/PSM/AutoGenEnum/Direction.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 45d800838..4e6ffa01f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,10 +1,13 @@ + - - true - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SySML2.NET.REST.Tests/MergeConflictExceptionTestFixture.cs b/SySML2.NET.REST.Tests/MergeConflictExceptionTestFixture.cs index e459c9aed..5d0e81381 100644 --- a/SySML2.NET.REST.Tests/MergeConflictExceptionTestFixture.cs +++ b/SySML2.NET.REST.Tests/MergeConflictExceptionTestFixture.cs @@ -41,6 +41,33 @@ public void VerifyConstructor() 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/PsmApiClientTestFixture.cs b/SySML2.NET.REST.Tests/PsmApiClientTestFixture.cs index 766fe8fce..6285acc2b 100644 --- a/SySML2.NET.REST.Tests/PsmApiClientTestFixture.cs +++ b/SySML2.NET.REST.Tests/PsmApiClientTestFixture.cs @@ -24,6 +24,7 @@ namespace SySML2.NET.REST.Tests using SysML2.NET.Core.DTO.Root.Namespaces; using SysML2.NET.PSM; using SysML2.NET.PSM.DTO; + using SysML2.NET.PSM.Enumerations; using SysML2.NET.Serializer.Json; [TestFixture] @@ -94,14 +95,13 @@ public void VerifyConstructor() public async Task VerifyGetBranchesByProjectAsync() { var branchesUri = $"{BaseAddress}projects/{ProjectId}/branches"; - var previousPage = new Page { Epoch = 1700000000, Identifier = OtherCommitId }; - var nextPage = new Page { Epoch = 1700000600, Identifier = CommitId }; + var previousPage = new Page { Epoch = 1618861496632, Identifier = OtherCommitId }; + var nextPage = new Page { Epoch = 1618861496632, Identifier = Guid.Parse("20109f40-4825-416a-86f6-7508ac42a021") }; this.mockHttp.Expect(HttpMethod.Get, branchesUri) - .WithQueryString("page[size]", "2") - .WithQueryString("page[after]", previousPage.ToString()) + .WithExactQueryString($"page[size]=2&page[after]={Uri.EscapeDataString(previousPage.ToString())}") .Respond(HttpStatusCode.OK, - CreateLinkHeader($"<{branchesUri}?page[after]={Uri.EscapeDataString(nextPage.ToString())}>; rel=\"next\", <{branchesUri}?page[before]={Uri.EscapeDataString(previousPage.ToString())}>; rel=\"prev\""), + 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 }); @@ -115,7 +115,7 @@ public async Task VerifyGetBranchesByProjectAsync() Assert.That(branches.Previous, Is.EqualTo(previousPage)); } - this.mockHttp.Expect(HttpMethod.Get, branchesUri).Respond(JsonMediaType, "[]"); + this.mockHttp.Expect(HttpMethod.Get, branchesUri).WithExactQueryString(string.Empty).Respond(JsonMediaType, "[]"); branches = await this.client.GetBranchesByProjectAsync(ProjectId); @@ -129,7 +129,9 @@ public async Task VerifyGetBranchesByProjectAsync() this.mockHttp.Expect(HttpMethod.Get, branchesUri) .Respond(HttpStatusCode.OK, CreateLinkHeader($"<{branchesUri}?page[after]=not-a-page>; rel=\"next\""), JsonMediaType, "[]"); - await Assert.ThatAsync(() => this.client.GetBranchesByProjectAsync(ProjectId), Throws.TypeOf()); + branches = await this.client.GetBranchesByProjectAsync(ProjectId); + + Assert.That(branches.Next, Is.Null); this.mockHttp.Expect(HttpMethod.Get, branchesUri).Respond(HttpStatusCode.NotFound, JsonMediaType, ErrorPayload); @@ -188,7 +190,7 @@ public async Task VerifyPostCommitByProjectAsync() var commitRequest = new CommitRequest { Description = "the commit description" }; this.mockHttp.Expect(HttpMethod.Post, commitsUri) - .WithQueryString("branchId", BranchId.ToString()) + .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")); @@ -213,7 +215,7 @@ public async Task VerifyGetRootsByProjectCommitAsync() var rootsUri = $"{BaseAddress}projects/{ProjectId}/commits/{CommitId}/roots"; this.mockHttp.Expect(HttpMethod.Get, rootsUri) - .WithQueryString("excludeUsed", "true") + .WithExactQueryString("excludeUsed=true") .Respond(JsonMediaType, ReadPayload("roots.json")); var roots = await this.client.GetRootsByProjectCommitAsync(ProjectId, CommitId, true); @@ -232,6 +234,32 @@ await Assert.ThatAsync(() => this.client.GetRootsByProjectCommitAsync(ProjectId, 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() { @@ -241,7 +269,7 @@ public async Task VerifyGetElementByProjectCommitIdAsync() using (var rootsDocument = JsonDocument.Parse(roots)) { this.mockHttp.Expect(HttpMethod.Get, elementUri) - .WithQueryString("excludeUsed", "false") + .WithExactQueryString("excludeUsed=false") .Respond(JsonMediaType, rootsDocument.RootElement[0].GetRawText()); } @@ -272,8 +300,7 @@ public async Task VerifyGetQueryResultsByProjectIdQueryPostAsync() var queryRequest = new QueryRequest { Name = "the query name" }; this.mockHttp.Expect(HttpMethod.Post, queryResultsUri) - .WithQueryString("commitId", CommitId.ToString()) - .WithQueryString("page[size]", "2") + .WithExactQueryString($"page[size]=2&commitId={CommitId}") .With(request => request.Content.ReadAsStringAsync().GetAwaiter().GetResult().Contains("the query name")) .Respond(JsonMediaType, ReadPayload("roots.json")); @@ -314,6 +341,14 @@ public async Task VerifyPutProjectByIdAsync() 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), @@ -330,8 +365,7 @@ public async Task VerifyMergeAsync() var mergeUri = $"{BaseAddress}projects/{ProjectId}/branches/{BranchId}/merge"; this.mockHttp.Expect(HttpMethod.Post, mergeUri) - .WithQueryString("sourceCommitId", $"{CommitId},{OtherCommitId}") - .WithQueryString("description", "the merge description") + .WithExactQueryString($"sourceCommitId={CommitId}%2C{OtherCommitId}&description=the%20merge%20description") .With(request => request.Content.ReadAsStringAsync().GetAwaiter().GetResult().Contains(RootNamespaceId.ToString())) .Respond(HttpStatusCode.Created, JsonMediaType, ReadPayload("commit.json")); @@ -339,11 +373,16 @@ public async Task VerifyMergeAsync() 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) - .With(request => request.Content is null && string.IsNullOrWhiteSpace(request.RequestUri.Query)) + .WithExactQueryString($"sourceCommitId={CommitId}") + .With(request => request.Content is null) .Respond(HttpStatusCode.Created, JsonMediaType, ReadPayload("commit.json")); - mergeCommit = await this.client.MergeAsync(ProjectId, [], BranchId, null); + mergeCommit = await this.client.MergeAsync(ProjectId, [CommitId], BranchId); Assert.That(mergeCommit.Id, Is.EqualTo(CommitId)); @@ -360,6 +399,18 @@ await Assert.ThatAsync(() => this.client.MergeAsync(ProjectId, [CommitId], Branc .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), @@ -368,6 +419,47 @@ await Assert.ThatAsync(() => this.client.MergeAsync(ProjectId, [CommitId], Branc 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 = new Page { Epoch = 1618861496632, Identifier = 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() { diff --git a/SySML2.NET.REST.Tests/PsmApiExceptionTestFixture.cs b/SySML2.NET.REST.Tests/PsmApiExceptionTestFixture.cs index f32d5baee..f6b649475 100644 --- a/SySML2.NET.REST.Tests/PsmApiExceptionTestFixture.cs +++ b/SySML2.NET.REST.Tests/PsmApiExceptionTestFixture.cs @@ -9,6 +9,7 @@ namespace SySML2.NET.REST.Tests { + using System; using System.Net; using NUnit.Framework; @@ -43,6 +44,29 @@ public void VerifyConstructor() 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/QueryStringBuilderTestFixture.cs b/SySML2.NET.REST.Tests/QueryStringBuilderTestFixture.cs index 244a6f02f..6b0b2da29 100644 --- a/SySML2.NET.REST.Tests/QueryStringBuilderTestFixture.cs +++ b/SySML2.NET.REST.Tests/QueryStringBuilderTestFixture.cs @@ -33,7 +33,7 @@ public void VerifyConstructor() Assert.That(new QueryStringBuilder(new QueryParameters()).ToString(), Is.Empty); - var page = new Page { Epoch = 1700000000, Identifier = FirstCommitId }; + var page = new Page { Epoch = 1618861496632, Identifier = FirstCommitId }; var builder = new QueryStringBuilder(new QueryParameters { PageSize = 2, PageAfter = page, PageBefore = page }); diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/BranchApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/BranchApi.cs index 53f39ac27..3e907a686 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/BranchApi.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/BranchApi.cs @@ -15,7 +15,6 @@ namespace SySML2.NET.REST { using System; - using System.CodeDom.Compiler; using System.Collections.Generic; using System.Net.Http; using System.Text.Json; @@ -50,12 +49,11 @@ public partial class PsmApiClient /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task> GetBranchesByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(queryParameters); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects/{Escape(projectId)}/branches{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/branches{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -80,13 +78,12 @@ public async Task> GetBranchesByProjectAsync(Guid projectId, /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task PostBranchByProjectAsync(Guid projectId, BranchRequest requestBody, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); using var content = await this.CreateContentAsync(requestBody, cancellationToken); - using var response = await this.SendAsync(new HttpMethod("POST"), $"projects/{Escape(projectId)}/branches{query}", content, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects/{Escape(projectId)}/branches{query}", content, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -111,12 +108,11 @@ public async Task PostBranchByProjectAsync(Guid projectId, BranchRequest /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task GetBranchesByProjectAndIdAsync(Guid projectId, Guid branchId, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects/{Escape(projectId)}/branches/{Escape(branchId)}{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/branches/{Escape(branchId)}{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -141,12 +137,11 @@ public async Task GetBranchesByProjectAndIdAsync(Guid projectId, Guid br /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task DeleteBranchByProjectAndIdAsync(Guid projectId, Guid branchId, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); - using var response = await this.SendAsync(new HttpMethod("DELETE"), $"projects/{Escape(projectId)}/branches/{Escape(branchId)}{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Delete, $"projects/{Escape(projectId)}/branches/{Escape(branchId)}{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/CommitApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/CommitApi.cs index c78fa5e43..2fd63751f 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/CommitApi.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/CommitApi.cs @@ -15,7 +15,6 @@ namespace SySML2.NET.REST { using System; - using System.CodeDom.Compiler; using System.Collections.Generic; using System.Net.Http; using System.Text.Json; @@ -50,12 +49,11 @@ public partial class PsmApiClient /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task> GetCommitsByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(queryParameters); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects/{Escape(projectId)}/commits{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -83,14 +81,13 @@ public async Task> GetCommitsByProjectAsync(Guid projectId, /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task PostCommitByProjectAsync(Guid projectId, CommitRequest requestBody, Guid? branchId = null, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); query.Add("branchId", branchId); using var content = await this.CreateContentAsync(requestBody, cancellationToken); - using var response = await this.SendAsync(new HttpMethod("POST"), $"projects/{Escape(projectId)}/commits{query}", content, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects/{Escape(projectId)}/commits{query}", content, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -115,12 +112,11 @@ public async Task PostCommitByProjectAsync(Guid projectId, CommitRequest /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task GetCommitByProjectAndIdAsync(Guid projectId, Guid commitId, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects/{Escape(projectId)}/commits/{Escape(commitId)}{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(commitId)}{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -148,12 +144,11 @@ public async Task GetCommitByProjectAndIdAsync(Guid projectId, Guid comm /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] 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(new HttpMethod("GET"), $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/changes{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/changes{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -181,12 +176,11 @@ public async Task> GetChangesByProjectCommitAsync(Guid /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task GetChangeByProjectCommitIdAsync(Guid projectId, Guid commitId, Guid changeId, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/changes/{Escape(changeId)}{query}", null, cancellationToken); + 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); diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/DiffMergeApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/DiffMergeApi.cs index 7227a23ee..136f0b2fe 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/DiffMergeApi.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/DiffMergeApi.cs @@ -15,7 +15,6 @@ namespace SySML2.NET.REST { using System; - using System.CodeDom.Compiler; using System.Collections.Generic; using System.Net.Http; using System.Text.Json; @@ -56,13 +55,12 @@ public partial class PsmApiClient /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] 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(new HttpMethod("GET"), $"projects/{Escape(projectId)}/commits/{Escape(compareCommitId)}/diff{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(compareCommitId)}/diff{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -96,21 +94,28 @@ public async Task> DiffAsync(Guid projectId, Guid ba /// /// 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 /// - [GeneratedCode("SysML2.NET", "latest")] - public async Task MergeAsync(Guid projectId, IEnumerable sourceCommitId, Guid targetBranchId, IEnumerable requestBody, string description = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + 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(new HttpMethod("POST"), $"projects/{Escape(projectId)}/branches/{Escape(targetBranchId)}/merge{query}", content, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects/{Escape(projectId)}/branches/{Escape(targetBranchId)}/merge{query}", content, cancellationToken); await this.ThrowOnConflictAsync(response, cancellationToken); diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/ElementApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/ElementApi.cs index 551c29ce2..e69d57fdd 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/ElementApi.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/ElementApi.cs @@ -15,7 +15,6 @@ namespace SySML2.NET.REST { using System; - using System.CodeDom.Compiler; using System.Collections.Generic; using System.Net.Http; using System.Text.Json; @@ -56,13 +55,12 @@ public partial class PsmApiClient /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] 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(new HttpMethod("GET"), $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/elements{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/elements{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -93,13 +91,12 @@ public async Task> GetElementsByProjectCommitAsync(Guid pr /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] 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(new HttpMethod("GET"), $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/elements/{Escape(elementId)}{query}", null, cancellationToken); + 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); @@ -127,12 +124,11 @@ public async Task GetElementByProjectCommitIdAsync(Guid projectId, Gui /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task GetProjectUsageByProjectCommitElementAsync(Guid projectId, Guid commitId, Guid elementId, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/elements/{Escape(elementId)}/projectUsage{query}", null, cancellationToken); + 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); @@ -163,13 +159,12 @@ public async Task GetProjectUsageByProjectCommitElementAsync(Guid /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] 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(new HttpMethod("GET"), $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/roots{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/roots{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/IPsmApiClient.cs b/SySML2.NET.REST/AutoGenPsmApiClient/IPsmApiClient.cs index eb72e3540..7a98b42ff 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/IPsmApiClient.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/IPsmApiClient.cs @@ -25,6 +25,7 @@ namespace SySML2.NET.REST using SysML2.NET.Core.DTO.Root.Elements; using SysML2.NET.PSM; using SysML2.NET.PSM.DTO; + using SysML2.NET.PSM.Enumerations; /// /// The client of the REST/HTTP platform specific model of the Systems Modeling API and Services @@ -64,7 +65,7 @@ public partial interface IPsmApiClient /// /// Thrown when the server responds with an error status code /// - Task PostProjectAsync(ProjectRequest requestBody, CancellationToken cancellationToken = default); + Task PostProjectAsync(ProjectRequest requestBody = null, CancellationToken cancellationToken = default); /// /// Get project by ID @@ -101,7 +102,7 @@ public partial interface IPsmApiClient /// /// Thrown when the server responds with an error status code /// - Task PutProjectByIdAsync(Guid projectId, ProjectRequest requestBody, CancellationToken cancellationToken = default); + Task PutProjectByIdAsync(Guid projectId, ProjectRequest requestBody = null, CancellationToken cancellationToken = default); /// /// Delete project by ID @@ -520,7 +521,7 @@ public partial interface IPsmApiClient /// /// Thrown when the server responds with an error status code /// - Task> GetRelationshipsByProjectCommitRelatedElementAsync(Guid projectId, Guid commitId, Guid relatedElementId, string direction = null, bool? excludeUsed = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + 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 @@ -575,13 +576,19 @@ public partial interface IPsmApiClient /// /// 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, string description = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + Task MergeAsync(Guid projectId, IEnumerable sourceCommitId, Guid targetBranchId, IEnumerable requestBody = null, string description = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); /// /// Get queries by project @@ -779,7 +786,7 @@ public partial interface IPsmApiClient /// /// Thrown when the server responds with an error status code /// - Task GetDatatypesAsync(QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + Task> GetDatatypesAsync(QueryParameters queryParameters = null, CancellationToken cancellationToken = default); /// /// Get datatype by ID diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/MetaApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/MetaApi.cs index 097384920..53d5c139d 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/MetaApi.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/MetaApi.cs @@ -15,7 +15,6 @@ namespace SySML2.NET.REST { using System; - using System.CodeDom.Compiler; using System.Collections.Generic; using System.Net.Http; using System.Text.Json; @@ -47,16 +46,15 @@ public partial class PsmApiClient /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] - public async Task GetDatatypesAsync(QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + public async Task> GetDatatypesAsync(QueryParameters queryParameters = null, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(queryParameters); - using var response = await this.SendAsync(new HttpMethod("GET"), $"meta/datatypes{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"meta/datatypes{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); - return await ReadJsonAsync(response, cancellationToken); + return await this.ReadPagedJsonAsync(response, cancellationToken); } /// @@ -74,12 +72,11 @@ public async Task GetDatatypesAsync(QueryParameters queryParameter /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task GetDatatypeByIdAsync(string datatypeId, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); - using var response = await this.SendAsync(new HttpMethod("GET"), $"meta/datatypes/{Escape(datatypeId)}{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"meta/datatypes/{Escape(datatypeId)}{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/ProjectApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/ProjectApi.cs index 19532c4b2..066b008eb 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/ProjectApi.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/ProjectApi.cs @@ -30,6 +30,7 @@ namespace SySML2.NET.REST /// /// Provides the Project operations of the Systems Modeling API and Services /// + [GeneratedCode("SysML2.NET", "latest")] public partial class PsmApiClient { /// @@ -47,12 +48,11 @@ public partial class PsmApiClient /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task> GetProjectsAsync(QueryParameters queryParameters = null, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(queryParameters); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -74,13 +74,12 @@ public async Task> GetProjectsAsync(QueryParameters queryPa /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] - public async Task PostProjectAsync(ProjectRequest requestBody, CancellationToken cancellationToken = default) + 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(new HttpMethod("POST"), $"projects{query}", content, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects{query}", content, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -102,12 +101,11 @@ public async Task PostProjectAsync(ProjectRequest requestBody, Cancella /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task GetProjectByIdAsync(Guid projectId, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects/{Escape(projectId)}{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -132,13 +130,12 @@ public async Task GetProjectByIdAsync(Guid projectId, CancellationToken /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] - public async Task PutProjectByIdAsync(Guid projectId, ProjectRequest requestBody, CancellationToken cancellationToken = default) + 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(new HttpMethod("PUT"), $"projects/{Escape(projectId)}{query}", content, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Put, $"projects/{Escape(projectId)}{query}", content, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -160,12 +157,11 @@ public async Task PutProjectByIdAsync(Guid projectId, ProjectRequest re /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task DeleteProjectByIdAsync(Guid projectId, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); - using var response = await this.SendAsync(new HttpMethod("DELETE"), $"projects/{Escape(projectId)}{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Delete, $"projects/{Escape(projectId)}{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/QueryApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/QueryApi.cs index 4841c6326..1ca4ecb11 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/QueryApi.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/QueryApi.cs @@ -15,7 +15,6 @@ namespace SySML2.NET.REST { using System; - using System.CodeDom.Compiler; using System.Collections.Generic; using System.Net.Http; using System.Text.Json; @@ -50,12 +49,11 @@ public partial class PsmApiClient /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task> GetQueriesByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(queryParameters); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects/{Escape(projectId)}/queries{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/queries{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -80,13 +78,12 @@ public async Task> GetQueriesByProjectAsync(Guid projectId, Q /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task PostQueryByProjectAsync(Guid projectId, QueryRequest requestBody, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); using var content = await this.CreateContentAsync(requestBody, cancellationToken); - using var response = await this.SendAsync(new HttpMethod("POST"), $"projects/{Escape(projectId)}/queries{query}", content, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects/{Escape(projectId)}/queries{query}", content, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -111,12 +108,11 @@ public async Task PostQueryByProjectAsync(Guid projectId, QueryRequest re /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task GetQueryByProjectAndIdAsync(Guid projectId, Guid queryId, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects/{Escape(projectId)}/queries/{Escape(queryId)}{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/queries/{Escape(queryId)}{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -144,13 +140,12 @@ public async Task GetQueryByProjectAndIdAsync(Guid projectId, Guid queryI /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task PutQueryByProjectAndIdAsync(Guid projectId, Guid queryId, QueryRequest requestBody, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); using var content = await this.CreateContentAsync(requestBody, cancellationToken); - using var response = await this.SendAsync(new HttpMethod("PUT"), $"projects/{Escape(projectId)}/queries/{Escape(queryId)}{query}", content, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Put, $"projects/{Escape(projectId)}/queries/{Escape(queryId)}{query}", content, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -175,12 +170,11 @@ public async Task PutQueryByProjectAndIdAsync(Guid projectId, Guid queryI /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task DeleteQueryByProjectAndIdAsync(Guid projectId, Guid queryId, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); - using var response = await this.SendAsync(new HttpMethod("DELETE"), $"projects/{Escape(projectId)}/queries/{Escape(queryId)}{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Delete, $"projects/{Escape(projectId)}/queries/{Escape(queryId)}{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -211,13 +205,12 @@ public async Task DeleteQueryByProjectAndIdAsync(Guid projectId, Guid que /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] 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(new HttpMethod("GET"), $"projects/{Escape(projectId)}/queries/{Escape(queryId)}/results{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/queries/{Escape(queryId)}/results{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -248,14 +241,13 @@ public async Task> GetQueryResultsByProjectIdQueryIdAsync(Gui /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task> GetQueryResultsByProjectIdQueryAsync(Guid projectId, QueryRequest requestBody, Guid? commitId = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(queryParameters); query.Add("commitId", commitId); using var content = await this.CreateContentAsync(requestBody, cancellationToken); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects/{Escape(projectId)}/query-results{query}", content, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/query-results{query}", content, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -286,14 +278,13 @@ public async Task> GetQueryResultsByProjectIdQueryAsync(Guid /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task> GetQueryResultsByProjectIdQueryPostAsync(Guid projectId, QueryRequest requestBody, Guid? commitId = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(queryParameters); query.Add("commitId", commitId); using var content = await this.CreateContentAsync(requestBody, cancellationToken); - using var response = await this.SendAsync(new HttpMethod("POST"), $"projects/{Escape(projectId)}/query-results{query}", content, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects/{Escape(projectId)}/query-results{query}", content, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/RelationshipApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/RelationshipApi.cs index 1138990d3..f41b35c26 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/RelationshipApi.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/RelationshipApi.cs @@ -15,7 +15,6 @@ namespace SySML2.NET.REST { using System; - using System.CodeDom.Compiler; using System.Collections.Generic; using System.Net.Http; using System.Text.Json; @@ -24,8 +23,10 @@ namespace SySML2.NET.REST 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; /// /// Provides the Relationship operations of the Systems Modeling API and Services @@ -62,14 +63,13 @@ public partial class PsmApiClient /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] - public async Task> GetRelationshipsByProjectCommitRelatedElementAsync(Guid projectId, Guid commitId, Guid relatedElementId, string direction = null, bool? excludeUsed = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + 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); + query.Add("direction", direction is { } directionValue ? ToQueryValue(DirectionProvider.ToUtf8Bytes(directionValue)) : null); query.Add("excludeUsed", excludeUsed); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/elements/{Escape(relatedElementId)}/relationships{query}", null, cancellationToken); + 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); diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/TagApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/TagApi.cs index afec4dc7a..d2c45afd6 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/TagApi.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/TagApi.cs @@ -15,7 +15,6 @@ namespace SySML2.NET.REST { using System; - using System.CodeDom.Compiler; using System.Collections.Generic; using System.Net.Http; using System.Text.Json; @@ -50,12 +49,11 @@ public partial class PsmApiClient /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task> GetTagsByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(queryParameters); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects/{Escape(projectId)}/tags{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/tags{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -80,13 +78,12 @@ public async Task> GetTagsByProjectAsync(Guid projectId, QueryP /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task PostTagByProjectAsync(Guid projectId, TagRequest requestBody, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); using var content = await this.CreateContentAsync(requestBody, cancellationToken); - using var response = await this.SendAsync(new HttpMethod("POST"), $"projects/{Escape(projectId)}/tags{query}", content, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects/{Escape(projectId)}/tags{query}", content, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -111,12 +108,11 @@ public async Task PostTagByProjectAsync(Guid projectId, TagRequest requestB /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task GetTagByProjectAndIdAsync(Guid projectId, Guid tagId, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects/{Escape(projectId)}/tags/{Escape(tagId)}{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/tags/{Escape(tagId)}{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -141,12 +137,11 @@ public async Task GetTagByProjectAndIdAsync(Guid projectId, Guid tagId, Can /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task DeleteTagByProjectAndIdAsync(Guid projectId, Guid tagId, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); - using var response = await this.SendAsync(new HttpMethod("DELETE"), $"projects/{Escape(projectId)}/tags/{Escape(tagId)}{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Delete, $"projects/{Escape(projectId)}/tags/{Escape(tagId)}{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); diff --git a/SySML2.NET.REST/MergeConflictException.cs b/SySML2.NET.REST/MergeConflictException.cs index bdad2595e..32d883361 100644 --- a/SySML2.NET.REST/MergeConflictException.cs +++ b/SySML2.NET.REST/MergeConflictException.cs @@ -9,6 +9,7 @@ namespace SySML2.NET.REST { + using System; using System.Collections.Generic; using System.Net; @@ -19,6 +20,37 @@ namespace SySML2.NET.REST /// 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 /// diff --git a/SySML2.NET.REST/PsmApiClient.cs b/SySML2.NET.REST/PsmApiClient.cs index 64c3565f2..fb98b5704 100644 --- a/SySML2.NET.REST/PsmApiClient.cs +++ b/SySML2.NET.REST/PsmApiClient.cs @@ -18,6 +18,7 @@ namespace SySML2.NET.REST 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; @@ -75,6 +76,11 @@ public partial class PsmApiClient : IPsmApiClient private static Regex LinkValuePattern() => LinkValueRegex; #endif + /// + /// The used to write every request payload + /// + private static readonly JsonWriterOptions WriterOptions = new(); + /// /// The whose addresses the server /// @@ -135,6 +141,51 @@ 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); + } + } + /// /// Queries the page that the link with the specified relation type of the Link header points to /// @@ -145,12 +196,9 @@ private static string Escape(object value) /// The relation types, any of which selects the link /// /// - /// The , or null when the header has no such link or the link carries no page token + /// The , or null when the header has no such link or the link carries no page token that accepts /// - /// - /// Thrown when the page token of the link is not an epoch and an identifier separated by a vertical bar - /// - private static Page? QueryLinkedPage(HttpResponseMessage response, params string[] relationTypes) + private Page? QueryLinkedPage(HttpResponseMessage response, params string[] relationTypes) { if (!response.Headers.TryGetValues("Link", out var headerValues)) { @@ -180,34 +228,19 @@ private static string Escape(object value) .Select(pair => Uri.UnescapeDataString(pair[1])) .FirstOrDefault(); - return token is null ? null : ParsePage(token); - } - - /// - /// Parses a page token formatted as writes it - /// - /// - /// The page token - /// - /// - /// The - /// - /// - /// Thrown when the token is not an epoch and an identifier separated by a vertical bar - /// - private static Page ParsePage(string token) - { - var parts = token.Split('|'); - - if (parts.Length != 2 || !int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var epoch) || !Guid.TryParse(parts[1], out var identifier)) + if (token is null) { - throw new FormatException($"The page token '{token}' is not an epoch and an identifier separated by a vertical bar"); + return null; } - return new Page + if (Page.TryParse(token, out var page)) { - Epoch = epoch, Identifier = identifier - }; + return page; + } + + this.logger.LogWarning("The page token {Token} of the {Relation} link is not a valid page token", token, relationTypes[0]); + + return null; } /// @@ -231,7 +264,7 @@ private async Task CreateContentAsync(IRequest requestBody, Cancell var stream = new MemoryStream(); - await this.serializer.SerializeRequestAsync(requestBody, SerializationModeKind.JSON, false, stream, new JsonWriterOptions(), cancellationToken); + await this.serializer.SerializeRequestAsync(requestBody, SerializationModeKind.JSON, false, stream, WriterOptions, cancellationToken); return CreateContent(stream); } @@ -257,7 +290,7 @@ private async Task CreateContentAsync(IEnumerable requestBod var stream = new MemoryStream(); - await this.serializer.SerializeAsync(requestBody, SerializationModeKind.JSON, false, stream, new JsonWriterOptions(), cancellationToken); + await this.serializer.SerializeAsync(requestBody, SerializationModeKind.JSON, false, stream, WriterOptions, cancellationToken); return CreateContent(stream); } @@ -330,21 +363,42 @@ private async Task EnsureSuccessAsync(HttpResponseMessage response, Cancellation if (response.Content?.Headers.ContentType?.MediaType == JsonMediaType) { - using var stream = await ReadContentStreamAsync(response.Content, cancellationToken); - - try - { - error = 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", response.StatusCode); - } + 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 /// @@ -358,7 +412,10 @@ private async Task EnsureSuccessAsync(HttpResponseMessage response, Cancellation /// An awaitable /// /// - /// Thrown when the response has the status code and a JSON body + /// 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) { @@ -367,9 +424,29 @@ private async Task ThrowOnConflictAsync(HttpResponseMessage response, Cancellati return; } - using var stream = await ReadContentStreamAsync(response.Content, cancellationToken); + await using var buffer = new MemoryStream(); + + await using (var stream = await ReadContentStreamAsync(response.Content, cancellationToken)) + { + await stream.CopyToAsync(buffer, cancellationToken); + } + + buffer.Position = 0; - var conflicts = await this.deSerializer.DeSerializeResponsesAsync(stream, SerializationModeKind.JSON, false, cancellationToken); + 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()); } @@ -396,7 +473,7 @@ private async Task ReadResponseAsync(HttpResponseMessage response, Cancell return default; } - using var stream = await ReadContentStreamAsync(response.Content, cancellationToken); + await using var stream = await ReadContentStreamAsync(response.Content, cancellationToken); return await this.deSerializer.DeSerializeResponseAsync(stream, SerializationModeKind.JSON, false, cancellationToken); } @@ -418,11 +495,11 @@ private async Task ReadResponseAsync(HttpResponseMessage response, Cancell /// private async Task> ReadPagedResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken) where T : IResponse { - using var stream = await ReadContentStreamAsync(response.Content, cancellationToken); + 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()); + return this.CreatePagedResult(response, records.ToList()); } /// @@ -445,11 +522,11 @@ private async Task> ReadPagedResponseAsync(HttpResponseMessage /// private async Task ReadDataAsync(HttpResponseMessage response, CancellationToken cancellationToken) where T : IData { - using var stream = await ReadContentStreamAsync(response.Content, cancellationToken); + await using var stream = await ReadContentStreamAsync(response.Content, cancellationToken); - var data = await this.deSerializer.DeSerializeAsync(stream, SerializationModeKind.JSON, SerializationTargetKind.PSM, false, cancellationToken); + var data = await this.deSerializer.DeSerializeAsync(stream, SerializationModeKind.JSON, SerializationTargetKind.PSM, false, cancellationToken); - return data.Cast().Single(); + return data.Single(); } /// @@ -471,9 +548,9 @@ private async Task> ReadPagedDataAsync(HttpResponseMessage res { await using var stream = await ReadContentStreamAsync(response.Content, cancellationToken); - var data = await this.deSerializer.DeSerializeAsync(stream, SerializationModeKind.JSON, SerializationTargetKind.PSM, false, cancellationToken); + var data = await this.deSerializer.DeSerializeAsync(stream, SerializationModeKind.JSON, SerializationTargetKind.PSM, false, cancellationToken); - return CreatePagedResult(response, data.Cast().ToList()); + return this.CreatePagedResult(response, data.ToList()); } /// @@ -495,6 +572,25 @@ private static async Task ReadJsonAsync(HttpResponseMessage respon 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 async Task> ReadPagedJsonAsync(HttpResponseMessage response, CancellationToken cancellationToken) + { + var document = await ReadJsonAsync(response, cancellationToken); + + return this.CreatePagedResult(response, [document]); + } + /// /// Creates a from the records of a response and the pages that its Link header points to /// @@ -510,13 +606,13 @@ private static async Task ReadJsonAsync(HttpResponseMessage respon /// /// The /// - private static PagedResult CreatePagedResult(HttpResponseMessage response, IReadOnlyList records) + private PagedResult CreatePagedResult(HttpResponseMessage response, IReadOnlyList records) { return new PagedResult { Records = records, - Next = QueryLinkedPage(response, "next"), - Previous = QueryLinkedPage(response, "prev", "previous") + Next = this.QueryLinkedPage(response, "next"), + Previous = this.QueryLinkedPage(response, "prev", "previous") }; } diff --git a/SySML2.NET.REST/PsmApiException.cs b/SySML2.NET.REST/PsmApiException.cs index c2604c298..dcc436df0 100644 --- a/SySML2.NET.REST/PsmApiException.cs +++ b/SySML2.NET.REST/PsmApiException.cs @@ -19,6 +19,53 @@ namespace SySML2.NET.REST /// 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 /// diff --git a/SySML2.NET.REST/SySML2.NET.REST.csproj b/SySML2.NET.REST/SySML2.NET.REST.csproj index 1c86c0b76..114fffbb2 100644 --- a/SySML2.NET.REST/SySML2.NET.REST.csproj +++ b/SySML2.NET.REST/SySML2.NET.REST.csproj @@ -15,7 +15,6 @@ true [Update] Aligned with SysML2.NET 0.25.2 - [Update] Code-generated PsmApiClient covering every operation of the Systems Modeling API and Services (breaking: replaces IRestClient/RestClient; Session no longer takes credentials) true true 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 index 53f39ac27..3e907a686 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/BranchApi.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/BranchApi.cs @@ -15,7 +15,6 @@ namespace SySML2.NET.REST { using System; - using System.CodeDom.Compiler; using System.Collections.Generic; using System.Net.Http; using System.Text.Json; @@ -50,12 +49,11 @@ public partial class PsmApiClient /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task> GetBranchesByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(queryParameters); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects/{Escape(projectId)}/branches{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/branches{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -80,13 +78,12 @@ public async Task> GetBranchesByProjectAsync(Guid projectId, /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task PostBranchByProjectAsync(Guid projectId, BranchRequest requestBody, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); using var content = await this.CreateContentAsync(requestBody, cancellationToken); - using var response = await this.SendAsync(new HttpMethod("POST"), $"projects/{Escape(projectId)}/branches{query}", content, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects/{Escape(projectId)}/branches{query}", content, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -111,12 +108,11 @@ public async Task PostBranchByProjectAsync(Guid projectId, BranchRequest /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task GetBranchesByProjectAndIdAsync(Guid projectId, Guid branchId, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects/{Escape(projectId)}/branches/{Escape(branchId)}{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/branches/{Escape(branchId)}{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -141,12 +137,11 @@ public async Task GetBranchesByProjectAndIdAsync(Guid projectId, Guid br /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task DeleteBranchByProjectAndIdAsync(Guid projectId, Guid branchId, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); - using var response = await this.SendAsync(new HttpMethod("DELETE"), $"projects/{Escape(projectId)}/branches/{Escape(branchId)}{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Delete, $"projects/{Escape(projectId)}/branches/{Escape(branchId)}{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/CommitApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/CommitApi.cs index c78fa5e43..2fd63751f 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/CommitApi.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/CommitApi.cs @@ -15,7 +15,6 @@ namespace SySML2.NET.REST { using System; - using System.CodeDom.Compiler; using System.Collections.Generic; using System.Net.Http; using System.Text.Json; @@ -50,12 +49,11 @@ public partial class PsmApiClient /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task> GetCommitsByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(queryParameters); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects/{Escape(projectId)}/commits{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -83,14 +81,13 @@ public async Task> GetCommitsByProjectAsync(Guid projectId, /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task PostCommitByProjectAsync(Guid projectId, CommitRequest requestBody, Guid? branchId = null, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); query.Add("branchId", branchId); using var content = await this.CreateContentAsync(requestBody, cancellationToken); - using var response = await this.SendAsync(new HttpMethod("POST"), $"projects/{Escape(projectId)}/commits{query}", content, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects/{Escape(projectId)}/commits{query}", content, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -115,12 +112,11 @@ public async Task PostCommitByProjectAsync(Guid projectId, CommitRequest /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task GetCommitByProjectAndIdAsync(Guid projectId, Guid commitId, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects/{Escape(projectId)}/commits/{Escape(commitId)}{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(commitId)}{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -148,12 +144,11 @@ public async Task GetCommitByProjectAndIdAsync(Guid projectId, Guid comm /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] 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(new HttpMethod("GET"), $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/changes{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/changes{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -181,12 +176,11 @@ public async Task> GetChangesByProjectCommitAsync(Guid /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task GetChangeByProjectCommitIdAsync(Guid projectId, Guid commitId, Guid changeId, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/changes/{Escape(changeId)}{query}", null, cancellationToken); + 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); diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/DiffMergeApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/DiffMergeApi.cs index 7227a23ee..136f0b2fe 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/DiffMergeApi.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/DiffMergeApi.cs @@ -15,7 +15,6 @@ namespace SySML2.NET.REST { using System; - using System.CodeDom.Compiler; using System.Collections.Generic; using System.Net.Http; using System.Text.Json; @@ -56,13 +55,12 @@ public partial class PsmApiClient /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] 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(new HttpMethod("GET"), $"projects/{Escape(projectId)}/commits/{Escape(compareCommitId)}/diff{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(compareCommitId)}/diff{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -96,21 +94,28 @@ public async Task> DiffAsync(Guid projectId, Guid ba /// /// 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 /// - [GeneratedCode("SysML2.NET", "latest")] - public async Task MergeAsync(Guid projectId, IEnumerable sourceCommitId, Guid targetBranchId, IEnumerable requestBody, string description = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + 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(new HttpMethod("POST"), $"projects/{Escape(projectId)}/branches/{Escape(targetBranchId)}/merge{query}", content, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects/{Escape(projectId)}/branches/{Escape(targetBranchId)}/merge{query}", content, cancellationToken); await this.ThrowOnConflictAsync(response, cancellationToken); diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/ElementApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/ElementApi.cs index 551c29ce2..e69d57fdd 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/ElementApi.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/ElementApi.cs @@ -15,7 +15,6 @@ namespace SySML2.NET.REST { using System; - using System.CodeDom.Compiler; using System.Collections.Generic; using System.Net.Http; using System.Text.Json; @@ -56,13 +55,12 @@ public partial class PsmApiClient /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] 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(new HttpMethod("GET"), $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/elements{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/elements{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -93,13 +91,12 @@ public async Task> GetElementsByProjectCommitAsync(Guid pr /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] 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(new HttpMethod("GET"), $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/elements/{Escape(elementId)}{query}", null, cancellationToken); + 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); @@ -127,12 +124,11 @@ public async Task GetElementByProjectCommitIdAsync(Guid projectId, Gui /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task GetProjectUsageByProjectCommitElementAsync(Guid projectId, Guid commitId, Guid elementId, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/elements/{Escape(elementId)}/projectUsage{query}", null, cancellationToken); + 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); @@ -163,13 +159,12 @@ public async Task GetProjectUsageByProjectCommitElementAsync(Guid /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] 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(new HttpMethod("GET"), $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/roots{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/roots{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/IPsmApiClient.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/IPsmApiClient.cs index eb72e3540..7a98b42ff 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/IPsmApiClient.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/IPsmApiClient.cs @@ -25,6 +25,7 @@ namespace SySML2.NET.REST using SysML2.NET.Core.DTO.Root.Elements; using SysML2.NET.PSM; using SysML2.NET.PSM.DTO; + using SysML2.NET.PSM.Enumerations; /// /// The client of the REST/HTTP platform specific model of the Systems Modeling API and Services @@ -64,7 +65,7 @@ public partial interface IPsmApiClient /// /// Thrown when the server responds with an error status code /// - Task PostProjectAsync(ProjectRequest requestBody, CancellationToken cancellationToken = default); + Task PostProjectAsync(ProjectRequest requestBody = null, CancellationToken cancellationToken = default); /// /// Get project by ID @@ -101,7 +102,7 @@ public partial interface IPsmApiClient /// /// Thrown when the server responds with an error status code /// - Task PutProjectByIdAsync(Guid projectId, ProjectRequest requestBody, CancellationToken cancellationToken = default); + Task PutProjectByIdAsync(Guid projectId, ProjectRequest requestBody = null, CancellationToken cancellationToken = default); /// /// Delete project by ID @@ -520,7 +521,7 @@ public partial interface IPsmApiClient /// /// Thrown when the server responds with an error status code /// - Task> GetRelationshipsByProjectCommitRelatedElementAsync(Guid projectId, Guid commitId, Guid relatedElementId, string direction = null, bool? excludeUsed = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + 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 @@ -575,13 +576,19 @@ public partial interface IPsmApiClient /// /// 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, string description = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + Task MergeAsync(Guid projectId, IEnumerable sourceCommitId, Guid targetBranchId, IEnumerable requestBody = null, string description = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default); /// /// Get queries by project @@ -779,7 +786,7 @@ public partial interface IPsmApiClient /// /// Thrown when the server responds with an error status code /// - Task GetDatatypesAsync(QueryParameters queryParameters = null, CancellationToken cancellationToken = default); + Task> GetDatatypesAsync(QueryParameters queryParameters = null, CancellationToken cancellationToken = default); /// /// Get datatype by ID diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/MetaApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/MetaApi.cs index 097384920..53d5c139d 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/MetaApi.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/MetaApi.cs @@ -15,7 +15,6 @@ namespace SySML2.NET.REST { using System; - using System.CodeDom.Compiler; using System.Collections.Generic; using System.Net.Http; using System.Text.Json; @@ -47,16 +46,15 @@ public partial class PsmApiClient /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] - public async Task GetDatatypesAsync(QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + public async Task> GetDatatypesAsync(QueryParameters queryParameters = null, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(queryParameters); - using var response = await this.SendAsync(new HttpMethod("GET"), $"meta/datatypes{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"meta/datatypes{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); - return await ReadJsonAsync(response, cancellationToken); + return await this.ReadPagedJsonAsync(response, cancellationToken); } /// @@ -74,12 +72,11 @@ public async Task GetDatatypesAsync(QueryParameters queryParameter /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task GetDatatypeByIdAsync(string datatypeId, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); - using var response = await this.SendAsync(new HttpMethod("GET"), $"meta/datatypes/{Escape(datatypeId)}{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"meta/datatypes/{Escape(datatypeId)}{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/ProjectApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/ProjectApi.cs index 19532c4b2..066b008eb 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/ProjectApi.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/ProjectApi.cs @@ -30,6 +30,7 @@ namespace SySML2.NET.REST /// /// Provides the Project operations of the Systems Modeling API and Services /// + [GeneratedCode("SysML2.NET", "latest")] public partial class PsmApiClient { /// @@ -47,12 +48,11 @@ public partial class PsmApiClient /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task> GetProjectsAsync(QueryParameters queryParameters = null, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(queryParameters); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -74,13 +74,12 @@ public async Task> GetProjectsAsync(QueryParameters queryPa /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] - public async Task PostProjectAsync(ProjectRequest requestBody, CancellationToken cancellationToken = default) + 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(new HttpMethod("POST"), $"projects{query}", content, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects{query}", content, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -102,12 +101,11 @@ public async Task PostProjectAsync(ProjectRequest requestBody, Cancella /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task GetProjectByIdAsync(Guid projectId, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects/{Escape(projectId)}{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -132,13 +130,12 @@ public async Task GetProjectByIdAsync(Guid projectId, CancellationToken /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] - public async Task PutProjectByIdAsync(Guid projectId, ProjectRequest requestBody, CancellationToken cancellationToken = default) + 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(new HttpMethod("PUT"), $"projects/{Escape(projectId)}{query}", content, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Put, $"projects/{Escape(projectId)}{query}", content, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -160,12 +157,11 @@ public async Task PutProjectByIdAsync(Guid projectId, ProjectRequest re /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task DeleteProjectByIdAsync(Guid projectId, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); - using var response = await this.SendAsync(new HttpMethod("DELETE"), $"projects/{Escape(projectId)}{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Delete, $"projects/{Escape(projectId)}{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/QueryApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/QueryApi.cs index 4841c6326..1ca4ecb11 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/QueryApi.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/QueryApi.cs @@ -15,7 +15,6 @@ namespace SySML2.NET.REST { using System; - using System.CodeDom.Compiler; using System.Collections.Generic; using System.Net.Http; using System.Text.Json; @@ -50,12 +49,11 @@ public partial class PsmApiClient /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task> GetQueriesByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(queryParameters); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects/{Escape(projectId)}/queries{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/queries{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -80,13 +78,12 @@ public async Task> GetQueriesByProjectAsync(Guid projectId, Q /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task PostQueryByProjectAsync(Guid projectId, QueryRequest requestBody, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); using var content = await this.CreateContentAsync(requestBody, cancellationToken); - using var response = await this.SendAsync(new HttpMethod("POST"), $"projects/{Escape(projectId)}/queries{query}", content, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects/{Escape(projectId)}/queries{query}", content, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -111,12 +108,11 @@ public async Task PostQueryByProjectAsync(Guid projectId, QueryRequest re /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task GetQueryByProjectAndIdAsync(Guid projectId, Guid queryId, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects/{Escape(projectId)}/queries/{Escape(queryId)}{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/queries/{Escape(queryId)}{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -144,13 +140,12 @@ public async Task GetQueryByProjectAndIdAsync(Guid projectId, Guid queryI /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task PutQueryByProjectAndIdAsync(Guid projectId, Guid queryId, QueryRequest requestBody, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); using var content = await this.CreateContentAsync(requestBody, cancellationToken); - using var response = await this.SendAsync(new HttpMethod("PUT"), $"projects/{Escape(projectId)}/queries/{Escape(queryId)}{query}", content, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Put, $"projects/{Escape(projectId)}/queries/{Escape(queryId)}{query}", content, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -175,12 +170,11 @@ public async Task PutQueryByProjectAndIdAsync(Guid projectId, Guid queryI /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task DeleteQueryByProjectAndIdAsync(Guid projectId, Guid queryId, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); - using var response = await this.SendAsync(new HttpMethod("DELETE"), $"projects/{Escape(projectId)}/queries/{Escape(queryId)}{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Delete, $"projects/{Escape(projectId)}/queries/{Escape(queryId)}{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -211,13 +205,12 @@ public async Task DeleteQueryByProjectAndIdAsync(Guid projectId, Guid que /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] 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(new HttpMethod("GET"), $"projects/{Escape(projectId)}/queries/{Escape(queryId)}/results{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/queries/{Escape(queryId)}/results{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -248,14 +241,13 @@ public async Task> GetQueryResultsByProjectIdQueryIdAsync(Gui /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task> GetQueryResultsByProjectIdQueryAsync(Guid projectId, QueryRequest requestBody, Guid? commitId = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(queryParameters); query.Add("commitId", commitId); using var content = await this.CreateContentAsync(requestBody, cancellationToken); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects/{Escape(projectId)}/query-results{query}", content, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/query-results{query}", content, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -286,14 +278,13 @@ public async Task> GetQueryResultsByProjectIdQueryAsync(Guid /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task> GetQueryResultsByProjectIdQueryPostAsync(Guid projectId, QueryRequest requestBody, Guid? commitId = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(queryParameters); query.Add("commitId", commitId); using var content = await this.CreateContentAsync(requestBody, cancellationToken); - using var response = await this.SendAsync(new HttpMethod("POST"), $"projects/{Escape(projectId)}/query-results{query}", content, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects/{Escape(projectId)}/query-results{query}", content, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/RelationshipApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/RelationshipApi.cs index 1138990d3..f41b35c26 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/RelationshipApi.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/RelationshipApi.cs @@ -15,7 +15,6 @@ namespace SySML2.NET.REST { using System; - using System.CodeDom.Compiler; using System.Collections.Generic; using System.Net.Http; using System.Text.Json; @@ -24,8 +23,10 @@ namespace SySML2.NET.REST 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; /// /// Provides the Relationship operations of the Systems Modeling API and Services @@ -62,14 +63,13 @@ public partial class PsmApiClient /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] - public async Task> GetRelationshipsByProjectCommitRelatedElementAsync(Guid projectId, Guid commitId, Guid relatedElementId, string direction = null, bool? excludeUsed = null, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) + 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); + query.Add("direction", direction is { } directionValue ? ToQueryValue(DirectionProvider.ToUtf8Bytes(directionValue)) : null); query.Add("excludeUsed", excludeUsed); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects/{Escape(projectId)}/commits/{Escape(commitId)}/elements/{Escape(relatedElementId)}/relationships{query}", null, cancellationToken); + 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); diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/TagApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/TagApi.cs index afec4dc7a..d2c45afd6 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/TagApi.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/TagApi.cs @@ -15,7 +15,6 @@ namespace SySML2.NET.REST { using System; - using System.CodeDom.Compiler; using System.Collections.Generic; using System.Net.Http; using System.Text.Json; @@ -50,12 +49,11 @@ public partial class PsmApiClient /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task> GetTagsByProjectAsync(Guid projectId, QueryParameters queryParameters = null, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(queryParameters); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects/{Escape(projectId)}/tags{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/tags{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -80,13 +78,12 @@ public async Task> GetTagsByProjectAsync(Guid projectId, QueryP /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task PostTagByProjectAsync(Guid projectId, TagRequest requestBody, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); using var content = await this.CreateContentAsync(requestBody, cancellationToken); - using var response = await this.SendAsync(new HttpMethod("POST"), $"projects/{Escape(projectId)}/tags{query}", content, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Post, $"projects/{Escape(projectId)}/tags{query}", content, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -111,12 +108,11 @@ public async Task PostTagByProjectAsync(Guid projectId, TagRequest requestB /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task GetTagByProjectAndIdAsync(Guid projectId, Guid tagId, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); - using var response = await this.SendAsync(new HttpMethod("GET"), $"projects/{Escape(projectId)}/tags/{Escape(tagId)}{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Get, $"projects/{Escape(projectId)}/tags/{Escape(tagId)}{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); @@ -141,12 +137,11 @@ public async Task GetTagByProjectAndIdAsync(Guid projectId, Guid tagId, Can /// /// Thrown when the server responds with an error status code /// - [GeneratedCode("SysML2.NET", "latest")] public async Task DeleteTagByProjectAndIdAsync(Guid projectId, Guid tagId, CancellationToken cancellationToken = default) { var query = new QueryStringBuilder(); - using var response = await this.SendAsync(new HttpMethod("DELETE"), $"projects/{Escape(projectId)}/tags/{Escape(tagId)}{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.Delete, $"projects/{Escape(projectId)}/tags/{Escape(tagId)}{query}", null, cancellationToken); await this.EnsureSuccessAsync(response, cancellationToken); diff --git a/SysML2.NET.CodeGenerator.Tests/Generators/OpenApiHandleBarsGenerators/OpenApiApiClientGeneratorTestFixture.cs b/SysML2.NET.CodeGenerator.Tests/Generators/OpenApiHandleBarsGenerators/OpenApiApiClientGeneratorTestFixture.cs index 338f988fa..282989474 100644 --- a/SysML2.NET.CodeGenerator.Tests/Generators/OpenApiHandleBarsGenerators/OpenApiApiClientGeneratorTestFixture.cs +++ b/SysML2.NET.CodeGenerator.Tests/Generators/OpenApiHandleBarsGenerators/OpenApiApiClientGeneratorTestFixture.cs @@ -13,6 +13,7 @@ namespace SysML2.NET.CodeGenerator.Tests.Generators.OpenApiHandleBarsGenerators using System.Collections.Generic; using System.IO; using System.Linq; + using System.Net.Http; using System.Threading.Tasks; using Microsoft.OpenApi; @@ -64,16 +65,71 @@ public void VerifyQueryTagModels() { Assert.That(() => OpenApiApiClientGenerator.QueryTagModels(null), Throws.TypeOf()); - var tagModels = OpenApiApiClientGenerator.QueryTagModels(this.openApiDocument); + 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()); + } - using (Assert.EnterMultipleScope()) + private static OpenApiDocument CreateDocument(OpenApiOperation operation) + { + var document = new OpenApiDocument { - Assert.That(tagModels, Has.Count.EqualTo(9)); - Assert.That(tagModels.Select(tagModel => tagModel.Name), Is.EqualTo(["Project", "Branch", "Tag", "Commit", "Element", "Relationship", "DiffMerge", "Query", "Meta"])); - Assert.That(tagModels.SelectMany(tagModel => tagModel.Operations).ToList(), Has.Count.EqualTo(35)); - Assert.That(tagModels[6].Tag, Is.EqualTo("Diff & Merge")); - Assert.That(tagModels[6].Operations.Select(operation => operation.HttpMethod), Is.EqualTo(["GET", "POST"])); - } + 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] 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 index a5c55e62b..903dc35fe 100644 --- a/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/ApiClientOperationModel.cs +++ b/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/ApiClientOperationModel.cs @@ -9,6 +9,9 @@ namespace SysML2.NET.CodeGenerator.Generators.OpenApiHandleBarsGenerators { + using System.Collections.Generic; + using System.Linq; + using Microsoft.OpenApi; /// @@ -16,6 +19,11 @@ namespace SysML2.NET.CodeGenerator.Generators.OpenApiHandleBarsGenerators /// 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 /// @@ -30,5 +38,22 @@ public sealed class ApiClientOperationModel /// 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 index 48eefdb9c..33d5b35b3 100644 --- a/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/ApiClientTagModel.cs +++ b/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/ApiClientTagModel.cs @@ -10,6 +10,7 @@ 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 @@ -26,9 +27,19 @@ public sealed class ApiClientTagModel /// 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 index 33b812b78..e866e3d87 100644 --- a/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/OpenApiApiClientGenerator.cs +++ b/SysML2.NET.CodeGenerator/Generators/OpenApiHandleBarsGenerators/OpenApiApiClientGenerator.cs @@ -122,6 +122,9 @@ public Task GenerateApiClientInterfaceAsync(OpenApiDocument openApiDocum /// /// 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); @@ -131,27 +134,64 @@ public static IReadOnlyList QueryTagModels(OpenApiDocument op { Path = path.Key, HttpMethod = operation.Key.Method, - Operation = operation.Value + Operation = ValidateOperation(operation.Value) }))) .ToLookup(entry => entry.Tag, entry => entry.Model, StringComparer.Ordinal); return openApiDocument.Tags .Where(tag => operationsByTag.Contains(tag.Name)) - .Select(tag => new ApiClientTagModel + .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(); } /// 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/Templates/OpenAPI/Partials/psm-apiclient-documentation-openapi-partial.hbs b/SysML2.NET.CodeGenerator/Templates/OpenAPI/Partials/psm-apiclient-documentation-openapi-partial.hbs index 9b2f4ba1e..0b15d1302 100644 --- 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 @@ -1,7 +1,7 @@ /// /// {{Operation.Summary}} /// -{{#each Operation.Parameters}} +{{#each Parameters}} {{#if Required}} /// /// {{Description}} @@ -13,35 +13,41 @@ /// The payload sent as the body of the request /// {{/if}} -{{#each Operation.Parameters}} +{{#each Parameters}} {{#unless Required}} -{{#unless (String.StartsWith Name 'page')}} /// /// {{Description}} /// {{/unless}} -{{/unless}} {{/each}} -{{#each Operation.Parameters}} -{{#if (String.Equal Name 'page[size]')}} +{{#if IsPaged}} /// /// The page of the result to request, or null to let the server choose /// {{/if}} -{{/each}} /// /// 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 +/// +{{/if}} +{{/if}} +{{/each}} /// /// Thrown when the server responds with an error status code /// -{{#each Operation.Responses}} -{{#if (String.Equal @key '409')}} +{{#if ThrowsMergeConflict}} /// /// Thrown when the server reports conflicting data /// {{/if}} -{{/each}} 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 index 037f59d4f..6fa877dec 100644 --- 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 @@ -5,30 +5,30 @@ {{~#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 (String.Equal (OpenApi.Text Schema.Type) 'Array')}}IEnumerable<{{> scalar Schema.Items}}>{{else}}{{> scalar Schema}}{{#unless Required}}{{> nullable Schema}}{{/unless}}{{/if~}} +{{~#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}}JsonDocument{{/if~}} +{{~#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 Operation.Parameters~}} +{{~#each Parameters~}} {{~#if Required}}{{> parametertype}} {{Name}}, {{/if~}} {{~/each~}} -{{~#each Operation.RequestBody.Content~}} -{{~#if Schema.Reference}}{{Schema.Reference.Id}} requestBody, {{/if~}} -{{~#each Schema.OneOf~}}{{~#if Items}}IEnumerable<{{> psm-apiclient-typename-openapi-partial Items}}> requestBody, {{/if~}}{{~/each~}} -{{~/each~}} -{{~#each Operation.Parameters~}} -{{~#unless Required}}{{#unless (String.StartsWith Name 'page')}}{{> parametertype}} {{Name}} = null, {{/unless}}{{/unless~}} +{{~#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~}} -{{~#each Operation.Parameters~}} -{{~#if (String.Equal Name 'page[size]')}}QueryParameters queryParameters = null, {{/if~}} +{{~/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/psm-apiclient-interface-openapi-template.hbs b/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-interface-openapi-template.hbs index 66c9d567b..4cf5e68e2 100644 --- a/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-interface-openapi-template.hbs +++ b/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-interface-openapi-template.hbs @@ -25,6 +25,7 @@ namespace SySML2.NET.REST using SysML2.NET.Core.DTO.Root.Elements; using SysML2.NET.PSM; using SysML2.NET.PSM.DTO; + using SysML2.NET.PSM.Enumerations; /// /// The client of the REST/HTTP platform specific model of the Systems Modeling API and Services diff --git a/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-openapi-template.hbs b/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-openapi-template.hbs index 8382dea8b..e8df1b275 100644 --- a/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-openapi-template.hbs +++ b/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-openapi-template.hbs @@ -18,7 +18,9 @@ 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; @@ -27,40 +29,56 @@ namespace SySML2.NET.REST 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}} /// /// 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}} - [GeneratedCode("SysML2.NET", "latest")] public async {{> psm-apiclient-signature-openapi-partial}} { - var query = new QueryStringBuilder({{#each Operation.Parameters}}{{#if (String.Equal Name 'page[size]')}}queryParameters{{/if}}{{/each}}); - {{#each Operation.Parameters}} + {{#each Parameters}} + {{#if Required}} + {{#if (String.Equal (OpenApi.Text Schema.Type) 'Array')}} + ThrowIfNullOrEmpty({{Name}}, nameof({{Name}})); + + {{/if}} + {{/if}} + {{/each}} + var query = new QueryStringBuilder({{#if IsPaged}}queryParameters{{/if}}); + {{#each Parameters}} {{#if (String.Equal (OpenApi.Text In) 'Query')}} - {{#unless (String.StartsWith Name 'page')}} + {{#if Schema.Enum}} + query.Add("{{Name}}", {{Name}} is { } {{Name}}Value ? ToQueryValue({{String.Pascalcase Name}}Provider.ToUtf8Bytes({{Name}}Value)) : null); + {{else}} query.Add("{{Name}}", {{Name}}); - {{/unless}} + {{/if}} {{/if}} {{/each}} {{#if Operation.RequestBody}} using var content = await this.CreateContentAsync(requestBody, cancellationToken); - using var response = await this.SendAsync(new HttpMethod("{{HttpMethod}}"), $"{{{String.Replace (String.Replace (String.Substring Path 1) '{' '{Escape(') '}' ')}'}}}{query}", content, 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(new HttpMethod("{{HttpMethod}}"), $"{{{String.Replace (String.Replace (String.Substring Path 1) '{' '{Escape(') '}' ')}'}}}{query}", null, cancellationToken); + using var response = await this.SendAsync(HttpMethod.{{OpenApi.HttpMethod HttpMethod}}, $"{{{String.Replace (String.Replace (String.Substring Path 1) '{' '{Escape(') '}' ')}'}}}{query}", null, cancellationToken); {{/if}} - {{#each Operation.Responses}} - {{#if (String.Equal @key '409')}} + {{#if ThrowsMergeConflict}} await this.ThrowOnConflictAsync(response, cancellationToken); {{/if}} - {{/each}} await this.EnsureSuccessAsync(response, cancellationToken); @@ -72,6 +90,8 @@ namespace SySML2.NET.REST 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 this.ReadPagedJsonAsync(response, cancellationToken); {{else}} return await ReadJsonAsync(response, cancellationToken); {{/if}} 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..c01e00b6c --- /dev/null +++ b/SysML2.NET.Tests/PSM/PageTestFixture.cs @@ -0,0 +1,70 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// 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 VerifyToString() + { + Assert.That(new Page { Epoch = 1618861496632, Identifier = SpecificationIdentifier }.ToString(), Is.EqualTo(SpecificationToken)); + + Assert.That(new Page().ToString(), Is.EqualTo(Encode($"0|{Guid.Empty}").TrimEnd('='))); + } + + [Test] + public void VerifyTryParse() + { + using (Assert.EnterMultipleScope()) + { + Assert.That(Page.TryParse(null, out _), Is.False); + Assert.That(Page.TryParse(" ", out _), Is.False); + Assert.That(Page.TryParse("not a token!", out _), Is.False); + Assert.That(Page.TryParse(Encode($"1618861496632{SpecificationIdentifier}"), out _), Is.False); + Assert.That(Page.TryParse(Encode($"epoch|{SpecificationIdentifier}"), out _), Is.False); + Assert.That(Page.TryParse(Encode($"-1|{SpecificationIdentifier}"), out _), Is.False); + Assert.That(Page.TryParse(Encode("1618861496632|not-a-guid"), out var rejected), Is.False); + Assert.That(rejected, Is.EqualTo(default(Page))); + } + + Assert.That(Page.TryParse(SpecificationToken, out var page), Is.True); + + using (Assert.EnterMultipleScope()) + { + Assert.That(page.Epoch, Is.EqualTo(1618861496632)); + Assert.That(page.Identifier, Is.EqualTo(SpecificationIdentifier)); + Assert.That(Page.TryParse(Encode($"1618861496632|{SpecificationIdentifier}"), out var padded), Is.True); + Assert.That(padded, Is.EqualTo(page)); + Assert.That(Page.TryParse(Encode($"1618861496632|{SpecificationIdentifier}?").Replace('/', '_').Replace('+', '-'), out _), Is.False); + } + + var roundTripped = new Page { Epoch = 1618862165315, Identifier = Guid.Parse("08f433db-b6d4-41b8-9020-e820ebc417be") }; + + Assert.That(Page.TryParse(roundTripped.ToString(), out var parsed) && parsed.Equals(roundTripped), Is.True); + } + } +} diff --git a/SysML2.NET.Tests/PSM/PagedResultTestFixture.cs b/SysML2.NET.Tests/PSM/PagedResultTestFixture.cs index 45705cd39..a6c7af110 100644 --- a/SysML2.NET.Tests/PSM/PagedResultTestFixture.cs +++ b/SysML2.NET.Tests/PSM/PagedResultTestFixture.cs @@ -43,13 +43,15 @@ 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 = new Page { Epoch = 1618861496632, Identifier = FirstIdentifier }; + pagedResult.Next = new Page { Epoch = 1618862165315, Identifier = LastIdentifier }; 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.Value.Epoch, Is.EqualTo(1618861496632)); + Assert.That(pagedResult.Previous.Value.Identifier, Is.EqualTo(FirstIdentifier)); + Assert.That(pagedResult.Next.Value.Epoch, Is.EqualTo(1618862165315)); + Assert.That(pagedResult.Next.Value.Identifier, Is.EqualTo(LastIdentifier)); } } } 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..c6608ab3a 100644 --- a/SysML2.NET/PSM/Page.cs +++ b/SysML2.NET/PSM/Page.cs @@ -10,6 +10,8 @@ namespace SysML2.NET.PSM { using System; + using System.Globalization; + using System.Text; /// /// Defines the structure of the page for the pagination process @@ -17,9 +19,9 @@ namespace SysML2.NET.PSM public struct Page { /// - /// the epoch time + /// The time, in milliseconds since the Unix epoch, that positions the page /// - public int Epoch { get; set; } + public long Epoch { get; set; } /// /// The page identifier @@ -27,14 +29,63 @@ public struct Page public Guid Identifier { get; set; } /// - /// Returns a string that represents the + /// Converts a page token into a /// + /// + /// The page token: the unpadded Base64 encoding of an epoch and an identifier separated by a vertical bar (Systems Modeling API and Services, Clause 8.1.3) + /// + /// + /// The when the conversion succeeds, or the default otherwise + /// /// - /// a string representation + /// true when was converted, false otherwise + /// + public static bool TryParse(string token, out Page page) + { + page = default; + + if (string.IsNullOrWhiteSpace(token)) + { + return false; + } + + var base64 = 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 epoch) || !Guid.TryParse(parts[1], out var identifier)) + { + return false; + } + + page = new Page + { + Epoch = epoch, + Identifier = identifier + }; + + return true; + } + + /// + /// Returns the page token that represents the + /// + /// + /// The unpadded Base64 encoding of the epoch and the identifier separated by a vertical bar /// public override string ToString() { - return $"{this.Epoch}|{this.Identifier}"; + var text = $"{this.Epoch.ToString(CultureInfo.InvariantCulture)}|{this.Identifier}"; + + return Convert.ToBase64String(Encoding.UTF8.GetBytes(text)).TrimEnd('='); } } } diff --git a/SysML2.NET/PSM/QueryParameters.cs b/SysML2.NET/PSM/QueryParameters.cs index 5a3344bd6..847777865 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 token written by : the unpadded Base64 encoding of epoch|GUID, the epoch in milliseconds since the Unix epoch /// 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 token written by : the unpadded Base64 encoding of epoch|GUID, the epoch in milliseconds since the Unix epoch /// public Page? PageBefore { get; set; } = null; From 721d8bd6762dc177d9f67175bd3db6ac7dc15f36 Mon Sep 17 00:00:00 2001 From: Zeynep Delal Mutlu Date: Thu, 5 Nov 2026 08:44:31 +0100 Subject: [PATCH 4/6] Pr check - paging and espace sequences with comma --- .../PsmApiClientTestFixture.cs | 16 ++- .../QueryStringBuilderTestFixture.cs | 8 +- SySML2.NET.REST/PsmApiClient.cs | 22 +---- SySML2.NET.REST/QueryStringBuilder.cs | 18 ++-- SysML2.NET.Tests/PSM/PageTestFixture.cs | 70 +++++++++---- .../PSM/PagedResultTestFixture.cs | 11 +-- SysML2.NET/PSM/Page.cs | 99 ++++++++++++------- SysML2.NET/PSM/QueryParameters.cs | 4 +- 8 files changed, 154 insertions(+), 94 deletions(-) diff --git a/SySML2.NET.REST.Tests/PsmApiClientTestFixture.cs b/SySML2.NET.REST.Tests/PsmApiClientTestFixture.cs index 6285acc2b..80fae1f19 100644 --- a/SySML2.NET.REST.Tests/PsmApiClientTestFixture.cs +++ b/SySML2.NET.REST.Tests/PsmApiClientTestFixture.cs @@ -95,8 +95,8 @@ public void VerifyConstructor() public async Task VerifyGetBranchesByProjectAsync() { var branchesUri = $"{BaseAddress}projects/{ProjectId}/branches"; - var previousPage = new Page { Epoch = 1618861496632, Identifier = OtherCommitId }; - var nextPage = new Page { Epoch = 1618861496632, Identifier = Guid.Parse("20109f40-4825-416a-86f6-7508ac42a021") }; + 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())}") @@ -131,6 +131,14 @@ public async Task VerifyGetBranchesByProjectAsync() 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); @@ -365,7 +373,7 @@ public async Task VerifyMergeAsync() var mergeUri = $"{BaseAddress}projects/{ProjectId}/branches/{BranchId}/merge"; this.mockHttp.Expect(HttpMethod.Post, mergeUri) - .WithExactQueryString($"sourceCommitId={CommitId}%2C{OtherCommitId}&description=the%20merge%20description") + .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")); @@ -424,7 +432,7 @@ 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 = new Page { Epoch = 1618861496632, Identifier = CommitId }; + var nextPage = Page.FromCursor(1618861496632, CommitId); this.mockHttp.Expect(HttpMethod.Get, datatypesUri) .WithExactQueryString("page[size]=1") diff --git a/SySML2.NET.REST.Tests/QueryStringBuilderTestFixture.cs b/SySML2.NET.REST.Tests/QueryStringBuilderTestFixture.cs index 6b0b2da29..755d8c4af 100644 --- a/SySML2.NET.REST.Tests/QueryStringBuilderTestFixture.cs +++ b/SySML2.NET.REST.Tests/QueryStringBuilderTestFixture.cs @@ -26,6 +26,8 @@ public class QueryStringBuilderTestFixture private static readonly Guid[] TwoCommitIds = [FirstCommitId, SecondCommitId]; + private static readonly string[] ValuesContainingTheDelimiter = ["first, value", "second&value"]; + [Test] public void VerifyConstructor() { @@ -33,7 +35,7 @@ public void VerifyConstructor() Assert.That(new QueryStringBuilder(new QueryParameters()).ToString(), Is.Empty); - var page = new Page { Epoch = 1618861496632, Identifier = FirstCommitId }; + var page = Page.FromCursor(1618861496632, FirstCommitId); var builder = new QueryStringBuilder(new QueryParameters { PageSize = 2, PageAfter = page, PageBefore = page }); @@ -47,6 +49,7 @@ public void VerifyAdd() builder.Add("commitId", null); builder.Add("sourceCommitId", NoCommitIds); + builder.Add("page[after]", default(Page)); Assert.That(builder.ToString(), Is.Empty); @@ -54,8 +57,9 @@ public void VerifyAdd() 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}%2C{SecondCommitId}&description=the%20merge%2C%20%26%20its%20description&pageSize=1.5")); + 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] diff --git a/SySML2.NET.REST/PsmApiClient.cs b/SySML2.NET.REST/PsmApiClient.cs index fb98b5704..bab45b937 100644 --- a/SySML2.NET.REST/PsmApiClient.cs +++ b/SySML2.NET.REST/PsmApiClient.cs @@ -196,9 +196,9 @@ private static void ThrowIfNullOrEmpty(IEnumerable value, string parameter /// The relation types, any of which selects the link /// /// - /// The , or null when the header has no such link or the link carries no page token that accepts + /// 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 Page? QueryLinkedPage(HttpResponseMessage response, params string[] relationTypes) + private static Page? QueryLinkedPage(HttpResponseMessage response, params string[] relationTypes) { if (!response.Headers.TryGetValues("Link", out var headerValues)) { @@ -228,19 +228,7 @@ private static void ThrowIfNullOrEmpty(IEnumerable value, string parameter .Select(pair => Uri.UnescapeDataString(pair[1])) .FirstOrDefault(); - if (token is null) - { - return null; - } - - if (Page.TryParse(token, out var page)) - { - return page; - } - - this.logger.LogWarning("The page token {Token} of the {Relation} link is not a valid page token", token, relationTypes[0]); - - return null; + return string.IsNullOrWhiteSpace(token) ? null : new Page(token); } /// @@ -611,8 +599,8 @@ private PagedResult CreatePagedResult(HttpResponseMessage response, IReadO return new PagedResult { Records = records, - Next = this.QueryLinkedPage(response, "next"), - Previous = this.QueryLinkedPage(response, "prev", "previous") + Next = QueryLinkedPage(response, "next"), + Previous = QueryLinkedPage(response, "prev", "previous") }; } diff --git a/SySML2.NET.REST/QueryStringBuilder.cs b/SySML2.NET.REST/QueryStringBuilder.cs index 619e57e48..5b061785d 100644 --- a/SySML2.NET.REST/QueryStringBuilder.cs +++ b/SySML2.NET.REST/QueryStringBuilder.cs @@ -50,7 +50,7 @@ public QueryStringBuilder(QueryParameters queryParameters = null) } /// - /// Adds a query parameter, writing a sequence as a comma separated list + /// 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 @@ -63,7 +63,7 @@ public void Add(string name, object value) var text = value switch { null => null, - string stringValue => stringValue, + string stringValue => Uri.EscapeDataString(stringValue), bool booleanValue => booleanValue ? "true" : "false", IEnumerable sequence => FormatSequence(sequence), _ => FormatValue(value) @@ -71,7 +71,7 @@ public void Add(string name, object value) if (text is not null) { - this.parameters.Add($"{name}={Uri.EscapeDataString(text)}"); + this.parameters.Add($"{name}={text}"); } } @@ -87,7 +87,7 @@ public override string ToString() } /// - /// Formats a sequence as a comma separated list of values formatted using the invariant culture + /// Formats a sequence as its escaped values joined by a literal comma /// /// /// The sequence to format @@ -99,21 +99,23 @@ private static string FormatSequence(IEnumerable sequence) { var values = sequence.Cast().Select(FormatValue).ToList(); - return values.Count == 0 ? null : string.Join(",", values); + return values.Count == 0 ? null : string.Join(',', values); } /// - /// Formats a scalar value using the invariant culture + /// Formats a scalar value using the invariant culture and escapes it /// /// /// The value to format /// /// - /// The formatted value + /// The escaped value, or null when the value formats as null /// private static string FormatValue(object value) { - return Convert.ToString(value, CultureInfo.InvariantCulture); + var text = Convert.ToString(value, CultureInfo.InvariantCulture); + + return text is null ? null : Uri.EscapeDataString(text); } } } diff --git a/SysML2.NET.Tests/PSM/PageTestFixture.cs b/SysML2.NET.Tests/PSM/PageTestFixture.cs index c01e00b6c..5f6c5804e 100644 --- a/SysML2.NET.Tests/PSM/PageTestFixture.cs +++ b/SysML2.NET.Tests/PSM/PageTestFixture.cs @@ -29,42 +29,72 @@ private static string Encode(string text) } [Test] - public void VerifyToString() + public void VerifyConstructor() { - Assert.That(new Page { Epoch = 1618861496632, Identifier = SpecificationIdentifier }.ToString(), Is.EqualTo(SpecificationToken)); + 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); + } - Assert.That(new Page().ToString(), Is.EqualTo(Encode($"0|{Guid.Empty}").TrimEnd('='))); + [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 VerifyTryParse() + public void VerifyTryDecode() { using (Assert.EnterMultipleScope()) { - Assert.That(Page.TryParse(null, out _), Is.False); - Assert.That(Page.TryParse(" ", out _), Is.False); - Assert.That(Page.TryParse("not a token!", out _), Is.False); - Assert.That(Page.TryParse(Encode($"1618861496632{SpecificationIdentifier}"), out _), Is.False); - Assert.That(Page.TryParse(Encode($"epoch|{SpecificationIdentifier}"), out _), Is.False); - Assert.That(Page.TryParse(Encode($"-1|{SpecificationIdentifier}"), out _), Is.False); - Assert.That(Page.TryParse(Encode("1618861496632|not-a-guid"), out var rejected), Is.False); - Assert.That(rejected, Is.EqualTo(default(Page))); + 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(Page.TryParse(SpecificationToken, out var page), Is.True); + Assert.That(new Page(SpecificationToken).TryDecode(out var epoch, out var identifier), Is.True); using (Assert.EnterMultipleScope()) { - Assert.That(page.Epoch, Is.EqualTo(1618861496632)); - Assert.That(page.Identifier, Is.EqualTo(SpecificationIdentifier)); - Assert.That(Page.TryParse(Encode($"1618861496632|{SpecificationIdentifier}"), out var padded), Is.True); - Assert.That(padded, Is.EqualTo(page)); - Assert.That(Page.TryParse(Encode($"1618861496632|{SpecificationIdentifier}?").Replace('/', '_').Replace('+', '-'), out _), Is.False); + 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 roundTripped = new Page { Epoch = 1618862165315, Identifier = Guid.Parse("08f433db-b6d4-41b8-9020-e820ebc417be") }; + var roundTripIdentifier = Guid.Parse("08f433db-b6d4-41b8-9020-e820ebc417be"); - Assert.That(Page.TryParse(roundTripped.ToString(), out var parsed) && parsed.Equals(roundTripped), Is.True); + 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 a6c7af110..6d36c4274 100644 --- a/SysML2.NET.Tests/PSM/PagedResultTestFixture.cs +++ b/SysML2.NET.Tests/PSM/PagedResultTestFixture.cs @@ -43,15 +43,14 @@ public void VerifyPagedResult() Assert.That(pagedResult.Records[^1], Is.EqualTo("the last record")); } - pagedResult.Previous = new Page { Epoch = 1618861496632, Identifier = FirstIdentifier }; - pagedResult.Next = new Page { Epoch = 1618862165315, Identifier = LastIdentifier }; + pagedResult.Previous = Page.FromCursor(1618861496632, FirstIdentifier); + pagedResult.Next = new Page("the next page token"); using (Assert.EnterMultipleScope()) { - Assert.That(pagedResult.Previous.Value.Epoch, Is.EqualTo(1618861496632)); - Assert.That(pagedResult.Previous.Value.Identifier, Is.EqualTo(FirstIdentifier)); - Assert.That(pagedResult.Next.Value.Epoch, Is.EqualTo(1618862165315)); - Assert.That(pagedResult.Next.Value.Identifier, Is.EqualTo(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/Page.cs b/SysML2.NET/PSM/Page.cs index c6608ab3a..16e9f5c3e 100644 --- a/SysML2.NET/PSM/Page.cs +++ b/SysML2.NET/PSM/Page.cs @@ -14,42 +14,87 @@ namespace SysML2.NET.PSM 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 time, in milliseconds since the Unix epoch, that positions the page + /// Initializes a new instance of the struct /// - public long 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; } /// - /// Converts a page token into a + /// Creates a whose token is the unpadded Base64 encoding of an epoch and an identifier separated by a vertical bar /// - /// - /// The page token: the unpadded Base64 encoding of an epoch and an identifier separated by a vertical bar (Systems Modeling API and Services, Clause 8.1.3) + /// + /// The time, in milliseconds since the Unix epoch, that positions the page /// - /// - /// The when the conversion succeeds, or the default otherwise + /// + /// The identifier that positions the page /// /// - /// true when was converted, false otherwise + /// The in the token format shown by the examples of Clause 8.1.3 /// - public static bool TryParse(string token, out Page page) + public static Page FromCursor(long epoch, Guid identifier) { - page = default; + var text = $"{epoch.ToString(CultureInfo.InvariantCulture)}|{identifier}"; - if (string.IsNullOrWhiteSpace(token)) + return new Page(Convert.ToBase64String(Encoding.UTF8.GetBytes(text)).TrimEnd('=')); + } + + /// + /// Returns the page token + /// + /// + /// The + /// + public override string ToString() + { + 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 = token.Replace('-', '+').Replace('_', '/'); + var base64 = this.Token.Replace('-', '+').Replace('_', '/'); base64 = base64.PadRight(base64.Length + (4 - base64.Length % 4) % 4, '='); var bytes = new byte[base64.Length * 3 / 4]; @@ -61,31 +106,15 @@ public static bool TryParse(string token, out Page page) var parts = Encoding.UTF8.GetString(bytes, 0, byteCount).Split('|'); - if (parts.Length != 2 || !long.TryParse(parts[0], NumberStyles.None, CultureInfo.InvariantCulture, out var epoch) || !Guid.TryParse(parts[1], out var identifier)) + if (parts.Length != 2 || !long.TryParse(parts[0], NumberStyles.None, CultureInfo.InvariantCulture, out var decodedEpoch) || !Guid.TryParse(parts[1], out var decodedIdentifier)) { return false; } - page = new Page - { - Epoch = epoch, - Identifier = identifier - }; + epoch = decodedEpoch; + identifier = decodedIdentifier; return true; } - - /// - /// Returns the page token that represents the - /// - /// - /// The unpadded Base64 encoding of the epoch and the identifier separated by a vertical bar - /// - public override string ToString() - { - var text = $"{this.Epoch.ToString(CultureInfo.InvariantCulture)}|{this.Identifier}"; - - return Convert.ToBase64String(Encoding.UTF8.GetBytes(text)).TrimEnd('='); - } } } diff --git a/SysML2.NET/PSM/QueryParameters.cs b/SysML2.NET/PSM/QueryParameters.cs index 847777865..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 /// /// - /// the token written by : the unpadded Base64 encoding of epoch|GUID, the epoch in milliseconds since the Unix epoch + /// 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 /// /// - /// the token written by : the unpadded Base64 encoding of epoch|GUID, the epoch in milliseconds since the Unix epoch + /// 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; From dd3cb74a5b2915115ad9a2998fa0ad771bf7c04a Mon Sep 17 00:00:00 2001 From: Zeynep Delal Mutlu Date: Thu, 5 Nov 2026 10:09:56 +0100 Subject: [PATCH 5/6] Exceptions folder and namespace adjustment --- .../MergeConflictExceptionTestFixture.cs | 3 ++- .../{ => Exceptions}/PsmApiExceptionTestFixture.cs | 3 ++- SySML2.NET.REST.Tests/PsmApiClientTestFixture.cs | 1 + SySML2.NET.REST.Tests/SessionTestFixture.cs | 1 + SySML2.NET.REST/AutoGenPsmApiClient/BranchApi.cs | 1 + SySML2.NET.REST/AutoGenPsmApiClient/CommitApi.cs | 1 + SySML2.NET.REST/AutoGenPsmApiClient/DiffMergeApi.cs | 1 + SySML2.NET.REST/AutoGenPsmApiClient/ElementApi.cs | 1 + SySML2.NET.REST/AutoGenPsmApiClient/IPsmApiClient.cs | 1 + SySML2.NET.REST/AutoGenPsmApiClient/MetaApi.cs | 3 ++- SySML2.NET.REST/AutoGenPsmApiClient/ProjectApi.cs | 1 + SySML2.NET.REST/AutoGenPsmApiClient/QueryApi.cs | 1 + .../AutoGenPsmApiClient/RelationshipApi.cs | 1 + SySML2.NET.REST/AutoGenPsmApiClient/TagApi.cs | 1 + .../{ => Exceptions}/MergeConflictException.cs | 2 +- SySML2.NET.REST/{ => Exceptions}/PsmApiException.cs | 2 +- SySML2.NET.REST/PsmApiClient.cs | 11 ++++++----- .../OpenAPI/REST/AutoGenPsmApiClient/BranchApi.cs | 1 + .../OpenAPI/REST/AutoGenPsmApiClient/CommitApi.cs | 1 + .../OpenAPI/REST/AutoGenPsmApiClient/DiffMergeApi.cs | 1 + .../OpenAPI/REST/AutoGenPsmApiClient/ElementApi.cs | 1 + .../OpenAPI/REST/AutoGenPsmApiClient/IPsmApiClient.cs | 1 + .../OpenAPI/REST/AutoGenPsmApiClient/MetaApi.cs | 3 ++- .../OpenAPI/REST/AutoGenPsmApiClient/ProjectApi.cs | 1 + .../OpenAPI/REST/AutoGenPsmApiClient/QueryApi.cs | 1 + .../REST/AutoGenPsmApiClient/RelationshipApi.cs | 1 + .../OpenAPI/REST/AutoGenPsmApiClient/TagApi.cs | 1 + .../psm-apiclient-interface-openapi-template.hbs | 1 + .../OpenAPI/psm-apiclient-openapi-template.hbs | 3 ++- 29 files changed, 39 insertions(+), 12 deletions(-) rename SySML2.NET.REST.Tests/{ => Exceptions}/MergeConflictExceptionTestFixture.cs (96%) rename SySML2.NET.REST.Tests/{ => Exceptions}/PsmApiExceptionTestFixture.cs (96%) rename SySML2.NET.REST/{ => Exceptions}/MergeConflictException.cs (98%) rename SySML2.NET.REST/{ => Exceptions}/PsmApiException.cs (98%) diff --git a/SySML2.NET.REST.Tests/MergeConflictExceptionTestFixture.cs b/SySML2.NET.REST.Tests/Exceptions/MergeConflictExceptionTestFixture.cs similarity index 96% rename from SySML2.NET.REST.Tests/MergeConflictExceptionTestFixture.cs rename to SySML2.NET.REST.Tests/Exceptions/MergeConflictExceptionTestFixture.cs index 5d0e81381..464a3f06f 100644 --- a/SySML2.NET.REST.Tests/MergeConflictExceptionTestFixture.cs +++ b/SySML2.NET.REST.Tests/Exceptions/MergeConflictExceptionTestFixture.cs @@ -7,7 +7,7 @@ // // ------------------------------------------------------------------------------------------------ -namespace SySML2.NET.REST.Tests +namespace SySML2.NET.REST.Tests.Exceptions { using System; using System.Net; @@ -15,6 +15,7 @@ namespace SySML2.NET.REST.Tests using NUnit.Framework; using SysML2.NET.PSM.DTO; + using SysML2.NET.REST.Exceptions; [TestFixture] public class MergeConflictExceptionTestFixture diff --git a/SySML2.NET.REST.Tests/PsmApiExceptionTestFixture.cs b/SySML2.NET.REST.Tests/Exceptions/PsmApiExceptionTestFixture.cs similarity index 96% rename from SySML2.NET.REST.Tests/PsmApiExceptionTestFixture.cs rename to SySML2.NET.REST.Tests/Exceptions/PsmApiExceptionTestFixture.cs index f6b649475..a40362939 100644 --- a/SySML2.NET.REST.Tests/PsmApiExceptionTestFixture.cs +++ b/SySML2.NET.REST.Tests/Exceptions/PsmApiExceptionTestFixture.cs @@ -7,7 +7,7 @@ // // ------------------------------------------------------------------------------------------------ -namespace SySML2.NET.REST.Tests +namespace SySML2.NET.REST.Tests.Exceptions { using System; using System.Net; @@ -15,6 +15,7 @@ namespace SySML2.NET.REST.Tests using NUnit.Framework; using SysML2.NET.PSM.DTO; + using SysML2.NET.REST.Exceptions; [TestFixture] public class PsmApiExceptionTestFixture diff --git a/SySML2.NET.REST.Tests/PsmApiClientTestFixture.cs b/SySML2.NET.REST.Tests/PsmApiClientTestFixture.cs index 80fae1f19..d872b7b13 100644 --- a/SySML2.NET.REST.Tests/PsmApiClientTestFixture.cs +++ b/SySML2.NET.REST.Tests/PsmApiClientTestFixture.cs @@ -25,6 +25,7 @@ namespace SySML2.NET.REST.Tests 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] diff --git a/SySML2.NET.REST.Tests/SessionTestFixture.cs b/SySML2.NET.REST.Tests/SessionTestFixture.cs index 51d9e1309..99663b86e 100644 --- a/SySML2.NET.REST.Tests/SessionTestFixture.cs +++ b/SySML2.NET.REST.Tests/SessionTestFixture.cs @@ -21,6 +21,7 @@ namespace SySML2.NET.REST.Tests using RichardSzalay.MockHttp; + using SysML2.NET.REST.Exceptions; using SysML2.NET.Serializer.Json; [TestFixture] diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/BranchApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/BranchApi.cs index 3e907a686..a8246cb5c 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/BranchApi.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/BranchApi.cs @@ -25,6 +25,7 @@ namespace SySML2.NET.REST 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 diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/CommitApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/CommitApi.cs index 2fd63751f..dad3d3207 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/CommitApi.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/CommitApi.cs @@ -25,6 +25,7 @@ namespace SySML2.NET.REST 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 diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/DiffMergeApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/DiffMergeApi.cs index 136f0b2fe..4ff7175ec 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/DiffMergeApi.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/DiffMergeApi.cs @@ -25,6 +25,7 @@ namespace SySML2.NET.REST 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 diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/ElementApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/ElementApi.cs index e69d57fdd..3b3d1a3e6 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/ElementApi.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/ElementApi.cs @@ -25,6 +25,7 @@ namespace SySML2.NET.REST 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 diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/IPsmApiClient.cs b/SySML2.NET.REST/AutoGenPsmApiClient/IPsmApiClient.cs index 7a98b42ff..073195f97 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/IPsmApiClient.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/IPsmApiClient.cs @@ -26,6 +26,7 @@ namespace SySML2.NET.REST 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 diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/MetaApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/MetaApi.cs index 53d5c139d..d8b09abb3 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/MetaApi.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/MetaApi.cs @@ -25,6 +25,7 @@ namespace SySML2.NET.REST 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 @@ -54,7 +55,7 @@ public async Task> GetDatatypesAsync(QueryParameters q await this.EnsureSuccessAsync(response, cancellationToken); - return await this.ReadPagedJsonAsync(response, cancellationToken); + return await ReadPagedJsonAsync(response, cancellationToken); } /// diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/ProjectApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/ProjectApi.cs index 066b008eb..cef1b1f45 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/ProjectApi.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/ProjectApi.cs @@ -26,6 +26,7 @@ namespace SySML2.NET.REST 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 diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/QueryApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/QueryApi.cs index 1ca4ecb11..41e312930 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/QueryApi.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/QueryApi.cs @@ -25,6 +25,7 @@ namespace SySML2.NET.REST 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 diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/RelationshipApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/RelationshipApi.cs index f41b35c26..f45bcd3ec 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/RelationshipApi.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/RelationshipApi.cs @@ -27,6 +27,7 @@ namespace SySML2.NET.REST 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 diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/TagApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/TagApi.cs index d2c45afd6..746ba6fe2 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/TagApi.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/TagApi.cs @@ -25,6 +25,7 @@ namespace SySML2.NET.REST 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 diff --git a/SySML2.NET.REST/MergeConflictException.cs b/SySML2.NET.REST/Exceptions/MergeConflictException.cs similarity index 98% rename from SySML2.NET.REST/MergeConflictException.cs rename to SySML2.NET.REST/Exceptions/MergeConflictException.cs index 32d883361..427942832 100644 --- a/SySML2.NET.REST/MergeConflictException.cs +++ b/SySML2.NET.REST/Exceptions/MergeConflictException.cs @@ -7,7 +7,7 @@ // // ------------------------------------------------------------------------------------------------ -namespace SySML2.NET.REST +namespace SysML2.NET.REST.Exceptions { using System; using System.Collections.Generic; diff --git a/SySML2.NET.REST/PsmApiException.cs b/SySML2.NET.REST/Exceptions/PsmApiException.cs similarity index 98% rename from SySML2.NET.REST/PsmApiException.cs rename to SySML2.NET.REST/Exceptions/PsmApiException.cs index dcc436df0..08ed4890c 100644 --- a/SySML2.NET.REST/PsmApiException.cs +++ b/SySML2.NET.REST/Exceptions/PsmApiException.cs @@ -7,7 +7,7 @@ // // ------------------------------------------------------------------------------------------------ -namespace SySML2.NET.REST +namespace SysML2.NET.REST.Exceptions { using System; using System.Net; diff --git a/SySML2.NET.REST/PsmApiClient.cs b/SySML2.NET.REST/PsmApiClient.cs index bab45b937..e990a6d1e 100644 --- a/SySML2.NET.REST/PsmApiClient.cs +++ b/SySML2.NET.REST/PsmApiClient.cs @@ -30,6 +30,7 @@ namespace SySML2.NET.REST using SysML2.NET.Common; using SysML2.NET.PSM; using SysML2.NET.PSM.DTO; + using SysML2.NET.REST.Exceptions; using SysML2.NET.Serializer.Json; /// @@ -487,7 +488,7 @@ private async Task> ReadPagedResponseAsync(HttpResponseMessage var records = await this.deSerializer.DeSerializeResponsesAsync(stream, SerializationModeKind.JSON, false, cancellationToken); - return this.CreatePagedResult(response, records.ToList()); + return CreatePagedResult(response, records.ToList()); } /// @@ -538,7 +539,7 @@ private async Task> ReadPagedDataAsync(HttpResponseMessage res var data = await this.deSerializer.DeSerializeAsync(stream, SerializationModeKind.JSON, SerializationTargetKind.PSM, false, cancellationToken); - return this.CreatePagedResult(response, data.ToList()); + return CreatePagedResult(response, data.ToList()); } /// @@ -572,11 +573,11 @@ private static async Task ReadJsonAsync(HttpResponseMessage respon /// /// The whose single record is the parsed , which the caller disposes /// - private async Task> ReadPagedJsonAsync(HttpResponseMessage response, CancellationToken cancellationToken) + private static async Task> ReadPagedJsonAsync(HttpResponseMessage response, CancellationToken cancellationToken) { var document = await ReadJsonAsync(response, cancellationToken); - return this.CreatePagedResult(response, [document]); + return CreatePagedResult(response, [document]); } /// @@ -594,7 +595,7 @@ private async Task> ReadPagedJsonAsync(HttpResponseMes /// /// The /// - private PagedResult CreatePagedResult(HttpResponseMessage response, IReadOnlyList records) + private static PagedResult CreatePagedResult(HttpResponseMessage response, IReadOnlyList records) { return new PagedResult { diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/BranchApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/BranchApi.cs index 3e907a686..a8246cb5c 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/BranchApi.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/BranchApi.cs @@ -25,6 +25,7 @@ namespace SySML2.NET.REST 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 diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/CommitApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/CommitApi.cs index 2fd63751f..dad3d3207 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/CommitApi.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/CommitApi.cs @@ -25,6 +25,7 @@ namespace SySML2.NET.REST 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 diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/DiffMergeApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/DiffMergeApi.cs index 136f0b2fe..4ff7175ec 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/DiffMergeApi.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/DiffMergeApi.cs @@ -25,6 +25,7 @@ namespace SySML2.NET.REST 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 diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/ElementApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/ElementApi.cs index e69d57fdd..3b3d1a3e6 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/ElementApi.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/ElementApi.cs @@ -25,6 +25,7 @@ namespace SySML2.NET.REST 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 diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/IPsmApiClient.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/IPsmApiClient.cs index 7a98b42ff..073195f97 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/IPsmApiClient.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/IPsmApiClient.cs @@ -26,6 +26,7 @@ namespace SySML2.NET.REST 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 diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/MetaApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/MetaApi.cs index 53d5c139d..d8b09abb3 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/MetaApi.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/MetaApi.cs @@ -25,6 +25,7 @@ namespace SySML2.NET.REST 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 @@ -54,7 +55,7 @@ public async Task> GetDatatypesAsync(QueryParameters q await this.EnsureSuccessAsync(response, cancellationToken); - return await this.ReadPagedJsonAsync(response, cancellationToken); + return await ReadPagedJsonAsync(response, cancellationToken); } /// diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/ProjectApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/ProjectApi.cs index 066b008eb..cef1b1f45 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/ProjectApi.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/ProjectApi.cs @@ -26,6 +26,7 @@ namespace SySML2.NET.REST 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 diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/QueryApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/QueryApi.cs index 1ca4ecb11..41e312930 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/QueryApi.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/QueryApi.cs @@ -25,6 +25,7 @@ namespace SySML2.NET.REST 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 diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/RelationshipApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/RelationshipApi.cs index f41b35c26..f45bcd3ec 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/RelationshipApi.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/RelationshipApi.cs @@ -27,6 +27,7 @@ namespace SySML2.NET.REST 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 diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/TagApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/TagApi.cs index d2c45afd6..746ba6fe2 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/TagApi.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/TagApi.cs @@ -25,6 +25,7 @@ namespace SySML2.NET.REST 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 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 index 4cf5e68e2..68915cb34 100644 --- a/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-interface-openapi-template.hbs +++ b/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-interface-openapi-template.hbs @@ -26,6 +26,7 @@ namespace SySML2.NET.REST 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 diff --git a/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-openapi-template.hbs b/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-openapi-template.hbs index e8df1b275..884a9a497 100644 --- a/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-openapi-template.hbs +++ b/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-openapi-template.hbs @@ -37,6 +37,7 @@ namespace SySML2.NET.REST {{#if UsesEnumerations}} using SysML2.NET.PSM.Enumerations; {{/if}} + using SysML2.NET.REST.Exceptions; /// /// Provides the {{Tag}} operations of the Systems Modeling API and Services @@ -91,7 +92,7 @@ namespace SySML2.NET.REST {{else if Schema.Reference}} return await this.Read{{> family Schema}}Async<{{> psm-apiclient-typename-openapi-partial Schema}}>(response, cancellationToken); {{else if ../../IsPaged}} - return await this.ReadPagedJsonAsync(response, cancellationToken); + return await ReadPagedJsonAsync(response, cancellationToken); {{else}} return await ReadJsonAsync(response, cancellationToken); {{/if}} From 4cccbb79b64fc6e8bfa7aa2c4dd50a1c3c324a44 Mon Sep 17 00:00:00 2001 From: Zeynep Delal Mutlu Date: Thu, 5 Nov 2026 12:08:35 +0100 Subject: [PATCH 6/6] review check for empty requestBody and null datatypaId --- .../PsmApiClientTestFixture.cs | 12 ++++- .../AutoGenPsmApiClient/BranchApi.cs | 5 ++ .../AutoGenPsmApiClient/CommitApi.cs | 5 ++ .../AutoGenPsmApiClient/IPsmApiClient.cs | 27 +++++++++++ .../AutoGenPsmApiClient/MetaApi.cs | 8 ++++ .../AutoGenPsmApiClient/QueryApi.cs | 20 ++++++++ SySML2.NET.REST/AutoGenPsmApiClient/TagApi.cs | 5 ++ SySML2.NET.REST/PsmApiClient.cs | 48 +++++++++++++++++++ .../REST/AutoGenPsmApiClient/BranchApi.cs | 5 ++ .../REST/AutoGenPsmApiClient/CommitApi.cs | 5 ++ .../REST/AutoGenPsmApiClient/IPsmApiClient.cs | 27 +++++++++++ .../REST/AutoGenPsmApiClient/MetaApi.cs | 8 ++++ .../REST/AutoGenPsmApiClient/QueryApi.cs | 20 ++++++++ .../REST/AutoGenPsmApiClient/TagApi.cs | 5 ++ ...piclient-documentation-openapi-partial.hbs | 16 +++++++ .../psm-apiclient-openapi-template.hbs | 11 +++++ 16 files changed, 225 insertions(+), 2 deletions(-) diff --git a/SySML2.NET.REST.Tests/PsmApiClientTestFixture.cs b/SySML2.NET.REST.Tests/PsmApiClientTestFixture.cs index d872b7b13..2c8ff879a 100644 --- a/SySML2.NET.REST.Tests/PsmApiClientTestFixture.cs +++ b/SySML2.NET.REST.Tests/PsmApiClientTestFixture.cs @@ -209,12 +209,14 @@ public async Task VerifyPostCommitByProjectAsync() Assert.That(commit.Id, Is.EqualTo(CommitId)); this.mockHttp.Expect(HttpMethod.Post, commitsUri) - .With(request => request.Content is null && string.IsNullOrWhiteSpace(request.RequestUri.Query)) + .With(request => string.IsNullOrWhiteSpace(request.RequestUri.Query)) .Respond(HttpStatusCode.BadRequest, JsonMediaType, ErrorPayload); - await Assert.ThatAsync(() => this.client.PostCommitByProjectAsync(ProjectId, null), + 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(); } @@ -492,6 +494,12 @@ await Assert.ThatAsync(() => this.client.GetDatatypeByIdAsync(datatypeId), 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/AutoGenPsmApiClient/BranchApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/BranchApi.cs index a8246cb5c..ea48225b1 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/BranchApi.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/BranchApi.cs @@ -76,11 +76,16 @@ public async Task> GetBranchesByProjectAsync(Guid projectId, /// /// 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); diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/CommitApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/CommitApi.cs index dad3d3207..a6b228774 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/CommitApi.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/CommitApi.cs @@ -79,11 +79,16 @@ public async Task> GetCommitsByProjectAsync(Guid projectId, /// /// 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); diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/IPsmApiClient.cs b/SySML2.NET.REST/AutoGenPsmApiClient/IPsmApiClient.cs index 073195f97..86b051b8d 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/IPsmApiClient.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/IPsmApiClient.cs @@ -157,6 +157,9 @@ public partial interface IPsmApiClient /// /// The deserialized response of the postBranchByProject operation /// + /// + /// Thrown when is null + /// /// /// Thrown when the server responds with an error status code /// @@ -237,6 +240,9 @@ public partial interface IPsmApiClient /// /// The deserialized response of the postTagByProject operation /// + /// + /// Thrown when is null + /// /// /// Thrown when the server responds with an error status code /// @@ -320,6 +326,9 @@ public partial interface IPsmApiClient /// /// The deserialized response of the postCommitByProject operation /// + /// + /// Thrown when is null + /// /// /// Thrown when the server responds with an error status code /// @@ -626,6 +635,9 @@ public partial interface IPsmApiClient /// /// The deserialized response of the postQueryByProject operation /// + /// + /// Thrown when is null + /// /// /// Thrown when the server responds with an error status code /// @@ -669,6 +681,9 @@ public partial interface IPsmApiClient /// /// The deserialized response of the putQueryByProjectAndId operation /// + /// + /// Thrown when is null + /// /// /// Thrown when the server responds with an error status code /// @@ -741,6 +756,9 @@ public partial interface IPsmApiClient /// /// The deserialized response of the getQueryResultsByProjectIdQuery operation /// + /// + /// Thrown when is null + /// /// /// Thrown when the server responds with an error status code /// @@ -767,6 +785,9 @@ public partial interface IPsmApiClient /// /// The deserialized response of the getQueryResultsByProjectIdQueryPost operation /// + /// + /// Thrown when is null + /// /// /// Thrown when the server responds with an error status code /// @@ -801,6 +822,12 @@ public partial interface IPsmApiClient /// /// 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 /// diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/MetaApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/MetaApi.cs index d8b09abb3..b0540e7ab 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/MetaApi.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/MetaApi.cs @@ -70,11 +70,19 @@ public async Task> GetDatatypesAsync(QueryParameters q /// /// 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); diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/QueryApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/QueryApi.cs index 41e312930..6eaeb4303 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/QueryApi.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/QueryApi.cs @@ -76,11 +76,16 @@ public async Task> GetQueriesByProjectAsync(Guid projectId, Q /// /// 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); @@ -138,11 +143,16 @@ public async Task GetQueryByProjectAndIdAsync(Guid projectId, Guid queryI /// /// 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); @@ -239,11 +249,16 @@ public async Task> GetQueryResultsByProjectIdQueryIdAsync(Gui /// /// 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); @@ -276,11 +291,16 @@ public async Task> GetQueryResultsByProjectIdQueryAsync(Guid /// /// 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); diff --git a/SySML2.NET.REST/AutoGenPsmApiClient/TagApi.cs b/SySML2.NET.REST/AutoGenPsmApiClient/TagApi.cs index 746ba6fe2..90c4d7795 100644 --- a/SySML2.NET.REST/AutoGenPsmApiClient/TagApi.cs +++ b/SySML2.NET.REST/AutoGenPsmApiClient/TagApi.cs @@ -76,11 +76,16 @@ public async Task> GetTagsByProjectAsync(Guid projectId, QueryP /// /// 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); diff --git a/SySML2.NET.REST/PsmApiClient.cs b/SySML2.NET.REST/PsmApiClient.cs index e990a6d1e..199224f9e 100644 --- a/SySML2.NET.REST/PsmApiClient.cs +++ b/SySML2.NET.REST/PsmApiClient.cs @@ -187,6 +187,54 @@ private static void ThrowIfNullOrEmpty(IEnumerable value, string parameter } } + /// + /// 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 /// diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/BranchApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/BranchApi.cs index a8246cb5c..ea48225b1 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/BranchApi.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/BranchApi.cs @@ -76,11 +76,16 @@ public async Task> GetBranchesByProjectAsync(Guid projectId, /// /// 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); diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/CommitApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/CommitApi.cs index dad3d3207..a6b228774 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/CommitApi.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/CommitApi.cs @@ -79,11 +79,16 @@ public async Task> GetCommitsByProjectAsync(Guid projectId, /// /// 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); diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/IPsmApiClient.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/IPsmApiClient.cs index 073195f97..86b051b8d 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/IPsmApiClient.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/IPsmApiClient.cs @@ -157,6 +157,9 @@ public partial interface IPsmApiClient /// /// The deserialized response of the postBranchByProject operation /// + /// + /// Thrown when is null + /// /// /// Thrown when the server responds with an error status code /// @@ -237,6 +240,9 @@ public partial interface IPsmApiClient /// /// The deserialized response of the postTagByProject operation /// + /// + /// Thrown when is null + /// /// /// Thrown when the server responds with an error status code /// @@ -320,6 +326,9 @@ public partial interface IPsmApiClient /// /// The deserialized response of the postCommitByProject operation /// + /// + /// Thrown when is null + /// /// /// Thrown when the server responds with an error status code /// @@ -626,6 +635,9 @@ public partial interface IPsmApiClient /// /// The deserialized response of the postQueryByProject operation /// + /// + /// Thrown when is null + /// /// /// Thrown when the server responds with an error status code /// @@ -669,6 +681,9 @@ public partial interface IPsmApiClient /// /// The deserialized response of the putQueryByProjectAndId operation /// + /// + /// Thrown when is null + /// /// /// Thrown when the server responds with an error status code /// @@ -741,6 +756,9 @@ public partial interface IPsmApiClient /// /// The deserialized response of the getQueryResultsByProjectIdQuery operation /// + /// + /// Thrown when is null + /// /// /// Thrown when the server responds with an error status code /// @@ -767,6 +785,9 @@ public partial interface IPsmApiClient /// /// The deserialized response of the getQueryResultsByProjectIdQueryPost operation /// + /// + /// Thrown when is null + /// /// /// Thrown when the server responds with an error status code /// @@ -801,6 +822,12 @@ public partial interface IPsmApiClient /// /// 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 /// diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/MetaApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/MetaApi.cs index d8b09abb3..b0540e7ab 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/MetaApi.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/MetaApi.cs @@ -70,11 +70,19 @@ public async Task> GetDatatypesAsync(QueryParameters q /// /// 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); diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/QueryApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/QueryApi.cs index 41e312930..6eaeb4303 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/QueryApi.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/QueryApi.cs @@ -76,11 +76,16 @@ public async Task> GetQueriesByProjectAsync(Guid projectId, Q /// /// 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); @@ -138,11 +143,16 @@ public async Task GetQueryByProjectAndIdAsync(Guid projectId, Guid queryI /// /// 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); @@ -239,11 +249,16 @@ public async Task> GetQueryResultsByProjectIdQueryIdAsync(Gui /// /// 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); @@ -276,11 +291,16 @@ public async Task> GetQueryResultsByProjectIdQueryAsync(Guid /// /// 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); diff --git a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/TagApi.cs b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/TagApi.cs index 746ba6fe2..90c4d7795 100644 --- a/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/TagApi.cs +++ b/SysML2.NET.CodeGenerator.Tests/Expected/OpenAPI/REST/AutoGenPsmApiClient/TagApi.cs @@ -76,11 +76,16 @@ public async Task> GetTagsByProjectAsync(Guid projectId, QueryP /// /// 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); 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 index 0b15d1302..f6b220ddf 100644 --- 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 @@ -40,9 +40,25 @@ /// /// 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 /// diff --git a/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-openapi-template.hbs b/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-openapi-template.hbs index 884a9a497..984fc4529 100644 --- a/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-openapi-template.hbs +++ b/SysML2.NET.CodeGenerator/Templates/OpenAPI/psm-apiclient-openapi-template.hbs @@ -56,9 +56,20 @@ namespace SySML2.NET.REST {{#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')}}