Compare commits

..

1 Commits
v2.0.6 ... main

Author SHA1 Message Date
Maksym Sadovnychyy
7958f3c961 (feature): add FromActionResult and FromHttpResponseAsync; preserve redirect status 2026-10-28 19:07:37 +01:00
27 changed files with 809 additions and 1425 deletions

View File

@ -5,6 +5,23 @@ 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/), 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). 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 ## [2.0.6] - 2026-10-27
### Changed ### Changed

View File

@ -26,7 +26,7 @@ dotnet build MaksIT.Results.slnx
### Running Tests ### 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 ```bash
cd src cd src

156
README.md
View File

@ -1,38 +1,57 @@
# MaksIT.Results # MaksIT.Results
![Line Coverage](https://img.shields.io/badge/Line%20Coverage-18.4%25-orange) [![NuGet](https://img.shields.io/nuget/v/MaksIT.Results)](https://www.nuget.org/packages/MaksIT.Results)
![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-76.3%25-green) [![License: MIT](https://img.shields.io/badge/License-MIT-blue)](LICENSE.md)
![Method Coverage](https://img.shields.io/badge/Method%20Coverage-12.8%25-orange)
`MaksIT.Results` is a .NET library for modeling operation outcomes as HTTP-aware result objects and converting them to `IActionResult` in ASP.NET Core. ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-44.8%25-yellowgreen)
![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-54.7%25-yellowgreen)
![Method Coverage](https://img.shields.io/badge/Method%20Coverage-20.5%25-yellow)
## 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. **Target:** .NET 10 (`net10.0`)
- 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`).
## 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 **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()`.
Install-Package MaksIT.Results
```
`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 ```bash
dotnet add package MaksIT.Results dotnet add package MaksIT.Results
``` ```
## Target Framework ```powershell
Install-Package MaksIT.Results
```
- `.NET 10` (`net10.0`) ## Quick start
## Quick Start
### Create results ### Create results
@ -42,33 +61,30 @@ using MaksIT.Results;
Result ok = Result.Ok("Operation completed"); Result ok = Result.Ok("Operation completed");
Result failed = Result.BadRequest("Validation failed"); Result failed = Result.BadRequest("Validation failed");
Result<int> okWithValue = Result<int>.Ok(42, "Answer generated"); Result<int> answer = Result<int>.Ok(42, "Answer generated");
Result<string?> notFound = Result<string?>.NotFound(null, "Entity not found"); Result<UserDto?> missing = Result<UserDto?>.NotFound(null, "Entity not found");
``` ```
### Convert between result types ### Map between result shapes
```csharp ```csharp
using MaksIT.Results;
Result<int> source = Result<int>.Ok(42, "Value loaded"); Result<int> source = Result<int>.Ok(42, "Value loaded");
// Result<T> -> Result<U> Result<string?> asString = source.ToResultOfType(v => v?.ToString());
Result<string?> mapped = source.ToResultOfType(v => v?.ToString());
// Result<T> -> Result
Result nonGeneric = source.ToResult(); Result nonGeneric = source.ToResult();
``` ```
### Use in an ASP.NET Core controller ### Controllers
```csharp ```csharp
using MaksIT.Results; using MaksIT.Results;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("users")]
public sealed class UsersController : ControllerBase { public sealed class UsersController : ControllerBase {
[HttpGet("{id:guid}")] [HttpGet("{id:guid}")]
public IActionResult GetUser(Guid id) { public IActionResult Get(Guid id) {
Result<UserDto?> result = id == Guid.Empty Result<UserDto?> result = id == Guid.Empty
? Result<UserDto?>.BadRequest(null, "Invalid id") ? Result<UserDto?>.BadRequest(null, "Invalid id")
: Result<UserDto?>.Ok(new UserDto(id, "maks"), "User loaded"); : 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); public sealed record UserDto(Guid Id, string Name);
``` ```
## `ToActionResult()` Behavior ### HTTP clients
- `Result` success: returns status-code-only response. Parse responses produced in the same contract (JSON body, redirect `Location`, or `problem+json`):
- `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. ```csharp
- Any failure: returns RFC 7807-style `ProblemDetails` JSON with: using HttpResponseMessage response = await http.GetAsync(url);
- `status` = result status code
- `title` = `"An error occurred"` Result bare = await Result.FromHttpResponseAsync(response);
- `detail` = joined `Messages` Result<UserDto> typed = await Result<UserDto>.FromHttpResponseAsync(response);
- content type `application/problem+json` ```
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 ## 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 ```csharp
builder.Services.AddControllers() 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. | Family | Examples |
- Success: `Result.Ok(...)`, `Result.Created(...)`, `Result.NoContent(...)`, etc. |--------|----------|
- Redirection: `Result.Found(...)`, `Result.PermanentRedirect(...)`, etc. | Informational | `Continue`, `SwitchingProtocols`, `Processing`, ... |
- Client error: `Result.BadRequest(...)`, `Result.NotFound(...)`, `Result.TooManyRequests(...)`, etc. | Success | `Ok`, `Created`, `NoContent`, ... |
- Server error: `Result.InternalServerError(...)`, `Result.ServiceUnavailable(...)`, etc. | 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 ## Contributing
See `CONTRIBUTING.md`. See [CONTRIBUTING.md](CONTRIBUTING.md). Changelog: [CHANGELOG.md](CHANGELOG.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)
## License ## License
See `LICENSE.md`. [MIT](LICENSE.md) - Maksym Sadovnychyy (MAKS-IT)
**Contact:** [maksym.sadovnychyy@gmail.com](mailto:maksym.sadovnychyy@gmail.com)

View File

@ -3,7 +3,6 @@ using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using MaksIT.Results.Mvc; using MaksIT.Results.Mvc;
namespace MaksIT.Results.Tests.Mvc; namespace MaksIT.Results.Tests.Mvc;

View 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);
}

View 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);
}

View File

@ -1,5 +1,4 @@
using System.Net; using System.Net;
using Microsoft.AspNetCore.Mvc;
using MaksIT.Results.Mvc; using MaksIT.Results.Mvc;
namespace MaksIT.Results.Tests; namespace MaksIT.Results.Tests;
@ -11,8 +10,8 @@ public class ResultToActionResultTests {
var actionResult = result.ToActionResult(); var actionResult = result.ToActionResult();
Assert.IsType<StatusCodeResult>(actionResult); var statusCodeResult = Assert.IsType<StatusCodeResult>(actionResult);
var statusCodeResult = (StatusCodeResult)actionResult;
Assert.Equal((int)HttpStatusCode.OK, statusCodeResult.StatusCode); Assert.Equal((int)HttpStatusCode.OK, statusCodeResult.StatusCode);
} }
@ -23,11 +22,12 @@ public class ResultToActionResultTests {
var actionResult = result.ToActionResult(); var actionResult = result.ToActionResult();
Assert.IsType<ObjectResult>(actionResult); var objectResult = Assert.IsType<ObjectResult>(actionResult);
var objectResult = (ObjectResult)actionResult;
Assert.Equal((int)HttpStatusCode.BadRequest, objectResult.StatusCode); 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((int)HttpStatusCode.BadRequest, problemDetails.Status);
Assert.Equal("An error occurred", problemDetails.Title); Assert.Equal("An error occurred", problemDetails.Title);
Assert.Equal(errorMessage, problemDetails.Detail); Assert.Equal(errorMessage, problemDetails.Detail);
@ -40,20 +40,21 @@ public class ResultToActionResultTests {
var actionResult = result.ToActionResult(); var actionResult = result.ToActionResult();
Assert.IsType<ObjectResult>(actionResult); var objectResult = Assert.IsType<ObjectResult>(actionResult);
var objectResult = (ObjectResult)actionResult;
Assert.Equal((int)HttpStatusCode.OK, objectResult.StatusCode); Assert.Equal((int)HttpStatusCode.OK, objectResult.StatusCode);
Assert.Equal(value, objectResult.Value); Assert.Equal(value, objectResult.Value);
} }
[Fact] [Fact]
public void ToActionResult_WhenGenericSuccessWithRedirect_ReturnsRedirectResult() { public void ToActionResult_WhenGenericSuccessWithRedirect_ReturnsRedirectResult() {
var result = Result<string>.Found("https://example.com/continue"); var result = Result<string>.Found("https://example.com/continue");
var actionResult = result.ToActionResult(); var actionResult = result.ToActionResult();
Assert.IsType<RedirectResult>(actionResult); var redirectResult = Assert.IsType<RedirectResult>(actionResult);
var redirectResult = (RedirectResult)actionResult;
Assert.Equal("https://example.com/continue", redirectResult.Location); Assert.Equal("https://example.com/continue", redirectResult.Location);
Assert.Equal((int)HttpStatusCode.Found, redirectResult.StatusCode);
} }
} }

View File

@ -8,7 +8,7 @@
<!-- NuGet package metadata --> <!-- NuGet package metadata -->
<PackageId>MaksIT.Results</PackageId> <PackageId>MaksIT.Results</PackageId>
<Version>2.0.6</Version> <Version>2.1.0</Version>
<Authors>Maksym Sadovnychyy</Authors> <Authors>Maksym Sadovnychyy</Authors>
<Company>MAKS-IT</Company> <Company>MAKS-IT</Company>
<Product>MaksIT.Results</Product> <Product>MaksIT.Results</Product>
@ -22,6 +22,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MaksIT.Core" Version="1.6.14" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.3.13" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.3.13" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.14" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.14" />
<PackageReference Include="Microsoft.Extensions.Options" 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="..\..\LICENSE.md" Pack="true" PackagePath="\" Link="LICENSE.md" />
<None Include="..\..\README.md" Pack="true" PackagePath="\" Link="README.md" /> <None Include="..\..\README.md" Pack="true" PackagePath="\" Link="README.md" />
<None Include="..\..\CHANGELOG.md" Pack="true" PackagePath="\" Link="CHANGELOG.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> </ItemGroup>
</Project> </Project>

