Skip to content
Draft
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
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ jobs:
java-version: "21"
cache: gradle

- name: Set up .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
# The sample libraries target net8.0 and their test projects target
# net9.0, so both runtimes have to be present. `cargo test` also shells
# out to `dotnet build` from tests/generate_dotnet.rs, which relied on
# whatever SDK the runner happened to preinstall until this step.
dotnet-version: |
8.0.x
9.0.x

- name: Install Python dependencies (samples)
working-directory: samples/python
run: uv sync --locked
Expand Down Expand Up @@ -137,3 +148,11 @@ jobs:
- name: Run Java tests (advanced)
working-directory: advanced/samples/java
run: ./gradlew build --no-daemon

- name: Run .NET tests (samples)
working-directory: samples/dotnet
run: dotnet test tests/ --nologo

- name: Run .NET tests (advanced)
working-directory: advanced/samples/dotnet
run: dotnet test tests/ --nologo
288 changes: 288 additions & 0 deletions advanced/samples/dotnet/json_schema/api/chat/Definitions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
// <auto-generated />
// Generated by nex-gen. DO NOT EDIT!
#nullable enable
#pragma warning disable CS1591

using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json;

namespace NexGen.ChatService
{

/// <summary>
/// A single constraint failure. <see cref="Path"/> is the JSON member path
/// (dotted for nested members); <see cref="Reason"/> is a human-readable
/// message naming the bound and the offending value.
/// </summary>
[GeneratedCode("nex-gen", null)]
public sealed class Violation
{
public Violation(string path, string reason)
{
Path = path;
Reason = reason;
}

public string Path { get; }

public string Reason { get; }

/// <summary>
/// Returns <c>"Path: Reason"</c>, or just <c>Reason</c> when the path is
/// empty.
/// </summary>
public override string ToString() =>
Path.Length == 0 ? Reason : Path + ": " + Reason;
}

/// <summary>
/// Aggregates every <see cref="Violation"/> found while (de)serializing a
/// value, surfacing them all in one error rather than stopping at the first.
/// </summary>
[GeneratedCode("nex-gen", null)]
public sealed class ValidationException : JsonException
{
public ValidationException(IReadOnlyList<Violation> violations)
: base(FormatMessage(violations))
{
Violations = violations;
}

/// <summary>
/// Every violation found, never a partial first-failure.
/// </summary>
public IReadOnlyList<Violation> Violations { get; }

private static string FormatMessage(IReadOnlyList<Violation> violations)
{
var parts = new string[violations.Count];
for (var index = 0; index < violations.Count; index++)
{
parts[index] = violations[index].ToString();
}
return $"{violations.Count} validation error(s): {string.Join("; ", parts)}";
}
}

/// <summary>
/// Read helpers shared by every generated model. Internal because they are an
/// implementation detail of the generated (de)serialization path rather than
/// part of the contract surface.
/// </summary>
[GeneratedCode("nex-gen", null)]
internal static class JsonRuntime
{
/// <summary>
/// The largest integer a JSON number carries losslessly (2^53-1).
///
/// Exceeding it is a **contract violation**, reported through
/// <see cref="ValidationException"/> with the offending member's path — not
/// a parse failure. Mirrors Go's `integerCap`.
/// </summary>
internal const long IntegerCap = 9007199254740991L;

/// <summary>
/// Reads an optional member out of the extension-data bag, falling back to
/// <paramref name="defaultValue"/> when absent.
/// </summary>
internal static T? ReadOptionalValue<T>(
IDictionary<string, object?> members,
string name,
T? defaultValue = default)
{
if (!members.TryGetValue(name, out var value))
{
return defaultValue;
}
return ReadJsonValue<T>(value);
}

internal static T? ReadJsonValue<T>(object? value)
{
if (value is null)
{
return default;
}
if (typeof(T) == typeof(long?) || typeof(T) == typeof(long))
{
return (T?)(object?)ReadJsonInteger(value);
}
if (value is JsonElement json)
{
return json.Deserialize<T>();
}
if (value is T typed)
{
return typed;
}
return (T)value;
}

/// <summary>
/// Reads a JSON number as an integer, rejecting non-integral values and
/// anything beyond the lossless integer range.
/// </summary>
internal static long? ReadJsonInteger(object? value)
{
if (value is null)
{
return default;
}
if (value is JsonElement json)
{
if (json.ValueKind == JsonValueKind.Null)
{
return default;
}
if (json.ValueKind != JsonValueKind.Number)
{
throw new JsonException("expected integer");
}
// Exact across the whole Int64 range, and fails for a non-integral
// number. Deliberately does not enforce IntegerCap: a value past
// 2^53-1 is a constraint violation the validator reports with a
// path, not a parse error. Reading through double would round it
// away before the validator ever saw it.
if (json.TryGetInt64(out var exact))
{
return exact;
}
// A number spelled with a decimal point but no fractional part —
// `1.0` — is a valid integer per JSON Schema, and TryGetInt64
// rejects that spelling. Fall back to the double reading, bounded
// to the range where double to long is exact. `% 1 != 0` also
// rejects NaN and infinity, whose remainder is NaN.
if (json.TryGetDouble(out var number)
&& number % 1 == 0
&& number >= -9007199254740992d
&& number <= 9007199254740992d)
{
return (long)number;
}
throw new JsonException("expected integer");
}
if (value is long longValue)
{
return longValue;
}
if (value is int intValue)
{
return intValue;
}
throw new JsonException("expected integer");
}

/// <summary>
/// Reports every <c>uniqueItems</c> duplicate, each against the index where
/// the value was first seen.
///
/// A repeated value therefore yields one violation per later occurrence
/// rather than one per pair, which is what the other targets do.
/// </summary>
internal static void CollectDuplicateItems<T>(
IReadOnlyList<T> items,
string path,
List<Violation> violations)
where T : notnull
{
var seen = new Dictionary<T, int>(items.Count);
for (var index = 0; index < items.Count; index++)
{
if (seen.TryGetValue(items[index], out var first))
{
violations.Add(new Violation(
path,
$"duplicate items: element at index {index} equals index {first}"));
}
else
{
seen[items[index]] = index;
}
}
}

/// <summary>
/// Counts elements equal to a <c>contains</c> <c>const</c> value, feeding the
/// <c>minContains</c>/<c>maxContains</c> occurrence window.
/// </summary>
internal static int CountMatchingItems<T>(IReadOnlyList<T> items, T expected)
{
var comparer = EqualityComparer<T>.Default;
var count = 0;
foreach (var item in items)
{
if (comparer.Equals(item, expected))
{
count++;
}
}
return count;
}

/// <summary>
/// Counts Unicode code points, which is the unit JSON Schema's
/// <c>minLength</c>/<c>maxLength</c> measure.
///
/// <c>string.Length</c> counts UTF-16 code units, so it would score an
/// astral character such as U+1F600 as 2 and reject a value the contract
/// permits. This matches Go's <c>utf8.RuneCountInString</c> and Java's
/// <c>codePointCount</c>, including counting an unpaired surrogate as one.
/// </summary>
internal static int CodePointCount(string value)
{
var count = 0;
for (var index = 0; index < value.Length; index++)
{
count++;
if (char.IsHighSurrogate(value[index])
&& index + 1 < value.Length
&& char.IsLowSurrogate(value[index + 1]))
{
index++;
}
}
return count;
}

/// <summary>
/// Quotes a string for a violation reason, mirroring Go's <c>%q</c> for the
/// values a contract admits. Used by the <c>enum</c> reason, which names the
/// offending value alongside the admitted set.
/// </summary>
internal static string Quote(string value) => "\"" + value + "\"";

/// <summary>
/// Joins a violation path prefix to a member name, so a nested model
/// reports <c>page.blocks.order</c> rather than a bare <c>order</c>.
/// </summary>
internal static string JoinPath(string prefix, string name) =>
prefix.Length == 0 ? name : prefix + "." + name;

/// <summary>
/// Renders a number for a violation reason using the invariant culture, so
/// the message never picks up a locale's decimal separator and stays
/// byte-identical to the other targets' diagnostics.
/// </summary>
internal static string FormatNumber(double value) =>
value.ToString(CultureInfo.InvariantCulture);

/// <inheritdoc cref="FormatNumber(double)"/>
internal static string FormatNumber(long value) =>
value.ToString(CultureInfo.InvariantCulture);

/// <summary>
/// Rejects an explicit JSON <c>null</c> for a member the contract declares
/// non-nullable.
/// </summary>
internal static void RejectNull(string name, object? value)
{
if (value is null || value is JsonElement { ValueKind: JsonValueKind.Null })
{
throw new JsonException($"{name}: explicit null not allowed");
}
}
}

}
Loading