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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
lint:
timeout-minutes: 10
name: lint
runs-on: ${{ github.repository == 'stainless-sdks/dedalus-csharp' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')

steps:
Expand All @@ -37,7 +37,7 @@ jobs:
build:
timeout-minutes: 10
name: build
runs-on: ${{ github.repository == 'stainless-sdks/dedalus-csharp' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')

steps:
Expand All @@ -54,7 +54,7 @@ jobs:
test:
timeout-minutes: 10
name: test
runs-on: ${{ github.repository == 'stainless-sdks/dedalus-csharp' && 'depot-windows-2022' || 'windows-latest' }}
runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-windows-2022' || 'windows-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.0.2"
".": "0.1.0"
}
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Changelog

## 0.1.0 (2026-08-19)

Full Changelog: [v0.0.2...v0.1.0](https://github.com/dedalus-labs/dedalus-csharp/compare/v0.0.2...v0.1.0)

### Features

* **stlc:** configurable CI runner and private-production-repo support in workflow templates ([e2f4b9c](https://github.com/dedalus-labs/dedalus-csharp/commit/e2f4b9ce8fdd9b5adcf46e851457b7c1ea81a8d0))


### Bug Fixes

* **client:** tolerate JSON null in required untyped fields and prefer more specific union variants ([5f86628](https://github.com/dedalus-labs/dedalus-csharp/commit/5f866285f31140fce3f41d031f315747687d1e19))


### Chores

* **internal:** allow the mock server port to be set with STAINLESS_MOCK_PORT ([62838d8](https://github.com/dedalus-labs/dedalus-csharp/commit/62838d82e2745297cc10cfa3697ee70457348255))

## 0.0.2 (2026-05-29)

Full Changelog: [v0.0.1...v0.0.2](https://github.com/dedalus-labs/dedalus-csharp/compare/v0.0.1...v0.0.2)
Expand Down
13 changes: 10 additions & 3 deletions scripts/mock
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ set -e

cd "$(dirname "$0")/.."

if [ "$1" == "--install" ]; then
npm exec --package=@stdy/cli@0.22.1 -- steady --version
exit 0
fi

if [[ -n "$1" && "$1" != '--'* ]]; then
URL="$1"
shift
Expand All @@ -20,16 +25,18 @@ fi
echo "==> Starting mock server with URL ${URL}"

# Run steady mock on the given spec
MOCK_PORT="${STAINLESS_MOCK_PORT:-4010}"

if [ "$1" == "--daemon" ]; then
# Pre-install the package so the download doesn't eat into the startup timeout
npm exec --package=@stdy/cli@0.22.1 -- steady --version

npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log &
npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p "$MOCK_PORT" --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log &

# Wait for server to come online via health endpoint (max 30s)
echo -n "Waiting for server"
attempts=0
while ! curl --silent --fail "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1; do
while ! curl --silent --fail "http://127.0.0.1:$MOCK_PORT/_x-steady/health" >/dev/null 2>&1; do
if ! kill -0 $! 2>/dev/null; then
echo
cat .stdy.log
Expand All @@ -48,5 +55,5 @@ if [ "$1" == "--daemon" ]; then

echo
else
npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL"
npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p "$MOCK_PORT" --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL"
fi
12 changes: 9 additions & 3 deletions scripts/test
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color

MOCK_PORT="${STAINLESS_MOCK_PORT:-4010}"

function steady_is_running() {
curl --silent "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1
curl --silent "http://127.0.0.1:$MOCK_PORT/_x-steady/health" >/dev/null 2>&1
}

kill_server_on_port() {
Expand All @@ -31,7 +33,7 @@ function is_overriding_api_base_url() {

if ! is_overriding_api_base_url && ! steady_is_running ; then
# When we exit this script, make sure to kill the background mock server process
trap 'kill_server_on_port 4010' EXIT
trap 'kill_server_on_port "$MOCK_PORT"' EXIT

# Start the dev server
./scripts/mock --daemon
Expand All @@ -47,7 +49,7 @@ elif ! steady_is_running ; then
echo -e "To run the server, pass in the path or url of your OpenAPI"
echo -e "spec to the steady command:"
echo
echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.22.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}"
echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.22.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p $MOCK_PORT --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}"
echo

exit 1
Expand All @@ -56,5 +58,9 @@ else
echo
fi

if [ -n "${STAINLESS_MOCK_PORT:-}" ] && ! is_overriding_api_base_url ; then
export TEST_API_BASE_URL="http://127.0.0.1:$MOCK_PORT"
fi

echo "==> Running tests"
dotnet test
37 changes: 37 additions & 0 deletions src/Dedalus.Tests/Core/JsonDictionaryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,43 @@ public void GetNotNullStruct_ThrowsWhenTypeInvalid()
Assert.Contains("'text' must be of type", exception.Message);
}

[Fact]
public void GetNotAbsentElement_ReturnsValue()
{
var dict = new JsonDictionary();
dict.Set("age", 30);

var age = dict.GetNotAbsentElement("age");

Assert.Equal(30, age.GetInt32());
}

[Fact]
public void GetNotAbsentElement_ThrowsWhenKeyAbsent()
{
var dict = new JsonDictionary();

var exception = Assert.Throws<DedalusInvalidDataException>(() =>
dict.GetNotAbsentElement("missing")
);
Assert.Contains("'missing' cannot be absent", exception.Message);
}

[Fact]
public void GetNotAbsentElement_ReturnsNullElementWhenValueIsNull()
{
var dict = new JsonDictionary(
new Dictionary<string, JsonElement>
{
["nullable"] = JsonSerializer.SerializeToElement<string?>(null),
}
);

var element = dict.GetNotAbsentElement("nullable");

Assert.Equal(JsonValueKind.Null, element.ValueKind);
}

[Fact]
public void GetNullableClass_ReturnsValueWhenPresent()
{
Expand Down
38 changes: 38 additions & 0 deletions src/Dedalus.Tests/Core/MultipartJsonDictionaryTest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Text.Json;
using Dedalus.Core;
using Dedalus.Exceptions;

Expand Down Expand Up @@ -205,6 +206,43 @@ public void GetNotNullStruct_ThrowsWhenTypeInvalid()
Assert.Contains("'text' must be of type", exception.Message);
}

[Fact]
public void GetNotAbsentElement_ReturnsValue()
{
var dict = new MultipartJsonDictionary();
dict.Set("age", 30);

var age = dict.GetNotAbsentElement("age");

Assert.Equal(30, age.GetInt32());
}

[Fact]
public void GetNotAbsentElement_ThrowsWhenKeyAbsent()
{
var dict = new MultipartJsonDictionary();

var exception = Assert.Throws<DedalusInvalidDataException>(() =>
dict.GetNotAbsentElement("missing")
);
Assert.Contains("'missing' cannot be absent", exception.Message);
}

[Fact]
public void GetNotAbsentElement_ReturnsNullElementWhenValueIsNull()
{
var dict = new MultipartJsonDictionary(
new Dictionary<string, MultipartJsonElement>
{
["nullable"] = MultipartJsonSerializer.SerializeToElement<string?>(null),
}
);

var element = dict.GetNotAbsentElement("nullable");

Assert.Equal(JsonValueKind.Null, element.ValueKind);
}

[Fact]
public void GetNullableClass_ReturnsValueWhenPresent()
{
Expand Down
16 changes: 16 additions & 0 deletions src/Dedalus/Core/JsonDictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,22 @@ public T GetNotNullStruct<T>(string key)
return deserialized;
}

/// <summary>
/// Gets the raw JSON element for a key that must be present, without rejecting JSON null.
///
/// <para>Used for values of unknown type, which can be any JSON value including null. A JSON
/// null is returned as a <see cref="JsonElement"/> whose <c>ValueKind</c> is
/// <c>Null</c>.</para>
/// </summary>
public JsonElement GetNotAbsentElement(string key)
{
if (!_rawData.TryGetValue(key, out JsonElement element))
{
throw new DedalusInvalidDataException($"'{key}' cannot be absent");
}
return element;
}

public override string ToString() =>
JsonSerializer.Serialize(
FriendlyJsonPrinter.PrintValue(this._rawData),
Expand Down
16 changes: 16 additions & 0 deletions src/Dedalus/Core/MultipartJsonDictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,22 @@ public T GetNotNullStruct<T>(string key)
return deserialized;
}

/// <summary>
/// Gets the raw JSON element for a key that must be present, without rejecting JSON null.
///
/// <para>Used for values of unknown type, which can be any JSON value including null. A JSON
/// null is returned as a <see cref="JsonElement"/> whose <c>ValueKind</c> is
/// <c>Null</c>.</para>
/// </summary>
public JsonElement GetNotAbsentElement(string key)
{
if (!_rawData.TryGetValue(key, out MultipartJsonElement element))
{
throw new DedalusInvalidDataException($"'{key}' cannot be absent");
}
return element.Json;
}

public override string ToString() =>
JsonSerializer.Serialize(
FriendlyJsonPrinter.PrintValue(this._rawData),
Expand Down
2 changes: 1 addition & 1 deletion src/Dedalus/Dedalus.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<!-- Metadata -->
<AssemblyTitle>Dedalus C#</AssemblyTitle>
<AssemblyName>Dedalus</AssemblyName>
<VersionPrefix>0.0.2</VersionPrefix>
<VersionPrefix>0.1.0</VersionPrefix>
<Description>The official .NET library for the Dedalus API.</Description>
<OutputType>Library</OutputType>
<PackageReadmeFile>README.md</PackageReadmeFile>
Expand Down
Loading