From 36cd1715bdde7f46f7980ba0cc20b71548645aff Mon Sep 17 00:00:00 2001 From: Adam Basfop Cavendish Date: Wed, 11 Nov 2026 23:02:39 +0800 Subject: [PATCH] fix: preserve Kotlin untagged-union member payloads - Select Kotlin OkHttp union members using retained finite scalar and structural facts so unconstrained Gson fields cannot swallow another member's payload. - Expose firstMatch, strict, and explicit raw policies while retaining typed sealed classes, data-class payloads, and existing root-null behavior by default. - Count original concrete members before opaque fallbacks and decode the selected member once, preserving nested strict errors and transparent type aliases. - Delegate serialization through each declared payload type so nested sealed unions do not leak their runtime Variant wrappers into the wire representation. - Compare exact significands and unbounded exponents without rounding, adding matcher scale limits, or hiding existing native scalar and byte decoder errors. - Cache successful and failed recursive predicates by JSON node identity with qualified Kotlin cache types that cannot collide with generated model names. - Preserve raw JSON trees, including null entries and large numbers, and restore the writer's null policy even when native tree serialization fails. - Emit adapters with structured declarations and semantic nested case types; share intersection wrapper keys between model emission and matching. - Retain ordinary and tagged codecs, wrapped intersections, and untouched generated dependencies without changing shared IR or other-language backends. - Exercise native model, recursive helper, and HTTP response/error consumers in isolated policy projects alongside exact render and Kotlin golden contracts. --- docs/src/SUMMARY.md | 1 + docs/src/kotlin_config.md | 49 ++ src/generators/kotlin/okhttp/codegen.rs | 1 + src/generators/kotlin/okhttp/config.rs | 36 ++ src/generators/kotlin/okhttp/matching.rs | 524 ++++++++++++++++++ src/generators/kotlin/okhttp/mod.rs | 2 + src/generators/kotlin/okhttp/sigil_emit.rs | 288 ++-------- src/generators/kotlin/okhttp/union.rs | 166 ++++++ src/generators/kotlin/okhttp/util.rs | 42 +- tests/generated-clients/jvm/README.md | 3 + .../cases/union-first-match/build.gradle | 2 + .../cases/union-first-match/gradle.lockfile | 25 + .../src/test/java/UnionPolicyTest.java | 3 + .../union-helper-types-strict/build.gradle | 1 + .../union-helper-types-strict/gradle.lockfile | 25 + .../src/test/java/StrictHelperTypesTest.java | 1 + .../cases/union-helper-types/gradle.lockfile | 25 + .../test/java/RecursiveHelperTypesTest.java | 17 + .../jvm/kotlin/cases/union-raw/build.gradle | 1 + .../kotlin/cases/union-raw/gradle.lockfile | 25 + .../union-raw/src/test/java/RawUnionTest.java | 68 +++ .../cases/union-selection/gradle.lockfile | 25 + .../src/test/java/UnionSelectionTest.java | 62 +++ .../kotlin/cases/union-strict/build.gradle | 2 + .../kotlin/cases/union-strict/gradle.lockfile | 25 + .../src/test/java/UnionPolicyTest.java | 3 + .../generated-clients/jvm/kotlin/fixtures.txt | 7 + .../support/http/UnionHttpTestSupport.java | 18 + .../union-policies/TypedUnionPolicyCases.java | 221 ++++++++ .../models/AnyOfTypes.kt.golden | 39 +- .../models/OneOfObjects.kt.golden | 142 ++++- .../models/OneOfTypes.kt.golden | 52 +- .../models/OneOfWithNull.kt.golden | 103 +++- .../enum-repr/models/MixedEnum.kt.golden | 166 +++++- .../enum-repr/models/UntaggedEnum.kt.golden | 118 +++- .../models/Choice.kt.golden | 119 +++- .../models/Foo.kt.golden | 104 +++- .../models/Foo.kt.golden | 94 +++- .../models/Qux.kt.golden | 71 ++- .../models/Foo.kt.golden | 109 +++- .../models/Config.kt.golden | 47 +- .../models/Foo.kt.golden | 113 +++- .../models/Foo.kt.golden | 104 +++- .../models/Foo.kt.golden | 52 +- .../union-first-match/README.md.golden | 7 + .../apis/SelectionApi.kt.golden | 101 ++++ .../union-first-match/build.gradle.kts.golden | 27 + .../models/AliasChoice.kt.golden | 130 +++++ .../union-first-match/models/Alpha.kt.golden | 12 + .../models/Ambiguous.kt.golden | 97 ++++ .../models/AmbiguousAlias.kt.golden | 7 + .../union-first-match/models/Beta.kt.golden | 12 + .../models/BinaryChoice.kt.golden | 124 +++++ .../models/BranchOne.kt.golden | 8 + .../models/BranchTwo.kt.golden | 12 + .../models/ChoiceEnvelope.kt.golden | 13 + .../models/ContainerChoice.kt.golden | 193 +++++++ .../models/ContainerOne.kt.golden | 8 + .../models/ContainerTwo.kt.golden | 11 + .../models/ExcludedNull.kt.golden | 8 + .../models/FiniteChoice.kt.golden | 316 +++++++++++ .../models/FiniteOne.kt.golden | 12 + .../models/FiniteTwo.kt.golden | 12 + .../models/FormatChoice.kt.golden | 141 +++++ .../models/FormatFallback.kt.golden | 123 ++++ .../models/IncludedNull.kt.golden | 11 + .../models/IntegerFallback.kt.golden | 112 ++++ .../models/LargeChoice.kt.golden | 169 ++++++ .../models/LargeOne.kt.golden | 8 + .../models/LargeTwo.kt.golden | 11 + .../models/LeftPart.kt.golden | 11 + .../union-first-match/models/Narrow.kt.golden | 8 + .../models/NullableChoice.kt.golden | 80 +++ .../models/NullableFields.kt.golden | 201 +++++++ .../models/NumberAlias.kt.golden | 7 + .../models/NumericFinite.kt.golden | 139 +++++ .../models/NumericZero.kt.golden | 139 +++++ .../models/OpaqueFirst.kt.golden | 220 ++++++++ .../models/OpenNested.kt.golden | 107 ++++ .../models/RecursiveChoice.kt.golden | 66 +++ .../models/RightPart.kt.golden | 11 + .../models/ScalarChoice.kt.golden | 212 +++++++ .../union-first-match/models/Tree.kt.golden | 217 ++++++++ .../models/VariantRaw.kt.golden | 97 ++++ .../union-first-match/models/Wide.kt.golden | 11 + .../models/WrappedChoice.kt.golden | 180 ++++++ .../models/WrappedPair.kt.golden | 8 + .../runtime/ApiClient.kt.golden | 61 ++ .../runtime/ApiException.kt.golden | 11 + .../union-first-match/runtime/Auth.kt.golden | 70 +++ .../README.md.golden | 7 + .../build.gradle.kts.golden | 27 + .../models/Boolean.kt.golden | 7 + .../models/Branch.kt.golden | 8 + .../models/Integer.kt.golden | 7 + .../models/Tree.kt.golden | 127 +++++ .../runtime/ApiClient.kt.golden | 61 ++ .../runtime/ApiException.kt.golden | 11 + .../runtime/Auth.kt.golden | 70 +++ .../union-helper-types/README.md.golden | 7 + .../build.gradle.kts.golden | 27 + .../models/Boolean.kt.golden | 7 + .../models/Branch.kt.golden | 8 + .../models/Integer.kt.golden | 7 + .../union-helper-types/models/Tree.kt.golden | 121 ++++ .../runtime/ApiClient.kt.golden | 61 ++ .../runtime/ApiException.kt.golden | 11 + .../union-helper-types/runtime/Auth.kt.golden | 70 +++ .../kotlin-okhttp/union-raw/README.md.golden | 7 + .../union-raw/apis/SelectionApi.kt.golden | 101 ++++ .../union-raw/build.gradle.kts.golden | 27 + .../union-raw/models/AliasChoice.kt.golden | 47 ++ .../union-raw/models/Alpha.kt.golden | 12 + .../union-raw/models/Ambiguous.kt.golden | 47 ++ .../union-raw/models/AmbiguousAlias.kt.golden | 7 + .../union-raw/models/Beta.kt.golden | 12 + .../union-raw/models/BinaryChoice.kt.golden | 47 ++ .../union-raw/models/BranchOne.kt.golden | 8 + .../union-raw/models/BranchTwo.kt.golden | 12 + .../union-raw/models/ChoiceEnvelope.kt.golden | 13 + .../models/ContainerChoice.kt.golden | 47 ++ .../union-raw/models/ContainerOne.kt.golden | 8 + .../union-raw/models/ContainerTwo.kt.golden | 11 + .../union-raw/models/ExcludedNull.kt.golden | 8 + .../union-raw/models/FiniteChoice.kt.golden | 47 ++ .../union-raw/models/FiniteOne.kt.golden | 12 + .../union-raw/models/FiniteTwo.kt.golden | 12 + .../union-raw/models/FormatChoice.kt.golden | 47 ++ .../union-raw/models/FormatFallback.kt.golden | 47 ++ .../union-raw/models/IncludedNull.kt.golden | 11 + .../models/IntegerFallback.kt.golden | 47 ++ .../union-raw/models/LargeChoice.kt.golden | 47 ++ .../union-raw/models/LargeOne.kt.golden | 8 + .../union-raw/models/LargeTwo.kt.golden | 11 + .../union-raw/models/LeftPart.kt.golden | 11 + .../union-raw/models/Narrow.kt.golden | 8 + .../union-raw/models/NullableChoice.kt.golden | 47 ++ .../union-raw/models/NullableFields.kt.golden | 47 ++ .../union-raw/models/NumberAlias.kt.golden | 7 + .../union-raw/models/NumericFinite.kt.golden | 47 ++ .../union-raw/models/NumericZero.kt.golden | 47 ++ .../union-raw/models/OpaqueFirst.kt.golden | 47 ++ .../union-raw/models/OpenNested.kt.golden | 47 ++ .../models/RecursiveChoice.kt.golden | 47 ++ .../union-raw/models/RightPart.kt.golden | 11 + .../union-raw/models/ScalarChoice.kt.golden | 47 ++ .../union-raw/models/Tree.kt.golden | 47 ++ .../union-raw/models/VariantRaw.kt.golden | 47 ++ .../union-raw/models/Wide.kt.golden | 11 + .../union-raw/models/WrappedChoice.kt.golden | 47 ++ .../union-raw/models/WrappedPair.kt.golden | 8 + .../union-raw/runtime/ApiClient.kt.golden | 61 ++ .../union-raw/runtime/ApiException.kt.golden | 11 + .../union-raw/runtime/Auth.kt.golden | 70 +++ .../union-selection/README.md.golden | 7 + .../union-selection/build.gradle.kts.golden | 27 + .../union-selection/models/Choice.kt.golden | 172 ++++++ .../models/ClientTool.kt.golden | 11 + .../union-selection/models/Envelope.kt.golden | 12 + .../models/NumericOne.kt.golden | 11 + .../models/NumericTwo.kt.golden | 11 + .../union-selection/models/Request.kt.golden | 8 + .../models/RequiredChoice.kt.golden | 172 ++++++ .../models/RequiredOne.kt.golden | 11 + .../models/RequiredTwo.kt.golden | 11 + .../models/SearchTool.kt.golden | 11 + .../union-selection/models/Tool.kt.golden | 167 ++++++ .../runtime/ApiClient.kt.golden | 61 ++ .../runtime/ApiException.kt.golden | 11 + .../union-selection/runtime/Auth.kt.golden | 70 +++ .../union-strict/README.md.golden | 7 + .../union-strict/apis/SelectionApi.kt.golden | 101 ++++ .../union-strict/build.gradle.kts.golden | 27 + .../union-strict/models/AliasChoice.kt.golden | 136 +++++ .../union-strict/models/Alpha.kt.golden | 12 + .../union-strict/models/Ambiguous.kt.golden | 103 ++++ .../models/AmbiguousAlias.kt.golden | 7 + .../union-strict/models/Beta.kt.golden | 12 + .../models/BinaryChoice.kt.golden | 130 +++++ .../union-strict/models/BranchOne.kt.golden | 8 + .../union-strict/models/BranchTwo.kt.golden | 12 + .../models/ChoiceEnvelope.kt.golden | 13 + .../models/ContainerChoice.kt.golden | 199 +++++++ .../models/ContainerOne.kt.golden | 8 + .../models/ContainerTwo.kt.golden | 11 + .../models/ExcludedNull.kt.golden | 8 + .../models/FiniteChoice.kt.golden | 322 +++++++++++ .../union-strict/models/FiniteOne.kt.golden | 12 + .../union-strict/models/FiniteTwo.kt.golden | 12 + .../models/FormatChoice.kt.golden | 147 +++++ .../models/FormatFallback.kt.golden | 126 +++++ .../models/IncludedNull.kt.golden | 11 + .../models/IntegerFallback.kt.golden | 115 ++++ .../union-strict/models/LargeChoice.kt.golden | 175 ++++++ .../union-strict/models/LargeOne.kt.golden | 8 + .../union-strict/models/LargeTwo.kt.golden | 11 + .../union-strict/models/LeftPart.kt.golden | 11 + .../union-strict/models/Narrow.kt.golden | 8 + .../models/NullableChoice.kt.golden | 83 +++ .../models/NullableFields.kt.golden | 207 +++++++ .../union-strict/models/NumberAlias.kt.golden | 7 + .../models/NumericFinite.kt.golden | 142 +++++ .../union-strict/models/NumericZero.kt.golden | 142 +++++ .../union-strict/models/OpaqueFirst.kt.golden | 223 ++++++++ .../union-strict/models/OpenNested.kt.golden | 110 ++++ .../models/RecursiveChoice.kt.golden | 69 +++ .../union-strict/models/RightPart.kt.golden | 11 + .../models/ScalarChoice.kt.golden | 221 ++++++++ .../union-strict/models/Tree.kt.golden | 223 ++++++++ .../union-strict/models/VariantRaw.kt.golden | 103 ++++ .../union-strict/models/Wide.kt.golden | 11 + .../models/WrappedChoice.kt.golden | 186 +++++++ .../union-strict/models/WrappedPair.kt.golden | 8 + .../union-strict/runtime/ApiClient.kt.golden | 61 ++ .../runtime/ApiException.kt.golden | 11 + .../union-strict/runtime/Auth.kt.golden | 70 +++ tests/golden_tests_kotlin_okhttp.rs | 81 +++ tests/ir_scalar_enums.rs | 14 + 218 files changed, 12805 insertions(+), 407 deletions(-) create mode 100644 docs/src/kotlin_config.md create mode 100644 src/generators/kotlin/okhttp/matching.rs create mode 100644 src/generators/kotlin/okhttp/union.rs create mode 100644 tests/generated-clients/jvm/kotlin/cases/union-first-match/build.gradle create mode 100644 tests/generated-clients/jvm/kotlin/cases/union-first-match/gradle.lockfile create mode 100644 tests/generated-clients/jvm/kotlin/cases/union-first-match/src/test/java/UnionPolicyTest.java create mode 100644 tests/generated-clients/jvm/kotlin/cases/union-helper-types-strict/build.gradle create mode 100644 tests/generated-clients/jvm/kotlin/cases/union-helper-types-strict/gradle.lockfile create mode 100644 tests/generated-clients/jvm/kotlin/cases/union-helper-types-strict/src/test/java/StrictHelperTypesTest.java create mode 100644 tests/generated-clients/jvm/kotlin/cases/union-helper-types/gradle.lockfile create mode 100644 tests/generated-clients/jvm/kotlin/cases/union-helper-types/src/test/java/RecursiveHelperTypesTest.java create mode 100644 tests/generated-clients/jvm/kotlin/cases/union-raw/build.gradle create mode 100644 tests/generated-clients/jvm/kotlin/cases/union-raw/gradle.lockfile create mode 100644 tests/generated-clients/jvm/kotlin/cases/union-raw/src/test/java/RawUnionTest.java create mode 100644 tests/generated-clients/jvm/kotlin/cases/union-selection/gradle.lockfile create mode 100644 tests/generated-clients/jvm/kotlin/cases/union-selection/src/test/java/UnionSelectionTest.java create mode 100644 tests/generated-clients/jvm/kotlin/cases/union-strict/build.gradle create mode 100644 tests/generated-clients/jvm/kotlin/cases/union-strict/gradle.lockfile create mode 100644 tests/generated-clients/jvm/kotlin/cases/union-strict/src/test/java/UnionPolicyTest.java create mode 100644 tests/generated-clients/jvm/kotlin/fixtures.txt create mode 100644 tests/generated-clients/jvm/kotlin/support/http/UnionHttpTestSupport.java create mode 100644 tests/generated-clients/jvm/kotlin/support/union-policies/TypedUnionPolicyCases.java create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/README.md.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/apis/SelectionApi.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/build.gradle.kts.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/AliasChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/Alpha.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/Ambiguous.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/AmbiguousAlias.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/Beta.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/BinaryChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/BranchOne.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/BranchTwo.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/ChoiceEnvelope.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/ContainerChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/ContainerOne.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/ContainerTwo.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/ExcludedNull.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/FiniteChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/FiniteOne.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/FiniteTwo.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/FormatChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/FormatFallback.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/IncludedNull.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/IntegerFallback.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/LargeChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/LargeOne.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/LargeTwo.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/LeftPart.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/Narrow.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/NullableChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/NullableFields.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/NumberAlias.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/NumericFinite.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/NumericZero.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/OpaqueFirst.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/OpenNested.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/RecursiveChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/RightPart.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/ScalarChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/Tree.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/VariantRaw.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/Wide.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/WrappedChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/models/WrappedPair.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/runtime/ApiClient.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/runtime/ApiException.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-first-match/runtime/Auth.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/README.md.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/build.gradle.kts.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/models/Boolean.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/models/Branch.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/models/Integer.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/models/Tree.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/runtime/ApiClient.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/runtime/ApiException.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/runtime/Auth.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-helper-types/README.md.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-helper-types/build.gradle.kts.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-helper-types/models/Boolean.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-helper-types/models/Branch.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-helper-types/models/Integer.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-helper-types/models/Tree.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-helper-types/runtime/ApiClient.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-helper-types/runtime/ApiException.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-helper-types/runtime/Auth.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/README.md.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/apis/SelectionApi.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/build.gradle.kts.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/AliasChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/Alpha.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/Ambiguous.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/AmbiguousAlias.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/Beta.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/BinaryChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/BranchOne.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/BranchTwo.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/ChoiceEnvelope.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/ContainerChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/ContainerOne.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/ContainerTwo.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/ExcludedNull.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/FiniteChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/FiniteOne.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/FiniteTwo.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/FormatChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/FormatFallback.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/IncludedNull.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/IntegerFallback.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/LargeChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/LargeOne.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/LargeTwo.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/LeftPart.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/Narrow.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/NullableChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/NullableFields.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/NumberAlias.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/NumericFinite.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/NumericZero.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/OpaqueFirst.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/OpenNested.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/RecursiveChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/RightPart.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/ScalarChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/Tree.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/VariantRaw.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/Wide.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/WrappedChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/models/WrappedPair.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/runtime/ApiClient.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/runtime/ApiException.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-raw/runtime/Auth.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-selection/README.md.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-selection/build.gradle.kts.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-selection/models/Choice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-selection/models/ClientTool.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-selection/models/Envelope.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-selection/models/NumericOne.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-selection/models/NumericTwo.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-selection/models/Request.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-selection/models/RequiredChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-selection/models/RequiredOne.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-selection/models/RequiredTwo.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-selection/models/SearchTool.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-selection/models/Tool.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-selection/runtime/ApiClient.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-selection/runtime/ApiException.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-selection/runtime/Auth.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/README.md.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/apis/SelectionApi.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/build.gradle.kts.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/AliasChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/Alpha.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/Ambiguous.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/AmbiguousAlias.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/Beta.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/BinaryChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/BranchOne.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/BranchTwo.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/ChoiceEnvelope.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/ContainerChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/ContainerOne.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/ContainerTwo.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/ExcludedNull.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/FiniteChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/FiniteOne.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/FiniteTwo.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/FormatChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/FormatFallback.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/IncludedNull.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/IntegerFallback.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/LargeChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/LargeOne.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/LargeTwo.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/LeftPart.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/Narrow.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/NullableChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/NullableFields.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/NumberAlias.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/NumericFinite.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/NumericZero.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/OpaqueFirst.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/OpenNested.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/RecursiveChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/RightPart.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/ScalarChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/Tree.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/VariantRaw.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/Wide.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/WrappedChoice.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/models/WrappedPair.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/runtime/ApiClient.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/runtime/ApiException.kt.golden create mode 100644 tests/golden/kotlin/kotlin-okhttp/union-strict/runtime/Auth.kt.golden diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 796c21068..3bcaa16e0 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -10,6 +10,7 @@ - [TypeScript Generator Configuration](ts_config.md) - [Python Generator Configuration](python_config.md) - [Java Generator Configuration](java_config.md) +- [Kotlin Generator Configuration](kotlin_config.md) - [Architecture](architecture.md) - [Adding a Generator](adding_a_generator.md) - [IR Reference](ir_reference.md) diff --git a/docs/src/kotlin_config.md b/docs/src/kotlin_config.md new file mode 100644 index 000000000..89f8313e9 --- /dev/null +++ b/docs/src/kotlin_config.md @@ -0,0 +1,49 @@ +# Kotlin Generator Configuration + +The `kotlin-okhttp` backend keeps sealed classes and typed data-class variants +for untagged unions, selecting a member before invoking its existing Gson codec. + +```toml +[kotlin-okhttp] +package_name = "com.example.sdk" +untagged_union_strategy = "firstMatch" +``` + +| Policy | Behavior | +| --- | --- | +| `firstMatch` (default) | First structural match in authored order; concrete members precede opaque fallbacks. | +| `strict` | Same typed API; reject multiple authored concrete matches before decoding. Use a fallback only if no concrete member matches. | +| `raw` | Explicit `VariantRaw(JsonElement)` API, preserving known, ambiguous, unmatched, and null JSON without typed selection. | + +Typed policies reject unmatched non-null values. Selection uses required wire +keys, finite scalar constraints (including numeric and boolean enums), nested +arrays/maps, and productive recursion. Numeric matching compares exact decimal +significands and unbounded exponents, without rounding through `Double` or imposing +Gson's decimal-accessor scale cap. Recursive predicates cache both successful and +failed checks by JSON-node identity. Matching is not full JSON Schema validation: +format bounds remain with the existing native codecs. + +After selection, the adapter invokes the original member's codec once. Native +decode errors propagate; they do not trigger another candidate or raw fallback. +Writing uses the declared payload type, so a nested sealed union uses its own +adapter instead of reflecting its runtime `Variant*` wrapper as a `value0` field. +Both typed policies retain the existing root-null behavior: nonnullable unions +return Kotlin `null`, while nullable unions use `VariantNull`. + +Kotlin `typealias` declarations remain transparent. Intersections retain their +existing wrapper fields (for example, `leftPart` and `rightPart`); this is not an +`allOf` codec redesign. Ordinary objects, aliases, enum declarations, and tagged +unions keep their existing APIs/codecs. Dictionary and additional-property repair +is outside this change. + +Raw mode uses Gson's native tree adapter and temporarily enables writer null +serialization, restoring its prior value even on failure. This preserves null +entries inside raw trees without changing ordinary model null-field policy. +An enclosing model named `VariantRaw` uses `VariantRawVariant` for its raw case. + +The policy can also be set through the CLI: + +```sh +openapi-nexus generate --input api.yaml --output sdk --generators kotlin-okhttp \ + --generator-config kotlin-okhttp.untagged_union_strategy=strict +``` diff --git a/src/generators/kotlin/okhttp/codegen.rs b/src/generators/kotlin/okhttp/codegen.rs index 674196423..3ad0c165b 100644 --- a/src/generators/kotlin/okhttp/codegen.rs +++ b/src/generators/kotlin/okhttp/codegen.rs @@ -43,6 +43,7 @@ impl KotlinOkhttpCodeGenerator { &package_name, &header, &request_inputs, + self.config.untagged_union_strategy, )?); files.extend(sigil_emit_api::generate_api_files( diff --git a/src/generators/kotlin/okhttp/config.rs b/src/generators/kotlin/okhttp/config.rs index 8e3cb7025..ef85cbf52 100644 --- a/src/generators/kotlin/okhttp/config.rs +++ b/src/generators/kotlin/okhttp/config.rs @@ -4,6 +4,17 @@ use serde::{Deserialize, Serialize}; pub struct KotlinOkhttpConfig { #[serde(default)] pub package_name: Option, + #[serde(default)] + pub untagged_union_strategy: UntaggedUnionStrategy, +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum UntaggedUnionStrategy { + #[default] + FirstMatch, + Strict, + Raw, } impl From for KotlinOkhttpConfig { @@ -14,3 +25,28 @@ impl From for KotlinOkhttpConfig { }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn policies_are_explicit_and_default_to_first_match() { + assert_eq!( + KotlinOkhttpConfig::default().untagged_union_strategy, + UntaggedUnionStrategy::FirstMatch + ); + for (value, policy) in [ + ("firstMatch", UntaggedUnionStrategy::FirstMatch), + ("strict", UntaggedUnionStrategy::Strict), + ("raw", UntaggedUnionStrategy::Raw), + ] { + let config: KotlinOkhttpConfig = + toml::from_str(&format!("untagged_union_strategy = {value:?}")).unwrap(); + assert_eq!(config.untagged_union_strategy, policy); + } + assert!( + toml::from_str::("untagged_union_strategy = 'unknown'").is_err() + ); + } +} diff --git a/src/generators/kotlin/okhttp/matching.rs b/src/generators/kotlin/okhttp/matching.rs new file mode 100644 index 000000000..da5fe38b3 --- /dev/null +++ b/src/generators/kotlin/okhttp/matching.rs @@ -0,0 +1,524 @@ +//! Kotlin Gson predicates over shared, target-independent selection facts. + +use std::borrow::Cow; + +use sigil_stitch::prelude::*; + +use crate::generators::type_application; +use crate::generators::union_match::{ + JsonShape, MatchScope, accepts_any, plan_named_shape, selection_shapes, +}; +use crate::ir::types::{IrObject, IrProperty, IrSchemaKind, IrSpec}; + +/// Kotlin aliases are transparent; intersections retain their wrapper fields. +pub(super) fn wire_ir(ir: &IrSpec) -> Cow<'_, IrSpec> { + if !ir + .schemas + .values() + .any(|schema| matches!(schema.kind, IrSchemaKind::Intersection(_))) + { + return Cow::Borrowed(ir); + } + let mut wire = ir.clone(); + for schema in wire.schemas.values_mut() { + let properties = match &schema.kind { + IrSchemaKind::Intersection(intersection) => { + super::util::intersection_fields(intersection) + } + _ => continue, + } + .into_iter() + .map(|(name, expr)| { + ( + name.clone(), + IrProperty { + name, + type_expr: expr.clone(), + required: true, + nullable: false, + description: None, + default_value: None, + format: None, + validation: None, + }, + ) + }) + .collect(); + schema.kind = IrSchemaKind::Object(IrObject { + properties, + additional_properties: None, + }); + } + Cow::Owned(wire) +} + +#[cfg(test)] +mod tests { + use super::*; + use sigil_stitch::assert_rendered; + use sigil_stitch::lang::kotlin::Kotlin; + + fn fixture() -> IrSpec { + crate::ir::lower::lower( + crate::parser::parse_content_yaml(include_str!( + "../../../../tests/fixtures/valid/java-union-policies.yaml" + )) + .unwrap(), + ) + .unwrap() + } + + #[test] + fn finite_predicates_preserve_scalar_kinds_and_exact_values() { + let ir = fixture(); + assert_rendered!( + Kotlin::new(), + width = 200, + MatchPlan::default() + .body(&JsonShape::Literal(9007199254740993_u64.into()), &ir) + .unwrap(), + "return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, \"9007199254740993\")\n" + ); + assert_rendered!( + Kotlin::new(), + width = 200, + MatchPlan::default() + .body(&JsonShape::Literal("wire\"value".into()), &ir) + .unwrap(), + "return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString && candidate.asString == \"wire\\\"value\"\n" + ); + } + + #[test] + fn recursive_checks_use_node_identity_and_keep_false_results() { + let ir = fixture(); + let shape = JsonShape::Reference { + schema: "Tree".into(), + scope: MatchScope::Any, + unrestricted: false, + accepts_null: false, + }; + let mut plan = MatchPlan::default(); + plan.register(&shape, &ir); + assert_rendered!( + Kotlin::new(), + width = 100, + plan.body(&shape, &ir).unwrap(), + r#"val results = memo.getOrPut(candidate) { + mutableMapOf() +} +val cached = results[0] +if (cached != null) { + return cached +} +val matched = matches1(candidate, memo) +results[0] = matched +return matched +"# + ); + } + + #[test] + fn exact_numeric_helpers_keep_exponents_out_of_bigdecimal_scale() { + use sigil_stitch::spec::modifiers::DeclarationContext; + let ir = fixture(); + let mut plan = MatchPlan::default(); + plan.register(&JsonShape::Literal(1.into()), &ir); + let helper = plan.numeric_functions().unwrap().remove(0); + assert_rendered!( + Kotlin::new(), + width = 200, + helper + .emit(&Kotlin::new(), DeclarationContext::Member) + .unwrap(), + r#"private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() +} +"# + ); + } + + #[test] + fn wire_projection_preserves_transparent_aliases_and_wrapped_intersections() { + let ir = fixture(); + let wire = wire_ir(&ir); + assert_eq!( + serde_json::to_value(&wire.schemas["NumberAlias"]).unwrap(), + serde_json::to_value(&ir.schemas["NumberAlias"]).unwrap() + ); + assert!(matches!( + ir.schemas["WrappedPair"].kind, + IrSchemaKind::Intersection(_) + )); + let IrSchemaKind::Object(pair) = &wire.schemas["WrappedPair"].kind else { + panic!("intersection wrapper facts") + }; + assert_eq!( + pair.properties + .keys() + .map(String::as_str) + .collect::>(), + ["leftPart", "rightPart"] + ); + } +} + +#[derive(Default)] +pub(super) struct MatchPlan { + shapes: Vec, +} + +impl MatchPlan { + fn constant(shape: &JsonShape) -> Option { + match shape { + JsonShape::Any => Some(true), + JsonShape::Never => Some(false), + JsonShape::Reference { + scope: MatchScope::Any, + unrestricted: true, + .. + } => Some(true), + JsonShape::AnyOf(_) | JsonShape::AllOf(_) | JsonShape::Nullable(_) + if accepts_any(shape) => + { + Some(true) + } + JsonShape::Literal(serde_json::Value::Array(_) | serde_json::Value::Object(_)) => { + Some(false) + } + _ => None, + } + } + + pub(super) fn register(&mut self, shape: &JsonShape, ir: &IrSpec) { + if Self::constant(shape).is_some() || self.shapes.contains(shape) { + return; + } + self.shapes.push(shape.clone()); + match shape { + JsonShape::Nullable(inner) | JsonShape::Array(inner) | JsonShape::Map(inner) => { + self.register(inner, ir) + } + JsonShape::MapWithConcreteValue { values, concrete } => { + self.register(&JsonShape::Map(values.clone()), ir); + self.register(concrete, ir); + } + JsonShape::Object(properties) => { + for property in properties { + self.register(&property.shape, ir); + } + } + JsonShape::ObjectWithAdditional { properties, values } => { + self.register(&JsonShape::Object(properties.clone()), ir); + self.register(values, ir); + } + JsonShape::AnyOf(children) | JsonShape::AllOf(children) => { + for child in children { + self.register(child, ir); + } + } + JsonShape::Reference { schema, scope, .. } => { + let shape = plan_named_shape(schema, ir); + if *scope == MatchScope::Any { + self.register(&shape, ir); + } else { + let (concrete, fallback) = selection_shapes(shape); + self.register(&concrete, ir); + if *scope == MatchScope::Fallback { + self.register(&fallback, ir); + } + } + } + _ => {} + } + } + + fn index(&self, shape: &JsonShape) -> usize { + self.shapes + .iter() + .position(|candidate| candidate == shape) + .expect("registered Kotlin union shape") + } + + fn recursive(shape: &JsonShape) -> bool { + if Self::constant(shape).is_some() { + return false; + } + match shape { + JsonShape::Reference { .. } => true, + JsonShape::Nullable(inner) | JsonShape::Array(inner) | JsonShape::Map(inner) => { + Self::recursive(inner) + } + JsonShape::MapWithConcreteValue { values, concrete } => { + Self::recursive(values) || Self::recursive(concrete) + } + JsonShape::Object(properties) => properties.iter().any(|p| Self::recursive(&p.shape)), + JsonShape::ObjectWithAdditional { properties, values } => { + properties.iter().any(|p| Self::recursive(&p.shape)) || Self::recursive(values) + } + JsonShape::AnyOf(children) | JsonShape::AllOf(children) => { + children.iter().any(Self::recursive) + } + _ => false, + } + } + + pub(super) fn is_recursive(&self) -> bool { + self.shapes + .iter() + .any(|shape| matches!(shape, JsonShape::Reference { .. })) + } + + pub(super) fn memo_type() -> TypeName { + type_application( + TypeName::qualified("java.util", "IdentityHashMap"), + vec![ + TypeName::importable("com.google.gson", "JsonElement"), + type_application( + TypeName::qualified("kotlin.collections", "MutableMap"), + vec![ + TypeName::qualified("kotlin", "Int"), + TypeName::qualified("kotlin", "Boolean"), + ], + ), + ], + ) + } + + pub(super) fn condition( + &self, + shape: &JsonShape, + candidate: CodeBlock, + ) -> Result { + match Self::constant(shape) { + Some(true) => sigil_quote!(Kotlin { true }), + Some(false) => sigil_quote!(Kotlin { false }), + None => { + sigil_quote!(Kotlin { $N(format!("matches{}", self.index(shape)))($C(candidate)$if(Self::recursive(shape)) { , memo }) }) + } + } + } + + pub(super) fn functions(&self, ir: &IrSpec) -> Result, SigilStitchError> { + let mut functions = self + .shapes + .iter() + .map(|shape| { + let mut function = FunSpec::builder(&format!("matches{}", self.index(shape))) + .visibility(Visibility::Private) + .add_param(ParameterSpec::of( + "candidate", + TypeName::importable("com.google.gson", "JsonElement"), + )) + .returns(vec![TypeName::qualified("kotlin", "Boolean")]); + if Self::recursive(shape) { + function = function.add_param(ParameterSpec::of("memo", Self::memo_type())); + } + function.body(self.body(shape, ir)?).build() + }) + .collect::, _>>()?; + functions.extend(self.numeric_functions()?); + Ok(functions) + } + + fn numeric_functions(&self) -> Result, SigilStitchError> { + if !self.shapes.iter().any(|shape| { + matches!( + shape, + JsonShape::Number { integer: true } + | JsonShape::Binary + | JsonShape::Literal(serde_json::Value::Number(_)) + ) + }) { + return Ok(Vec::new()); + } + let decimal = TypeName::qualified("java.math", "BigDecimal"); + let integer = TypeName::qualified("java.math", "BigInteger"); + let text = TypeName::qualified("kotlin", "String"); + let element = TypeName::importable("com.google.gson", "JsonElement"); + let boolean = TypeName::qualified("kotlin", "Boolean"); + // Gson's BigDecimal accessor imposes a scale cap; separate the exact + // significand from the unbounded exponent before choosing a codec. + let significand = FunSpec::builder("numberSignificand").visibility(Visibility::Private) + .add_param(ParameterSpec::of("literal", text.clone())).returns(vec![decimal.clone()]) + .body(sigil_quote!(Kotlin { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return $T(decimal.clone())(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + })?).build()?; + let exponent = FunSpec::builder("numberExponent").visibility(Visibility::Private) + .add_param(ParameterSpec::of("literal", text.clone())).returns(vec![integer.clone()]) + .body(sigil_quote!(Kotlin { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) $T(integer.clone()).ZERO else $T(integer.clone())(literal.substring(exponent + 1)) + })?).build()?; + let integral = FunSpec::builder("isIntegerNumber").visibility(Visibility::Private) + .add_param(ParameterSpec::of("candidate", element.clone())).returns(vec![boolean.clone()]) + .body(sigil_quote!(Kotlin { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo($T(integer.clone()).valueOf(significand.scale().toLong())) >= 0 + })?).build()?; + let binary = FunSpec::builder("isBinaryNumber").visibility(Visibility::Private) + .add_param(ParameterSpec::of("candidate", element.clone())).returns(vec![boolean.clone()]) + .body(sigil_quote!(Kotlin { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { return false } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { return true } + val scale = numberExponent(literal).subtract($T(integer.clone()).valueOf(significand.scale().toLong())) + if (scale.compareTo($T(integer.clone()).valueOf(2)) > 0) { return false } + val number = significand.unscaledValue().multiply($T(integer.clone()).TEN.pow(scale.toInt())) + return number.compareTo($T(integer.clone()).valueOf(-128)) >= 0 && number.compareTo($T(integer.clone()).valueOf(255)) <= 0 + })?).build()?; + let equal = FunSpec::builder("matchesNumber").visibility(Visibility::Private) + .add_param(ParameterSpec::of("candidate", element)).add_param(ParameterSpec::of("expected", text)) + .returns(vec![boolean]).body(sigil_quote!(Kotlin { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = $T(decimal.clone())(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { return false } + return significand.signum() == 0 || numberExponent(literal) == $T(integer.clone()).valueOf(significand.scale().toLong() - expectedNumber.scale()) + })?).build()?; + Ok(vec![significand, exponent, integral, binary, equal]) + } + + fn body(&self, shape: &JsonShape, ir: &IrSpec) -> Result { + match shape { + JsonShape::Any => sigil_quote!(Kotlin { return true }), + JsonShape::Never => sigil_quote!(Kotlin { return false }), + JsonShape::Null | JsonShape::Literal(serde_json::Value::Null) => { + sigil_quote!(Kotlin { return candidate.isJsonNull }) + } + JsonShape::Boolean => { + sigil_quote!(Kotlin { return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isBoolean }) + } + JsonShape::String => { + sigil_quote!(Kotlin { return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString }) + } + JsonShape::Number { integer: true } => { + sigil_quote!(Kotlin { return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) }) + } + JsonShape::Number { integer: false } => { + sigil_quote!(Kotlin { return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber }) + } + JsonShape::Literal(serde_json::Value::Bool(value)) => { + sigil_quote!(Kotlin { return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isBoolean && candidate.asBoolean == $L(value.to_string()) }) + } + JsonShape::Literal(serde_json::Value::String(value)) => { + sigil_quote!(Kotlin { return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString && candidate.asString == $S(value) }) + } + JsonShape::Literal(serde_json::Value::Number(value)) => { + sigil_quote!(Kotlin { return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, $S(value.to_string())) }) + } + JsonShape::Literal(_) => sigil_quote!(Kotlin { return false }), + JsonShape::Nullable(inner) => { + let check = self.condition(inner, sigil_quote!(Kotlin { candidate })?)?; + sigil_quote!(Kotlin { return candidate.isJsonNull || $C(check) }) + } + JsonShape::Binary => sigil_quote!(Kotlin { + if (!candidate.isJsonArray) { return false } + for (item in candidate.asJsonArray) { if (!isBinaryNumber(item)) { return false } } + return true + }), + JsonShape::Array(inner) | JsonShape::Map(inner) => { + let array = matches!(shape, JsonShape::Array(_)); + let check = self.condition(inner, sigil_quote!(Kotlin { item })?)?; + sigil_quote!(Kotlin { + $if(array) { + if (!candidate.isJsonArray) { return false } + for (item in candidate.asJsonArray) { if (!$C(check)) { return false } } + } $else { + if (!candidate.isJsonObject) { return false } + for (item in candidate.asJsonObject.asMap().values) { if (!$C(check)) { return false } } + } + return true + }) + } + JsonShape::MapWithConcreteValue { values, concrete } => { + let values = self.condition( + &JsonShape::Map(values.clone()), + sigil_quote!(Kotlin { candidate })?, + )?; + let concrete = self.condition(concrete, sigil_quote!(Kotlin { item })?)?; + sigil_quote!(Kotlin { + if (!$C(values)) { return false } + for (item in candidate.asJsonObject.asMap().values) { if ($C(concrete)) { return true } } + return false + }) + } + JsonShape::Object(properties) => { + let checks = properties.iter().map(|property| { + let key = &property.wire_name; + let check = self.condition(&property.shape, sigil_quote!(Kotlin { jsonObject.get($S(key)) })?)?; + if property.required { sigil_quote!(Kotlin { if (!jsonObject.has($S(key)) || !$C(check)) { return false } }) } + else { sigil_quote!(Kotlin { if (jsonObject.has($S(key)) && !$C(check)) { return false } }) } + }).collect::, _>>()?; + sigil_quote!(Kotlin { + if (!candidate.isJsonObject) { return false } + $if(!properties.is_empty()) { val jsonObject = candidate.asJsonObject } + $C_each(checks) + return true + }) + } + JsonShape::ObjectWithAdditional { properties, values } => { + let object = self.condition( + &JsonShape::Object(properties.clone()), + sigil_quote!(Kotlin { candidate })?, + )?; + let check = self.condition(values, sigil_quote!(Kotlin { item.value })?)?; + let keys: Vec<_> = properties.iter().map(|p| &p.wire_name).collect(); + let text = TypeName::qualified("kotlin", "String"); + sigil_quote!(Kotlin { + if (!$C(object)) { return false } + val declared = setOf<$T(text)>($for(key in &keys; separator = ", ") { $S(key) }) + for (item in candidate.asJsonObject.entrySet()) { if (!declared.contains(item.key) && !$C(check)) { return false } } + return true + }) + } + JsonShape::AnyOf(children) | JsonShape::AllOf(children) => { + let any = matches!(shape, JsonShape::AnyOf(_)); + let checks = children + .iter() + .map(|child| { + let check = self.condition(child, sigil_quote!(Kotlin { candidate })?)?; + if any { + sigil_quote!(Kotlin { if ($C(check)) { return true } }) + } else { + sigil_quote!(Kotlin { if (!$C(check)) { return false } }) + } + }) + .collect::, _>>()?; + sigil_quote!(Kotlin { $C_each(checks) $if(any) { return false } $else { return true } }) + } + JsonShape::Reference { schema, scope, .. } => { + let index = self.index(shape).to_string(); + let named = plan_named_shape(schema, ir); + let expression = if *scope == MatchScope::Any { + self.condition(&named, sigil_quote!(Kotlin { candidate })?)? + } else { + let (concrete, fallback) = selection_shapes(named); + let concrete = + self.condition(&concrete, sigil_quote!(Kotlin { candidate })?)?; + if *scope == MatchScope::Concrete { + concrete + } else { + let fallback = + self.condition(&fallback, sigil_quote!(Kotlin { candidate })?)?; + sigil_quote!(Kotlin { (!$C(concrete) && $C(fallback)) })? + } + }; + // Node identity avoids structural hashing; nullable lookup, not + // truthiness, retains failed matches as well as successful ones. + sigil_quote!(Kotlin { + val results = memo.getOrPut(candidate) { mutableMapOf() } + val cached = results[$L(index.clone())] + if (cached != null) { return cached } + val matched = $C(expression) + results[$L(index)] = matched + return matched + }) + } + } + } +} diff --git a/src/generators/kotlin/okhttp/mod.rs b/src/generators/kotlin/okhttp/mod.rs index 6ecf94744..82d2d8df0 100644 --- a/src/generators/kotlin/okhttp/mod.rs +++ b/src/generators/kotlin/okhttp/mod.rs @@ -1,8 +1,10 @@ pub mod codegen; pub mod config; +mod matching; pub mod runtime; pub mod sigil_emit; pub mod sigil_emit_api; +mod union; pub mod util; pub use codegen::KotlinOkhttpCodeGenerator; diff --git a/src/generators/kotlin/okhttp/sigil_emit.rs b/src/generators/kotlin/okhttp/sigil_emit.rs index c9283b0c4..7020a54ec 100644 --- a/src/generators/kotlin/okhttp/sigil_emit.rs +++ b/src/generators/kotlin/okhttp/sigil_emit.rs @@ -1,5 +1,3 @@ -use std::collections::HashSet; - use crate::codegen::traits::file_writer::FileInfo; use crate::generators::closed_sum::{ ClosedSumCasePlan, ClosedSumEncoding, ClosedSumPlan, plan_tagged_union, plan_union, @@ -12,14 +10,17 @@ use crate::ir::types::{ IrEnum, IrEnumValueType, IrIntersection, IrObject, IrPrimitive, IrSchema, IrSchemaKind, IrSpec, IrTaggedUnion, IrTypeExpr, IrUnion, }; -use heck::{ToLowerCamelCase, ToPascalCase}; +use heck::ToPascalCase; use sigil_stitch::lang::kotlin::Kotlin; use sigil_stitch::prelude::*; use sigil_stitch::spec::closed_sum_case_spec::ClosedSumCaseSpec; use sigil_stitch::spec::closed_sum_spec::ClosedSumSpec; use sigil_stitch::spec::where_spec::TypeArgument; -use super::util::{escape_kt_string, kt_decl_name, kt_primitive, kt_ref, kt_type_str, unique_name}; +use super::config::UntaggedUnionStrategy; +use super::util::{ + escape_kt_string, intersection_fields, kt_decl_name, kt_primitive, kt_ref, kt_type_str, +}; const RENDER_WIDTH: usize = 100; @@ -28,10 +29,12 @@ pub(crate) fn generate_model_files( package_name: &str, header: &str, request_inputs: &RequestInputPlan, + strategy: UntaggedUnionStrategy, ) -> Result, GeneratorError> { let mut files = Vec::new(); + let matching_ir = super::matching::wire_ir(ir); for (_name, schema) in &ir.schemas { - let body = emit_model_body(schema, ir, package_name) + let body = emit_model_body(schema, ir, &matching_ir, package_name, strategy) .map_err(|error| GeneratorError::emission(ArtifactKind::Model, &schema.name, error))?; let class_name = schema.name.to_pascal_case(); let filename = format!("{class_name}.kt"); @@ -88,13 +91,15 @@ fn request_input_kt_type(field: &RequestInputField) -> String { fn emit_model_body( schema: &IrSchema, ir: &IrSpec, + matching_ir: &IrSpec, package_name: &str, + strategy: UntaggedUnionStrategy, ) -> Result { let file_spec = match &schema.kind { IrSchemaKind::Object(obj) => emit_object(schema, obj, package_name), IrSchemaKind::Enum(en) => emit_enum(schema, en, package_name), IrSchemaKind::Alias(expr) => emit_alias(schema, expr, package_name), - IrSchemaKind::Union(u) => emit_union(schema, u, ir, package_name), + IrSchemaKind::Union(u) => emit_union(schema, u, matching_ir, package_name, strategy), IrSchemaKind::Intersection(i) => emit_intersection(schema, i, package_name), IrSchemaKind::TaggedUnion(tu) => emit_tagged_union(schema, tu, ir, package_name), }?; @@ -262,12 +267,25 @@ fn emit_union( union: &IrUnion, ir: &IrSpec, package_name: &str, + strategy: UntaggedUnionStrategy, ) -> Result { - emit_closed_sum( - &plan_union(&schema.name, schema.description.as_deref(), union), - &ir.schemas, - package_name, - ) + let mut plan = plan_union(&schema.name, schema.description.as_deref(), union); + if strategy == UntaggedUnionStrategy::Raw { + plan.cases = vec![ClosedSumCasePlan { + name: if plan.name == "VariantRaw" { + "VariantRawVariant" + } else { + "VariantRaw" + } + .into(), + payload: Some(IrTypeExpr::Any), + doc: None, + wire_name: None, + is_null: false, + }]; + } + let adapter = super::union::adapter(schema, &plan, ir, strategy)?; + closed_sum_file(&plan, package_name, adapter) } // --------------------------------------------------------------------------- @@ -288,10 +306,8 @@ fn emit_intersection( tb = tb.doc(doc); } - let mut used_names: HashSet = HashSet::new(); - for member in &inter.members { + for (field_name, member) in intersection_fields(inter) { let member_type = kt_type_str(member); - let field_name = unique_name(&member_type.to_lower_camel_case(), &mut used_names); tb = tb.add_primary_constructor_param( ParameterSpec::builder(&field_name, TypeName::primitive(&member_type)) .is_property() @@ -315,15 +331,18 @@ fn emit_tagged_union( ) -> Result { emit_closed_sum( &plan_tagged_union(&schema.name, schema.description.as_deref(), tu, ir), - &ir.schemas, package_name, ) } -fn emit_closed_sum( +fn emit_closed_sum(plan: &ClosedSumPlan, package_name: &str) -> Result { + closed_sum_file(plan, package_name, kotlin_closed_sum_adapter(plan)?) +} + +fn closed_sum_file( plan: &ClosedSumPlan, - schemas: &indexmap::IndexMap, package_name: &str, + adapter: CodeBlock, ) -> Result { let mut sum = ClosedSumSpec::builder(&plan.name).visibility(Visibility::Public); for doc in &plan.docs { @@ -349,9 +368,7 @@ fn emit_closed_sum( for import in kotlin_adapter_imports() { file = file.add_import(import); } - file.add_closed_sum(sum.build()?) - .add_code(kotlin_closed_sum_adapter(plan, schemas)?) - .build() + file.add_closed_sum(sum.build()?).add_code(adapter).build() } fn kotlin_closed_sum_case(case: &ClosedSumCasePlan) -> Result { @@ -365,7 +382,7 @@ fn kotlin_closed_sum_case(case: &ClosedSumCasePlan) -> Result TypeName { +pub(super) fn kotlin_type_name(expr: &IrTypeExpr) -> TypeName { match expr { IrTypeExpr::ScalarEnum { representation, .. } => kotlin_type_name(representation), IrTypeExpr::Named(name) => TypeName::primitive(&name.to_pascal_case()), @@ -406,12 +423,9 @@ fn kotlin_adapter_imports() -> [ImportSpec; 12] { ] } -fn kotlin_closed_sum_adapter( - plan: &ClosedSumPlan, - schemas: &indexmap::IndexMap, -) -> Result { +fn kotlin_closed_sum_adapter(plan: &ClosedSumPlan) -> Result { let read_dispatch = - kotlin_adapter_read_dispatch(plan, schemas).map_err(|error| SigilStitchError::Render { + kotlin_adapter_read_dispatch(plan).map_err(|error| SigilStitchError::Render { context: format!("Kotlin {} Gson adapter read dispatch", plan.name), message: error.to_string(), })?; @@ -454,63 +468,9 @@ fn kotlin_closed_sum_adapter( }) } -fn kotlin_adapter_read_dispatch( - plan: &ClosedSumPlan, - schemas: &indexmap::IndexMap, -) -> Result { +fn kotlin_adapter_read_dispatch(plan: &ClosedSumPlan) -> Result { match &plan.encoding { - ClosedSumEncoding::Untagged => { - let null_case = plan.cases.iter().find(|case| case.is_null); - let null_result = match null_case { - Some(case) => sigil_quote!(Kotlin { - $N(plan.name.as_str()).$N(case.name.as_str()) - })?, - None => sigil_quote!(Kotlin { null })?, - }; - let mut branches = vec![sigil_quote!(Kotlin { - element.isJsonNull -> $C(null_result) - })?]; - let mut catch_all = None; - let cases = plan - .cases - .iter() - .filter(|case| !case.is_null && !is_catch_all_case(case)) - .chain( - plan.cases - .iter() - .filter(|case| !case.is_null && is_catch_all_case(case)), - ); - for case in cases { - let Some(payload) = &case.payload else { - continue; - }; - let condition = kotlin_json_condition(payload, schemas)?; - let source = sigil_quote!(Kotlin { element })?; - let result = kotlin_adapter_read_value(plan, case, payload, source)?; - if is_catch_all_case(case) { - catch_all = Some(result); - } else { - branches.push(sigil_quote!(Kotlin { - $C(condition) -> $C(result) - })?); - } - } - let fallback = match catch_all { - Some(result) => sigil_quote!(Kotlin { else -> $C(result) })?, - None => sigil_quote!(Kotlin { - else -> throw JsonParseException($S(format!( - "unknown {} union value", - plan.name - ))) - })?, - }; - sigil_quote!(Kotlin { - return when { - $C_each(branches) - $C(fallback) - } - }) - } + ClosedSumEncoding::Untagged => unreachable!("untagged unions use structural selection"), ClosedSumEncoding::Internal { discriminator_field, } => { @@ -602,7 +562,7 @@ fn kotlin_external_read_case( }) } -fn kotlin_adapter_read_value( +pub(super) fn kotlin_adapter_read_value( plan: &ClosedSumPlan, case: &ClosedSumCasePlan, payload: &IrTypeExpr, @@ -678,13 +638,7 @@ fn kotlin_adapter_write_case( let case_name = case.name.as_str(); let body = match &plan.encoding { ClosedSumEncoding::Untagged => { - if case.is_null { - sigil_quote!(Kotlin { writer.nullValue() })? - } else { - sigil_quote!(Kotlin { - gson.toJson(gson.toJsonTree(value.value0), writer) - })? - } + unreachable!("untagged unions delegate declared payload types") } ClosedSumEncoding::Internal { discriminator_field, @@ -748,142 +702,6 @@ fn kotlin_adapter_write_case( }) } -fn kotlin_json_condition( - expr: &IrTypeExpr, - schemas: &indexmap::IndexMap, -) -> Result { - match expr { - IrTypeExpr::ScalarEnum { representation, .. } => { - kotlin_json_condition(representation, schemas) - } - IrTypeExpr::Primitive( - IrPrimitive::String - | IrPrimitive::StringWithFormat(_) - | IrPrimitive::Date - | IrPrimitive::DateTime - | IrPrimitive::Uuid, - ) - | IrTypeExpr::StringLiteral(_) - | IrTypeExpr::StringEnum(_) => { - let base = sigil_quote!(Kotlin { - element.isJsonPrimitive && element.asJsonPrimitive.isString - })?; - match expr { - IrTypeExpr::StringLiteral(value) => sigil_quote!(Kotlin { - $C(base) && element.asString == $S(value) - }), - IrTypeExpr::StringEnum(values) => { - let checks = values - .iter() - .map(|value| sigil_quote!(Kotlin { element.asString == $S(value) })) - .collect::, _>>()?; - let Some((first, rest)) = checks.split_first() else { - return sigil_quote!(Kotlin { false }); - }; - let values = kotlin_join_conditions(first.clone(), rest.to_vec(), true)?; - sigil_quote!(Kotlin { ($C(base) && $C(values)) }) - } - _ => Ok(base), - } - } - IrTypeExpr::Primitive(IrPrimitive::Boolean) => { - sigil_quote!(Kotlin { - element.isJsonPrimitive && element.asJsonPrimitive.isBoolean - }) - } - IrTypeExpr::Primitive( - IrPrimitive::Integer - | IrPrimitive::IntegerWithFormat(_) - | IrPrimitive::Number - | IrPrimitive::NumberWithFormat(_), - ) => sigil_quote!(Kotlin { - element.isJsonPrimitive && element.asJsonPrimitive.isNumber - }), - IrTypeExpr::Primitive(IrPrimitive::Binary) => { - sigil_quote!(Kotlin { element.isJsonPrimitive && element.asJsonPrimitive.isString }) - } - IrTypeExpr::Array(_) => sigil_quote!(Kotlin { element.isJsonArray }), - IrTypeExpr::Map(_) => sigil_quote!(Kotlin { element.isJsonObject }), - IrTypeExpr::Nullable(inner) => { - let inner = kotlin_json_condition(inner, schemas)?; - sigil_quote!(Kotlin { element.isJsonNull || $C(inner) }) - } - IrTypeExpr::Any => sigil_quote!(Kotlin { true }), - IrTypeExpr::Union(members) => { - let conditions = members - .iter() - .map(|member| kotlin_json_condition(member, schemas)) - .collect::, _>>()?; - let Some((first, rest)) = conditions.split_first() else { - return sigil_quote!(Kotlin { false }); - }; - kotlin_join_conditions(first.clone(), rest.to_vec(), true) - } - IrTypeExpr::Named(name) => match schemas.get(name).map(|schema| &schema.kind) { - Some(IrSchemaKind::Object(object)) => { - let required = object - .properties - .iter() - .filter(|(_, property)| property.required) - .map(|(wire_name, _)| wire_name) - .collect::>(); - let checks = required - .iter() - .map(|wire_name| { - sigil_quote!(Kotlin { - element.asJsonObject.has($S(wire_name.as_str())) - }) - }) - .collect::, _>>()?; - let object_check = sigil_quote!(Kotlin { element.isJsonObject })?; - Ok(kotlin_join_conditions(object_check, checks, false)?) - } - Some(IrSchemaKind::Enum(en)) => match en.value_type { - IrEnumValueType::String => sigil_quote!(Kotlin { - element.isJsonPrimitive && element.asJsonPrimitive.isString - }), - IrEnumValueType::Integer | IrEnumValueType::Number => sigil_quote!(Kotlin { - element.isJsonPrimitive && element.asJsonPrimitive.isNumber - }), - IrEnumValueType::Mixed => sigil_quote!(Kotlin { element.isJsonPrimitive }), - }, - Some(IrSchemaKind::Alias(inner)) => kotlin_json_condition(inner, schemas), - Some(IrSchemaKind::Union(union)) => { - let conditions = union - .members - .iter() - .map(|member| kotlin_json_condition(member, schemas)) - .collect::, _>>()?; - let Some((first, rest)) = conditions.split_first() else { - return sigil_quote!(Kotlin { false }); - }; - kotlin_join_conditions(first.clone(), rest.to_vec(), true) - } - _ => sigil_quote!(Kotlin { element.isJsonObject }), - }, - } -} - -fn is_catch_all_case(case: &ClosedSumCasePlan) -> bool { - matches!(case.payload.as_ref(), Some(IrTypeExpr::Any)) -} - -fn kotlin_join_conditions( - first: CodeBlock, - rest: Vec, - disjunction: bool, -) -> Result { - let mut result = first; - for condition in rest { - result = if disjunction { - sigil_quote!(Kotlin { ($C(result) || $C(condition)) })? - } else { - sigil_quote!(Kotlin { ($C(result) && $C(condition)) })? - }; - } - Ok(result) -} - // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- @@ -913,7 +731,7 @@ mod tests { use sigil_stitch::assert_rendered; #[test] - fn closed_sum_adapter_render_is_pinned() { + fn tagged_adapter_render_is_pinned() { let plan = ClosedSumPlan { name: "Choice".to_string(), docs: Vec::new(), @@ -921,12 +739,12 @@ mod tests { name: "VariantString".to_string(), payload: Some(IrTypeExpr::Primitive(IrPrimitive::String)), doc: None, - wire_name: None, + wire_name: Some("text".into()), is_null: false, }], - encoding: ClosedSumEncoding::Untagged, + encoding: ClosedSumEncoding::External, }; - let adapter = kotlin_closed_sum_adapter(&plan, &indexmap::IndexMap::new()).unwrap(); + let adapter = kotlin_closed_sum_adapter(&plan).unwrap(); assert_rendered!( Kotlin::new(), width = 100, @@ -936,10 +754,10 @@ mod tests { override fun read (reader: JsonReader): Choice? { val element = JsonParser.parseReader(reader) + val jsonObject = element.asJsonObject return when { - element.isJsonNull -> null - element.isJsonPrimitive && element.asJsonPrimitive.isString -> Choice.VariantString(element.asString) - else -> throw JsonParseException("unknown Choice union value") + jsonObject.has("text") -> Choice.VariantString(jsonObject["text"].asString) + else -> throw JsonParseException("unknown Choice variant") } } @@ -950,7 +768,9 @@ mod tests { } when (value) { is Choice.VariantString -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + val jsonObject = JsonObject() + jsonObject.add("text", gson.toJsonTree(value.value0)) + gson.toJson(jsonObject, writer) } } } diff --git a/src/generators/kotlin/okhttp/union.rs b/src/generators/kotlin/okhttp/union.rs new file mode 100644 index 000000000..385882e1d --- /dev/null +++ b/src/generators/kotlin/okhttp/union.rs @@ -0,0 +1,166 @@ +//! Original-member selection before a single ordinary Kotlin Gson delegation. + +use sigil_stitch::lang::kotlin::Kotlin; +use sigil_stitch::prelude::*; + +use crate::generators::closed_sum::ClosedSumPlan; +use crate::generators::type_application; +use crate::generators::union_match::plan_union_members; +use crate::ir::types::{IrSchema, IrSchemaKind, IrSpec}; + +use super::config::UntaggedUnionStrategy; +use super::matching::MatchPlan; +use super::sigil_emit::{kotlin_adapter_read_value, kotlin_type_name}; + +pub(super) fn adapter( + schema: &IrSchema, + plan: &ClosedSumPlan, + ir: &IrSpec, + strategy: UntaggedUnionStrategy, +) -> Result { + let raw = strategy == UntaggedUnionStrategy::Raw; + let mut matching = MatchPlan::default(); + let root = TypeName::primitive(&plan.name); + let read_body = if raw { + let case = &plan.cases[0].name; + sigil_quote!(Kotlin { return $N(&plan.name).$N(case)(element) })? + } else { + typed_read(schema, plan, ir, strategy, &mut matching)? + }; + let read = FunSpec::builder("read") + .is_override() + .add_param(ParameterSpec::of( + "reader", + TypeName::importable("com.google.gson.stream", "JsonReader"), + )) + .returns(vec![TypeName::optional(root.clone())]) + .body(sigil_quote!(Kotlin { + val element = JsonParser.parseReader(reader) + $C(read_body) + })?) + .build()?; + let cases = + if raw { + Vec::new() + } else { + plan.cases + .iter() + .map(|case| { + let body = if let Some(payload) = &case.payload { + let payload = kotlin_type_name(payload); + // Kotlin sealed subclasses do not inherit the root's Gson + // annotation. Delegate through the declared member type, not + // reflection over its runtime Variant* wrapper. + sigil_quote!(Kotlin { + gson.toJson(value.value0, object : TypeToken<$T(payload)>() {}.type, writer) + })? + } else { sigil_quote!(Kotlin { writer.nullValue() })? }; + let case_type = TypeName::associated_type(root.clone(), None, &case.name); + sigil_quote!(Kotlin { is $T(case_type) -> { $C(body) } }) + }) + .collect::, _>>()? + }; + let write_body = if raw { + let case_type = TypeName::associated_type(root.clone(), None, &plan.cases[0].name); + sigil_quote!(Kotlin { + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { gson.getAdapter(JsonElement::class.java).write(writer, (value as $T(case_type)).value0) } + finally { writer.serializeNulls = serializeNulls } + })? + } else { + sigil_quote!(Kotlin { when (value) { $C_each(cases) } })? + }; + let write = FunSpec::builder("write") + .is_override() + .add_param(ParameterSpec::of( + "writer", + TypeName::importable("com.google.gson.stream", "JsonWriter"), + )) + .add_param(ParameterSpec::of("value", TypeName::optional(root.clone()))) + .body(sigil_quote!(Kotlin { + if (value == null) { writer.nullValue(); return } + $C(write_body) + })?) + .build()?; + let field = FieldSpec::builder("gson", TypeName::importable("com.google.gson", "Gson")) + .visibility(Visibility::Private) + .is_readonly() + .initializer(sigil_quote!(Kotlin { Gson() })?.__sigil_trim_trailing_newline()) + .build()?; + let mut adapter = TypeSpec::builder(&format!("{}JsonAdapter", plan.name), TypeKind::Class) + .visibility(Visibility::Private) + .extends(type_application( + TypeName::importable("com.google.gson", "TypeAdapter"), + vec![root], + )) + .add_field(field) + .add_method(read) + .add_method(write); + for helper in matching.functions(ir)? { + adapter = adapter.add_method(helper); + } + let declarations = adapter.build()?.emit(&Kotlin::new())?; + sigil_quote!(Kotlin { $C_each(declarations) }) +} + +fn typed_read( + schema: &IrSchema, + plan: &ClosedSumPlan, + ir: &IrSpec, + strategy: UntaggedUnionStrategy, + matching: &mut MatchPlan, +) -> Result { + let IrSchemaKind::Union(union) = &schema.kind else { + unreachable!("untagged union schema") + }; + let members = plan_union_members(&schema.name, union, ir); + for member in &members { + matching.register(&member.shape, ir); + } + let null = match plan.cases.iter().find(|case| case.is_null) { + Some(case) => sigil_quote!(Kotlin { return $N(&plan.name).$N(&case.name) })?, + None => sigil_quote!(Kotlin { return null })?, + }; + let strict = strategy == UntaggedUnionStrategy::Strict; + let mut selection = Vec::new(); + for member in &members { + let check = matching.condition(&member.shape, sigil_quote!(Kotlin { element })?)?; + let index = member.source_index.to_string(); + selection.push(if strict && !member.fallback { + sigil_quote!(Kotlin { + if ($C(check)) { + if (selected >= 0) { throw JsonParseException($S(format!("ambiguous untagged union {}: multiple concrete members match", plan.name))) } + selected = $L(index) + } + })? + } else { + sigil_quote!(Kotlin { if (selected < 0 && $C(check)) { selected = $L(index) } })? + }); + } + let delegates = plan + .cases + .iter() + .enumerate() + .filter_map(|(index, case)| { + case.payload.as_ref().map(|payload| { + let read = kotlin_adapter_read_value( + plan, + case, + payload, + sigil_quote!(Kotlin { element })?, + )?; + sigil_quote!(Kotlin { if (selected == $L(index.to_string())) { return $C(read) } }) + }) + }) + .collect::, SigilStitchError>>()?; + let memo = MatchPlan::memo_type(); + sigil_quote!(Kotlin { + if (element.isJsonNull) { $C(null) } + $if(matching.is_recursive()) { val memo = $T(memo)() } + var selected = -1 + $C_each(selection) + $C_each(delegates) + throw JsonParseException($S(format!("unknown {} union value", plan.name))) + }) +} diff --git a/src/generators/kotlin/okhttp/util.rs b/src/generators/kotlin/okhttp/util.rs index 0a6cf035d..7e08a92b0 100644 --- a/src/generators/kotlin/okhttp/util.rs +++ b/src/generators/kotlin/okhttp/util.rs @@ -1,6 +1,6 @@ use std::collections::HashSet; -use crate::ir::types::{IrPrimitive, IrTypeExpr}; +use crate::ir::types::{IrIntersection, IrPrimitive, IrTypeExpr}; use heck::{ToLowerCamelCase, ToPascalCase}; use sigil_stitch::lang::kotlin::Kotlin; use sigil_stitch::prelude::RendererLang; @@ -90,6 +90,20 @@ pub fn unique_name(desired: &str, used: &mut HashSet) -> String { unreachable!() } +/// Preserve the same member order and wrapper keys for declarations and matching. +pub(super) fn intersection_fields(intersection: &IrIntersection) -> Vec<(String, &IrTypeExpr)> { + let mut used_names = HashSet::new(); + intersection + .members + .iter() + .map(|member| { + let field_name = + unique_name(&kt_type_str(member).to_lower_camel_case(), &mut used_names); + (field_name, member) + }) + .collect() +} + pub fn sanitize_operation_id(op_id: &str, method: &str, path: &str) -> String { if !op_id.is_empty() { return op_id.to_string(); @@ -135,7 +149,8 @@ pub fn render_value_as_string(value_expr: &str, t: &IrTypeExpr) -> String { #[cfg(test)] mod tests { - use super::{kt_decl_name, kt_param_name, kt_ref}; + use super::{intersection_fields, kt_decl_name, kt_param_name, kt_ref}; + use crate::ir::types::{IrIntersection, IrTypeExpr}; #[test] fn kotlin_reserved_identifiers_are_escaped_at_emission() { @@ -146,4 +161,27 @@ mod tests { assert_eq!(kt_ref("file"), "`file`"); assert_eq!(kt_ref("name"), "name"); } + + #[test] + fn intersection_wrapper_fields_retain_order_and_disambiguate_type_spellings() { + let intersection = IrIntersection { + members: vec![ + IrTypeExpr::Named("foo_bar".into()), + IrTypeExpr::Named("FooBar".into()), + IrTypeExpr::Nullable(Box::new(IrTypeExpr::Named("FooBar".into()))), + IrTypeExpr::Array(Box::new(IrTypeExpr::Named("FooBar".into()))), + ], + }; + let fields = intersection_fields(&intersection); + assert_eq!( + fields + .iter() + .map(|(name, _)| name.as_str()) + .collect::>(), + ["fooBar", "fooBar2", "fooBar3", "listFooBar"] + ); + for ((_, member), original) in fields.iter().zip(&intersection.members) { + assert!(std::ptr::eq(*member, original)); + } + } } diff --git a/tests/generated-clients/jvm/README.md b/tests/generated-clients/jvm/README.md index c3b65d738..6506aa396 100644 --- a/tests/generated-clients/jvm/README.md +++ b/tests/generated-clients/jvm/README.md @@ -10,6 +10,9 @@ boundaries, raw null preservation, and OkHttp response/error-body integration. Independent helper-type projects also compile recursive unions alongside models named `Integer` and `Boolean`; numeric controls exercise extreme exponents without weakening native delegate errors or finite-value precision. +Kotlin's independent policy consumers use the same schema fixtures, preserving +transparent type aliases and its own native scalar decoding behavior while +checking exact finite matching, recursion, wrapped intersections and raw trees. Generated manifests and source are used unchanged. Each fixture has a separate consumer subproject, so identically named SDK classes do not share a classpath. diff --git a/tests/generated-clients/jvm/kotlin/cases/union-first-match/build.gradle b/tests/generated-clients/jvm/kotlin/cases/union-first-match/build.gradle new file mode 100644 index 000000000..e76eca30a --- /dev/null +++ b/tests/generated-clients/jvm/kotlin/cases/union-first-match/build.gradle @@ -0,0 +1,2 @@ +sourceSets.test.java.srcDir("../../support/union-policies") +sourceSets.test.java.srcDir("../../support/http") diff --git a/tests/generated-clients/jvm/kotlin/cases/union-first-match/gradle.lockfile b/tests/generated-clients/jvm/kotlin/cases/union-first-match/gradle.lockfile new file mode 100644 index 000000000..08245d89f --- /dev/null +++ b/tests/generated-clients/jvm/kotlin/cases/union-first-match/gradle.lockfile @@ -0,0 +1,25 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +# To regenerate this file, run: ./gradlew :cases:union-first-match:dependencies --write-locks +com.google.code.gson:gson:2.11.0=testCompileClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.27.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okhttp3:okhttp:4.12.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okio:okio-jvm:3.6.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okio:okio:3.6.0=testCompileClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath +org.jetbrains.kotlin:kotlin-stdlib-common:2.2.21=testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10=testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10=testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:2.2.21=testCompileClasspath,testRuntimeClasspath +org.jetbrains:annotations:13.0=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.12.2=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.12.2=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.12.2=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.12.2=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.12.2=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.12.2=testRuntimeClasspath +org.junit.platform:junit-platform-launcher:1.12.2=testRuntimeClasspath +org.junit:junit-bom:5.12.2=testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +empty=testAnnotationProcessor diff --git a/tests/generated-clients/jvm/kotlin/cases/union-first-match/src/test/java/UnionPolicyTest.java b/tests/generated-clients/jvm/kotlin/cases/union-first-match/src/test/java/UnionPolicyTest.java new file mode 100644 index 000000000..0727293c7 --- /dev/null +++ b/tests/generated-clients/jvm/kotlin/cases/union-first-match/src/test/java/UnionPolicyTest.java @@ -0,0 +1,3 @@ +class UnionPolicyTest extends TypedUnionPolicyCases { + @Override protected boolean strict() { return false; } +} diff --git a/tests/generated-clients/jvm/kotlin/cases/union-helper-types-strict/build.gradle b/tests/generated-clients/jvm/kotlin/cases/union-helper-types-strict/build.gradle new file mode 100644 index 000000000..f3e53f438 --- /dev/null +++ b/tests/generated-clients/jvm/kotlin/cases/union-helper-types-strict/build.gradle @@ -0,0 +1 @@ +sourceSets.test.java.srcDir("../union-helper-types/src/test/java") diff --git a/tests/generated-clients/jvm/kotlin/cases/union-helper-types-strict/gradle.lockfile b/tests/generated-clients/jvm/kotlin/cases/union-helper-types-strict/gradle.lockfile new file mode 100644 index 000000000..47e7f570d --- /dev/null +++ b/tests/generated-clients/jvm/kotlin/cases/union-helper-types-strict/gradle.lockfile @@ -0,0 +1,25 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +# To regenerate this file, run: ./gradlew :cases:union-helper-types-strict:dependencies --write-locks +com.google.code.gson:gson:2.11.0=testCompileClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.27.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okhttp3:okhttp:4.12.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okio:okio-jvm:3.6.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okio:okio:3.6.0=testCompileClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath +org.jetbrains.kotlin:kotlin-stdlib-common:2.2.21=testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10=testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10=testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:2.2.21=testCompileClasspath,testRuntimeClasspath +org.jetbrains:annotations:13.0=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.12.2=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.12.2=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.12.2=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.12.2=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.12.2=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.12.2=testRuntimeClasspath +org.junit.platform:junit-platform-launcher:1.12.2=testRuntimeClasspath +org.junit:junit-bom:5.12.2=testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +empty=testAnnotationProcessor diff --git a/tests/generated-clients/jvm/kotlin/cases/union-helper-types-strict/src/test/java/StrictHelperTypesTest.java b/tests/generated-clients/jvm/kotlin/cases/union-helper-types-strict/src/test/java/StrictHelperTypesTest.java new file mode 100644 index 000000000..918b2bc11 --- /dev/null +++ b/tests/generated-clients/jvm/kotlin/cases/union-helper-types-strict/src/test/java/StrictHelperTypesTest.java @@ -0,0 +1 @@ +class StrictHelperTypesTest extends RecursiveHelperTypesTest {} diff --git a/tests/generated-clients/jvm/kotlin/cases/union-helper-types/gradle.lockfile b/tests/generated-clients/jvm/kotlin/cases/union-helper-types/gradle.lockfile new file mode 100644 index 000000000..8353493ce --- /dev/null +++ b/tests/generated-clients/jvm/kotlin/cases/union-helper-types/gradle.lockfile @@ -0,0 +1,25 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +# To regenerate this file, run: ./gradlew :cases:union-helper-types:dependencies --write-locks +com.google.code.gson:gson:2.11.0=testCompileClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.27.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okhttp3:okhttp:4.12.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okio:okio-jvm:3.6.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okio:okio:3.6.0=testCompileClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath +org.jetbrains.kotlin:kotlin-stdlib-common:2.2.21=testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10=testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10=testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:2.2.21=testCompileClasspath,testRuntimeClasspath +org.jetbrains:annotations:13.0=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.12.2=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.12.2=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.12.2=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.12.2=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.12.2=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.12.2=testRuntimeClasspath +org.junit.platform:junit-platform-launcher:1.12.2=testRuntimeClasspath +org.junit:junit-bom:5.12.2=testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +empty=testAnnotationProcessor diff --git a/tests/generated-clients/jvm/kotlin/cases/union-helper-types/src/test/java/RecursiveHelperTypesTest.java b/tests/generated-clients/jvm/kotlin/cases/union-helper-types/src/test/java/RecursiveHelperTypesTest.java new file mode 100644 index 000000000..444b79039 --- /dev/null +++ b/tests/generated-clients/jvm/kotlin/cases/union-helper-types/src/test/java/RecursiveHelperTypesTest.java @@ -0,0 +1,17 @@ +import com.example.sdk.models.Tree; +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class RecursiveHelperTypesTest { + @Test void recursivePredicatesCompileAlongsideModelsNamedBooleanAndInteger() { + Gson gson = new Gson(); + String wire = "{\"children\":[{\"children\":[\"leaf\"]}]}"; + Tree value = gson.fromJson(wire, Tree.class); + assertInstanceOf(Tree.VariantBranch.class, value); + assertEquals(JsonParser.parseString(wire), gson.toJsonTree(value, Tree.class)); + assertThrows(JsonParseException.class, () -> gson.fromJson("{\"children\":[42]}", Tree.class)); + } +} diff --git a/tests/generated-clients/jvm/kotlin/cases/union-raw/build.gradle b/tests/generated-clients/jvm/kotlin/cases/union-raw/build.gradle new file mode 100644 index 000000000..e5074adee --- /dev/null +++ b/tests/generated-clients/jvm/kotlin/cases/union-raw/build.gradle @@ -0,0 +1 @@ +sourceSets.test.java.srcDir("../../support/http") diff --git a/tests/generated-clients/jvm/kotlin/cases/union-raw/gradle.lockfile b/tests/generated-clients/jvm/kotlin/cases/union-raw/gradle.lockfile new file mode 100644 index 000000000..851a6dd39 --- /dev/null +++ b/tests/generated-clients/jvm/kotlin/cases/union-raw/gradle.lockfile @@ -0,0 +1,25 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +# To regenerate this file, run: ./gradlew :cases:union-raw:dependencies --write-locks +com.google.code.gson:gson:2.11.0=testCompileClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.27.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okhttp3:okhttp:4.12.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okio:okio-jvm:3.6.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okio:okio:3.6.0=testCompileClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath +org.jetbrains.kotlin:kotlin-stdlib-common:2.2.21=testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10=testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10=testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:2.2.21=testCompileClasspath,testRuntimeClasspath +org.jetbrains:annotations:13.0=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.12.2=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.12.2=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.12.2=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.12.2=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.12.2=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.12.2=testRuntimeClasspath +org.junit.platform:junit-platform-launcher:1.12.2=testRuntimeClasspath +org.junit:junit-bom:5.12.2=testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +empty=testAnnotationProcessor diff --git a/tests/generated-clients/jvm/kotlin/cases/union-raw/src/test/java/RawUnionTest.java b/tests/generated-clients/jvm/kotlin/cases/union-raw/src/test/java/RawUnionTest.java new file mode 100644 index 000000000..a9e7e8bce --- /dev/null +++ b/tests/generated-clients/jvm/kotlin/cases/union-raw/src/test/java/RawUnionTest.java @@ -0,0 +1,68 @@ +import com.example.sdk.models.*; +import com.example.sdk.apis.*; +import com.google.gson.*; +import com.google.gson.stream.JsonWriter; +import java.io.StringWriter; +import java.io.Writer; +import java.io.IOException; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class RawUnionTest { + private final Gson gson = new Gson(); + + @Test void httpSuccessAndErrorsPreserveUnknownRawTrees() { + String wire = "{\"unknown\":null,\"number\":9007199254740993}"; + GetChoiceResponse response = UnionHttpTestSupport.api(200, wire).getChoice(); + assertInstanceOf(FiniteChoice.VariantRaw.class, response.getStatus200()); + assertEquals(JsonParser.parseString(wire), gson.toJsonTree(response.getStatus200(), FiniteChoice.class)); + GetChoiceException error = assertThrows(GetChoiceException.class, () -> UnionHttpTestSupport.api(400, wire).getChoice()); + GetChoiceBadRequest detail = assertInstanceOf(GetChoiceBadRequest.class, error.getDetail()); + assertEquals(JsonParser.parseString(wire), gson.toJsonTree(detail.body(), FiniteChoice.class)); + } + + @Test void knownAmbiguousUnknownAndNullTreesRoundTrip() { + for (String wire : new String[] {"null", "42", "[null,9007199254740993]", "{\"unknown\":null,\"revision\":2}", "{\"wire_name\":\"both\"}"}) { + FiniteChoice value = gson.fromJson(wire, FiniteChoice.class); + FiniteChoice.VariantRaw raw = assertInstanceOf(FiniteChoice.VariantRaw.class, value); + assertEquals(JsonParser.parseString(wire), raw.getValue0()); + assertEquals(JsonParser.parseString(wire), gson.toJsonTree(value, FiniteChoice.class)); + assertEquals(JsonParser.parseString(wire), JsonParser.parseString(gson.toJson(value, FiniteChoice.class))); + } + } + + @Test void rawCaseNamesDoNotCollideWithTheEnclosingClass() { + VariantRaw value = gson.fromJson("{\"null\":null}", VariantRaw.class); + assertInstanceOf(VariantRaw.VariantRawVariant.class, value); + assertEquals(JsonParser.parseString("{\"null\":null}"), gson.toJsonTree(value, VariantRaw.class)); + } + + @Test void containingModelsKeepRawNullsButStillOmitOrdinaryNullFields() { + String raw = "{\"unknown\":null,\"number\":9007199254740993}"; + String wire = "{\"caption\":\"raw\",\"choice\":" + raw + ",\"choices\":[null],\"by_name\":{\"keep_key\":null}}"; + ChoiceEnvelope value = gson.fromJson(wire, ChoiceEnvelope.class); + assertInstanceOf(FiniteChoice.VariantRaw.class, value.getChoices().get(0)); + assertInstanceOf(FiniteChoice.VariantRaw.class, value.getByName().get("keep_key")); + assertEquals(JsonParser.parseString(wire), gson.toJsonTree(value)); + } + + @Test void writerNullPolicyIsRestoredAfterSuccessAndFailure() throws Exception { + FiniteChoice value = gson.fromJson("{\"unknown\":null}", FiniteChoice.class); + TypeAdapter adapter = gson.getAdapter(FiniteChoice.class); + StringWriter text = new StringWriter(); + JsonWriter writer = new JsonWriter(text); + writer.setSerializeNulls(false); + adapter.write(writer, value); + assertFalse(writer.getSerializeNulls()); + assertEquals("{\"unknown\":null}", text.toString()); + Writer broken = new Writer() { + public void write(char[] buffer, int offset, int length) throws IOException { throw new IOException("broken writer"); } + public void flush() {} + public void close() {} + }; + JsonWriter failing = new JsonWriter(broken); + failing.setSerializeNulls(false); + assertThrows(IOException.class, () -> adapter.write(failing, value)); + assertFalse(failing.getSerializeNulls()); + } +} diff --git a/tests/generated-clients/jvm/kotlin/cases/union-selection/gradle.lockfile b/tests/generated-clients/jvm/kotlin/cases/union-selection/gradle.lockfile new file mode 100644 index 000000000..10ef51b14 --- /dev/null +++ b/tests/generated-clients/jvm/kotlin/cases/union-selection/gradle.lockfile @@ -0,0 +1,25 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +# To regenerate this file, run: ./gradlew :cases:union-selection:dependencies --write-locks +com.google.code.gson:gson:2.11.0=testCompileClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.27.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okhttp3:okhttp:4.12.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okio:okio-jvm:3.6.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okio:okio:3.6.0=testCompileClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath +org.jetbrains.kotlin:kotlin-stdlib-common:2.2.21=testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10=testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10=testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:2.2.21=testCompileClasspath,testRuntimeClasspath +org.jetbrains:annotations:13.0=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.12.2=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.12.2=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.12.2=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.12.2=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.12.2=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.12.2=testRuntimeClasspath +org.junit.platform:junit-platform-launcher:1.12.2=testRuntimeClasspath +org.junit:junit-bom:5.12.2=testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +empty=testAnnotationProcessor diff --git a/tests/generated-clients/jvm/kotlin/cases/union-selection/src/test/java/UnionSelectionTest.java b/tests/generated-clients/jvm/kotlin/cases/union-selection/src/test/java/UnionSelectionTest.java new file mode 100644 index 000000000..46afe71b4 --- /dev/null +++ b/tests/generated-clients/jvm/kotlin/cases/union-selection/src/test/java/UnionSelectionTest.java @@ -0,0 +1,62 @@ +import com.example.sdk.models.*; +import com.google.gson.Gson; +import com.google.gson.JsonParser; +import java.util.List; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class UnionSelectionTest { + private final Gson gson = new Gson(); + private static final String CLIENT = "{\"name\":\"weather\",\"input_schema\":{\"type\":\"object\"}}"; + private static final String SEARCH = "{\"type\":\"web_search_20250305\",\"name\":\"web_search\",\"max_uses\":1}"; + private static final String NUMERIC = "{\"revision\":2,\"second_value\":\"two\"}"; + + private void roundTrip(String wire, Class type) { + T value = gson.fromJson(wire, type); + assertEquals(JsonParser.parseString(wire), gson.toJsonTree(value, type)); + } + + @Test void directToolMembersPreserveWire() { + roundTrip(CLIENT, ClientTool.class); + roundTrip(SEARCH, SearchTool.class); + } + + @Test void typedUnionRequestIsJsonSerializable() { + Request request = new Request(List.of( + new Tool.VariantClientTool(gson.fromJson(CLIENT, ClientTool.class)), + new Tool.VariantSearchTool(gson.fromJson(SEARCH, SearchTool.class)) + )); + assertEquals(JsonParser.parseString("{\"tools\":[" + CLIENT + "," + SEARCH + "]}"), gson.toJsonTree(request)); + } + + @Test void inboundToolRequestConstructsUnionVariants() { + String wire = "{\"tools\":[" + CLIENT + "," + SEARCH + "]}"; + Request request = gson.fromJson(wire, Request.class); + assertInstanceOf(Tool.VariantClientTool.class, request.getTools().get(0)); + assertInstanceOf(Tool.VariantSearchTool.class, request.getTools().get(1)); + assertEquals(JsonParser.parseString(wire), gson.toJsonTree(request)); + } + + @Test void directNumericMemberPreservesPayload() { roundTrip(NUMERIC, NumericTwo.class); } + + @Test void disjointRequiredFieldControlSelectsSecondMember() { + RequiredChoice value = gson.fromJson(NUMERIC, RequiredChoice.class); + assertInstanceOf(RequiredChoice.VariantRequiredTwo.class, value); + roundTrip(NUMERIC, RequiredChoice.class); + } + + @Test void disjointEnumConstraintSelectsSecondMember() { + Choice value = gson.fromJson(NUMERIC, Choice.class); + assertInstanceOf(Choice.VariantNumericTwo.class, value); + roundTrip(NUMERIC, Choice.class); + } + + @Test void nestedUnionSelectionPreservesScalarArrayAndMapPayloads() { + String wire = "{\"choice\":" + NUMERIC + ",\"choice_list\":[" + NUMERIC + "],\"choice_map\":{\"keep_key\":" + NUMERIC + "}}"; + Envelope value = gson.fromJson(wire, Envelope.class); + assertInstanceOf(Choice.VariantNumericTwo.class, value.getChoice()); + assertInstanceOf(Choice.VariantNumericTwo.class, value.getChoiceList().get(0)); + assertInstanceOf(Choice.VariantNumericTwo.class, value.getChoiceMap().get("keep_key")); + roundTrip(wire, Envelope.class); + } +} diff --git a/tests/generated-clients/jvm/kotlin/cases/union-strict/build.gradle b/tests/generated-clients/jvm/kotlin/cases/union-strict/build.gradle new file mode 100644 index 000000000..e76eca30a --- /dev/null +++ b/tests/generated-clients/jvm/kotlin/cases/union-strict/build.gradle @@ -0,0 +1,2 @@ +sourceSets.test.java.srcDir("../../support/union-policies") +sourceSets.test.java.srcDir("../../support/http") diff --git a/tests/generated-clients/jvm/kotlin/cases/union-strict/gradle.lockfile b/tests/generated-clients/jvm/kotlin/cases/union-strict/gradle.lockfile new file mode 100644 index 000000000..fb055b68a --- /dev/null +++ b/tests/generated-clients/jvm/kotlin/cases/union-strict/gradle.lockfile @@ -0,0 +1,25 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +# To regenerate this file, run: ./gradlew :cases:union-strict:dependencies --write-locks +com.google.code.gson:gson:2.11.0=testCompileClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.27.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okhttp3:okhttp:4.12.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okio:okio-jvm:3.6.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okio:okio:3.6.0=testCompileClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath +org.jetbrains.kotlin:kotlin-stdlib-common:2.2.21=testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10=testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10=testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:2.2.21=testCompileClasspath,testRuntimeClasspath +org.jetbrains:annotations:13.0=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.12.2=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.12.2=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.12.2=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.12.2=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.12.2=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.12.2=testRuntimeClasspath +org.junit.platform:junit-platform-launcher:1.12.2=testRuntimeClasspath +org.junit:junit-bom:5.12.2=testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +empty=testAnnotationProcessor diff --git a/tests/generated-clients/jvm/kotlin/cases/union-strict/src/test/java/UnionPolicyTest.java b/tests/generated-clients/jvm/kotlin/cases/union-strict/src/test/java/UnionPolicyTest.java new file mode 100644 index 000000000..95e13f0d3 --- /dev/null +++ b/tests/generated-clients/jvm/kotlin/cases/union-strict/src/test/java/UnionPolicyTest.java @@ -0,0 +1,3 @@ +class UnionPolicyTest extends TypedUnionPolicyCases { + @Override protected boolean strict() { return true; } +} diff --git a/tests/generated-clients/jvm/kotlin/fixtures.txt b/tests/generated-clients/jvm/kotlin/fixtures.txt new file mode 100644 index 000000000..160504ba4 --- /dev/null +++ b/tests/generated-clients/jvm/kotlin/fixtures.txt @@ -0,0 +1,7 @@ +# Consumer project name and input under tests/fixtures/. +union-selection valid/union-selection.yaml +union-first-match valid/java-union-policies.yaml firstMatch +union-strict valid/java-union-policies.yaml strict +union-raw valid/java-union-policies.yaml raw +union-helper-types valid/java-union-helper-types.yaml firstMatch +union-helper-types-strict valid/java-union-helper-types.yaml strict diff --git a/tests/generated-clients/jvm/kotlin/support/http/UnionHttpTestSupport.java b/tests/generated-clients/jvm/kotlin/support/http/UnionHttpTestSupport.java new file mode 100644 index 000000000..22013c432 --- /dev/null +++ b/tests/generated-clients/jvm/kotlin/support/http/UnionHttpTestSupport.java @@ -0,0 +1,18 @@ +import com.example.sdk.apis.SelectionApi; +import com.example.sdk.runtime.ApiClient; +import java.util.Map; +import okhttp3.*; +import static org.junit.jupiter.api.Assertions.*; + +final class UnionHttpTestSupport { + static SelectionApi api(int status, String wire) { + OkHttpClient transport = new OkHttpClient.Builder().addInterceptor(chain -> { + assertEquals("/choice", chain.request().url().encodedPath()); + return new Response.Builder().request(chain.request()).protocol(Protocol.HTTP_1_1) + .code(status).message(status == 200 ? "OK" : "Bad Request") + .header("Content-Type", "application/json") + .body(ResponseBody.create(wire, MediaType.get("application/json"))).build(); + }).build(); + return new SelectionApi(new ApiClient("https://union.test", transport, null, Map.of())); + } +} diff --git a/tests/generated-clients/jvm/kotlin/support/union-policies/TypedUnionPolicyCases.java b/tests/generated-clients/jvm/kotlin/support/union-policies/TypedUnionPolicyCases.java new file mode 100644 index 000000000..a9f0ac68e --- /dev/null +++ b/tests/generated-clients/jvm/kotlin/support/union-policies/TypedUnionPolicyCases.java @@ -0,0 +1,221 @@ +import com.example.sdk.models.*; +import com.example.sdk.apis.*; +import com.google.gson.*; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +abstract class TypedUnionPolicyCases { + protected abstract boolean strict(); + private final Gson gson = new Gson(); + private static final String FINITE = "{\"revision\":2,\"values\":[2,2.5],\"flags\":{\"keep_key\":true},\"second_value\":\"two\"}"; + + private T read(String wire, Class type) { return gson.fromJson(wire, type); } + + private void roundTrip(String wire, Class type) { + assertEquals(JsonParser.parseString(wire), gson.toJsonTree(read(wire, type), type)); + } + + @Test void finiteScalarArrayAndMapFactsSelectTheSecondMember() { + assertInstanceOf(FiniteChoice.VariantFiniteTwo.class, read(FINITE, FiniteChoice.class)); + roundTrip(FINITE, FiniteChoice.class); + } + + @Test void unmatchedAndCoercedScalarKindsAreRejected() { + for (String wire : new String[] { + FINITE.replace("\"revision\":2", "\"revision\":3"), + FINITE.replace("\"revision\":2", "\"revision\":\"2\""), + FINITE.replace("[2,2.5]", "[1.5]"), FINITE.replace("true", "false"), + "{\"revision\":2}", "42", "[]" + }) { assertThrows(JsonParseException.class, () -> read(wire, FiniteChoice.class), wire); } + } + + @Test void containerFactsDistinguishMembersWithoutAScalarTag() { + String wire = "{\"values\":[2,2],\"second_value\":\"two\"}"; + assertInstanceOf(ContainerChoice.VariantContainerTwo.class, read(wire, ContainerChoice.class)); + roundTrip(wire, ContainerChoice.class); + assertThrows(JsonParseException.class, () -> read("{\"values\":[3]}", ContainerChoice.class)); + } + + @Test void largeIntegersRemainDisjointAboveDoublePrecision() { + String wire = "{\"revision\":9007199254740993,\"second_value\":\"two\"}"; + LargeChoice.VariantLargeTwo value = assertInstanceOf(LargeChoice.VariantLargeTwo.class, read(wire, LargeChoice.class)); + assertEquals(9007199254740993L, value.getValue0().getRevision()); + roundTrip(wire, LargeChoice.class); + } + + @Test void extremeZeroExponentsPreserveOrdinaryDoubleBehavior() { + for (String wire : new String[] {"0e10000", "0e-10000", "-0.000e10000", "0e2147483648", "0e-2147483649"}) { + double ordinary = JsonParser.parseString(wire).getAsDouble(); + NumericZero.VariantNumber selected = assertInstanceOf(NumericZero.VariantNumber.class, read(wire, NumericZero.class)); + assertEquals(Double.doubleToRawLongBits(ordinary), Double.doubleToRawLongBits(selected.getValue0()), wire); + } + } + + @Test void fallbackNumbersStayExactAndFiniteNumbersDoNotRoundIntoMatches() { + for (String wire : new String[] {"1e10000", "1e-10000", "1e2147483648", "1e-2147483649", "12500000000000000001e-19"}) { + NumericFinite.VariantAny selected = assertInstanceOf(NumericFinite.VariantAny.class, read(wire, NumericFinite.class)); + assertEquals(wire, selected.getValue0().getAsString()); + } + for (String wire : new String[] {"1.25", "125e-2", "0.0125000e2", "12500000000000000000e-19"}) { + assertInstanceOf(NumericFinite.VariantNumber.class, read(wire, NumericFinite.class)); + } + } + + @Test void integerSelectionRetainsTheExistingKotlinScalarDelegate() { + for (String wire : new String[] {"0e10000", "1e2147483648"}) { + RuntimeException ordinary = assertThrows(RuntimeException.class, () -> JsonParser.parseString(wire).getAsInt()); + RuntimeException selected = assertThrows(RuntimeException.class, () -> read(wire, IntegerFallback.class)); + assertEquals(ordinary.getClass(), selected.getClass()); + assertEquals(ordinary.getMessage(), selected.getMessage()); + } + for (String wire : new String[] {"1e-10000", "1e-2147483649"}) { + assertInstanceOf(IntegerFallback.VariantAny.class, read(wire, IntegerFallback.class)); + } + } + + @Test void binaryMatchingDoesNotMaskNativeByteErrors() { + for (String wire : new String[] {"[1e10000]", "[-1e10000]", "[1e-10000]", "[1e2147483648]", "[256]"}) { + JsonParseException error = assertThrows(JsonParseException.class, () -> read(wire, BinaryChoice.class)); + assertTrue(error.getMessage().contains("unknown BinaryChoice union value")); + } + String wire = "[0e10000]"; + JsonParseException ordinary = assertThrows(JsonParseException.class, () -> gson.fromJson(JsonParser.parseString(wire), byte[].class)); + JsonParseException selected = assertThrows(JsonParseException.class, () -> read(wire, BinaryChoice.class)); + assertEquals(ordinary.getMessage(), selected.getMessage()); + assertInstanceOf(BinaryChoice.VariantBinary.class, read("[-128,0,127]", BinaryChoice.class)); + roundTrip("[-128,0,127]", BinaryChoice.class); + assertInstanceOf(BinaryChoice.VariantString.class, read("\"text\"", BinaryChoice.class)); + } + + @Test void scalarFiniteFactsDoNotWidenTheirKinds() { + assertInstanceOf(ScalarChoice.VariantInteger.class, read("1", ScalarChoice.class)); + assertInstanceOf(ScalarChoice.VariantNumber.class, read("2.5", ScalarChoice.class)); + assertInstanceOf(ScalarChoice.VariantBoolean.class, read("true", ScalarChoice.class)); + for (String wire : new String[] {"2", "false", "\"1\""}) { + assertThrows(JsonParseException.class, () -> read(wire, ScalarChoice.class)); + } + } + + @Test void finiteNullPermissionsStayAtTheirAuthoredBoundary() { + assertInstanceOf(NullableFields.VariantIncludedNull.class, read("{\"revision\":null,\"second_value\":\"two\"}", NullableFields.class)); + assertInstanceOf(NullableFields.VariantExcludedNull.class, read("{\"revision\":1}", NullableFields.class)); + } + + @Test void typedRootNullRetainsTheExistingKotlinApi() { + assertNull(read("null", FiniteChoice.class)); + assertSame(NullableChoice.VariantNull.INSTANCE, read("null", NullableChoice.class)); + roundTrip("null", NullableChoice.class); + } + + @Test void concreteMembersPrecedeOpaqueFallbacks() { + assertInstanceOf(OpaqueFirst.VariantFiniteTwo.class, read(FINITE, OpaqueFirst.class)); + roundTrip(FINITE, OpaqueFirst.class); + assertInstanceOf(OpaqueFirst.VariantAny.class, read("42", OpaqueFirst.class)); + roundTrip("42", OpaqueFirst.class); + } + + @Test void ambiguityIsResolvedBeforeDelegation() { + String wire = "{\"wire_name\":\"detail\",\"first_value\":\"one\"}"; + if (strict()) { + JsonParseException error = assertThrows(JsonParseException.class, () -> read(wire, Ambiguous.class)); + assertTrue(error.getMessage().contains("ambiguous untagged union Ambiguous")); + } else { + assertInstanceOf(Ambiguous.VariantAlpha.class, read(wire, Ambiguous.class)); + roundTrip(wire, Ambiguous.class); + } + } + + @Test void transparentAliasesKeepTheirOwnConversionBoundary() { + String wire = "7"; + AliasChoice.VariantNumberAlias value = assertInstanceOf(AliasChoice.VariantNumberAlias.class, read(wire, AliasChoice.class)); + assertEquals(7, value.getValue0()); + roundTrip(wire, AliasChoice.class); + } + + @Test void nestedStrictErrorsDoNotRetryOpaqueFallback() { + String wire = "{\"wire_name\":\"detail\"}"; + if (strict()) { + JsonParseException error = assertThrows(JsonParseException.class, () -> read(wire, OpenNested.class)); + assertTrue(error.getMessage().contains("ambiguous untagged union Ambiguous")); + } else { + assertInstanceOf(OpenNested.VariantAmbiguousAlias.class, read(wire, OpenNested.class)); + roundTrip(wire, OpenNested.class); + } + } + + @Test void intersectionMembersKeepTheirWrappedWireRepresentation() { + String wire = "{\"leftPart\":{\"left_value\":\"left\"},\"rightPart\":{\"right_value\":2}}"; + assertInstanceOf(WrappedChoice.VariantWrappedPair.class, read(wire, WrappedChoice.class)); + roundTrip(wire, WrappedChoice.class); + } + + @Test void recursivePredicatesReuseSuccessfulAndFailedChecks() { + String valid = "{\"revision\":2,\"children\":[],\"second_value\":\"leaf\"}"; + String invalid = "{\"revision\":3,\"children\":[]}"; + for (int i = 0; i < 24; i++) { + valid = "{\"children\":[" + valid + "],\"revision\":2,\"second_value\":\"branch\"}"; + invalid = "{\"children\":[" + invalid + "],\"revision\":2}"; + } + assertInstanceOf(Tree.VariantBranchTwo.class, read(valid, Tree.class)); + roundTrip(valid, Tree.class); + String rejected = invalid; + assertThrows(JsonParseException.class, () -> read(rejected, Tree.class)); + } + + @Test void sourceSchemaIdentityIsNotItsPascalCaseClassName() { + assertInstanceOf(RecursiveChoice.VariantString.class, read("\"safe\"", RecursiveChoice.class)); + roundTrip("\"safe\"", RecursiveChoice.class); + } + + @Test void nativeIntegerOverflowDoesNotBecomeASelectionValidator() { + String wire = "{\"identifier\":2147483648,\"second_value\":\"two\"}"; + int ordinary = gson.fromJson(JsonParser.parseString(wire), Narrow.class).getIdentifier(); + if (strict()) { + assertThrows(JsonParseException.class, () -> read(wire, FormatChoice.class)); + } else { + FormatChoice.VariantNarrow selected = assertInstanceOf(FormatChoice.VariantNarrow.class, read(wire, FormatChoice.class)); + assertEquals(ordinary, selected.getValue0().getIdentifier()); + } + FormatFallback.VariantNarrow selected = assertInstanceOf(FormatFallback.VariantNarrow.class, read(wire, FormatFallback.class)); + assertEquals(ordinary, selected.getValue0().getIdentifier()); + } + + @Test void containingFieldsListsAndMapsUseTheSameSelection() { + String wire = "{\"caption\":\"envelope\",\"choice\":" + FINITE + ",\"choices\":[" + FINITE + "],\"by_name\":{\"keep_key\":" + FINITE + "}}"; + ChoiceEnvelope value = read(wire, ChoiceEnvelope.class); + assertInstanceOf(FiniteChoice.VariantFiniteTwo.class, value.getChoice()); + assertInstanceOf(FiniteChoice.VariantFiniteTwo.class, value.getChoices().get(0)); + assertInstanceOf(FiniteChoice.VariantFiniteTwo.class, value.getByName().get("keep_key")); + roundTrip(wire, ChoiceEnvelope.class); + } + + @Test void successfulHttpResponsesUseTheTypedMember() { + GetChoiceResponse response = UnionHttpTestSupport.api(200, FINITE).getChoice(); + assertEquals(200, response.getStatusCode()); + assertInstanceOf(FiniteChoice.VariantFiniteTwo.class, response.getStatus200()); + assertEquals(JsonParser.parseString(FINITE), gson.toJsonTree(response.getStatus200(), FiniteChoice.class)); + } + + @Test void typedHttpErrorsKeepTheirPayloadAndRawBytes() { + GetChoiceException error = assertThrows(GetChoiceException.class, () -> UnionHttpTestSupport.api(400, FINITE).getChoice()); + GetChoiceBadRequest detail = assertInstanceOf(GetChoiceBadRequest.class, error.getDetail()); + assertInstanceOf(FiniteChoice.VariantFiniteTwo.class, detail.body()); + assertArrayEquals(FINITE.getBytes(StandardCharsets.UTF_8), error.rawBody()); + assertArrayEquals(error.rawBody(), detail.rawBody()); + } + + @Test void unmatchedHttpSuccessPropagatesTheSelectionError() { + String wire = FINITE.replace("\"revision\":2", "\"revision\":3"); + assertThrows(JsonParseException.class, () -> UnionHttpTestSupport.api(200, wire).getChoice()); + } + + @Test void unmatchedHttpErrorsStayLazyWithoutLosingDiagnostics() { + String wire = FINITE.replace("\"revision\":2", "\"revision\":3"); + GetChoiceException error = assertThrows(GetChoiceException.class, () -> UnionHttpTestSupport.api(400, wire).getChoice()); + GetChoiceBadRequest detail = assertInstanceOf(GetChoiceBadRequest.class, error.getDetail()); + assertThrows(JsonParseException.class, detail::body); + assertThrows(JsonParseException.class, detail::body); + assertArrayEquals(wire.getBytes(StandardCharsets.UTF_8), error.rawBody()); + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/comprehensive-schemas/models/AnyOfTypes.kt.golden b/tests/golden/kotlin/kotlin-okhttp/comprehensive-schemas/models/AnyOfTypes.kt.golden index dca34862f..2ffad715a 100644 --- a/tests/golden/kotlin/kotlin-okhttp/comprehensive-schemas/models/AnyOfTypes.kt.golden +++ b/tests/golden/kotlin/kotlin-okhttp/comprehensive-schemas/models/AnyOfTypes.kt.golden @@ -28,17 +28,28 @@ sealed class AnyOfTypes private constructor() { data class VariantNumber(val value0: Double) : AnyOfTypes() } -private class AnyOfTypesJsonAdapter: TypeAdapter() { - private val gson = Gson() +private class AnyOfTypesJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() - override fun read (reader: JsonReader): AnyOfTypes? { + override fun read(reader: JsonReader): AnyOfTypes? { val element = JsonParser.parseReader(reader) - return when { - element.isJsonNull -> null - element.isJsonPrimitive && element.asJsonPrimitive.isString -> AnyOfTypes.VariantString(element.asString) - element.isJsonPrimitive && element.asJsonPrimitive.isNumber -> AnyOfTypes.VariantNumber(element.asDouble) - else -> throw JsonParseException("unknown AnyOfTypes union value") + if (element.isJsonNull) { + return null } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches1(element)) { + selected = 1 + } + if (selected == 0) { + return AnyOfTypes.VariantString(element.asString) + } + if (selected == 1) { + return AnyOfTypes.VariantNumber(element.asDouble) + } + throw JsonParseException("unknown AnyOfTypes union value") } override fun write(writer: JsonWriter, value: AnyOfTypes?) { @@ -48,11 +59,19 @@ private class AnyOfTypesJsonAdapter: TypeAdapter() { } when (value) { is AnyOfTypes.VariantString -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is AnyOfTypes.VariantNumber -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } } } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber + } } diff --git a/tests/golden/kotlin/kotlin-okhttp/comprehensive-schemas/models/OneOfObjects.kt.golden b/tests/golden/kotlin/kotlin-okhttp/comprehensive-schemas/models/OneOfObjects.kt.golden index ea049f53c..848325e21 100644 --- a/tests/golden/kotlin/kotlin-okhttp/comprehensive-schemas/models/OneOfObjects.kt.golden +++ b/tests/golden/kotlin/kotlin-okhttp/comprehensive-schemas/models/OneOfObjects.kt.golden @@ -28,17 +28,28 @@ sealed class OneOfObjects private constructor() { data class VariantNestedObject(val value0: NestedObject) : OneOfObjects() } -private class OneOfObjectsJsonAdapter: TypeAdapter() { - private val gson = Gson() +private class OneOfObjectsJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() - override fun read (reader: JsonReader): OneOfObjects? { + override fun read(reader: JsonReader): OneOfObjects? { val element = JsonParser.parseReader(reader) - return when { - element.isJsonNull -> null - ((element.isJsonObject && element.asJsonObject.has("id")) && element.asJsonObject.has("name")) -> OneOfObjects.VariantBasicObject(gson.fromJson(element, object: TypeToken() {}.type)) - element.isJsonObject -> OneOfObjects.VariantNestedObject(gson.fromJson(element, object: TypeToken() {}.type)) - else -> throw JsonParseException("unknown OneOfObjects union value") + if (element.isJsonNull) { + return null } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches4(element)) { + selected = 1 + } + if (selected == 0) { + return OneOfObjects.VariantBasicObject(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return OneOfObjects.VariantNestedObject(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown OneOfObjects union value") } override fun write(writer: JsonWriter, value: OneOfObjects?) { @@ -48,11 +59,122 @@ private class OneOfObjectsJsonAdapter: TypeAdapter() { } when (value) { is OneOfObjects.VariantBasicObject -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is OneOfObjects.VariantNestedObject -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } } } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("active") && !matches1(jsonObject.get("active"))) { + return false + } + if (!jsonObject.has("id") || !matches2(jsonObject.get("id"))) { + return false + } + if (!jsonObject.has("name") || !matches3(jsonObject.get("name"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isBoolean + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("metadata") && !matches5(jsonObject.get("metadata"))) { + return false + } + if (jsonObject.has("user") && !matches6(jsonObject.get("user"))) { + return false + } + return true + } + + private fun matches5(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("created") && !matches3(jsonObject.get("created"))) { + return false + } + return true + } + + private fun matches6(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("id") || !matches2(jsonObject.get("id"))) { + return false + } + if (jsonObject.has("name") && !matches3(jsonObject.get("name"))) { + return false + } + return true + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } } diff --git a/tests/golden/kotlin/kotlin-okhttp/comprehensive-schemas/models/OneOfTypes.kt.golden b/tests/golden/kotlin/kotlin-okhttp/comprehensive-schemas/models/OneOfTypes.kt.golden index 51ffe40c0..0281d2f3d 100644 --- a/tests/golden/kotlin/kotlin-okhttp/comprehensive-schemas/models/OneOfTypes.kt.golden +++ b/tests/golden/kotlin/kotlin-okhttp/comprehensive-schemas/models/OneOfTypes.kt.golden @@ -30,18 +30,34 @@ sealed class OneOfTypes private constructor() { data class VariantBoolean(val value0: Boolean) : OneOfTypes() } -private class OneOfTypesJsonAdapter: TypeAdapter() { - private val gson = Gson() +private class OneOfTypesJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() - override fun read (reader: JsonReader): OneOfTypes? { + override fun read(reader: JsonReader): OneOfTypes? { val element = JsonParser.parseReader(reader) - return when { - element.isJsonNull -> null - element.isJsonPrimitive && element.asJsonPrimitive.isString -> OneOfTypes.VariantString(element.asString) - element.isJsonPrimitive && element.asJsonPrimitive.isNumber -> OneOfTypes.VariantNumber(element.asDouble) - element.isJsonPrimitive && element.asJsonPrimitive.isBoolean -> OneOfTypes.VariantBoolean(element.asBoolean) - else -> throw JsonParseException("unknown OneOfTypes union value") + if (element.isJsonNull) { + return null } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches1(element)) { + selected = 1 + } + if (selected < 0 && matches2(element)) { + selected = 2 + } + if (selected == 0) { + return OneOfTypes.VariantString(element.asString) + } + if (selected == 1) { + return OneOfTypes.VariantNumber(element.asDouble) + } + if (selected == 2) { + return OneOfTypes.VariantBoolean(element.asBoolean) + } + throw JsonParseException("unknown OneOfTypes union value") } override fun write(writer: JsonWriter, value: OneOfTypes?) { @@ -51,14 +67,26 @@ private class OneOfTypesJsonAdapter: TypeAdapter() { } when (value) { is OneOfTypes.VariantString -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is OneOfTypes.VariantNumber -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is OneOfTypes.VariantBoolean -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } } } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isBoolean + } } diff --git a/tests/golden/kotlin/kotlin-okhttp/comprehensive-schemas/models/OneOfWithNull.kt.golden b/tests/golden/kotlin/kotlin-okhttp/comprehensive-schemas/models/OneOfWithNull.kt.golden index d04150e45..8ba2fe7b4 100644 --- a/tests/golden/kotlin/kotlin-okhttp/comprehensive-schemas/models/OneOfWithNull.kt.golden +++ b/tests/golden/kotlin/kotlin-okhttp/comprehensive-schemas/models/OneOfWithNull.kt.golden @@ -33,17 +33,28 @@ sealed class OneOfWithNull private constructor() { data object VariantNull : OneOfWithNull() } -private class OneOfWithNullJsonAdapter: TypeAdapter() { - private val gson = Gson() +private class OneOfWithNullJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() - override fun read (reader: JsonReader): OneOfWithNull? { + override fun read(reader: JsonReader): OneOfWithNull? { val element = JsonParser.parseReader(reader) - return when { - element.isJsonNull -> OneOfWithNull.VariantNull - element.isJsonPrimitive && element.asJsonPrimitive.isString -> OneOfWithNull.VariantString(element.asString) - ((element.isJsonObject && element.asJsonObject.has("id")) && element.asJsonObject.has("name")) -> OneOfWithNull.VariantBasicObject(gson.fromJson(element, object: TypeToken() {}.type)) - else -> throw JsonParseException("unknown OneOfWithNull union value") + if (element.isJsonNull) { + return OneOfWithNull.VariantNull } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches1(element)) { + selected = 1 + } + if (selected == 0) { + return OneOfWithNull.VariantString(element.asString) + } + if (selected == 1) { + return OneOfWithNull.VariantBasicObject(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown OneOfWithNull union value") } override fun write(writer: JsonWriter, value: OneOfWithNull?) { @@ -53,14 +64,86 @@ private class OneOfWithNullJsonAdapter: TypeAdapter() { } when (value) { is OneOfWithNull.VariantString -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is OneOfWithNull.VariantBasicObject -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is OneOfWithNull.VariantNull -> { writer.nullValue() } } } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("active") && !matches2(jsonObject.get("active"))) { + return false + } + if (!jsonObject.has("id") || !matches3(jsonObject.get("id"))) { + return false + } + if (!jsonObject.has("name") || !matches0(jsonObject.get("name"))) { + return false + } + return true + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isBoolean + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } } diff --git a/tests/golden/kotlin/kotlin-okhttp/enum-repr/models/MixedEnum.kt.golden b/tests/golden/kotlin/kotlin-okhttp/enum-repr/models/MixedEnum.kt.golden index 0bb23b763..b3e3b5d7b 100644 --- a/tests/golden/kotlin/kotlin-okhttp/enum-repr/models/MixedEnum.kt.golden +++ b/tests/golden/kotlin/kotlin-okhttp/enum-repr/models/MixedEnum.kt.golden @@ -35,19 +35,40 @@ sealed class MixedEnum private constructor() { data class VariantMixedEnumMember4(val value0: MixedEnumMember4) : MixedEnum() } -private class MixedEnumJsonAdapter: TypeAdapter() { - private val gson = Gson() +private class MixedEnumJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() - override fun read (reader: JsonReader): MixedEnum? { + override fun read(reader: JsonReader): MixedEnum? { val element = JsonParser.parseReader(reader) - return when { - element.isJsonNull -> null - element.isJsonPrimitive && element.asJsonPrimitive.isString && element.asString == "SimpleA" -> MixedEnum.VariantSimpleA(element.asString) - (element.isJsonObject && element.asJsonObject.has("VariantA")) -> MixedEnum.VariantMixedEnumMember2(gson.fromJson(element, object: TypeToken() {}.type)) - element.isJsonPrimitive && element.asJsonPrimitive.isString && element.asString == "SimpleB" -> MixedEnum.VariantSimpleB(element.asString) - (element.isJsonObject && element.asJsonObject.has("VariantB")) -> MixedEnum.VariantMixedEnumMember4(gson.fromJson(element, object: TypeToken() {}.type)) - else -> throw JsonParseException("unknown MixedEnum union value") + if (element.isJsonNull) { + return null } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches1(element)) { + selected = 1 + } + if (selected < 0 && matches5(element)) { + selected = 2 + } + if (selected < 0 && matches6(element)) { + selected = 3 + } + if (selected == 0) { + return MixedEnum.VariantSimpleA(element.asString) + } + if (selected == 1) { + return MixedEnum.VariantMixedEnumMember2(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 2) { + return MixedEnum.VariantSimpleB(element.asString) + } + if (selected == 3) { + return MixedEnum.VariantMixedEnumMember4(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown MixedEnum union value") } override fun write(writer: JsonWriter, value: MixedEnum?) { @@ -57,17 +78,134 @@ private class MixedEnumJsonAdapter: TypeAdapter() { } when (value) { is MixedEnum.VariantSimpleA -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is MixedEnum.VariantMixedEnumMember2 -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is MixedEnum.VariantSimpleB -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is MixedEnum.VariantMixedEnumMember4 -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } } } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString && candidate.asString == "SimpleA" + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("VariantA") || !matches2(jsonObject.get("VariantA"))) { + return false + } + return true + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("field1") || !matches3(jsonObject.get("field1"))) { + return false + } + if (!jsonObject.has("field2") || !matches4(jsonObject.get("field2"))) { + return false + } + return true + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches5(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString && candidate.asString == "SimpleB" + } + + private fun matches6(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("VariantB") || !matches7(jsonObject.get("VariantB"))) { + return false + } + return true + } + + private fun matches7(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("field3") || !matches8(jsonObject.get("field3"))) { + return false + } + if (!jsonObject.has("field4") || !matches9(jsonObject.get("field4"))) { + return false + } + return true + } + + private fun matches8(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isBoolean + } + + private fun matches9(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } } diff --git a/tests/golden/kotlin/kotlin-okhttp/enum-repr/models/UntaggedEnum.kt.golden b/tests/golden/kotlin/kotlin-okhttp/enum-repr/models/UntaggedEnum.kt.golden index 792b3d0cb..ffef8f953 100644 --- a/tests/golden/kotlin/kotlin-okhttp/enum-repr/models/UntaggedEnum.kt.golden +++ b/tests/golden/kotlin/kotlin-okhttp/enum-repr/models/UntaggedEnum.kt.golden @@ -31,17 +31,28 @@ sealed class UntaggedEnum private constructor() { data class VariantVariantB(val value0: VariantB) : UntaggedEnum() } -private class UntaggedEnumJsonAdapter: TypeAdapter() { - private val gson = Gson() +private class UntaggedEnumJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() - override fun read (reader: JsonReader): UntaggedEnum? { + override fun read(reader: JsonReader): UntaggedEnum? { val element = JsonParser.parseReader(reader) - return when { - element.isJsonNull -> null - ((element.isJsonObject && element.asJsonObject.has("field1")) && element.asJsonObject.has("field2")) -> UntaggedEnum.VariantVariantA(gson.fromJson(element, object: TypeToken() {}.type)) - ((element.isJsonObject && element.asJsonObject.has("field3")) && element.asJsonObject.has("field4")) -> UntaggedEnum.VariantVariantB(gson.fromJson(element, object: TypeToken() {}.type)) - else -> throw JsonParseException("unknown UntaggedEnum union value") + if (element.isJsonNull) { + return null } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches3(element)) { + selected = 1 + } + if (selected == 0) { + return UntaggedEnum.VariantVariantA(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return UntaggedEnum.VariantVariantB(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown UntaggedEnum union value") } override fun write(writer: JsonWriter, value: UntaggedEnum?) { @@ -51,11 +62,98 @@ private class UntaggedEnumJsonAdapter: TypeAdapter() { } when (value) { is UntaggedEnum.VariantVariantA -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is UntaggedEnum.VariantVariantB -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } } } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("field1") || !matches1(jsonObject.get("field1"))) { + return false + } + if (!jsonObject.has("field2") || !matches2(jsonObject.get("field2"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("field3") || !matches4(jsonObject.get("field3"))) { + return false + } + if (!jsonObject.has("field4") || !matches5(jsonObject.get("field4"))) { + return false + } + return true + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isBoolean + } + + private fun matches5(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } } diff --git a/tests/golden/kotlin/kotlin-okhttp/type-aliases-closed-sum-payload-shapes/models/Choice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/type-aliases-closed-sum-payload-shapes/models/Choice.kt.golden index 6f979aaaa..eaabc641f 100644 --- a/tests/golden/kotlin/kotlin-okhttp/type-aliases-closed-sum-payload-shapes/models/Choice.kt.golden +++ b/tests/golden/kotlin/kotlin-okhttp/type-aliases-closed-sum-payload-shapes/models/Choice.kt.golden @@ -32,19 +32,35 @@ sealed class Choice private constructor() { data object VariantNull : Choice() } -private class ChoiceJsonAdapter: TypeAdapter() { - private val gson = Gson() +private class ChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() - override fun read (reader: JsonReader): Choice? { + override fun read(reader: JsonReader): Choice? { val element = JsonParser.parseReader(reader) - return when { - element.isJsonNull -> Choice.VariantNull - element.isJsonPrimitive && element.asJsonPrimitive.isString -> Choice.VariantString(element.asString) - element.isJsonArray -> Choice.VariantArray(gson.fromJson(element, object: TypeToken>() {}.type)) - element.isJsonObject -> Choice.VariantMap(gson.fromJson(element, object: TypeToken>() {}.type)) + } + if (selected == 2) { + return Choice.VariantMap(gson.fromJson(element, object: TypeToken>() {}.type)) - else -> throw JsonParseException("unknown Choice union value") } + throw JsonParseException("unknown Choice union value") } override fun write(writer: JsonWriter, value: Choice?) { @@ -54,17 +70,96 @@ private class ChoiceJsonAdapter: TypeAdapter() { } when (value) { is Choice.VariantString -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is Choice.VariantArray -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken>() {}.type, writer) } is Choice.VariantMap -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken>() {}.type, writer) } is Choice.VariantNull -> { writer.nullValue() } } } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonArray) { + return false + } + for (item in candidate.asJsonArray) { + if (!matches2(item)) { + return false + } + } + return true + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + for (item in candidate.asJsonObject.asMap().values) { + if (!matches4(item)) { + return false + } + } + return true + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isBoolean + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } } diff --git a/tests/golden/kotlin/kotlin-okhttp/type-aliases-complex-union/models/Foo.kt.golden b/tests/golden/kotlin/kotlin-okhttp/type-aliases-complex-union/models/Foo.kt.golden index cc6bb4366..9441a0d25 100644 --- a/tests/golden/kotlin/kotlin-okhttp/type-aliases-complex-union/models/Foo.kt.golden +++ b/tests/golden/kotlin/kotlin-okhttp/type-aliases-complex-union/models/Foo.kt.golden @@ -27,18 +27,34 @@ sealed class Foo private constructor() { data class VariantQux(val value0: Qux) : Foo() } -private class FooJsonAdapter: TypeAdapter() { - private val gson = Gson() +private class FooJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() - override fun read (reader: JsonReader): Foo? { + override fun read(reader: JsonReader): Foo? { val element = JsonParser.parseReader(reader) - return when { - element.isJsonNull -> null - (((element.isJsonObject && element.asJsonObject.has("bucket")) && element.asJsonObject.has("endpoint")) && element.asJsonObject.has("type")) -> Foo.VariantBar(gson.fromJson(element, object: TypeToken() {}.type)) - ((element.isJsonObject && element.asJsonObject.has("path")) && element.asJsonObject.has("type")) -> Foo.VariantBaz(gson.fromJson(element, object: TypeToken() {}.type)) - (((element.isJsonObject && element.asJsonObject.has("namespace")) && element.asJsonObject.has("type")) && element.asJsonObject.has("url")) -> Foo.VariantQux(gson.fromJson(element, object: TypeToken() {}.type)) - else -> throw JsonParseException("unknown Foo union value") + if (element.isJsonNull) { + return null } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches3(element)) { + selected = 1 + } + if (selected < 0 && matches5(element)) { + selected = 2 + } + if (selected == 0) { + return Foo.VariantBar(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return Foo.VariantBaz(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 2) { + return Foo.VariantQux(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown Foo union value") } override fun write(writer: JsonWriter, value: Foo?) { @@ -48,14 +64,78 @@ private class FooJsonAdapter: TypeAdapter() { } when (value) { is Foo.VariantBar -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is Foo.VariantBaz -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is Foo.VariantQux -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } } } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("bucket") || !matches1(jsonObject.get("bucket"))) { + return false + } + if (!jsonObject.has("endpoint") || !matches1(jsonObject.get("endpoint"))) { + return false + } + if (!jsonObject.has("type") || !matches2(jsonObject.get("type"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString && candidate.asString == "bar" + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("path") || !matches1(jsonObject.get("path"))) { + return false + } + if (!jsonObject.has("type") || !matches4(jsonObject.get("type"))) { + return false + } + return true + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString && candidate.asString == "baz" + } + + private fun matches5(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("namespace") || !matches1(jsonObject.get("namespace"))) { + return false + } + if (!jsonObject.has("type") || !matches6(jsonObject.get("type"))) { + return false + } + if (!jsonObject.has("url") || !matches1(jsonObject.get("url"))) { + return false + } + return true + } + + private fun matches6(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString && candidate.asString == "qux" + } } diff --git a/tests/golden/kotlin/kotlin-okhttp/type-aliases-nested-union/models/Foo.kt.golden b/tests/golden/kotlin/kotlin-okhttp/type-aliases-nested-union/models/Foo.kt.golden index ec6c64bdb..c00d6b977 100644 --- a/tests/golden/kotlin/kotlin-okhttp/type-aliases-nested-union/models/Foo.kt.golden +++ b/tests/golden/kotlin/kotlin-okhttp/type-aliases-nested-union/models/Foo.kt.golden @@ -27,18 +27,34 @@ sealed class Foo private constructor() { data class VariantNumber(val value0: Double) : Foo() } -private class FooJsonAdapter: TypeAdapter() { - private val gson = Gson() +private class FooJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() - override fun read (reader: JsonReader): Foo? { + override fun read(reader: JsonReader): Foo? { val element = JsonParser.parseReader(reader) - return when { - element.isJsonNull -> null - (((element.isJsonObject && element.asJsonObject.has("endpoint")) && element.asJsonObject.has("type")) || ((element.isJsonObject && element.asJsonObject.has("path")) && element.asJsonObject.has("type"))) -> Foo.VariantQux(gson.fromJson(element, object: TypeToken() {}.type)) - element.isJsonPrimitive && element.asJsonPrimitive.isString -> Foo.VariantString(element.asString) - element.isJsonPrimitive && element.asJsonPrimitive.isNumber -> Foo.VariantNumber(element.asDouble) - else -> throw JsonParseException("unknown Foo union value") + if (element.isJsonNull) { + return null } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches2(element)) { + selected = 1 + } + if (selected < 0 && matches6(element)) { + selected = 2 + } + if (selected == 0) { + return Foo.VariantQux(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return Foo.VariantString(element.asString) + } + if (selected == 2) { + return Foo.VariantNumber(element.asDouble) + } + throw JsonParseException("unknown Foo union value") } override fun write(writer: JsonWriter, value: Foo?) { @@ -48,14 +64,68 @@ private class FooJsonAdapter: TypeAdapter() { } when (value) { is Foo.VariantQux -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is Foo.VariantString -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is Foo.VariantNumber -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } } } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (matches1(candidate)) { + return true + } + if (matches4(candidate)) { + return true + } + return false + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("endpoint") || !matches2(jsonObject.get("endpoint"))) { + return false + } + if (!jsonObject.has("type") || !matches3(jsonObject.get("type"))) { + return false + } + return true + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString && candidate.asString == "bar" + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("path") || !matches2(jsonObject.get("path"))) { + return false + } + if (!jsonObject.has("type") || !matches5(jsonObject.get("type"))) { + return false + } + return true + } + + private fun matches5(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString && candidate.asString == "baz" + } + + private fun matches6(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber + } } diff --git a/tests/golden/kotlin/kotlin-okhttp/type-aliases-nested-union/models/Qux.kt.golden b/tests/golden/kotlin/kotlin-okhttp/type-aliases-nested-union/models/Qux.kt.golden index 275ceb71a..c25f54d7b 100644 --- a/tests/golden/kotlin/kotlin-okhttp/type-aliases-nested-union/models/Qux.kt.golden +++ b/tests/golden/kotlin/kotlin-okhttp/type-aliases-nested-union/models/Qux.kt.golden @@ -25,17 +25,28 @@ sealed class Qux private constructor() { data class VariantBaz(val value0: Baz) : Qux() } -private class QuxJsonAdapter: TypeAdapter() { - private val gson = Gson() +private class QuxJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() - override fun read (reader: JsonReader): Qux? { + override fun read(reader: JsonReader): Qux? { val element = JsonParser.parseReader(reader) - return when { - element.isJsonNull -> null - ((element.isJsonObject && element.asJsonObject.has("endpoint")) && element.asJsonObject.has("type")) -> Qux.VariantBar(gson.fromJson(element, object: TypeToken() {}.type)) - ((element.isJsonObject && element.asJsonObject.has("path")) && element.asJsonObject.has("type")) -> Qux.VariantBaz(gson.fromJson(element, object: TypeToken() {}.type)) - else -> throw JsonParseException("unknown Qux union value") + if (element.isJsonNull) { + return null } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches3(element)) { + selected = 1 + } + if (selected == 0) { + return Qux.VariantBar(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return Qux.VariantBaz(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown Qux union value") } override fun write(writer: JsonWriter, value: Qux?) { @@ -45,11 +56,51 @@ private class QuxJsonAdapter: TypeAdapter() { } when (value) { is Qux.VariantBar -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is Qux.VariantBaz -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } } } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("endpoint") || !matches1(jsonObject.get("endpoint"))) { + return false + } + if (!jsonObject.has("type") || !matches2(jsonObject.get("type"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString && candidate.asString == "bar" + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("path") || !matches1(jsonObject.get("path"))) { + return false + } + if (!jsonObject.has("type") || !matches4(jsonObject.get("type"))) { + return false + } + return true + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString && candidate.asString == "baz" + } } diff --git a/tests/golden/kotlin/kotlin-okhttp/type-aliases-union-mixed/models/Foo.kt.golden b/tests/golden/kotlin/kotlin-okhttp/type-aliases-union-mixed/models/Foo.kt.golden index 2bbb7bef6..872ec43b8 100644 --- a/tests/golden/kotlin/kotlin-okhttp/type-aliases-union-mixed/models/Foo.kt.golden +++ b/tests/golden/kotlin/kotlin-okhttp/type-aliases-union-mixed/models/Foo.kt.golden @@ -27,18 +27,34 @@ sealed class Foo private constructor() { data class VariantNumber(val value0: Double) : Foo() } -private class FooJsonAdapter: TypeAdapter() { - private val gson = Gson() +private class FooJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() - override fun read (reader: JsonReader): Foo? { + override fun read(reader: JsonReader): Foo? { val element = JsonParser.parseReader(reader) - return when { - element.isJsonNull -> null - ((element.isJsonObject && element.asJsonObject.has("id")) && element.asJsonObject.has("name")) -> Foo.VariantBar(gson.fromJson(element, object: TypeToken() {}.type)) - element.isJsonPrimitive && element.asJsonPrimitive.isString -> Foo.VariantString(element.asString) - element.isJsonPrimitive && element.asJsonPrimitive.isNumber -> Foo.VariantNumber(element.asDouble) - else -> throw JsonParseException("unknown Foo union value") + if (element.isJsonNull) { + return null } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches2(element)) { + selected = 1 + } + if (selected < 0 && matches3(element)) { + selected = 2 + } + if (selected == 0) { + return Foo.VariantBar(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return Foo.VariantString(element.asString) + } + if (selected == 2) { + return Foo.VariantNumber(element.asDouble) + } + throw JsonParseException("unknown Foo union value") } override fun write(writer: JsonWriter, value: Foo?) { @@ -48,14 +64,83 @@ private class FooJsonAdapter: TypeAdapter() { } when (value) { is Foo.VariantBar -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is Foo.VariantString -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is Foo.VariantNumber -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } } } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("id") || !matches1(jsonObject.get("id"))) { + return false + } + if (!jsonObject.has("name") || !matches2(jsonObject.get("name"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } } diff --git a/tests/golden/kotlin/kotlin-okhttp/type-aliases-union-with-any/models/Config.kt.golden b/tests/golden/kotlin/kotlin-okhttp/type-aliases-union-with-any/models/Config.kt.golden index 36aa45693..fa4fabd0f 100644 --- a/tests/golden/kotlin/kotlin-okhttp/type-aliases-union-with-any/models/Config.kt.golden +++ b/tests/golden/kotlin/kotlin-okhttp/type-aliases-union-with-any/models/Config.kt.golden @@ -27,17 +27,34 @@ sealed class Config private constructor() { data class VariantAny(val value0: JsonElement) : Config() } -private class ConfigJsonAdapter: TypeAdapter() { - private val gson = Gson() +private class ConfigJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() - override fun read (reader: JsonReader): Config? { + override fun read(reader: JsonReader): Config? { val element = JsonParser.parseReader(reader) - return when { - element.isJsonNull -> null - element.isJsonPrimitive && element.asJsonPrimitive.isString -> Config.VariantString(element.asString) - element.isJsonPrimitive && element.asJsonPrimitive.isNumber -> Config.VariantNumber(element.asDouble) - else -> Config.VariantAny(gson.fromJson(element, object: TypeToken() {}.type)) + if (element.isJsonNull) { + return null } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches1(element)) { + selected = 1 + } + if (selected < 0 && true) { + selected = 2 + } + if (selected == 0) { + return Config.VariantString(element.asString) + } + if (selected == 1) { + return Config.VariantNumber(element.asDouble) + } + if (selected == 2) { + return Config.VariantAny(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown Config union value") } override fun write(writer: JsonWriter, value: Config?) { @@ -47,14 +64,22 @@ private class ConfigJsonAdapter: TypeAdapter() { } when (value) { is Config.VariantString -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is Config.VariantNumber -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is Config.VariantAny -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } } } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber + } } diff --git a/tests/golden/kotlin/kotlin-okhttp/type-aliases-union-with-inline-objects/models/Foo.kt.golden b/tests/golden/kotlin/kotlin-okhttp/type-aliases-union-with-inline-objects/models/Foo.kt.golden index 62d259fb0..744be5a17 100644 --- a/tests/golden/kotlin/kotlin-okhttp/type-aliases-union-with-inline-objects/models/Foo.kt.golden +++ b/tests/golden/kotlin/kotlin-okhttp/type-aliases-union-with-inline-objects/models/Foo.kt.golden @@ -30,18 +30,34 @@ sealed class Foo private constructor() { data class VariantFooMember3(val value0: FooMember3) : Foo() } -private class FooJsonAdapter: TypeAdapter() { - private val gson = Gson() +private class FooJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() - override fun read (reader: JsonReader): Foo? { + override fun read(reader: JsonReader): Foo? { val element = JsonParser.parseReader(reader) - return when { - element.isJsonNull -> null - (element.isJsonObject && element.asJsonObject.has("bar")) -> Foo.VariantFooMember1(gson.fromJson(element, object: TypeToken() {}.type)) - (element.isJsonObject && element.asJsonObject.has("quux")) -> Foo.VariantFooMember2(gson.fromJson(element, object: TypeToken() {}.type)) - (element.isJsonObject && element.asJsonObject.has("grault")) -> Foo.VariantFooMember3(gson.fromJson(element, object: TypeToken() {}.type)) - else -> throw JsonParseException("unknown Foo union value") + if (element.isJsonNull) { + return null } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches3(element)) { + selected = 1 + } + if (selected < 0 && matches5(element)) { + selected = 2 + } + if (selected == 0) { + return Foo.VariantFooMember1(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return Foo.VariantFooMember2(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 2) { + return Foo.VariantFooMember3(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown Foo union value") } override fun write(writer: JsonWriter, value: Foo?) { @@ -51,14 +67,87 @@ private class FooJsonAdapter: TypeAdapter() { } when (value) { is Foo.VariantFooMember1 -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is Foo.VariantFooMember2 -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is Foo.VariantFooMember3 -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } } } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("bar") || !matches1(jsonObject.get("bar"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("baz") || !matches2(jsonObject.get("baz"))) { + return false + } + if (!jsonObject.has("qux") || !matches2(jsonObject.get("qux"))) { + return false + } + return true + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("quux") || !matches4(jsonObject.get("quux"))) { + return false + } + return true + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("corge") || !matches2(jsonObject.get("corge"))) { + return false + } + return true + } + + private fun matches5(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("grault") || !matches6(jsonObject.get("grault"))) { + return false + } + return true + } + + private fun matches6(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("garply") || !matches2(jsonObject.get("garply"))) { + return false + } + return true + } } diff --git a/tests/golden/kotlin/kotlin-okhttp/type-aliases-union-with-interfaces/models/Foo.kt.golden b/tests/golden/kotlin/kotlin-okhttp/type-aliases-union-with-interfaces/models/Foo.kt.golden index af67f9c0e..c3e0d4258 100644 --- a/tests/golden/kotlin/kotlin-okhttp/type-aliases-union-with-interfaces/models/Foo.kt.golden +++ b/tests/golden/kotlin/kotlin-okhttp/type-aliases-union-with-interfaces/models/Foo.kt.golden @@ -27,18 +27,34 @@ sealed class Foo private constructor() { data class VariantQux(val value0: Qux) : Foo() } -private class FooJsonAdapter: TypeAdapter() { - private val gson = Gson() +private class FooJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() - override fun read (reader: JsonReader): Foo? { + override fun read(reader: JsonReader): Foo? { val element = JsonParser.parseReader(reader) - return when { - element.isJsonNull -> null - (((element.isJsonObject && element.asJsonObject.has("bucket")) && element.asJsonObject.has("endpoint")) && element.asJsonObject.has("type")) -> Foo.VariantBar(gson.fromJson(element, object: TypeToken() {}.type)) - ((element.isJsonObject && element.asJsonObject.has("path")) && element.asJsonObject.has("type")) -> Foo.VariantBaz(gson.fromJson(element, object: TypeToken() {}.type)) - (((element.isJsonObject && element.asJsonObject.has("namespace")) && element.asJsonObject.has("type")) && element.asJsonObject.has("url")) -> Foo.VariantQux(gson.fromJson(element, object: TypeToken() {}.type)) - else -> throw JsonParseException("unknown Foo union value") + if (element.isJsonNull) { + return null } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches3(element)) { + selected = 1 + } + if (selected < 0 && matches5(element)) { + selected = 2 + } + if (selected == 0) { + return Foo.VariantBar(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return Foo.VariantBaz(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 2) { + return Foo.VariantQux(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown Foo union value") } override fun write(writer: JsonWriter, value: Foo?) { @@ -48,14 +64,78 @@ private class FooJsonAdapter: TypeAdapter() { } when (value) { is Foo.VariantBar -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is Foo.VariantBaz -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is Foo.VariantQux -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } } } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("bucket") || !matches1(jsonObject.get("bucket"))) { + return false + } + if (!jsonObject.has("endpoint") || !matches1(jsonObject.get("endpoint"))) { + return false + } + if (!jsonObject.has("type") || !matches2(jsonObject.get("type"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString && candidate.asString == "bar" + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("path") || !matches1(jsonObject.get("path"))) { + return false + } + if (!jsonObject.has("type") || !matches4(jsonObject.get("type"))) { + return false + } + return true + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString && candidate.asString == "baz" + } + + private fun matches5(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("namespace") || !matches1(jsonObject.get("namespace"))) { + return false + } + if (!jsonObject.has("type") || !matches6(jsonObject.get("type"))) { + return false + } + if (!jsonObject.has("url") || !matches1(jsonObject.get("url"))) { + return false + } + return true + } + + private fun matches6(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString && candidate.asString == "qux" + } } diff --git a/tests/golden/kotlin/kotlin-okhttp/type-aliases-union-with-primitives/models/Foo.kt.golden b/tests/golden/kotlin/kotlin-okhttp/type-aliases-union-with-primitives/models/Foo.kt.golden index eda0ec595..b180344d7 100644 --- a/tests/golden/kotlin/kotlin-okhttp/type-aliases-union-with-primitives/models/Foo.kt.golden +++ b/tests/golden/kotlin/kotlin-okhttp/type-aliases-union-with-primitives/models/Foo.kt.golden @@ -32,18 +32,34 @@ sealed class Foo private constructor() { data object VariantNull : Foo() } -private class FooJsonAdapter: TypeAdapter() { - private val gson = Gson() +private class FooJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() - override fun read (reader: JsonReader): Foo? { + override fun read(reader: JsonReader): Foo? { val element = JsonParser.parseReader(reader) - return when { - element.isJsonNull -> Foo.VariantNull - element.isJsonPrimitive && element.asJsonPrimitive.isString -> Foo.VariantString(element.asString) - element.isJsonPrimitive && element.asJsonPrimitive.isNumber -> Foo.VariantNumber(element.asDouble) - element.isJsonPrimitive && element.asJsonPrimitive.isBoolean -> Foo.VariantBoolean(element.asBoolean) - else -> throw JsonParseException("unknown Foo union value") + if (element.isJsonNull) { + return Foo.VariantNull } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches1(element)) { + selected = 1 + } + if (selected < 0 && matches2(element)) { + selected = 2 + } + if (selected == 0) { + return Foo.VariantString(element.asString) + } + if (selected == 1) { + return Foo.VariantNumber(element.asDouble) + } + if (selected == 2) { + return Foo.VariantBoolean(element.asBoolean) + } + throw JsonParseException("unknown Foo union value") } override fun write(writer: JsonWriter, value: Foo?) { @@ -53,17 +69,29 @@ private class FooJsonAdapter: TypeAdapter() { } when (value) { is Foo.VariantString -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is Foo.VariantNumber -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is Foo.VariantBoolean -> { - gson.toJson(gson.toJsonTree(value.value0), writer) + gson.toJson(value.value0, object: TypeToken() {}.type, writer) } is Foo.VariantNull -> { writer.nullValue() } } } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isBoolean + } } diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/README.md.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/README.md.golden new file mode 100644 index 000000000..09219a3d7 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/README.md.golden @@ -0,0 +1,7 @@ +# Java union policies + +Generated Kotlin SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `java-union-policies`. diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/apis/SelectionApi.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/apis/SelectionApi.kt.golden new file mode 100644 index 000000000..1dd8e417c --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/apis/SelectionApi.kt.golden @@ -0,0 +1,101 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.apis + +import com.example.sdk.models.* +import com.example.sdk.runtime.ApiClient +import com.example.sdk.runtime.ApiException +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Response + +/** + * GetChoiceResponse carries the response from getChoice. + */ +data class GetChoiceResponse(val statusCode: Int, val raw: Response, + val status200: FiniteChoice? = null) { +} + +sealed interface GetChoiceExceptionDetail { + val statusCode: Int + val headers: okhttp3.Headers + fun rawBody(): ByteArray +} + +class GetChoiceException( + val detail: GetChoiceExceptionDetail, + statusCode: Int, + status: String, + body: String, + val headers: okhttp3.Headers, + rawBodyBytes: ByteArray, +) : ApiException(statusCode, status, body) { + private val rawBodyBytes: ByteArray = rawBodyBytes.copyOf() + fun rawBody(): ByteArray = this.rawBodyBytes.copyOf() +} + +class GetChoiceBadRequest( + override val statusCode: Int, + override val headers: okhttp3.Headers, + rawBodyBytes: ByteArray, +) : GetChoiceExceptionDetail { + private val rawBodyBytes: ByteArray = rawBodyBytes.copyOf() + override fun rawBody(): ByteArray = this.rawBodyBytes.copyOf() + private fun textBody(): String { + val charset = headers["Content-Type"]?.toMediaTypeOrNull()?.charset(Charsets.UTF_8) ?: Charsets.UTF_8 + return rawBodyBytes.toString(charset) + } + fun body(): FiniteChoice = Gson().fromJson(textBody().ifEmpty { "null" }, object : TypeToken() {}.type) +} + +class GetChoiceUnexpected( + override val statusCode: Int, + override val headers: okhttp3.Headers, + rawBodyBytes: ByteArray, +) : GetChoiceExceptionDetail { + private val rawBodyBytes: ByteArray = rawBodyBytes.copyOf() + override fun rawBody(): ByteArray = this.rawBodyBytes.copyOf() + fun body(): ByteArray = rawBody() +} + +/** + * SelectionApi groups operations under the Selection tag. + */ +class SelectionApi(private val client: ApiClient) { + private val gson: Gson = Gson() + + /** + * getChoice GET /choice. + */ + fun getChoice(): GetChoiceResponse { + val path = "/choice" + val request = client.newRequest("GET", path, null, null) + val response = client.execute(request) + + if (!response.isSuccessful) { + val responseBody = response.body + val responseCharset = responseBody?.contentType()?.charset(Charsets.UTF_8) ?: Charsets.UTF_8 + val responseBytes = responseBody?.bytes() ?: ByteArray(0) + val responseText = responseBytes.toString(responseCharset) + val detail: GetChoiceExceptionDetail = when (response.code) { + 400 -> GetChoiceBadRequest(statusCode = response.code, headers = response.headers, rawBodyBytes = responseBytes) + else -> { + GetChoiceUnexpected(statusCode = response.code, headers = response.headers, rawBodyBytes = responseBytes) + } + } + throw GetChoiceException(detail = detail, statusCode = response.code, status = response.message, body = responseText, headers = response.headers, rawBodyBytes = responseBytes) + } + + val responseBytes = response.body?.bytes() ?: ByteArray(0) + val responseText = responseBytes.toString(Charsets.UTF_8) + var status200: FiniteChoice? = null + + if (response.code == 200) { + status200 = gson.fromJson(responseText.ifEmpty { "null" }, object : TypeToken() {}.type) + } + return GetChoiceResponse(statusCode = response.code, raw = response, status200 = status200) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/build.gradle.kts.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/build.gradle.kts.golden new file mode 100644 index 000000000..093728ca0 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/build.gradle.kts.golden @@ -0,0 +1,27 @@ +plugins { + kotlin("jvm") version "2.2.21" +} + +group = "com.example.sdk" +version = "1.0.0" + +repositories { + mavenCentral() +} + +dependencies { + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.google.code.gson:gson:2.11.0") +} + +kotlin { + jvmToolchain(17) +} + +sourceSets { + main { + kotlin { + srcDirs("apis", "models", "runtime") + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/AliasChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/AliasChoice.kt.golden new file mode 100644 index 000000000..ad6edf4b6 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/AliasChoice.kt.golden @@ -0,0 +1,130 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(AliasChoiceJsonAdapter::class, nullSafe = false) +sealed class AliasChoice private constructor() { + data class VariantNumberAlias(val value0: NumberAlias) : AliasChoice() + + data class VariantAlpha(val value0: Alpha) : AliasChoice() +} + +private class AliasChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): AliasChoice? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches1(element)) { + selected = 1 + } + if (selected == 0) { + return AliasChoice.VariantNumberAlias(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return AliasChoice.VariantAlpha(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown AliasChoice union value") + } + + override fun write(writer: JsonWriter, value: AliasChoice?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is AliasChoice.VariantNumberAlias -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is AliasChoice.VariantAlpha -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("first_value") && !matches2(jsonObject.get("first_value"))) { + return false + } + if (!jsonObject.has("wire_name") || !matches2(jsonObject.get("wire_name"))) { + return false + } + return true + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/Alpha.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/Alpha.kt.golden new file mode 100644 index 000000000..d37b3c5bd --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/Alpha.kt.golden @@ -0,0 +1,12 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class Alpha(@field:SerializedName("first_value") + val firstValue: String? = null, @field:SerializedName("wire_name") + val wireName: String) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/Ambiguous.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/Ambiguous.kt.golden new file mode 100644 index 000000000..12a1a73b9 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/Ambiguous.kt.golden @@ -0,0 +1,97 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(AmbiguousJsonAdapter::class, nullSafe = false) +sealed class Ambiguous private constructor() { + data class VariantAlpha(val value0: Alpha) : Ambiguous() + + data class VariantBeta(val value0: Beta) : Ambiguous() +} + +private class AmbiguousJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): Ambiguous? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches2(element)) { + selected = 1 + } + if (selected == 0) { + return Ambiguous.VariantAlpha(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return Ambiguous.VariantBeta(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown Ambiguous union value") + } + + override fun write(writer: JsonWriter, value: Ambiguous?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is Ambiguous.VariantAlpha -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is Ambiguous.VariantBeta -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("first_value") && !matches1(jsonObject.get("first_value"))) { + return false + } + if (!jsonObject.has("wire_name") || !matches1(jsonObject.get("wire_name"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("second_value") && !matches1(jsonObject.get("second_value"))) { + return false + } + if (!jsonObject.has("wire_name") || !matches1(jsonObject.get("wire_name"))) { + return false + } + return true + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/AmbiguousAlias.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/AmbiguousAlias.kt.golden new file mode 100644 index 000000000..968f4c07b --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/AmbiguousAlias.kt.golden @@ -0,0 +1,7 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +typealias AmbiguousAlias = Ambiguous diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/Beta.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/Beta.kt.golden new file mode 100644 index 000000000..66aacf6a6 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/Beta.kt.golden @@ -0,0 +1,12 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class Beta(@field:SerializedName("second_value") + val secondValue: String? = null, @field:SerializedName("wire_name") + val wireName: String) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/BinaryChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/BinaryChoice.kt.golden new file mode 100644 index 000000000..31b64f808 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/BinaryChoice.kt.golden @@ -0,0 +1,124 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(BinaryChoiceJsonAdapter::class, nullSafe = false) +sealed class BinaryChoice private constructor() { + data class VariantBinary(val value0: ByteArray) : BinaryChoice() + + data class VariantString(val value0: String) : BinaryChoice() +} + +private class BinaryChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): BinaryChoice? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches1(element)) { + selected = 1 + } + if (selected == 0) { + return BinaryChoice.VariantBinary(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return BinaryChoice.VariantString(element.asString) + } + throw JsonParseException("unknown BinaryChoice union value") + } + + override fun write(writer: JsonWriter, value: BinaryChoice?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is BinaryChoice.VariantBinary -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is BinaryChoice.VariantString -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonArray) { + return false + } + for (item in candidate.asJsonArray) { + if (!isBinaryNumber(item)) { + return false + } + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/BranchOne.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/BranchOne.kt.golden new file mode 100644 index 000000000..609156ffe --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/BranchOne.kt.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +data class BranchOne(val children: List, val revision: Int) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/BranchTwo.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/BranchTwo.kt.golden new file mode 100644 index 000000000..f082326a6 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/BranchTwo.kt.golden @@ -0,0 +1,12 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class BranchTwo(val children: List, val revision: Int, + @field:SerializedName("second_value") + val secondValue: String? = null) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/ChoiceEnvelope.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/ChoiceEnvelope.kt.golden new file mode 100644 index 000000000..ebb34058c --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/ChoiceEnvelope.kt.golden @@ -0,0 +1,13 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class ChoiceEnvelope(@field:SerializedName("by_name") + val byName: Map, val caption: String, val choice: FiniteChoice, + val choices: List, @field:SerializedName("ordinary_null") + val ordinaryNull: String? = null) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/ContainerChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/ContainerChoice.kt.golden new file mode 100644 index 000000000..eefcfee54 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/ContainerChoice.kt.golden @@ -0,0 +1,193 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(ContainerChoiceJsonAdapter::class, nullSafe = false) +sealed class ContainerChoice private constructor() { + data class VariantContainerOne(val value0: ContainerOne) : ContainerChoice() + + data class VariantContainerTwo(val value0: ContainerTwo) : ContainerChoice() +} + +private class ContainerChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): ContainerChoice? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches5(element)) { + selected = 1 + } + if (selected == 0) { + return ContainerChoice.VariantContainerOne(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return ContainerChoice.VariantContainerTwo(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown ContainerChoice union value") + } + + override fun write(writer: JsonWriter, value: ContainerChoice?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is ContainerChoice.VariantContainerOne -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is ContainerChoice.VariantContainerTwo -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("values") || !matches1(jsonObject.get("values"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonArray) { + return false + } + for (item in candidate.asJsonArray) { + if (!matches2(item)) { + return false + } + } + return true + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + if (!matches3(candidate)) { + return false + } + if (!matches4(candidate)) { + return false + } + return true + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "1") + } + + private fun matches5(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("second_value") && !matches6(jsonObject.get("second_value"))) { + return false + } + if (!jsonObject.has("values") || !matches7(jsonObject.get("values"))) { + return false + } + return true + } + + private fun matches6(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches7(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonArray) { + return false + } + for (item in candidate.asJsonArray) { + if (!matches8(item)) { + return false + } + } + return true + } + + private fun matches8(candidate: JsonElement): kotlin.Boolean { + if (!matches3(candidate)) { + return false + } + if (!matches9(candidate)) { + return false + } + return true + } + + private fun matches9(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "2") + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/ContainerOne.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/ContainerOne.kt.golden new file mode 100644 index 000000000..34f5d490b --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/ContainerOne.kt.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +data class ContainerOne(val values: List) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/ContainerTwo.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/ContainerTwo.kt.golden new file mode 100644 index 000000000..f5978fcf8 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/ContainerTwo.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class ContainerTwo(@field:SerializedName("second_value") + val secondValue: String? = null, val values: List) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/ExcludedNull.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/ExcludedNull.kt.golden new file mode 100644 index 000000000..0b04d6f11 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/ExcludedNull.kt.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +data class ExcludedNull(val revision: Int? = null) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/FiniteChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/FiniteChoice.kt.golden new file mode 100644 index 000000000..149538f92 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/FiniteChoice.kt.golden @@ -0,0 +1,316 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(FiniteChoiceJsonAdapter::class, nullSafe = false) +sealed class FiniteChoice private constructor() { + data class VariantFiniteOne(val value0: FiniteOne) : FiniteChoice() + + data class VariantFiniteTwo(val value0: FiniteTwo) : FiniteChoice() +} + +private class FiniteChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): FiniteChoice? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches14(element)) { + selected = 1 + } + if (selected == 0) { + return FiniteChoice.VariantFiniteOne(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return FiniteChoice.VariantFiniteTwo(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown FiniteChoice union value") + } + + override fun write(writer: JsonWriter, value: FiniteChoice?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is FiniteChoice.VariantFiniteOne -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is FiniteChoice.VariantFiniteTwo -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("first_value") && !matches1(jsonObject.get("first_value"))) { + return false + } + if (!jsonObject.has("flags") || !matches2(jsonObject.get("flags"))) { + return false + } + if (!jsonObject.has("revision") || !matches6(jsonObject.get("revision"))) { + return false + } + if (!jsonObject.has("values") || !matches9(jsonObject.get("values"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + for (item in candidate.asJsonObject.asMap().values) { + if (!matches3(item)) { + return false + } + } + return true + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + if (!matches4(candidate)) { + return false + } + if (!matches5(candidate)) { + return false + } + return true + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isBoolean + } + + private fun matches5(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isBoolean && candidate.asBoolean == false + } + + private fun matches6(candidate: JsonElement): kotlin.Boolean { + if (!matches7(candidate)) { + return false + } + if (!matches8(candidate)) { + return false + } + return true + } + + private fun matches7(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches8(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "1") + } + + private fun matches9(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonArray) { + return false + } + for (item in candidate.asJsonArray) { + if (!matches10(item)) { + return false + } + } + return true + } + + private fun matches10(candidate: JsonElement): kotlin.Boolean { + if (!matches11(candidate)) { + return false + } + if (!matches12(candidate)) { + return false + } + return true + } + + private fun matches11(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber + } + + private fun matches12(candidate: JsonElement): kotlin.Boolean { + if (matches8(candidate)) { + return true + } + if (matches13(candidate)) { + return true + } + return false + } + + private fun matches13(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "1.5") + } + + private fun matches14(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("flags") || !matches15(jsonObject.get("flags"))) { + return false + } + if (!jsonObject.has("revision") || !matches18(jsonObject.get("revision"))) { + return false + } + if (jsonObject.has("second_value") && !matches1(jsonObject.get("second_value"))) { + return false + } + if (!jsonObject.has("values") || !matches20(jsonObject.get("values"))) { + return false + } + return true + } + + private fun matches15(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + for (item in candidate.asJsonObject.asMap().values) { + if (!matches16(item)) { + return false + } + } + return true + } + + private fun matches16(candidate: JsonElement): kotlin.Boolean { + if (!matches4(candidate)) { + return false + } + if (!matches17(candidate)) { + return false + } + return true + } + + private fun matches17(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isBoolean && candidate.asBoolean == true + } + + private fun matches18(candidate: JsonElement): kotlin.Boolean { + if (!matches7(candidate)) { + return false + } + if (!matches19(candidate)) { + return false + } + return true + } + + private fun matches19(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "2") + } + + private fun matches20(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonArray) { + return false + } + for (item in candidate.asJsonArray) { + if (!matches21(item)) { + return false + } + } + return true + } + + private fun matches21(candidate: JsonElement): kotlin.Boolean { + if (!matches11(candidate)) { + return false + } + if (!matches22(candidate)) { + return false + } + return true + } + + private fun matches22(candidate: JsonElement): kotlin.Boolean { + if (matches19(candidate)) { + return true + } + if (matches23(candidate)) { + return true + } + return false + } + + private fun matches23(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "2.5") + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/FiniteOne.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/FiniteOne.kt.golden new file mode 100644 index 000000000..c20d52ca6 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/FiniteOne.kt.golden @@ -0,0 +1,12 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class FiniteOne(@field:SerializedName("first_value") + val firstValue: String? = null, val flags: Map, val revision: Int, + val values: List) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/FiniteTwo.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/FiniteTwo.kt.golden new file mode 100644 index 000000000..6cbe6bcd1 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/FiniteTwo.kt.golden @@ -0,0 +1,12 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class FiniteTwo(val flags: Map, val revision: Int, + @field:SerializedName("second_value") + val secondValue: String? = null, val values: List) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/FormatChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/FormatChoice.kt.golden new file mode 100644 index 000000000..3e698a388 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/FormatChoice.kt.golden @@ -0,0 +1,141 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(FormatChoiceJsonAdapter::class, nullSafe = false) +sealed class FormatChoice private constructor() { + data class VariantNarrow(val value0: Narrow) : FormatChoice() + + data class VariantWide(val value0: Wide) : FormatChoice() +} + +private class FormatChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): FormatChoice? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches2(element)) { + selected = 1 + } + if (selected == 0) { + return FormatChoice.VariantNarrow(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return FormatChoice.VariantWide(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown FormatChoice union value") + } + + override fun write(writer: JsonWriter, value: FormatChoice?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is FormatChoice.VariantNarrow -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is FormatChoice.VariantWide -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("identifier") || !matches1(jsonObject.get("identifier"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("identifier") || !matches1(jsonObject.get("identifier"))) { + return false + } + if (jsonObject.has("second_value") && !matches3(jsonObject.get("second_value"))) { + return false + } + return true + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/FormatFallback.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/FormatFallback.kt.golden new file mode 100644 index 000000000..9a80a9583 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/FormatFallback.kt.golden @@ -0,0 +1,123 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(FormatFallbackJsonAdapter::class, nullSafe = false) +sealed class FormatFallback private constructor() { + data class VariantNarrow(val value0: Narrow) : FormatFallback() + + data class VariantAny(val value0: JsonElement) : FormatFallback() +} + +private class FormatFallbackJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): FormatFallback? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && true) { + selected = 1 + } + if (selected == 0) { + return FormatFallback.VariantNarrow(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return FormatFallback.VariantAny(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown FormatFallback union value") + } + + override fun write(writer: JsonWriter, value: FormatFallback?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is FormatFallback.VariantNarrow -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is FormatFallback.VariantAny -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("identifier") || !matches1(jsonObject.get("identifier"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/IncludedNull.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/IncludedNull.kt.golden new file mode 100644 index 000000000..942b2b97d --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/IncludedNull.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class IncludedNull(val revision: Int? = null, @field:SerializedName("second_value") + val secondValue: String? = null) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/IntegerFallback.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/IntegerFallback.kt.golden new file mode 100644 index 000000000..10bdc428f --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/IntegerFallback.kt.golden @@ -0,0 +1,112 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(IntegerFallbackJsonAdapter::class, nullSafe = false) +sealed class IntegerFallback private constructor() { + data class VariantInteger(val value0: Int) : IntegerFallback() + + data class VariantAny(val value0: JsonElement) : IntegerFallback() +} + +private class IntegerFallbackJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): IntegerFallback? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && true) { + selected = 1 + } + if (selected == 0) { + return IntegerFallback.VariantInteger(element.asInt) + } + if (selected == 1) { + return IntegerFallback.VariantAny(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown IntegerFallback union value") + } + + override fun write(writer: JsonWriter, value: IntegerFallback?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is IntegerFallback.VariantInteger -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is IntegerFallback.VariantAny -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/LargeChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/LargeChoice.kt.golden new file mode 100644 index 000000000..7c4db616d --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/LargeChoice.kt.golden @@ -0,0 +1,169 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(LargeChoiceJsonAdapter::class, nullSafe = false) +sealed class LargeChoice private constructor() { + data class VariantLargeOne(val value0: LargeOne) : LargeChoice() + + data class VariantLargeTwo(val value0: LargeTwo) : LargeChoice() +} + +private class LargeChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): LargeChoice? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches4(element)) { + selected = 1 + } + if (selected == 0) { + return LargeChoice.VariantLargeOne(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return LargeChoice.VariantLargeTwo(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown LargeChoice union value") + } + + override fun write(writer: JsonWriter, value: LargeChoice?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is LargeChoice.VariantLargeOne -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is LargeChoice.VariantLargeTwo -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("revision") || !matches1(jsonObject.get("revision"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + if (!matches2(candidate)) { + return false + } + if (!matches3(candidate)) { + return false + } + return true + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "9007199254740992") + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("revision") || !matches5(jsonObject.get("revision"))) { + return false + } + if (jsonObject.has("second_value") && !matches7(jsonObject.get("second_value"))) { + return false + } + return true + } + + private fun matches5(candidate: JsonElement): kotlin.Boolean { + if (!matches2(candidate)) { + return false + } + if (!matches6(candidate)) { + return false + } + return true + } + + private fun matches6(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "9007199254740993") + } + + private fun matches7(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/LargeOne.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/LargeOne.kt.golden new file mode 100644 index 000000000..4c5f300e5 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/LargeOne.kt.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +data class LargeOne(val revision: Long) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/LargeTwo.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/LargeTwo.kt.golden new file mode 100644 index 000000000..5cc5cb94d --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/LargeTwo.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class LargeTwo(val revision: Long, @field:SerializedName("second_value") + val secondValue: String? = null) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/LeftPart.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/LeftPart.kt.golden new file mode 100644 index 000000000..23121ff0b --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/LeftPart.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class LeftPart(@field:SerializedName("left_value") + val leftValue: String) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/Narrow.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/Narrow.kt.golden new file mode 100644 index 000000000..697c17d40 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/Narrow.kt.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +data class Narrow(val identifier: Int) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/NullableChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/NullableChoice.kt.golden new file mode 100644 index 000000000..d02d04690 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/NullableChoice.kt.golden @@ -0,0 +1,80 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(NullableChoiceJsonAdapter::class, nullSafe = false) +sealed class NullableChoice private constructor() { + data class VariantAlpha(val value0: Alpha) : NullableChoice() + + /** + * The union's null value. + */ + data object VariantNull : NullableChoice() +} + +private class NullableChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): NullableChoice? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return NullableChoice.VariantNull + } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected == 0) { + return NullableChoice.VariantAlpha(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown NullableChoice union value") + } + + override fun write(writer: JsonWriter, value: NullableChoice?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is NullableChoice.VariantAlpha -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is NullableChoice.VariantNull -> { + writer.nullValue() + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("first_value") && !matches1(jsonObject.get("first_value"))) { + return false + } + if (!jsonObject.has("wire_name") || !matches1(jsonObject.get("wire_name"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/NullableFields.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/NullableFields.kt.golden new file mode 100644 index 000000000..10fd68668 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/NullableFields.kt.golden @@ -0,0 +1,201 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(NullableFieldsJsonAdapter::class, nullSafe = false) +sealed class NullableFields private constructor() { + data class VariantExcludedNull(val value0: ExcludedNull) : NullableFields() + + data class VariantIncludedNull(val value0: IncludedNull) : NullableFields() +} + +private class NullableFieldsJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): NullableFields? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches6(element)) { + selected = 1 + } + if (selected == 0) { + return NullableFields.VariantExcludedNull(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return NullableFields.VariantIncludedNull(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown NullableFields union value") + } + + override fun write(writer: JsonWriter, value: NullableFields?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is NullableFields.VariantExcludedNull -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is NullableFields.VariantIncludedNull -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("revision") || !matches1(jsonObject.get("revision"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + if (!matches2(candidate)) { + return false + } + if (!matches5(candidate)) { + return false + } + return true + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonNull || matches3(candidate) + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + if (!matches4(candidate)) { + return false + } + if (!matches5(candidate)) { + return false + } + return true + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches5(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "1") + } + + private fun matches6(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("revision") || !matches7(jsonObject.get("revision"))) { + return false + } + if (jsonObject.has("second_value") && !matches12(jsonObject.get("second_value"))) { + return false + } + return true + } + + private fun matches7(candidate: JsonElement): kotlin.Boolean { + if (!matches8(candidate)) { + return false + } + if (!matches9(candidate)) { + return false + } + return true + } + + private fun matches8(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonNull || matches4(candidate) + } + + private fun matches9(candidate: JsonElement): kotlin.Boolean { + if (matches10(candidate)) { + return true + } + if (matches11(candidate)) { + return true + } + return false + } + + private fun matches10(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "2") + } + + private fun matches11(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonNull + } + + private fun matches12(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/NumberAlias.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/NumberAlias.kt.golden new file mode 100644 index 000000000..afefcf57a --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/NumberAlias.kt.golden @@ -0,0 +1,7 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +typealias NumberAlias = Int diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/NumericFinite.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/NumericFinite.kt.golden new file mode 100644 index 000000000..4383f9e2d --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/NumericFinite.kt.golden @@ -0,0 +1,139 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(NumericFiniteJsonAdapter::class, nullSafe = false) +sealed class NumericFinite private constructor() { + data class VariantNumber(val value0: Double) : NumericFinite() + + data class VariantAny(val value0: JsonElement) : NumericFinite() +} + +private class NumericFiniteJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): NumericFinite? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && true) { + selected = 1 + } + if (selected == 0) { + return NumericFinite.VariantNumber(element.asDouble) + } + if (selected == 1) { + return NumericFinite.VariantAny(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown NumericFinite union value") + } + + override fun write(writer: JsonWriter, value: NumericFinite?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is NumericFinite.VariantNumber -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is NumericFinite.VariantAny -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!matches1(candidate)) { + return false + } + if (!matches2(candidate)) { + return false + } + if (!matches3(candidate)) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "1.25") + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + if (matches1(candidate)) { + return true + } + if (matches2(candidate)) { + return true + } + return false + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/NumericZero.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/NumericZero.kt.golden new file mode 100644 index 000000000..1abfba367 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/NumericZero.kt.golden @@ -0,0 +1,139 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(NumericZeroJsonAdapter::class, nullSafe = false) +sealed class NumericZero private constructor() { + data class VariantNumber(val value0: Double) : NumericZero() + + data class VariantAny(val value0: JsonElement) : NumericZero() +} + +private class NumericZeroJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): NumericZero? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && true) { + selected = 1 + } + if (selected == 0) { + return NumericZero.VariantNumber(element.asDouble) + } + if (selected == 1) { + return NumericZero.VariantAny(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown NumericZero union value") + } + + override fun write(writer: JsonWriter, value: NumericZero?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is NumericZero.VariantNumber -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is NumericZero.VariantAny -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!matches1(candidate)) { + return false + } + if (!matches2(candidate)) { + return false + } + if (!matches3(candidate)) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "0") + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + if (matches1(candidate)) { + return true + } + if (matches2(candidate)) { + return true + } + return false + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/OpaqueFirst.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/OpaqueFirst.kt.golden new file mode 100644 index 000000000..3ae058d76 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/OpaqueFirst.kt.golden @@ -0,0 +1,220 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(OpaqueFirstJsonAdapter::class, nullSafe = false) +sealed class OpaqueFirst private constructor() { + data class VariantAny(val value0: JsonElement) : OpaqueFirst() + + data class VariantFiniteTwo(val value0: FiniteTwo) : OpaqueFirst() +} + +private class OpaqueFirstJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): OpaqueFirst? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 1 + } + if (selected < 0 && true) { + selected = 0 + } + if (selected == 0) { + return OpaqueFirst.VariantAny(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return OpaqueFirst.VariantFiniteTwo(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown OpaqueFirst union value") + } + + override fun write(writer: JsonWriter, value: OpaqueFirst?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is OpaqueFirst.VariantAny -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is OpaqueFirst.VariantFiniteTwo -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("flags") || !matches1(jsonObject.get("flags"))) { + return false + } + if (!jsonObject.has("revision") || !matches5(jsonObject.get("revision"))) { + return false + } + if (jsonObject.has("second_value") && !matches8(jsonObject.get("second_value"))) { + return false + } + if (!jsonObject.has("values") || !matches9(jsonObject.get("values"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + for (item in candidate.asJsonObject.asMap().values) { + if (!matches2(item)) { + return false + } + } + return true + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + if (!matches3(candidate)) { + return false + } + if (!matches4(candidate)) { + return false + } + return true + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isBoolean + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isBoolean && candidate.asBoolean == true + } + + private fun matches5(candidate: JsonElement): kotlin.Boolean { + if (!matches6(candidate)) { + return false + } + if (!matches7(candidate)) { + return false + } + return true + } + + private fun matches6(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches7(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "2") + } + + private fun matches8(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches9(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonArray) { + return false + } + for (item in candidate.asJsonArray) { + if (!matches10(item)) { + return false + } + } + return true + } + + private fun matches10(candidate: JsonElement): kotlin.Boolean { + if (!matches11(candidate)) { + return false + } + if (!matches12(candidate)) { + return false + } + return true + } + + private fun matches11(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber + } + + private fun matches12(candidate: JsonElement): kotlin.Boolean { + if (matches7(candidate)) { + return true + } + if (matches13(candidate)) { + return true + } + return false + } + + private fun matches13(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "2.5") + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/OpenNested.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/OpenNested.kt.golden new file mode 100644 index 000000000..eed217b13 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/OpenNested.kt.golden @@ -0,0 +1,107 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(OpenNestedJsonAdapter::class, nullSafe = false) +sealed class OpenNested private constructor() { + data class VariantAmbiguousAlias(val value0: AmbiguousAlias) : OpenNested() + + data class VariantAny(val value0: JsonElement) : OpenNested() +} + +private class OpenNestedJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): OpenNested? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && true) { + selected = 1 + } + if (selected == 0) { + return OpenNested.VariantAmbiguousAlias(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return OpenNested.VariantAny(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown OpenNested union value") + } + + override fun write(writer: JsonWriter, value: OpenNested?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is OpenNested.VariantAmbiguousAlias -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is OpenNested.VariantAny -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (matches1(candidate)) { + return true + } + if (matches3(candidate)) { + return true + } + return false + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("first_value") && !matches2(jsonObject.get("first_value"))) { + return false + } + if (!jsonObject.has("wire_name") || !matches2(jsonObject.get("wire_name"))) { + return false + } + return true + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("second_value") && !matches2(jsonObject.get("second_value"))) { + return false + } + if (!jsonObject.has("wire_name") || !matches2(jsonObject.get("wire_name"))) { + return false + } + return true + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/RecursiveChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/RecursiveChoice.kt.golden new file mode 100644 index 000000000..44abb4db1 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/RecursiveChoice.kt.golden @@ -0,0 +1,66 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(RecursiveChoiceJsonAdapter::class, nullSafe = false) +sealed class RecursiveChoice private constructor() { + data class VariantRecursiveChoice(val value0: RecursiveChoice) : RecursiveChoice() + + data class VariantString(val value0: String) : RecursiveChoice() +} + +private class RecursiveChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): RecursiveChoice? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 1 + } + if (selected == 0) { + return RecursiveChoice.VariantRecursiveChoice(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return RecursiveChoice.VariantString(element.asString) + } + throw JsonParseException("unknown RecursiveChoice union value") + } + + override fun write(writer: JsonWriter, value: RecursiveChoice?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is RecursiveChoice.VariantRecursiveChoice -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is RecursiveChoice.VariantString -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/RightPart.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/RightPart.kt.golden new file mode 100644 index 000000000..f2dab6563 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/RightPart.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class RightPart(@field:SerializedName("right_value") + val rightValue: Int) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/ScalarChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/ScalarChoice.kt.golden new file mode 100644 index 000000000..f2fd20bdd --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/ScalarChoice.kt.golden @@ -0,0 +1,212 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(ScalarChoiceJsonAdapter::class, nullSafe = false) +sealed class ScalarChoice private constructor() { + data class VariantInteger(val value0: Int) : ScalarChoice() + + data class VariantNumber(val value0: Double) : ScalarChoice() + + data class VariantBoolean(val value0: Boolean) : ScalarChoice() +} + +private class ScalarChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): ScalarChoice? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches4(element)) { + selected = 1 + } + if (selected < 0 && matches8(element)) { + selected = 2 + } + if (selected == 0) { + return ScalarChoice.VariantInteger(element.asInt) + } + if (selected == 1) { + return ScalarChoice.VariantNumber(element.asDouble) + } + if (selected == 2) { + return ScalarChoice.VariantBoolean(element.asBoolean) + } + throw JsonParseException("unknown ScalarChoice union value") + } + + override fun write(writer: JsonWriter, value: ScalarChoice?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is ScalarChoice.VariantInteger -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is ScalarChoice.VariantNumber -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is ScalarChoice.VariantBoolean -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!matches1(candidate)) { + return false + } + if (!matches2(candidate)) { + return false + } + if (!matches3(candidate)) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "1") + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + if (matches1(candidate)) { + return true + } + if (matches2(candidate)) { + return true + } + return false + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + if (!matches5(candidate)) { + return false + } + if (!matches6(candidate)) { + return false + } + if (!matches7(candidate)) { + return false + } + return true + } + + private fun matches5(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber + } + + private fun matches6(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "2.5") + } + + private fun matches7(candidate: JsonElement): kotlin.Boolean { + if (matches5(candidate)) { + return true + } + if (matches6(candidate)) { + return true + } + return false + } + + private fun matches8(candidate: JsonElement): kotlin.Boolean { + if (!matches9(candidate)) { + return false + } + if (!matches10(candidate)) { + return false + } + if (!matches11(candidate)) { + return false + } + return true + } + + private fun matches9(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isBoolean + } + + private fun matches10(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isBoolean && candidate.asBoolean == true + } + + private fun matches11(candidate: JsonElement): kotlin.Boolean { + if (matches9(candidate)) { + return true + } + if (matches10(candidate)) { + return true + } + return false + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/Tree.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/Tree.kt.golden new file mode 100644 index 000000000..f18ca8173 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/Tree.kt.golden @@ -0,0 +1,217 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(TreeJsonAdapter::class, nullSafe = false) +sealed class Tree private constructor() { + data class VariantBranchOne(val value0: BranchOne) : Tree() + + data class VariantBranchTwo(val value0: BranchTwo) : Tree() +} + +private class TreeJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): Tree? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + val memo = java.util.IdentityHashMap>() + var selected = -1 + if (selected < 0 && matches0(element, memo)) { + selected = 0 + } + if (selected < 0 && matches4(element, memo)) { + selected = 1 + } + if (selected == 0) { + return Tree.VariantBranchOne(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return Tree.VariantBranchTwo(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown Tree union value") + } + + override fun write(writer: JsonWriter, value: Tree?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is Tree.VariantBranchOne -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is Tree.VariantBranchTwo -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement, memo: java.util.IdentityHashMap>): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("children") || !matches1(jsonObject.get("children"), memo)) { + return false + } + if (!jsonObject.has("revision") || !matches9(jsonObject.get("revision"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement, memo: java.util.IdentityHashMap>): kotlin.Boolean { + if (!candidate.isJsonArray) { + return false + } + for (item in candidate.asJsonArray) { + if (!matches2(item, memo)) { + return false + } + } + return true + } + + private fun matches2(candidate: JsonElement, memo: java.util.IdentityHashMap>): kotlin.Boolean { + val results = memo.getOrPut(candidate) { + mutableMapOf() + } + val cached = results[2] + if (cached != null) { + return cached + } + val matched = matches3(candidate, memo) + results[2] = matched + return matched + } + + private fun matches3(candidate: JsonElement, memo: java.util.IdentityHashMap>): kotlin.Boolean { + if (matches0(candidate, memo)) { + return true + } + if (matches4(candidate, memo)) { + return true + } + return false + } + + private fun matches4(candidate: JsonElement, memo: java.util.IdentityHashMap>): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("children") || !matches1(jsonObject.get("children"), memo)) { + return false + } + if (!jsonObject.has("revision") || !matches5(jsonObject.get("revision"))) { + return false + } + if (jsonObject.has("second_value") && !matches8(jsonObject.get("second_value"))) { + return false + } + return true + } + + private fun matches5(candidate: JsonElement): kotlin.Boolean { + if (!matches6(candidate)) { + return false + } + if (!matches7(candidate)) { + return false + } + return true + } + + private fun matches6(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches7(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "2") + } + + private fun matches8(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches9(candidate: JsonElement): kotlin.Boolean { + if (!matches6(candidate)) { + return false + } + if (!matches10(candidate)) { + return false + } + return true + } + + private fun matches10(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "1") + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/VariantRaw.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/VariantRaw.kt.golden new file mode 100644 index 000000000..8fff0ed1f --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/VariantRaw.kt.golden @@ -0,0 +1,97 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(VariantRawJsonAdapter::class, nullSafe = false) +sealed class VariantRaw private constructor() { + data class VariantAlpha(val value0: Alpha) : VariantRaw() + + data class VariantBeta(val value0: Beta) : VariantRaw() +} + +private class VariantRawJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): VariantRaw? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches2(element)) { + selected = 1 + } + if (selected == 0) { + return VariantRaw.VariantAlpha(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return VariantRaw.VariantBeta(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown VariantRaw union value") + } + + override fun write(writer: JsonWriter, value: VariantRaw?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is VariantRaw.VariantAlpha -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is VariantRaw.VariantBeta -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("first_value") && !matches1(jsonObject.get("first_value"))) { + return false + } + if (!jsonObject.has("wire_name") || !matches1(jsonObject.get("wire_name"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("second_value") && !matches1(jsonObject.get("second_value"))) { + return false + } + if (!jsonObject.has("wire_name") || !matches1(jsonObject.get("wire_name"))) { + return false + } + return true + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/Wide.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/Wide.kt.golden new file mode 100644 index 000000000..814951a27 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/Wide.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class Wide(val identifier: Long, @field:SerializedName("second_value") + val secondValue: String? = null) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/WrappedChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/WrappedChoice.kt.golden new file mode 100644 index 000000000..f529ee047 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/WrappedChoice.kt.golden @@ -0,0 +1,180 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(WrappedChoiceJsonAdapter::class, nullSafe = false) +sealed class WrappedChoice private constructor() { + data class VariantWrappedPair(val value0: WrappedPair) : WrappedChoice() + + data class VariantAlpha(val value0: Alpha) : WrappedChoice() +} + +private class WrappedChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): WrappedChoice? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches7(element)) { + selected = 1 + } + if (selected == 0) { + return WrappedChoice.VariantWrappedPair(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return WrappedChoice.VariantAlpha(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown WrappedChoice union value") + } + + override fun write(writer: JsonWriter, value: WrappedChoice?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is WrappedChoice.VariantWrappedPair -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is WrappedChoice.VariantAlpha -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("leftPart") || !matches1(jsonObject.get("leftPart"))) { + return false + } + if (!jsonObject.has("rightPart") || !matches3(jsonObject.get("rightPart"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("left_value") || !matches2(jsonObject.get("left_value"))) { + return false + } + return true + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("right_value") || !matches4(jsonObject.get("right_value"))) { + return false + } + return true + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + if (!matches5(candidate)) { + return false + } + if (!matches6(candidate)) { + return false + } + return true + } + + private fun matches5(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches6(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "2") + } + + private fun matches7(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("first_value") && !matches2(jsonObject.get("first_value"))) { + return false + } + if (!jsonObject.has("wire_name") || !matches2(jsonObject.get("wire_name"))) { + return false + } + return true + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/WrappedPair.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/WrappedPair.kt.golden new file mode 100644 index 000000000..74f240da5 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/models/WrappedPair.kt.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +data class WrappedPair(val leftPart: LeftPart, val rightPart: RightPart) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/runtime/ApiClient.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/runtime/ApiClient.kt.golden new file mode 100644 index 000000000..290c3aadb --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/runtime/ApiClient.kt.golden @@ -0,0 +1,61 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.runtime + +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response + +class ApiClient( + private val baseUrl: String, + private val client: OkHttpClient = OkHttpClient(), + private val authenticator: Authenticator? = null, + private val defaultHeaders: Map = emptyMap(), +) { + fun newRequest( + method: String, + path: String, + query: Map? = null, + body: String? = null, + ): Request { + return buildRequest(method, path, query, body?.toRequestBody("application/json".toMediaType())) + } + + fun newRequestWithBody( + method: String, + path: String, + query: Map? = null, + body: okhttp3.RequestBody?, + ): Request { + return buildRequest(method, path, query, body) + } + + private fun buildRequest( + method: String, + path: String, + query: Map? = null, + body: okhttp3.RequestBody?, + ): Request { + val urlBuilder: HttpUrl.Builder = (baseUrl.trimEnd('/') + path).toHttpUrl().newBuilder() + query?.forEach { (k, v) -> urlBuilder.addQueryParameter(k, v) } + + val builder = Request.Builder() + .url(urlBuilder.build()) + .method(method, body) + + defaultHeaders.forEach { (k, v) -> builder.header(k, v) } + authenticator?.authenticate(builder) + + return builder.build() + } + + fun execute(request: Request): Response { + return client.newCall(request).execute() + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/runtime/ApiException.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/runtime/ApiException.kt.golden new file mode 100644 index 000000000..995bd74a4 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/runtime/ApiException.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.runtime + +open class ApiException( + val statusCode: Int, + val status: String, + val body: String, +) : RuntimeException("API error $statusCode: $status") diff --git a/tests/golden/kotlin/kotlin-okhttp/union-first-match/runtime/Auth.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-first-match/runtime/Auth.kt.golden new file mode 100644 index 000000000..6f6400a20 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-first-match/runtime/Auth.kt.golden @@ -0,0 +1,70 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.runtime + +import okhttp3.Request + +interface Authenticator { + fun authenticate(builder: Request.Builder) +} + +class BearerAuth : Authenticator { + private val token: String? + private val tokenProvider: (() -> String)? + + constructor(token: String) { + this.token = token + this.tokenProvider = null + } + + constructor(tokenProvider: () -> String) { + this.token = null + this.tokenProvider = tokenProvider + } + + override fun authenticate(builder: Request.Builder) { + val t = tokenProvider?.invoke() ?: token!! + builder.header("Authorization", "Bearer $t") + } +} + +class ApiKeyAuth : Authenticator { + private val key: String? + private val keyProvider: (() -> String)? + private val name: String + private val location: ApiKeyLocation + + constructor(key: String, name: String, location: ApiKeyLocation) { + this.key = key + this.keyProvider = null + this.name = name + this.location = location + } + + constructor(keyProvider: () -> String, name: String, location: ApiKeyLocation) { + this.key = null + this.keyProvider = keyProvider + this.name = name + this.location = location + } + + override fun authenticate(builder: Request.Builder) { + val k = keyProvider?.invoke() ?: key!! + when (location) { + ApiKeyLocation.HEADER -> builder.header(name, k) + ApiKeyLocation.QUERY -> { + val url = builder.build().url.newBuilder() + .addQueryParameter(name, k) + .build() + builder.url(url) + } + } + } +} + +enum class ApiKeyLocation { + HEADER, + QUERY, +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/README.md.golden b/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/README.md.golden new file mode 100644 index 000000000..43f007397 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/README.md.golden @@ -0,0 +1,7 @@ +# Java union helper types + +Generated Kotlin SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `java-union-helper-types`. diff --git a/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/build.gradle.kts.golden b/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/build.gradle.kts.golden new file mode 100644 index 000000000..093728ca0 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/build.gradle.kts.golden @@ -0,0 +1,27 @@ +plugins { + kotlin("jvm") version "2.2.21" +} + +group = "com.example.sdk" +version = "1.0.0" + +repositories { + mavenCentral() +} + +dependencies { + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.google.code.gson:gson:2.11.0") +} + +kotlin { + jvmToolchain(17) +} + +sourceSets { + main { + kotlin { + srcDirs("apis", "models", "runtime") + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/models/Boolean.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/models/Boolean.kt.golden new file mode 100644 index 000000000..99208f38c --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/models/Boolean.kt.golden @@ -0,0 +1,7 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union helper types — 1.0.0 + +package com.example.sdk.models + +typealias Boolean = Any diff --git a/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/models/Branch.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/models/Branch.kt.golden new file mode 100644 index 000000000..97b91d75c --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/models/Branch.kt.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union helper types — 1.0.0 + +package com.example.sdk.models + +data class Branch(val children: List) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/models/Integer.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/models/Integer.kt.golden new file mode 100644 index 000000000..995be67bc --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/models/Integer.kt.golden @@ -0,0 +1,7 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union helper types — 1.0.0 + +package com.example.sdk.models + +typealias Integer = Any diff --git a/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/models/Tree.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/models/Tree.kt.golden new file mode 100644 index 000000000..839f70a8c --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/models/Tree.kt.golden @@ -0,0 +1,127 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union helper types — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(TreeJsonAdapter::class, nullSafe = false) +sealed class Tree private constructor() { + data class VariantBranch(val value0: Branch) : Tree() + + data class VariantString(val value0: String) : Tree() +} + +private class TreeJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): Tree? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + val memo = java.util.IdentityHashMap>() + var selected = -1 + if (matches0(element, memo)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union Tree: multiple concrete members match") + } + selected = 0 + } + if (matches4(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union Tree: multiple concrete members match") + } + selected = 1 + } + if (selected == 0) { + return Tree.VariantBranch(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return Tree.VariantString(element.asString) + } + throw JsonParseException("unknown Tree union value") + } + + override fun write(writer: JsonWriter, value: Tree?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is Tree.VariantBranch -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is Tree.VariantString -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement, memo: java.util.IdentityHashMap>): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("children") || !matches1(jsonObject.get("children"), memo)) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement, memo: java.util.IdentityHashMap>): kotlin.Boolean { + if (!candidate.isJsonArray) { + return false + } + for (item in candidate.asJsonArray) { + if (!matches2(item, memo)) { + return false + } + } + return true + } + + private fun matches2(candidate: JsonElement, memo: java.util.IdentityHashMap>): kotlin.Boolean { + val results = memo.getOrPut(candidate) { + mutableMapOf() + } + val cached = results[2] + if (cached != null) { + return cached + } + val matched = matches3(candidate, memo) + results[2] = matched + return matched + } + + private fun matches3(candidate: JsonElement, memo: java.util.IdentityHashMap>): kotlin.Boolean { + if (matches0(candidate, memo)) { + return true + } + if (matches4(candidate)) { + return true + } + return false + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/runtime/ApiClient.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/runtime/ApiClient.kt.golden new file mode 100644 index 000000000..8eaff0db1 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/runtime/ApiClient.kt.golden @@ -0,0 +1,61 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union helper types — 1.0.0 + +package com.example.sdk.runtime + +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response + +class ApiClient( + private val baseUrl: String, + private val client: OkHttpClient = OkHttpClient(), + private val authenticator: Authenticator? = null, + private val defaultHeaders: Map = emptyMap(), +) { + fun newRequest( + method: String, + path: String, + query: Map? = null, + body: String? = null, + ): Request { + return buildRequest(method, path, query, body?.toRequestBody("application/json".toMediaType())) + } + + fun newRequestWithBody( + method: String, + path: String, + query: Map? = null, + body: okhttp3.RequestBody?, + ): Request { + return buildRequest(method, path, query, body) + } + + private fun buildRequest( + method: String, + path: String, + query: Map? = null, + body: okhttp3.RequestBody?, + ): Request { + val urlBuilder: HttpUrl.Builder = (baseUrl.trimEnd('/') + path).toHttpUrl().newBuilder() + query?.forEach { (k, v) -> urlBuilder.addQueryParameter(k, v) } + + val builder = Request.Builder() + .url(urlBuilder.build()) + .method(method, body) + + defaultHeaders.forEach { (k, v) -> builder.header(k, v) } + authenticator?.authenticate(builder) + + return builder.build() + } + + fun execute(request: Request): Response { + return client.newCall(request).execute() + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/runtime/ApiException.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/runtime/ApiException.kt.golden new file mode 100644 index 000000000..6a7ae7924 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/runtime/ApiException.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union helper types — 1.0.0 + +package com.example.sdk.runtime + +open class ApiException( + val statusCode: Int, + val status: String, + val body: String, +) : RuntimeException("API error $statusCode: $status") diff --git a/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/runtime/Auth.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/runtime/Auth.kt.golden new file mode 100644 index 000000000..2430c60c4 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-helper-types-strict/runtime/Auth.kt.golden @@ -0,0 +1,70 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union helper types — 1.0.0 + +package com.example.sdk.runtime + +import okhttp3.Request + +interface Authenticator { + fun authenticate(builder: Request.Builder) +} + +class BearerAuth : Authenticator { + private val token: String? + private val tokenProvider: (() -> String)? + + constructor(token: String) { + this.token = token + this.tokenProvider = null + } + + constructor(tokenProvider: () -> String) { + this.token = null + this.tokenProvider = tokenProvider + } + + override fun authenticate(builder: Request.Builder) { + val t = tokenProvider?.invoke() ?: token!! + builder.header("Authorization", "Bearer $t") + } +} + +class ApiKeyAuth : Authenticator { + private val key: String? + private val keyProvider: (() -> String)? + private val name: String + private val location: ApiKeyLocation + + constructor(key: String, name: String, location: ApiKeyLocation) { + this.key = key + this.keyProvider = null + this.name = name + this.location = location + } + + constructor(keyProvider: () -> String, name: String, location: ApiKeyLocation) { + this.key = null + this.keyProvider = keyProvider + this.name = name + this.location = location + } + + override fun authenticate(builder: Request.Builder) { + val k = keyProvider?.invoke() ?: key!! + when (location) { + ApiKeyLocation.HEADER -> builder.header(name, k) + ApiKeyLocation.QUERY -> { + val url = builder.build().url.newBuilder() + .addQueryParameter(name, k) + .build() + builder.url(url) + } + } + } +} + +enum class ApiKeyLocation { + HEADER, + QUERY, +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-helper-types/README.md.golden b/tests/golden/kotlin/kotlin-okhttp/union-helper-types/README.md.golden new file mode 100644 index 000000000..43f007397 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-helper-types/README.md.golden @@ -0,0 +1,7 @@ +# Java union helper types + +Generated Kotlin SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `java-union-helper-types`. diff --git a/tests/golden/kotlin/kotlin-okhttp/union-helper-types/build.gradle.kts.golden b/tests/golden/kotlin/kotlin-okhttp/union-helper-types/build.gradle.kts.golden new file mode 100644 index 000000000..093728ca0 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-helper-types/build.gradle.kts.golden @@ -0,0 +1,27 @@ +plugins { + kotlin("jvm") version "2.2.21" +} + +group = "com.example.sdk" +version = "1.0.0" + +repositories { + mavenCentral() +} + +dependencies { + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.google.code.gson:gson:2.11.0") +} + +kotlin { + jvmToolchain(17) +} + +sourceSets { + main { + kotlin { + srcDirs("apis", "models", "runtime") + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-helper-types/models/Boolean.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-helper-types/models/Boolean.kt.golden new file mode 100644 index 000000000..99208f38c --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-helper-types/models/Boolean.kt.golden @@ -0,0 +1,7 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union helper types — 1.0.0 + +package com.example.sdk.models + +typealias Boolean = Any diff --git a/tests/golden/kotlin/kotlin-okhttp/union-helper-types/models/Branch.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-helper-types/models/Branch.kt.golden new file mode 100644 index 000000000..97b91d75c --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-helper-types/models/Branch.kt.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union helper types — 1.0.0 + +package com.example.sdk.models + +data class Branch(val children: List) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-helper-types/models/Integer.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-helper-types/models/Integer.kt.golden new file mode 100644 index 000000000..995be67bc --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-helper-types/models/Integer.kt.golden @@ -0,0 +1,7 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union helper types — 1.0.0 + +package com.example.sdk.models + +typealias Integer = Any diff --git a/tests/golden/kotlin/kotlin-okhttp/union-helper-types/models/Tree.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-helper-types/models/Tree.kt.golden new file mode 100644 index 000000000..d3e8cadfd --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-helper-types/models/Tree.kt.golden @@ -0,0 +1,121 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union helper types — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(TreeJsonAdapter::class, nullSafe = false) +sealed class Tree private constructor() { + data class VariantBranch(val value0: Branch) : Tree() + + data class VariantString(val value0: String) : Tree() +} + +private class TreeJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): Tree? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + val memo = java.util.IdentityHashMap>() + var selected = -1 + if (selected < 0 && matches0(element, memo)) { + selected = 0 + } + if (selected < 0 && matches4(element)) { + selected = 1 + } + if (selected == 0) { + return Tree.VariantBranch(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return Tree.VariantString(element.asString) + } + throw JsonParseException("unknown Tree union value") + } + + override fun write(writer: JsonWriter, value: Tree?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is Tree.VariantBranch -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is Tree.VariantString -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement, memo: java.util.IdentityHashMap>): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("children") || !matches1(jsonObject.get("children"), memo)) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement, memo: java.util.IdentityHashMap>): kotlin.Boolean { + if (!candidate.isJsonArray) { + return false + } + for (item in candidate.asJsonArray) { + if (!matches2(item, memo)) { + return false + } + } + return true + } + + private fun matches2(candidate: JsonElement, memo: java.util.IdentityHashMap>): kotlin.Boolean { + val results = memo.getOrPut(candidate) { + mutableMapOf() + } + val cached = results[2] + if (cached != null) { + return cached + } + val matched = matches3(candidate, memo) + results[2] = matched + return matched + } + + private fun matches3(candidate: JsonElement, memo: java.util.IdentityHashMap>): kotlin.Boolean { + if (matches0(candidate, memo)) { + return true + } + if (matches4(candidate)) { + return true + } + return false + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-helper-types/runtime/ApiClient.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-helper-types/runtime/ApiClient.kt.golden new file mode 100644 index 000000000..8eaff0db1 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-helper-types/runtime/ApiClient.kt.golden @@ -0,0 +1,61 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union helper types — 1.0.0 + +package com.example.sdk.runtime + +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response + +class ApiClient( + private val baseUrl: String, + private val client: OkHttpClient = OkHttpClient(), + private val authenticator: Authenticator? = null, + private val defaultHeaders: Map = emptyMap(), +) { + fun newRequest( + method: String, + path: String, + query: Map? = null, + body: String? = null, + ): Request { + return buildRequest(method, path, query, body?.toRequestBody("application/json".toMediaType())) + } + + fun newRequestWithBody( + method: String, + path: String, + query: Map? = null, + body: okhttp3.RequestBody?, + ): Request { + return buildRequest(method, path, query, body) + } + + private fun buildRequest( + method: String, + path: String, + query: Map? = null, + body: okhttp3.RequestBody?, + ): Request { + val urlBuilder: HttpUrl.Builder = (baseUrl.trimEnd('/') + path).toHttpUrl().newBuilder() + query?.forEach { (k, v) -> urlBuilder.addQueryParameter(k, v) } + + val builder = Request.Builder() + .url(urlBuilder.build()) + .method(method, body) + + defaultHeaders.forEach { (k, v) -> builder.header(k, v) } + authenticator?.authenticate(builder) + + return builder.build() + } + + fun execute(request: Request): Response { + return client.newCall(request).execute() + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-helper-types/runtime/ApiException.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-helper-types/runtime/ApiException.kt.golden new file mode 100644 index 000000000..6a7ae7924 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-helper-types/runtime/ApiException.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union helper types — 1.0.0 + +package com.example.sdk.runtime + +open class ApiException( + val statusCode: Int, + val status: String, + val body: String, +) : RuntimeException("API error $statusCode: $status") diff --git a/tests/golden/kotlin/kotlin-okhttp/union-helper-types/runtime/Auth.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-helper-types/runtime/Auth.kt.golden new file mode 100644 index 000000000..2430c60c4 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-helper-types/runtime/Auth.kt.golden @@ -0,0 +1,70 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union helper types — 1.0.0 + +package com.example.sdk.runtime + +import okhttp3.Request + +interface Authenticator { + fun authenticate(builder: Request.Builder) +} + +class BearerAuth : Authenticator { + private val token: String? + private val tokenProvider: (() -> String)? + + constructor(token: String) { + this.token = token + this.tokenProvider = null + } + + constructor(tokenProvider: () -> String) { + this.token = null + this.tokenProvider = tokenProvider + } + + override fun authenticate(builder: Request.Builder) { + val t = tokenProvider?.invoke() ?: token!! + builder.header("Authorization", "Bearer $t") + } +} + +class ApiKeyAuth : Authenticator { + private val key: String? + private val keyProvider: (() -> String)? + private val name: String + private val location: ApiKeyLocation + + constructor(key: String, name: String, location: ApiKeyLocation) { + this.key = key + this.keyProvider = null + this.name = name + this.location = location + } + + constructor(keyProvider: () -> String, name: String, location: ApiKeyLocation) { + this.key = null + this.keyProvider = keyProvider + this.name = name + this.location = location + } + + override fun authenticate(builder: Request.Builder) { + val k = keyProvider?.invoke() ?: key!! + when (location) { + ApiKeyLocation.HEADER -> builder.header(name, k) + ApiKeyLocation.QUERY -> { + val url = builder.build().url.newBuilder() + .addQueryParameter(name, k) + .build() + builder.url(url) + } + } + } +} + +enum class ApiKeyLocation { + HEADER, + QUERY, +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/README.md.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/README.md.golden new file mode 100644 index 000000000..09219a3d7 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/README.md.golden @@ -0,0 +1,7 @@ +# Java union policies + +Generated Kotlin SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `java-union-policies`. diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/apis/SelectionApi.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/apis/SelectionApi.kt.golden new file mode 100644 index 000000000..1dd8e417c --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/apis/SelectionApi.kt.golden @@ -0,0 +1,101 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.apis + +import com.example.sdk.models.* +import com.example.sdk.runtime.ApiClient +import com.example.sdk.runtime.ApiException +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Response + +/** + * GetChoiceResponse carries the response from getChoice. + */ +data class GetChoiceResponse(val statusCode: Int, val raw: Response, + val status200: FiniteChoice? = null) { +} + +sealed interface GetChoiceExceptionDetail { + val statusCode: Int + val headers: okhttp3.Headers + fun rawBody(): ByteArray +} + +class GetChoiceException( + val detail: GetChoiceExceptionDetail, + statusCode: Int, + status: String, + body: String, + val headers: okhttp3.Headers, + rawBodyBytes: ByteArray, +) : ApiException(statusCode, status, body) { + private val rawBodyBytes: ByteArray = rawBodyBytes.copyOf() + fun rawBody(): ByteArray = this.rawBodyBytes.copyOf() +} + +class GetChoiceBadRequest( + override val statusCode: Int, + override val headers: okhttp3.Headers, + rawBodyBytes: ByteArray, +) : GetChoiceExceptionDetail { + private val rawBodyBytes: ByteArray = rawBodyBytes.copyOf() + override fun rawBody(): ByteArray = this.rawBodyBytes.copyOf() + private fun textBody(): String { + val charset = headers["Content-Type"]?.toMediaTypeOrNull()?.charset(Charsets.UTF_8) ?: Charsets.UTF_8 + return rawBodyBytes.toString(charset) + } + fun body(): FiniteChoice = Gson().fromJson(textBody().ifEmpty { "null" }, object : TypeToken() {}.type) +} + +class GetChoiceUnexpected( + override val statusCode: Int, + override val headers: okhttp3.Headers, + rawBodyBytes: ByteArray, +) : GetChoiceExceptionDetail { + private val rawBodyBytes: ByteArray = rawBodyBytes.copyOf() + override fun rawBody(): ByteArray = this.rawBodyBytes.copyOf() + fun body(): ByteArray = rawBody() +} + +/** + * SelectionApi groups operations under the Selection tag. + */ +class SelectionApi(private val client: ApiClient) { + private val gson: Gson = Gson() + + /** + * getChoice GET /choice. + */ + fun getChoice(): GetChoiceResponse { + val path = "/choice" + val request = client.newRequest("GET", path, null, null) + val response = client.execute(request) + + if (!response.isSuccessful) { + val responseBody = response.body + val responseCharset = responseBody?.contentType()?.charset(Charsets.UTF_8) ?: Charsets.UTF_8 + val responseBytes = responseBody?.bytes() ?: ByteArray(0) + val responseText = responseBytes.toString(responseCharset) + val detail: GetChoiceExceptionDetail = when (response.code) { + 400 -> GetChoiceBadRequest(statusCode = response.code, headers = response.headers, rawBodyBytes = responseBytes) + else -> { + GetChoiceUnexpected(statusCode = response.code, headers = response.headers, rawBodyBytes = responseBytes) + } + } + throw GetChoiceException(detail = detail, statusCode = response.code, status = response.message, body = responseText, headers = response.headers, rawBodyBytes = responseBytes) + } + + val responseBytes = response.body?.bytes() ?: ByteArray(0) + val responseText = responseBytes.toString(Charsets.UTF_8) + var status200: FiniteChoice? = null + + if (response.code == 200) { + status200 = gson.fromJson(responseText.ifEmpty { "null" }, object : TypeToken() {}.type) + } + return GetChoiceResponse(statusCode = response.code, raw = response, status200 = status200) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/build.gradle.kts.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/build.gradle.kts.golden new file mode 100644 index 000000000..093728ca0 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/build.gradle.kts.golden @@ -0,0 +1,27 @@ +plugins { + kotlin("jvm") version "2.2.21" +} + +group = "com.example.sdk" +version = "1.0.0" + +repositories { + mavenCentral() +} + +dependencies { + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.google.code.gson:gson:2.11.0") +} + +kotlin { + jvmToolchain(17) +} + +sourceSets { + main { + kotlin { + srcDirs("apis", "models", "runtime") + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/AliasChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/AliasChoice.kt.golden new file mode 100644 index 000000000..dfddf3cae --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/AliasChoice.kt.golden @@ -0,0 +1,47 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(AliasChoiceJsonAdapter::class, nullSafe = false) +sealed class AliasChoice private constructor() { + data class VariantRaw(val value0: JsonElement) : AliasChoice() +} + +private class AliasChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): AliasChoice? { + val element = JsonParser.parseReader(reader) + return AliasChoice.VariantRaw(element) + } + + override fun write(writer: JsonWriter, value: AliasChoice?) { + if (value == null) { + writer.nullValue() + return + } + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, (value as AliasChoice.VariantRaw).value0) + } + finally { + writer.serializeNulls = serializeNulls + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/Alpha.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/Alpha.kt.golden new file mode 100644 index 000000000..d37b3c5bd --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/Alpha.kt.golden @@ -0,0 +1,12 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class Alpha(@field:SerializedName("first_value") + val firstValue: String? = null, @field:SerializedName("wire_name") + val wireName: String) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/Ambiguous.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/Ambiguous.kt.golden new file mode 100644 index 000000000..d930eb4c0 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/Ambiguous.kt.golden @@ -0,0 +1,47 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(AmbiguousJsonAdapter::class, nullSafe = false) +sealed class Ambiguous private constructor() { + data class VariantRaw(val value0: JsonElement) : Ambiguous() +} + +private class AmbiguousJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): Ambiguous? { + val element = JsonParser.parseReader(reader) + return Ambiguous.VariantRaw(element) + } + + override fun write(writer: JsonWriter, value: Ambiguous?) { + if (value == null) { + writer.nullValue() + return + } + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, (value as Ambiguous.VariantRaw).value0) + } + finally { + writer.serializeNulls = serializeNulls + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/AmbiguousAlias.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/AmbiguousAlias.kt.golden new file mode 100644 index 000000000..968f4c07b --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/AmbiguousAlias.kt.golden @@ -0,0 +1,7 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +typealias AmbiguousAlias = Ambiguous diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/Beta.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/Beta.kt.golden new file mode 100644 index 000000000..66aacf6a6 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/Beta.kt.golden @@ -0,0 +1,12 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class Beta(@field:SerializedName("second_value") + val secondValue: String? = null, @field:SerializedName("wire_name") + val wireName: String) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/BinaryChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/BinaryChoice.kt.golden new file mode 100644 index 000000000..a472972e2 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/BinaryChoice.kt.golden @@ -0,0 +1,47 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(BinaryChoiceJsonAdapter::class, nullSafe = false) +sealed class BinaryChoice private constructor() { + data class VariantRaw(val value0: JsonElement) : BinaryChoice() +} + +private class BinaryChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): BinaryChoice? { + val element = JsonParser.parseReader(reader) + return BinaryChoice.VariantRaw(element) + } + + override fun write(writer: JsonWriter, value: BinaryChoice?) { + if (value == null) { + writer.nullValue() + return + } + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, (value as BinaryChoice.VariantRaw).value0) + } + finally { + writer.serializeNulls = serializeNulls + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/BranchOne.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/BranchOne.kt.golden new file mode 100644 index 000000000..609156ffe --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/BranchOne.kt.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +data class BranchOne(val children: List, val revision: Int) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/BranchTwo.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/BranchTwo.kt.golden new file mode 100644 index 000000000..f082326a6 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/BranchTwo.kt.golden @@ -0,0 +1,12 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class BranchTwo(val children: List, val revision: Int, + @field:SerializedName("second_value") + val secondValue: String? = null) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/ChoiceEnvelope.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/ChoiceEnvelope.kt.golden new file mode 100644 index 000000000..ebb34058c --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/ChoiceEnvelope.kt.golden @@ -0,0 +1,13 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class ChoiceEnvelope(@field:SerializedName("by_name") + val byName: Map, val caption: String, val choice: FiniteChoice, + val choices: List, @field:SerializedName("ordinary_null") + val ordinaryNull: String? = null) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/ContainerChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/ContainerChoice.kt.golden new file mode 100644 index 000000000..b6c03d1b6 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/ContainerChoice.kt.golden @@ -0,0 +1,47 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(ContainerChoiceJsonAdapter::class, nullSafe = false) +sealed class ContainerChoice private constructor() { + data class VariantRaw(val value0: JsonElement) : ContainerChoice() +} + +private class ContainerChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): ContainerChoice? { + val element = JsonParser.parseReader(reader) + return ContainerChoice.VariantRaw(element) + } + + override fun write(writer: JsonWriter, value: ContainerChoice?) { + if (value == null) { + writer.nullValue() + return + } + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, (value as ContainerChoice.VariantRaw).value0) + } + finally { + writer.serializeNulls = serializeNulls + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/ContainerOne.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/ContainerOne.kt.golden new file mode 100644 index 000000000..34f5d490b --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/ContainerOne.kt.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +data class ContainerOne(val values: List) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/ContainerTwo.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/ContainerTwo.kt.golden new file mode 100644 index 000000000..f5978fcf8 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/ContainerTwo.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class ContainerTwo(@field:SerializedName("second_value") + val secondValue: String? = null, val values: List) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/ExcludedNull.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/ExcludedNull.kt.golden new file mode 100644 index 000000000..0b04d6f11 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/ExcludedNull.kt.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +data class ExcludedNull(val revision: Int? = null) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/FiniteChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/FiniteChoice.kt.golden new file mode 100644 index 000000000..0d1339cfd --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/FiniteChoice.kt.golden @@ -0,0 +1,47 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(FiniteChoiceJsonAdapter::class, nullSafe = false) +sealed class FiniteChoice private constructor() { + data class VariantRaw(val value0: JsonElement) : FiniteChoice() +} + +private class FiniteChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): FiniteChoice? { + val element = JsonParser.parseReader(reader) + return FiniteChoice.VariantRaw(element) + } + + override fun write(writer: JsonWriter, value: FiniteChoice?) { + if (value == null) { + writer.nullValue() + return + } + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, (value as FiniteChoice.VariantRaw).value0) + } + finally { + writer.serializeNulls = serializeNulls + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/FiniteOne.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/FiniteOne.kt.golden new file mode 100644 index 000000000..c20d52ca6 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/FiniteOne.kt.golden @@ -0,0 +1,12 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class FiniteOne(@field:SerializedName("first_value") + val firstValue: String? = null, val flags: Map, val revision: Int, + val values: List) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/FiniteTwo.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/FiniteTwo.kt.golden new file mode 100644 index 000000000..6cbe6bcd1 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/FiniteTwo.kt.golden @@ -0,0 +1,12 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class FiniteTwo(val flags: Map, val revision: Int, + @field:SerializedName("second_value") + val secondValue: String? = null, val values: List) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/FormatChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/FormatChoice.kt.golden new file mode 100644 index 000000000..889e6e750 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/FormatChoice.kt.golden @@ -0,0 +1,47 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(FormatChoiceJsonAdapter::class, nullSafe = false) +sealed class FormatChoice private constructor() { + data class VariantRaw(val value0: JsonElement) : FormatChoice() +} + +private class FormatChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): FormatChoice? { + val element = JsonParser.parseReader(reader) + return FormatChoice.VariantRaw(element) + } + + override fun write(writer: JsonWriter, value: FormatChoice?) { + if (value == null) { + writer.nullValue() + return + } + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, (value as FormatChoice.VariantRaw).value0) + } + finally { + writer.serializeNulls = serializeNulls + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/FormatFallback.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/FormatFallback.kt.golden new file mode 100644 index 000000000..096d8b169 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/FormatFallback.kt.golden @@ -0,0 +1,47 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(FormatFallbackJsonAdapter::class, nullSafe = false) +sealed class FormatFallback private constructor() { + data class VariantRaw(val value0: JsonElement) : FormatFallback() +} + +private class FormatFallbackJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): FormatFallback? { + val element = JsonParser.parseReader(reader) + return FormatFallback.VariantRaw(element) + } + + override fun write(writer: JsonWriter, value: FormatFallback?) { + if (value == null) { + writer.nullValue() + return + } + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, (value as FormatFallback.VariantRaw).value0) + } + finally { + writer.serializeNulls = serializeNulls + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/IncludedNull.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/IncludedNull.kt.golden new file mode 100644 index 000000000..942b2b97d --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/IncludedNull.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class IncludedNull(val revision: Int? = null, @field:SerializedName("second_value") + val secondValue: String? = null) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/IntegerFallback.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/IntegerFallback.kt.golden new file mode 100644 index 000000000..5cca5d79a --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/IntegerFallback.kt.golden @@ -0,0 +1,47 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(IntegerFallbackJsonAdapter::class, nullSafe = false) +sealed class IntegerFallback private constructor() { + data class VariantRaw(val value0: JsonElement) : IntegerFallback() +} + +private class IntegerFallbackJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): IntegerFallback? { + val element = JsonParser.parseReader(reader) + return IntegerFallback.VariantRaw(element) + } + + override fun write(writer: JsonWriter, value: IntegerFallback?) { + if (value == null) { + writer.nullValue() + return + } + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, (value as IntegerFallback.VariantRaw).value0) + } + finally { + writer.serializeNulls = serializeNulls + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/LargeChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/LargeChoice.kt.golden new file mode 100644 index 000000000..9c9a57adc --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/LargeChoice.kt.golden @@ -0,0 +1,47 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(LargeChoiceJsonAdapter::class, nullSafe = false) +sealed class LargeChoice private constructor() { + data class VariantRaw(val value0: JsonElement) : LargeChoice() +} + +private class LargeChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): LargeChoice? { + val element = JsonParser.parseReader(reader) + return LargeChoice.VariantRaw(element) + } + + override fun write(writer: JsonWriter, value: LargeChoice?) { + if (value == null) { + writer.nullValue() + return + } + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, (value as LargeChoice.VariantRaw).value0) + } + finally { + writer.serializeNulls = serializeNulls + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/LargeOne.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/LargeOne.kt.golden new file mode 100644 index 000000000..4c5f300e5 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/LargeOne.kt.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +data class LargeOne(val revision: Long) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/LargeTwo.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/LargeTwo.kt.golden new file mode 100644 index 000000000..5cc5cb94d --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/LargeTwo.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class LargeTwo(val revision: Long, @field:SerializedName("second_value") + val secondValue: String? = null) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/LeftPart.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/LeftPart.kt.golden new file mode 100644 index 000000000..23121ff0b --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/LeftPart.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class LeftPart(@field:SerializedName("left_value") + val leftValue: String) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/Narrow.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/Narrow.kt.golden new file mode 100644 index 000000000..697c17d40 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/Narrow.kt.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +data class Narrow(val identifier: Int) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/NullableChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/NullableChoice.kt.golden new file mode 100644 index 000000000..6e6f39185 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/NullableChoice.kt.golden @@ -0,0 +1,47 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(NullableChoiceJsonAdapter::class, nullSafe = false) +sealed class NullableChoice private constructor() { + data class VariantRaw(val value0: JsonElement) : NullableChoice() +} + +private class NullableChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): NullableChoice? { + val element = JsonParser.parseReader(reader) + return NullableChoice.VariantRaw(element) + } + + override fun write(writer: JsonWriter, value: NullableChoice?) { + if (value == null) { + writer.nullValue() + return + } + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, (value as NullableChoice.VariantRaw).value0) + } + finally { + writer.serializeNulls = serializeNulls + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/NullableFields.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/NullableFields.kt.golden new file mode 100644 index 000000000..45181e5f7 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/NullableFields.kt.golden @@ -0,0 +1,47 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(NullableFieldsJsonAdapter::class, nullSafe = false) +sealed class NullableFields private constructor() { + data class VariantRaw(val value0: JsonElement) : NullableFields() +} + +private class NullableFieldsJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): NullableFields? { + val element = JsonParser.parseReader(reader) + return NullableFields.VariantRaw(element) + } + + override fun write(writer: JsonWriter, value: NullableFields?) { + if (value == null) { + writer.nullValue() + return + } + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, (value as NullableFields.VariantRaw).value0) + } + finally { + writer.serializeNulls = serializeNulls + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/NumberAlias.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/NumberAlias.kt.golden new file mode 100644 index 000000000..afefcf57a --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/NumberAlias.kt.golden @@ -0,0 +1,7 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +typealias NumberAlias = Int diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/NumericFinite.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/NumericFinite.kt.golden new file mode 100644 index 000000000..a62814de2 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/NumericFinite.kt.golden @@ -0,0 +1,47 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(NumericFiniteJsonAdapter::class, nullSafe = false) +sealed class NumericFinite private constructor() { + data class VariantRaw(val value0: JsonElement) : NumericFinite() +} + +private class NumericFiniteJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): NumericFinite? { + val element = JsonParser.parseReader(reader) + return NumericFinite.VariantRaw(element) + } + + override fun write(writer: JsonWriter, value: NumericFinite?) { + if (value == null) { + writer.nullValue() + return + } + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, (value as NumericFinite.VariantRaw).value0) + } + finally { + writer.serializeNulls = serializeNulls + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/NumericZero.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/NumericZero.kt.golden new file mode 100644 index 000000000..920c8cb02 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/NumericZero.kt.golden @@ -0,0 +1,47 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(NumericZeroJsonAdapter::class, nullSafe = false) +sealed class NumericZero private constructor() { + data class VariantRaw(val value0: JsonElement) : NumericZero() +} + +private class NumericZeroJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): NumericZero? { + val element = JsonParser.parseReader(reader) + return NumericZero.VariantRaw(element) + } + + override fun write(writer: JsonWriter, value: NumericZero?) { + if (value == null) { + writer.nullValue() + return + } + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, (value as NumericZero.VariantRaw).value0) + } + finally { + writer.serializeNulls = serializeNulls + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/OpaqueFirst.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/OpaqueFirst.kt.golden new file mode 100644 index 000000000..244e697a4 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/OpaqueFirst.kt.golden @@ -0,0 +1,47 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(OpaqueFirstJsonAdapter::class, nullSafe = false) +sealed class OpaqueFirst private constructor() { + data class VariantRaw(val value0: JsonElement) : OpaqueFirst() +} + +private class OpaqueFirstJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): OpaqueFirst? { + val element = JsonParser.parseReader(reader) + return OpaqueFirst.VariantRaw(element) + } + + override fun write(writer: JsonWriter, value: OpaqueFirst?) { + if (value == null) { + writer.nullValue() + return + } + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, (value as OpaqueFirst.VariantRaw).value0) + } + finally { + writer.serializeNulls = serializeNulls + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/OpenNested.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/OpenNested.kt.golden new file mode 100644 index 000000000..3dac6d99c --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/OpenNested.kt.golden @@ -0,0 +1,47 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(OpenNestedJsonAdapter::class, nullSafe = false) +sealed class OpenNested private constructor() { + data class VariantRaw(val value0: JsonElement) : OpenNested() +} + +private class OpenNestedJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): OpenNested? { + val element = JsonParser.parseReader(reader) + return OpenNested.VariantRaw(element) + } + + override fun write(writer: JsonWriter, value: OpenNested?) { + if (value == null) { + writer.nullValue() + return + } + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, (value as OpenNested.VariantRaw).value0) + } + finally { + writer.serializeNulls = serializeNulls + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/RecursiveChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/RecursiveChoice.kt.golden new file mode 100644 index 000000000..6018ed46e --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/RecursiveChoice.kt.golden @@ -0,0 +1,47 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(RecursiveChoiceJsonAdapter::class, nullSafe = false) +sealed class RecursiveChoice private constructor() { + data class VariantRaw(val value0: JsonElement) : RecursiveChoice() +} + +private class RecursiveChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): RecursiveChoice? { + val element = JsonParser.parseReader(reader) + return RecursiveChoice.VariantRaw(element) + } + + override fun write(writer: JsonWriter, value: RecursiveChoice?) { + if (value == null) { + writer.nullValue() + return + } + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, (value as RecursiveChoice.VariantRaw).value0) + } + finally { + writer.serializeNulls = serializeNulls + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/RightPart.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/RightPart.kt.golden new file mode 100644 index 000000000..f2dab6563 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/RightPart.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class RightPart(@field:SerializedName("right_value") + val rightValue: Int) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/ScalarChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/ScalarChoice.kt.golden new file mode 100644 index 000000000..52c7a11d1 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/ScalarChoice.kt.golden @@ -0,0 +1,47 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(ScalarChoiceJsonAdapter::class, nullSafe = false) +sealed class ScalarChoice private constructor() { + data class VariantRaw(val value0: JsonElement) : ScalarChoice() +} + +private class ScalarChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): ScalarChoice? { + val element = JsonParser.parseReader(reader) + return ScalarChoice.VariantRaw(element) + } + + override fun write(writer: JsonWriter, value: ScalarChoice?) { + if (value == null) { + writer.nullValue() + return + } + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, (value as ScalarChoice.VariantRaw).value0) + } + finally { + writer.serializeNulls = serializeNulls + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/Tree.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/Tree.kt.golden new file mode 100644 index 000000000..738f6fb91 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/Tree.kt.golden @@ -0,0 +1,47 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(TreeJsonAdapter::class, nullSafe = false) +sealed class Tree private constructor() { + data class VariantRaw(val value0: JsonElement) : Tree() +} + +private class TreeJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): Tree? { + val element = JsonParser.parseReader(reader) + return Tree.VariantRaw(element) + } + + override fun write(writer: JsonWriter, value: Tree?) { + if (value == null) { + writer.nullValue() + return + } + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, (value as Tree.VariantRaw).value0) + } + finally { + writer.serializeNulls = serializeNulls + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/VariantRaw.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/VariantRaw.kt.golden new file mode 100644 index 000000000..37d71db29 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/VariantRaw.kt.golden @@ -0,0 +1,47 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(VariantRawJsonAdapter::class, nullSafe = false) +sealed class VariantRaw private constructor() { + data class VariantRawVariant(val value0: JsonElement) : VariantRaw() +} + +private class VariantRawJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): VariantRaw? { + val element = JsonParser.parseReader(reader) + return VariantRaw.VariantRawVariant(element) + } + + override fun write(writer: JsonWriter, value: VariantRaw?) { + if (value == null) { + writer.nullValue() + return + } + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, (value as VariantRaw.VariantRawVariant).value0) + } + finally { + writer.serializeNulls = serializeNulls + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/Wide.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/Wide.kt.golden new file mode 100644 index 000000000..814951a27 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/Wide.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class Wide(val identifier: Long, @field:SerializedName("second_value") + val secondValue: String? = null) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/WrappedChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/WrappedChoice.kt.golden new file mode 100644 index 000000000..426bba781 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/WrappedChoice.kt.golden @@ -0,0 +1,47 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(WrappedChoiceJsonAdapter::class, nullSafe = false) +sealed class WrappedChoice private constructor() { + data class VariantRaw(val value0: JsonElement) : WrappedChoice() +} + +private class WrappedChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): WrappedChoice? { + val element = JsonParser.parseReader(reader) + return WrappedChoice.VariantRaw(element) + } + + override fun write(writer: JsonWriter, value: WrappedChoice?) { + if (value == null) { + writer.nullValue() + return + } + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, (value as WrappedChoice.VariantRaw).value0) + } + finally { + writer.serializeNulls = serializeNulls + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/models/WrappedPair.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/WrappedPair.kt.golden new file mode 100644 index 000000000..74f240da5 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/models/WrappedPair.kt.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +data class WrappedPair(val leftPart: LeftPart, val rightPart: RightPart) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/runtime/ApiClient.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/runtime/ApiClient.kt.golden new file mode 100644 index 000000000..290c3aadb --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/runtime/ApiClient.kt.golden @@ -0,0 +1,61 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.runtime + +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response + +class ApiClient( + private val baseUrl: String, + private val client: OkHttpClient = OkHttpClient(), + private val authenticator: Authenticator? = null, + private val defaultHeaders: Map = emptyMap(), +) { + fun newRequest( + method: String, + path: String, + query: Map? = null, + body: String? = null, + ): Request { + return buildRequest(method, path, query, body?.toRequestBody("application/json".toMediaType())) + } + + fun newRequestWithBody( + method: String, + path: String, + query: Map? = null, + body: okhttp3.RequestBody?, + ): Request { + return buildRequest(method, path, query, body) + } + + private fun buildRequest( + method: String, + path: String, + query: Map? = null, + body: okhttp3.RequestBody?, + ): Request { + val urlBuilder: HttpUrl.Builder = (baseUrl.trimEnd('/') + path).toHttpUrl().newBuilder() + query?.forEach { (k, v) -> urlBuilder.addQueryParameter(k, v) } + + val builder = Request.Builder() + .url(urlBuilder.build()) + .method(method, body) + + defaultHeaders.forEach { (k, v) -> builder.header(k, v) } + authenticator?.authenticate(builder) + + return builder.build() + } + + fun execute(request: Request): Response { + return client.newCall(request).execute() + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/runtime/ApiException.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/runtime/ApiException.kt.golden new file mode 100644 index 000000000..995bd74a4 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/runtime/ApiException.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.runtime + +open class ApiException( + val statusCode: Int, + val status: String, + val body: String, +) : RuntimeException("API error $statusCode: $status") diff --git a/tests/golden/kotlin/kotlin-okhttp/union-raw/runtime/Auth.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-raw/runtime/Auth.kt.golden new file mode 100644 index 000000000..6f6400a20 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-raw/runtime/Auth.kt.golden @@ -0,0 +1,70 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.runtime + +import okhttp3.Request + +interface Authenticator { + fun authenticate(builder: Request.Builder) +} + +class BearerAuth : Authenticator { + private val token: String? + private val tokenProvider: (() -> String)? + + constructor(token: String) { + this.token = token + this.tokenProvider = null + } + + constructor(tokenProvider: () -> String) { + this.token = null + this.tokenProvider = tokenProvider + } + + override fun authenticate(builder: Request.Builder) { + val t = tokenProvider?.invoke() ?: token!! + builder.header("Authorization", "Bearer $t") + } +} + +class ApiKeyAuth : Authenticator { + private val key: String? + private val keyProvider: (() -> String)? + private val name: String + private val location: ApiKeyLocation + + constructor(key: String, name: String, location: ApiKeyLocation) { + this.key = key + this.keyProvider = null + this.name = name + this.location = location + } + + constructor(keyProvider: () -> String, name: String, location: ApiKeyLocation) { + this.key = null + this.keyProvider = keyProvider + this.name = name + this.location = location + } + + override fun authenticate(builder: Request.Builder) { + val k = keyProvider?.invoke() ?: key!! + when (location) { + ApiKeyLocation.HEADER -> builder.header(name, k) + ApiKeyLocation.QUERY -> { + val url = builder.build().url.newBuilder() + .addQueryParameter(name, k) + .build() + builder.url(url) + } + } + } +} + +enum class ApiKeyLocation { + HEADER, + QUERY, +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-selection/README.md.golden b/tests/golden/kotlin/kotlin-okhttp/union-selection/README.md.golden new file mode 100644 index 000000000..4f6de84ec --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-selection/README.md.golden @@ -0,0 +1,7 @@ +# Union selection contract + +Generated Kotlin SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `union-selection-contract`. diff --git a/tests/golden/kotlin/kotlin-okhttp/union-selection/build.gradle.kts.golden b/tests/golden/kotlin/kotlin-okhttp/union-selection/build.gradle.kts.golden new file mode 100644 index 000000000..093728ca0 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-selection/build.gradle.kts.golden @@ -0,0 +1,27 @@ +plugins { + kotlin("jvm") version "2.2.21" +} + +group = "com.example.sdk" +version = "1.0.0" + +repositories { + mavenCentral() +} + +dependencies { + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.google.code.gson:gson:2.11.0") +} + +kotlin { + jvmToolchain(17) +} + +sourceSets { + main { + kotlin { + srcDirs("apis", "models", "runtime") + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-selection/models/Choice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-selection/models/Choice.kt.golden new file mode 100644 index 000000000..4dce0ed32 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-selection/models/Choice.kt.golden @@ -0,0 +1,172 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Union selection contract — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(ChoiceJsonAdapter::class, nullSafe = false) +sealed class Choice private constructor() { + data class VariantNumericOne(val value0: NumericOne) : Choice() + + data class VariantNumericTwo(val value0: NumericTwo) : Choice() +} + +private class ChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): Choice? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches5(element)) { + selected = 1 + } + if (selected == 0) { + return Choice.VariantNumericOne(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return Choice.VariantNumericTwo(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown Choice union value") + } + + override fun write(writer: JsonWriter, value: Choice?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is Choice.VariantNumericOne -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is Choice.VariantNumericTwo -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("first_value") && !matches1(jsonObject.get("first_value"))) { + return false + } + if (!jsonObject.has("revision") || !matches2(jsonObject.get("revision"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + if (!matches3(candidate)) { + return false + } + if (!matches4(candidate)) { + return false + } + return true + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "1") + } + + private fun matches5(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("revision") || !matches6(jsonObject.get("revision"))) { + return false + } + if (jsonObject.has("second_value") && !matches1(jsonObject.get("second_value"))) { + return false + } + return true + } + + private fun matches6(candidate: JsonElement): kotlin.Boolean { + if (!matches3(candidate)) { + return false + } + if (!matches7(candidate)) { + return false + } + return true + } + + private fun matches7(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "2") + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-selection/models/ClientTool.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-selection/models/ClientTool.kt.golden new file mode 100644 index 000000000..e0b7fd4ac --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-selection/models/ClientTool.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Union selection contract — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class ClientTool(@field:SerializedName("input_schema") + val inputSchema: Map, val name: String) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-selection/models/Envelope.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-selection/models/Envelope.kt.golden new file mode 100644 index 000000000..30f1f6157 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-selection/models/Envelope.kt.golden @@ -0,0 +1,12 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Union selection contract — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class Envelope(val choice: Choice, @field:SerializedName("choice_list") + val choiceList: List, @field:SerializedName("choice_map") + val choiceMap: Map) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-selection/models/NumericOne.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-selection/models/NumericOne.kt.golden new file mode 100644 index 000000000..fe0f06ccc --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-selection/models/NumericOne.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Union selection contract — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class NumericOne(@field:SerializedName("first_value") + val firstValue: String? = null, val revision: Int) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-selection/models/NumericTwo.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-selection/models/NumericTwo.kt.golden new file mode 100644 index 000000000..a86b5e4e7 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-selection/models/NumericTwo.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Union selection contract — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class NumericTwo(val revision: Int, @field:SerializedName("second_value") + val secondValue: String? = null) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-selection/models/Request.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-selection/models/Request.kt.golden new file mode 100644 index 000000000..61becc6b9 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-selection/models/Request.kt.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Union selection contract — 1.0.0 + +package com.example.sdk.models + +data class Request(val tools: List) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-selection/models/RequiredChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-selection/models/RequiredChoice.kt.golden new file mode 100644 index 000000000..d3ca0b4d5 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-selection/models/RequiredChoice.kt.golden @@ -0,0 +1,172 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Union selection contract — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(RequiredChoiceJsonAdapter::class, nullSafe = false) +sealed class RequiredChoice private constructor() { + data class VariantRequiredOne(val value0: RequiredOne) : RequiredChoice() + + data class VariantRequiredTwo(val value0: RequiredTwo) : RequiredChoice() +} + +private class RequiredChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): RequiredChoice? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches5(element)) { + selected = 1 + } + if (selected == 0) { + return RequiredChoice.VariantRequiredOne(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return RequiredChoice.VariantRequiredTwo(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown RequiredChoice union value") + } + + override fun write(writer: JsonWriter, value: RequiredChoice?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is RequiredChoice.VariantRequiredOne -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is RequiredChoice.VariantRequiredTwo -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("first_value") || !matches1(jsonObject.get("first_value"))) { + return false + } + if (!jsonObject.has("revision") || !matches2(jsonObject.get("revision"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + if (!matches3(candidate)) { + return false + } + if (!matches4(candidate)) { + return false + } + return true + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "1") + } + + private fun matches5(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("revision") || !matches6(jsonObject.get("revision"))) { + return false + } + if (!jsonObject.has("second_value") || !matches1(jsonObject.get("second_value"))) { + return false + } + return true + } + + private fun matches6(candidate: JsonElement): kotlin.Boolean { + if (!matches3(candidate)) { + return false + } + if (!matches7(candidate)) { + return false + } + return true + } + + private fun matches7(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "2") + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-selection/models/RequiredOne.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-selection/models/RequiredOne.kt.golden new file mode 100644 index 000000000..d8ce62879 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-selection/models/RequiredOne.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Union selection contract — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class RequiredOne(@field:SerializedName("first_value") + val firstValue: String, val revision: Int) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-selection/models/RequiredTwo.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-selection/models/RequiredTwo.kt.golden new file mode 100644 index 000000000..017dfd8b6 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-selection/models/RequiredTwo.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Union selection contract — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class RequiredTwo(val revision: Int, @field:SerializedName("second_value") + val secondValue: String) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-selection/models/SearchTool.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-selection/models/SearchTool.kt.golden new file mode 100644 index 000000000..8ca4816f6 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-selection/models/SearchTool.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Union selection contract — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class SearchTool(@field:SerializedName("max_uses") + val maxUses: Int? = null, val name: String, val type: String) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-selection/models/Tool.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-selection/models/Tool.kt.golden new file mode 100644 index 000000000..ec620cfa4 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-selection/models/Tool.kt.golden @@ -0,0 +1,167 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Union selection contract — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(ToolJsonAdapter::class, nullSafe = false) +sealed class Tool private constructor() { + data class VariantClientTool(val value0: ClientTool) : Tool() + + data class VariantSearchTool(val value0: SearchTool) : Tool() +} + +private class ToolJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): Tool? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (selected < 0 && matches0(element)) { + selected = 0 + } + if (selected < 0 && matches3(element)) { + selected = 1 + } + if (selected == 0) { + return Tool.VariantClientTool(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return Tool.VariantSearchTool(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown Tool union value") + } + + override fun write(writer: JsonWriter, value: Tool?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is Tool.VariantClientTool -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is Tool.VariantSearchTool -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("input_schema") || !matches1(jsonObject.get("input_schema"))) { + return false + } + if (!jsonObject.has("name") || !matches2(jsonObject.get("name"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + for (item in candidate.asJsonObject.asMap().values) { + if (!true) { + return false + } + } + return true + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("max_uses") && !matches4(jsonObject.get("max_uses"))) { + return false + } + if (!jsonObject.has("name") || !matches5(jsonObject.get("name"))) { + return false + } + if (!jsonObject.has("type") || !matches6(jsonObject.get("type"))) { + return false + } + return true + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches5(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString && candidate.asString == "web_search" + } + + private fun matches6(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString && candidate.asString == "web_search_20250305" + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-selection/runtime/ApiClient.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-selection/runtime/ApiClient.kt.golden new file mode 100644 index 000000000..64e3ee4a8 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-selection/runtime/ApiClient.kt.golden @@ -0,0 +1,61 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Union selection contract — 1.0.0 + +package com.example.sdk.runtime + +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response + +class ApiClient( + private val baseUrl: String, + private val client: OkHttpClient = OkHttpClient(), + private val authenticator: Authenticator? = null, + private val defaultHeaders: Map = emptyMap(), +) { + fun newRequest( + method: String, + path: String, + query: Map? = null, + body: String? = null, + ): Request { + return buildRequest(method, path, query, body?.toRequestBody("application/json".toMediaType())) + } + + fun newRequestWithBody( + method: String, + path: String, + query: Map? = null, + body: okhttp3.RequestBody?, + ): Request { + return buildRequest(method, path, query, body) + } + + private fun buildRequest( + method: String, + path: String, + query: Map? = null, + body: okhttp3.RequestBody?, + ): Request { + val urlBuilder: HttpUrl.Builder = (baseUrl.trimEnd('/') + path).toHttpUrl().newBuilder() + query?.forEach { (k, v) -> urlBuilder.addQueryParameter(k, v) } + + val builder = Request.Builder() + .url(urlBuilder.build()) + .method(method, body) + + defaultHeaders.forEach { (k, v) -> builder.header(k, v) } + authenticator?.authenticate(builder) + + return builder.build() + } + + fun execute(request: Request): Response { + return client.newCall(request).execute() + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-selection/runtime/ApiException.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-selection/runtime/ApiException.kt.golden new file mode 100644 index 000000000..eb61d835b --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-selection/runtime/ApiException.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Union selection contract — 1.0.0 + +package com.example.sdk.runtime + +open class ApiException( + val statusCode: Int, + val status: String, + val body: String, +) : RuntimeException("API error $statusCode: $status") diff --git a/tests/golden/kotlin/kotlin-okhttp/union-selection/runtime/Auth.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-selection/runtime/Auth.kt.golden new file mode 100644 index 000000000..d60dc55b1 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-selection/runtime/Auth.kt.golden @@ -0,0 +1,70 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Union selection contract — 1.0.0 + +package com.example.sdk.runtime + +import okhttp3.Request + +interface Authenticator { + fun authenticate(builder: Request.Builder) +} + +class BearerAuth : Authenticator { + private val token: String? + private val tokenProvider: (() -> String)? + + constructor(token: String) { + this.token = token + this.tokenProvider = null + } + + constructor(tokenProvider: () -> String) { + this.token = null + this.tokenProvider = tokenProvider + } + + override fun authenticate(builder: Request.Builder) { + val t = tokenProvider?.invoke() ?: token!! + builder.header("Authorization", "Bearer $t") + } +} + +class ApiKeyAuth : Authenticator { + private val key: String? + private val keyProvider: (() -> String)? + private val name: String + private val location: ApiKeyLocation + + constructor(key: String, name: String, location: ApiKeyLocation) { + this.key = key + this.keyProvider = null + this.name = name + this.location = location + } + + constructor(keyProvider: () -> String, name: String, location: ApiKeyLocation) { + this.key = null + this.keyProvider = keyProvider + this.name = name + this.location = location + } + + override fun authenticate(builder: Request.Builder) { + val k = keyProvider?.invoke() ?: key!! + when (location) { + ApiKeyLocation.HEADER -> builder.header(name, k) + ApiKeyLocation.QUERY -> { + val url = builder.build().url.newBuilder() + .addQueryParameter(name, k) + .build() + builder.url(url) + } + } + } +} + +enum class ApiKeyLocation { + HEADER, + QUERY, +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/README.md.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/README.md.golden new file mode 100644 index 000000000..09219a3d7 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/README.md.golden @@ -0,0 +1,7 @@ +# Java union policies + +Generated Kotlin SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `java-union-policies`. diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/apis/SelectionApi.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/apis/SelectionApi.kt.golden new file mode 100644 index 000000000..1dd8e417c --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/apis/SelectionApi.kt.golden @@ -0,0 +1,101 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.apis + +import com.example.sdk.models.* +import com.example.sdk.runtime.ApiClient +import com.example.sdk.runtime.ApiException +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Response + +/** + * GetChoiceResponse carries the response from getChoice. + */ +data class GetChoiceResponse(val statusCode: Int, val raw: Response, + val status200: FiniteChoice? = null) { +} + +sealed interface GetChoiceExceptionDetail { + val statusCode: Int + val headers: okhttp3.Headers + fun rawBody(): ByteArray +} + +class GetChoiceException( + val detail: GetChoiceExceptionDetail, + statusCode: Int, + status: String, + body: String, + val headers: okhttp3.Headers, + rawBodyBytes: ByteArray, +) : ApiException(statusCode, status, body) { + private val rawBodyBytes: ByteArray = rawBodyBytes.copyOf() + fun rawBody(): ByteArray = this.rawBodyBytes.copyOf() +} + +class GetChoiceBadRequest( + override val statusCode: Int, + override val headers: okhttp3.Headers, + rawBodyBytes: ByteArray, +) : GetChoiceExceptionDetail { + private val rawBodyBytes: ByteArray = rawBodyBytes.copyOf() + override fun rawBody(): ByteArray = this.rawBodyBytes.copyOf() + private fun textBody(): String { + val charset = headers["Content-Type"]?.toMediaTypeOrNull()?.charset(Charsets.UTF_8) ?: Charsets.UTF_8 + return rawBodyBytes.toString(charset) + } + fun body(): FiniteChoice = Gson().fromJson(textBody().ifEmpty { "null" }, object : TypeToken() {}.type) +} + +class GetChoiceUnexpected( + override val statusCode: Int, + override val headers: okhttp3.Headers, + rawBodyBytes: ByteArray, +) : GetChoiceExceptionDetail { + private val rawBodyBytes: ByteArray = rawBodyBytes.copyOf() + override fun rawBody(): ByteArray = this.rawBodyBytes.copyOf() + fun body(): ByteArray = rawBody() +} + +/** + * SelectionApi groups operations under the Selection tag. + */ +class SelectionApi(private val client: ApiClient) { + private val gson: Gson = Gson() + + /** + * getChoice GET /choice. + */ + fun getChoice(): GetChoiceResponse { + val path = "/choice" + val request = client.newRequest("GET", path, null, null) + val response = client.execute(request) + + if (!response.isSuccessful) { + val responseBody = response.body + val responseCharset = responseBody?.contentType()?.charset(Charsets.UTF_8) ?: Charsets.UTF_8 + val responseBytes = responseBody?.bytes() ?: ByteArray(0) + val responseText = responseBytes.toString(responseCharset) + val detail: GetChoiceExceptionDetail = when (response.code) { + 400 -> GetChoiceBadRequest(statusCode = response.code, headers = response.headers, rawBodyBytes = responseBytes) + else -> { + GetChoiceUnexpected(statusCode = response.code, headers = response.headers, rawBodyBytes = responseBytes) + } + } + throw GetChoiceException(detail = detail, statusCode = response.code, status = response.message, body = responseText, headers = response.headers, rawBodyBytes = responseBytes) + } + + val responseBytes = response.body?.bytes() ?: ByteArray(0) + val responseText = responseBytes.toString(Charsets.UTF_8) + var status200: FiniteChoice? = null + + if (response.code == 200) { + status200 = gson.fromJson(responseText.ifEmpty { "null" }, object : TypeToken() {}.type) + } + return GetChoiceResponse(statusCode = response.code, raw = response, status200 = status200) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/build.gradle.kts.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/build.gradle.kts.golden new file mode 100644 index 000000000..093728ca0 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/build.gradle.kts.golden @@ -0,0 +1,27 @@ +plugins { + kotlin("jvm") version "2.2.21" +} + +group = "com.example.sdk" +version = "1.0.0" + +repositories { + mavenCentral() +} + +dependencies { + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.google.code.gson:gson:2.11.0") +} + +kotlin { + jvmToolchain(17) +} + +sourceSets { + main { + kotlin { + srcDirs("apis", "models", "runtime") + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/AliasChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/AliasChoice.kt.golden new file mode 100644 index 000000000..6668ab1df --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/AliasChoice.kt.golden @@ -0,0 +1,136 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(AliasChoiceJsonAdapter::class, nullSafe = false) +sealed class AliasChoice private constructor() { + data class VariantNumberAlias(val value0: NumberAlias) : AliasChoice() + + data class VariantAlpha(val value0: Alpha) : AliasChoice() +} + +private class AliasChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): AliasChoice? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (matches0(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union AliasChoice: multiple concrete members match") + } + selected = 0 + } + if (matches1(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union AliasChoice: multiple concrete members match") + } + selected = 1 + } + if (selected == 0) { + return AliasChoice.VariantNumberAlias(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return AliasChoice.VariantAlpha(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown AliasChoice union value") + } + + override fun write(writer: JsonWriter, value: AliasChoice?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is AliasChoice.VariantNumberAlias -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is AliasChoice.VariantAlpha -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("first_value") && !matches2(jsonObject.get("first_value"))) { + return false + } + if (!jsonObject.has("wire_name") || !matches2(jsonObject.get("wire_name"))) { + return false + } + return true + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/Alpha.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/Alpha.kt.golden new file mode 100644 index 000000000..d37b3c5bd --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/Alpha.kt.golden @@ -0,0 +1,12 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class Alpha(@field:SerializedName("first_value") + val firstValue: String? = null, @field:SerializedName("wire_name") + val wireName: String) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/Ambiguous.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/Ambiguous.kt.golden new file mode 100644 index 000000000..a2d0f8b2c --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/Ambiguous.kt.golden @@ -0,0 +1,103 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(AmbiguousJsonAdapter::class, nullSafe = false) +sealed class Ambiguous private constructor() { + data class VariantAlpha(val value0: Alpha) : Ambiguous() + + data class VariantBeta(val value0: Beta) : Ambiguous() +} + +private class AmbiguousJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): Ambiguous? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (matches0(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union Ambiguous: multiple concrete members match") + } + selected = 0 + } + if (matches2(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union Ambiguous: multiple concrete members match") + } + selected = 1 + } + if (selected == 0) { + return Ambiguous.VariantAlpha(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return Ambiguous.VariantBeta(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown Ambiguous union value") + } + + override fun write(writer: JsonWriter, value: Ambiguous?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is Ambiguous.VariantAlpha -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is Ambiguous.VariantBeta -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("first_value") && !matches1(jsonObject.get("first_value"))) { + return false + } + if (!jsonObject.has("wire_name") || !matches1(jsonObject.get("wire_name"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("second_value") && !matches1(jsonObject.get("second_value"))) { + return false + } + if (!jsonObject.has("wire_name") || !matches1(jsonObject.get("wire_name"))) { + return false + } + return true + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/AmbiguousAlias.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/AmbiguousAlias.kt.golden new file mode 100644 index 000000000..968f4c07b --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/AmbiguousAlias.kt.golden @@ -0,0 +1,7 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +typealias AmbiguousAlias = Ambiguous diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/Beta.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/Beta.kt.golden new file mode 100644 index 000000000..66aacf6a6 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/Beta.kt.golden @@ -0,0 +1,12 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class Beta(@field:SerializedName("second_value") + val secondValue: String? = null, @field:SerializedName("wire_name") + val wireName: String) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/BinaryChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/BinaryChoice.kt.golden new file mode 100644 index 000000000..48bc7e448 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/BinaryChoice.kt.golden @@ -0,0 +1,130 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(BinaryChoiceJsonAdapter::class, nullSafe = false) +sealed class BinaryChoice private constructor() { + data class VariantBinary(val value0: ByteArray) : BinaryChoice() + + data class VariantString(val value0: String) : BinaryChoice() +} + +private class BinaryChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): BinaryChoice? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (matches0(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union BinaryChoice: multiple concrete members match") + } + selected = 0 + } + if (matches1(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union BinaryChoice: multiple concrete members match") + } + selected = 1 + } + if (selected == 0) { + return BinaryChoice.VariantBinary(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return BinaryChoice.VariantString(element.asString) + } + throw JsonParseException("unknown BinaryChoice union value") + } + + override fun write(writer: JsonWriter, value: BinaryChoice?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is BinaryChoice.VariantBinary -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is BinaryChoice.VariantString -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonArray) { + return false + } + for (item in candidate.asJsonArray) { + if (!isBinaryNumber(item)) { + return false + } + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/BranchOne.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/BranchOne.kt.golden new file mode 100644 index 000000000..609156ffe --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/BranchOne.kt.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +data class BranchOne(val children: List, val revision: Int) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/BranchTwo.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/BranchTwo.kt.golden new file mode 100644 index 000000000..f082326a6 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/BranchTwo.kt.golden @@ -0,0 +1,12 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class BranchTwo(val children: List, val revision: Int, + @field:SerializedName("second_value") + val secondValue: String? = null) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/ChoiceEnvelope.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/ChoiceEnvelope.kt.golden new file mode 100644 index 000000000..ebb34058c --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/ChoiceEnvelope.kt.golden @@ -0,0 +1,13 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class ChoiceEnvelope(@field:SerializedName("by_name") + val byName: Map, val caption: String, val choice: FiniteChoice, + val choices: List, @field:SerializedName("ordinary_null") + val ordinaryNull: String? = null) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/ContainerChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/ContainerChoice.kt.golden new file mode 100644 index 000000000..c46f87f6c --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/ContainerChoice.kt.golden @@ -0,0 +1,199 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(ContainerChoiceJsonAdapter::class, nullSafe = false) +sealed class ContainerChoice private constructor() { + data class VariantContainerOne(val value0: ContainerOne) : ContainerChoice() + + data class VariantContainerTwo(val value0: ContainerTwo) : ContainerChoice() +} + +private class ContainerChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): ContainerChoice? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (matches0(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union ContainerChoice: multiple concrete members match") + } + selected = 0 + } + if (matches5(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union ContainerChoice: multiple concrete members match") + } + selected = 1 + } + if (selected == 0) { + return ContainerChoice.VariantContainerOne(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return ContainerChoice.VariantContainerTwo(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown ContainerChoice union value") + } + + override fun write(writer: JsonWriter, value: ContainerChoice?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is ContainerChoice.VariantContainerOne -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is ContainerChoice.VariantContainerTwo -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("values") || !matches1(jsonObject.get("values"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonArray) { + return false + } + for (item in candidate.asJsonArray) { + if (!matches2(item)) { + return false + } + } + return true + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + if (!matches3(candidate)) { + return false + } + if (!matches4(candidate)) { + return false + } + return true + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "1") + } + + private fun matches5(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("second_value") && !matches6(jsonObject.get("second_value"))) { + return false + } + if (!jsonObject.has("values") || !matches7(jsonObject.get("values"))) { + return false + } + return true + } + + private fun matches6(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches7(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonArray) { + return false + } + for (item in candidate.asJsonArray) { + if (!matches8(item)) { + return false + } + } + return true + } + + private fun matches8(candidate: JsonElement): kotlin.Boolean { + if (!matches3(candidate)) { + return false + } + if (!matches9(candidate)) { + return false + } + return true + } + + private fun matches9(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "2") + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/ContainerOne.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/ContainerOne.kt.golden new file mode 100644 index 000000000..34f5d490b --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/ContainerOne.kt.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +data class ContainerOne(val values: List) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/ContainerTwo.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/ContainerTwo.kt.golden new file mode 100644 index 000000000..f5978fcf8 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/ContainerTwo.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class ContainerTwo(@field:SerializedName("second_value") + val secondValue: String? = null, val values: List) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/ExcludedNull.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/ExcludedNull.kt.golden new file mode 100644 index 000000000..0b04d6f11 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/ExcludedNull.kt.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +data class ExcludedNull(val revision: Int? = null) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/FiniteChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/FiniteChoice.kt.golden new file mode 100644 index 000000000..028e8daf6 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/FiniteChoice.kt.golden @@ -0,0 +1,322 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(FiniteChoiceJsonAdapter::class, nullSafe = false) +sealed class FiniteChoice private constructor() { + data class VariantFiniteOne(val value0: FiniteOne) : FiniteChoice() + + data class VariantFiniteTwo(val value0: FiniteTwo) : FiniteChoice() +} + +private class FiniteChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): FiniteChoice? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (matches0(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union FiniteChoice: multiple concrete members match") + } + selected = 0 + } + if (matches14(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union FiniteChoice: multiple concrete members match") + } + selected = 1 + } + if (selected == 0) { + return FiniteChoice.VariantFiniteOne(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return FiniteChoice.VariantFiniteTwo(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown FiniteChoice union value") + } + + override fun write(writer: JsonWriter, value: FiniteChoice?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is FiniteChoice.VariantFiniteOne -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is FiniteChoice.VariantFiniteTwo -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("first_value") && !matches1(jsonObject.get("first_value"))) { + return false + } + if (!jsonObject.has("flags") || !matches2(jsonObject.get("flags"))) { + return false + } + if (!jsonObject.has("revision") || !matches6(jsonObject.get("revision"))) { + return false + } + if (!jsonObject.has("values") || !matches9(jsonObject.get("values"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + for (item in candidate.asJsonObject.asMap().values) { + if (!matches3(item)) { + return false + } + } + return true + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + if (!matches4(candidate)) { + return false + } + if (!matches5(candidate)) { + return false + } + return true + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isBoolean + } + + private fun matches5(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isBoolean && candidate.asBoolean == false + } + + private fun matches6(candidate: JsonElement): kotlin.Boolean { + if (!matches7(candidate)) { + return false + } + if (!matches8(candidate)) { + return false + } + return true + } + + private fun matches7(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches8(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "1") + } + + private fun matches9(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonArray) { + return false + } + for (item in candidate.asJsonArray) { + if (!matches10(item)) { + return false + } + } + return true + } + + private fun matches10(candidate: JsonElement): kotlin.Boolean { + if (!matches11(candidate)) { + return false + } + if (!matches12(candidate)) { + return false + } + return true + } + + private fun matches11(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber + } + + private fun matches12(candidate: JsonElement): kotlin.Boolean { + if (matches8(candidate)) { + return true + } + if (matches13(candidate)) { + return true + } + return false + } + + private fun matches13(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "1.5") + } + + private fun matches14(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("flags") || !matches15(jsonObject.get("flags"))) { + return false + } + if (!jsonObject.has("revision") || !matches18(jsonObject.get("revision"))) { + return false + } + if (jsonObject.has("second_value") && !matches1(jsonObject.get("second_value"))) { + return false + } + if (!jsonObject.has("values") || !matches20(jsonObject.get("values"))) { + return false + } + return true + } + + private fun matches15(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + for (item in candidate.asJsonObject.asMap().values) { + if (!matches16(item)) { + return false + } + } + return true + } + + private fun matches16(candidate: JsonElement): kotlin.Boolean { + if (!matches4(candidate)) { + return false + } + if (!matches17(candidate)) { + return false + } + return true + } + + private fun matches17(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isBoolean && candidate.asBoolean == true + } + + private fun matches18(candidate: JsonElement): kotlin.Boolean { + if (!matches7(candidate)) { + return false + } + if (!matches19(candidate)) { + return false + } + return true + } + + private fun matches19(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "2") + } + + private fun matches20(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonArray) { + return false + } + for (item in candidate.asJsonArray) { + if (!matches21(item)) { + return false + } + } + return true + } + + private fun matches21(candidate: JsonElement): kotlin.Boolean { + if (!matches11(candidate)) { + return false + } + if (!matches22(candidate)) { + return false + } + return true + } + + private fun matches22(candidate: JsonElement): kotlin.Boolean { + if (matches19(candidate)) { + return true + } + if (matches23(candidate)) { + return true + } + return false + } + + private fun matches23(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "2.5") + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/FiniteOne.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/FiniteOne.kt.golden new file mode 100644 index 000000000..c20d52ca6 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/FiniteOne.kt.golden @@ -0,0 +1,12 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class FiniteOne(@field:SerializedName("first_value") + val firstValue: String? = null, val flags: Map, val revision: Int, + val values: List) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/FiniteTwo.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/FiniteTwo.kt.golden new file mode 100644 index 000000000..6cbe6bcd1 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/FiniteTwo.kt.golden @@ -0,0 +1,12 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class FiniteTwo(val flags: Map, val revision: Int, + @field:SerializedName("second_value") + val secondValue: String? = null, val values: List) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/FormatChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/FormatChoice.kt.golden new file mode 100644 index 000000000..30cd4e146 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/FormatChoice.kt.golden @@ -0,0 +1,147 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(FormatChoiceJsonAdapter::class, nullSafe = false) +sealed class FormatChoice private constructor() { + data class VariantNarrow(val value0: Narrow) : FormatChoice() + + data class VariantWide(val value0: Wide) : FormatChoice() +} + +private class FormatChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): FormatChoice? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (matches0(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union FormatChoice: multiple concrete members match") + } + selected = 0 + } + if (matches2(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union FormatChoice: multiple concrete members match") + } + selected = 1 + } + if (selected == 0) { + return FormatChoice.VariantNarrow(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return FormatChoice.VariantWide(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown FormatChoice union value") + } + + override fun write(writer: JsonWriter, value: FormatChoice?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is FormatChoice.VariantNarrow -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is FormatChoice.VariantWide -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("identifier") || !matches1(jsonObject.get("identifier"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("identifier") || !matches1(jsonObject.get("identifier"))) { + return false + } + if (jsonObject.has("second_value") && !matches3(jsonObject.get("second_value"))) { + return false + } + return true + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/FormatFallback.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/FormatFallback.kt.golden new file mode 100644 index 000000000..cff0d36b7 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/FormatFallback.kt.golden @@ -0,0 +1,126 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(FormatFallbackJsonAdapter::class, nullSafe = false) +sealed class FormatFallback private constructor() { + data class VariantNarrow(val value0: Narrow) : FormatFallback() + + data class VariantAny(val value0: JsonElement) : FormatFallback() +} + +private class FormatFallbackJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): FormatFallback? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (matches0(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union FormatFallback: multiple concrete members match") + } + selected = 0 + } + if (selected < 0 && true) { + selected = 1 + } + if (selected == 0) { + return FormatFallback.VariantNarrow(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return FormatFallback.VariantAny(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown FormatFallback union value") + } + + override fun write(writer: JsonWriter, value: FormatFallback?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is FormatFallback.VariantNarrow -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is FormatFallback.VariantAny -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("identifier") || !matches1(jsonObject.get("identifier"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/IncludedNull.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/IncludedNull.kt.golden new file mode 100644 index 000000000..942b2b97d --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/IncludedNull.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class IncludedNull(val revision: Int? = null, @field:SerializedName("second_value") + val secondValue: String? = null) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/IntegerFallback.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/IntegerFallback.kt.golden new file mode 100644 index 000000000..cf87b331d --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/IntegerFallback.kt.golden @@ -0,0 +1,115 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(IntegerFallbackJsonAdapter::class, nullSafe = false) +sealed class IntegerFallback private constructor() { + data class VariantInteger(val value0: Int) : IntegerFallback() + + data class VariantAny(val value0: JsonElement) : IntegerFallback() +} + +private class IntegerFallbackJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): IntegerFallback? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (matches0(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union IntegerFallback: multiple concrete members match") + } + selected = 0 + } + if (selected < 0 && true) { + selected = 1 + } + if (selected == 0) { + return IntegerFallback.VariantInteger(element.asInt) + } + if (selected == 1) { + return IntegerFallback.VariantAny(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown IntegerFallback union value") + } + + override fun write(writer: JsonWriter, value: IntegerFallback?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is IntegerFallback.VariantInteger -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is IntegerFallback.VariantAny -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/LargeChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/LargeChoice.kt.golden new file mode 100644 index 000000000..e66c1b518 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/LargeChoice.kt.golden @@ -0,0 +1,175 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(LargeChoiceJsonAdapter::class, nullSafe = false) +sealed class LargeChoice private constructor() { + data class VariantLargeOne(val value0: LargeOne) : LargeChoice() + + data class VariantLargeTwo(val value0: LargeTwo) : LargeChoice() +} + +private class LargeChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): LargeChoice? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (matches0(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union LargeChoice: multiple concrete members match") + } + selected = 0 + } + if (matches4(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union LargeChoice: multiple concrete members match") + } + selected = 1 + } + if (selected == 0) { + return LargeChoice.VariantLargeOne(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return LargeChoice.VariantLargeTwo(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown LargeChoice union value") + } + + override fun write(writer: JsonWriter, value: LargeChoice?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is LargeChoice.VariantLargeOne -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is LargeChoice.VariantLargeTwo -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("revision") || !matches1(jsonObject.get("revision"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + if (!matches2(candidate)) { + return false + } + if (!matches3(candidate)) { + return false + } + return true + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "9007199254740992") + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("revision") || !matches5(jsonObject.get("revision"))) { + return false + } + if (jsonObject.has("second_value") && !matches7(jsonObject.get("second_value"))) { + return false + } + return true + } + + private fun matches5(candidate: JsonElement): kotlin.Boolean { + if (!matches2(candidate)) { + return false + } + if (!matches6(candidate)) { + return false + } + return true + } + + private fun matches6(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "9007199254740993") + } + + private fun matches7(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/LargeOne.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/LargeOne.kt.golden new file mode 100644 index 000000000..4c5f300e5 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/LargeOne.kt.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +data class LargeOne(val revision: Long) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/LargeTwo.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/LargeTwo.kt.golden new file mode 100644 index 000000000..5cc5cb94d --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/LargeTwo.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class LargeTwo(val revision: Long, @field:SerializedName("second_value") + val secondValue: String? = null) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/LeftPart.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/LeftPart.kt.golden new file mode 100644 index 000000000..23121ff0b --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/LeftPart.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class LeftPart(@field:SerializedName("left_value") + val leftValue: String) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/Narrow.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/Narrow.kt.golden new file mode 100644 index 000000000..697c17d40 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/Narrow.kt.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +data class Narrow(val identifier: Int) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/NullableChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/NullableChoice.kt.golden new file mode 100644 index 000000000..ef48d8834 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/NullableChoice.kt.golden @@ -0,0 +1,83 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(NullableChoiceJsonAdapter::class, nullSafe = false) +sealed class NullableChoice private constructor() { + data class VariantAlpha(val value0: Alpha) : NullableChoice() + + /** + * The union's null value. + */ + data object VariantNull : NullableChoice() +} + +private class NullableChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): NullableChoice? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return NullableChoice.VariantNull + } + var selected = -1 + if (matches0(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union NullableChoice: multiple concrete members match") + } + selected = 0 + } + if (selected == 0) { + return NullableChoice.VariantAlpha(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown NullableChoice union value") + } + + override fun write(writer: JsonWriter, value: NullableChoice?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is NullableChoice.VariantAlpha -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is NullableChoice.VariantNull -> { + writer.nullValue() + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("first_value") && !matches1(jsonObject.get("first_value"))) { + return false + } + if (!jsonObject.has("wire_name") || !matches1(jsonObject.get("wire_name"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/NullableFields.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/NullableFields.kt.golden new file mode 100644 index 000000000..46ed97dd0 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/NullableFields.kt.golden @@ -0,0 +1,207 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(NullableFieldsJsonAdapter::class, nullSafe = false) +sealed class NullableFields private constructor() { + data class VariantExcludedNull(val value0: ExcludedNull) : NullableFields() + + data class VariantIncludedNull(val value0: IncludedNull) : NullableFields() +} + +private class NullableFieldsJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): NullableFields? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (matches0(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union NullableFields: multiple concrete members match") + } + selected = 0 + } + if (matches6(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union NullableFields: multiple concrete members match") + } + selected = 1 + } + if (selected == 0) { + return NullableFields.VariantExcludedNull(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return NullableFields.VariantIncludedNull(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown NullableFields union value") + } + + override fun write(writer: JsonWriter, value: NullableFields?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is NullableFields.VariantExcludedNull -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is NullableFields.VariantIncludedNull -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("revision") || !matches1(jsonObject.get("revision"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + if (!matches2(candidate)) { + return false + } + if (!matches5(candidate)) { + return false + } + return true + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonNull || matches3(candidate) + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + if (!matches4(candidate)) { + return false + } + if (!matches5(candidate)) { + return false + } + return true + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches5(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "1") + } + + private fun matches6(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("revision") || !matches7(jsonObject.get("revision"))) { + return false + } + if (jsonObject.has("second_value") && !matches12(jsonObject.get("second_value"))) { + return false + } + return true + } + + private fun matches7(candidate: JsonElement): kotlin.Boolean { + if (!matches8(candidate)) { + return false + } + if (!matches9(candidate)) { + return false + } + return true + } + + private fun matches8(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonNull || matches4(candidate) + } + + private fun matches9(candidate: JsonElement): kotlin.Boolean { + if (matches10(candidate)) { + return true + } + if (matches11(candidate)) { + return true + } + return false + } + + private fun matches10(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "2") + } + + private fun matches11(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonNull + } + + private fun matches12(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/NumberAlias.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/NumberAlias.kt.golden new file mode 100644 index 000000000..afefcf57a --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/NumberAlias.kt.golden @@ -0,0 +1,7 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +typealias NumberAlias = Int diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/NumericFinite.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/NumericFinite.kt.golden new file mode 100644 index 000000000..f322ce78f --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/NumericFinite.kt.golden @@ -0,0 +1,142 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(NumericFiniteJsonAdapter::class, nullSafe = false) +sealed class NumericFinite private constructor() { + data class VariantNumber(val value0: Double) : NumericFinite() + + data class VariantAny(val value0: JsonElement) : NumericFinite() +} + +private class NumericFiniteJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): NumericFinite? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (matches0(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union NumericFinite: multiple concrete members match") + } + selected = 0 + } + if (selected < 0 && true) { + selected = 1 + } + if (selected == 0) { + return NumericFinite.VariantNumber(element.asDouble) + } + if (selected == 1) { + return NumericFinite.VariantAny(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown NumericFinite union value") + } + + override fun write(writer: JsonWriter, value: NumericFinite?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is NumericFinite.VariantNumber -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is NumericFinite.VariantAny -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!matches1(candidate)) { + return false + } + if (!matches2(candidate)) { + return false + } + if (!matches3(candidate)) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "1.25") + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + if (matches1(candidate)) { + return true + } + if (matches2(candidate)) { + return true + } + return false + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/NumericZero.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/NumericZero.kt.golden new file mode 100644 index 000000000..980adba03 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/NumericZero.kt.golden @@ -0,0 +1,142 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(NumericZeroJsonAdapter::class, nullSafe = false) +sealed class NumericZero private constructor() { + data class VariantNumber(val value0: Double) : NumericZero() + + data class VariantAny(val value0: JsonElement) : NumericZero() +} + +private class NumericZeroJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): NumericZero? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (matches0(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union NumericZero: multiple concrete members match") + } + selected = 0 + } + if (selected < 0 && true) { + selected = 1 + } + if (selected == 0) { + return NumericZero.VariantNumber(element.asDouble) + } + if (selected == 1) { + return NumericZero.VariantAny(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown NumericZero union value") + } + + override fun write(writer: JsonWriter, value: NumericZero?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is NumericZero.VariantNumber -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is NumericZero.VariantAny -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!matches1(candidate)) { + return false + } + if (!matches2(candidate)) { + return false + } + if (!matches3(candidate)) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "0") + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + if (matches1(candidate)) { + return true + } + if (matches2(candidate)) { + return true + } + return false + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/OpaqueFirst.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/OpaqueFirst.kt.golden new file mode 100644 index 000000000..b55fbc627 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/OpaqueFirst.kt.golden @@ -0,0 +1,223 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(OpaqueFirstJsonAdapter::class, nullSafe = false) +sealed class OpaqueFirst private constructor() { + data class VariantAny(val value0: JsonElement) : OpaqueFirst() + + data class VariantFiniteTwo(val value0: FiniteTwo) : OpaqueFirst() +} + +private class OpaqueFirstJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): OpaqueFirst? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (matches0(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union OpaqueFirst: multiple concrete members match") + } + selected = 1 + } + if (selected < 0 && true) { + selected = 0 + } + if (selected == 0) { + return OpaqueFirst.VariantAny(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return OpaqueFirst.VariantFiniteTwo(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown OpaqueFirst union value") + } + + override fun write(writer: JsonWriter, value: OpaqueFirst?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is OpaqueFirst.VariantAny -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is OpaqueFirst.VariantFiniteTwo -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("flags") || !matches1(jsonObject.get("flags"))) { + return false + } + if (!jsonObject.has("revision") || !matches5(jsonObject.get("revision"))) { + return false + } + if (jsonObject.has("second_value") && !matches8(jsonObject.get("second_value"))) { + return false + } + if (!jsonObject.has("values") || !matches9(jsonObject.get("values"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + for (item in candidate.asJsonObject.asMap().values) { + if (!matches2(item)) { + return false + } + } + return true + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + if (!matches3(candidate)) { + return false + } + if (!matches4(candidate)) { + return false + } + return true + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isBoolean + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isBoolean && candidate.asBoolean == true + } + + private fun matches5(candidate: JsonElement): kotlin.Boolean { + if (!matches6(candidate)) { + return false + } + if (!matches7(candidate)) { + return false + } + return true + } + + private fun matches6(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches7(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "2") + } + + private fun matches8(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches9(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonArray) { + return false + } + for (item in candidate.asJsonArray) { + if (!matches10(item)) { + return false + } + } + return true + } + + private fun matches10(candidate: JsonElement): kotlin.Boolean { + if (!matches11(candidate)) { + return false + } + if (!matches12(candidate)) { + return false + } + return true + } + + private fun matches11(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber + } + + private fun matches12(candidate: JsonElement): kotlin.Boolean { + if (matches7(candidate)) { + return true + } + if (matches13(candidate)) { + return true + } + return false + } + + private fun matches13(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "2.5") + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/OpenNested.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/OpenNested.kt.golden new file mode 100644 index 000000000..86e8c02cc --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/OpenNested.kt.golden @@ -0,0 +1,110 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(OpenNestedJsonAdapter::class, nullSafe = false) +sealed class OpenNested private constructor() { + data class VariantAmbiguousAlias(val value0: AmbiguousAlias) : OpenNested() + + data class VariantAny(val value0: JsonElement) : OpenNested() +} + +private class OpenNestedJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): OpenNested? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (matches0(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union OpenNested: multiple concrete members match") + } + selected = 0 + } + if (selected < 0 && true) { + selected = 1 + } + if (selected == 0) { + return OpenNested.VariantAmbiguousAlias(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return OpenNested.VariantAny(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown OpenNested union value") + } + + override fun write(writer: JsonWriter, value: OpenNested?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is OpenNested.VariantAmbiguousAlias -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is OpenNested.VariantAny -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (matches1(candidate)) { + return true + } + if (matches3(candidate)) { + return true + } + return false + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("first_value") && !matches2(jsonObject.get("first_value"))) { + return false + } + if (!jsonObject.has("wire_name") || !matches2(jsonObject.get("wire_name"))) { + return false + } + return true + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("second_value") && !matches2(jsonObject.get("second_value"))) { + return false + } + if (!jsonObject.has("wire_name") || !matches2(jsonObject.get("wire_name"))) { + return false + } + return true + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/RecursiveChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/RecursiveChoice.kt.golden new file mode 100644 index 000000000..857d3a9ae --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/RecursiveChoice.kt.golden @@ -0,0 +1,69 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(RecursiveChoiceJsonAdapter::class, nullSafe = false) +sealed class RecursiveChoice private constructor() { + data class VariantRecursiveChoice(val value0: RecursiveChoice) : RecursiveChoice() + + data class VariantString(val value0: String) : RecursiveChoice() +} + +private class RecursiveChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): RecursiveChoice? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (matches0(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union RecursiveChoice: multiple concrete members match") + } + selected = 1 + } + if (selected == 0) { + return RecursiveChoice.VariantRecursiveChoice(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return RecursiveChoice.VariantString(element.asString) + } + throw JsonParseException("unknown RecursiveChoice union value") + } + + override fun write(writer: JsonWriter, value: RecursiveChoice?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is RecursiveChoice.VariantRecursiveChoice -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is RecursiveChoice.VariantString -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/RightPart.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/RightPart.kt.golden new file mode 100644 index 000000000..f2dab6563 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/RightPart.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class RightPart(@field:SerializedName("right_value") + val rightValue: Int) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/ScalarChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/ScalarChoice.kt.golden new file mode 100644 index 000000000..34f954d24 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/ScalarChoice.kt.golden @@ -0,0 +1,221 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(ScalarChoiceJsonAdapter::class, nullSafe = false) +sealed class ScalarChoice private constructor() { + data class VariantInteger(val value0: Int) : ScalarChoice() + + data class VariantNumber(val value0: Double) : ScalarChoice() + + data class VariantBoolean(val value0: Boolean) : ScalarChoice() +} + +private class ScalarChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): ScalarChoice? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (matches0(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union ScalarChoice: multiple concrete members match") + } + selected = 0 + } + if (matches4(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union ScalarChoice: multiple concrete members match") + } + selected = 1 + } + if (matches8(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union ScalarChoice: multiple concrete members match") + } + selected = 2 + } + if (selected == 0) { + return ScalarChoice.VariantInteger(element.asInt) + } + if (selected == 1) { + return ScalarChoice.VariantNumber(element.asDouble) + } + if (selected == 2) { + return ScalarChoice.VariantBoolean(element.asBoolean) + } + throw JsonParseException("unknown ScalarChoice union value") + } + + override fun write(writer: JsonWriter, value: ScalarChoice?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is ScalarChoice.VariantInteger -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is ScalarChoice.VariantNumber -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is ScalarChoice.VariantBoolean -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!matches1(candidate)) { + return false + } + if (!matches2(candidate)) { + return false + } + if (!matches3(candidate)) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "1") + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + if (matches1(candidate)) { + return true + } + if (matches2(candidate)) { + return true + } + return false + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + if (!matches5(candidate)) { + return false + } + if (!matches6(candidate)) { + return false + } + if (!matches7(candidate)) { + return false + } + return true + } + + private fun matches5(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber + } + + private fun matches6(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "2.5") + } + + private fun matches7(candidate: JsonElement): kotlin.Boolean { + if (matches5(candidate)) { + return true + } + if (matches6(candidate)) { + return true + } + return false + } + + private fun matches8(candidate: JsonElement): kotlin.Boolean { + if (!matches9(candidate)) { + return false + } + if (!matches10(candidate)) { + return false + } + if (!matches11(candidate)) { + return false + } + return true + } + + private fun matches9(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isBoolean + } + + private fun matches10(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isBoolean && candidate.asBoolean == true + } + + private fun matches11(candidate: JsonElement): kotlin.Boolean { + if (matches9(candidate)) { + return true + } + if (matches10(candidate)) { + return true + } + return false + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/Tree.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/Tree.kt.golden new file mode 100644 index 000000000..818aee64c --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/Tree.kt.golden @@ -0,0 +1,223 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(TreeJsonAdapter::class, nullSafe = false) +sealed class Tree private constructor() { + data class VariantBranchOne(val value0: BranchOne) : Tree() + + data class VariantBranchTwo(val value0: BranchTwo) : Tree() +} + +private class TreeJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): Tree? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + val memo = java.util.IdentityHashMap>() + var selected = -1 + if (matches0(element, memo)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union Tree: multiple concrete members match") + } + selected = 0 + } + if (matches4(element, memo)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union Tree: multiple concrete members match") + } + selected = 1 + } + if (selected == 0) { + return Tree.VariantBranchOne(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return Tree.VariantBranchTwo(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown Tree union value") + } + + override fun write(writer: JsonWriter, value: Tree?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is Tree.VariantBranchOne -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is Tree.VariantBranchTwo -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement, memo: java.util.IdentityHashMap>): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("children") || !matches1(jsonObject.get("children"), memo)) { + return false + } + if (!jsonObject.has("revision") || !matches9(jsonObject.get("revision"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement, memo: java.util.IdentityHashMap>): kotlin.Boolean { + if (!candidate.isJsonArray) { + return false + } + for (item in candidate.asJsonArray) { + if (!matches2(item, memo)) { + return false + } + } + return true + } + + private fun matches2(candidate: JsonElement, memo: java.util.IdentityHashMap>): kotlin.Boolean { + val results = memo.getOrPut(candidate) { + mutableMapOf() + } + val cached = results[2] + if (cached != null) { + return cached + } + val matched = matches3(candidate, memo) + results[2] = matched + return matched + } + + private fun matches3(candidate: JsonElement, memo: java.util.IdentityHashMap>): kotlin.Boolean { + if (matches0(candidate, memo)) { + return true + } + if (matches4(candidate, memo)) { + return true + } + return false + } + + private fun matches4(candidate: JsonElement, memo: java.util.IdentityHashMap>): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("children") || !matches1(jsonObject.get("children"), memo)) { + return false + } + if (!jsonObject.has("revision") || !matches5(jsonObject.get("revision"))) { + return false + } + if (jsonObject.has("second_value") && !matches8(jsonObject.get("second_value"))) { + return false + } + return true + } + + private fun matches5(candidate: JsonElement): kotlin.Boolean { + if (!matches6(candidate)) { + return false + } + if (!matches7(candidate)) { + return false + } + return true + } + + private fun matches6(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches7(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "2") + } + + private fun matches8(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches9(candidate: JsonElement): kotlin.Boolean { + if (!matches6(candidate)) { + return false + } + if (!matches10(candidate)) { + return false + } + return true + } + + private fun matches10(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "1") + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/VariantRaw.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/VariantRaw.kt.golden new file mode 100644 index 000000000..821c4d757 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/VariantRaw.kt.golden @@ -0,0 +1,103 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(VariantRawJsonAdapter::class, nullSafe = false) +sealed class VariantRaw private constructor() { + data class VariantAlpha(val value0: Alpha) : VariantRaw() + + data class VariantBeta(val value0: Beta) : VariantRaw() +} + +private class VariantRawJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): VariantRaw? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (matches0(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union VariantRaw: multiple concrete members match") + } + selected = 0 + } + if (matches2(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union VariantRaw: multiple concrete members match") + } + selected = 1 + } + if (selected == 0) { + return VariantRaw.VariantAlpha(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return VariantRaw.VariantBeta(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown VariantRaw union value") + } + + override fun write(writer: JsonWriter, value: VariantRaw?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is VariantRaw.VariantAlpha -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is VariantRaw.VariantBeta -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("first_value") && !matches1(jsonObject.get("first_value"))) { + return false + } + if (!jsonObject.has("wire_name") || !matches1(jsonObject.get("wire_name"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("second_value") && !matches1(jsonObject.get("second_value"))) { + return false + } + if (!jsonObject.has("wire_name") || !matches1(jsonObject.get("wire_name"))) { + return false + } + return true + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/Wide.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/Wide.kt.golden new file mode 100644 index 000000000..814951a27 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/Wide.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class Wide(val identifier: Long, @field:SerializedName("second_value") + val secondValue: String? = null) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/WrappedChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/WrappedChoice.kt.golden new file mode 100644 index 000000000..7ce98e51b --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/WrappedChoice.kt.golden @@ -0,0 +1,186 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +@JsonAdapter(WrappedChoiceJsonAdapter::class, nullSafe = false) +sealed class WrappedChoice private constructor() { + data class VariantWrappedPair(val value0: WrappedPair) : WrappedChoice() + + data class VariantAlpha(val value0: Alpha) : WrappedChoice() +} + +private class WrappedChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): WrappedChoice? { + val element = JsonParser.parseReader(reader) + if (element.isJsonNull) { + return null + } + var selected = -1 + if (matches0(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union WrappedChoice: multiple concrete members match") + } + selected = 0 + } + if (matches7(element)) { + if (selected >= 0) { + throw JsonParseException("ambiguous untagged union WrappedChoice: multiple concrete members match") + } + selected = 1 + } + if (selected == 0) { + return WrappedChoice.VariantWrappedPair(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return WrappedChoice.VariantAlpha(gson.fromJson(element, object: TypeToken() {}.type)) + } + throw JsonParseException("unknown WrappedChoice union value") + } + + override fun write(writer: JsonWriter, value: WrappedChoice?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is WrappedChoice.VariantWrappedPair -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is WrappedChoice.VariantAlpha -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("leftPart") || !matches1(jsonObject.get("leftPart"))) { + return false + } + if (!jsonObject.has("rightPart") || !matches3(jsonObject.get("rightPart"))) { + return false + } + return true + } + + private fun matches1(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("left_value") || !matches2(jsonObject.get("left_value"))) { + return false + } + return true + } + + private fun matches2(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isString + } + + private fun matches3(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (!jsonObject.has("right_value") || !matches4(jsonObject.get("right_value"))) { + return false + } + return true + } + + private fun matches4(candidate: JsonElement): kotlin.Boolean { + if (!matches5(candidate)) { + return false + } + if (!matches6(candidate)) { + return false + } + return true + } + + private fun matches5(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && isIntegerNumber(candidate) + } + + private fun matches6(candidate: JsonElement): kotlin.Boolean { + return candidate.isJsonPrimitive && candidate.asJsonPrimitive.isNumber && matchesNumber(candidate, "2") + } + + private fun matches7(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + val jsonObject = candidate.asJsonObject + if (jsonObject.has("first_value") && !matches2(jsonObject.get("first_value"))) { + return false + } + if (!jsonObject.has("wire_name") || !matches2(jsonObject.get("wire_name"))) { + return false + } + return true + } + + private fun numberSignificand(literal: kotlin.String): java.math.BigDecimal { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return java.math.BigDecimal(if (exponent < 0) literal else literal.substring(0, exponent)).stripTrailingZeros() + } + + private fun numberExponent(literal: kotlin.String): java.math.BigInteger { + val exponent = maxOf(literal.indexOf('e'), literal.indexOf('E')) + return if (exponent < 0) java.math.BigInteger.ZERO else java.math.BigInteger(literal.substring(exponent + 1)) + } + + private fun isIntegerNumber(candidate: JsonElement): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale().toLong())) >= 0 + } + + private fun isBinaryNumber(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonPrimitive || !candidate.asJsonPrimitive.isNumber || !isIntegerNumber(candidate)) { + return false + } + val literal = candidate.asString + val significand = numberSignificand(literal) + if (significand.signum() == 0) { + return true + } + val scale = numberExponent(literal).subtract(java.math.BigInteger.valueOf(significand.scale().toLong())) + if (scale.compareTo(java.math.BigInteger.valueOf(2)) > 0) { + return false + } + val number = significand.unscaledValue().multiply(java.math.BigInteger.TEN.pow(scale.toInt())) + return number.compareTo(java.math.BigInteger.valueOf(-128)) >= 0 && number.compareTo(java.math.BigInteger.valueOf(255)) <= 0 + } + + private fun matchesNumber(candidate: JsonElement, expected: kotlin.String): kotlin.Boolean { + val literal = candidate.asString + val significand = numberSignificand(literal) + val expectedNumber = java.math.BigDecimal(expected).stripTrailingZeros() + if (significand.unscaledValue() != expectedNumber.unscaledValue()) { + return false + } + return significand.signum() == 0 || numberExponent(literal) == java.math.BigInteger.valueOf(significand.scale().toLong() - expectedNumber.scale()) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/models/WrappedPair.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/WrappedPair.kt.golden new file mode 100644 index 000000000..74f240da5 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/models/WrappedPair.kt.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.models + +data class WrappedPair(val leftPart: LeftPart, val rightPart: RightPart) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/runtime/ApiClient.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/runtime/ApiClient.kt.golden new file mode 100644 index 000000000..290c3aadb --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/runtime/ApiClient.kt.golden @@ -0,0 +1,61 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.runtime + +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response + +class ApiClient( + private val baseUrl: String, + private val client: OkHttpClient = OkHttpClient(), + private val authenticator: Authenticator? = null, + private val defaultHeaders: Map = emptyMap(), +) { + fun newRequest( + method: String, + path: String, + query: Map? = null, + body: String? = null, + ): Request { + return buildRequest(method, path, query, body?.toRequestBody("application/json".toMediaType())) + } + + fun newRequestWithBody( + method: String, + path: String, + query: Map? = null, + body: okhttp3.RequestBody?, + ): Request { + return buildRequest(method, path, query, body) + } + + private fun buildRequest( + method: String, + path: String, + query: Map? = null, + body: okhttp3.RequestBody?, + ): Request { + val urlBuilder: HttpUrl.Builder = (baseUrl.trimEnd('/') + path).toHttpUrl().newBuilder() + query?.forEach { (k, v) -> urlBuilder.addQueryParameter(k, v) } + + val builder = Request.Builder() + .url(urlBuilder.build()) + .method(method, body) + + defaultHeaders.forEach { (k, v) -> builder.header(k, v) } + authenticator?.authenticate(builder) + + return builder.build() + } + + fun execute(request: Request): Response { + return client.newCall(request).execute() + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/runtime/ApiException.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/runtime/ApiException.kt.golden new file mode 100644 index 000000000..995bd74a4 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/runtime/ApiException.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.runtime + +open class ApiException( + val statusCode: Int, + val status: String, + val body: String, +) : RuntimeException("API error $statusCode: $status") diff --git a/tests/golden/kotlin/kotlin-okhttp/union-strict/runtime/Auth.kt.golden b/tests/golden/kotlin/kotlin-okhttp/union-strict/runtime/Auth.kt.golden new file mode 100644 index 000000000..6f6400a20 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/union-strict/runtime/Auth.kt.golden @@ -0,0 +1,70 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java union policies — 1.0.0 + +package com.example.sdk.runtime + +import okhttp3.Request + +interface Authenticator { + fun authenticate(builder: Request.Builder) +} + +class BearerAuth : Authenticator { + private val token: String? + private val tokenProvider: (() -> String)? + + constructor(token: String) { + this.token = token + this.tokenProvider = null + } + + constructor(tokenProvider: () -> String) { + this.token = null + this.tokenProvider = tokenProvider + } + + override fun authenticate(builder: Request.Builder) { + val t = tokenProvider?.invoke() ?: token!! + builder.header("Authorization", "Bearer $t") + } +} + +class ApiKeyAuth : Authenticator { + private val key: String? + private val keyProvider: (() -> String)? + private val name: String + private val location: ApiKeyLocation + + constructor(key: String, name: String, location: ApiKeyLocation) { + this.key = key + this.keyProvider = null + this.name = name + this.location = location + } + + constructor(keyProvider: () -> String, name: String, location: ApiKeyLocation) { + this.key = null + this.keyProvider = keyProvider + this.name = name + this.location = location + } + + override fun authenticate(builder: Request.Builder) { + val k = keyProvider?.invoke() ?: key!! + when (location) { + ApiKeyLocation.HEADER -> builder.header(name, k) + ApiKeyLocation.QUERY -> { + val url = builder.build().url.newBuilder() + .addQueryParameter(name, k) + .build() + builder.url(url) + } + } + } +} + +enum class ApiKeyLocation { + HEADER, + QUERY, +} diff --git a/tests/golden_tests_kotlin_okhttp.rs b/tests/golden_tests_kotlin_okhttp.rs index 8f172f0ce..8dd480b90 100644 --- a/tests/golden_tests_kotlin_okhttp.rs +++ b/tests/golden_tests_kotlin_okhttp.rs @@ -204,3 +204,84 @@ fn kotlin_primary_constructor_annotations_preserve_wire_names() { "the generated property keeps its Kotlin identifier:\n{model}" ); } + +#[test] +fn untagged_union_policies_preserve_typed_or_raw_apis() { + for (name, policy) in [ + ("union-first-match", "firstMatch"), + ("union-strict", "strict"), + ("union-raw", "raw"), + ] { + let config = toml::from_str(&format!("untagged_union_strategy = {policy:?}")).unwrap(); + run_golden_test( + &KotlinOkhttpCodeGenerator::new(config), + golden_dir(), + name, + "valid/java-union-policies.yaml", + UPDATE_HINT, + ); + } + run_golden_test( + &KotlinOkhttpCodeGenerator::new(Default::default()), + golden_dir(), + "union-selection", + "valid/union-selection.yaml", + UPDATE_HINT, + ); + for (name, policy) in [ + ("union-helper-types", "firstMatch"), + ("union-helper-types-strict", "strict"), + ] { + run_golden_test( + &KotlinOkhttpCodeGenerator::new( + toml::from_str(&format!("untagged_union_strategy = {policy:?}")).unwrap(), + ), + golden_dir(), + name, + "valid/java-union-helper-types.yaml", + UPDATE_HINT, + ); + } +} + +#[test] +fn policy_changes_are_confined_to_untagged_union_adapters_and_raw_cases() { + use heck::ToPascalCase; + use openapi_nexus::ir::{lower::lower, types::IrSchemaKind}; + use openapi_nexus::parser::parse_content_yaml; + + for fixture in [ + "valid/comprehensive-schemas.yaml", + "valid/java-union-policies.yaml", + "valid/type-aliases/discriminated-union-mixed-unit-and-allof.yaml", + ] { + let source = read_fixture(fixture); + let ir = lower(parse_content_yaml(&source).unwrap()).unwrap(); + let unions: std::collections::HashSet<_> = ir + .schemas + .values() + .filter(|schema| matches!(schema.kind, IrSchemaKind::Union(_))) + .map(|schema| format!("models/{}.kt", schema.name.to_pascal_case())) + .collect(); + let typed = + generate_files(&KotlinOkhttpCodeGenerator::new(Default::default()), &source).unwrap(); + for policy in ["strict", "raw"] { + let generator = KotlinOkhttpCodeGenerator::new( + toml::from_str(&format!("untagged_union_strategy = {policy:?}")).unwrap(), + ); + let changed = generate_files(&generator, &source).unwrap(); + assert_eq!(typed.len(), changed.len()); + for (file, content) in &typed { + if !unions.contains(file) { + assert_eq!(content, &changed[file], "{fixture} {policy} {file}"); + } else if policy == "strict" { + assert_eq!( + content.split("\nprivate class").next(), + changed[file].split("\nprivate class").next(), + "typed API {file}" + ); + } + } + } + } +} diff --git a/tests/ir_scalar_enums.rs b/tests/ir_scalar_enums.rs index d1a073cf8..b1874b148 100644 --- a/tests/ir_scalar_enums.rs +++ b/tests/ir_scalar_enums.rs @@ -324,6 +324,20 @@ components: assert!(actual.contains("candidate.getAsBoolean() == true")); continue; } + if generator.generator_type().language() == openapi_nexus::codegen::Language::Kotlin + && filename == "Models/Choice.kt" + { + let actual = &actual[filename]; + assert_ne!(actual, expected, "Kotlin dispatch consumes finite facts"); + assert_eq!( + actual.split("\nprivate class ChoiceJsonAdapter").next(), + expected.split("\nprivate class ChoiceJsonAdapter").next(), + "Kotlin typed cases and payloads do not change" + ); + assert!(actual.contains("matchesNumber(candidate, \"1\")")); + assert!(actual.contains("candidate.asBoolean == true")); + continue; + } assert_eq!( actual.get(filename), Some(expected),