diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe0f576..a48e4be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: @@ -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: @@ -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 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c7159c1..3d2ac0b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.0.2" + ".": "0.1.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5823536..7d8cd1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/scripts/mock b/scripts/mock index 9c7c439..6376077 100755 --- a/scripts/mock +++ b/scripts/mock @@ -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 @@ -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 @@ -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 diff --git a/scripts/test b/scripts/test index 09d6098..7e2e6b2 100755 --- a/scripts/test +++ b/scripts/test @@ -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() { @@ -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 @@ -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 @@ -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 diff --git a/src/Dedalus.Tests/Core/JsonDictionaryTest.cs b/src/Dedalus.Tests/Core/JsonDictionaryTest.cs index 14a169a..a249233 100644 --- a/src/Dedalus.Tests/Core/JsonDictionaryTest.cs +++ b/src/Dedalus.Tests/Core/JsonDictionaryTest.cs @@ -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(() => + dict.GetNotAbsentElement("missing") + ); + Assert.Contains("'missing' cannot be absent", exception.Message); + } + + [Fact] + public void GetNotAbsentElement_ReturnsNullElementWhenValueIsNull() + { + var dict = new JsonDictionary( + new Dictionary + { + ["nullable"] = JsonSerializer.SerializeToElement(null), + } + ); + + var element = dict.GetNotAbsentElement("nullable"); + + Assert.Equal(JsonValueKind.Null, element.ValueKind); + } + [Fact] public void GetNullableClass_ReturnsValueWhenPresent() { diff --git a/src/Dedalus.Tests/Core/MultipartJsonDictionaryTest.cs b/src/Dedalus.Tests/Core/MultipartJsonDictionaryTest.cs index 6e91573..b2944b3 100644 --- a/src/Dedalus.Tests/Core/MultipartJsonDictionaryTest.cs +++ b/src/Dedalus.Tests/Core/MultipartJsonDictionaryTest.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Frozen; using System.Collections.Generic; +using System.Text.Json; using Dedalus.Core; using Dedalus.Exceptions; @@ -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(() => + dict.GetNotAbsentElement("missing") + ); + Assert.Contains("'missing' cannot be absent", exception.Message); + } + + [Fact] + public void GetNotAbsentElement_ReturnsNullElementWhenValueIsNull() + { + var dict = new MultipartJsonDictionary( + new Dictionary + { + ["nullable"] = MultipartJsonSerializer.SerializeToElement(null), + } + ); + + var element = dict.GetNotAbsentElement("nullable"); + + Assert.Equal(JsonValueKind.Null, element.ValueKind); + } + [Fact] public void GetNullableClass_ReturnsValueWhenPresent() { diff --git a/src/Dedalus/Core/JsonDictionary.cs b/src/Dedalus/Core/JsonDictionary.cs index 76e51cb..c8cf259 100644 --- a/src/Dedalus/Core/JsonDictionary.cs +++ b/src/Dedalus/Core/JsonDictionary.cs @@ -149,6 +149,22 @@ public T GetNotNullStruct(string key) return deserialized; } + /// + /// Gets the raw JSON element for a key that must be present, without rejecting JSON null. + /// + /// Used for values of unknown type, which can be any JSON value including null. A JSON + /// null is returned as a whose ValueKind is + /// Null. + /// + 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), diff --git a/src/Dedalus/Core/MultipartJsonDictionary.cs b/src/Dedalus/Core/MultipartJsonDictionary.cs index 5765b10..00b4644 100644 --- a/src/Dedalus/Core/MultipartJsonDictionary.cs +++ b/src/Dedalus/Core/MultipartJsonDictionary.cs @@ -152,6 +152,22 @@ public T GetNotNullStruct(string key) return deserialized; } + /// + /// Gets the raw JSON element for a key that must be present, without rejecting JSON null. + /// + /// Used for values of unknown type, which can be any JSON value including null. A JSON + /// null is returned as a whose ValueKind is + /// Null. + /// + 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), diff --git a/src/Dedalus/Dedalus.csproj b/src/Dedalus/Dedalus.csproj index e9294f6..28cdd03 100644 --- a/src/Dedalus/Dedalus.csproj +++ b/src/Dedalus/Dedalus.csproj @@ -3,7 +3,7 @@ Dedalus C# Dedalus - 0.0.2 + 0.1.0 The official .NET library for the Dedalus API. Library README.md