View File

@ -1,4 +1,3 @@
using System.Text.Json; using System.Text.Json;
namespace MaksIT.Results.Mvc; namespace MaksIT.Results.Mvc;

View File

@ -2,7 +2,6 @@
namespace MaksIT.Results.Mvc; namespace MaksIT.Results.Mvc;
/// <summary> /// <summary>
/// A machine-readable format for specifying errors in HTTP API responses based on <see href="https://tools.ietf.org/html/rfc7807"/>. /// A machine-readable format for specifying errors in HTTP API responses based on <see href="https://tools.ietf.org/html/rfc7807"/>.
/// </summary> /// </summary>

View File

@ -2,11 +2,30 @@ using Microsoft.AspNetCore.Mvc;
namespace MaksIT.Results.Mvc; namespace MaksIT.Results.Mvc;
public class RedirectResult(string location) : IActionResult { /// <summary>
public string Location { get; } = location; /// 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) { 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; return Task.CompletedTask;
} }
} }

View File

@ -1,17 +1,12 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace MaksIT.Results.Mvc; namespace MaksIT.Results.Mvc;
public class StatusCodeResult : IActionResult { public class StatusCodeResult(int statusCode) : IActionResult {
public int StatusCode { get; } public int StatusCode { get; } = statusCode;
public StatusCodeResult(int statusCode) {
StatusCode = statusCode;
}
public async Task ExecuteResultAsync(ActionContext context) { public async Task ExecuteResultAsync(ActionContext context) {
context.HttpContext.Response.StatusCode = StatusCode; context.HttpContext.Response.StatusCode = StatusCode;
await Task.CompletedTask; await Task.CompletedTask;
} }
} }

View File

