Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions policies/dotnet/Devolutions.Now.Policy.Api/BrokerApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public static class BrokerApi
public const string ExecutionResponseKind = "ExecutionResponse";
public const string StatusResponseKind = "StatusResponse";
public const string CancelResponseKind = "CancelResponse";
public const string PolicyResponseKind = "PolicyResponse";
public const string ErrorResponseKind = "ErrorResponse";

internal static string ValidateMessageKind(string? value, string expected, string propertyName)
Expand Down
55 changes: 45 additions & 10 deletions policies/dotnet/Devolutions.Now.Policy.Api/BrokerJson.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;

using Devolutions.Now.Policy.Model;

namespace Devolutions.Now.Policy.Api;

/// <summary>Canonical schema URI used in the <c>$schema</c> field of policy documents.</summary>
Expand All @@ -19,30 +21,34 @@ public static class BrokerJson
/// (via explicit <c>[JsonPropertyName]</c> attributes), PascalCase enum values, and
/// null optionals omitted (mirroring the Rust <c>skip_serializing_if = "Option::is_none"</c>).
/// </summary>
public static readonly JsonSerializerOptions Options = new(BrokerJsonSerializerContext.Default.Options)
{
};
public static readonly JsonSerializerOptions Options = CreateOptions(writeIndented: false);

public static readonly JsonSerializerOptions PrettyOptions = new(Options)
{
WriteIndented = true,
};
public static readonly JsonSerializerOptions PrettyOptions = CreateOptions(writeIndented: true);

public static string Serialize<T>(T value) =>
JsonSerializer.Serialize(value, TypeInfo<T>());

public static T? Deserialize<T>(string json) =>
JsonSerializer.Deserialize(json, TypeInfo<T>());

public static T? DeserializeStrict<T>(string json) =>
JsonSerializer.Deserialize(json, StrictTypeInfo<T>());
public static T? DeserializeStrict<T>(string json)
{
var value = JsonSerializer.Deserialize(json, StrictTypeInfo<T>());
if (value is PolicyResponse response)
{
PolicyJson.ValidateRequiredCollectionElements(response.Policy);
}

return value;
}

private static JsonTypeInfo<T> TypeInfo<T>() =>
typeof(T) == typeof(PackageRequest) ? Cast<T>(BrokerJsonSerializerContext.Default.PackageRequest) :
typeof(T) == typeof(StatusRequest) ? Cast<T>(BrokerJsonSerializerContext.Default.StatusRequest) :
typeof(T) == typeof(CancelRequest) ? Cast<T>(BrokerJsonSerializerContext.Default.CancelRequest) :
typeof(T) == typeof(HealthResponse) ? Cast<T>(BrokerJsonSerializerContext.Default.HealthResponse) :
typeof(T) == typeof(CapabilitiesResponse) ? Cast<T>(BrokerJsonSerializerContext.Default.CapabilitiesResponse) :
typeof(T) == typeof(PolicyResponse) ? Cast<T>(BrokerPolicyJsonSerializerContext.Default.PolicyResponse) :
typeof(T) == typeof(EvaluationResponse) ? Cast<T>(BrokerJsonSerializerContext.Default.EvaluationResponse) :
typeof(T) == typeof(ExecutionResponse) ? Cast<T>(BrokerJsonSerializerContext.Default.ExecutionResponse) :
typeof(T) == typeof(StatusResponse) ? Cast<T>(BrokerJsonSerializerContext.Default.StatusResponse) :
Expand All @@ -56,6 +62,7 @@ private static JsonTypeInfo<T> StrictTypeInfo<T>() =>
typeof(T) == typeof(CancelRequest) ? Cast<T>(BrokerJsonStrictSerializerContext.Default.CancelRequest) :
typeof(T) == typeof(HealthResponse) ? Cast<T>(BrokerJsonStrictSerializerContext.Default.HealthResponse) :
typeof(T) == typeof(CapabilitiesResponse) ? Cast<T>(BrokerJsonStrictSerializerContext.Default.CapabilitiesResponse) :
typeof(T) == typeof(PolicyResponse) ? Cast<T>(BrokerPolicyJsonStrictSerializerContext.Default.PolicyResponse) :
typeof(T) == typeof(EvaluationResponse) ? Cast<T>(BrokerJsonStrictSerializerContext.Default.EvaluationResponse) :
typeof(T) == typeof(ExecutionResponse) ? Cast<T>(BrokerJsonStrictSerializerContext.Default.ExecutionResponse) :
typeof(T) == typeof(StatusResponse) ? Cast<T>(BrokerJsonStrictSerializerContext.Default.StatusResponse) :
Expand All @@ -65,10 +72,20 @@ private static JsonTypeInfo<T> StrictTypeInfo<T>() =>

