mirror of
https://github.com/MAKS-IT-COM/maksit-results.git
synced 2026-11-15 05:46:10 +01:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7958f3c961 | ||
|
|
0a3e14b2e1 | ||
|
|
93fc765c9c |
34
.github/workflows/publish-nuget.yml
vendored
Normal file
34
.github/workflows/publish-nuget.yml
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
name: Publish to NuGet
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Pack
|
||||
run: |
|
||||
dotnet pack src/MaksIT.Results/MaksIT.Results.csproj -c Release -o artifacts
|
||||
|
||||
- name: NuGet login (OIDC)
|
||||
id: login
|
||||
uses: NuGet/login@v1
|
||||
with:
|
||||
user: maks-it
|
||||
|
||||
- name: Push to NuGet
|
||||
run: dotnet nuget push artifacts/*.nupkg --api-key ${{ steps.login.outputs.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
|
||||
22
CHANGELOG.md
22
CHANGELOG.md
@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [2.1.0] - 2026-10-28
|
||||
|
||||
### Added
|
||||
- `Result.FromActionResult` / `Result<T>.FromActionResult` - reverse of `ToActionResult` for MaksIT MVC result types.
|
||||
- `Result.FromHttpResponseAsync` / `Result<T>.FromHttpResponseAsync` - parse HTTP responses in the `ToActionResult` wire shape (JSON, redirect `Location`, `ProblemDetails`).
|
||||
- Package reference to `MaksIT.Core` for JSON helpers used by the HTTP parse path.
|
||||
|
||||
### Changed
|
||||
- `RedirectResult` accepts an explicit status code; default remains 302 Found.
|
||||
- README rewritten around the service <-> API <-> client round-trip (`ToActionResult` / `FromActionResult` / `FromHttpResponseAsync`).
|
||||
- Pruned unused RepoUtils plugins so `utils/plugins` matches the enabled list in `scriptSettings.json`.
|
||||
- Stopped packaging local `assets/badges` copies; coverage badges remain shields.io URLs in the README.
|
||||
|
||||
### Fixed
|
||||
- `RedirectResult` now sets `StatusCode` and `Location` instead of always using `Response.Redirect` (302).
|
||||
- `Result<T>.ToActionResult` passes the result status into `RedirectResult`.
|
||||
|
||||
## [2.0.6] - 2026-10-27
|
||||
|
||||
### Changed
|
||||
- NuGet publish workflow: bump `actions/checkout` and `actions/setup-dotnet` to v5; use NuGet.org username `maks-it`.
|
||||
|
||||
## [2.0.5] - 2026-10-26
|
||||
|
||||
### Fixed
|
||||
|
||||
@ -26,7 +26,7 @@ dotnet build MaksIT.Results.slnx
|
||||
|
||||
### Running Tests
|
||||
|
||||
Preferred: `utils\Invoke-TestEngine.bat` (DotNetTest → QualityGate → CoverageBadges). Tests run under **Microsoft Testing Platform** (`src/global.json` `test.runner` next to the `.slnx`) with **xunit.v3** and **coverlet.MTP**.
|
||||
Preferred: `utils\Invoke-TestEngine.bat` (DotNetTest -> QualityGate -> CoverageBadges). Tests run under **Microsoft Testing Platform** (`src/global.json` `test.runner` next to the `.slnx`) with **xunit.v3** and **coverlet.MTP**.
|
||||
|
||||
```bash
|
||||
cd src
|
||||
|
||||
156
README.md
156
README.md
@ -1,38 +1,57 @@
|
||||
# MaksIT.Results
|
||||
|
||||

|
||||

|
||||

|
||||
[](https://www.nuget.org/packages/MaksIT.Results)
|
||||
[](LICENSE.md)
|
||||
|
||||
`MaksIT.Results` is a .NET library for modeling operation outcomes as HTTP-aware result objects and converting them to `IActionResult` in ASP.NET Core.
|
||||

|
||||

|
||||

|
||||
|
||||
## Features
|
||||
HTTP-aware `Result` / `Result<T>` for .NET - model success and failure once, then map to ASP.NET Core `IActionResult` or parse the same wire shape from an `HttpResponseMessage`.
|
||||
|
||||
- `Result` and `Result<T>` models with status code, success flag, and messages.
|
||||
- Static factory methods for common and extended HTTP status codes (1xx, 2xx, 3xx, 4xx, 5xx).
|
||||
- Built-in conversion to `IActionResult` via `ToActionResult()`.
|
||||
- RFC 7807-style error payloads for failures (`application/problem+json`).
|
||||
- Camel-case JSON serialization for response bodies; respects app-configured `JsonSerializerOptions` (e.g. `AddJsonOptions` with `DefaultIgnoreCondition.WhenWritingNull`).
|
||||
**Target:** .NET 10 (`net10.0`)
|
||||
|
||||
## Installation
|
||||
## Why
|
||||
|
||||
Package Manager:
|
||||
Most Result libraries give you success/failure (and sometimes a typed error), then leave HTTP as a second mapping in the controller: domain `NotFound` -> 404, `Conflict` -> 409, and so on. That remapping is where APIs drift.
|
||||
|
||||
```bash
|
||||
Install-Package MaksIT.Results
|
||||
```
|
||||
**MaksIT.Results uses HTTP status codes as the outcome vocabulary.** One type carries success/failure, messages, and a shared classification (`NotFound`, `Conflict`, `TooManyRequests`, ...). The service already decided the status; the controller calls `ToActionResult()`.
|
||||
|
||||
`dotnet` CLI:
|
||||
That is not "leaking HTTP into the domain." Status codes are a mature taxonomy of outcomes that apply beyond the wire - including desktop, workers, and CLI:
|
||||
|
||||
| Status idea | Same idea outside the browser |
|
||||
|-------------|-------------------------------|
|
||||
| 404 | entity / file / setting not found |
|
||||
| 409 | concurrent edit / state conflict |
|
||||
| 401 / 403 | not signed in / not allowed |
|
||||
| 422 | validation failed |
|
||||
| 429 | rate limit / throttle |
|
||||
| 503 | dependency down / retry later |
|
||||
| 202 | accepted, still processing |
|
||||
|
||||
### Compared to typical Result libs
|
||||
|
||||
| | Typical Result lib | MaksIT.Results |
|
||||
|--|--------------------|----------------|
|
||||
| Outcome model | Success / Failure (+ custom errors) | Success / Failure + `HttpStatusCode` |
|
||||
| Controller mapping | Invent codes, map again to HTTP | Status already set; `ToActionResult()` |
|
||||
| Failure body | Often ad hoc | RFC 7807 `ProblemDetails` |
|
||||
| Client parse | Rare / out of scope | `FromHttpResponseAsync` / `FromActionResult` |
|
||||
| Status factories | Usually `Ok` / `Fail` (+ a few) | Named 1xx-5xx factories |
|
||||
|
||||
**Bottom line:** most Result packages stop at "return from a method." This one is a service <-> API <-> client result model - one contract from domain to wire and back.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
dotnet add package MaksIT.Results
|
||||
```
|
||||
|
||||
## Target Framework
|
||||
```powershell
|
||||
Install-Package MaksIT.Results
|
||||
```
|
||||
|
||||
- `.NET 10` (`net10.0`)
|
||||
|
||||
## Quick Start
|
||||
## Quick start
|
||||
|
||||
### Create results
|
||||
|
||||
@ -42,33 +61,30 @@ using MaksIT.Results;
|
||||
Result ok = Result.Ok("Operation completed");
|
||||
Result failed = Result.BadRequest("Validation failed");
|
||||
|
||||
Result<int> okWithValue = Result<int>.Ok(42, "Answer generated");
|
||||
Result<string?> notFound = Result<string?>.NotFound(null, "Entity not found");
|
||||
Result<int> answer = Result<int>.Ok(42, "Answer generated");
|
||||
Result<UserDto?> missing = Result<UserDto?>.NotFound(null, "Entity not found");
|
||||
```
|
||||
|
||||
### Convert between result types
|
||||
### Map between result shapes
|
||||
|
||||
```csharp
|
||||
using MaksIT.Results;
|
||||
|
||||
Result<int> source = Result<int>.Ok(42, "Value loaded");
|
||||
|
||||
// Result<T> -> Result<U>
|
||||
Result<string?> mapped = source.ToResultOfType(v => v?.ToString());
|
||||
|
||||
// Result<T> -> Result
|
||||
Result<string?> asString = source.ToResultOfType(v => v?.ToString());
|
||||
Result nonGeneric = source.ToResult();
|
||||
```
|
||||
|
||||
### Use in an ASP.NET Core controller
|
||||
### Controllers
|
||||
|
||||
```csharp
|
||||
using MaksIT.Results;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
[ApiController]
|
||||
[Route("users")]
|
||||
public sealed class UsersController : ControllerBase {
|
||||
[HttpGet("{id:guid}")]
|
||||
public IActionResult GetUser(Guid id) {
|
||||
public IActionResult Get(Guid id) {
|
||||
Result<UserDto?> result = id == Guid.Empty
|
||||
? Result<UserDto?>.BadRequest(null, "Invalid id")
|
||||
: Result<UserDto?>.Ok(new UserDto(id, "maks"), "User loaded");
|
||||
@ -80,20 +96,47 @@ public sealed class UsersController : ControllerBase {
|
||||
public sealed record UserDto(Guid Id, string Name);
|
||||
```
|
||||
|
||||
## `ToActionResult()` Behavior
|
||||
### HTTP clients
|
||||
|
||||
- `Result` success: returns status-code-only response.
|
||||
- `Result<T>` success with non-null `Value`: returns JSON body + status code.
|
||||
- `Result<T>` success with a redirection status code (300–303, 307–308) and a non-empty string `Value`: returns an HTTP redirect to that URL.
|
||||
- Any failure: returns RFC 7807-style `ProblemDetails` JSON with:
|
||||
- `status` = result status code
|
||||
- `title` = `"An error occurred"`
|
||||
- `detail` = joined `Messages`
|
||||
- content type `application/problem+json`
|
||||
Parse responses produced in the same contract (JSON body, redirect `Location`, or `problem+json`):
|
||||
|
||||
```csharp
|
||||
using HttpResponseMessage response = await http.GetAsync(url);
|
||||
|
||||
Result bare = await Result.FromHttpResponseAsync(response);
|
||||
Result<UserDto> typed = await Result<UserDto>.FromHttpResponseAsync(response);
|
||||
```
|
||||
|
||||
Or reverse an `IActionResult` you already built:
|
||||
|
||||
```csharp
|
||||
IActionResult action = result.ToActionResult();
|
||||
|
||||
Result parsed = Result.FromActionResult(action);
|
||||
Result<UserDto> typed = Result<UserDto>.FromActionResult(action);
|
||||
```
|
||||
|
||||
## `ToActionResult()` contract
|
||||
|
||||
| Outcome | Response |
|
||||
|--------|----------|
|
||||
| `Result` success | Status code only |
|
||||
| `Result<T>` success, non-null `Value` | JSON body + status |
|
||||
| `Result<T>` success, redirect status (300-303, 307-308) and non-empty string `Value` | Redirect; status preserved on `RedirectResult` |
|
||||
| Any failure | `ProblemDetails`: `status`, `title` = `"An error occurred"`, `detail` = joined `Messages`, content type `application/problem+json` |
|
||||
|
||||
## `FromActionResult` / `FromHttpResponseAsync`
|
||||
|
||||
| Wire shape | Result |
|
||||
|------------|--------|
|
||||
| Success, no body | Success `Result` / empty `Result<T>` |
|
||||
| Success JSON body | Deserialized into `Result<T>.Value` |
|
||||
| Redirect + `Location` | `Result<string>` with that URL and redirect status |
|
||||
| `application/problem+json` (or JSON with `detail` / `title`) | Failure; messages from problem details |
|
||||
|
||||
## JSON options
|
||||
|
||||
`ObjectResult` uses the same `JsonSerializerOptions` as your app when you configure them with `AddJsonOptions`:
|
||||
`ObjectResult` uses the app's `JsonSerializerOptions` when configured via `AddJsonOptions`:
|
||||
|
||||
```csharp
|
||||
builder.Services.AddControllers()
|
||||
@ -102,29 +145,26 @@ builder.Services.AddControllers()
|
||||
});
|
||||
```
|
||||
|
||||
If no options are registered, a default (camel-case) serializer is used.
|
||||
If none are registered, responses fall back to camel case.
|
||||
|
||||
## Status Code Factories
|
||||
## Status code factories
|
||||
|
||||
- Informational: `Result.Continue(...)`, `Result.SwitchingProtocols(...)`, `Result.Processing(...)`, etc.
|
||||
- Success: `Result.Ok(...)`, `Result.Created(...)`, `Result.NoContent(...)`, etc.
|
||||
- Redirection: `Result.Found(...)`, `Result.PermanentRedirect(...)`, etc.
|
||||
- Client error: `Result.BadRequest(...)`, `Result.NotFound(...)`, `Result.TooManyRequests(...)`, etc.
|
||||
- Server error: `Result.InternalServerError(...)`, `Result.ServiceUnavailable(...)`, etc.
|
||||
| Family | Examples |
|
||||
|--------|----------|
|
||||
| Informational | `Continue`, `SwitchingProtocols`, `Processing`, ... |
|
||||
| Success | `Ok`, `Created`, `NoContent`, ... |
|
||||
| Redirection | `Found`, `MovedPermanently`, `PermanentRedirect`, ... |
|
||||
| Client error | `BadRequest`, `NotFound`, `Conflict`, `TooManyRequests`, ... |
|
||||
| Server error | `InternalServerError`, `ServiceUnavailable`, ... |
|
||||
|
||||
Generic equivalents are available via `Result<T>`, for example `Result<MyDto>.Ok(value, "message")`.
|
||||
Use `Result.*` or `Result<T>.*` (e.g. `Result<MyDto>.Ok(value, "message")`).
|
||||
|
||||
## Contributing
|
||||
|
||||
See `CONTRIBUTING.md`.
|
||||
|
||||
## Contact
|
||||
|
||||
If you have any questions or need further assistance, feel free to reach out:
|
||||
|
||||
- **Email**: [maksym.sadovnychyy@gmail.com](mailto:maksym.sadovnychyy@gmail.com)
|
||||
- **Reddit**: [MaksIT.Results: Streamline Your ASP.NET Core API Response Handling](https://www.reddit.com/r/MaksIT/comments/1f89ifn/maksitresults_streamline_your_aspnet_core_api/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button)
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md). Changelog: [CHANGELOG.md](CHANGELOG.md).
|
||||
|
||||
## License
|
||||
|
||||
See `LICENSE.md`.
|
||||
[MIT](LICENSE.md) - Maksym Sadovnychyy (MAKS-IT)
|
||||
|
||||
**Contact:** [maksym.sadovnychyy@gmail.com](mailto:maksym.sadovnychyy@gmail.com)
|
||||
|
||||
@ -3,7 +3,6 @@ using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MaksIT.Results.Mvc;
|
||||
|
||||
namespace MaksIT.Results.Tests.Mvc;
|
||||
|
||||
61
src/MaksIT.Results.Tests/ResultFromActionResultTests.cs
Normal file
61
src/MaksIT.Results.Tests/ResultFromActionResultTests.cs
Normal file
@ -0,0 +1,61 @@
|
||||
using System.Net;
|
||||
using MaksIT.Results.Mvc;
|
||||
|
||||
namespace MaksIT.Results.Tests;
|
||||
|
||||
public class ResultFromActionResultTests {
|
||||
[Fact]
|
||||
public void FromActionResult_RoundTripsSuccessStatusCode() {
|
||||
var original = Result.Ok("done");
|
||||
var action = original.ToActionResult();
|
||||
|
||||
var parsed = Result.FromActionResult(action);
|
||||
|
||||
Assert.True(parsed.IsSuccess);
|
||||
Assert.Equal(HttpStatusCode.OK, parsed.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromActionResult_RoundTripsFailureProblemDetails() {
|
||||
var original = Result.BadRequest("Validation failed", "Field required");
|
||||
var action = original.ToActionResult();
|
||||
|
||||
var parsed = Result.FromActionResult(action);
|
||||
|
||||
Assert.False(parsed.IsSuccess);
|
||||
Assert.Equal(HttpStatusCode.BadRequest, parsed.StatusCode);
|
||||
Assert.Equal(original.Messages, parsed.Messages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromActionResult_RoundTripsGenericValue() {
|
||||
var dto = new SampleDto(7, "vault");
|
||||
var original = Result<SampleDto>.Ok(dto);
|
||||
var action = original.ToActionResult();
|
||||
|
||||
var parsed = Result<SampleDto>.FromActionResult(action);
|
||||
|
||||
Assert.True(parsed.IsSuccess);
|
||||
Assert.Equal(HttpStatusCode.OK, parsed.StatusCode);
|
||||
Assert.NotNull(parsed.Value);
|
||||
Assert.Equal(dto.Id, parsed.Value!.Id);
|
||||
Assert.Equal(dto.Name, parsed.Value.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromActionResult_RoundTripsRedirectWithStatus() {
|
||||
var original = Result<string>.MovedPermanently("https://example.com/new");
|
||||
var action = original.ToActionResult();
|
||||
|
||||
var redirect = Assert.IsType<RedirectResult>(action);
|
||||
Assert.Equal((int)HttpStatusCode.MovedPermanently, redirect.StatusCode);
|
||||
|
||||
var parsed = Result<string>.FromActionResult(action);
|
||||
|
||||
Assert.True(parsed.IsSuccess);
|
||||
Assert.Equal(HttpStatusCode.MovedPermanently, parsed.StatusCode);
|
||||
Assert.Equal("https://example.com/new", parsed.Value);
|
||||
}
|
||||
|
||||
private sealed record SampleDto(int Id, string Name);
|
||||
}
|
||||
77
src/MaksIT.Results.Tests/ResultFromHttpResponseTests.cs
Normal file
77
src/MaksIT.Results.Tests/ResultFromHttpResponseTests.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using MaksIT.Results.Mvc;
|
||||
|
||||
namespace MaksIT.Results.Tests;
|
||||
|
||||
public class ResultFromHttpResponseTests {
|
||||
[Fact]
|
||||
public async Task FromHttpResponseAsync_WhenOk_ReturnsSuccess() {
|
||||
using var response = new HttpResponseMessage(HttpStatusCode.OK);
|
||||
|
||||
var result = await Result.FromHttpResponseAsync(response, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FromHttpResponseAsync_WhenProblemDetails_ReturnsFailureMessages() {
|
||||
var problem = new ProblemDetails {
|
||||
Status = 404,
|
||||
Title = "An error occurred",
|
||||
Detail = "Missing; Gone"
|
||||
};
|
||||
|
||||
using var response = new HttpResponseMessage(HttpStatusCode.NotFound) {
|
||||
Content = new StringContent(JsonSerializer.Serialize(problem), Encoding.UTF8, "application/problem+json")
|
||||
};
|
||||
|
||||
var result = await Result.FromHttpResponseAsync(response, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result.IsSuccess);
|
||||
Assert.Equal(HttpStatusCode.NotFound, result.StatusCode);
|
||||
Assert.Equal(["Missing", "Gone"], result.Messages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FromHttpResponseAsync_WhenJsonBody_DeserializesValue() {
|
||||
using var response = new HttpResponseMessage(HttpStatusCode.OK) {
|
||||
Content = new StringContent("""{"id":3,"name":"hub"}""", Encoding.UTF8, "application/json")
|
||||
};
|
||||
|
||||
var result = await Result<SampleDto>.FromHttpResponseAsync(response, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(3, result.Value!.Id);
|
||||
Assert.Equal("hub", result.Value.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FromHttpResponseAsync_WhenRedirect_ReturnsLocation() {
|
||||
using var response = new HttpResponseMessage(HttpStatusCode.Found);
|
||||
response.Headers.Location = new Uri("https://accounts.example.com/auth");
|
||||
|
||||
var result = await Result<string>.FromHttpResponseAsync(response, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(HttpStatusCode.Found, result.StatusCode);
|
||||
Assert.Equal("https://accounts.example.com/auth", result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FromHttpResponseAsync_WhenDetailOnlyJson_ParsesWithoutProblemMediaType() {
|
||||
using var response = new HttpResponseMessage(HttpStatusCode.NotFound) {
|
||||
Content = new StringContent("""{"detail":"Invalid or expired code."}""", Encoding.UTF8, "application/json")
|
||||
};
|
||||
|
||||
var result = await Result<string>.FromHttpResponseAsync(response, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result.IsSuccess);
|
||||
Assert.Equal(HttpStatusCode.NotFound, result.StatusCode);
|
||||
Assert.Contains("Invalid or expired code.", result.Messages[0]);
|
||||
}
|
||||
|
||||
private sealed record SampleDto(int Id, string Name);
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MaksIT.Results.Mvc;
|
||||
|
||||
namespace MaksIT.Results.Tests;
|
||||
@ -11,8 +10,8 @@ public class ResultToActionResultTests {
|
||||
|
||||
var actionResult = result.ToActionResult();
|
||||
|
||||
Assert.IsType<StatusCodeResult>(actionResult);
|
||||
var statusCodeResult = (StatusCodeResult)actionResult;
|
||||
var statusCodeResult = Assert.IsType<StatusCodeResult>(actionResult);
|
||||
|
||||
Assert.Equal((int)HttpStatusCode.OK, statusCodeResult.StatusCode);
|
||||
}
|
||||
|
||||
@ -23,11 +22,12 @@ public class ResultToActionResultTests {
|
||||
|
||||
var actionResult = result.ToActionResult();
|
||||
|
||||
Assert.IsType<ObjectResult>(actionResult);
|
||||
var objectResult = (ObjectResult)actionResult;
|
||||
var objectResult = Assert.IsType<ObjectResult>(actionResult);
|
||||
|
||||
Assert.Equal((int)HttpStatusCode.BadRequest, objectResult.StatusCode);
|
||||
Assert.IsType<ProblemDetails>(objectResult.Value);
|
||||
var problemDetails = (ProblemDetails)objectResult.Value!;
|
||||
|
||||
var problemDetails = Assert.IsType<ProblemDetails>(objectResult.Value);
|
||||
|
||||
Assert.Equal((int)HttpStatusCode.BadRequest, problemDetails.Status);
|
||||
Assert.Equal("An error occurred", problemDetails.Title);
|
||||
Assert.Equal(errorMessage, problemDetails.Detail);
|
||||
@ -40,20 +40,21 @@ public class ResultToActionResultTests {
|
||||
|
||||
var actionResult = result.ToActionResult();
|
||||
|
||||
Assert.IsType<ObjectResult>(actionResult);
|
||||
var objectResult = (ObjectResult)actionResult;
|
||||
var objectResult = Assert.IsType<ObjectResult>(actionResult);
|
||||
|
||||
Assert.Equal((int)HttpStatusCode.OK, objectResult.StatusCode);
|
||||
Assert.Equal(value, objectResult.Value);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void ToActionResult_WhenGenericSuccessWithRedirect_ReturnsRedirectResult() {
|
||||
var result = Result<string>.Found("https://example.com/continue");
|
||||
|
||||
var actionResult = result.ToActionResult();
|
||||
|
||||
Assert.IsType<RedirectResult>(actionResult);
|
||||
var redirectResult = (RedirectResult)actionResult;
|
||||
var redirectResult = Assert.IsType<RedirectResult>(actionResult);
|
||||
|
||||
Assert.Equal("https://example.com/continue", redirectResult.Location);
|
||||
Assert.Equal((int)HttpStatusCode.Found, redirectResult.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
|
||||
<!-- NuGet package metadata -->
|
||||
<PackageId>MaksIT.Results</PackageId>
|
||||
<Version>2.0.5</Version>
|
||||
<Version>2.1.0</Version>
|
||||
<Authors>Maksym Sadovnychyy</Authors>
|
||||
<Company>MAKS-IT</Company>
|
||||
<Product>MaksIT.Results</Product>
|
||||
@ -22,6 +22,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MaksIT.Core" Version="1.6.14" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.3.13" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.14" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.14" />
|
||||
@ -31,9 +32,5 @@
|
||||
<None Include="..\..\LICENSE.md" Pack="true" PackagePath="\" Link="LICENSE.md" />
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" Link="README.md" />
|
||||
<None Include="..\..\CHANGELOG.md" Pack="true" PackagePath="\" Link="CHANGELOG.md" />
|
||||
|
||||
<None Include="..\..\assets\badges\**\*" Link="assets\badges\%(RecursiveDir)%(Filename)%(Extension)">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
using System.Text.Json;
|
||||
|
||||
namespace MaksIT.Results.Mvc;
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
namespace MaksIT.Results.Mvc;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A machine-readable format for specifying errors in HTTP API responses based on <see href="https://tools.ietf.org/html/rfc7807"/>.
|
||||
/// </summary>
|
||||
|
||||
@ -2,11 +2,30 @@ using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace MaksIT.Results.Mvc;
|
||||
|
||||
public class RedirectResult(string location) : IActionResult {
|
||||
public string Location { get; } = location;
|
||||
/// <summary>
|
||||
/// HTTP redirect <see cref="IActionResult"/>. Preserves the redirection status code (301/302/303/307/308).
|
||||
/// </summary>
|
||||
public class RedirectResult : IActionResult {
|
||||
/// <summary>Target URL for the <c>Location</c> header.</summary>
|
||||
public string Location { get; }
|
||||
|
||||
/// <summary>HTTP redirection status code.</summary>
|
||||
public int StatusCode { get; }
|
||||
|
||||
/// <summary>Creates a 302 Found redirect.</summary>
|
||||
public RedirectResult(string location) : this(location, (int)System.Net.HttpStatusCode.Found) {
|
||||
}
|
||||
|
||||
/// <summary>Creates a redirect with an explicit status code.</summary>
|
||||
public RedirectResult(string location, int statusCode) {
|
||||
Location = location;
|
||||
StatusCode = statusCode;
|
||||
}
|
||||
|
||||
public Task ExecuteResultAsync(ActionContext context) {
|
||||
context.HttpContext.Response.Redirect(Location);
|
||||
var response = context.HttpContext.Response;
|
||||
response.StatusCode = StatusCode;
|
||||
response.Headers["Location"] = Location;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,17 +1,12 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
|
||||
namespace MaksIT.Results.Mvc;
|
||||
|
||||
public class StatusCodeResult : IActionResult {
|
||||
public int StatusCode { get; }
|
||||
|
||||
public StatusCodeResult(int statusCode) {
|
||||
StatusCode = statusCode;
|
||||
}
|
||||
public class StatusCodeResult(int statusCode) : IActionResult {
|
||||
public int StatusCode { get; } = statusCode;
|
||||
|
||||
public async Task ExecuteResultAsync(ActionContext context) {
|
||||
context.HttpContext.Response.StatusCode = StatusCode;
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
using System.Net;
|
||||
|
||||
|
||||
namespace MaksIT.Results;
|
||||
|
||||
public partial class Result {
|
||||
@ -11,81 +10,71 @@ public partial class Result {
|
||||
/// Returns a result indicating that the server could not understand the request due to invalid syntax.
|
||||
/// Corresponds to HTTP status code 400 Bad Request.
|
||||
/// </summary>
|
||||
public static Result BadRequest(params string [] messages) {
|
||||
return new Result(false, [..messages], HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
public static Result BadRequest(params string [] messages) =>
|
||||
new (false, [..messages], HttpStatusCode.BadRequest);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the client must authenticate itself to get the requested response.
|
||||
/// Corresponds to HTTP status code 401 Unauthorized.
|
||||
/// </summary>
|
||||
public static Result Unauthorized(params string [] messages) {
|
||||
return new Result(false, [..messages], HttpStatusCode.Unauthorized);
|
||||
}
|
||||
public static Result Unauthorized(params string [] messages) =>
|
||||
new (false, [..messages], HttpStatusCode.Unauthorized);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the client does not have access rights to the content.
|
||||
/// Corresponds to HTTP status code 403 Forbidden.
|
||||
/// </summary>
|
||||
public static Result Forbidden(params string [] messages) {
|
||||
return new Result(false, [..messages], HttpStatusCode.Forbidden);
|
||||
}
|
||||
public static Result Forbidden(params string [] messages) =>
|
||||
new (false, [..messages], HttpStatusCode.Forbidden);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server can not find the requested resource.
|
||||
/// Corresponds to HTTP status code 404 Not Found.
|
||||
/// </summary>
|
||||
public static Result NotFound(params string [] messages) {
|
||||
return new Result(false, [.. messages], HttpStatusCode.NotFound);
|
||||
}
|
||||
public static Result NotFound(params string [] messages) =>
|
||||
new (false, [.. messages], HttpStatusCode.NotFound);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the request could not be completed due to a conflict with the current state of the resource.
|
||||
/// Corresponds to HTTP status code 409 Conflict.
|
||||
/// </summary>
|
||||
public static Result Conflict(params string [] messages) {
|
||||
return new Result(false, [..messages], HttpStatusCode.Conflict);
|
||||
}
|
||||
public static Result Conflict(params string [] messages) =>
|
||||
new (false, [..messages], HttpStatusCode.Conflict);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the requested resource is no longer available and will not be available again.
|
||||
/// Corresponds to HTTP status code 410 Gone.
|
||||
/// </summary>
|
||||
public static Result Gone(params string [] messages) {
|
||||
return new Result(false, [..messages], (HttpStatusCode)410); // 410 Gone
|
||||
}
|
||||
public static Result Gone(params string [] messages) =>
|
||||
new (false, [..messages], (HttpStatusCode)410); // 410 Gone
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server cannot process the request because it expects the request to have a defined Content-Length header.
|
||||
/// Corresponds to HTTP status code 411 Length Required.
|
||||
/// </summary>
|
||||
public static Result LengthRequired(params string [] messages) {
|
||||
return new Result(false, [..messages], (HttpStatusCode)411); // 411 Length Required
|
||||
}
|
||||
public static Result LengthRequired(params string [] messages) =>
|
||||
new (false, [..messages], (HttpStatusCode)411); // 411 Length Required
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server cannot process the request entity because it is too large.
|
||||
/// Corresponds to HTTP status code 413 Payload Too Large.
|
||||
/// </summary>
|
||||
public static Result PayloadTooLarge(params string [] messages) {
|
||||
return new Result(false, [..messages], (HttpStatusCode)413); // 413 Payload Too Large
|
||||
}
|
||||
public static Result PayloadTooLarge(params string [] messages) =>
|
||||
new (false, [..messages], (HttpStatusCode)413); // 413 Payload Too Large
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server cannot process the request because the URI is too long.
|
||||
/// Corresponds to HTTP status code 414 URI Too Long.
|
||||
/// </summary>
|
||||
public static Result UriTooLong(params string [] messages) {
|
||||
return new Result(false, [..messages], (HttpStatusCode)414); // 414 URI Too Long
|
||||
}
|
||||
public static Result UriTooLong(params string [] messages) =>
|
||||
new (false, [..messages], (HttpStatusCode)414); // 414 URI Too Long
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server cannot process the request because the media type is unsupported.
|
||||
/// Corresponds to HTTP status code 415 Unsupported Media Type.
|
||||
/// </summary>
|
||||
public static Result UnsupportedMediaType(params string [] messages) {
|
||||
return new Result(false, [..messages], HttpStatusCode.UnsupportedMediaType);
|
||||
}
|
||||
public static Result UnsupportedMediaType(params string [] messages) =>
|
||||
new (false, [..messages], HttpStatusCode.UnsupportedMediaType);
|
||||
|
||||
#endregion
|
||||
|
||||
@ -95,153 +84,134 @@ public partial class Result {
|
||||
/// Returns a result indicating that payment is required to access the requested resource.
|
||||
/// Corresponds to HTTP status code 402 Payment Required.
|
||||
/// </summary>
|
||||
public static Result PaymentRequired(params string [] messages) {
|
||||
return new Result(false, [..messages], (HttpStatusCode)402); // 402 Payment Required
|
||||
}
|
||||
public static Result PaymentRequired(params string [] messages) =>
|
||||
new (false, [..messages], (HttpStatusCode)402); // 402 Payment Required
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the request method is known by the server but is not supported by the target resource.
|
||||
/// Corresponds to HTTP status code 405 Method Not Allowed.
|
||||
/// </summary>
|
||||
public static Result MethodNotAllowed(params string [] messages) {
|
||||
return new Result(false, [..messages], (HttpStatusCode)405); // 405 Method Not Allowed
|
||||
}
|
||||
public static Result MethodNotAllowed(params string [] messages) =>
|
||||
new (false, [..messages], (HttpStatusCode)405); // 405 Method Not Allowed
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server cannot produce a response matching the list of acceptable values defined in the request headers.
|
||||
/// Corresponds to HTTP status code 406 Not Acceptable.
|
||||
/// </summary>
|
||||
public static Result NotAcceptable(params string [] messages) {
|
||||
return new Result(false, [..messages], (HttpStatusCode)406); // 406 Not Acceptable
|
||||
}
|
||||
public static Result NotAcceptable(params string [] messages) =>
|
||||
new (false, [..messages], (HttpStatusCode)406); // 406 Not Acceptable
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that authentication with a proxy is required.
|
||||
/// Corresponds to HTTP status code 407 Proxy Authentication Required.
|
||||
/// </summary>
|
||||
public static Result ProxyAuthenticationRequired(params string [] messages) {
|
||||
return new Result(false, [..messages], (HttpStatusCode)407); // 407 Proxy Authentication Required
|
||||
}
|
||||
public static Result ProxyAuthenticationRequired(params string [] messages) =>
|
||||
new (false, [..messages], (HttpStatusCode)407); // 407 Proxy Authentication Required
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server timed out waiting for the request.
|
||||
/// Corresponds to HTTP status code 408 Request Timeout.
|
||||
/// </summary>
|
||||
public static Result RequestTimeout(params string [] messages) {
|
||||
return new Result(false, [..messages], (HttpStatusCode)408); // 408 Request Timeout
|
||||
}
|
||||
public static Result RequestTimeout(params string [] messages) =>
|
||||
new (false, [..messages], (HttpStatusCode)408); // 408 Request Timeout
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that one or more conditions given in the request header fields evaluated to false.
|
||||
/// Corresponds to HTTP status code 412 Precondition Failed.
|
||||
/// </summary>
|
||||
public static Result PreconditionFailed(params string [] messages) {
|
||||
return new Result(false, [..messages], (HttpStatusCode)412); // 412 Precondition Failed
|
||||
}
|
||||
public static Result PreconditionFailed(params string [] messages) =>
|
||||
new (false, [..messages], (HttpStatusCode)412); // 412 Precondition Failed
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the range specified by the Range header field cannot be fulfilled.
|
||||
/// Corresponds to HTTP status code 416 Range Not Satisfiable.
|
||||
/// </summary>
|
||||
public static Result RangeNotSatisfiable(params string [] messages) {
|
||||
return new Result(false, [..messages], (HttpStatusCode)416); // 416 Range Not Satisfiable
|
||||
}
|
||||
public static Result RangeNotSatisfiable(params string [] messages) =>
|
||||
new (false, [..messages], (HttpStatusCode)416); // 416 Range Not Satisfiable
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the expectation given in the request's Expect header could not be met.
|
||||
/// Corresponds to HTTP status code 417 Expectation Failed.
|
||||
/// </summary>
|
||||
public static Result ExpectationFailed(params string [] messages) {
|
||||
return new Result(false, [..messages], (HttpStatusCode)417); // 417 Expectation Failed
|
||||
}
|
||||
public static Result ExpectationFailed(params string [] messages) =>
|
||||
new (false, [..messages], (HttpStatusCode)417); // 417 Expectation Failed
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server refuses to brew coffee because it is, permanently, a teapot.
|
||||
/// Corresponds to HTTP status code 418 I'm a teapot.
|
||||
/// </summary>
|
||||
public static Result ImATeapot(params string [] messages) {
|
||||
return new Result(false, [..messages], (HttpStatusCode)418); // 418 I'm a teapot
|
||||
}
|
||||
public static Result ImATeapot(params string [] messages) =>
|
||||
new (false, [..messages], (HttpStatusCode)418); // 418 I'm a teapot
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the request was directed at a server that is not able to produce a response.
|
||||
/// Corresponds to HTTP status code 421 Misdirected Request.
|
||||
/// </summary>
|
||||
public static Result MisdirectedRequest(params string [] messages) {
|
||||
return new Result(false, [..messages], (HttpStatusCode)421); // 421 Misdirected Request
|
||||
}
|
||||
public static Result MisdirectedRequest(params string [] messages) =>
|
||||
new (false, [..messages], (HttpStatusCode)421); // 421 Misdirected Request
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server cannot process the request due to an illegal request entity.
|
||||
/// Corresponds to HTTP status code 422 Unprocessable Entity.
|
||||
/// </summary>
|
||||
public static Result UnprocessableEntity(params string [] messages) {
|
||||
return new Result(false, [..messages], (HttpStatusCode)422); // 422 Unprocessable Entity
|
||||
}
|
||||
public static Result UnprocessableEntity(params string [] messages) =>
|
||||
new (false, [..messages], (HttpStatusCode)422); // 422 Unprocessable Entity
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that access to the target resource is denied because the resource is locked.
|
||||
/// Corresponds to HTTP status code 423 Locked.
|
||||
/// </summary>
|
||||
public static Result Locked(params string [] messages) {
|
||||
return new Result(false, [..messages], (HttpStatusCode)423); // 423 Locked
|
||||
}
|
||||
public static Result Locked(params string [] messages) =>
|
||||
new (false, [..messages], (HttpStatusCode)423); // 423 Locked
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the request failed because it depended on another request and that request failed.
|
||||
/// Corresponds to HTTP status code 424 Failed Dependency.
|
||||
/// </summary>
|
||||
public static Result FailedDependency(params string [] messages) {
|
||||
return new Result(false, [..messages], (HttpStatusCode)424); // 424 Failed Dependency
|
||||
}
|
||||
public static Result FailedDependency(params string [] messages) =>
|
||||
new (false, [..messages], (HttpStatusCode)424); // 424 Failed Dependency
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server is unwilling to risk processing a request that might be replayed.
|
||||
/// Corresponds to HTTP status code 425 Too Early.
|
||||
/// </summary>
|
||||
public static Result TooEarly(params string [] messages) {
|
||||
return new Result(false, [..messages], (HttpStatusCode)425); // 425 Too Early
|
||||
}
|
||||
public static Result TooEarly(params string [] messages) =>
|
||||
new (false, [..messages], (HttpStatusCode)425); // 425 Too Early
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server refuses to perform the request using the current protocol.
|
||||
/// Corresponds to HTTP status code 426 Upgrade Required.
|
||||
/// </summary>
|
||||
public static Result UpgradeRequired(params string [] messages) {
|
||||
return new Result(false, [..messages], (HttpStatusCode)426); // 426 Upgrade Required
|
||||
}
|
||||
public static Result UpgradeRequired(params string [] messages) =>
|
||||
new (false, [..messages], (HttpStatusCode)426); // 426 Upgrade Required
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server requires the request to be conditional.
|
||||
/// Corresponds to HTTP status code 428 Precondition Required.
|
||||
/// </summary>
|
||||
public static Result PreconditionRequired(params string [] messages) {
|
||||
return new Result(false, [..messages], (HttpStatusCode)428); // 428 Precondition Required
|
||||
}
|
||||
public static Result PreconditionRequired(params string [] messages) =>
|
||||
new (false, [..messages], (HttpStatusCode)428); // 428 Precondition Required
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the user has sent too many requests in a given amount of time.
|
||||
/// Corresponds to HTTP status code 429 Too Many Requests.
|
||||
/// </summary>
|
||||
public static Result TooManyRequests(params string [] messages) {
|
||||
return new Result(false, [..messages], (HttpStatusCode)429); // 429 Too Many Requests
|
||||
}
|
||||
public static Result TooManyRequests(params string [] messages) =>
|
||||
new (false, [..messages], (HttpStatusCode)429); // 429 Too Many Requests
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server is unwilling to process the request because its header fields are too large.
|
||||
/// Corresponds to HTTP status code 431 Request Header Fields Too Large.
|
||||
/// </summary>
|
||||
public static Result RequestHeaderFieldsTooLarge(params string [] messages) {
|
||||
return new Result(false, [..messages], (HttpStatusCode)431); // 431 Request Header Fields Too Large
|
||||
}
|
||||
public static Result RequestHeaderFieldsTooLarge(params string [] messages) =>
|
||||
new (false, [..messages], (HttpStatusCode)431); // 431 Request Header Fields Too Large
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that access to the requested resource is denied for legal reasons.
|
||||
/// Corresponds to HTTP status code 451 Unavailable For Legal Reasons.
|
||||
/// </summary>
|
||||
public static Result UnavailableForLegalReasons(params string [] messages) {
|
||||
return new Result(false, [..messages], (HttpStatusCode)451); // 451 Unavailable For Legal Reasons
|
||||
}
|
||||
public static Result UnavailableForLegalReasons(params string [] messages) =>
|
||||
new (false, [..messages], (HttpStatusCode)451); // 451 Unavailable For Legal Reasons
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -254,81 +224,71 @@ public partial class Result<T> : Result {
|
||||
/// Returns a result indicating that the server could not understand the request due to invalid syntax.
|
||||
/// Corresponds to HTTP status code 400 Bad Request.
|
||||
/// </summary>
|
||||
public static Result<T> BadRequest(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], HttpStatusCode.BadRequest);
|
||||
}
|
||||
public static Result<T> BadRequest(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], HttpStatusCode.BadRequest);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the client must authenticate itself to get the requested response.
|
||||
/// Corresponds to HTTP status code 401 Unauthorized.
|
||||
/// </summary>
|
||||
public static Result<T> Unauthorized(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], HttpStatusCode.Unauthorized);
|
||||
}
|
||||
public static Result<T> Unauthorized(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], HttpStatusCode.Unauthorized);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the client does not have access rights to the content.
|
||||
/// Corresponds to HTTP status code 403 Forbidden.
|
||||
/// </summary>
|
||||
public static Result<T> Forbidden(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], HttpStatusCode.Forbidden);
|
||||
}
|
||||
public static Result<T> Forbidden(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], HttpStatusCode.Forbidden);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server can not find the requested resource.
|
||||
/// Corresponds to HTTP status code 404 Not Found.
|
||||
/// </summary>
|
||||
public static Result<T> NotFound(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], HttpStatusCode.NotFound);
|
||||
}
|
||||
public static Result<T> NotFound(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], HttpStatusCode.NotFound);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the request could not be completed due to a conflict with the current state of the resource.
|
||||
/// Corresponds to HTTP status code 409 Conflict.
|
||||
/// </summary>
|
||||
public static Result<T> Conflict(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], HttpStatusCode.Conflict);
|
||||
}
|
||||
public static Result<T> Conflict(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], HttpStatusCode.Conflict);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the requested resource is no longer available and will not be available again.
|
||||
/// Corresponds to HTTP status code 410 Gone.
|
||||
/// </summary>
|
||||
public static Result<T> Gone(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], (HttpStatusCode)410); // 410 Gone
|
||||
}
|
||||
public static Result<T> Gone(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], (HttpStatusCode)410); // 410 Gone
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server cannot process the request because it expects the request to have a defined Content-Length header.
|
||||
/// Corresponds to HTTP status code 411 Length Required.
|
||||
/// </summary>
|
||||
public static Result<T> LengthRequired(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], (HttpStatusCode)411); // 411 Length Required
|
||||
}
|
||||
public static Result<T> LengthRequired(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], (HttpStatusCode)411); // 411 Length Required
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server cannot process the request entity because it is too large.
|
||||
/// Corresponds to HTTP status code 413 Payload Too Large.
|
||||
/// </summary>
|
||||
public static Result<T> PayloadTooLarge(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], (HttpStatusCode)413); // 413 Payload Too Large
|
||||
}
|
||||
public static Result<T> PayloadTooLarge(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], (HttpStatusCode)413); // 413 Payload Too Large
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server cannot process the request because the URI is too long.
|
||||
/// Corresponds to HTTP status code 414 URI Too Long.
|
||||
/// </summary>
|
||||
public static Result<T> UriTooLong(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], (HttpStatusCode)414); // 414 URI Too Long
|
||||
}
|
||||
public static Result<T> UriTooLong(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], (HttpStatusCode)414); // 414 URI Too Long
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server cannot process the request because the media type is unsupported.
|
||||
/// Corresponds to HTTP status code 415 Unsupported Media Type.
|
||||
/// </summary>
|
||||
public static Result<T> UnsupportedMediaType(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], HttpStatusCode.UnsupportedMediaType);
|
||||
}
|
||||
public static Result<T> UnsupportedMediaType(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], HttpStatusCode.UnsupportedMediaType);
|
||||
|
||||
#endregion
|
||||
|
||||
@ -338,153 +298,134 @@ public partial class Result<T> : Result {
|
||||
/// Returns a result indicating that payment is required to access the requested resource.
|
||||
/// Corresponds to HTTP status code 402 Payment Required.
|
||||
/// </summary>
|
||||
public static Result<T> PaymentRequired(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], (HttpStatusCode)402); // 402 Payment Required
|
||||
}
|
||||
public static Result<T> PaymentRequired(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], (HttpStatusCode)402); // 402 Payment Required
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the request method is known by the server but is not supported by the target resource.
|
||||
/// Corresponds to HTTP status code 405 Method Not Allowed.
|
||||
/// </summary>
|
||||
public static Result<T> MethodNotAllowed(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], (HttpStatusCode)405); // 405 Method Not Allowed
|
||||
}
|
||||
public static Result<T> MethodNotAllowed(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], (HttpStatusCode)405); // 405 Method Not Allowed
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server cannot produce a response matching the list of acceptable values defined in the request headers.
|
||||
/// Corresponds to HTTP status code 406 Not Acceptable.
|
||||
/// </summary>
|
||||
public static Result<T> NotAcceptable(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], (HttpStatusCode)406); // 406 Not Acceptable
|
||||
}
|
||||
public static Result<T> NotAcceptable(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], (HttpStatusCode)406); // 406 Not Acceptable
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that authentication with a proxy is required.
|
||||
/// Corresponds to HTTP status code 407 Proxy Authentication Required.
|
||||
/// </summary>
|
||||
public static Result<T> ProxyAuthenticationRequired(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], (HttpStatusCode)407); // 407 Proxy Authentication Required
|
||||
}
|
||||
public static Result<T> ProxyAuthenticationRequired(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], (HttpStatusCode)407); // 407 Proxy Authentication Required
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server timed out waiting for the request.
|
||||
/// Corresponds to HTTP status code 408 Request Timeout.
|
||||
/// </summary>
|
||||
public static Result<T> RequestTimeout(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], (HttpStatusCode)408); // 408 Request Timeout
|
||||
}
|
||||
public static Result<T> RequestTimeout(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], (HttpStatusCode)408); // 408 Request Timeout
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that one or more conditions given in the request header fields evaluated to false.
|
||||
/// Corresponds to HTTP status code 412 Precondition Failed.
|
||||
/// </summary>
|
||||
public static Result<T> PreconditionFailed(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], (HttpStatusCode)412); // 412 Precondition Failed
|
||||
}
|
||||
public static Result<T> PreconditionFailed(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], (HttpStatusCode)412); // 412 Precondition Failed
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the range specified by the Range header field cannot be fulfilled.
|
||||
/// Corresponds to HTTP status code 416 Range Not Satisfiable.
|
||||
/// </summary>
|
||||
public static Result<T> RangeNotSatisfiable(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], (HttpStatusCode)416); // 416 Range Not Satisfiable
|
||||
}
|
||||
public static Result<T> RangeNotSatisfiable(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], (HttpStatusCode)416); // 416 Range Not Satisfiable
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the expectation given in the request's Expect header could not be met.
|
||||
/// Corresponds to HTTP status code 417 Expectation Failed.
|
||||
/// </summary>
|
||||
public static Result<T> ExpectationFailed(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], (HttpStatusCode)417); // 417 Expectation Failed
|
||||
}
|
||||
public static Result<T> ExpectationFailed(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], (HttpStatusCode)417); // 417 Expectation Failed
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server refuses to brew coffee because it is, permanently, a teapot.
|
||||
/// Corresponds to HTTP status code 418 I'm a teapot.
|
||||
/// </summary>
|
||||
public static Result<T> ImATeapot(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], (HttpStatusCode)418); // 418 I'm a teapot
|
||||
}
|
||||
public static Result<T> ImATeapot(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], (HttpStatusCode)418); // 418 I'm a teapot
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the request was directed at a server that is not able to produce a response.
|
||||
/// Corresponds to HTTP status code 421 Misdirected Request.
|
||||
/// </summary>
|
||||
public static Result<T> MisdirectedRequest(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], (HttpStatusCode)421); // 421 Misdirected Request
|
||||
}
|
||||
public static Result<T> MisdirectedRequest(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], (HttpStatusCode)421); // 421 Misdirected Request
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server cannot process the request due to an illegal request entity.
|
||||
/// Corresponds to HTTP status code 422 Unprocessable Entity.
|
||||
/// </summary>
|
||||
public static Result<T> UnprocessableEntity(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], (HttpStatusCode)422); // 422 Unprocessable Entity
|
||||
}
|
||||
public static Result<T> UnprocessableEntity(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], (HttpStatusCode)422); // 422 Unprocessable Entity
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that access to the target resource is denied because the resource is locked.
|
||||
/// Corresponds to HTTP status code 423 Locked.
|
||||
/// </summary>
|
||||
public static Result<T> Locked(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], (HttpStatusCode)423); // 423 Locked
|
||||
}
|
||||
public static Result<T> Locked(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], (HttpStatusCode)423); // 423 Locked
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the request failed because it depended on another request and that request failed.
|
||||
/// Corresponds to HTTP status code 424 Failed Dependency.
|
||||
/// </summary>
|
||||
public static Result<T> FailedDependency(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], (HttpStatusCode)424); // 424 Failed Dependency
|
||||
}
|
||||
public static Result<T> FailedDependency(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], (HttpStatusCode)424); // 424 Failed Dependency
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server is unwilling to risk processing a request that might be replayed.
|
||||
/// Corresponds to HTTP status code 425 Too Early.
|
||||
/// </summary>
|
||||
public static Result<T> TooEarly(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], (HttpStatusCode)425); // 425 Too Early
|
||||
}
|
||||
public static Result<T> TooEarly(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], (HttpStatusCode)425); // 425 Too Early
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server refuses to perform the request using the current protocol.
|
||||
/// Corresponds to HTTP status code 426 Upgrade Required.
|
||||
/// </summary>
|
||||
public static Result<T> UpgradeRequired(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], (HttpStatusCode)426); // 426 Upgrade Required
|
||||
}
|
||||
public static Result<T> UpgradeRequired(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], (HttpStatusCode)426); // 426 Upgrade Required
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server requires the request to be conditional.
|
||||
/// Corresponds to HTTP status code 428 Precondition Required.
|
||||
/// </summary>
|
||||
public static Result<T> PreconditionRequired(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], (HttpStatusCode)428); // 428 Precondition Required
|
||||
}
|
||||
public static Result<T> PreconditionRequired(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], (HttpStatusCode)428); // 428 Precondition Required
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the user has sent too many requests in a given amount of time.
|
||||
/// Corresponds to HTTP status code 429 Too Many Requests.
|
||||
/// </summary>
|
||||
public static Result<T> TooManyRequests(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], (HttpStatusCode)429); // 429 Too Many Requests
|
||||
}
|
||||
public static Result<T> TooManyRequests(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], (HttpStatusCode)429); // 429 Too Many Requests
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server is unwilling to process the request because its header fields are too large.
|
||||
/// Corresponds to HTTP status code 431 Request Header Fields Too Large.
|
||||
/// </summary>
|
||||
public static Result<T> RequestHeaderFieldsTooLarge(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], (HttpStatusCode)431); // 431 Request Header Fields Too Large
|
||||
}
|
||||
public static Result<T> RequestHeaderFieldsTooLarge(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], (HttpStatusCode)431); // 431 Request Header Fields Too Large
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that access to the requested resource is denied for legal reasons.
|
||||
/// Corresponds to HTTP status code 451 Unavailable For Legal Reasons.
|
||||
/// </summary>
|
||||
public static Result<T> UnavailableForLegalReasons(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], (HttpStatusCode)451); // 451 Unavailable For Legal Reasons
|
||||
}
|
||||
public static Result<T> UnavailableForLegalReasons(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], (HttpStatusCode)451); // 451 Unavailable For Legal Reasons
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
253
src/MaksIT.Results/Result.Http.cs
Normal file
253
src/MaksIT.Results/Result.Http.cs
Normal file
@ -0,0 +1,253 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MaksIT.Core.Extensions;
|
||||
using MaksIT.Results.Mvc;
|
||||
|
||||
namespace MaksIT.Results;
|
||||
|
||||
public partial class Result {
|
||||
/// <summary>
|
||||
/// Reconstructs a <see cref="Result"/> from an <see cref="IActionResult"/> produced by <see cref="ToActionResult"/>.
|
||||
/// </summary>
|
||||
public static Result FromActionResult(IActionResult actionResult) {
|
||||
ArgumentNullException.ThrowIfNull(actionResult);
|
||||
|
||||
switch (actionResult) {
|
||||
case StatusCodeResult statusCodeResult:
|
||||
return new (true, [], (HttpStatusCode)statusCodeResult.StatusCode);
|
||||
|
||||
case RedirectResult redirectResult:
|
||||
return new (true, [redirectResult.Location], (HttpStatusCode)redirectResult.StatusCode);
|
||||
|
||||
case ObjectResult { Value: ProblemDetails problem } objectResult:
|
||||
return FromProblemDetails(problem, objectResult.StatusCode);
|
||||
|
||||
case ObjectResult objectResult:
|
||||
var status = (HttpStatusCode)(objectResult.StatusCode ?? (int)HttpStatusCode.OK);
|
||||
if (IsSuccessStatusCode(status))
|
||||
return new (true, [], status);
|
||||
|
||||
return new (false, ["Response indicated failure without ProblemDetails."], status);
|
||||
|
||||
default:
|
||||
return InternalServerError($"Unsupported IActionResult type: {actionResult.GetType().FullName}.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reconstructs a <see cref="Result"/> from an HTTP response written in the <see cref="ToActionResult"/> wire shape
|
||||
/// (status-only success, <c>application/problem+json</c> failures).
|
||||
/// </summary>
|
||||
public static Task<Result> FromHttpResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken = default) {
|
||||
ArgumentNullException.ThrowIfNull(response);
|
||||
return FromHttpResponseCoreAsync(response, cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task<Result> FromHttpResponseCoreAsync(HttpResponseMessage response, CancellationToken cancellationToken) {
|
||||
var status = response.StatusCode;
|
||||
|
||||
if (IsRedirectionStatusCode(status)) {
|
||||
var location = response.Headers.Location?.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(location))
|
||||
return new (true, [location], status);
|
||||
|
||||
return new (false, ["Redirect response missing Location header."], status);
|
||||
}
|
||||
|
||||
if (IsSuccessStatusCode(status))
|
||||
return new (true, [], status);
|
||||
|
||||
return await FailFromHttpContentAsync(response, status, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal static bool IsRedirectionStatusCode(HttpStatusCode statusCode) =>
|
||||
statusCode is HttpStatusCode.MultipleChoices
|
||||
or HttpStatusCode.MovedPermanently
|
||||
or HttpStatusCode.Found
|
||||
or HttpStatusCode.SeeOther
|
||||
or HttpStatusCode.TemporaryRedirect
|
||||
or HttpStatusCode.PermanentRedirect;
|
||||
|
||||
internal static bool IsSuccessStatusCode(HttpStatusCode statusCode) {
|
||||
var code = (int)statusCode;
|
||||
return code is >= 200 and <= 299;
|
||||
}
|
||||
|
||||
internal static Result Create(bool isSuccess, List<string> messages, HttpStatusCode statusCode) =>
|
||||
new(isSuccess, messages, statusCode);
|
||||
|
||||
internal static Result FromProblemDetails(ProblemDetails problem, int? statusCodeOverride) {
|
||||
var status = (HttpStatusCode)(statusCodeOverride ?? problem.Status ?? (int)HttpStatusCode.InternalServerError);
|
||||
var messages = ReadProblemMessages(problem);
|
||||
return Create(false, messages, status);
|
||||
}
|
||||
|
||||
internal static List<string> ReadProblemMessages(ProblemDetails problem) {
|
||||
if (!string.IsNullOrWhiteSpace(problem.Detail)) {
|
||||
var parts = problem.Detail.Split("; ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (parts.Length > 0)
|
||||
return [.. parts];
|
||||
|
||||
return [problem.Detail];
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(problem.Title))
|
||||
return [problem.Title];
|
||||
|
||||
return ["An error occurred"];
|
||||
}
|
||||
|
||||
internal static async Task<Result> FailFromHttpContentAsync(
|
||||
HttpResponseMessage response,
|
||||
HttpStatusCode status,
|
||||
CancellationToken cancellationToken) {
|
||||
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (TryReadProblemDetails(response.Content.Headers.ContentType?.MediaType, body, out var problem))
|
||||
return FromProblemDetails(problem, (int)status);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
return new (false, [$"HTTP {(int)status}."], status);
|
||||
|
||||
return new (false, [Truncate($"HTTP {(int)status}. Response: {body}")], status);
|
||||
}
|
||||
|
||||
internal static bool TryReadProblemDetails(string? mediaType, string body, out ProblemDetails problem) {
|
||||
problem = null!;
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
return false;
|
||||
|
||||
var isProblemJson = mediaType is not null
|
||||
&& mediaType.Equals("application/problem+json", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!isProblemJson && !LooksLikeJsonObject(body))
|
||||
return false;
|
||||
|
||||
try {
|
||||
var parsed = body.ToObject<ProblemDetails>();
|
||||
if (parsed is null)
|
||||
return false;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(parsed.Detail) && string.IsNullOrWhiteSpace(parsed.Title) && parsed.Status is null)
|
||||
return false;
|
||||
|
||||
problem = parsed;
|
||||
return true;
|
||||
}
|
||||
catch (JsonException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool LooksLikeJsonObject(string body) {
|
||||
var trimmed = body.AsSpan().TrimStart();
|
||||
return trimmed.Length > 0 && trimmed[0] == '{';
|
||||
}
|
||||
|
||||
internal static string Truncate(string value) {
|
||||
const int maxLen = 500;
|
||||
return value.Length <= maxLen ? value : value[..maxLen] + "…";
|
||||
}
|
||||
}
|
||||
|
||||
public partial class Result<T> {
|
||||
/// <summary>
|
||||
/// Reconstructs a <see cref="Result{T}"/> from an <see cref="IActionResult"/> produced by <see cref="ToActionResult"/>.
|
||||
/// </summary>
|
||||
public new static Result<T> FromActionResult(IActionResult actionResult) {
|
||||
ArgumentNullException.ThrowIfNull(actionResult);
|
||||
|
||||
switch (actionResult) {
|
||||
case RedirectResult redirectResult when typeof(T) == typeof(string):
|
||||
return new ((T)(object)redirectResult.Location, true, [], (HttpStatusCode)redirectResult.StatusCode);
|
||||
|
||||
case RedirectResult redirectResult:
|
||||
return new (default, false, ["Redirect Location cannot be mapped to the requested result type."], (HttpStatusCode)redirectResult.StatusCode);
|
||||
|
||||
case StatusCodeResult statusCodeResult:
|
||||
return new (default, true, [], (HttpStatusCode)statusCodeResult.StatusCode);
|
||||
|
||||
case ObjectResult { Value: ProblemDetails problem } objectResult:
|
||||
var failed = Result.FromProblemDetails(problem, objectResult.StatusCode);
|
||||
return new (default, false, failed.Messages, failed.StatusCode);
|
||||
|
||||
case ObjectResult { Value: T value } objectResult:
|
||||
return new (value, true, [], (HttpStatusCode)(objectResult.StatusCode ?? (int)HttpStatusCode.OK));
|
||||
|
||||
case ObjectResult { Value: not null } objectResult:
|
||||
var status = (HttpStatusCode)(objectResult.StatusCode ?? (int)HttpStatusCode.OK);
|
||||
if (!Result.IsSuccessStatusCode(status)) {
|
||||
var failure = Result.Create(false, ["Response indicated failure without ProblemDetails."], status);
|
||||
return new (default, false, failure.Messages, failure.StatusCode);
|
||||
}
|
||||
|
||||
try {
|
||||
var converted = objectResult.Value.ToJson().ToObject<T>();
|
||||
return new (converted, true, [], status);
|
||||
}
|
||||
catch (JsonException ex) {
|
||||
return new (default, false, [$"Could not map ObjectResult value to {typeof(T).Name}: {ex.Message}"], status);
|
||||
}
|
||||
|
||||
case ObjectResult objectResult:
|
||||
return new (default, true, [], (HttpStatusCode)(objectResult.StatusCode ?? (int)HttpStatusCode.OK));
|
||||
|
||||
default:
|
||||
return new (default, false, [$"Unsupported IActionResult type: {actionResult.GetType().FullName}."], HttpStatusCode.InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reconstructs a <see cref="Result{T}"/> from an HTTP response written in the <see cref="Result.ToActionResult"/> wire shape
|
||||
/// (JSON body, redirect <c>Location</c>, or <c>application/problem+json</c>).
|
||||
/// </summary>
|
||||
public new static Task<Result<T>> FromHttpResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken = default) {
|
||||
ArgumentNullException.ThrowIfNull(response);
|
||||
return FromHttpResponseCoreAsync(response, cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task<Result<T>> FromHttpResponseCoreAsync(HttpResponseMessage response, CancellationToken cancellationToken) {
|
||||
var status = response.StatusCode;
|
||||
|
||||
if (Result.IsRedirectionStatusCode(status)) {
|
||||
var location = response.Headers.Location?.ToString();
|
||||
if (typeof(T) == typeof(string) && !string.IsNullOrWhiteSpace(location))
|
||||
return new ((T)(object)location, true, [], status);
|
||||
|
||||
return new (default, false, [
|
||||
string.IsNullOrWhiteSpace(location)
|
||||
? "Redirect response missing Location header."
|
||||
: "Redirect Location cannot be mapped to the requested result type."
|
||||
], status);
|
||||
}
|
||||
|
||||
if (!Result.IsSuccessStatusCode(status)) {
|
||||
var failed = await Result.FailFromHttpContentAsync(response, status, cancellationToken).ConfigureAwait(false);
|
||||
return new (default, false, failed.Messages, failed.StatusCode);
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(object))
|
||||
return new (default, true, [], status);
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (typeof(T) == typeof(string)) {
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
return new (default, false, ["Response body was empty."], status);
|
||||
|
||||
return new ((T)(object)body, true, [], status);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
return new (default, true, [], status);
|
||||
|
||||
try {
|
||||
var value = body.ToObject<T>();
|
||||
return new (value, true, [], status);
|
||||
}
|
||||
catch (JsonException ex) {
|
||||
return new (default, false, [$"Could not deserialize response as {typeof(T).Name}: {ex.Message}"], status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
using System.Net;
|
||||
|
||||
|
||||
namespace MaksIT.Results;
|
||||
|
||||
public partial class Result {
|
||||
@ -11,17 +10,15 @@ public partial class Result {
|
||||
/// Returns a result indicating that the initial part of a request has been received and the client should continue with the request.
|
||||
/// Corresponds to HTTP status code 100 Continue.
|
||||
/// </summary>
|
||||
public static Result Continue(params string [] messages) {
|
||||
return new Result(true, [..messages], HttpStatusCode.Continue);
|
||||
}
|
||||
public static Result Continue(params string [] messages) =>
|
||||
new (true, [..messages], HttpStatusCode.Continue);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server is switching to a different protocol as requested by the client.
|
||||
/// Corresponds to HTTP status code 101 Switching Protocols.
|
||||
/// </summary>
|
||||
public static Result SwitchingProtocols(params string [] messages) {
|
||||
return new Result(true, [..messages], HttpStatusCode.SwitchingProtocols);
|
||||
}
|
||||
public static Result SwitchingProtocols(params string [] messages) =>
|
||||
new (true, [..messages], HttpStatusCode.SwitchingProtocols);
|
||||
|
||||
#endregion
|
||||
|
||||
@ -31,17 +28,15 @@ public partial class Result {
|
||||
/// Returns a result indicating that the server has received and is processing the request, but no response is available yet.
|
||||
/// Corresponds to HTTP status code 102 Processing.
|
||||
/// </summary>
|
||||
public static Result Processing(params string [] messages) {
|
||||
return new Result(true, [..messages], HttpStatusCode.Processing);
|
||||
}
|
||||
public static Result Processing(params string [] messages) =>
|
||||
new (true, [..messages], HttpStatusCode.Processing);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server is sending information about early hints that may be used by the client to begin preloading resources while the server prepares a final response.
|
||||
/// Corresponds to HTTP status code 103 Early Hints.
|
||||
/// </summary>
|
||||
public static Result EarlyHints(params string [] messages) {
|
||||
return new Result(true, [..messages], (HttpStatusCode)103); // Early Hints is not defined in HttpStatusCode enum, 103 is the official code
|
||||
}
|
||||
public static Result EarlyHints(params string [] messages) =>
|
||||
new (true, [..messages], (HttpStatusCode)103); // Early Hints is not defined in HttpStatusCode enum, 103 is the official code
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -54,17 +49,15 @@ public partial class Result<T> : Result {
|
||||
/// Returns a result indicating that the initial part of a request has been received and the client should continue with the request.
|
||||
/// Corresponds to HTTP status code 100 Continue.
|
||||
/// </summary>
|
||||
public static Result<T> Continue(T? value, params string [] messages) {
|
||||
return new Result<T>(value, true, [..messages], HttpStatusCode.Continue);
|
||||
}
|
||||
public static Result<T> Continue(T? value, params string [] messages) =>
|
||||
new (value, true, [..messages], HttpStatusCode.Continue);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server is switching to a different protocol as requested by the client.
|
||||
/// Corresponds to HTTP status code 101 Switching Protocols.
|
||||
/// </summary>
|
||||
public static Result<T> SwitchingProtocols(T? value, params string [] messages) {
|
||||
return new Result<T>(value, true, [..messages], HttpStatusCode.SwitchingProtocols);
|
||||
}
|
||||
public static Result<T> SwitchingProtocols(T? value, params string [] messages) =>
|
||||
new (value, true, [..messages], HttpStatusCode.SwitchingProtocols);
|
||||
|
||||
#endregion
|
||||
|
||||
@ -74,17 +67,15 @@ public partial class Result<T> : Result {
|
||||
/// Returns a result indicating that the server has received and is processing the request, but no response is available yet.
|
||||
/// Corresponds to HTTP status code 102 Processing.
|
||||
/// </summary>
|
||||
public static Result<T> Processing(T? value, params string [] messages) {
|
||||
return new Result<T>(value, true, [..messages], HttpStatusCode.Processing);
|
||||
}
|
||||
public static Result<T> Processing(T? value, params string [] messages) =>
|
||||
new (value, true, [..messages], HttpStatusCode.Processing);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the server is sending information about early hints that may be used by the client to begin preloading resources while the server prepares a final response.
|
||||
/// Corresponds to HTTP status code 103 Early Hints.
|
||||
/// </summary>
|
||||
public static Result<T> EarlyHints(T? value, params string [] messages) {
|
||||
return new Result<T>(value, true, [..messages], (HttpStatusCode)103); // Early Hints is not defined in HttpStatusCode enum, 103 is the official code
|
||||
}
|
||||
public static Result<T> EarlyHints(T? value, params string [] messages) =>
|
||||
new (value, true, [..messages], (HttpStatusCode)103); // Early Hints is not defined in HttpStatusCode enum, 103 is the official code
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
using System.Net;
|
||||
|
||||
using System.Net;
|
||||
|
||||
namespace MaksIT.Results;
|
||||
|
||||
@ -9,65 +8,57 @@ public partial class Result {
|
||||
/// Returns a result indicating that the request has multiple options, and the user or user-agent should select one of them.
|
||||
/// Corresponds to HTTP status code 300 Multiple Choices.
|
||||
/// </summary>
|
||||
public static Result MultipleChoices(params string [] messages) {
|
||||
return new Result(true, [..messages], HttpStatusCode.MultipleChoices);
|
||||
}
|
||||
public static Result MultipleChoices(params string [] messages) =>
|
||||
new (true, [..messages], HttpStatusCode.MultipleChoices);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the requested resource has been permanently moved to a new URI.
|
||||
/// Corresponds to HTTP status code 301 Moved Permanently.
|
||||
/// </summary>
|
||||
public static Result MovedPermanently(params string [] messages) {
|
||||
return new Result(true, [..messages], HttpStatusCode.MovedPermanently);
|
||||
}
|
||||
public static Result MovedPermanently(params string [] messages) =>
|
||||
new (true, [..messages], HttpStatusCode.MovedPermanently);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the requested resource resides temporarily under a different URI.
|
||||
/// Corresponds to HTTP status code 302 Found.
|
||||
/// </summary>
|
||||
public static Result Found(params string [] messages) {
|
||||
return new Result(true, [..messages], HttpStatusCode.Found);
|
||||
}
|
||||
public static Result Found(params string [] messages) =>
|
||||
new (true, [..messages], HttpStatusCode.Found);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the response to the request can be found under another URI using the GET method.
|
||||
/// Corresponds to HTTP status code 303 See Other.
|
||||
/// </summary>
|
||||
public static Result SeeOther(params string [] messages) {
|
||||
return new Result(true, [..messages], HttpStatusCode.SeeOther);
|
||||
}
|
||||
public static Result SeeOther(params string [] messages) =>
|
||||
new (true, [..messages], HttpStatusCode.SeeOther);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the requested resource has not been modified since the last request.
|
||||
/// Corresponds to HTTP status code 304 Not Modified.
|
||||
/// </summary>
|
||||
public static Result NotModified(params string [] messages) {
|
||||
return new Result(true, [..messages], HttpStatusCode.NotModified);
|
||||
}
|
||||
public static Result NotModified(params string [] messages) =>
|
||||
new (true, [..messages], HttpStatusCode.NotModified);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the requested resource must be accessed through the proxy given by the location field.
|
||||
/// Corresponds to HTTP status code 305 Use Proxy.
|
||||
/// </summary>
|
||||
public static Result UseProxy(params string [] messages) {
|
||||
return new Result(true, [..messages], HttpStatusCode.UseProxy);
|
||||
}
|
||||
public static Result UseProxy(params string [] messages) =>
|
||||
new (true, [..messages], HttpStatusCode.UseProxy);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the requested resource resides temporarily under a different URI, but future requests should still use the original URI.
|
||||
/// Corresponds to HTTP status code 307 Temporary Redirect.
|
||||
/// </summary>
|
||||
public static Result TemporaryRedirect(params string [] messages) {
|
||||
return new Result(true, [..messages], HttpStatusCode.TemporaryRedirect);
|
||||
}
|
||||
public static Result TemporaryRedirect(params string [] messages) =>
|
||||
new (true, [..messages], HttpStatusCode.TemporaryRedirect);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the requested resource has been permanently moved to a new URI, and future references should use the new URI.
|
||||
/// Corresponds to HTTP status code 308 Permanent Redirect.
|
||||
/// </summary>
|
||||
public static Result PermanentRedirect(params string [] messages) {
|
||||
return new Result(true, [..messages], HttpStatusCode.PermanentRedirect);
|
||||
}
|
||||
public static Result PermanentRedirect(params string [] messages) =>
|
||||
new (true, [..messages], HttpStatusCode.PermanentRedirect);
|
||||
}
|
||||
|
||||
public partial class Result<T> : Result {
|
||||
@ -76,63 +67,55 @@ public partial class Result<T> : Result {
|
||||
/// Returns a result indicating that the request has multiple options, and the user or user-agent should select one of them.
|
||||
/// Corresponds to HTTP status code 300 Multiple Choices.
|
||||
/// </summary>
|
||||
public static Result<T> MultipleChoices(T? value, params string [] messages) {
|
||||
return new Result<T>(value, true, [..messages], HttpStatusCode.MultipleChoices);
|
||||
}
|
||||
public static Result<T> MultipleChoices(T? value, params string [] messages) =>
|
||||
new (value, true, [..messages], HttpStatusCode.MultipleChoices);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the requested resource has been permanently moved to a new URI.
|
||||
/// Corresponds to HTTP status code 301 Moved Permanently.
|
||||
/// </summary>
|
||||
public static Result<T> MovedPermanently(T? value, params string [] messages) {
|
||||
return new Result<T>(value, true, [..messages], HttpStatusCode.MovedPermanently);
|
||||
}
|
||||
public static Result<T> MovedPermanently(T? value, params string [] messages) =>
|
||||
new (value, true, [..messages], HttpStatusCode.MovedPermanently);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the requested resource resides temporarily under a different URI.
|
||||
/// Corresponds to HTTP status code 302 Found.
|
||||
/// </summary>
|
||||
public static Result<T> Found(T? value, params string [] messages) {
|
||||
return new Result<T>(value, true, [..messages], HttpStatusCode.Found);
|
||||
}
|
||||
public static Result<T> Found(T? value, params string [] messages) =>
|
||||
new (value, true, [..messages], HttpStatusCode.Found);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the response to the request can be found under another URI using the GET method.
|
||||
/// Corresponds to HTTP status code 303 See Other.
|
||||
/// </summary>
|
||||
public static Result<T> SeeOther(T? value, params string [] messages) {
|
||||
return new Result<T>(value, true, [..messages], HttpStatusCode.SeeOther);
|
||||
}
|
||||
public static Result<T> SeeOther(T? value, params string [] messages) =>
|
||||
new (value, true, [..messages], HttpStatusCode.SeeOther);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the requested resource has not been modified since the last request.
|
||||
/// Corresponds to HTTP status code 304 Not Modified.
|
||||
/// </summary>
|
||||
public static Result<T> NotModified(T? value, params string [] messages) {
|
||||
return new Result<T>(value, true, [..messages], HttpStatusCode.NotModified);
|
||||
}
|
||||
public static Result<T> NotModified(T? value, params string [] messages) =>
|
||||
new (value, true, [..messages], HttpStatusCode.NotModified);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the requested resource must be accessed through the proxy given by the location field.
|
||||
/// Corresponds to HTTP status code 305 Use Proxy.
|
||||
/// </summary>
|
||||
public static Result<T> UseProxy(T? value, params string [] messages) {
|
||||
return new Result<T>(value, true, [..messages], HttpStatusCode.UseProxy);
|
||||
}
|
||||
public static Result<T> UseProxy(T? value, params string [] messages) =>
|
||||
new (value, true, [..messages], HttpStatusCode.UseProxy);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the requested resource resides temporarily under a different URI, but future requests should still use the original URI.
|
||||
/// Corresponds to HTTP status code 307 Temporary Redirect.
|
||||
/// </summary>
|
||||
public static Result<T> TemporaryRedirect(T? value, params string [] messages) {
|
||||
return new Result<T>(value, true, [..messages], HttpStatusCode.TemporaryRedirect);
|
||||
}
|
||||
public static Result<T> TemporaryRedirect(T? value, params string [] messages) =>
|
||||
new (value, true, [..messages], HttpStatusCode.TemporaryRedirect);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating that the requested resource has been permanently moved to a new URI, and future references should use the new URI.
|
||||
/// Corresponds to HTTP status code 308 Permanent Redirect.
|
||||
/// </summary>
|
||||
public static Result<T> PermanentRedirect(T? value, params string [] messages) {
|
||||
return new Result<T>(value, true, [..messages], HttpStatusCode.PermanentRedirect);
|
||||
}
|
||||
public static Result<T> PermanentRedirect(T? value, params string [] messages) =>
|
||||
new (value, true, [..messages], HttpStatusCode.PermanentRedirect);
|
||||
}
|
||||
|
||||
@ -10,49 +10,43 @@ public partial class Result {
|
||||
/// Returns a result indicating the server encountered an unexpected condition that prevented it from fulfilling the request.
|
||||
/// Corresponds to HTTP status code 500 Internal Server Error.
|
||||
/// </summary>
|
||||
public static Result InternalServerError(params string [] messages) {
|
||||
return new Result(false, [..messages], HttpStatusCode.InternalServerError);
|
||||
}
|
||||
public static Result InternalServerError(params string [] messages) =>
|
||||
new (false, [..messages], HttpStatusCode.InternalServerError);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the server does not support the functionality required to fulfill the request.
|
||||
/// Corresponds to HTTP status code 501 Not Implemented.
|
||||
/// </summary>
|
||||
public static Result NotImplemented(params string [] messages) {
|
||||
return new Result(false, [..messages], HttpStatusCode.NotImplemented);
|
||||
}
|
||||
public static Result NotImplemented(params string [] messages) =>
|
||||
new (false, [..messages], HttpStatusCode.NotImplemented);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the server, while acting as a gateway or proxy, received an invalid response from the upstream server.
|
||||
/// Corresponds to HTTP status code 502 Bad Gateway.
|
||||
/// </summary>
|
||||
public static Result BadGateway(params string [] messages) {
|
||||
return new Result(false, [..messages], HttpStatusCode.BadGateway);
|
||||
}
|
||||
public static Result BadGateway(params string [] messages) =>
|
||||
new (false, [..messages], HttpStatusCode.BadGateway);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the server is currently unable to handle the request due to temporary overload or maintenance of the server.
|
||||
/// Corresponds to HTTP status code 503 Service Unavailable.
|
||||
/// </summary>
|
||||
public static Result ServiceUnavailable(params string [] messages) {
|
||||
return new Result(false, [..messages], HttpStatusCode.ServiceUnavailable);
|
||||
}
|
||||
public static Result ServiceUnavailable(params string [] messages) =>
|
||||
new (false, [..messages], HttpStatusCode.ServiceUnavailable);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the server, while acting as a gateway or proxy, did not receive a timely response from the upstream server.
|
||||
/// Corresponds to HTTP status code 504 Gateway Timeout.
|
||||
/// </summary>
|
||||
public static Result GatewayTimeout(params string [] messages) {
|
||||
return new Result(false, [..messages], HttpStatusCode.GatewayTimeout);
|
||||
}
|
||||
public static Result GatewayTimeout(params string [] messages) =>
|
||||
new (false, [..messages], HttpStatusCode.GatewayTimeout);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the server does not support the HTTP protocol version used in the request.
|
||||
/// Corresponds to HTTP status code 505 HTTP Version Not Supported.
|
||||
/// </summary>
|
||||
public static Result HttpVersionNotSupported(params string [] messages) {
|
||||
return new Result(false, [..messages], HttpStatusCode.HttpVersionNotSupported);
|
||||
}
|
||||
public static Result HttpVersionNotSupported(params string [] messages) =>
|
||||
new (false, [..messages], HttpStatusCode.HttpVersionNotSupported);
|
||||
|
||||
#endregion
|
||||
|
||||
@ -62,41 +56,36 @@ public partial class Result {
|
||||
/// Returns a result indicating the server has an internal configuration error: the chosen variant resource is configured to engage in transparent content negotiation itself, and is therefore not a proper end point in the negotiation process.
|
||||
/// Corresponds to HTTP status code 506 Variant Also Negotiates.
|
||||
/// </summary>
|
||||
public static Result VariantAlsoNegotiates(params string [] messages) {
|
||||
return new Result(false, [..messages], HttpStatusCode.VariantAlsoNegotiates);
|
||||
}
|
||||
public static Result VariantAlsoNegotiates(params string [] messages) =>
|
||||
new (false, [..messages], HttpStatusCode.VariantAlsoNegotiates);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the server is unable to store the representation needed to complete the request.
|
||||
/// Corresponds to HTTP status code 507 Insufficient Storage.
|
||||
/// </summary>
|
||||
public static Result InsufficientStorage(params string [] messages) {
|
||||
return new Result(false, [..messages], HttpStatusCode.InsufficientStorage);
|
||||
}
|
||||
public static Result InsufficientStorage(params string [] messages) =>
|
||||
new (false, [..messages], HttpStatusCode.InsufficientStorage);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the server detected an infinite loop while processing a request with depth: infinity. Usually encountered in WebDAV scenarios.
|
||||
/// Corresponds to HTTP status code 508 Loop Detected.
|
||||
/// </summary>
|
||||
public static Result LoopDetected(params string [] messages) {
|
||||
return new Result(false, [..messages], HttpStatusCode.LoopDetected);
|
||||
}
|
||||
public static Result LoopDetected(params string [] messages) =>
|
||||
new (false, [..messages], HttpStatusCode.LoopDetected);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating further extensions to the request are required for the server to fulfill it.
|
||||
/// Corresponds to HTTP status code 510 Not Extended.
|
||||
/// </summary>
|
||||
public static Result NotExtended(params string [] messages) {
|
||||
return new Result(false, [..messages], HttpStatusCode.NotExtended);
|
||||
}
|
||||
public static Result NotExtended(params string [] messages) =>
|
||||
new (false, [..messages], HttpStatusCode.NotExtended);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the client needs to authenticate to gain network access.
|
||||
/// Corresponds to HTTP status code 511 Network Authentication Required.
|
||||
/// </summary>
|
||||
public static Result NetworkAuthenticationRequired(params string [] messages) {
|
||||
return new Result(false, [..messages], HttpStatusCode.NetworkAuthenticationRequired);
|
||||
}
|
||||
public static Result NetworkAuthenticationRequired(params string [] messages) =>
|
||||
new (false, [..messages], HttpStatusCode.NetworkAuthenticationRequired);
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -109,49 +98,43 @@ public partial class Result<T> : Result {
|
||||
/// Returns a result indicating the server encountered an unexpected condition that prevented it from fulfilling the request.
|
||||
/// Corresponds to HTTP status code 500 Internal Server Error.
|
||||
/// </summary>
|
||||
public static Result<T> InternalServerError(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], HttpStatusCode.InternalServerError);
|
||||
}
|
||||
public static Result<T> InternalServerError(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], HttpStatusCode.InternalServerError);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the server does not support the functionality required to fulfill the request.
|
||||
/// Corresponds to HTTP status code 501 Not Implemented.
|
||||
/// </summary>
|
||||
public static Result<T> NotImplemented(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], HttpStatusCode.NotImplemented);
|
||||
}
|
||||
public static Result<T> NotImplemented(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], HttpStatusCode.NotImplemented);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the server, while acting as a gateway or proxy, received an invalid response from the upstream server.
|
||||
/// Corresponds to HTTP status code 502 Bad Gateway.
|
||||
/// </summary>
|
||||
public static Result<T> BadGateway(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], HttpStatusCode.BadGateway);
|
||||
}
|
||||
public static Result<T> BadGateway(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], HttpStatusCode.BadGateway);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the server is currently unable to handle the request due to temporary overload or maintenance of the server.
|
||||
/// Corresponds to HTTP status code 503 Service Unavailable.
|
||||
/// </summary>
|
||||
public static Result<T> ServiceUnavailable(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], HttpStatusCode.ServiceUnavailable);
|
||||
}
|
||||
public static Result<T> ServiceUnavailable(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], HttpStatusCode.ServiceUnavailable);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the server, while acting as a gateway or proxy, did not receive a timely response from the upstream server.
|
||||
/// Corresponds to HTTP status code 504 Gateway Timeout.
|
||||
/// </summary>
|
||||
public static Result<T> GatewayTimeout(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], HttpStatusCode.GatewayTimeout);
|
||||
}
|
||||
public static Result<T> GatewayTimeout(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], HttpStatusCode.GatewayTimeout);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the server does not support the HTTP protocol version used in the request.
|
||||
/// Corresponds to HTTP status code 505 HTTP Version Not Supported.
|
||||
/// </summary>
|
||||
public static Result<T> HttpVersionNotSupported(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], HttpStatusCode.HttpVersionNotSupported);
|
||||
}
|
||||
public static Result<T> HttpVersionNotSupported(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], HttpStatusCode.HttpVersionNotSupported);
|
||||
|
||||
#endregion
|
||||
|
||||
@ -161,41 +144,36 @@ public partial class Result<T> : Result {
|
||||
/// Returns a result indicating the server has an internal configuration error: the chosen variant resource is configured to engage in transparent content negotiation itself, and is therefore not a proper end point in the negotiation process.
|
||||
/// Corresponds to HTTP status code 506 Variant Also Negotiates.
|
||||
/// </summary>
|
||||
public static Result<T> VariantAlsoNegotiates(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], HttpStatusCode.VariantAlsoNegotiates);
|
||||
}
|
||||
public static Result<T> VariantAlsoNegotiates(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], HttpStatusCode.VariantAlsoNegotiates);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the server is unable to store the representation needed to complete the request.
|
||||
/// Corresponds to HTTP status code 507 Insufficient Storage.
|
||||
/// </summary>
|
||||
public static Result<T> InsufficientStorage(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], HttpStatusCode.InsufficientStorage);
|
||||
}
|
||||
public static Result<T> InsufficientStorage(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], HttpStatusCode.InsufficientStorage);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the server detected an infinite loop while processing a request with depth: infinity. Usually encountered in WebDAV scenarios.
|
||||
/// Corresponds to HTTP status code 508 Loop Detected.
|
||||
/// </summary>
|
||||
public static Result<T> LoopDetected(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], HttpStatusCode.LoopDetected);
|
||||
}
|
||||
public static Result<T> LoopDetected(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], HttpStatusCode.LoopDetected);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating further extensions to the request are required for the server to fulfill it.
|
||||
/// Corresponds to HTTP status code 510 Not Extended.
|
||||
/// </summary>
|
||||
public static Result<T> NotExtended(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], HttpStatusCode.NotExtended);
|
||||
}
|
||||
public static Result<T> NotExtended(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], HttpStatusCode.NotExtended);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the client needs to authenticate to gain network access.
|
||||
/// Corresponds to HTTP status code 511 Network Authentication Required.
|
||||
/// </summary>
|
||||
public static Result<T> NetworkAuthenticationRequired(T? value, params string [] messages) {
|
||||
return new Result<T>(value, false, [..messages], HttpStatusCode.NetworkAuthenticationRequired);
|
||||
}
|
||||
public static Result<T> NetworkAuthenticationRequired(T? value, params string [] messages) =>
|
||||
new (value, false, [..messages], HttpStatusCode.NetworkAuthenticationRequired);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
using System.Net;
|
||||
|
||||
|
||||
namespace MaksIT.Results;
|
||||
|
||||
public partial class Result {
|
||||
@ -11,57 +10,50 @@ public partial class Result {
|
||||
/// Returns a result indicating the request was successful and the server returned the requested data.
|
||||
/// Corresponds to HTTP status code 200 OK.
|
||||
/// </summary>
|
||||
public static Result Ok(params string[] messages) {
|
||||
return new Result(true, [..messages], HttpStatusCode.OK);
|
||||
}
|
||||
public static Result Ok(params string[] messages) =>
|
||||
new (true, [..messages], HttpStatusCode.OK);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the request was successful and a new resource was created.
|
||||
/// Corresponds to HTTP status code 201 Created.
|
||||
/// </summary>
|
||||
public static Result Created(params string[] messages) {
|
||||
return new Result(true, [..messages], HttpStatusCode.Created);
|
||||
}
|
||||
public static Result Created(params string[] messages) =>
|
||||
new (true, [..messages], HttpStatusCode.Created);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the request has been accepted for processing, but the processing is not yet complete.
|
||||
/// Corresponds to HTTP status code 202 Accepted.
|
||||
/// </summary>
|
||||
public static Result Accepted(params string[] messages) {
|
||||
return new Result(true, [..messages], HttpStatusCode.Accepted);
|
||||
}
|
||||
public static Result Accepted(params string[] messages) =>
|
||||
new (true, [..messages], HttpStatusCode.Accepted);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the request was successful but the response contains metadata from a source other than the origin server.
|
||||
/// Corresponds to HTTP status code 203 Non-Authoritative Information.
|
||||
/// </summary>
|
||||
public static Result NonAuthoritativeInformation(params string[] messages) {
|
||||
return new Result(true, [..messages], HttpStatusCode.NonAuthoritativeInformation);
|
||||
}
|
||||
public static Result NonAuthoritativeInformation(params string[] messages) =>
|
||||
new (true, [..messages], HttpStatusCode.NonAuthoritativeInformation);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the request was successful but there is no content to send in the response.
|
||||
/// Corresponds to HTTP status code 204 No Content.
|
||||
/// </summary>
|
||||
public static Result NoContent(params string[] messages) {
|
||||
return new Result(true, [..messages], HttpStatusCode.NoContent);
|
||||
}
|
||||
public static Result NoContent(params string[] messages) =>
|
||||
new (true, [..messages], HttpStatusCode.NoContent);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the request was successful, but the user-agent should reset the document view that caused the request.
|
||||
/// Corresponds to HTTP status code 205 Reset Content.
|
||||
/// </summary>
|
||||
public static Result ResetContent(params string[] messages) {
|
||||
return new Result(true, [..messages], HttpStatusCode.ResetContent);
|
||||
}
|
||||
public static Result ResetContent(params string[] messages) =>
|
||||
new (true, [..messages], HttpStatusCode.ResetContent);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the request was successful and the server is delivering only part of the resource due to a range header sent by the client.
|
||||
/// Corresponds to HTTP status code 206 Partial Content.
|
||||
/// </summary>
|
||||
public static Result PartialContent(params string[] messages) {
|
||||
return new Result(true, [..messages], HttpStatusCode.PartialContent);
|
||||
}
|
||||
public static Result PartialContent(params string[] messages) =>
|
||||
new (true, [..messages], HttpStatusCode.PartialContent);
|
||||
|
||||
#endregion
|
||||
|
||||
@ -71,25 +63,22 @@ public partial class Result {
|
||||
/// Returns a result indicating the request was successful and the response contains multiple status codes, typically used for WebDAV.
|
||||
/// Corresponds to HTTP status code 207 Multi-Status.
|
||||
/// </summary>
|
||||
public static Result MultiStatus(params string[] messages) {
|
||||
return new Result(true, [..messages], HttpStatusCode.MultiStatus);
|
||||
}
|
||||
public static Result MultiStatus(params string[] messages) =>
|
||||
new (true, [..messages], HttpStatusCode.MultiStatus);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the request was successful and the information has already been reported in a previous response.
|
||||
/// Corresponds to HTTP status code 208 Already Reported.
|
||||
/// </summary>
|
||||
public static Result AlreadyReported(params string[] messages) {
|
||||
return new Result(true, [..messages], HttpStatusCode.AlreadyReported);
|
||||
}
|
||||
public static Result AlreadyReported(params string[] messages) =>
|
||||
new (true, [..messages], HttpStatusCode.AlreadyReported);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the request was successful and the server fulfilled the request for the resource using the delta encoding method.
|
||||
/// Corresponds to HTTP status code 226 IM Used.
|
||||
/// </summary>
|
||||
public static Result IMUsed(params string[] messages) {
|
||||
return new Result(true, [..messages], (HttpStatusCode)226); // 226 is the official status code for IM Used
|
||||
}
|
||||
public static Result IMUsed(params string[] messages) =>
|
||||
new (true, [..messages], (HttpStatusCode)226); // 226 is the official status code for IM Used
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -101,57 +90,50 @@ public partial class Result<T> : Result {
|
||||
/// Returns a result indicating the request was successful and the server returned the requested data.
|
||||
/// Corresponds to HTTP status code 200 OK.
|
||||
/// </summary>
|
||||
public static Result<T> Ok(T? value, params string[] messages) {
|
||||
return new Result<T>(value, true, [..messages], HttpStatusCode.OK);
|
||||
}
|
||||
public static Result<T> Ok(T? value, params string[] messages) =>
|
||||
new (value, true, [..messages], HttpStatusCode.OK);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the request was successful and a new resource was created.
|
||||
/// Corresponds to HTTP status code 201 Created.
|
||||
/// </summary>
|
||||
public static Result<T> Created(T? value, params string[] messages) {
|
||||
return new Result<T>(value, true, [..messages], HttpStatusCode.Created);
|
||||
}
|
||||
public static Result<T> Created(T? value, params string[] messages) =>
|
||||
new (value, true, [..messages], HttpStatusCode.Created);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the request has been accepted for processing, but the processing is not yet complete.
|
||||
/// Corresponds to HTTP status code 202 Accepted.
|
||||
/// </summary>
|
||||
public static Result<T> Accepted(T? value, params string[] messages) {
|
||||
return new Result<T>(value, true, [..messages], HttpStatusCode.Accepted);
|
||||
}
|
||||
public static Result<T> Accepted(T? value, params string[] messages) =>
|
||||
new (value, true, [..messages], HttpStatusCode.Accepted);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the request was successful but the response contains metadata from a source other than the origin server.
|
||||
/// Corresponds to HTTP status code 203 Non-Authoritative Information.
|
||||
/// </summary>
|
||||
public static Result<T> NonAuthoritativeInformation(T? value, params string[] messages) {
|
||||
return new Result<T>(value, true, [..messages], HttpStatusCode.NonAuthoritativeInformation);
|
||||
}
|
||||
public static Result<T> NonAuthoritativeInformation(T? value, params string[] messages) =>
|
||||
new (value, true, [..messages], HttpStatusCode.NonAuthoritativeInformation);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the request was successful but there is no content to send in the response.
|
||||
/// Corresponds to HTTP status code 204 No Content.
|
||||
/// </summary>
|
||||
public static Result<T> NoContent(T? value, params string[] messages) {
|
||||
return new Result<T>(value, true, [..messages], HttpStatusCode.NoContent);
|
||||
}
|
||||
public static Result<T> NoContent(T? value, params string[] messages) =>
|
||||
new (value, true, [..messages], HttpStatusCode.NoContent);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the request was successful, but the user-agent should reset the document view that caused the request.
|
||||
/// Corresponds to HTTP status code 205 Reset Content.
|
||||
/// </summary>
|
||||
public static Result<T> ResetContent(T? value, params string[] messages) {
|
||||
return new Result<T>(value, true, [..messages], HttpStatusCode.ResetContent);
|
||||
}
|
||||
public static Result<T> ResetContent(T? value, params string[] messages) =>
|
||||
new (value, true, [..messages], HttpStatusCode.ResetContent);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the request was successful and the server is delivering only part of the resource due to a range header sent by the client.
|
||||
/// Corresponds to HTTP status code 206 Partial Content.
|
||||
/// </summary>
|
||||
public static Result<T> PartialContent(T? value, params string[] messages) {
|
||||
return new Result<T>(value, true, [..messages], HttpStatusCode.PartialContent);
|
||||
}
|
||||
public static Result<T> PartialContent(T? value, params string[] messages) =>
|
||||
new (value, true, [..messages], HttpStatusCode.PartialContent);
|
||||
|
||||
#endregion
|
||||
|
||||
@ -161,25 +143,22 @@ public partial class Result<T> : Result {
|
||||
/// Returns a result indicating the request was successful and the response contains multiple status codes, typically used for WebDAV.
|
||||
/// Corresponds to HTTP status code 207 Multi-Status.
|
||||
/// </summary>
|
||||
public static Result<T> MultiStatus(T? value, params string[] messages) {
|
||||
return new Result<T>(value, true, [..messages], HttpStatusCode.MultiStatus);
|
||||
}
|
||||
public static Result<T> MultiStatus(T? value, params string[] messages) =>
|
||||
new (value, true, [..messages], HttpStatusCode.MultiStatus);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the request was successful and the information has already been reported in a previous response.
|
||||
/// Corresponds to HTTP status code 208 Already Reported.
|
||||
/// </summary>
|
||||
public static Result<T> AlreadyReported(T? value, params string[] messages) {
|
||||
return new Result<T>(value, true, [..messages], HttpStatusCode.AlreadyReported);
|
||||
}
|
||||
public static Result<T> AlreadyReported(T? value, params string[] messages) =>
|
||||
new (value, true, [..messages], HttpStatusCode.AlreadyReported);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a result indicating the request was successful and the server fulfilled the request for the resource using the delta encoding method.
|
||||
/// Corresponds to HTTP status code 226 IM Used.
|
||||
/// </summary>
|
||||
public static Result<T> IMUsed(T? value, params string[] messages) {
|
||||
return new Result<T>(value, true, [..messages], (HttpStatusCode)226); // 226 is the official status code for IM Used
|
||||
}
|
||||
public static Result<T> IMUsed(T? value, params string[] messages) =>
|
||||
new (value, true, [..messages], (HttpStatusCode)226); // 226 is the official status code for IM Used
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MaksIT.Results.Mvc;
|
||||
|
||||
|
||||
namespace MaksIT.Results;
|
||||
|
||||
public partial class Result {
|
||||
@ -12,7 +11,7 @@ public partial class Result {
|
||||
|
||||
protected Result(bool isSuccess, List<string> messages, HttpStatusCode statusCode) {
|
||||
IsSuccess = isSuccess;
|
||||
Messages = messages ?? new List<string>();
|
||||
Messages = messages ?? [];
|
||||
StatusCode = statusCode;
|
||||
}
|
||||
|
||||
@ -20,9 +19,8 @@ public partial class Result {
|
||||
/// Converts the current Result{T} to a non-generic Result.
|
||||
/// </summary>
|
||||
/// <returns>A non-generic Result object.</returns>
|
||||
public Result ToResult() {
|
||||
return new Result(IsSuccess, Messages, StatusCode);
|
||||
}
|
||||
public Result ToResult() =>
|
||||
new (IsSuccess, Messages, StatusCode);
|
||||
|
||||
/// <summary>
|
||||
/// Converts this Result into a Result of another type while retaining the same success status, messages, and status code.
|
||||
@ -30,9 +28,8 @@ public partial class Result {
|
||||
/// <typeparam name="U">The target type for the Result.</typeparam>
|
||||
/// <param name="value">The new value of type U for the converted Result.</param>
|
||||
/// <returns>A Result of type U.</returns>
|
||||
public Result<U?> ToResultOfType<U>(U? value) {
|
||||
return new Result<U?>(value, IsSuccess, Messages, StatusCode);
|
||||
}
|
||||
public Result<U?> ToResultOfType<U>(U? value) =>
|
||||
new(value, IsSuccess, Messages, StatusCode);
|
||||
|
||||
/// <summary>
|
||||
/// Converts the current Result to an IActionResult.
|
||||
@ -49,18 +46,15 @@ public partial class Result {
|
||||
Detail = string.Join("; ", Messages),
|
||||
Instance = null // You can customize the instance URI if needed
|
||||
};
|
||||
|
||||
return new ObjectResult(problemDetails) { StatusCode = (int)StatusCode };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public partial class Result<T> : Result {
|
||||
public T? Value { get; }
|
||||
|
||||
public Result(T? value, bool isSuccess, List<string> messages, HttpStatusCode statusCode)
|
||||
: base(isSuccess, messages, statusCode) {
|
||||
Value = value;
|
||||
}
|
||||
public partial class Result<T>(T? value, bool isSuccess, List<string> messages, HttpStatusCode statusCode)
|
||||
: Result(isSuccess, messages, statusCode) {
|
||||
public T? Value { get; } = value;
|
||||
|
||||
/// <summary>
|
||||
/// Converts this Result<T> to a Result<U> while retaining success status, messages, and status code.
|
||||
@ -68,9 +62,8 @@ public partial class Result<T> : Result {
|
||||
/// <typeparam name="U">The target type for the Result.</typeparam>
|
||||
/// <param name="newValueFunc">A function to transform the current value to a new value of type U.</param>
|
||||
/// <returns>A Result<U> containing the transformed value.</returns>
|
||||
public Result<U?> ToResultOfType<U>(Func<T?, U?> newValueFunc) {
|
||||
return new Result<U?>(newValueFunc(Value), IsSuccess, Messages, StatusCode);
|
||||
}
|
||||
public Result<U?> ToResultOfType<U>(Func<T?, U?> newValueFunc) =>
|
||||
new (newValueFunc(Value), IsSuccess, Messages, StatusCode);
|
||||
|
||||
/// <summary>
|
||||
/// Converts the current Result<T> to an IActionResult.
|
||||
@ -79,7 +72,7 @@ public partial class Result<T> : Result {
|
||||
public override IActionResult ToActionResult() {
|
||||
if (IsSuccess) {
|
||||
if (IsRedirectionStatusCode(StatusCode) && Value is string location && !string.IsNullOrWhiteSpace(location))
|
||||
return new RedirectResult(location);
|
||||
return new RedirectResult(location, (int)StatusCode);
|
||||
|
||||
if (Value is not null)
|
||||
return new ObjectResult(Value) { StatusCode = (int)StatusCode };
|
||||
@ -90,12 +83,4 @@ public partial class Result<T> : Result {
|
||||
return base.ToActionResult();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsRedirectionStatusCode(HttpStatusCode statusCode) =>
|
||||
statusCode is HttpStatusCode.MultipleChoices
|
||||
or HttpStatusCode.MovedPermanently
|
||||
or HttpStatusCode.Found
|
||||
or HttpStatusCode.SeeOther
|
||||
or HttpStatusCode.TemporaryRedirect
|
||||
or HttpStatusCode.PermanentRedirect;
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft-07/schema",
|
||||
"title": "Release Package Script Settings",
|
||||
"description": "Invoke-ReleasePackage.ps1 plugin settings. Place ReleasePublishGuard before GitHub/DotNetNuGet/DockerImagePush/HelmPush/NpmPublish; use its branches and tag rules. Publish plugins omit per-plugin branches. Semver comes from DotNetReleaseVersion (projectFiles).",
|
||||
"description": "Invoke-ReleasePackage.ps1 plugin settings. Place ReleasePublishGuard before GitHub/DockerImagePush/HelmPush/NpmPublish; use its branches and tag rules. Publish plugins omit per-plugin branches. Semver comes from DotNetReleaseVersion (projectFiles).",
|
||||
"repoUtilsSecretsShared": "RepoUtilsSecretsShared",
|
||||
"repoUtilsSecrets": "RepoUtilsSecrets",
|
||||
"plugins": [
|
||||
@ -68,41 +68,6 @@
|
||||
"releaseNotesFile": "..\\..\\..\\CHANGELOG.md",
|
||||
"releaseTitlePattern": "Release {version}"
|
||||
},
|
||||
{
|
||||
"name": "DotNetNuGet",
|
||||
"stageLabel": "release",
|
||||
"enabled": true,
|
||||
"nugetSecret": "NuGet",
|
||||
"source": "https://api.nuget.org/v3/index.json"
|
||||
},
|
||||
{
|
||||
"name": "NpmReleaseVersion",
|
||||
"stageLabel": "build",
|
||||
"enabled": false,
|
||||
"packageJsonPath": "..\\..\\..\\src\\package.json",
|
||||
"syncWorkspaceVersions": true
|
||||
},
|
||||
{
|
||||
"name": "NpmBuild",
|
||||
"stageLabel": "build",
|
||||
"enabled": false,
|
||||
"workspaceRoot": "..\\..\\..\\src",
|
||||
"useCi": true,
|
||||
"buildScript": "build"
|
||||
},
|
||||
{
|
||||
"name": "NpmPublish",
|
||||
"stageLabel": "release",
|
||||
"enabled": false,
|
||||
"npmSecret": "Npm",
|
||||
"registry": "https://registry.npmjs.org",
|
||||
"access": "public",
|
||||
"workspaceRoot": "..\\..\\..\\src",
|
||||
"publishOrder": [
|
||||
"@scope/example-contracts",
|
||||
"@scope/example-core"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "CleanupArtifacts",
|
||||
"stageLabel": "release",
|
||||
|
||||
@ -1,69 +0,0 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Discovers existing .NET NuGet package artifacts.
|
||||
|
||||
.DESCRIPTION
|
||||
Scans artifactsDir for .nupkg/.snupkg matching the release version and populates shared
|
||||
context (packageFile, symbolsPackageFile, releaseArchiveInputs) for downstream plugins.
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$pluginSupportModulePath = Join-Path $srcDir 'modules/Engine/PluginSupport.psm1'
|
||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Plugin {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
Import-PluginDependency -ModuleName 'Logging' -RequiredCommand 'Write-Log'
|
||||
Import-PluginDependency -ModuleName 'DotNetArtifactSupport' -RequiredCommand 'Resolve-DotNetPackageArtifacts'
|
||||
Import-PluginDependency -ModuleName 'EngineContext' -RequiredCommand 'Set-EngineFact'
|
||||
|
||||
$pluginSettings = $Settings
|
||||
$sharedSettings = $Settings.context
|
||||
$scriptDir = $sharedSettings.scriptDir
|
||||
$version = $sharedSettings.version
|
||||
|
||||
if ($Settings.PSObject.Properties['artifactsDir'] -and -not [string]::IsNullOrWhiteSpace([string]$Settings.artifactsDir)) {
|
||||
$artifactsDirectory = [System.IO.Path]::GetFullPath((Join-Path $scriptDir ([string]$Settings.artifactsDir)))
|
||||
Set-EngineState -Context $sharedSettings -Name 'artifactsDirectory' -Value $artifactsDirectory
|
||||
Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory
|
||||
}
|
||||
else {
|
||||
$artifactsDirectory = $sharedSettings.artifactsDirectory
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($artifactsDirectory)) {
|
||||
throw 'DiscoverDotNetPackageArtifacts requires artifactsDir in plugin settings or artifactsDirectory on shared context.'
|
||||
}
|
||||
|
||||
Write-Log -Level 'STEP' -Message "Discovering NuGet package artifacts in $artifactsDirectory ..."
|
||||
$resolved = Resolve-DotNetPackageArtifacts -ArtifactsDirectory $artifactsDirectory -Version $version
|
||||
|
||||
Write-Log -Level 'OK' -Message " Package ready: $($resolved.PackageFile.FullName)"
|
||||
if ($resolved.SymbolsPackageFile) {
|
||||
Write-Log -Level 'OK' -Message " Symbols package ready: $($resolved.SymbolsPackageFile.FullName)"
|
||||
}
|
||||
else {
|
||||
Write-Log -Level 'WARN' -Message " Symbols package (.snupkg) not found for version $version."
|
||||
}
|
||||
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'packageFile' -Value $resolved.PackageFile -Overwrite Replace -LegacyProperty 'packageFile'
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'symbolsPackageFile' -Value $resolved.SymbolsPackageFile -Overwrite Replace -LegacyProperty 'symbolsPackageFile'
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value $resolved.ReleaseArchiveInputs -Overwrite Replace -LegacyProperty 'releaseArchiveInputs'
|
||||
}
|
||||
|
||||
function Get-PluginMetadata {
|
||||
[pscustomobject]@{ mutatesRemote = $false }
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata
|
||||
@ -1,86 +0,0 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
.NET NuGet publish plugin.
|
||||
|
||||
.DESCRIPTION
|
||||
This plugin publishes the package artifact from shared runtime
|
||||
context to the configured NuGet feed using the configured API key.
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
|
||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Plugin {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
||||
|
||||
$pluginSettings = $Settings
|
||||
$sharedSettings = $Settings.context
|
||||
$packageFile = $sharedSettings.packageFile
|
||||
|
||||
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $sharedSettings
|
||||
$nugetSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'nugetSecret' -PluginDisplayName 'DotNetNuGet' -Required
|
||||
|
||||
Assert-Command dotnet
|
||||
|
||||
if (-not $packageFile) {
|
||||
throw "DotNetNuGet plugin requires a NuGet package artifact. Ensure DotNetPack produced a .nupkg before running DotNetNuGet."
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$nugetSource = if ([string]::IsNullOrWhiteSpace($pluginSettings.source)) {
|
||||
"https://api.nuget.org/v3/index.json"
|
||||
}
|
||||
else {
|
||||
$pluginSettings.source
|
||||
}
|
||||
|
||||
Write-Log -Level "INFO" -Message "Dry run: would push $($packageFile.FullName) to $nugetSource"
|
||||
return
|
||||
}
|
||||
|
||||
$nugetKey = Get-RepoUtilsSecretSlot -Name $nugetSecret -Settings $sharedSettings
|
||||
if ([string]::IsNullOrWhiteSpace($nugetKey)) {
|
||||
throw "NuGet API key is not set. Set RepoUtilsSecrets slot '$nugetSecret' (nugetSecret)."
|
||||
}
|
||||
|
||||
$nugetSource = if ([string]::IsNullOrWhiteSpace($pluginSettings.source)) {
|
||||
"https://api.nuget.org/v3/index.json"
|
||||
}
|
||||
else {
|
||||
$pluginSettings.source
|
||||
}
|
||||
|
||||
Write-Log -Level "STEP" -Message "Pushing package to NuGet feed..."
|
||||
dotnet nuget push $packageFile.FullName -k $nugetKey -s $nugetSource --skip-duplicate
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to push the package to NuGet feed."
|
||||
}
|
||||
|
||||
Write-Log -Level "OK" -Message " NuGet push completed."
|
||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Add-EnginePublishCompletion"
|
||||
Add-EnginePublishCompletion -Context $sharedSettings -Publisher 'DotNetNuGet'
|
||||
}
|
||||
|
||||
function Get-PluginMetadata {
|
||||
[pscustomobject]@{ mutatesRemote = $true }
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata
|
||||
|
||||
|
||||
|
||||
@ -1,199 +0,0 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
.NET publish plugin for producing application release artifacts.
|
||||
|
||||
.DESCRIPTION
|
||||
This plugin publishes configured .NET projects into the artifacts directory
|
||||
and appends those publish folders to shared archive inputs so later plugins
|
||||
can zip them next to any earlier pack outputs. Existing NuGet package facts
|
||||
(packageFile) are left unchanged.
|
||||
|
||||
Optional runtimeIdentifiers (or runtimeIdentifier) produce per-RID folders
|
||||
under artifactsDir/<ProjectName>/<rid> for desktop installers (Windows MSI /
|
||||
Linux Flatpak). When RIDs are set, selfContained defaults to true.
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
|
||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
function Get-DotNetPublishRuntimeIdentifiers {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$PluginSettings
|
||||
)
|
||||
|
||||
$rids = [System.Collections.Generic.List[string]]::new()
|
||||
if ($PluginSettings.PSObject.Properties['runtimeIdentifiers'] -and $null -ne $PluginSettings.runtimeIdentifiers) {
|
||||
foreach ($item in @($PluginSettings.runtimeIdentifiers)) {
|
||||
$value = [string]$item
|
||||
if (-not [string]::IsNullOrWhiteSpace($value)) {
|
||||
$rids.Add($value.Trim())
|
||||
}
|
||||
}
|
||||
}
|
||||
elseif ($PluginSettings.PSObject.Properties['runtimeIdentifier'] -and -not [string]::IsNullOrWhiteSpace([string]$PluginSettings.runtimeIdentifier)) {
|
||||
$rids.Add(([string]$PluginSettings.runtimeIdentifier).Trim())
|
||||
}
|
||||
|
||||
return @($rids)
|
||||
}
|
||||
|
||||
function Invoke-Plugin {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineFact"
|
||||
Import-PluginDependency -ModuleName "ExternalCommandSupport" -RequiredCommand "Invoke-ExternalCommand"
|
||||
|
||||
$pluginSettings = $Settings
|
||||
$sharedSettings = $Settings.context
|
||||
$scriptDir = $sharedSettings.scriptDir
|
||||
$projectFiles = @()
|
||||
|
||||
Assert-Command dotnet
|
||||
|
||||
if ($pluginSettings.PSObject.Properties['projectFiles'] -and $null -ne $pluginSettings.projectFiles) {
|
||||
$projectFiles = @(Resolve-RelativePaths -Value $pluginSettings.projectFiles -BasePath $scriptDir)
|
||||
}
|
||||
else {
|
||||
$fromFact = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'projectFiles' -LegacyProperty @('projectFiles')
|
||||
if ($null -ne $fromFact) {
|
||||
$projectFiles = @($fromFact)
|
||||
}
|
||||
elseif ($sharedSettings.PSObject.Properties['projectFiles'] -and $null -ne $sharedSettings.projectFiles) {
|
||||
$projectFiles = @($sharedSettings.projectFiles)
|
||||
}
|
||||
}
|
||||
|
||||
if ($projectFiles.Count -eq 0) {
|
||||
throw "DotNetPublish plugin requires projectFiles in plugin settings or projectFiles on shared context."
|
||||
}
|
||||
|
||||
if ($pluginSettings.PSObject.Properties['artifactsDir'] -and -not [string]::IsNullOrWhiteSpace([string]$pluginSettings.artifactsDir)) {
|
||||
$artifactsDirectory = [System.IO.Path]::GetFullPath((Join-Path $scriptDir ([string]$pluginSettings.artifactsDir)))
|
||||
Set-EngineState -Context $sharedSettings -Name 'artifactsDirectory' -Value $artifactsDirectory
|
||||
Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory
|
||||
}
|
||||
else {
|
||||
$artifactsDirectory = $sharedSettings.artifactsDirectory
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace([string]$artifactsDirectory)) {
|
||||
throw "DotNetPublish plugin requires artifactsDir in plugin settings or artifactsDirectory on shared context."
|
||||
}
|
||||
|
||||
if (!(Test-Path $artifactsDirectory)) {
|
||||
New-Item -ItemType Directory -Path $artifactsDirectory | Out-Null
|
||||
}
|
||||
|
||||
$existing = Get-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -LegacyProperty @('releaseArchiveInputs')
|
||||
$archiveInputs = [System.Collections.Generic.List[object]]::new()
|
||||
if ($null -ne $existing) {
|
||||
foreach ($item in @($existing)) {
|
||||
if ($null -ne $item) {
|
||||
$archiveInputs.Add($item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$runtimeIdentifiers = @(Get-DotNetPublishRuntimeIdentifiers -PluginSettings $pluginSettings)
|
||||
$selfContained = $null
|
||||
if ($pluginSettings.PSObject.Properties['selfContained'] -and $null -ne $pluginSettings.selfContained) {
|
||||
$selfContained = [bool]$pluginSettings.selfContained
|
||||
}
|
||||
elseif ($runtimeIdentifiers.Count -gt 0) {
|
||||
$selfContained = $true
|
||||
}
|
||||
|
||||
$publishSingleFile = $false
|
||||
if ($pluginSettings.PSObject.Properties['publishSingleFile'] -and $null -ne $pluginSettings.publishSingleFile) {
|
||||
$publishSingleFile = [bool]$pluginSettings.publishSingleFile
|
||||
}
|
||||
|
||||
$additionalArguments = @()
|
||||
if ($pluginSettings.PSObject.Properties['additionalPublishArguments'] -and $null -ne $pluginSettings.additionalPublishArguments) {
|
||||
$additionalArguments = @($pluginSettings.additionalPublishArguments | ForEach-Object { [string]$_ } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
||||
}
|
||||
|
||||
$publishOutputs = [System.Collections.Generic.List[object]]::new()
|
||||
$ridLoop = @($null)
|
||||
if ($runtimeIdentifiers.Count -gt 0) {
|
||||
$ridLoop = @($runtimeIdentifiers)
|
||||
}
|
||||
|
||||
foreach ($publishProjectPath in $projectFiles) {
|
||||
$projectName = [System.IO.Path]::GetFileNameWithoutExtension($publishProjectPath)
|
||||
$projectRoot = Join-Path $artifactsDirectory $projectName
|
||||
|
||||
foreach ($runtimeIdentifier in $ridLoop) {
|
||||
$publishDir = if ([string]::IsNullOrWhiteSpace([string]$runtimeIdentifier)) {
|
||||
$projectRoot
|
||||
}
|
||||
else {
|
||||
Join-Path $projectRoot ([string]$runtimeIdentifier)
|
||||
}
|
||||
|
||||
if (Test-Path $publishDir) {
|
||||
Remove-Item -Path $publishDir -Recurse -Force
|
||||
}
|
||||
|
||||
$ridLabel = if ([string]::IsNullOrWhiteSpace([string]$runtimeIdentifier)) { 'current runtime' } else { [string]$runtimeIdentifier }
|
||||
Write-Log -Level "STEP" -Message "Publishing release artifact ($projectName / $ridLabel)..."
|
||||
$dotnetPublishArguments = @(
|
||||
'publish', $publishProjectPath, '-c', 'Release', '-o', $publishDir, '--nologo'
|
||||
)
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$runtimeIdentifier)) {
|
||||
$dotnetPublishArguments += @('-r', [string]$runtimeIdentifier)
|
||||
}
|
||||
|
||||
if ($null -ne $selfContained) {
|
||||
if ($selfContained) {
|
||||
$dotnetPublishArguments += '--self-contained'
|
||||
}
|
||||
else {
|
||||
$dotnetPublishArguments += '--no-self-contained'
|
||||
}
|
||||
}
|
||||
|
||||
if ($publishSingleFile) {
|
||||
$dotnetPublishArguments += '-p:PublishSingleFile=true'
|
||||
}
|
||||
|
||||
if ($additionalArguments.Count -gt 0) {
|
||||
$dotnetPublishArguments += $additionalArguments
|
||||
}
|
||||
|
||||
Invoke-ExternalCommand -Name dotnet -ArgumentList $dotnetPublishArguments | Out-Null
|
||||
|
||||
$publishedItems = @(Get-ChildItem -Path $publishDir -Force -ErrorAction SilentlyContinue)
|
||||
if ($publishedItems.Count -eq 0) {
|
||||
throw "dotnet publish completed, but no files were produced in: $publishDir"
|
||||
}
|
||||
|
||||
Write-Log -Level "OK" -Message " Published artifact ready: $publishDir"
|
||||
$archiveInputs.Add($publishDir)
|
||||
$publishOutputs.Add([pscustomobject]@{
|
||||
projectName = $projectName
|
||||
runtimeIdentifier = [string]$runtimeIdentifier
|
||||
directory = $publishDir
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value @($archiveInputs) -Overwrite Replace -LegacyProperty 'releaseArchiveInputs'
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'publishOutputs' -Value @($publishOutputs) -Overwrite Replace
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Invoke-Plugin, Get-DotNetPublishRuntimeIdentifiers
|
||||
@ -1,78 +0,0 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Collects coverage metrics from an existing test results directory.
|
||||
|
||||
.DESCRIPTION
|
||||
Stack-agnostic parser for recovered container or host test output. Auto-detects .NET
|
||||
Cobertura (coverage.cobertura.xml) or Jest (coverage-summary.json) and publishes the
|
||||
same shared-context keys as DotNetTest / NpmJestTest for QualityGate.
|
||||
Use after DockerContainerBuilder / PodmanContainerBuilder when tests run inside a container image.
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$pluginSupportModulePath = Join-Path $srcDir 'modules/Engine/PluginSupport.psm1'
|
||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Plugin {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
Import-PluginDependency -ModuleName 'Logging' -RequiredCommand 'Write-Log'
|
||||
Import-PluginDependency -ModuleName 'TestRunner' -RequiredCommand 'Get-CoverageFromResultsDirectory'
|
||||
Import-PluginDependency -ModuleName 'TestRunner' -RequiredCommand 'Publish-CoverageMetricsToSharedContext'
|
||||
|
||||
$pluginSettings = $Settings
|
||||
$sharedSettings = $Settings.context
|
||||
$scriptDir = $sharedSettings.scriptDir
|
||||
|
||||
$resultsDirSetting = $pluginSettings.resultsDir
|
||||
if ([string]::IsNullOrWhiteSpace($resultsDirSetting)) {
|
||||
if ($sharedSettings.PSObject.Properties.Name -contains 'testResultsDirectory' -and $sharedSettings.testResultsDirectory) {
|
||||
$resultsDirSetting = $sharedSettings.testResultsDirectory
|
||||
}
|
||||
else {
|
||||
throw "CollectCoverage requires 'resultsDir' in plugin settings or testResultsDirectory on shared context (from ContainerBuilder)."
|
||||
}
|
||||
}
|
||||
|
||||
$resultsDirectory = if ([System.IO.Path]::IsPathRooted([string]$resultsDirSetting)) {
|
||||
[string]$resultsDirSetting
|
||||
}
|
||||
else {
|
||||
[System.IO.Path]::GetFullPath((Join-Path $scriptDir ([string]$resultsDirSetting)))
|
||||
}
|
||||
|
||||
$format = 'auto'
|
||||
if ($pluginSettings.PSObject.Properties.Name -contains 'format' -and -not [string]::IsNullOrWhiteSpace([string]$pluginSettings.format)) {
|
||||
$format = [string]$pluginSettings.format
|
||||
}
|
||||
|
||||
Write-Log -Level 'STEP' -Message "Collecting coverage from $resultsDirectory ..."
|
||||
$testResult = Get-CoverageFromResultsDirectory -ResultsDirectory $resultsDirectory -Format $format -Silent:$true
|
||||
if (-not $testResult.Success) {
|
||||
throw $testResult.Error
|
||||
}
|
||||
|
||||
Publish-CoverageMetricsToSharedContext -SharedSettings $sharedSettings -TestResult $testResult
|
||||
|
||||
$formatLabel = if ($testResult.CoverageFormat) { $testResult.CoverageFormat } else { 'unknown' }
|
||||
Write-Log -Level 'OK' -Message " Coverage collected ($formatLabel)."
|
||||
Write-Log -Level 'INFO' -Message " Line Coverage: $($testResult.LineRate)%"
|
||||
Write-Log -Level 'INFO' -Message " Branch Coverage: $($testResult.BranchRate)%"
|
||||
Write-Log -Level 'INFO' -Message " Method Coverage: $($testResult.MethodRate)%"
|
||||
}
|
||||
|
||||
function Get-PluginMetadata {
|
||||
[pscustomobject]@{ mutatesRemote = $false }
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata
|
||||
@ -1,52 +0,0 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Probes docker/podman availability and sets mandatory shared engine state for the pipeline.
|
||||
|
||||
.DESCRIPTION
|
||||
Must be the first enabled plugin. Sets containerEngineProbeCompleted, activeContainerEngine,
|
||||
dockerEngineAvailable, podmanEngineAvailable, and compose CLI fields on shared context.
|
||||
Fails the pipeline when no usable engine is found.
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$pluginSupportModulePath = Join-Path $srcDir 'modules/Engine/PluginSupport.psm1'
|
||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Plugin {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
Import-PluginDependency -ModuleName 'Logging' -RequiredCommand 'Write-Log'
|
||||
Import-PluginDependency -ModuleName 'ContainerEngineSupport' -RequiredCommand 'Resolve-ActiveContainerEngineState'
|
||||
|
||||
$shared = $Settings.context
|
||||
|
||||
Write-Log -Level 'STEP' -Message 'Probing container engine availability...'
|
||||
$state = Resolve-ActiveContainerEngineState
|
||||
Set-ContainerEngineSharedState -SharedSettings $shared -State $state
|
||||
|
||||
$display = Get-ContainerEngineDisplayName -Engine (Get-SharedContainerEngineInvocation -SharedSettings $shared)
|
||||
Write-Log -Level 'OK' -Message " Active engine: $display"
|
||||
Write-Log -Level 'INFO' -Message " docker CLI available: $($state.DockerEngineAvailable); podman CLI available: $($state.PodmanEngineAvailable)"
|
||||
if ([string]::IsNullOrWhiteSpace([string]$state.ComposeExecutable)) {
|
||||
Write-Log -Level 'INFO' -Message ' Compose CLI: not available (Compose plugins will fail if enabled).'
|
||||
}
|
||||
else {
|
||||
Write-Log -Level 'INFO' -Message " Compose CLI: $($state.ComposeExecutable) $($state.ComposeBaseArgs -join ' ')"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-PluginMetadata {
|
||||
[pscustomobject]@{ mutatesRemote = $false }
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata
|
||||
@ -1,124 +0,0 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Discovers package artifacts produced by pack plugins or container build pipelines.
|
||||
|
||||
.DESCRIPTION
|
||||
Stack-agnostic scanner for artifactsDir. Detects .NET .nupkg/.snupkg or npm .tgz files
|
||||
and populates shared context (packageFile, releaseAssetPaths, releaseArchiveInputs).
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$pluginSupportModulePath = Join-Path $srcDir 'modules/Engine/PluginSupport.psm1'
|
||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Plugin {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
Import-PluginDependency -ModuleName 'Logging' -RequiredCommand 'Write-Log'
|
||||
Import-PluginDependency -ModuleName 'DotNetArtifactSupport' -RequiredCommand 'Resolve-DotNetPackageArtifacts'
|
||||
|
||||
$pluginSettings = $Settings
|
||||
$sharedSettings = $Settings.context
|
||||
$scriptDir = $sharedSettings.scriptDir
|
||||
$version = $sharedSettings.version
|
||||
|
||||
if ($Settings.PSObject.Properties['artifactsDir'] -and -not [string]::IsNullOrWhiteSpace([string]$Settings.artifactsDir)) {
|
||||
$artifactsDirectory = [System.IO.Path]::GetFullPath((Join-Path $scriptDir ([string]$Settings.artifactsDir)))
|
||||
if (Get-Command Set-EngineState -ErrorAction SilentlyContinue) {
|
||||
Set-EngineState -Context $sharedSettings -Name 'artifactsDirectory' -Value $artifactsDirectory
|
||||
Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory
|
||||
}
|
||||
else {
|
||||
$sharedSettings | Add-Member -NotePropertyName artifactsDirectory -NotePropertyValue $artifactsDirectory -Force
|
||||
$sharedSettings | Add-Member -NotePropertyName releaseDir -NotePropertyValue $artifactsDirectory -Force
|
||||
}
|
||||
}
|
||||
else {
|
||||
$artifactsDirectory = $sharedSettings.artifactsDirectory
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($artifactsDirectory)) {
|
||||
throw 'DiscoverPackageArtifacts requires artifactsDir in plugin settings or artifactsDirectory on shared context.'
|
||||
}
|
||||
|
||||
$nupkgCandidates = @(Get-ChildItem -Path $artifactsDirectory -Filter '*.nupkg' -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.Name -notlike '*.symbols.nupkg' -and $_.Name -notlike '*.snupkg'
|
||||
})
|
||||
$tgzCandidates = @(Get-ChildItem -Path $artifactsDirectory -Filter '*.tgz' -ErrorAction SilentlyContinue)
|
||||
|
||||
if ($nupkgCandidates.Count -gt 0 -and $tgzCandidates.Count -gt 0) {
|
||||
throw "DiscoverPackageArtifacts found both NuGet and npm artifacts in $artifactsDirectory. Use one package stack per repository."
|
||||
}
|
||||
|
||||
if ($nupkgCandidates.Count -gt 0) {
|
||||
if ([string]::IsNullOrWhiteSpace([string]$version)) {
|
||||
throw 'DiscoverPackageArtifacts requires release version on shared context when discovering .nupkg artifacts.'
|
||||
}
|
||||
|
||||
Write-Log -Level 'STEP' -Message "Discovering NuGet package artifacts in $artifactsDirectory ..."
|
||||
$resolved = Resolve-DotNetPackageArtifacts -ArtifactsDirectory $artifactsDirectory -Version $version
|
||||
|
||||
Write-Log -Level 'OK' -Message " Package ready: $($resolved.PackageFile.FullName)"
|
||||
if ($resolved.SymbolsPackageFile) {
|
||||
Write-Log -Level 'OK' -Message " Symbols package ready: $($resolved.SymbolsPackageFile.FullName)"
|
||||
}
|
||||
else {
|
||||
Write-Log -Level 'WARN' -Message " Symbols package (.snupkg) not found for version $version."
|
||||
}
|
||||
|
||||
$sharedSettings | Add-Member -NotePropertyName packageFile -NotePropertyValue $resolved.PackageFile -Force
|
||||
$sharedSettings | Add-Member -NotePropertyName symbolsPackageFile -NotePropertyValue $resolved.SymbolsPackageFile -Force
|
||||
$sharedSettings | Add-Member -NotePropertyName releaseArchiveInputs -NotePropertyValue $resolved.ReleaseArchiveInputs -Force
|
||||
$sharedSettings | Add-Member -NotePropertyName releaseAssetPaths -NotePropertyValue $resolved.ReleaseArchiveInputs -Force
|
||||
if (Get-Command Set-EngineFact -ErrorAction SilentlyContinue) {
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'packageFile' -Value $resolved.PackageFile -Overwrite Replace -LegacyProperty 'packageFile'
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'symbolsPackageFile' -Value $resolved.SymbolsPackageFile -Overwrite Replace -LegacyProperty 'symbolsPackageFile'
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value $resolved.ReleaseArchiveInputs -Overwrite Replace -LegacyProperty 'releaseArchiveInputs'
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'assetPaths' -Value $resolved.ReleaseArchiveInputs -Overwrite Replace -LegacyProperty 'releaseAssetPaths'
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if ($tgzCandidates.Count -gt 0) {
|
||||
Import-PluginDependency -ModuleName 'NpmArtifactSupport' -RequiredCommand 'Resolve-NpmPackageArtifacts'
|
||||
|
||||
Write-Log -Level 'STEP' -Message "Discovering npm package artifacts in $artifactsDirectory ..."
|
||||
$resolved = Resolve-NpmPackageArtifacts -ArtifactsDirectory $artifactsDirectory -Version $version
|
||||
|
||||
foreach ($tarball in $resolved.PackageFiles) {
|
||||
Write-Log -Level 'OK' -Message " Package ready: $($tarball.FullName)"
|
||||
}
|
||||
|
||||
$sharedSettings | Add-Member -NotePropertyName packageFile -NotePropertyValue $resolved.PackageFile -Force
|
||||
$sharedSettings | Add-Member -NotePropertyName releaseArchiveInputs -NotePropertyValue $resolved.ReleaseArchiveInputs -Force
|
||||
$sharedSettings | Add-Member -NotePropertyName releaseAssetPaths -NotePropertyValue $resolved.ReleaseAssetPaths -Force
|
||||
$sharedSettings | Add-Member -NotePropertyName releaseDir -NotePropertyValue $artifactsDirectory -Force
|
||||
if (Get-Command Set-EngineFact -ErrorAction SilentlyContinue) {
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'npm' -Name 'packageFile' -Value $resolved.PackageFile -Overwrite Replace -LegacyProperty 'packageFile'
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value $resolved.ReleaseArchiveInputs -Overwrite Replace -LegacyProperty 'releaseArchiveInputs'
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'assetPaths' -Value $resolved.ReleaseAssetPaths -Overwrite Replace -LegacyProperty 'releaseAssetPaths'
|
||||
if (Get-Command Set-EngineState -ErrorAction SilentlyContinue) {
|
||||
Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
throw "DiscoverPackageArtifacts found no .nupkg or .tgz files in: $artifactsDirectory"
|
||||
}
|
||||
|
||||
function Get-PluginMetadata {
|
||||
[pscustomobject]@{ mutatesRemote = $false }
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata
|
||||
@ -1,84 +0,0 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Loads release version from a repo-root VERSION file into shared context.
|
||||
|
||||
.DESCRIPTION
|
||||
Reads a single-line semver from the configured versionFilePath (default
|
||||
repo-root VERSION), including optional prerelease (0.1.0-alpha.1). Useful for
|
||||
repositories without .csproj or package.json version metadata. Declares
|
||||
providesVersion = $true so the engine can discover it as the single release
|
||||
version source.
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
|
||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
function Get-VersionFileSemverInternal {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$VersionFilePath
|
||||
)
|
||||
|
||||
if (-not (Test-Path $VersionFilePath -PathType Leaf)) {
|
||||
throw "FileReleaseVersion: VERSION file not found at '$VersionFilePath'."
|
||||
}
|
||||
|
||||
$version = (Get-Content -Path $VersionFilePath -Raw -Encoding UTF8).Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($version)) {
|
||||
throw "FileReleaseVersion: VERSION file is empty at '$VersionFilePath'."
|
||||
}
|
||||
|
||||
$version = $version -replace '^[vV]', ''
|
||||
Import-PluginDependency -ModuleName "ChangelogSupport" -RequiredCommand "Test-ReleaseSemver"
|
||||
if (-not (Test-ReleaseSemver -Version $version)) {
|
||||
throw "FileReleaseVersion: version '$version' in '$VersionFilePath' is not a valid semver (X.Y.Z or X.Y.Z-prerelease)."
|
||||
}
|
||||
|
||||
return $version
|
||||
}
|
||||
|
||||
function Get-PluginMetadata {
|
||||
return [pscustomobject]@{ providesVersion = $true }
|
||||
}
|
||||
|
||||
function Invoke-Plugin {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineState"
|
||||
Import-PluginDependency -ModuleName "ChangelogSupport" -RequiredCommand "Test-ReleaseSemver"
|
||||
|
||||
$shared = $Settings.context
|
||||
$versionFileSetting = if ($Settings.versionFilePath) {
|
||||
$Settings.versionFilePath
|
||||
}
|
||||
else {
|
||||
'..\..\..\VERSION'
|
||||
}
|
||||
|
||||
$versionFilePaths = @(Resolve-RelativePaths -Value $versionFileSetting -BasePath $shared.scriptDir)
|
||||
if ($versionFilePaths.Count -eq 0) {
|
||||
throw "FileReleaseVersion plugin requires 'versionFilePath' (repo-root VERSION file) in scriptSettings.json."
|
||||
}
|
||||
|
||||
$versionFilePath = $versionFilePaths[0]
|
||||
Write-Log -Level "INFO" -Message "Reading version from VERSION file (versionFilePath)..."
|
||||
$version = Get-VersionFileSemverInternal -VersionFilePath $versionFilePath
|
||||
|
||||
Set-EngineState -Context $shared -Name 'version' -Value $version
|
||||
Set-EngineFact -Context $shared -Namespace 'release' -Name 'versionFilePath' -Value $versionFilePath -Overwrite Replace -LegacyProperty 'versionFilePath'
|
||||
Write-Log -Level "OK" -Message " Release version loaded by FileReleaseVersion plugin: $version"
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata
|
||||
@ -1,238 +0,0 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Pester test plugin for the RepoUtils test engine.
|
||||
|
||||
.DESCRIPTION
|
||||
Runs the RepoUtils Pester suite and publishes normalized coverage metrics on the
|
||||
shared engine context for QualityGate.
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$pluginSupportModulePath = Join-Path $srcDir 'modules/Engine/PluginSupport.psm1'
|
||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
function Get-JaCoCoCoverageMetrics {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ReportPath
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ReportPath -PathType Leaf)) {
|
||||
return [PSCustomObject]@{
|
||||
Success = $false
|
||||
Error = "JaCoCo coverage report not found at: $ReportPath"
|
||||
}
|
||||
}
|
||||
|
||||
[xml]$reportXml = Get-Content -LiteralPath $ReportPath -Raw -Encoding UTF8
|
||||
$reportNode = $reportXml.report
|
||||
if ($null -eq $reportNode) {
|
||||
return [PSCustomObject]@{
|
||||
Success = $false
|
||||
Error = "Invalid JaCoCo report (missing root <report>): $ReportPath"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-CounterRate {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$CounterType
|
||||
)
|
||||
|
||||
$counter = @($reportNode.counter) | Where-Object { $_.type -eq $CounterType } | Select-Object -First 1
|
||||
if ($null -eq $counter) {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
$missed = [long]$counter.missed
|
||||
$covered = [long]$counter.covered
|
||||
$total = $missed + $covered
|
||||
if ($total -le 0) {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
return [math]::Round(($covered / $total) * 100, 1)
|
||||
}
|
||||
|
||||
function Get-CounterTotal {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$CounterType
|
||||
)
|
||||
|
||||
$counter = @($reportNode.counter) | Where-Object { $_.type -eq $CounterType } | Select-Object -First 1
|
||||
if ($null -eq $counter) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return [long]$counter.missed + [long]$counter.covered
|
||||
}
|
||||
|
||||
function Get-CounterCovered {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$CounterType
|
||||
)
|
||||
|
||||
$counter = @($reportNode.counter) | Where-Object { $_.type -eq $CounterType } | Select-Object -First 1
|
||||
if ($null -eq $counter) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return [long]$counter.covered
|
||||
}
|
||||
|
||||
return [PSCustomObject]@{
|
||||
Success = $true
|
||||
LineRate = Get-CounterRate -CounterType 'LINE'
|
||||
BranchRate = Get-CounterRate -CounterType 'BRANCH'
|
||||
MethodRate = Get-CounterRate -CounterType 'METHOD'
|
||||
TotalMethods = Get-CounterTotal -CounterType 'METHOD'
|
||||
CoveredMethods = Get-CounterCovered -CounterType 'METHOD'
|
||||
CoverageFile = $ReportPath
|
||||
CoverageFiles = @($ReportPath)
|
||||
}
|
||||
}
|
||||
|
||||
function Import-PesterModuleIfNeeded {
|
||||
if (-not (Get-Module -ListAvailable -Name Pester | Where-Object { $_.Version.Major -ge 5 })) {
|
||||
Write-Log -Level 'INFO' -Message 'Installing Pester 5...'
|
||||
Install-Module Pester -MinimumVersion 5.5.0 -MaximumVersion 5.99.99 -Scope CurrentUser -Force -SkipPublisherCheck
|
||||
}
|
||||
|
||||
Import-Module Pester -MinimumVersion 5.5.0 -MaximumVersion 5.99.99 -Force
|
||||
}
|
||||
|
||||
function Invoke-Plugin {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
Import-PluginDependency -ModuleName 'Logging' -RequiredCommand 'Write-Log'
|
||||
Import-PluginDependency -ModuleName 'EngineContext' -RequiredCommand 'Resolve-RelativePaths'
|
||||
|
||||
$pluginSettings = $Settings
|
||||
$sharedSettings = $Settings.context
|
||||
$scriptDir = $sharedSettings.scriptDir
|
||||
|
||||
if (-not $pluginSettings.testsDir) {
|
||||
throw "PesterTest plugin requires 'testsDir' in scriptSettings.json."
|
||||
}
|
||||
|
||||
$testsDirs = @(Resolve-RelativePaths -Value $pluginSettings.testsDir -BasePath $scriptDir)
|
||||
$testsDir = $testsDirs[0]
|
||||
|
||||
$configFileSetting = 'pester.config.ps1'
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$pluginSettings.configFile)) {
|
||||
$configFileSetting = [string]$pluginSettings.configFile
|
||||
}
|
||||
|
||||
$configPaths = @(Resolve-RelativePaths -Value $configFileSetting -BasePath $scriptDir)
|
||||
$configPath = $configPaths[0]
|
||||
|
||||
if (-not (Test-Path -LiteralPath $testsDir -PathType Container)) {
|
||||
throw "Pester tests directory not found at: $testsDir"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $configPath -PathType Leaf)) {
|
||||
throw "Pester config not found at: $configPath"
|
||||
}
|
||||
|
||||
Import-PesterModuleIfNeeded
|
||||
|
||||
Write-Log -Level 'STEP' -Message 'Running Pester tests...'
|
||||
Write-Log -Level 'INFO' -Message " Tests: $testsDir"
|
||||
Write-Log -Level 'INFO' -Message " Config: $configPath"
|
||||
|
||||
Push-Location $testsDir
|
||||
try {
|
||||
$config = & $configPath
|
||||
if ($null -eq $config) {
|
||||
throw "Pester config did not return a configuration object: $configPath"
|
||||
}
|
||||
|
||||
$config.Run.PassThru = $true
|
||||
$config.Run.Exit = $false
|
||||
|
||||
$resultsDirSetting = [string]$pluginSettings.resultsDir
|
||||
if ([string]::IsNullOrWhiteSpace($resultsDirSetting)) {
|
||||
$resultsDirSetting = '..\..\..\test-results'
|
||||
}
|
||||
$resultsDirectory = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $resultsDirSetting))
|
||||
New-Item -ItemType Directory -Path $resultsDirectory -Force | Out-Null
|
||||
|
||||
# Point Pester report outputs at the resolved results dir (overrides pester.config relative paths).
|
||||
if ($null -ne $config.TestResult -and $config.TestResult.Enabled) {
|
||||
$config.TestResult.OutputPath = Join-Path $resultsDirectory 'pester-results.xml'
|
||||
}
|
||||
if ($null -ne $config.CodeCoverage -and $config.CodeCoverage.Enabled) {
|
||||
$config.CodeCoverage.OutputPath = Join-Path $resultsDirectory 'coverage.xml'
|
||||
}
|
||||
|
||||
Write-Log -Level 'INFO' -Message " Results: $resultsDirectory"
|
||||
|
||||
$result = Invoke-Pester -Configuration $config
|
||||
if ($result.FailedCount -gt 0) {
|
||||
throw "Pester tests failed: $($result.FailedCount) failed, $($result.PassedCount) passed, $($result.SkippedCount) skipped."
|
||||
}
|
||||
|
||||
$coveragePath = Join-Path $resultsDirectory 'coverage.xml'
|
||||
$coverageMetrics = $null
|
||||
if (Test-Path -LiteralPath $coveragePath -PathType Leaf) {
|
||||
$coverageMetrics = Get-JaCoCoCoverageMetrics -ReportPath $coveragePath
|
||||
if (-not $coverageMetrics.Success) {
|
||||
throw $coverageMetrics.Error
|
||||
}
|
||||
}
|
||||
else {
|
||||
$coverageMetrics = [PSCustomObject]@{
|
||||
Success = $true
|
||||
LineRate = 0.0
|
||||
BranchRate = 0.0
|
||||
MethodRate = 0.0
|
||||
TotalMethods = 0
|
||||
CoveredMethods = 0
|
||||
CoverageFile = $null
|
||||
CoverageFiles = @()
|
||||
}
|
||||
}
|
||||
|
||||
$testResult = [PSCustomObject]@{
|
||||
Success = $true
|
||||
LineRate = $coverageMetrics.LineRate
|
||||
BranchRate = $coverageMetrics.BranchRate
|
||||
MethodRate = $coverageMetrics.MethodRate
|
||||
TotalMethods = $coverageMetrics.TotalMethods
|
||||
CoveredMethods = $coverageMetrics.CoveredMethods
|
||||
CoverageFile = $coverageMetrics.CoverageFile
|
||||
CoverageFiles = @($coverageMetrics.CoverageFiles)
|
||||
ResultsDirectory = $resultsDirectory
|
||||
PesterResult = $result
|
||||
}
|
||||
|
||||
Import-PluginDependency -ModuleName 'TestRunner' -RequiredCommand 'Publish-CoverageMetricsToSharedContext'
|
||||
Publish-CoverageMetricsToSharedContext -SharedSettings $sharedSettings -TestResult $testResult
|
||||
|
||||
Write-Log -Level 'OK' -Message " All tests passed! ($($result.PassedCount) passed)"
|
||||
Write-Log -Level 'INFO' -Message " Line Coverage: $($testResult.LineRate)%"
|
||||
Write-Log -Level 'INFO' -Message " Branch Coverage: $($testResult.BranchRate)%"
|
||||
Write-Log -Level 'INFO' -Message " Method Coverage: $($testResult.MethodRate)%"
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
function Get-PluginMetadata {
|
||||
[pscustomobject]@{ mutatesRemote = $false }
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata, Get-JaCoCoCoverageMetrics
|
||||
Loading…
Reference in New Issue
Block a user