@ -1,6 +1,5 @@
using System.Net; using System.Net;
namespace MaksIT.Results; namespace MaksIT.Results;
public partial class Result { 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. /// Returns a result indicating that the server could not understand the request due to invalid syntax.
/// Corresponds to HTTP status code 400 Bad Request. /// Corresponds to HTTP status code 400 Bad Request.
/// </summary> /// </summary>
public static Result BadRequest(params string [] messages) { public static Result BadRequest(params string [] messages) =>
return new Result(false, [..messages], HttpStatusCode.BadRequest); new (false, [..messages], HttpStatusCode.BadRequest);
}
/// <summary> /// <summary>
/// Returns a result indicating that the client must authenticate itself to get the requested response. /// Returns a result indicating that the client must authenticate itself to get the requested response.
/// Corresponds to HTTP status code 401 Unauthorized. /// Corresponds to HTTP status code 401 Unauthorized.
/// </summary> /// </summary>
public static Result Unauthorized(params string [] messages) { public static Result Unauthorized(params string [] messages) =>
return new Result(false, [..messages], HttpStatusCode.Unauthorized); new (false, [..messages], HttpStatusCode.Unauthorized);
}
/// <summary> /// <summary>
/// Returns a result indicating that the client does not have access rights to the content. /// Returns a result indicating that the client does not have access rights to the content.
/// Corresponds to HTTP status code 403 Forbidden. /// Corresponds to HTTP status code 403 Forbidden.
/// </summary> /// </summary>
public static Result Forbidden(params string [] messages) { public static Result Forbidden(params string [] messages) =>
return new Result(false, [..messages], HttpStatusCode.Forbidden); new (false, [..messages], HttpStatusCode.Forbidden);
}
/// <summary> /// <summary>
/// Returns a result indicating that the server can not find the requested resource. /// Returns a result indicating that the server can not find the requested resource.
/// Corresponds to HTTP status code 404 Not Found. /// Corresponds to HTTP status code 404 Not Found.
/// </summary> /// </summary>
public static Result NotFound(params string [] messages) { public static Result NotFound(params string [] messages) =>
return new Result(false, [.. messages], HttpStatusCode.NotFound); new (false, [.. messages], HttpStatusCode.NotFound);
}
/// <summary> /// <summary>
/// Returns a result indicating that the request could not be completed due to a conflict with the current state of the resource. /// 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. /// Corresponds to HTTP status code 409 Conflict.
/// </summary> /// </summary>
public static Result Conflict(params string [] messages) { public static Result Conflict(params string [] messages) =>
return new Result(false, [..messages], HttpStatusCode.Conflict); new (false, [..messages], HttpStatusCode.Conflict);
}
/// <summary> /// <summary>
/// Returns a result indicating that the requested resource is no longer available and will not be available again. /// 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. /// Corresponds to HTTP status code 410 Gone.
/// </summary> /// </summary>
public static Result Gone(params string [] messages) { public static Result Gone(params string [] messages) =>
return new Result(false, [..messages], (HttpStatusCode)410); // 410 Gone new (false, [..messages], (HttpStatusCode)410); // 410 Gone
}
/// <summary> /// <summary>
/// Returns a result indicating that the server cannot process the request because it expects the request to have a defined Content-Length header. /// 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. /// Corresponds to HTTP status code 411 Length Required.
/// </summary> /// </summary>
public static Result LengthRequired(params string [] messages) { public static Result LengthRequired(params string [] messages) =>
return new Result(false, [..messages], (HttpStatusCode)411); // 411 Length Required new (false, [..messages], (HttpStatusCode)411); // 411 Length Required
}
/// <summary> /// <summary>
/// Returns a result indicating that the server cannot process the request entity because it is too large. /// 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. /// Corresponds to HTTP status code 413 Payload Too Large.
/// </summary> /// </summary>
public static Result PayloadTooLarge(params string [] messages) { public static Result PayloadTooLarge(params string [] messages) =>
return new Result(false, [..messages], (HttpStatusCode)413); // 413 Payload Too Large new (false, [..messages], (HttpStatusCode)413); // 413 Payload Too Large
}
/// <summary> /// <summary>
/// Returns a result indicating that the server cannot process the request because the URI is too long. /// 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. /// Corresponds to HTTP status code 414 URI Too Long.
/// </summary> /// </summary>
public static Result UriTooLong(params string [] messages) { public static Result UriTooLong(params string [] messages) =>
return new Result(false, [..messages], (HttpStatusCode)414); // 414 URI Too Long new (false, [..messages], (HttpStatusCode)414); // 414 URI Too Long
}
/// <summary> /// <summary>
/// Returns a result indicating that the server cannot process the request because the media type is unsupported. /// 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. /// Corresponds to HTTP status code 415 Unsupported Media Type.
/// </summary> /// </summary>
public static Result UnsupportedMediaType(params string [] messages) { public static Result UnsupportedMediaType(params string [] messages) =>
return new Result(false, [..messages], HttpStatusCode.UnsupportedMediaType); new (false, [..messages], HttpStatusCode.UnsupportedMediaType);
}
#endregion #endregion
@ -95,153 +84,134 @@ public partial class Result {
/// Returns a result indicating that payment is required to access the requested resource. /// Returns a result indicating that payment is required to access the requested resource.
/// Corresponds to HTTP status code 402 Payment Required. /// Corresponds to HTTP status code 402 Payment Required.
/// </summary> /// </summary>
public static Result PaymentRequired(params string [] messages) { public static Result PaymentRequired(params string [] messages) =>
return new Result(false, [..messages], (HttpStatusCode)402); // 402 Payment Required new (false, [..messages], (HttpStatusCode)402); // 402 Payment Required
}
/// <summary> /// <summary>
/// Returns a result indicating that the request method is known by the server but is not supported by the target resource. /// 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. /// Corresponds to HTTP status code 405 Method Not Allowed.
/// </summary> /// </summary>
public static Result MethodNotAllowed(params string [] messages) { public static Result MethodNotAllowed(params string [] messages) =>
return new Result(false, [..messages], (HttpStatusCode)405); // 405 Method Not Allowed new (false, [..messages], (HttpStatusCode)405); // 405 Method Not Allowed
}
/// <summary> /// <summary>
/// Returns a result indicating that the server cannot produce a response matching the list of acceptable values defined in the request headers. /// 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. /// Corresponds to HTTP status code 406 Not Acceptable.
/// </summary> /// </summary>
public static Result NotAcceptable(params string [] messages) { public static Result NotAcceptable(params string [] messages) =>
return new Result(false, [..messages], (HttpStatusCode)406); // 406 Not Acceptable new (false, [..messages], (HttpStatusCode)406); // 406 Not Acceptable
}
/// <summary> /// <summary>
/// Returns a result indicating that authentication with a proxy is required. /// Returns a result indicating that authentication with a proxy is required.
/// Corresponds to HTTP status code 407 Proxy Authentication Required. /// Corresponds to HTTP status code 407 Proxy Authentication Required.
/// </summary> /// </summary>
public static Result ProxyAuthenticationRequired(params string [] messages) { public static Result ProxyAuthenticationRequired(params string [] messages) =>
return new Result(false, [..messages], (HttpStatusCode)407); // 407 Proxy Authentication Required new (false, [..messages], (HttpStatusCode)407); // 407 Proxy Authentication Required
}
/// <summary> /// <summary>
/// Returns a result indicating that the server timed out waiting for the request. /// Returns a result indicating that the server timed out waiting for the request.
/// Corresponds to HTTP status code 408 Request Timeout. /// Corresponds to HTTP status code 408 Request Timeout.
/// </summary> /// </summary>
public static Result RequestTimeout(params string [] messages) { public static Result RequestTimeout(params string [] messages) =>
return new Result(false, [..messages], (HttpStatusCode)408); // 408 Request Timeout new (false, [..messages], (HttpStatusCode)408); // 408 Request Timeout
}
/// <summary> /// <summary>
/// Returns a result indicating that one or more conditions given in the request header fields evaluated to false. /// 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. /// Corresponds to HTTP status code 412 Precondition Failed.
/// </summary> /// </summary>
public static Result PreconditionFailed(params string [] messages) { public static Result PreconditionFailed(params string [] messages) =>
return new Result(false, [..messages], (HttpStatusCode)412); // 412 Precondition Failed new (false, [..messages], (HttpStatusCode)412); // 412 Precondition Failed
}
/// <summary> /// <summary>
/// Returns a result indicating that the range specified by the Range header field cannot be fulfilled. /// 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. /// Corresponds to HTTP status code 416 Range Not Satisfiable.
/// </summary> /// </summary>
public static Result RangeNotSatisfiable(params string [] messages) { public static Result RangeNotSatisfiable(params string [] messages) =>
return new Result(false, [..messages], (HttpStatusCode)416); // 416 Range Not Satisfiable new (false, [..messages], (HttpStatusCode)416); // 416 Range Not Satisfiable
}
/// <summary> /// <summary>
/// Returns a result indicating that the expectation given in the request's Expect header could not be met. /// 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. /// Corresponds to HTTP status code 417 Expectation Failed.
/// </summary> /// </summary>
public static Result ExpectationFailed(params string [] messages) { public static Result ExpectationFailed(params string [] messages) =>
return new Result(false, [..messages], (HttpStatusCode)417); // 417 Expectation Failed new (false, [..messages], (HttpStatusCode)417); // 417 Expectation Failed
}
/// <summary> /// <summary>
/// Returns a result indicating that the server refuses to brew coffee because it is, permanently, a teapot. /// 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. /// Corresponds to HTTP status code 418 I'm a teapot.
/// </summary> /// </summary>
public static Result ImATeapot(params string [] messages) { public static Result ImATeapot(params string [] messages) =>
return new Result(false, [..messages], (HttpStatusCode)418); // 418 I'm a teapot new (false, [..messages], (HttpStatusCode)418); // 418 I'm a teapot
}
/// <summary> /// <summary>
/// Returns a result indicating that the request was directed at a server that is not able to produce a response. /// 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. /// Corresponds to HTTP status code 421 Misdirected Request.
/// </summary> /// </summary>
public static Result MisdirectedRequest(params string [] messages) { public static Result MisdirectedRequest(params string [] messages) =>
return new Result(false, [..messages], (HttpStatusCode)421); // 421 Misdirected Request new (false, [..messages], (HttpStatusCode)421); // 421 Misdirected Request
}
/// <summary> /// <summary>
/// Returns a result indicating that the server cannot process the request due to an illegal request entity. /// 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. /// Corresponds to HTTP status code 422 Unprocessable Entity.
/// </summary> /// </summary>
public static Result UnprocessableEntity(params string [] messages) { public static Result UnprocessableEntity(params string [] messages) =>
return new Result(false, [..messages], (HttpStatusCode)422); // 422 Unprocessable Entity new (false, [..messages], (HttpStatusCode)422); // 422 Unprocessable Entity
}
/// <summary> /// <summary>
/// Returns a result indicating that access to the target resource is denied because the resource is locked. /// Returns a result indicating that access to the target resource is denied because the resource is locked.
/// Corresponds to HTTP status code 423 Locked. /// Corresponds to HTTP status code 423 Locked.
/// </summary> /// </summary>
public static Result Locked(params string [] messages) { public static Result Locked(params string [] messages) =>
return new Result(false, [..messages], (HttpStatusCode)423); // 423 Locked new (false, [..messages], (HttpStatusCode)423); // 423 Locked
}
/// <summary> /// <summary>
/// Returns a result indicating that the request failed because it depended on another request and that request failed. /// 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. /// Corresponds to HTTP status code 424 Failed Dependency.
/// </summary> /// </summary>
public static Result FailedDependency(params string [] messages) { public static Result FailedDependency(params string [] messages) =>
return new Result(false, [..messages], (HttpStatusCode)424); // 424 Failed Dependency new (false, [..messages], (HttpStatusCode)424); // 424 Failed Dependency
}
/// <summary> /// <summary>
/// Returns a result indicating that the server is unwilling to risk processing a request that might be replayed. /// 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. /// Corresponds to HTTP status code 425 Too Early.
/// </summary> /// </summary>
public static Result TooEarly(params string [] messages) { public static Result TooEarly(params string [] messages) =>
return new Result(false, [..messages], (HttpStatusCode)425); // 425 Too Early new (false, [..messages], (HttpStatusCode)425); // 425 Too Early
}
/// <summary> /// <summary>
/// Returns a result indicating that the server refuses to perform the request using the current protocol. /// Returns a result indicating that the server refuses to perform the request using the current protocol.
/// Corresponds to HTTP status code 426 Upgrade Required. /// Corresponds to HTTP status code 426 Upgrade Required.
/// </summary> /// </summary>
public static Result UpgradeRequired(params string [] messages) { public static Result UpgradeRequired(params string [] messages) =>
return new Result(false, [..messages], (HttpStatusCode)426); // 426 Upgrade Required new (false, [..messages], (HttpStatusCode)426); // 426 Upgrade Required
}
/// <summary> /// <summary>
/// Returns a result indicating that the server requires the request to be conditional. /// Returns a result indicating that the server requires the request to be conditional.
/// Corresponds to HTTP status code 428 Precondition Required. /// Corresponds to HTTP status code 428 Precondition Required.
/// </summary> /// </summary>
public static Result PreconditionRequired(params string [] messages) { public static Result PreconditionRequired(params string [] messages) =>
return new Result(false, [..messages], (HttpStatusCode)428); // 428 Precondition Required new (false, [..messages], (HttpStatusCode)428); // 428 Precondition Required
}
/// <summary> /// <summary>
/// Returns a result indicating that the user has sent too many requests in a given amount of time. /// 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. /// Corresponds to HTTP status code 429 Too Many Requests.
/// </summary> /// </summary>
public static Result TooManyRequests(params string [] messages) { public static Result TooManyRequests(params string [] messages) =>
return new Result(false, [..messages], (HttpStatusCode)429); // 429 Too Many Requests new (false, [..messages], (HttpStatusCode)429); // 429 Too Many Requests
}
/// <summary> /// <summary>
/// Returns a result indicating that the server is unwilling to process the request because its header fields are too large. /// 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. /// Corresponds to HTTP status code 431 Request Header Fields Too Large.
/// </summary> /// </summary>
public static Result RequestHeaderFieldsTooLarge(params string [] messages) { public static Result RequestHeaderFieldsTooLarge(params string [] messages) =>
return new Result(false, [..messages], (HttpStatusCode)431); // 431 Request Header Fields Too Large new (false, [..messages], (HttpStatusCode)431); // 431 Request Header Fields Too Large
}
/// <summary> /// <summary>
/// Returns a result indicating that access to the requested resource is denied for legal reasons. /// 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. /// Corresponds to HTTP status code 451 Unavailable For Legal Reasons.
/// </summary> /// </summary>
public static Result UnavailableForLegalReasons(params string [] messages) { public static Result UnavailableForLegalReasons(params string [] messages) =>
return new Result(false, [..messages], (HttpStatusCode)451); // 451 Unavailable For Legal Reasons new (false, [..messages], (HttpStatusCode)451); // 451 Unavailable For Legal Reasons
}
#endregion #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. /// Returns a result indicating that the server could not understand the request due to invalid syntax.
/// Corresponds to HTTP status code 400 Bad Request. /// Corresponds to HTTP status code 400 Bad Request.
/// </summary> /// </summary>
public static Result<T> BadRequest(T? value, params string [] messages) { public static Result<T> BadRequest(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], HttpStatusCode.BadRequest); new (value, false, [..messages], HttpStatusCode.BadRequest);
}
/// <summary> /// <summary>
/// Returns a result indicating that the client must authenticate itself to get the requested response. /// Returns a result indicating that the client must authenticate itself to get the requested response.
/// Corresponds to HTTP status code 401 Unauthorized. /// Corresponds to HTTP status code 401 Unauthorized.
/// </summary> /// </summary>
public static Result<T> Unauthorized(T? value, params string [] messages) { public static Result<T> Unauthorized(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], HttpStatusCode.Unauthorized); new (value, false, [..messages], HttpStatusCode.Unauthorized);
}
/// <summary> /// <summary>
/// Returns a result indicating that the client does not have access rights to the content. /// Returns a result indicating that the client does not have access rights to the content.
/// Corresponds to HTTP status code 403 Forbidden. /// Corresponds to HTTP status code 403 Forbidden.
/// </summary> /// </summary>
public static Result<T> Forbidden(T? value, params string [] messages) { public static Result<T> Forbidden(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], HttpStatusCode.Forbidden); new (value, false, [..messages], HttpStatusCode.Forbidden);
}
/// <summary> /// <summary>
/// Returns a result indicating that the server can not find the requested resource. /// Returns a result indicating that the server can not find the requested resource.
/// Corresponds to HTTP status code 404 Not Found. /// Corresponds to HTTP status code 404 Not Found.
/// </summary> /// </summary>
public static Result<T> NotFound(T? value, params string [] messages) { public static Result<T> NotFound(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], HttpStatusCode.NotFound); new (value, false, [..messages], HttpStatusCode.NotFound);
}
/// <summary> /// <summary>
/// Returns a result indicating that the request could not be completed due to a conflict with the current state of the resource. /// 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. /// Corresponds to HTTP status code 409 Conflict.
/// </summary> /// </summary>
public static Result<T> Conflict(T? value, params string [] messages) { public static Result<T> Conflict(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], HttpStatusCode.Conflict); new (value, false, [..messages], HttpStatusCode.Conflict);
}
/// <summary> /// <summary>
/// Returns a result indicating that the requested resource is no longer available and will not be available again. /// 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. /// Corresponds to HTTP status code 410 Gone.
/// </summary> /// </summary>
public static Result<T> Gone(T? value, params string [] messages) { public static Result<T> Gone(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], (HttpStatusCode)410); // 410 Gone new (value, false, [..messages], (HttpStatusCode)410); // 410 Gone
}
/// <summary> /// <summary>
/// Returns a result indicating that the server cannot process the request because it expects the request to have a defined Content-Length header. /// 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. /// Corresponds to HTTP status code 411 Length Required.
/// </summary> /// </summary>
public static Result<T> LengthRequired(T? value, params string [] messages) { public static Result<T> LengthRequired(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], (HttpStatusCode)411); // 411 Length Required new (value, false, [..messages], (HttpStatusCode)411); // 411 Length Required
}
/// <summary> /// <summary>
/// Returns a result indicating that the server cannot process the request entity because it is too large. /// 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. /// Corresponds to HTTP status code 413 Payload Too Large.
/// </summary> /// </summary>
public static Result<T> PayloadTooLarge(T? value, params string [] messages) { public static Result<T> PayloadTooLarge(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], (HttpStatusCode)413); // 413 Payload Too Large new (value, false, [..messages], (HttpStatusCode)413); // 413 Payload Too Large
}
/// <summary> /// <summary>
/// Returns a result indicating that the server cannot process the request because the URI is too long. /// 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. /// Corresponds to HTTP status code 414 URI Too Long.
/// </summary> /// </summary>
public static Result<T> UriTooLong(T? value, params string [] messages) { public static Result<T> UriTooLong(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], (HttpStatusCode)414); // 414 URI Too Long new (value, false, [..messages], (HttpStatusCode)414); // 414 URI Too Long
}
/// <summary> /// <summary>
/// Returns a result indicating that the server cannot process the request because the media type is unsupported. /// 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. /// Corresponds to HTTP status code 415 Unsupported Media Type.
/// </summary> /// </summary>
public static Result<T> UnsupportedMediaType(T? value, params string [] messages) { public static Result<T> UnsupportedMediaType(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], HttpStatusCode.UnsupportedMediaType); new (value, false, [..messages], HttpStatusCode.UnsupportedMediaType);
}
#endregion #endregion
@ -338,153 +298,134 @@ public partial class Result<T> : Result {
/// Returns a result indicating that payment is required to access the requested resource. /// Returns a result indicating that payment is required to access the requested resource.
/// Corresponds to HTTP status code 402 Payment Required. /// Corresponds to HTTP status code 402 Payment Required.
/// </summary> /// </summary>
public static Result<T> PaymentRequired(T? value, params string [] messages) { public static Result<T> PaymentRequired(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], (HttpStatusCode)402); // 402 Payment Required new (value, false, [..messages], (HttpStatusCode)402); // 402 Payment Required
}
/// <summary> /// <summary>
/// Returns a result indicating that the request method is known by the server but is not supported by the target resource. /// 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. /// Corresponds to HTTP status code 405 Method Not Allowed.
/// </summary> /// </summary>
public static Result<T> MethodNotAllowed(T? value, params string [] messages) { public static Result<T> MethodNotAllowed(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], (HttpStatusCode)405); // 405 Method Not Allowed new (value, false, [..messages], (HttpStatusCode)405); // 405 Method Not Allowed
}
/// <summary> /// <summary>
/// Returns a result indicating that the server cannot produce a response matching the list of acceptable values defined in the request headers. /// 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. /// Corresponds to HTTP status code 406 Not Acceptable.
/// </summary> /// </summary>
public static Result<T> NotAcceptable(T? value, params string [] messages) { public static Result<T> NotAcceptable(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], (HttpStatusCode)406); // 406 Not Acceptable new (value, false, [..messages], (HttpStatusCode)406); // 406 Not Acceptable
}
/// <summary> /// <summary>
/// Returns a result indicating that authentication with a proxy is required. /// Returns a result indicating that authentication with a proxy is required.
/// Corresponds to HTTP status code 407 Proxy Authentication Required. /// Corresponds to HTTP status code 407 Proxy Authentication Required.
/// </summary> /// </summary>
public static Result<T> ProxyAuthenticationRequired(T? value, params string [] messages) { public static Result<T> ProxyAuthenticationRequired(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], (HttpStatusCode)407); // 407 Proxy Authentication Required new (value, false, [..messages], (HttpStatusCode)407); // 407 Proxy Authentication Required
}
/// <summary> /// <summary>
/// Returns a result indicating that the server timed out waiting for the request. /// Returns a result indicating that the server timed out waiting for the request.
/// Corresponds to HTTP status code 408 Request Timeout. /// Corresponds to HTTP status code 408 Request Timeout.
/// </summary> /// </summary>
public static Result<T> RequestTimeout(T? value, params string [] messages) { public static Result<T> RequestTimeout(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], (HttpStatusCode)408); // 408 Request Timeout new (value, false, [..messages], (HttpStatusCode)408); // 408 Request Timeout
}
/// <summary> /// <summary>
/// Returns a result indicating that one or more conditions given in the request header fields evaluated to false. /// 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. /// Corresponds to HTTP status code 412 Precondition Failed.
/// </summary> /// </summary>
public static Result<T> PreconditionFailed(T? value, params string [] messages) { public static Result<T> PreconditionFailed(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], (HttpStatusCode)412); // 412 Precondition Failed new (value, false, [..messages], (HttpStatusCode)412); // 412 Precondition Failed
}
/// <summary> /// <summary>
/// Returns a result indicating that the range specified by the Range header field cannot be fulfilled. /// 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. /// Corresponds to HTTP status code 416 Range Not Satisfiable.
/// </summary> /// </summary>
public static Result<T> RangeNotSatisfiable(T? value, params string [] messages) { public static Result<T> RangeNotSatisfiable(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], (HttpStatusCode)416); // 416 Range Not Satisfiable new (value, false, [..messages], (HttpStatusCode)416); // 416 Range Not Satisfiable
}
/// <summary> /// <summary>
/// Returns a result indicating that the expectation given in the request's Expect header could not be met. /// 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. /// Corresponds to HTTP status code 417 Expectation Failed.
/// </summary> /// </summary>
public static Result<T> ExpectationFailed(T? value, params string [] messages) { public static Result<T> ExpectationFailed(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], (HttpStatusCode)417); // 417 Expectation Failed new (value, false, [..messages], (HttpStatusCode)417); // 417 Expectation Failed
}
/// <summary> /// <summary>
/// Returns a result indicating that the server refuses to brew coffee because it is, permanently, a teapot. /// 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. /// Corresponds to HTTP status code 418 I'm a teapot.
/// </summary> /// </summary>
public static Result<T> ImATeapot(T? value, params string [] messages) { public static Result<T> ImATeapot(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], (HttpStatusCode)418); // 418 I'm a teapot new (value, false, [..messages], (HttpStatusCode)418); // 418 I'm a teapot
}
/// <summary> /// <summary>
/// Returns a result indicating that the request was directed at a server that is not able to produce a response. /// 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. /// Corresponds to HTTP status code 421 Misdirected Request.
/// </summary> /// </summary>
public static Result<T> MisdirectedRequest(T? value, params string [] messages) { public static Result<T> MisdirectedRequest(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], (HttpStatusCode)421); // 421 Misdirected Request new (value, false, [..messages], (HttpStatusCode)421); // 421 Misdirected Request
}
/// <summary> /// <summary>
/// Returns a result indicating that the server cannot process the request due to an illegal request entity. /// 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. /// Corresponds to HTTP status code 422 Unprocessable Entity.
/// </summary> /// </summary>
public static Result<T> UnprocessableEntity(T? value, params string [] messages) { public static Result<T> UnprocessableEntity(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], (HttpStatusCode)422); // 422 Unprocessable Entity new (value, false, [..messages], (HttpStatusCode)422); // 422 Unprocessable Entity
}
/// <summary> /// <summary>
/// Returns a result indicating that access to the target resource is denied because the resource is locked. /// Returns a result indicating that access to the target resource is denied because the resource is locked.
/// Corresponds to HTTP status code 423 Locked. /// Corresponds to HTTP status code 423 Locked.
/// </summary> /// </summary>
public static Result<T> Locked(T? value, params string [] messages) { public static Result<T> Locked(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], (HttpStatusCode)423); // 423 Locked new (value, false, [..messages], (HttpStatusCode)423); // 423 Locked
}
/// <summary> /// <summary>
/// Returns a result indicating that the request failed because it depended on another request and that request failed. /// 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. /// Corresponds to HTTP status code 424 Failed Dependency.
/// </summary> /// </summary>
public static Result<T> FailedDependency(T? value, params string [] messages) { public static Result<T> FailedDependency(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], (HttpStatusCode)424); // 424 Failed Dependency new (value, false, [..messages], (HttpStatusCode)424); // 424 Failed Dependency
}
/// <summary> /// <summary>
/// Returns a result indicating that the server is unwilling to risk processing a request that might be replayed. /// 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. /// Corresponds to HTTP status code 425 Too Early.
/// </summary> /// </summary>
public static Result<T> TooEarly(T? value, params string [] messages) { public static Result<T> TooEarly(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], (HttpStatusCode)425); // 425 Too Early new (value, false, [..messages], (HttpStatusCode)425); // 425 Too Early
}
/// <summary> /// <summary>
/// Returns a result indicating that the server refuses to perform the request using the current protocol. /// Returns a result indicating that the server refuses to perform the request using the current protocol.
/// Corresponds to HTTP status code 426 Upgrade Required. /// Corresponds to HTTP status code 426 Upgrade Required.
/// </summary> /// </summary>
public static Result<T> UpgradeRequired(T? value, params string [] messages) { public static Result<T> UpgradeRequired(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], (HttpStatusCode)426); // 426 Upgrade Required new (value, false, [..messages], (HttpStatusCode)426); // 426 Upgrade Required
}
/// <summary> /// <summary>
/// Returns a result indicating that the server requires the request to be conditional. /// Returns a result indicating that the server requires the request to be conditional.
/// Corresponds to HTTP status code 428 Precondition Required. /// Corresponds to HTTP status code 428 Precondition Required.
/// </summary> /// </summary>
public static Result<T> PreconditionRequired(T? value, params string [] messages) { public static Result<T> PreconditionRequired(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], (HttpStatusCode)428); // 428 Precondition Required new (value, false, [..messages], (HttpStatusCode)428); // 428 Precondition Required
}
/// <summary> /// <summary>
/// Returns a result indicating that the user has sent too many requests in a given amount of time. /// 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. /// Corresponds to HTTP status code 429 Too Many Requests.
/// </summary> /// </summary>
public static Result<T> TooManyRequests(T? value, params string [] messages) { public static Result<T> TooManyRequests(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], (HttpStatusCode)429); // 429 Too Many Requests new (value, false, [..messages], (HttpStatusCode)429); // 429 Too Many Requests
}
/// <summary> /// <summary>
/// Returns a result indicating that the server is unwilling to process the request because its header fields are too large. /// 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. /// Corresponds to HTTP status code 431 Request Header Fields Too Large.
/// </summary> /// </summary>
public static Result<T> RequestHeaderFieldsTooLarge(T? value, params string [] messages) { 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 new (value, false, [..messages], (HttpStatusCode)431); // 431 Request Header Fields Too Large
}
/// <summary> /// <summary>
/// Returns a result indicating that access to the requested resource is denied for legal reasons. /// 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. /// Corresponds to HTTP status code 451 Unavailable For Legal Reasons.
/// </summary> /// </summary>
public static Result<T> UnavailableForLegalReasons(T? value, params string [] messages) { public static Result<T> UnavailableForLegalReasons(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], (HttpStatusCode)451); // 451 Unavailable For Legal Reasons new (value, false, [..messages], (HttpStatusCode)451); // 451 Unavailable For Legal Reasons
}
#endregion #endregion
} }