private static JsonTypeInfo<T> Cast<T>(JsonTypeInfo jsonTypeInfo) =>
(JsonTypeInfo<T>)jsonTypeInfo;

private static JsonSerializerOptions CreateOptions(bool writeIndented) =>
new(BrokerJsonSerializerContext.Default.Options)
{
TypeInfoResolver = JsonTypeInfoResolver.Combine(
BrokerJsonSerializerContext.Default,
BrokerPolicyJsonSerializerContext.Default),
WriteIndented = writeIndented,
};
}

[JsonSourceGenerationOptions(
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
RespectNullableAnnotations = true,
WriteIndented = false)]
[JsonSerializable(typeof(PackageRequest))]
[JsonSerializable(typeof(StatusRequest))]
Expand All @@ -87,6 +104,7 @@ internal sealed partial class BrokerJsonSerializerContext : JsonSerializerContex

[JsonSourceGenerationOptions(
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
RespectNullableAnnotations = true,
WriteIndented = false,
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow)]
[JsonSerializable(typeof(PackageRequest))]
Expand All @@ -102,4 +120,21 @@ internal sealed partial class BrokerJsonSerializerContext : JsonSerializerContex
[JsonSerializable(typeof(JsonNode))]
[JsonSerializable(typeof(JsonObject))]
[JsonSerializable(typeof(JsonArray))]
internal sealed partial class BrokerJsonStrictSerializerContext : JsonSerializerContext;
internal sealed partial class BrokerJsonStrictSerializerContext : JsonSerializerContext;

[JsonSourceGenerationOptions(
Converters = new[] { typeof(ExactCaseTransportConverter) },
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
RespectNullableAnnotations = true,
WriteIndented = false)]
[JsonSerializable(typeof(PolicyResponse))]
internal sealed partial class BrokerPolicyJsonSerializerContext : JsonSerializerContext;

[JsonSourceGenerationOptions(
Converters = new[] { typeof(ExactCaseTransportConverter) },
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
RespectNullableAnnotations = true,
WriteIndented = false,
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow)]
[JsonSerializable(typeof(PolicyResponse))]
internal sealed partial class BrokerPolicyJsonStrictSerializerContext : JsonSerializerContext;
32 changes: 32 additions & 0 deletions policies/dotnet/Devolutions.Now.Policy.Api/Enums.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,42 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Devolutions.Now.Policy.Api;

// Enum members are spelled exactly as they appear on the wire (PascalCase), so the
// default JsonStringEnumConverter round-trips them without a naming policy.

internal class ExactCaseStringEnumConverter<TEnum> : JsonConverter<TEnum>
where TEnum : struct, Enum
{
public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.String)
{
throw new JsonException($"Expected a string value for {typeof(TEnum).Name}.");
}

var name = reader.GetString();
if (name is null ||
!Enum.TryParse<TEnum>(name, ignoreCase: false, out var value) ||
Enum.GetName(value) != name)
{
throw new JsonException($"'{name}' is not a canonical {typeof(TEnum).Name} value.");
}

return value;
}

public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
{
var name = Enum.GetName(value)
?? throw new JsonException($"'{value}' is not a defined {typeof(TEnum).Name} value.");
writer.WriteStringValue(name);
}
}

internal sealed class ExactCaseTransportConverter : ExactCaseStringEnumConverter<Transport>;

/// <summary>Package operation type.</summary>
[JsonConverter(typeof(JsonStringEnumConverter<Operation>))]
public enum Operation
Expand Down
10 changes: 5 additions & 5 deletions policies/dotnet/Devolutions.Now.Policy.Api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Devolutions NOW package broker API for .NET
Purpose
-------