View 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);
}
}
}

View File

@ -1,6 +1,5 @@
using System.Net; using System.Net;
namespace MaksIT.Results; namespace MaksIT.Results;
public partial class Result { 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. /// 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. /// Corresponds to HTTP status code 100 Continue.
/// </summary> /// </summary>
public static Result Continue(params string [] messages) { public static Result Continue(params string [] messages) =>
return new Result(true, [..messages], HttpStatusCode.Continue); new (true, [..messages], HttpStatusCode.Continue);
}
/// <summary> /// <summary>
/// Returns a result indicating that the server is switching to a different protocol as requested by the client. /// 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. /// Corresponds to HTTP status code 101 Switching Protocols.
/// </summary> /// </summary>
public static Result SwitchingProtocols(params string [] messages) { public static Result SwitchingProtocols(params string [] messages) =>
return new Result(true, [..messages], HttpStatusCode.SwitchingProtocols); new (true, [..messages], HttpStatusCode.SwitchingProtocols);
}
#endregion #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. /// 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. /// Corresponds to HTTP status code 102 Processing.
/// </summary> /// </summary>
public static Result Processing(params string [] messages) { public static Result Processing(params string [] messages) =>
return new Result(true, [..messages], HttpStatusCode.Processing); new (true, [..messages], HttpStatusCode.Processing);
}
/// <summary> /// <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. /// 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. /// Corresponds to HTTP status code 103 Early Hints.
/// </summary> /// </summary>
public static Result EarlyHints(params string [] messages) { 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 new (true, [..messages], (HttpStatusCode)103); // Early Hints is not defined in HttpStatusCode enum, 103 is the official code
}
#endregion #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. /// 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. /// Corresponds to HTTP status code 100 Continue.
/// </summary> /// </summary>
public static Result<T> Continue(T? value, params string [] messages) { public static Result<T> Continue(T? value, params string [] messages) =>
return new Result<T>(value, true, [..messages], HttpStatusCode.Continue); new (value, true, [..messages], HttpStatusCode.Continue);
}
/// <summary> /// <summary>
/// Returns a result indicating that the server is switching to a different protocol as requested by the client. /// 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. /// Corresponds to HTTP status code 101 Switching Protocols.
/// </summary> /// </summary>
public static Result<T> SwitchingProtocols(T? value, params string [] messages) { public static Result<T> SwitchingProtocols(T? value, params string [] messages) =>
return new Result<T>(value, true, [..messages], HttpStatusCode.SwitchingProtocols); new (value, true, [..messages], HttpStatusCode.SwitchingProtocols);
}
#endregion #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. /// 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. /// Corresponds to HTTP status code 102 Processing.
/// </summary> /// </summary>
public static Result<T> Processing(T? value, params string [] messages) { public static Result<T> Processing(T? value, params string [] messages) =>
return new Result<T>(value, true, [..messages], HttpStatusCode.Processing); new (value, true, [..messages], HttpStatusCode.Processing);
}
/// <summary> /// <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. /// 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. /// Corresponds to HTTP status code 103 Early Hints.
/// </summary> /// </summary>
public static Result<T> EarlyHints(T? value, params string [] messages) { 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 new (value, true, [..messages], (HttpStatusCode)103); // Early Hints is not defined in HttpStatusCode enum, 103 is the official code
}
#endregion #endregion
} }

View File

@ -1,5 +1,4 @@
using System.Net; using System.Net;
namespace MaksIT.Results; 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. /// 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. /// Corresponds to HTTP status code 300 Multiple Choices.
/// </summary> /// </summary>
public static Result MultipleChoices(params string [] messages) { public static Result MultipleChoices(params string [] messages) =>
return new Result(true, [..messages], HttpStatusCode.MultipleChoices); new (true, [..messages], HttpStatusCode.MultipleChoices);
}
/// <summary> /// <summary>
/// Returns a result indicating that the requested resource has been permanently moved to a new URI. /// Returns a result indicating that the requested resource has been permanently moved to a new URI.
/// Corresponds to HTTP status code 301 Moved Permanently. /// Corresponds to HTTP status code 301 Moved Permanently.
/// </summary> /// </summary>
public static Result MovedPermanently(params string [] messages) { public static Result MovedPermanently(params string [] messages) =>
return new Result(true, [..messages], HttpStatusCode.MovedPermanently); new (true, [..messages], HttpStatusCode.MovedPermanently);
}
/// <summary> /// <summary>
/// Returns a result indicating that the requested resource resides temporarily under a different URI. /// Returns a result indicating that the requested resource resides temporarily under a different URI.
/// Corresponds to HTTP status code 302 Found. /// Corresponds to HTTP status code 302 Found.
/// </summary> /// </summary>
public static Result Found(params string [] messages) { public static Result Found(params string [] messages) =>
return new Result(true, [..messages], HttpStatusCode.Found); new (true, [..messages], HttpStatusCode.Found);
}
/// <summary> /// <summary>
/// Returns a result indicating that the response to the request can be found under another URI using the GET method. /// 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. /// Corresponds to HTTP status code 303 See Other.
/// </summary> /// </summary>
public static Result SeeOther(params string [] messages) { public static Result SeeOther(params string [] messages) =>
return new Result(true, [..messages], HttpStatusCode.SeeOther); new (true, [..messages], HttpStatusCode.SeeOther);
}
/// <summary> /// <summary>
/// Returns a result indicating that the requested resource has not been modified since the last request. /// Returns a result indicating that the requested resource has not been modified since the last request.
/// Corresponds to HTTP status code 304 Not Modified. /// Corresponds to HTTP status code 304 Not Modified.
/// </summary> /// </summary>
public static Result NotModified(params string [] messages) { public static Result NotModified(params string [] messages) =>
return new Result(true, [..messages], HttpStatusCode.NotModified); new (true, [..messages], HttpStatusCode.NotModified);
}
/// <summary> /// <summary>
/// Returns a result indicating that the requested resource must be accessed through the proxy given by the location field. /// 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. /// Corresponds to HTTP status code 305 Use Proxy.
/// </summary> /// </summary>
public static Result UseProxy(params string [] messages) { public static Result UseProxy(params string [] messages) =>
return new Result(true, [..messages], HttpStatusCode.UseProxy); new (true, [..messages], HttpStatusCode.UseProxy);
}
/// <summary> /// <summary>
/// Returns a result indicating that the requested resource resides temporarily under a different URI, but future requests should still use the original URI. /// 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. /// Corresponds to HTTP status code 307 Temporary Redirect.
/// </summary> /// </summary>
public static Result TemporaryRedirect(params string [] messages) { public static Result TemporaryRedirect(params string [] messages) =>
return new Result(true, [..messages], HttpStatusCode.TemporaryRedirect); new (true, [..messages], HttpStatusCode.TemporaryRedirect);
}
/// <summary> /// <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. /// 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. /// Corresponds to HTTP status code 308 Permanent Redirect.
/// </summary> /// </summary>
public static Result PermanentRedirect(params string [] messages) { public static Result PermanentRedirect(params string [] messages) =>
return new Result(true, [..messages], HttpStatusCode.PermanentRedirect); new (true, [..messages], HttpStatusCode.PermanentRedirect);
}
} }
public partial class Result<T> : Result { 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. /// 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. /// Corresponds to HTTP status code 300 Multiple Choices.
/// </summary> /// </summary>
public static Result<T> MultipleChoices(T? value, params string [] messages) { public static Result<T> MultipleChoices(T? value, params string [] messages) =>
return new Result<T>(value, true, [..messages], HttpStatusCode.MultipleChoices); new (value, true, [..messages], HttpStatusCode.MultipleChoices);
}
/// <summary> /// <summary>
/// Returns a result indicating that the requested resource has been permanently moved to a new URI. /// Returns a result indicating that the requested resource has been permanently moved to a new URI.
/// Corresponds to HTTP status code 301 Moved Permanently. /// Corresponds to HTTP status code 301 Moved Permanently.
/// </summary> /// </summary>
public static Result<T> MovedPermanently(T? value, params string [] messages) { public static Result<T> MovedPermanently(T? value, params string [] messages) =>
return new Result<T>(value, true, [..messages], HttpStatusCode.MovedPermanently); new (value, true, [..messages], HttpStatusCode.MovedPermanently);
}
/// <summary> /// <summary>
/// Returns a result indicating that the requested resource resides temporarily under a different URI. /// Returns a result indicating that the requested resource resides temporarily under a different URI.
/// Corresponds to HTTP status code 302 Found. /// Corresponds to HTTP status code 302 Found.
/// </summary> /// </summary>
public static Result<T> Found(T? value, params string [] messages) { public static Result<T> Found(T? value, params string [] messages) =>
return new Result<T>(value, true, [..messages], HttpStatusCode.Found); new (value, true, [..messages], HttpStatusCode.Found);
}
/// <summary> /// <summary>
/// Returns a result indicating that the response to the request can be found under another URI using the GET method. /// 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. /// Corresponds to HTTP status code 303 See Other.
/// </summary> /// </summary>
public static Result<T> SeeOther(T? value, params string [] messages) { public static Result<T> SeeOther(T? value, params string [] messages) =>
return new Result<T>(value, true, [..messages], HttpStatusCode.SeeOther); new (value, true, [..messages], HttpStatusCode.SeeOther);
}
/// <summary> /// <summary>
/// Returns a result indicating that the requested resource has not been modified since the last request. /// Returns a result indicating that the requested resource has not been modified since the last request.
/// Corresponds to HTTP status code 304 Not Modified. /// Corresponds to HTTP status code 304 Not Modified.
/// </summary> /// </summary>
public static Result<T> NotModified(T? value, params string [] messages) { public static Result<T> NotModified(T? value, params string [] messages) =>
return new Result<T>(value, true, [..messages], HttpStatusCode.NotModified); new (value, true, [..messages], HttpStatusCode.NotModified);
}
/// <summary> /// <summary>
/// Returns a result indicating that the requested resource must be accessed through the proxy given by the location field. /// 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. /// Corresponds to HTTP status code 305 Use Proxy.
/// </summary> /// </summary>
public static Result<T> UseProxy(T? value, params string [] messages) { public static Result<T> UseProxy(T? value, params string [] messages) =>
return new Result<T>(value, true, [..messages], HttpStatusCode.UseProxy); new (value, true, [..messages], HttpStatusCode.UseProxy);
}
/// <summary> /// <summary>
/// Returns a result indicating that the requested resource resides temporarily under a different URI, but future requests should still use the original URI. /// 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. /// Corresponds to HTTP status code 307 Temporary Redirect.
/// </summary> /// </summary>
public static Result<T> TemporaryRedirect(T? value, params string [] messages) { public static Result<T> TemporaryRedirect(T? value, params string [] messages) =>
return new Result<T>(value, true, [..messages], HttpStatusCode.TemporaryRedirect); new (value, true, [..messages], HttpStatusCode.TemporaryRedirect);
}
/// <summary> /// <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. /// 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. /// Corresponds to HTTP status code 308 Permanent Redirect.
/// </summary> /// </summary>
public static Result<T> PermanentRedirect(T? value, params string [] messages) { public static Result<T> PermanentRedirect(T? value, params string [] messages) =>
return new Result<T>(value, true, [..messages], HttpStatusCode.PermanentRedirect); new (value, true, [..messages], HttpStatusCode.PermanentRedirect);
}
} }