This package contains request, response, status, health, capabilities, and error DTOs for package broker clients and implementations. It does not perform HTTP transport, named-pipe I/O, policy evaluation, or package-manager execution.
This package contains request, response, status, health, capabilities, active-policy inspection, and error DTOs for package broker clients and implementations. It does not perform HTTP transport, named-pipe I/O, policy evaluation, or package-manager execution.

Top-level request DTOs carry `RequestKind` and `RequestVersion`; top-level response DTOs carry
`ResponseKind` and `ResponseVersion`. Kind properties are fixed discriminators that serialize
Expand All @@ -17,19 +17,19 @@ the wire schema.
The DTOs are used to:

- serialize package broker requests from .NET clients;
- deserialize broker health, capability, evaluation, execution, status, and error responses;
- deserialize broker health, capability, active-policy, evaluation, execution, status, and error responses;
- share the same JSON wire shape as the Rust source-of-truth model;
- provide compatibility conversions between package broker API enums and the `Devolutions.Now.Policy.Model` policy enums.

Architecture
------------

- `RequestModels.cs` defines `PackageRequest` and request context/options.
- `ResponseModels.cs` defines evaluation and execution responses plus shared response context, summaries, decisions, policy info, diagnostics, and operation submission.
- `ResponseModels.cs` defines active-policy, evaluation, and execution responses plus shared response context, summaries, decisions, policy info, diagnostics, and operation submission. `PolicyResponse` embeds the canonical `Devolutions.Now.Policy.Model.PolicyDocument`.
- `StatusModels.cs` defines status query request/response DTOs.
- `MetaModels.cs` defines health, capabilities, manager capability, and error DTOs.
- `Enums.cs` defines package broker API enums and JSON string enum converters.
- `BrokerJson.cs` defines serializer options for the broker wire format.
- `BrokerJson.cs` defines source-generated serializer options for the broker wire format. Public `BrokerJson.Options` and `BrokerJson.PrettyOptions` support every broker DTO, including the embedded policy model, without reflection and reject JSON null for non-nullable contract members.
- `PolicyCompatibility.cs` maps compatible API enums to and from `Devolutions.Now.Policy.Model` enums.

OpenAPI relationship
Expand All @@ -44,7 +44,7 @@ policies\rust\now-policy-api\openapi\now-policy-api.yaml
Regenerate it with:

```powershell
cargo run -p now-policy-server-template --bin generate-now-policy-api-openapi --locked
cargo run -p now-policy-server-template --features policy-compat --bin generate-now-policy-api-openapi --locked
```

After schema changes, run the .NET client tests to verify these DTOs still match the Rust contract.
Expand Down
31 changes: 31 additions & 0 deletions policies/dotnet/Devolutions.Now.Policy.Api/ResponseModels.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,36 @@
using System.Text.Json.Serialization;

using Devolutions.Now.Policy.Model;

namespace Devolutions.Now.Policy.Api;

/// <summary>Response containing the broker's active parsed policy document.</summary>
public sealed class PolicyResponse
{
private const string Kind = BrokerApi.PolicyResponseKind;
private string _responseKind = Kind;

[JsonPropertyName("ResponseKind")]
[JsonRequired]
public string ResponseKind
{
get => _responseKind;
set => _responseKind = BrokerApi.ValidateMessageKind(value, Kind, nameof(ResponseKind));
}

[JsonPropertyName("ResponseVersion")]
[JsonRequired]
public string ResponseVersion { get; set; } = BrokerApi.Version;

[JsonPropertyName("Server")]
[JsonRequired]
public ServerContext Server { get; set; } = new();

[JsonPropertyName("Policy")]
[JsonRequired]
public PolicyDocument Policy { get; set; } = new();
Comment thread
CBenoit marked this conversation as resolved.
}

/// <summary>Canonical response returned by the broker after evaluating a request.</summary>
public sealed class EvaluationResponse
{
Expand Down Expand Up @@ -95,9 +124,11 @@ public string ResponseKind
public sealed class ServerContext
{
[JsonPropertyName("ServerVersion")]
[JsonRequired]
public string ServerVersion { get; set; } = "";

[JsonPropertyName("Transport")]
[JsonRequired]
public Transport Transport { get; set; }
}

Expand Down
Loading