View File

@ -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. /// 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. /// Corresponds to HTTP status code 500 Internal Server Error.
/// </summary> /// </summary>
public static Result InternalServerError(params string [] messages) { public static Result InternalServerError(params string [] messages) =>
return new Result(false, [..messages], HttpStatusCode.InternalServerError); new (false, [..messages], HttpStatusCode.InternalServerError);
}
/// <summary> /// <summary>
/// Returns a result indicating the server does not support the functionality required to fulfill the request. /// Returns a result indicating the server does not support the functionality required to fulfill the request.
/// Corresponds to HTTP status code 501 Not Implemented. /// Corresponds to HTTP status code 501 Not Implemented.
/// </summary> /// </summary>
public static Result NotImplemented(params string [] messages) { public static Result NotImplemented(params string [] messages) =>
return new Result(false, [..messages], HttpStatusCode.NotImplemented); new (false, [..messages], HttpStatusCode.NotImplemented);
}
/// <summary> /// <summary>
/// Returns a result indicating the server, while acting as a gateway or proxy, received an invalid response from the upstream server. /// 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. /// Corresponds to HTTP status code 502 Bad Gateway.
/// </summary> /// </summary>
public static Result BadGateway(params string [] messages) { public static Result BadGateway(params string [] messages) =>
return new Result(false, [..messages], HttpStatusCode.BadGateway); new (false, [..messages], HttpStatusCode.BadGateway);
}
/// <summary> /// <summary>
/// Returns a result indicating the server is currently unable to handle the request due to temporary overload or maintenance of the server. /// 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. /// Corresponds to HTTP status code 503 Service Unavailable.
/// </summary> /// </summary>
public static Result ServiceUnavailable(params string [] messages) { public static Result ServiceUnavailable(params string [] messages) =>
return new Result(false, [..messages], HttpStatusCode.ServiceUnavailable); new (false, [..messages], HttpStatusCode.ServiceUnavailable);
}
/// <summary> /// <summary>
/// Returns a result indicating the server, while acting as a gateway or proxy, did not receive a timely response from the upstream server. /// 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. /// Corresponds to HTTP status code 504 Gateway Timeout.
/// </summary> /// </summary>
public static Result GatewayTimeout(params string [] messages) { public static Result GatewayTimeout(params string [] messages) =>
return new Result(false, [..messages], HttpStatusCode.GatewayTimeout); new (false, [..messages], HttpStatusCode.GatewayTimeout);
}
/// <summary> /// <summary>
/// Returns a result indicating the server does not support the HTTP protocol version used in the request. /// 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. /// Corresponds to HTTP status code 505 HTTP Version Not Supported.
/// </summary> /// </summary>
public static Result HttpVersionNotSupported(params string [] messages) { public static Result HttpVersionNotSupported(params string [] messages) =>
return new Result(false, [..messages], HttpStatusCode.HttpVersionNotSupported); new (false, [..messages], HttpStatusCode.HttpVersionNotSupported);
}
#endregion #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. /// 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. /// Corresponds to HTTP status code 506 Variant Also Negotiates.
/// </summary> /// </summary>
public static Result VariantAlsoNegotiates(params string [] messages) { public static Result VariantAlsoNegotiates(params string [] messages) =>
return new Result(false, [..messages], HttpStatusCode.VariantAlsoNegotiates); new (false, [..messages], HttpStatusCode.VariantAlsoNegotiates);
}
/// <summary> /// <summary>
/// Returns a result indicating the server is unable to store the representation needed to complete the request. /// 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. /// Corresponds to HTTP status code 507 Insufficient Storage.
/// </summary> /// </summary>
public static Result InsufficientStorage(params string [] messages) { public static Result InsufficientStorage(params string [] messages) =>
return new Result(false, [..messages], HttpStatusCode.InsufficientStorage); new (false, [..messages], HttpStatusCode.InsufficientStorage);
}
/// <summary> /// <summary>
/// Returns a result indicating the server detected an infinite loop while processing a request with depth: infinity. Usually encountered in WebDAV scenarios. /// 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. /// Corresponds to HTTP status code 508 Loop Detected.
/// </summary> /// </summary>
public static Result LoopDetected(params string [] messages) { public static Result LoopDetected(params string [] messages) =>
return new Result(false, [..messages], HttpStatusCode.LoopDetected); new (false, [..messages], HttpStatusCode.LoopDetected);
}
/// <summary> /// <summary>
/// Returns a result indicating further extensions to the request are required for the server to fulfill it. /// 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. /// Corresponds to HTTP status code 510 Not Extended.
/// </summary> /// </summary>
public static Result NotExtended(params string [] messages) { public static Result NotExtended(params string [] messages) =>
return new Result(false, [..messages], HttpStatusCode.NotExtended); new (false, [..messages], HttpStatusCode.NotExtended);
}
/// <summary> /// <summary>
/// Returns a result indicating the client needs to authenticate to gain network access. /// Returns a result indicating the client needs to authenticate to gain network access.
/// Corresponds to HTTP status code 511 Network Authentication Required. /// Corresponds to HTTP status code 511 Network Authentication Required.
/// </summary> /// </summary>
public static Result NetworkAuthenticationRequired(params string [] messages) { public static Result NetworkAuthenticationRequired(params string [] messages) =>
return new Result(false, [..messages], HttpStatusCode.NetworkAuthenticationRequired); new (false, [..messages], HttpStatusCode.NetworkAuthenticationRequired);
}
#endregion #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. /// 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. /// Corresponds to HTTP status code 500 Internal Server Error.
/// </summary> /// </summary>
public static Result<T> InternalServerError(T? value, params string [] messages) { public static Result<T> InternalServerError(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], HttpStatusCode.InternalServerError); new (value, false, [..messages], HttpStatusCode.InternalServerError);
}
/// <summary> /// <summary>
/// Returns a result indicating the server does not support the functionality required to fulfill the request. /// Returns a result indicating the server does not support the functionality required to fulfill the request.
/// Corresponds to HTTP status code 501 Not Implemented. /// Corresponds to HTTP status code 501 Not Implemented.
/// </summary> /// </summary>
public static Result<T> NotImplemented(T? value, params string [] messages) { public static Result<T> NotImplemented(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], HttpStatusCode.NotImplemented); new (value, false, [..messages], HttpStatusCode.NotImplemented);
}
/// <summary> /// <summary>
/// Returns a result indicating the server, while acting as a gateway or proxy, received an invalid response from the upstream server. /// 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. /// Corresponds to HTTP status code 502 Bad Gateway.
/// </summary> /// </summary>
public static Result<T> BadGateway(T? value, params string [] messages) { public static Result<T> BadGateway(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], HttpStatusCode.BadGateway); new (value, false, [..messages], HttpStatusCode.BadGateway);
}
/// <summary> /// <summary>
/// Returns a result indicating the server is currently unable to handle the request due to temporary overload or maintenance of the server. /// 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. /// Corresponds to HTTP status code 503 Service Unavailable.
/// </summary> /// </summary>
public static Result<T> ServiceUnavailable(T? value, params string [] messages) { public static Result<T> ServiceUnavailable(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], HttpStatusCode.ServiceUnavailable); new (value, false, [..messages], HttpStatusCode.ServiceUnavailable);
}
/// <summary> /// <summary>
/// Returns a result indicating the server, while acting as a gateway or proxy, did not receive a timely response from the upstream server. /// 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. /// Corresponds to HTTP status code 504 Gateway Timeout.
/// </summary> /// </summary>
public static Result<T> GatewayTimeout(T? value, params string [] messages) { public static Result<T> GatewayTimeout(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], HttpStatusCode.GatewayTimeout); new (value, false, [..messages], HttpStatusCode.GatewayTimeout);
}
/// <summary> /// <summary>
/// Returns a result indicating the server does not support the HTTP protocol version used in the request. /// 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. /// Corresponds to HTTP status code 505 HTTP Version Not Supported.
/// </summary> /// </summary>
public static Result<T> HttpVersionNotSupported(T? value, params string [] messages) { public static Result<T> HttpVersionNotSupported(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], HttpStatusCode.HttpVersionNotSupported); new (value, false, [..messages], HttpStatusCode.HttpVersionNotSupported);
}
#endregion #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. /// 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. /// Corresponds to HTTP status code 506 Variant Also Negotiates.
/// </summary> /// </summary>
public static Result<T> VariantAlsoNegotiates(T? value, params string [] messages) { public static Result<T> VariantAlsoNegotiates(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], HttpStatusCode.VariantAlsoNegotiates); new (value, false, [..messages], HttpStatusCode.VariantAlsoNegotiates);
}
/// <summary> /// <summary>
/// Returns a result indicating the server is unable to store the representation needed to complete the request. /// 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. /// Corresponds to HTTP status code 507 Insufficient Storage.
/// </summary> /// </summary>
public static Result<T> InsufficientStorage(T? value, params string [] messages) { public static Result<T> InsufficientStorage(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], HttpStatusCode.InsufficientStorage); new (value, false, [..messages], HttpStatusCode.InsufficientStorage);
}
/// <summary> /// <summary>
/// Returns a result indicating the server detected an infinite loop while processing a request with depth: infinity. Usually encountered in WebDAV scenarios. /// 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. /// Corresponds to HTTP status code 508 Loop Detected.
/// </summary> /// </summary>
public static Result<T> LoopDetected(T? value, params string [] messages) { public static Result<T> LoopDetected(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], HttpStatusCode.LoopDetected); new (value, false, [..messages], HttpStatusCode.LoopDetected);
}
/// <summary> /// <summary>
/// Returns a result indicating further extensions to the request are required for the server to fulfill it. /// 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. /// Corresponds to HTTP status code 510 Not Extended.
/// </summary> /// </summary>
public static Result<T> NotExtended(T? value, params string [] messages) { public static Result<T> NotExtended(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], HttpStatusCode.NotExtended); new (value, false, [..messages], HttpStatusCode.NotExtended);
}
/// <summary> /// <summary>
/// Returns a result indicating the client needs to authenticate to gain network access. /// Returns a result indicating the client needs to authenticate to gain network access.
/// Corresponds to HTTP status code 511 Network Authentication Required. /// Corresponds to HTTP status code 511 Network Authentication Required.
/// </summary> /// </summary>
public static Result<T> NetworkAuthenticationRequired(T? value, params string [] messages) { public static Result<T> NetworkAuthenticationRequired(T? value, params string [] messages) =>
return new Result<T>(value, false, [..messages], HttpStatusCode.NetworkAuthenticationRequired); new (value, false, [..messages], HttpStatusCode.NetworkAuthenticationRequired);
}
#endregion #endregion
} }

View File

@ -1,6 +1,5 @@
using System.Net; using System.Net;
namespace MaksIT.Results; namespace MaksIT.Results;
public partial class Result { 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. /// Returns a result indicating the request was successful and the server returned the requested data.
/// Corresponds to HTTP status code 200 OK. /// Corresponds to HTTP status code 200 OK.
/// </summary> /// </summary>
public static Result Ok(params string[] messages) { public static Result Ok(params string[] messages) =>
return new Result(true, [..messages], HttpStatusCode.OK); new (true, [..messages], HttpStatusCode.OK);
}
/// <summary> /// <summary>
/// Returns a result indicating the request was successful and a new resource was created. /// Returns a result indicating the request was successful and a new resource was created.
/// Corresponds to HTTP status code 201 Created. /// Corresponds to HTTP status code 201 Created.
/// </summary> /// </summary>
public static Result Created(params string[] messages) { public static Result Created(params string[] messages) =>
return new Result(true, [..messages], HttpStatusCode.Created); new (true, [..messages], HttpStatusCode.Created);
}
/// <summary> /// <summary>
/// Returns a result indicating the request has been accepted for processing, but the processing is not yet complete. /// 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. /// Corresponds to HTTP status code 202 Accepted.
/// </summary> /// </summary>
public static Result Accepted(params string[] messages) { public static Result Accepted(params string[] messages) =>
return new Result(true, [..messages], HttpStatusCode.Accepted); new (true, [..messages], HttpStatusCode.Accepted);
}
/// <summary> /// <summary>
/// Returns a result indicating the request was successful but the response contains metadata from a source other than the origin server. /// 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. /// Corresponds to HTTP status code 203 Non-Authoritative Information.
/// </summary> /// </summary>
public static Result NonAuthoritativeInformation(params string[] messages) { public static Result NonAuthoritativeInformation(params string[] messages) =>
return new Result(true, [..messages], HttpStatusCode.NonAuthoritativeInformation); new (true, [..messages], HttpStatusCode.NonAuthoritativeInformation);
}
/// <summary> /// <summary>
/// Returns a result indicating the request was successful but there is no content to send in the response. /// 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. /// Corresponds to HTTP status code 204 No Content.
/// </summary> /// </summary>
public static Result NoContent(params string[] messages) { public static Result NoContent(params string[] messages) =>
return new Result(true, [..messages], HttpStatusCode.NoContent); new (true, [..messages], HttpStatusCode.NoContent);
}
/// <summary> /// <summary>
/// Returns a result indicating the request was successful, but the user-agent should reset the document view that caused the request. /// 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. /// Corresponds to HTTP status code 205 Reset Content.
/// </summary> /// </summary>
public static Result ResetContent(params string[] messages) { public static Result ResetContent(params string[] messages) =>
return new Result(true, [..messages], HttpStatusCode.ResetContent); new (true, [..messages], HttpStatusCode.ResetContent);
}
/// <summary> /// <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. /// 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. /// Corresponds to HTTP status code 206 Partial Content.
/// </summary> /// </summary>
public static Result PartialContent(params string[] messages) { public static Result PartialContent(params string[] messages) =>
return new Result(true, [..messages], HttpStatusCode.PartialContent); new (true, [..messages], HttpStatusCode.PartialContent);
}
#endregion #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. /// 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. /// Corresponds to HTTP status code 207 Multi-Status.
/// </summary> /// </summary>
public static Result MultiStatus(params string[] messages) { public static Result MultiStatus(params string[] messages) =>
return new Result(true, [..messages], HttpStatusCode.MultiStatus); new (true, [..messages], HttpStatusCode.MultiStatus);
}
/// <summary> /// <summary>
/// Returns a result indicating the request was successful and the information has already been reported in a previous response. /// 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. /// Corresponds to HTTP status code 208 Already Reported.
/// </summary> /// </summary>
public static Result AlreadyReported(params string[] messages) { public static Result AlreadyReported(params string[] messages) =>
return new Result(true, [..messages], HttpStatusCode.AlreadyReported); new (true, [..messages], HttpStatusCode.AlreadyReported);
}
/// <summary> /// <summary>
/// Returns a result indicating the request was successful and the server fulfilled the request for the resource using the delta encoding method. /// 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. /// Corresponds to HTTP status code 226 IM Used.
/// </summary> /// </summary>
public static Result IMUsed(params string[] messages) { public static Result IMUsed(params string[] messages) =>
return new Result(true, [..messages], (HttpStatusCode)226); // 226 is the official status code for IM Used new (true, [..messages], (HttpStatusCode)226); // 226 is the official status code for IM Used
}
#endregion #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. /// Returns a result indicating the request was successful and the server returned the requested data.
/// Corresponds to HTTP status code 200 OK. /// Corresponds to HTTP status code 200 OK.
/// </summary> /// </summary>
public static Result<T> Ok(T? value, params string[] messages) { public static Result<T> Ok(T? value, params string[] messages) =>
return new Result<T>(value, true, [..messages], HttpStatusCode.OK); new (value, true, [..messages], HttpStatusCode.OK);
}
/// <summary> /// <summary>
/// Returns a result indicating the request was successful and a new resource was created. /// Returns a result indicating the request was successful and a new resource was created.
/// Corresponds to HTTP status code 201 Created. /// Corresponds to HTTP status code 201 Created.
/// </summary> /// </summary>
public static Result<T> Created(T? value, params string[] messages) { public static Result<T> Created(T? value, params string[] messages) =>
return new Result<T>(value, true, [..messages], HttpStatusCode.Created); new (value, true, [..messages], HttpStatusCode.Created);
}
/// <summary> /// <summary>
/// Returns a result indicating the request has been accepted for processing, but the processing is not yet complete. /// 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. /// Corresponds to HTTP status code 202 Accepted.
/// </summary> /// </summary>
public static Result<T> Accepted(T? value, params string[] messages) { public static Result<T> Accepted(T? value, params string[] messages) =>
return new Result<T>(value, true, [..messages], HttpStatusCode.Accepted); new (value, true, [..messages], HttpStatusCode.Accepted);
}
/// <summary> /// <summary>
/// Returns a result indicating the request was successful but the response contains metadata from a source other than the origin server. /// 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. /// Corresponds to HTTP status code 203 Non-Authoritative Information.
/// </summary> /// </summary>
public static Result<T> NonAuthoritativeInformation(T? value, params string[] messages) { public static Result<T> NonAuthoritativeInformation(T? value, params string[] messages) =>
return new Result<T>(value, true, [..messages], HttpStatusCode.NonAuthoritativeInformation); new (value, true, [..messages], HttpStatusCode.NonAuthoritativeInformation);
}
/// <summary> /// <summary>
/// Returns a result indicating the request was successful but there is no content to send in the response. /// 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. /// Corresponds to HTTP status code 204 No Content.
/// </summary> /// </summary>
public static Result<T> NoContent(T? value, params string[] messages) { public static Result<T> NoContent(T? value, params string[] messages) =>
return new Result<T>(value, true, [..messages], HttpStatusCode.NoContent); new (value, true, [..messages], HttpStatusCode.NoContent);
}
/// <summary> /// <summary>
/// Returns a result indicating the request was successful, but the user-agent should reset the document view that caused the request. /// 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. /// Corresponds to HTTP status code 205 Reset Content.
/// </summary> /// </summary>
public static Result<T> ResetContent(T? value, params string[] messages) { public static Result<T> ResetContent(T? value, params string[] messages) =>
return new Result<T>(value, true, [..messages], HttpStatusCode.ResetContent); new (value, true, [..messages], HttpStatusCode.ResetContent);
}
/// <summary> /// <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. /// 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. /// Corresponds to HTTP status code 206 Partial Content.
/// </summary> /// </summary>
public static Result<T> PartialContent(T? value, params string[] messages) { public static Result<T> PartialContent(T? value, params string[] messages) =>
return new Result<T>(value, true, [..messages], HttpStatusCode.PartialContent); new (value, true, [..messages], HttpStatusCode.PartialContent);
}
#endregion #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. /// 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. /// Corresponds to HTTP status code 207 Multi-Status.
/// </summary> /// </summary>
public static Result<T> MultiStatus(T? value, params string[] messages) { public static Result<T> MultiStatus(T? value, params string[] messages) =>
return new Result<T>(value, true, [..messages], HttpStatusCode.MultiStatus); new (value, true, [..messages], HttpStatusCode.MultiStatus);
}
/// <summary> /// <summary>
/// Returns a result indicating the request was successful and the information has already been reported in a previous response. /// 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. /// Corresponds to HTTP status code 208 Already Reported.
/// </summary> /// </summary>
public static Result<T> AlreadyReported(T? value, params string[] messages) { public static Result<T> AlreadyReported(T? value, params string[] messages) =>
return new Result<T>(value, true, [..messages], HttpStatusCode.AlreadyReported); new (value, true, [..messages], HttpStatusCode.AlreadyReported);
}
/// <summary> /// <summary>
/// Returns a result indicating the request was successful and the server fulfilled the request for the resource using the delta encoding method. /// 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. /// Corresponds to HTTP status code 226 IM Used.
/// </summary> /// </summary>
public static Result<T> IMUsed(T? value, params string[] messages) { 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 new (value, true, [..messages], (HttpStatusCode)226); // 226 is the official status code for IM Used
}
#endregion #endregion
} }

View File

@ -2,7 +2,6 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using MaksIT.Results.Mvc; using MaksIT.Results.Mvc;
namespace MaksIT.Results; namespace MaksIT.Results;
public partial class Result { public partial class Result {
@ -12,7 +11,7 @@ public partial class Result {
protected Result(bool isSuccess, List<string> messages, HttpStatusCode statusCode) { protected Result(bool isSuccess, List<string> messages, HttpStatusCode statusCode) {
IsSuccess = isSuccess; IsSuccess = isSuccess;
Messages = messages ?? new List<string>(); Messages = messages ?? [];
StatusCode = statusCode; StatusCode = statusCode;
} }
@ -20,9 +19,8 @@ public partial class Result {
/// Converts the current Result{T} to a non-generic Result. /// Converts the current Result{T} to a non-generic Result.
/// </summary> /// </summary>
/// <returns>A non-generic Result object.</returns> /// <returns>A non-generic Result object.</returns>
public Result ToResult() { public Result ToResult() =>
return new Result(IsSuccess, Messages, StatusCode); new (IsSuccess, Messages, StatusCode);
}
/// <summary> /// <summary>
/// Converts this Result into a Result of another type while retaining the same success status, messages, and status code. /// 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> /// <typeparam name="U">The target type for the Result.</typeparam>
/// <param name="value">The new value of type U for the converted Result.</param> /// <param name="value">The new value of type U for the converted Result.</param>
/// <returns>A Result of type U.</returns> /// <returns>A Result of type U.</returns>
public Result<U?> ToResultOfType<U>(U? value) { public Result<U?> ToResultOfType<U>(U? value) =>
return new Result<U?>(value, IsSuccess, Messages, StatusCode); new(value, IsSuccess, Messages, StatusCode);
}
/// <summary> /// <summary>
/// Converts the current Result to an IActionResult. /// Converts the current Result to an IActionResult.
@ -49,18 +46,15 @@ public partial class Result {
Detail = string.Join("; ", Messages), Detail = string.Join("; ", Messages),
Instance = null // You can customize the instance URI if needed Instance = null // You can customize the instance URI if needed
}; };
return new ObjectResult(problemDetails) { StatusCode = (int)StatusCode }; return new ObjectResult(problemDetails) { StatusCode = (int)StatusCode };
} }
} }
} }
public partial class Result<T> : Result { public partial class Result<T>(T? value, bool isSuccess, List<string> messages, HttpStatusCode statusCode)
public T? Value { get; } : Result(isSuccess, messages, statusCode) {
public T? Value { get; } = value;
public Result(T? value, bool isSuccess, List<string> messages, HttpStatusCode statusCode)
: base(isSuccess, messages, statusCode) {
Value = value;
}
/// <summary> /// <summary>
/// Converts this Result<T> to a Result<U> while retaining success status, messages, and status code. /// 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> /// <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> /// <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> /// <returns>A Result<U> containing the transformed value.</returns>
public Result<U?> ToResultOfType<U>(Func<T?, U?> newValueFunc) { public Result<U?> ToResultOfType<U>(Func<T?, U?> newValueFunc) =>
return new Result<U?>(newValueFunc(Value), IsSuccess, Messages, StatusCode); new (newValueFunc(Value), IsSuccess, Messages, StatusCode);
}
/// <summary> /// <summary>
/// Converts the current Result<T> to an IActionResult. /// Converts the current Result<T> to an IActionResult.
@ -79,7 +72,7 @@ public partial class Result<T> : Result {
public override IActionResult ToActionResult() { public override IActionResult ToActionResult() {
if (IsSuccess) { if (IsSuccess) {
if (IsRedirectionStatusCode(StatusCode) && Value is string location && !string.IsNullOrWhiteSpace(location)) 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) if (Value is not null)
return new ObjectResult(Value) { StatusCode = (int)StatusCode }; return new ObjectResult(Value) { StatusCode = (int)StatusCode };
@ -90,12 +83,4 @@ public partial class Result<T> : Result {
return base.ToActionResult(); 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;
} }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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