diff --git a/docs/src/kotlin_config.md b/docs/src/kotlin_config.md index 89f8313e..ebc2ccc9 100644 --- a/docs/src/kotlin_config.md +++ b/docs/src/kotlin_config.md @@ -33,8 +33,8 @@ 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. +unions keep their existing APIs/codecs. Pure dictionaries have the representation +described below; mixed-object additional-property repair remains separate. Raw mode uses Gson's native tree adapter and temporarily enables writer null serialization, restoring its prior value even on failure. This preserves null @@ -47,3 +47,31 @@ The policy can also be set through the CLI: openapi-nexus generate --input api.yaml --output sdk --generators kotlin-okhttp \ --generator-config kotlin-okhttp.untagged_union_strategy=strict ``` + +## Pure dictionary models + +A named object with no named properties and an explicit `additionalProperties` +schema generates a data class with `additionalProperties: MutableMap` +storage, defaulting to an empty map. Its Gson adapter reads and writes a flat JSON +object: the storage property's name is not added to the wire representation. + +```kotlin +val labels = Labels() +labels.additionalProperties["keep_key"] = "value" +val json = Gson().toJson(labels) // {"keep_key":"value"} +``` + +Keys stay verbatim. Values retain their declared model, scalar, nullable or +container types; `additionalProperties: true` and `{}` use `JsonElement` values +to preserve arbitrary JSON, null entries and large numeric literals. Dictionary +aliases remain transparent and containing fields, lists, maps and unions use the +same adapter. JSON null at the root stays Kotlin null; non-object roots fail +instead of becoming empty dictionaries. + +Dictionary entry nulls are preserved without changing the writer's surrounding +null policy. Values decode from the original JSON reader through their declared +Gson codecs, preserving native decode errors as well as ordinary model null-field +omission. Authored model types remain distinct from Gson helper types, even when +they share names such as `TypeToken` or `JsonElement`. This does not preserve +unspecified fields in mixed objects or redesign `allOf` wrappers; those repairs +remain separate. diff --git a/src/generators/kotlin/okhttp/dictionary.rs b/src/generators/kotlin/okhttp/dictionary.rs new file mode 100644 index 00000000..eac45810 --- /dev/null +++ b/src/generators/kotlin/okhttp/dictionary.rs @@ -0,0 +1,177 @@ +//! Named pure dictionaries retain their wire object shape and declared values. + +use heck::ToPascalCase; +use sigil_stitch::lang::kotlin::Kotlin; +use sigil_stitch::prelude::*; + +use crate::generators::type_application; +use crate::ir::types::{IrSchema, IrTypeExpr}; + +use super::sigil_emit::{kotlin_type_name_in, package_header}; + +pub(super) fn file( + schema: &IrSchema, + values: &IrTypeExpr, + package_name: &str, +) -> Result { + let name = schema.name.to_pascal_case(); + let models_package = format!("{package_name}.models"); + let root = TypeName::qualified(&models_package, &name); + let value_type = kotlin_type_name_in(values, Some(&models_package)); + let adapter_name = format!("{name}JsonAdapter"); + let map = type_application( + TypeName::qualified("kotlin.collections", "MutableMap"), + vec![TypeName::qualified("kotlin", "String"), value_type.clone()], + ); + // Gson's built-in map factory precedes @JsonAdapter, so a Map subtype would + // bypass this codec and drop null entries. Keep explicit dictionary storage. + let mut dictionary = TypeSpec::builder(&name, TypeKind::Struct) + .visibility(Visibility::Public) + .add_primary_constructor_param( + ParameterSpec::builder("additionalProperties", map) + .is_property() + .default_value( + sigil_quote!(Kotlin { linkedMapOf() })?.__sigil_trim_trailing_newline(), + ) + .build()?, + ) + .annotate( + AnnotationSpec::importable(TypeName::importable( + "com.google.gson.annotations", + "JsonAdapter", + )) + .args([format!("{adapter_name}::class"), "nullSafe = false".into()]), + ); + if let Some(doc) = &schema.description { + dictionary = dictionary.doc(doc); + } + let gson = TypeName::importable("com.google.gson", "Gson"); + let element = TypeName::importable("com.google.gson", "JsonElement"); + let exception = TypeName::importable("com.google.gson", "JsonParseException"); + let json_token = TypeName::importable("com.google.gson.stream", "JsonToken"); + let token = type_application( + TypeName::importable("com.google.gson.reflect", "TypeToken"), + vec![value_type.clone()], + ); + // Delegate from the original reader: Gson's tree reader silently truncates + // fractional/overflowing integer members that its streaming reader rejects. + 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 { + if (reader.peek() == $T(json_token.clone()).NULL) { reader.nextNull(); return null } + if (reader.peek() != $T(json_token).BEGIN_OBJECT) { throw $T(exception)($S(format!("expected {name} dictionary object"))) } + reader.beginObject() + val result = $T(root.clone())() + while (reader.hasNext()) { + val key = reader.nextName() + result.additionalProperties[key] = gson.fromJson(reader, object : $T(token.clone())() {}.type) + } + reader.endObject() + return result + })?).build()?; + let mut value_functions = Vec::new(); + let value_function = encode_value(values, &models_package, &mut value_functions)?; + 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 } + writer.beginObject() + for (entry in value.additionalProperties.entries) { + val tree = $N(value_function)(entry.value) + writer.name(entry.key) + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { gson.getAdapter($T(element)::class.java).write(writer, tree) } + finally { writer.serializeNulls = serializeNulls } + } + writer.endObject() + })?) + .build()?; + let mut adapter = TypeSpec::builder(&adapter_name, TypeKind::Class) + .visibility(Visibility::Private) + .extends(type_application( + TypeName::importable("com.google.gson", "TypeAdapter"), + vec![root], + )) + .add_field( + FieldSpec::builder("gson", gson.clone()) + .visibility(Visibility::Private) + .is_readonly() + .initializer(sigil_quote!(Kotlin { $T(gson)() })?.__sigil_trim_trailing_newline()) + .build()?, + ) + .add_method(read) + .add_method(write); + for function in value_functions { + adapter = adapter.add_method(function); + } + FileSpec::builder_with("model.kt", Kotlin::new()) + .header(package_header(package_name)?) + .add_type(dictionary.build()?) + .add_type(adapter.build()?) + .build() +} + +/// Containers preserve their entries without forcing serializeNulls on ordinary +/// model codecs. Named members still serialize through their declared adapter. +fn encode_value( + expr: &IrTypeExpr, + models_package: &str, + functions: &mut Vec, +) -> Result { + let expr = expr.representation(); + if let IrTypeExpr::Nullable(inner) = expr { + return encode_value(inner, models_package, functions); + } + let body = match expr { + IrTypeExpr::Any | IrTypeExpr::Union(_) => sigil_quote!(Kotlin { return value })?, + IrTypeExpr::Array(inner) => { + let child = encode_value(inner, models_package, functions)?; + let array = TypeName::importable("com.google.gson", "JsonArray"); + sigil_quote!(Kotlin { + val result = $T(array)() + for (item in value) { result.add($N(child)(item)) } + return result + })? + } + IrTypeExpr::Map(inner) => { + let child = encode_value(inner, models_package, functions)?; + let object = TypeName::importable("com.google.gson", "JsonObject"); + sigil_quote!(Kotlin { + val result = $T(object)() + for (entry in value.entries) { result.add(entry.key, $N(child)(entry.value)) } + return result + })? + } + _ => { + let token = type_application( + TypeName::importable("com.google.gson.reflect", "TypeToken"), + vec![kotlin_type_name_in(expr, Some(models_package))], + ); + sigil_quote!(Kotlin { return gson.toJsonTree(value, object : $T(token)() {}.type) })? + } + }; + let name = format!("dictionaryValue{}", functions.len()); + let null = TypeName::importable("com.google.gson", "JsonNull"); + functions.push( + FunSpec::builder(&name) + .visibility(Visibility::Private) + .add_param(ParameterSpec::of( + "value", + TypeName::optional(kotlin_type_name_in(expr, Some(models_package))), + )) + .returns(vec![TypeName::importable("com.google.gson", "JsonElement")]) + .body(sigil_quote!(Kotlin { + if (value == null) { return $T(null).INSTANCE } + $C(body) + })?) + .build()?, + ); + Ok(name) +} diff --git a/src/generators/kotlin/okhttp/mod.rs b/src/generators/kotlin/okhttp/mod.rs index 82d2d8df..b29c461f 100644 --- a/src/generators/kotlin/okhttp/mod.rs +++ b/src/generators/kotlin/okhttp/mod.rs @@ -1,5 +1,6 @@ pub mod codegen; pub mod config; +mod dictionary; mod matching; pub mod runtime; pub mod sigil_emit; diff --git a/src/generators/kotlin/okhttp/sigil_emit.rs b/src/generators/kotlin/okhttp/sigil_emit.rs index 7020a54e..9461f7ff 100644 --- a/src/generators/kotlin/okhttp/sigil_emit.rs +++ b/src/generators/kotlin/okhttp/sigil_emit.rs @@ -106,7 +106,7 @@ fn emit_model_body( file_spec.render(RENDER_WIDTH) } -fn package_header(package_name: &str) -> Result { +pub(super) fn package_header(package_name: &str) -> Result { sigil_quote!(Kotlin { package $L(format!("{package_name}.models")) }) @@ -121,6 +121,11 @@ fn emit_object( obj: &IrObject, package_name: &str, ) -> Result { + if obj.properties.is_empty() + && let Some(values) = &obj.additional_properties + { + return super::dictionary::file(schema, values, package_name); + } let name = schema.name.to_pascal_case(); let mut file = @@ -383,23 +388,42 @@ fn kotlin_closed_sum_case(case: &ClosedSumCasePlan) -> Result TypeName { + kotlin_type_name_in(expr, None) +} + +/// Authored model identities must remain distinct from an adapter's imports. +pub(super) fn kotlin_type_name_in(expr: &IrTypeExpr, models_package: Option<&str>) -> TypeName { + let builtin = |module: &str, name: &str| match models_package { + Some(_) => TypeName::qualified(module, name), + None => TypeName::primitive(name), + }; match expr { - IrTypeExpr::ScalarEnum { representation, .. } => kotlin_type_name(representation), - IrTypeExpr::Named(name) => TypeName::primitive(&name.to_pascal_case()), - IrTypeExpr::Primitive(primitive) => TypeName::primitive(kt_primitive(primitive)), + IrTypeExpr::ScalarEnum { representation, .. } => { + kotlin_type_name_in(representation, models_package) + } + IrTypeExpr::Named(name) => match models_package { + Some(package) => TypeName::qualified(package, &name.to_pascal_case()), + None => TypeName::primitive(&name.to_pascal_case()), + }, + IrTypeExpr::Primitive(primitive) => builtin("kotlin", kt_primitive(primitive)), IrTypeExpr::Array(inner) => TypeName::application( - TypeName::primitive("List"), - vec![TypeArgument::Single(kotlin_type_name(inner))], + builtin("kotlin.collections", "List"), + vec![TypeArgument::Single(kotlin_type_name_in( + inner, + models_package, + ))], ), IrTypeExpr::Map(inner) => TypeName::application( - TypeName::primitive("Map"), + builtin("kotlin.collections", "Map"), vec![ - TypeArgument::Single(TypeName::primitive("String")), - TypeArgument::Single(kotlin_type_name(inner)), + TypeArgument::Single(builtin("kotlin", "String")), + TypeArgument::Single(kotlin_type_name_in(inner, models_package)), ], ), - IrTypeExpr::Nullable(inner) => TypeName::optional(kotlin_type_name(inner)), - IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => TypeName::primitive("String"), + IrTypeExpr::Nullable(inner) => { + TypeName::optional(kotlin_type_name_in(inner, models_package)) + } + IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => builtin("kotlin", "String"), IrTypeExpr::Union(_) | IrTypeExpr::Any => { TypeName::importable("com.google.gson", "JsonElement") } @@ -730,6 +754,32 @@ mod tests { use super::*; use sigil_stitch::assert_rendered; + #[test] + fn dictionary_value_types_keep_qualified_model_identities() { + let expr = IrTypeExpr::Map(Box::new(IrTypeExpr::Array(Box::new(IrTypeExpr::Nullable( + Box::new(IrTypeExpr::Named("JsonElement".into())), + ))))); + let value_type = kotlin_type_name_in(&expr, Some("sdk.models")); + let token = crate::generators::type_application( + TypeName::importable("com.google.gson.reflect", "TypeToken"), + vec![kotlin_type_name_in( + &IrTypeExpr::Named("TypeToken".into()), + Some("sdk.models"), + )], + ); + let body = sigil_quote!(Kotlin { + val values: $T(value_type) + val token = object : $T(token)() {} + }) + .unwrap(); + assert_rendered!( + Kotlin::new(), + width = 120, + body, + "import com.google.gson.reflect.TypeToken\n\nval values: kotlin.collections.Map>\nval token = object: TypeToken() {\n}\n", + ); + } + #[test] fn tagged_adapter_render_is_pinned() { let plan = ClosedSumPlan { diff --git a/tests/fixtures/valid/kotlin-dictionaries.yaml b/tests/fixtures/valid/kotlin-dictionaries.yaml new file mode 100644 index 00000000..fd1df7ed --- /dev/null +++ b/tests/fixtures/valid/kotlin-dictionaries.yaml @@ -0,0 +1,104 @@ +openapi: 3.1.0 +info: {title: Kotlin dictionaries, version: '1.0.0'} +paths: + /labels: + post: + tags: [Dictionary] + operationId: echoLabels + requestBody: + required: true + content: + application/json: + schema: {$ref: '#/components/schemas/Labels'} + responses: + '200': + description: Preserved dictionary + content: + application/json: + schema: {$ref: '#/components/schemas/Labels'} + '400': + description: Dictionary error + content: + application/json: + schema: {$ref: '#/components/schemas/Values'} +components: + schemas: + Labels: + description: Verbatim labels indexed by their wire keys. + type: object + additionalProperties: {type: string} + NestedValue: + type: object + required: [wire_value] + properties: + wire_value: {type: string} + ordinary_null: {type: string} + native_int: {type: integer} + ValueHolder: + type: object + properties: + value: {$ref: '#/components/schemas/NestedValue'} + Values: + type: object + additionalProperties: {$ref: '#/components/schemas/NestedValue'} + JsonValues: + type: object + additionalProperties: true + EmptySchemaValues: + type: object + additionalProperties: {} + Numbers: + type: object + additionalProperties: {type: integer, format: int64} + Integers: + type: object + additionalProperties: {type: integer} + Flags: + type: object + additionalProperties: {type: boolean} + NullableValues: + type: object + additionalProperties: + anyOf: [{$ref: '#/components/schemas/NestedValue'}, {type: 'null'}] + Lists: + type: object + additionalProperties: {type: array, items: {$ref: '#/components/schemas/NestedValue'}} + NestedMaps: + type: object + additionalProperties: {type: object, additionalProperties: {$ref: '#/components/schemas/NestedValue'}} + NestedNullableMaps: + type: object + additionalProperties: + type: object + additionalProperties: + anyOf: [{$ref: '#/components/schemas/NestedValue'}, {type: 'null'}] + RawLists: + type: object + additionalProperties: {type: array, items: true} + Alias: {$ref: '#/components/schemas/Values'} + Node: + type: object + required: [wire_value, children] + properties: + wire_value: {type: string} + children: {$ref: '#/components/schemas/Nodes'} + Nodes: + type: object + additionalProperties: {$ref: '#/components/schemas/Node'} + LabelChoice: + anyOf: + - {$ref: '#/components/schemas/Labels'} + - {type: integer} + Choices: + type: object + additionalProperties: {$ref: '#/components/schemas/LabelChoice'} + Envelope: + type: object + required: [labels, values, dictionaries, aliases, json_values] + properties: + labels: {$ref: '#/components/schemas/Labels'} + values: {$ref: '#/components/schemas/Values'} + dictionaries: {type: array, items: {$ref: '#/components/schemas/Values'}} + aliases: {$ref: '#/components/schemas/Alias'} + json_values: {$ref: '#/components/schemas/JsonValues'} + ordinary_null: {type: string} diff --git a/tests/fixtures/valid/kotlin-dictionary-helper-types.yaml b/tests/fixtures/valid/kotlin-dictionary-helper-types.yaml new file mode 100644 index 00000000..02cffa1d --- /dev/null +++ b/tests/fixtures/valid/kotlin-dictionary-helper-types.yaml @@ -0,0 +1,54 @@ +openapi: 3.1.0 +info: {title: Kotlin dictionary helper types, version: '1.0.0'} +paths: {} +components: + schemas: + TypeToken: + type: object + required: [wire_value] + properties: + wire_value: {type: string} + JsonElement: + type: object + required: [wire_value] + properties: + wire_value: {type: string} + JsonNull: + type: object + required: [wire_value] + properties: + wire_value: {type: string} + Gson: + type: object + required: [wire_value] + properties: + wire_value: {type: string} + TokenValues: + type: object + additionalProperties: {$ref: '#/components/schemas/TypeToken'} + ElementValues: + type: object + additionalProperties: {$ref: '#/components/schemas/JsonElement'} + NullValues: + type: object + additionalProperties: {$ref: '#/components/schemas/JsonNull'} + GsonValues: + type: object + additionalProperties: {$ref: '#/components/schemas/Gson'} + NestedValues: + type: object + additionalProperties: + type: object + additionalProperties: + type: array + items: + anyOf: [{$ref: '#/components/schemas/JsonElement'}, {type: 'null'}] + JsonAdapter: + type: object + additionalProperties: true + JsonReader: + type: object + additionalProperties: {$ref: '#/components/schemas/TypeToken'} + TypeAdapter: + type: object + additionalProperties: {type: string} diff --git a/tests/generated-clients/jvm/README.md b/tests/generated-clients/jvm/README.md index 6506aa39..d6c8787b 100644 --- a/tests/generated-clients/jvm/README.md +++ b/tests/generated-clients/jvm/README.md @@ -13,6 +13,10 @@ 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. +The Kotlin dictionary consumer checks flat typed/opaque maps, null entries, +recursive values, native integer decode errors, containing models and HTTP +request/response/error boundaries. Its helper-type consumer also verifies authored +models and dictionaries named after Gson helpers retain their declared identities. 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/dictionaries/gradle.lockfile b/tests/generated-clients/jvm/kotlin/cases/dictionaries/gradle.lockfile new file mode 100644 index 00000000..2f6f3df4 --- /dev/null +++ b/tests/generated-clients/jvm/kotlin/cases/dictionaries/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:dictionaries: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/dictionaries/src/test/java/DictionaryTest.java b/tests/generated-clients/jvm/kotlin/cases/dictionaries/src/test/java/DictionaryTest.java new file mode 100644 index 00000000..7ab19fff --- /dev/null +++ b/tests/generated-clients/jvm/kotlin/cases/dictionaries/src/test/java/DictionaryTest.java @@ -0,0 +1,189 @@ +import com.example.sdk.models.*; +import com.example.sdk.apis.*; +import com.example.sdk.runtime.ApiClient; +import com.google.gson.*; +import com.google.gson.stream.JsonWriter; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import okhttp3.*; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class DictionaryTest { + private final Gson gson = new Gson(); + private static final String VALUES = "{\"keep_key\":{\"wire_value\":\"nested\"}}"; + + private T roundTrip(String wire, Class type) { + T value = gson.fromJson(wire, type); + assertEquals(JsonParser.parseString(wire), gson.toJsonTree(value, type)); + assertEquals(JsonParser.parseString(wire), JsonParser.parseString(gson.toJson(value, type))); + return value; + } + + @Test void stringDictionariesAreUsableMapsWithVerbatimKeys() { + Labels labels = roundTrip("{\"snake_key\":\"one\",\"snakeKey\":\"two\",\"additionalProperties\":\"not_storage\",\"\":\"empty\",\"quote\\\"$key\":\"value\"}", Labels.class); + assertEquals("one", labels.getAdditionalProperties().get("snake_key")); + labels.getAdditionalProperties().put("new_key", "three"); + assertEquals("three", gson.toJsonTree(labels).getAsJsonObject().get("new_key").getAsString()); + assertTrue(roundTrip("{}", Labels.class).getAdditionalProperties().isEmpty()); + assertTrue(new Labels().getAdditionalProperties().isEmpty()); + assertEquals("not_storage", labels.getAdditionalProperties().get("additionalProperties")); + } + + @Test void modelValuesDecodeAsTheirDeclaredModels() { + Values values = roundTrip(VALUES, Values.class); + assertInstanceOf(NestedValue.class, values.getAdditionalProperties().get("keep_key")); + assertEquals("nested", values.getAdditionalProperties().get("keep_key").getWireValue()); + } + + @Test void dictionaryNullPolicyDoesNotChangeOrdinaryMemberFieldPolicy() { + Values values = gson.fromJson("{\"keep_key\":{\"wire_value\":\"nested\",\"ordinary_null\":null}}", Values.class); + assertEquals(JsonParser.parseString(VALUES), gson.toJsonTree(values)); + } + + @Test void modelValuesKeepNativeIntegerDecodeErrors() { + for (String number : new String[] {"2147483648", "-2147483649", "1.5"}) { + String value = "{\"wire_value\":\"nested\",\"native_int\":" + number + "}"; + assertThrows(JsonParseException.class, () -> gson.fromJson(value, NestedValue.class), number); + assertThrows(JsonParseException.class, () -> gson.fromJson("{\"value\":" + value + "}", ValueHolder.class), number); + assertThrows(JsonParseException.class, () -> gson.fromJson("{\"key\":" + value + "}", Values.class), number); + assertThrows(JsonParseException.class, () -> gson.fromJson("{\"key\":" + value + "}", NullableValues.class), number); + assertThrows(JsonParseException.class, () -> gson.fromJson("{\"key\":[" + value + "]}", Lists.class), number); + assertThrows(JsonParseException.class, () -> gson.fromJson("{\"key\":{\"nested\":" + value + "}}", NestedMaps.class), number); + } + } + + @Test void scalarValuesKeepNativeIntegerDecodeErrors() { + for (String number : new String[] {"2147483648", "-2147483649", "1.5"}) { + assertThrows(JsonParseException.class, () -> gson.fromJson(number, Integer.class), number); + assertThrows(JsonParseException.class, () -> gson.fromJson("{\"key\":" + number + "}", Integers.class), number); + } + } + + @Test void validIntegerBoundariesStayExact() { + Integers values = roundTrip("{\"min\":-2147483648,\"max\":2147483647}", Integers.class); + assertEquals(Integer.MIN_VALUE, values.getAdditionalProperties().get("min")); + assertEquals(Integer.MAX_VALUE, values.getAdditionalProperties().get("max")); + roundTrip("{\"key\":{\"wire_value\":\"nested\",\"native_int\":2147483647}}", Values.class); + } + + @Test void opaqueTreesPreserveNestedNullsAndLargeNumbers() { + String wire = "{\"null_key\":null,\"unknown\":{\"null_value\":null,\"number\":9007199254740993},\"array\":[null,true,1.25],\"huge\":1e10000}"; + JsonValues values = roundTrip(wire, JsonValues.class); + assertSame(JsonNull.INSTANCE, values.getAdditionalProperties().get("null_key")); + assertEquals("9007199254740993", values.getAdditionalProperties().get("unknown").getAsJsonObject().get("number").getAsString()); + assertEquals("1e10000", values.getAdditionalProperties().get("huge").getAsString()); + roundTrip(wire, EmptySchemaValues.class); + } + + @Test void primitiveValuesKeepTheirNativeTypesAndPrecision() { + Numbers numbers = roundTrip("{\"large\":9007199254740993,\"negative\":-2}", Numbers.class); + assertEquals(9007199254740993L, numbers.getAdditionalProperties().get("large")); + Flags flags = roundTrip("{\"yes\":true,\"no\":false}", Flags.class); + assertEquals(Boolean.TRUE, flags.getAdditionalProperties().get("yes")); + assertEquals(Boolean.FALSE, flags.getAdditionalProperties().get("no")); + } + + @Test void nullableDictionaryEntriesRemainPresent() { + NullableValues values = roundTrip("{\"absent_value\":null,\"present\":{\"wire_value\":\"yes\"}}", NullableValues.class); + assertTrue(values.getAdditionalProperties().containsKey("absent_value")); + assertNull(values.getAdditionalProperties().get("absent_value")); + assertEquals("yes", values.getAdditionalProperties().get("present").getWireValue()); + } + + @Test void containerValuesRetainTheirDeclaredElementModels() { + Lists lists = roundTrip("{\"keep_key\":[{\"wire_value\":\"one\"},{\"wire_value\":\"two\"}]}", Lists.class); + assertEquals("two", lists.getAdditionalProperties().get("keep_key").get(1).getWireValue()); + NestedMaps maps = roundTrip("{\"outer_key\":" + VALUES + "}", NestedMaps.class); + assertEquals("nested", maps.getAdditionalProperties().get("outer_key").get("keep_key").getWireValue()); + } + + @Test void nullEntriesAndOpaqueTreesSurviveInsideContainerValues() { + roundTrip("{\"outer\":{\"nil\":null,\"present\":{\"wire_value\":\"yes\"}}}", NestedNullableMaps.class); + roundTrip("{\"outer\":[{\"nested_null\":null,\"number\":9007199254740993},null]}", RawLists.class); + } + + @Test void containingFieldsListsAndAliasesKeepMapTypes() { + String wire = "{\"labels\":{\"keep_key\":\"yes\"},\"values\":" + VALUES + ",\"dictionaries\":[" + VALUES + "],\"aliases\":" + VALUES + ",\"json_values\":{\"nil\":null}}"; + Envelope value = roundTrip(wire, Envelope.class); + assertEquals("yes", value.getLabels().getAdditionalProperties().get("keep_key")); + assertEquals("nested", value.getDictionaries().get(0).getAdditionalProperties().get("keep_key").getWireValue()); + assertEquals("nested", value.getAliases().getAdditionalProperties().get("keep_key").getWireValue()); + assertNull(value.getOrdinaryNull()); + } + + @Test void recursiveModelValuesStayTyped() { + String wire = "{\"root\":{\"wire_value\":\"parent\",\"children\":{\"leaf\":{\"wire_value\":\"child\",\"children\":{}}}}}"; + Nodes nodes = roundTrip(wire, Nodes.class); + assertEquals("child", nodes.getAdditionalProperties().get("root").getChildren().getAdditionalProperties().get("leaf").getWireValue()); + } + + @Test void dictionaryAndUnionBoundariesDelegateNormally() { + roundTrip("{\"keep_key\":\"text\"}", LabelChoice.class); + roundTrip("42", LabelChoice.class); + Choices choices = roundTrip("{\"object\":{\"keep_key\":\"text\"},\"scalar\":42}", Choices.class); + assertInstanceOf(LabelChoice.VariantLabels.class, choices.getAdditionalProperties().get("object")); + assertInstanceOf(LabelChoice.VariantInteger.class, choices.getAdditionalProperties().get("scalar")); + } + + @Test void rootNullAndNonObjectInputDoNotBecomeEmptyDictionaries() { + assertNull(roundTrip("null", Labels.class)); + for (String wire : new String[] {"[]", "42", "\"text\"", "true"}) { + assertThrows(JsonParseException.class, () -> gson.fromJson(wire, Labels.class)); + } + } + + @Test void writerNullPolicyIsRestoredOnSuccessAndFailure() throws Exception { + JsonValues values = gson.fromJson("{\"nil\":null}", JsonValues.class); + TypeAdapter adapter = gson.getAdapter(JsonValues.class); + for (boolean previous : new boolean[] {false, true}) { + StringWriter text = new StringWriter(); + JsonWriter writer = new JsonWriter(text); + writer.setSerializeNulls(previous); + adapter.write(writer, values); + assertEquals(previous, writer.getSerializeNulls()); + assertEquals("{\"nil\":null}", text.toString()); + Writer broken = new Writer() { + public void write(char[] buffer, int offset, int length) throws IOException { + if (new String(buffer, offset, length).contains("null")) throw new IOException("broken null writer"); + } + public void flush() {} + public void close() {} + }; + JsonWriter failing = new JsonWriter(broken); + failing.setSerializeNulls(previous); + assertThrows(IOException.class, () -> adapter.write(failing, values)); + assertEquals(previous, failing.getSerializeNulls()); + } + } + + private DictionaryApi api(int status, String wire) { + OkHttpClient client = new OkHttpClient.Builder().addInterceptor(chain -> { + assertEquals("/labels", chain.request().url().encodedPath()); + okio.Buffer request = new okio.Buffer(); + chain.request().body().writeTo(request); + assertEquals(JsonParser.parseString("{\"keep_key\":\"sent\"}"), JsonParser.parseString(request.readUtf8())); + return new Response.Builder().request(chain.request()).protocol(Protocol.HTTP_1_1) + .code(status).message("Dictionary").header("Content-Type", "application/json") + .body(ResponseBody.create(wire, MediaType.get("application/json"))).build(); + }).build(); + return new DictionaryApi(new ApiClient("https://dictionary.test", client, null, Map.of())); + } + + @Test void httpRequestAndResponseBodiesKeepDictionaryEntries() { + Labels request = new Labels(); + request.getAdditionalProperties().put("keep_key", "sent"); + EchoLabelsResponse response = api(200, "{\"response_key\":\"received\"}").echoLabels(request); + assertEquals("received", response.getStatus200().getAdditionalProperties().get("response_key")); + } + + @Test void typedHttpErrorBodiesRetainNestedModelsAndRawBytes() { + Labels request = new Labels(); + request.getAdditionalProperties().put("keep_key", "sent"); + EchoLabelsException error = assertThrows(EchoLabelsException.class, () -> api(400, VALUES).echoLabels(request)); + EchoLabelsBadRequest detail = assertInstanceOf(EchoLabelsBadRequest.class, error.getDetail()); + assertEquals("nested", detail.body().getAdditionalProperties().get("keep_key").getWireValue()); + assertArrayEquals(VALUES.getBytes(StandardCharsets.UTF_8), error.rawBody()); + } +} diff --git a/tests/generated-clients/jvm/kotlin/cases/dictionary-helper-types/gradle.lockfile b/tests/generated-clients/jvm/kotlin/cases/dictionary-helper-types/gradle.lockfile new file mode 100644 index 00000000..17de2fb2 --- /dev/null +++ b/tests/generated-clients/jvm/kotlin/cases/dictionary-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:dictionary-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/dictionary-helper-types/src/test/java/DictionaryHelperTypesTest.java b/tests/generated-clients/jvm/kotlin/cases/dictionary-helper-types/src/test/java/DictionaryHelperTypesTest.java new file mode 100644 index 00000000..9a449f89 --- /dev/null +++ b/tests/generated-clients/jvm/kotlin/cases/dictionary-helper-types/src/test/java/DictionaryHelperTypesTest.java @@ -0,0 +1,53 @@ +import com.example.sdk.models.*; +import com.google.gson.Gson; +import com.google.gson.JsonParser; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class DictionaryHelperTypesTest { + private final Gson gson = new Gson(); + private static final String WIRE = "{\"key\":{\"wire_value\":\"model\"}}"; + + private T roundTrip(String wire, Class type) { + T value = gson.fromJson(wire, type); + assertEquals(JsonParser.parseString(wire), JsonParser.parseString(gson.toJson(value, type))); + return value; + } + + @Test void typeTokenModelDoesNotResolveToGsonTypeToken() { + TokenValues value = roundTrip(WIRE, TokenValues.class); + assertInstanceOf(com.example.sdk.models.TypeToken.class, value.getAdditionalProperties().get("key")); + assertEquals("model", value.getAdditionalProperties().get("key").getWireValue()); + } + + @Test void jsonElementModelDoesNotBecomeAnOpaqueTree() { + ElementValues value = roundTrip(WIRE, ElementValues.class); + assertInstanceOf(com.example.sdk.models.JsonElement.class, value.getAdditionalProperties().get("key")); + assertEquals("model", value.getAdditionalProperties().get("key").getWireValue()); + } + + @Test void jsonNullModelDoesNotBecomeGsonNull() { + NullValues value = roundTrip(WIRE, NullValues.class); + assertInstanceOf(com.example.sdk.models.JsonNull.class, value.getAdditionalProperties().get("key")); + assertEquals("model", value.getAdditionalProperties().get("key").getWireValue()); + } + + @Test void gsonModelDoesNotReflectIntoGsonInternals() { + GsonValues value = roundTrip(WIRE, GsonValues.class); + assertInstanceOf(com.example.sdk.models.Gson.class, value.getAdditionalProperties().get("key")); + assertEquals("model", value.getAdditionalProperties().get("key").getWireValue()); + } + + @Test void nestedContainersKeepAuthoredModelIdentities() { + NestedValues value = roundTrip("{\"key\":{\"list\":[{\"wire_value\":\"model\"},null]}}", NestedValues.class); + Object item = value.getAdditionalProperties().get("key").get("list").get(0); + assertInstanceOf(com.example.sdk.models.JsonElement.class, item); + assertNull(value.getAdditionalProperties().get("key").get("list").get(1)); + } + + @Test void dictionaryNamesDoNotShadowTheirOwnAdapterHelpers() { + roundTrip("{\"nil\":null,\"raw\":{\"number\":9007199254740993}}", JsonAdapter.class); + roundTrip(WIRE, JsonReader.class); + roundTrip("{\"key\":\"value\"}", TypeAdapter.class); + } +} diff --git a/tests/generated-clients/jvm/kotlin/fixtures.txt b/tests/generated-clients/jvm/kotlin/fixtures.txt index 160504ba..c4ec237a 100644 --- a/tests/generated-clients/jvm/kotlin/fixtures.txt +++ b/tests/generated-clients/jvm/kotlin/fixtures.txt @@ -5,3 +5,5 @@ 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 +dictionaries valid/kotlin-dictionaries.yaml +dictionary-helper-types valid/kotlin-dictionary-helper-types.yaml diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionaries/README.md.golden b/tests/golden/kotlin/kotlin-okhttp/dictionaries/README.md.golden new file mode 100644 index 00000000..3885f51e --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionaries/README.md.golden @@ -0,0 +1,7 @@ +# Kotlin dictionaries + +Generated Kotlin SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `kotlin-dictionaries`. diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionaries/apis/DictionaryApi.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionaries/apis/DictionaryApi.kt.golden new file mode 100644 index 00000000..33fe03b4 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionaries/apis/DictionaryApi.kt.golden @@ -0,0 +1,105 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionaries — 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.toMediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response + +/** + * EchoLabelsResponse carries the response from echoLabels. + */ +data class EchoLabelsResponse(val statusCode: Int, val raw: Response, + val status200: Labels? = null) { +} + +sealed interface EchoLabelsExceptionDetail { + val statusCode: Int + val headers: okhttp3.Headers + fun rawBody(): ByteArray +} + +class EchoLabelsException( + val detail: EchoLabelsExceptionDetail, + 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 EchoLabelsBadRequest( + override val statusCode: Int, + override val headers: okhttp3.Headers, + rawBodyBytes: ByteArray, +) : EchoLabelsExceptionDetail { + 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(): Values = Gson().fromJson(textBody().ifEmpty { "null" }, object : TypeToken() {}.type) +} + +class EchoLabelsUnexpected( + override val statusCode: Int, + override val headers: okhttp3.Headers, + rawBodyBytes: ByteArray, +) : EchoLabelsExceptionDetail { + private val rawBodyBytes: ByteArray = rawBodyBytes.copyOf() + override fun rawBody(): ByteArray = this.rawBodyBytes.copyOf() + fun body(): ByteArray = rawBody() +} + +/** + * DictionaryApi groups operations under the Dictionary tag. + */ +class DictionaryApi(private val client: ApiClient) { + private val gson: Gson = Gson() + + /** + * echoLabels POST /labels. + */ + fun echoLabels(body: Labels): EchoLabelsResponse { + val path = "/labels" + val jsonBody = gson.toJson(body) + val requestBody = jsonBody.toRequestBody("application/json".toMediaType()) + val request = client.newRequestWithBody("POST", path, null, requestBody) + 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: EchoLabelsExceptionDetail = when (response.code) { + 400 -> EchoLabelsBadRequest(statusCode = response.code, headers = response.headers, rawBodyBytes = responseBytes) + else -> { + EchoLabelsUnexpected(statusCode = response.code, headers = response.headers, rawBodyBytes = responseBytes) + } + } + throw EchoLabelsException(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: Labels? = null + + if (response.code == 200) { + status200 = gson.fromJson(responseText.ifEmpty { "null" }, object : TypeToken() {}.type) + } + return EchoLabelsResponse(statusCode = response.code, raw = response, status200 = status200) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionaries/build.gradle.kts.golden b/tests/golden/kotlin/kotlin-okhttp/dictionaries/build.gradle.kts.golden new file mode 100644 index 00000000..093728ca --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionaries/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/dictionaries/models/Alias.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Alias.kt.golden new file mode 100644 index 00000000..26671311 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Alias.kt.golden @@ -0,0 +1,7 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionaries — 1.0.0 + +package com.example.sdk.models + +typealias Alias = Values diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Choices.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Choices.kt.golden new file mode 100644 index 00000000..b129b400 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Choices.kt.golden @@ -0,0 +1,71 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionaries — 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.JsonParseException +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.JsonToken +import com.google.gson.stream.JsonWriter + +@JsonAdapter(ChoicesJsonAdapter::class, nullSafe = false) +data class Choices(val additionalProperties: kotlin.collections.MutableMap = linkedMapOf()) { +} + +private class ChoicesJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): com.example.sdk.models.Choices? { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull() + return null + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw JsonParseException("expected Choices dictionary object") + } + reader.beginObject() + val result = com.example.sdk.models.Choices() + while (reader.hasNext()) { + val key = reader.nextName() + result.additionalProperties[key] = gson.fromJson(reader, object: TypeToken() {}.type) + } + reader.endObject() + return result + } + + override fun write(writer: JsonWriter, value: com.example.sdk.models.Choices?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + for (entry in value.additionalProperties.entries) { + val tree = dictionaryValue0(entry.value) + writer.name(entry.key) + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, tree) + } + finally { + writer.serializeNulls = serializeNulls + } + } + writer.endObject() + } + + private fun dictionaryValue0(value: com.example.sdk.models.LabelChoice?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + return gson.toJsonTree(value, object: TypeToken() {}.type) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/EmptySchemaValues.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/EmptySchemaValues.kt.golden new file mode 100644 index 00000000..05c49ed0 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/EmptySchemaValues.kt.golden @@ -0,0 +1,71 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionaries — 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.JsonParseException +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.JsonToken +import com.google.gson.stream.JsonWriter + +@JsonAdapter(EmptySchemaValuesJsonAdapter::class, nullSafe = false) +data class EmptySchemaValues(val additionalProperties: kotlin.collections.MutableMap = linkedMapOf()) { +} + +private class EmptySchemaValuesJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): com.example.sdk.models.EmptySchemaValues? { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull() + return null + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw JsonParseException("expected EmptySchemaValues dictionary object") + } + reader.beginObject() + val result = com.example.sdk.models.EmptySchemaValues() + while (reader.hasNext()) { + val key = reader.nextName() + result.additionalProperties[key] = gson.fromJson(reader, object: TypeToken() {}.type) + } + reader.endObject() + return result + } + + override fun write(writer: JsonWriter, value: com.example.sdk.models.EmptySchemaValues?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + for (entry in value.additionalProperties.entries) { + val tree = dictionaryValue0(entry.value) + writer.name(entry.key) + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, tree) + } + finally { + writer.serializeNulls = serializeNulls + } + } + writer.endObject() + } + + private fun dictionaryValue0(value: JsonElement?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + return value + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Envelope.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Envelope.kt.golden new file mode 100644 index 00000000..3213eabe --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Envelope.kt.golden @@ -0,0 +1,13 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionaries — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class Envelope(val aliases: Alias, val dictionaries: List, + @field:SerializedName("json_values") + val jsonValues: JsonValues, val labels: Labels, @field:SerializedName("ordinary_null") + val ordinaryNull: String? = null, val values: Values) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Flags.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Flags.kt.golden new file mode 100644 index 00000000..50ac196c --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Flags.kt.golden @@ -0,0 +1,71 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionaries — 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.JsonParseException +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.JsonToken +import com.google.gson.stream.JsonWriter + +@JsonAdapter(FlagsJsonAdapter::class, nullSafe = false) +data class Flags(val additionalProperties: kotlin.collections.MutableMap = linkedMapOf()) { +} + +private class FlagsJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): com.example.sdk.models.Flags? { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull() + return null + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw JsonParseException("expected Flags dictionary object") + } + reader.beginObject() + val result = com.example.sdk.models.Flags() + while (reader.hasNext()) { + val key = reader.nextName() + result.additionalProperties[key] = gson.fromJson(reader, object: TypeToken() {}.type) + } + reader.endObject() + return result + } + + override fun write(writer: JsonWriter, value: com.example.sdk.models.Flags?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + for (entry in value.additionalProperties.entries) { + val tree = dictionaryValue0(entry.value) + writer.name(entry.key) + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, tree) + } + finally { + writer.serializeNulls = serializeNulls + } + } + writer.endObject() + } + + private fun dictionaryValue0(value: kotlin.Boolean?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + return gson.toJsonTree(value, object: TypeToken() {}.type) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Integers.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Integers.kt.golden new file mode 100644 index 00000000..23f6d457 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Integers.kt.golden @@ -0,0 +1,71 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionaries — 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.JsonParseException +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.JsonToken +import com.google.gson.stream.JsonWriter + +@JsonAdapter(IntegersJsonAdapter::class, nullSafe = false) +data class Integers(val additionalProperties: kotlin.collections.MutableMap = linkedMapOf()) { +} + +private class IntegersJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): com.example.sdk.models.Integers? { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull() + return null + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw JsonParseException("expected Integers dictionary object") + } + reader.beginObject() + val result = com.example.sdk.models.Integers() + while (reader.hasNext()) { + val key = reader.nextName() + result.additionalProperties[key] = gson.fromJson(reader, object: TypeToken() {}.type) + } + reader.endObject() + return result + } + + override fun write(writer: JsonWriter, value: com.example.sdk.models.Integers?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + for (entry in value.additionalProperties.entries) { + val tree = dictionaryValue0(entry.value) + writer.name(entry.key) + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, tree) + } + finally { + writer.serializeNulls = serializeNulls + } + } + writer.endObject() + } + + private fun dictionaryValue0(value: kotlin.Int?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + return gson.toJsonTree(value, object: TypeToken() {}.type) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/JsonValues.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/JsonValues.kt.golden new file mode 100644 index 00000000..3fdba2ee --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/JsonValues.kt.golden @@ -0,0 +1,71 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionaries — 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.JsonParseException +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.JsonToken +import com.google.gson.stream.JsonWriter + +@JsonAdapter(JsonValuesJsonAdapter::class, nullSafe = false) +data class JsonValues(val additionalProperties: kotlin.collections.MutableMap = linkedMapOf()) { +} + +private class JsonValuesJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): com.example.sdk.models.JsonValues? { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull() + return null + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw JsonParseException("expected JsonValues dictionary object") + } + reader.beginObject() + val result = com.example.sdk.models.JsonValues() + while (reader.hasNext()) { + val key = reader.nextName() + result.additionalProperties[key] = gson.fromJson(reader, object: TypeToken() {}.type) + } + reader.endObject() + return result + } + + override fun write(writer: JsonWriter, value: com.example.sdk.models.JsonValues?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + for (entry in value.additionalProperties.entries) { + val tree = dictionaryValue0(entry.value) + writer.name(entry.key) + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, tree) + } + finally { + writer.serializeNulls = serializeNulls + } + } + writer.endObject() + } + + private fun dictionaryValue0(value: JsonElement?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + return value + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/LabelChoice.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/LabelChoice.kt.golden new file mode 100644 index 00000000..c2a398ab --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/LabelChoice.kt.golden @@ -0,0 +1,128 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionaries — 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(LabelChoiceJsonAdapter::class, nullSafe = false) +sealed class LabelChoice private constructor() { + data class VariantLabels(val value0: Labels) : LabelChoice() + + data class VariantInteger(val value0: Int) : LabelChoice() +} + +private class LabelChoiceJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): LabelChoice? { + 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 LabelChoice.VariantLabels(gson.fromJson(element, object: TypeToken() {}.type)) + } + if (selected == 1) { + return LabelChoice.VariantInteger(element.asInt) + } + throw JsonParseException("unknown LabelChoice union value") + } + + override fun write(writer: JsonWriter, value: LabelChoice?) { + if (value == null) { + writer.nullValue() + return + } + when (value) { + is LabelChoice.VariantLabels -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + is LabelChoice.VariantInteger -> { + gson.toJson(value.value0, object: TypeToken() {}.type, writer) + } + } + } + + private fun matches0(candidate: JsonElement): kotlin.Boolean { + if (!candidate.isJsonObject) { + return false + } + for (item in candidate.asJsonObject.asMap().values) { + if (!matches1(item)) { + 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 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/dictionaries/models/Labels.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Labels.kt.golden new file mode 100644 index 00000000..7cc6ebfa --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Labels.kt.golden @@ -0,0 +1,74 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionaries — 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.JsonParseException +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.JsonToken +import com.google.gson.stream.JsonWriter + +/** + * Verbatim labels indexed by their wire keys. + */ +@JsonAdapter(LabelsJsonAdapter::class, nullSafe = false) +data class Labels(val additionalProperties: kotlin.collections.MutableMap = linkedMapOf()) { +} + +private class LabelsJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): com.example.sdk.models.Labels? { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull() + return null + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw JsonParseException("expected Labels dictionary object") + } + reader.beginObject() + val result = com.example.sdk.models.Labels() + while (reader.hasNext()) { + val key = reader.nextName() + result.additionalProperties[key] = gson.fromJson(reader, object: TypeToken() {}.type) + } + reader.endObject() + return result + } + + override fun write(writer: JsonWriter, value: com.example.sdk.models.Labels?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + for (entry in value.additionalProperties.entries) { + val tree = dictionaryValue0(entry.value) + writer.name(entry.key) + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, tree) + } + finally { + writer.serializeNulls = serializeNulls + } + } + writer.endObject() + } + + private fun dictionaryValue0(value: kotlin.String?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + return gson.toJsonTree(value, object: TypeToken() {}.type) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Lists.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Lists.kt.golden new file mode 100644 index 00000000..d78007de --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Lists.kt.golden @@ -0,0 +1,83 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionaries — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonParseException +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.JsonToken +import com.google.gson.stream.JsonWriter + +@JsonAdapter(ListsJsonAdapter::class, nullSafe = false) +data class Lists(val additionalProperties: kotlin.collections.MutableMap> = linkedMapOf()) { +} + +private class ListsJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): com.example.sdk.models.Lists? { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull() + return null + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw JsonParseException("expected Lists dictionary object") + } + reader.beginObject() + val result = com.example.sdk.models.Lists() + while (reader.hasNext()) { + val key = reader.nextName() + result.additionalProperties[key] = gson.fromJson(reader, object: TypeToken>() {}.type) + } + reader.endObject() + return result + } + + override fun write(writer: JsonWriter, value: com.example.sdk.models.Lists?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + for (entry in value.additionalProperties.entries) { + val tree = dictionaryValue1(entry.value) + writer.name(entry.key) + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, tree) + } + finally { + writer.serializeNulls = serializeNulls + } + } + writer.endObject() + } + + private fun dictionaryValue0(value: com.example.sdk.models.NestedValue?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + return gson.toJsonTree(value, object: TypeToken() {}.type) + } + + private fun dictionaryValue1(value: kotlin.collections.List?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + val result = JsonArray() + for (item in value) { + result.add(dictionaryValue0(item)) + } + return result + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/NestedMaps.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/NestedMaps.kt.golden new file mode 100644 index 00000000..3696456e --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/NestedMaps.kt.golden @@ -0,0 +1,85 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionaries — 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.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken +import com.google.gson.stream.JsonWriter + +@JsonAdapter(NestedMapsJsonAdapter::class, nullSafe = false) +data class NestedMaps(val additionalProperties: kotlin.collections.MutableMap> = linkedMapOf()) { +} + +private class NestedMapsJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): com.example.sdk.models.NestedMaps? { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull() + return null + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw JsonParseException("expected NestedMaps dictionary object") + } + reader.beginObject() + val result = com.example.sdk.models.NestedMaps() + while (reader.hasNext()) { + val key = reader.nextName() + result.additionalProperties[key] = gson.fromJson(reader, object: TypeToken>() {}.type) + } + reader.endObject() + return result + } + + override fun write(writer: JsonWriter, value: com.example.sdk.models.NestedMaps?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + for (entry in value.additionalProperties.entries) { + val tree = dictionaryValue1(entry.value) + writer.name(entry.key) + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, tree) + } + finally { + writer.serializeNulls = serializeNulls + } + } + writer.endObject() + } + + private fun dictionaryValue0(value: com.example.sdk.models.NestedValue?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + return gson.toJsonTree(value, object: TypeToken() {}.type) + } + + private fun dictionaryValue1(value: kotlin.collections.Map?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + val result = JsonObject() + for (entry in value.entries) { + result.add(entry.key, dictionaryValue0(entry.value)) + } + return result + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/NestedNullableMaps.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/NestedNullableMaps.kt.golden new file mode 100644 index 00000000..80675321 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/NestedNullableMaps.kt.golden @@ -0,0 +1,85 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionaries — 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.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken +import com.google.gson.stream.JsonWriter + +@JsonAdapter(NestedNullableMapsJsonAdapter::class, nullSafe = false) +data class NestedNullableMaps(val additionalProperties: kotlin.collections.MutableMap> = linkedMapOf()) { +} + +private class NestedNullableMapsJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): com.example.sdk.models.NestedNullableMaps? { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull() + return null + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw JsonParseException("expected NestedNullableMaps dictionary object") + } + reader.beginObject() + val result = com.example.sdk.models.NestedNullableMaps() + while (reader.hasNext()) { + val key = reader.nextName() + result.additionalProperties[key] = gson.fromJson(reader, object: TypeToken>() {}.type) + } + reader.endObject() + return result + } + + override fun write(writer: JsonWriter, value: com.example.sdk.models.NestedNullableMaps?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + for (entry in value.additionalProperties.entries) { + val tree = dictionaryValue1(entry.value) + writer.name(entry.key) + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, tree) + } + finally { + writer.serializeNulls = serializeNulls + } + } + writer.endObject() + } + + private fun dictionaryValue0(value: com.example.sdk.models.NestedValue?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + return gson.toJsonTree(value, object: TypeToken() {}.type) + } + + private fun dictionaryValue1(value: kotlin.collections.Map?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + val result = JsonObject() + for (entry in value.entries) { + result.add(entry.key, dictionaryValue0(entry.value)) + } + return result + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/NestedValue.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/NestedValue.kt.golden new file mode 100644 index 00000000..d27a26d7 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/NestedValue.kt.golden @@ -0,0 +1,13 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionaries — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class NestedValue(@field:SerializedName("native_int") + val nativeInt: Int? = null, @field:SerializedName("ordinary_null") + val ordinaryNull: String? = null, @field:SerializedName("wire_value") + val wireValue: String) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Node.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Node.kt.golden new file mode 100644 index 00000000..33f5fc24 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Node.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionaries — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class Node(val children: Nodes, @field:SerializedName("wire_value") + val wireValue: String) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Nodes.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Nodes.kt.golden new file mode 100644 index 00000000..00274817 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Nodes.kt.golden @@ -0,0 +1,71 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionaries — 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.JsonParseException +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.JsonToken +import com.google.gson.stream.JsonWriter + +@JsonAdapter(NodesJsonAdapter::class, nullSafe = false) +data class Nodes(val additionalProperties: kotlin.collections.MutableMap = linkedMapOf()) { +} + +private class NodesJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): com.example.sdk.models.Nodes? { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull() + return null + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw JsonParseException("expected Nodes dictionary object") + } + reader.beginObject() + val result = com.example.sdk.models.Nodes() + while (reader.hasNext()) { + val key = reader.nextName() + result.additionalProperties[key] = gson.fromJson(reader, object: TypeToken() {}.type) + } + reader.endObject() + return result + } + + override fun write(writer: JsonWriter, value: com.example.sdk.models.Nodes?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + for (entry in value.additionalProperties.entries) { + val tree = dictionaryValue0(entry.value) + writer.name(entry.key) + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, tree) + } + finally { + writer.serializeNulls = serializeNulls + } + } + writer.endObject() + } + + private fun dictionaryValue0(value: com.example.sdk.models.Node?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + return gson.toJsonTree(value, object: TypeToken() {}.type) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/NullableValues.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/NullableValues.kt.golden new file mode 100644 index 00000000..d2196020 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/NullableValues.kt.golden @@ -0,0 +1,71 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionaries — 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.JsonParseException +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.JsonToken +import com.google.gson.stream.JsonWriter + +@JsonAdapter(NullableValuesJsonAdapter::class, nullSafe = false) +data class NullableValues(val additionalProperties: kotlin.collections.MutableMap = linkedMapOf()) { +} + +private class NullableValuesJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): com.example.sdk.models.NullableValues? { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull() + return null + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw JsonParseException("expected NullableValues dictionary object") + } + reader.beginObject() + val result = com.example.sdk.models.NullableValues() + while (reader.hasNext()) { + val key = reader.nextName() + result.additionalProperties[key] = gson.fromJson(reader, object: TypeToken() {}.type) + } + reader.endObject() + return result + } + + override fun write(writer: JsonWriter, value: com.example.sdk.models.NullableValues?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + for (entry in value.additionalProperties.entries) { + val tree = dictionaryValue0(entry.value) + writer.name(entry.key) + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, tree) + } + finally { + writer.serializeNulls = serializeNulls + } + } + writer.endObject() + } + + private fun dictionaryValue0(value: com.example.sdk.models.NestedValue?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + return gson.toJsonTree(value, object: TypeToken() {}.type) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Numbers.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Numbers.kt.golden new file mode 100644 index 00000000..5ca00400 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Numbers.kt.golden @@ -0,0 +1,71 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionaries — 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.JsonParseException +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.JsonToken +import com.google.gson.stream.JsonWriter + +@JsonAdapter(NumbersJsonAdapter::class, nullSafe = false) +data class Numbers(val additionalProperties: kotlin.collections.MutableMap = linkedMapOf()) { +} + +private class NumbersJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): com.example.sdk.models.Numbers? { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull() + return null + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw JsonParseException("expected Numbers dictionary object") + } + reader.beginObject() + val result = com.example.sdk.models.Numbers() + while (reader.hasNext()) { + val key = reader.nextName() + result.additionalProperties[key] = gson.fromJson(reader, object: TypeToken() {}.type) + } + reader.endObject() + return result + } + + override fun write(writer: JsonWriter, value: com.example.sdk.models.Numbers?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + for (entry in value.additionalProperties.entries) { + val tree = dictionaryValue0(entry.value) + writer.name(entry.key) + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, tree) + } + finally { + writer.serializeNulls = serializeNulls + } + } + writer.endObject() + } + + private fun dictionaryValue0(value: kotlin.Long?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + return gson.toJsonTree(value, object: TypeToken() {}.type) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/RawLists.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/RawLists.kt.golden new file mode 100644 index 00000000..9d056a4b --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/RawLists.kt.golden @@ -0,0 +1,83 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionaries — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonParseException +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.JsonToken +import com.google.gson.stream.JsonWriter + +@JsonAdapter(RawListsJsonAdapter::class, nullSafe = false) +data class RawLists(val additionalProperties: kotlin.collections.MutableMap> = linkedMapOf()) { +} + +private class RawListsJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): com.example.sdk.models.RawLists? { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull() + return null + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw JsonParseException("expected RawLists dictionary object") + } + reader.beginObject() + val result = com.example.sdk.models.RawLists() + while (reader.hasNext()) { + val key = reader.nextName() + result.additionalProperties[key] = gson.fromJson(reader, object: TypeToken>() {}.type) + } + reader.endObject() + return result + } + + override fun write(writer: JsonWriter, value: com.example.sdk.models.RawLists?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + for (entry in value.additionalProperties.entries) { + val tree = dictionaryValue1(entry.value) + writer.name(entry.key) + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, tree) + } + finally { + writer.serializeNulls = serializeNulls + } + } + writer.endObject() + } + + private fun dictionaryValue0(value: JsonElement?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + return value + } + + private fun dictionaryValue1(value: kotlin.collections.List?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + val result = JsonArray() + for (item in value) { + result.add(dictionaryValue0(item)) + } + return result + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/ValueHolder.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/ValueHolder.kt.golden new file mode 100644 index 00000000..6224eeee --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/ValueHolder.kt.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionaries — 1.0.0 + +package com.example.sdk.models + +data class ValueHolder(val value: NestedValue? = null) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Values.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Values.kt.golden new file mode 100644 index 00000000..c9d95925 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionaries/models/Values.kt.golden @@ -0,0 +1,71 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionaries — 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.JsonParseException +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.JsonToken +import com.google.gson.stream.JsonWriter + +@JsonAdapter(ValuesJsonAdapter::class, nullSafe = false) +data class Values(val additionalProperties: kotlin.collections.MutableMap = linkedMapOf()) { +} + +private class ValuesJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): com.example.sdk.models.Values? { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull() + return null + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw JsonParseException("expected Values dictionary object") + } + reader.beginObject() + val result = com.example.sdk.models.Values() + while (reader.hasNext()) { + val key = reader.nextName() + result.additionalProperties[key] = gson.fromJson(reader, object: TypeToken() {}.type) + } + reader.endObject() + return result + } + + override fun write(writer: JsonWriter, value: com.example.sdk.models.Values?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + for (entry in value.additionalProperties.entries) { + val tree = dictionaryValue0(entry.value) + writer.name(entry.key) + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, tree) + } + finally { + writer.serializeNulls = serializeNulls + } + } + writer.endObject() + } + + private fun dictionaryValue0(value: com.example.sdk.models.NestedValue?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + return gson.toJsonTree(value, object: TypeToken() {}.type) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionaries/runtime/ApiClient.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionaries/runtime/ApiClient.kt.golden new file mode 100644 index 00000000..fbe10b60 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionaries/runtime/ApiClient.kt.golden @@ -0,0 +1,61 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionaries — 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/dictionaries/runtime/ApiException.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionaries/runtime/ApiException.kt.golden new file mode 100644 index 00000000..c76652cd --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionaries/runtime/ApiException.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionaries — 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/dictionaries/runtime/Auth.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionaries/runtime/Auth.kt.golden new file mode 100644 index 00000000..6afec8a8 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionaries/runtime/Auth.kt.golden @@ -0,0 +1,70 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionaries — 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/dictionary-helper-types/README.md.golden b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/README.md.golden new file mode 100644 index 00000000..491346bd --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/README.md.golden @@ -0,0 +1,7 @@ +# Kotlin dictionary helper types + +Generated Kotlin SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `kotlin-dictionary-helper-types`. diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/build.gradle.kts.golden b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/build.gradle.kts.golden new file mode 100644 index 00000000..093728ca --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionary-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/dictionary-helper-types/models/ElementValues.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/ElementValues.kt.golden new file mode 100644 index 00000000..79c0af54 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/ElementValues.kt.golden @@ -0,0 +1,71 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionary 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.JsonParseException +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.JsonToken +import com.google.gson.stream.JsonWriter + +@JsonAdapter(ElementValuesJsonAdapter::class, nullSafe = false) +data class ElementValues(val additionalProperties: kotlin.collections.MutableMap = linkedMapOf()) { +} + +private class ElementValuesJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): com.example.sdk.models.ElementValues? { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull() + return null + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw JsonParseException("expected ElementValues dictionary object") + } + reader.beginObject() + val result = com.example.sdk.models.ElementValues() + while (reader.hasNext()) { + val key = reader.nextName() + result.additionalProperties[key] = gson.fromJson(reader, object: TypeToken() {}.type) + } + reader.endObject() + return result + } + + override fun write(writer: JsonWriter, value: com.example.sdk.models.ElementValues?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + for (entry in value.additionalProperties.entries) { + val tree = dictionaryValue0(entry.value) + writer.name(entry.key) + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, tree) + } + finally { + writer.serializeNulls = serializeNulls + } + } + writer.endObject() + } + + private fun dictionaryValue0(value: com.example.sdk.models.JsonElement?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + return gson.toJsonTree(value, object: TypeToken() {}.type) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/Gson.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/Gson.kt.golden new file mode 100644 index 00000000..2372dc75 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/Gson.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionary helper types — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class Gson(@field:SerializedName("wire_value") + val wireValue: String) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/GsonValues.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/GsonValues.kt.golden new file mode 100644 index 00000000..35a58140 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/GsonValues.kt.golden @@ -0,0 +1,71 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionary 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.JsonParseException +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.JsonToken +import com.google.gson.stream.JsonWriter + +@JsonAdapter(GsonValuesJsonAdapter::class, nullSafe = false) +data class GsonValues(val additionalProperties: kotlin.collections.MutableMap = linkedMapOf()) { +} + +private class GsonValuesJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): com.example.sdk.models.GsonValues? { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull() + return null + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw JsonParseException("expected GsonValues dictionary object") + } + reader.beginObject() + val result = com.example.sdk.models.GsonValues() + while (reader.hasNext()) { + val key = reader.nextName() + result.additionalProperties[key] = gson.fromJson(reader, object: TypeToken() {}.type) + } + reader.endObject() + return result + } + + override fun write(writer: JsonWriter, value: com.example.sdk.models.GsonValues?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + for (entry in value.additionalProperties.entries) { + val tree = dictionaryValue0(entry.value) + writer.name(entry.key) + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, tree) + } + finally { + writer.serializeNulls = serializeNulls + } + } + writer.endObject() + } + + private fun dictionaryValue0(value: com.example.sdk.models.Gson?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + return gson.toJsonTree(value, object: TypeToken() {}.type) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/JsonAdapter.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/JsonAdapter.kt.golden new file mode 100644 index 00000000..e9ccb649 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/JsonAdapter.kt.golden @@ -0,0 +1,71 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionary 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.JsonParseException +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.JsonToken +import com.google.gson.stream.JsonWriter + +@JsonAdapter(JsonAdapterJsonAdapter::class, nullSafe = false) +data class JsonAdapter(val additionalProperties: kotlin.collections.MutableMap = linkedMapOf()) { +} + +private class JsonAdapterJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): com.example.sdk.models.JsonAdapter? { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull() + return null + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw JsonParseException("expected JsonAdapter dictionary object") + } + reader.beginObject() + val result = com.example.sdk.models.JsonAdapter() + while (reader.hasNext()) { + val key = reader.nextName() + result.additionalProperties[key] = gson.fromJson(reader, object: TypeToken() {}.type) + } + reader.endObject() + return result + } + + override fun write(writer: JsonWriter, value: com.example.sdk.models.JsonAdapter?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + for (entry in value.additionalProperties.entries) { + val tree = dictionaryValue0(entry.value) + writer.name(entry.key) + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, tree) + } + finally { + writer.serializeNulls = serializeNulls + } + } + writer.endObject() + } + + private fun dictionaryValue0(value: JsonElement?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + return value + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/JsonElement.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/JsonElement.kt.golden new file mode 100644 index 00000000..e8306f20 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/JsonElement.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionary helper types — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class JsonElement(@field:SerializedName("wire_value") + val wireValue: String) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/JsonNull.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/JsonNull.kt.golden new file mode 100644 index 00000000..d4e16dda --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/JsonNull.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionary helper types — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class JsonNull(@field:SerializedName("wire_value") + val wireValue: String) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/JsonReader.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/JsonReader.kt.golden new file mode 100644 index 00000000..9c73c874 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/JsonReader.kt.golden @@ -0,0 +1,71 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionary 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.JsonParseException +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.JsonToken +import com.google.gson.stream.JsonWriter + +@JsonAdapter(JsonReaderJsonAdapter::class, nullSafe = false) +data class JsonReader(val additionalProperties: kotlin.collections.MutableMap = linkedMapOf()) { +} + +private class JsonReaderJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): com.example.sdk.models.JsonReader? { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull() + return null + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw JsonParseException("expected JsonReader dictionary object") + } + reader.beginObject() + val result = com.example.sdk.models.JsonReader() + while (reader.hasNext()) { + val key = reader.nextName() + result.additionalProperties[key] = gson.fromJson(reader, object: TypeToken() {}.type) + } + reader.endObject() + return result + } + + override fun write(writer: JsonWriter, value: com.example.sdk.models.JsonReader?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + for (entry in value.additionalProperties.entries) { + val tree = dictionaryValue0(entry.value) + writer.name(entry.key) + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, tree) + } + finally { + writer.serializeNulls = serializeNulls + } + } + writer.endObject() + } + + private fun dictionaryValue0(value: com.example.sdk.models.TypeToken?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + return gson.toJsonTree(value, object: TypeToken() {}.type) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/NestedValues.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/NestedValues.kt.golden new file mode 100644 index 00000000..ab73c77e --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/NestedValues.kt.golden @@ -0,0 +1,98 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionary helper types — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.Gson +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +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.JsonToken +import com.google.gson.stream.JsonWriter + +@JsonAdapter(NestedValuesJsonAdapter::class, nullSafe = false) +data class NestedValues(val additionalProperties: kotlin.collections.MutableMap>> = linkedMapOf()) { +} + +private class NestedValuesJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): com.example.sdk.models.NestedValues? { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull() + return null + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw JsonParseException("expected NestedValues dictionary object") + } + reader.beginObject() + val result = com.example.sdk.models.NestedValues() + while (reader.hasNext()) { + val key = reader.nextName() + result.additionalProperties[key] = gson.fromJson(reader, object: TypeToken>>() {}.type) + } + reader.endObject() + return result + } + + override fun write(writer: JsonWriter, value: com.example.sdk.models.NestedValues?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + for (entry in value.additionalProperties.entries) { + val tree = dictionaryValue2(entry.value) + writer.name(entry.key) + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, tree) + } + finally { + writer.serializeNulls = serializeNulls + } + } + writer.endObject() + } + + private fun dictionaryValue0(value: com.example.sdk.models.JsonElement?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + return gson.toJsonTree(value, object: TypeToken() {}.type) + } + + private fun dictionaryValue1(value: kotlin.collections.List?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + val result = JsonArray() + for (item in value) { + result.add(dictionaryValue0(item)) + } + return result + } + + private fun dictionaryValue2(value: kotlin.collections.Map>?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + val result = JsonObject() + for (entry in value.entries) { + result.add(entry.key, dictionaryValue1(entry.value)) + } + return result + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/NullValues.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/NullValues.kt.golden new file mode 100644 index 00000000..3c3c8df8 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/NullValues.kt.golden @@ -0,0 +1,71 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionary 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.JsonParseException +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.JsonToken +import com.google.gson.stream.JsonWriter + +@JsonAdapter(NullValuesJsonAdapter::class, nullSafe = false) +data class NullValues(val additionalProperties: kotlin.collections.MutableMap = linkedMapOf()) { +} + +private class NullValuesJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): com.example.sdk.models.NullValues? { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull() + return null + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw JsonParseException("expected NullValues dictionary object") + } + reader.beginObject() + val result = com.example.sdk.models.NullValues() + while (reader.hasNext()) { + val key = reader.nextName() + result.additionalProperties[key] = gson.fromJson(reader, object: TypeToken() {}.type) + } + reader.endObject() + return result + } + + override fun write(writer: JsonWriter, value: com.example.sdk.models.NullValues?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + for (entry in value.additionalProperties.entries) { + val tree = dictionaryValue0(entry.value) + writer.name(entry.key) + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, tree) + } + finally { + writer.serializeNulls = serializeNulls + } + } + writer.endObject() + } + + private fun dictionaryValue0(value: com.example.sdk.models.JsonNull?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + return gson.toJsonTree(value, object: TypeToken() {}.type) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/TokenValues.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/TokenValues.kt.golden new file mode 100644 index 00000000..ad0c82f4 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/TokenValues.kt.golden @@ -0,0 +1,71 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionary 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.JsonParseException +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.JsonToken +import com.google.gson.stream.JsonWriter + +@JsonAdapter(TokenValuesJsonAdapter::class, nullSafe = false) +data class TokenValues(val additionalProperties: kotlin.collections.MutableMap = linkedMapOf()) { +} + +private class TokenValuesJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): com.example.sdk.models.TokenValues? { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull() + return null + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw JsonParseException("expected TokenValues dictionary object") + } + reader.beginObject() + val result = com.example.sdk.models.TokenValues() + while (reader.hasNext()) { + val key = reader.nextName() + result.additionalProperties[key] = gson.fromJson(reader, object: TypeToken() {}.type) + } + reader.endObject() + return result + } + + override fun write(writer: JsonWriter, value: com.example.sdk.models.TokenValues?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + for (entry in value.additionalProperties.entries) { + val tree = dictionaryValue0(entry.value) + writer.name(entry.key) + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, tree) + } + finally { + writer.serializeNulls = serializeNulls + } + } + writer.endObject() + } + + private fun dictionaryValue0(value: com.example.sdk.models.TypeToken?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + return gson.toJsonTree(value, object: TypeToken() {}.type) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/TypeAdapter.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/TypeAdapter.kt.golden new file mode 100644 index 00000000..7eb68d6e --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/TypeAdapter.kt.golden @@ -0,0 +1,71 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionary 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.JsonParseException +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.JsonToken +import com.google.gson.stream.JsonWriter + +@JsonAdapter(TypeAdapterJsonAdapter::class, nullSafe = false) +data class TypeAdapter(val additionalProperties: kotlin.collections.MutableMap = linkedMapOf()) { +} + +private class TypeAdapterJsonAdapter : TypeAdapter() { + private val gson: Gson = Gson() + + override fun read(reader: JsonReader): com.example.sdk.models.TypeAdapter? { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull() + return null + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw JsonParseException("expected TypeAdapter dictionary object") + } + reader.beginObject() + val result = com.example.sdk.models.TypeAdapter() + while (reader.hasNext()) { + val key = reader.nextName() + result.additionalProperties[key] = gson.fromJson(reader, object: TypeToken() {}.type) + } + reader.endObject() + return result + } + + override fun write(writer: JsonWriter, value: com.example.sdk.models.TypeAdapter?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + for (entry in value.additionalProperties.entries) { + val tree = dictionaryValue0(entry.value) + writer.name(entry.key) + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true + try { + gson.getAdapter(JsonElement::class.java).write(writer, tree) + } + finally { + writer.serializeNulls = serializeNulls + } + } + writer.endObject() + } + + private fun dictionaryValue0(value: kotlin.String?): JsonElement { + if (value == null) { + return JsonNull.INSTANCE + } + return gson.toJsonTree(value, object: TypeToken() {}.type) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/TypeToken.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/TypeToken.kt.golden new file mode 100644 index 00000000..4cc8a890 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/models/TypeToken.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionary helper types — 1.0.0 + +package com.example.sdk.models + +import com.google.gson.annotations.SerializedName + +data class TypeToken(@field:SerializedName("wire_value") + val wireValue: String) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/runtime/ApiClient.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/runtime/ApiClient.kt.golden new file mode 100644 index 00000000..6b014342 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/runtime/ApiClient.kt.golden @@ -0,0 +1,61 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionary 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/dictionary-helper-types/runtime/ApiException.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/runtime/ApiException.kt.golden new file mode 100644 index 00000000..03b99f63 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/runtime/ApiException.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionary 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/dictionary-helper-types/runtime/Auth.kt.golden b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/runtime/Auth.kt.golden new file mode 100644 index 00000000..0badfdba --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/dictionary-helper-types/runtime/Auth.kt.golden @@ -0,0 +1,70 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Kotlin dictionary 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_tests_kotlin_okhttp.rs b/tests/golden_tests_kotlin_okhttp.rs index 8dd480b9..99efd370 100644 --- a/tests/golden_tests_kotlin_okhttp.rs +++ b/tests/golden_tests_kotlin_okhttp.rs @@ -91,6 +91,8 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("query-param-enum", "valid/query/query-param-enum.yaml"), ("server-path-prefix", "valid/server-path-prefix.yaml"), ("reserved-word-fields", "valid/reserved-word-fields.yaml"), + ("dictionaries", "valid/kotlin-dictionaries.yaml"), + ("dictionary-helper-types", "valid/kotlin-dictionary-helper-types.yaml"), ]) } @@ -184,6 +186,8 @@ generate_golden_tests! { test_query_param_enum_golden: "query-param-enum", test_server_path_prefix_golden: "server-path-prefix", test_reserved_word_fields_golden: "reserved-word-fields", + test_dictionaries_golden: "dictionaries", + test_dictionary_helper_types_golden: "dictionary-helper-types", } #[test] diff --git a/tests/kotlin_dictionaries.rs b/tests/kotlin_dictionaries.rs new file mode 100644 index 00000000..1b41241d --- /dev/null +++ b/tests/kotlin_dictionaries.rs @@ -0,0 +1,92 @@ +use openapi_nexus::codegen::traits::code_generator::CodeGenerator; +use openapi_nexus::generators::kotlin::okhttp::KotlinOkhttpCodeGenerator; +use openapi_nexus::ir::{lower::lower, types::IrSchemaKind}; +use openapi_nexus::parser::parse_content_yaml; + +#[test] +fn explicit_dictionary_models_generate_without_empty_data_classes() { + for version in ["3.0.3", "3.1.0", "3.2.0"] { + let source = format!( + "openapi: {version}\ninfo: {{title: Dictionary, version: '1'}}\npaths: {{}}\ncomponents:\n schemas:\n Labels:\n type: object\n additionalProperties: {{type: string}}\n" + ); + let ir = lower(parse_content_yaml(&source).unwrap()).unwrap(); + let IrSchemaKind::Object(object) = &ir.schemas["Labels"].kind else { + panic!("named dictionaries retain object facts"); + }; + assert!(object.properties.is_empty()); + assert!(object.additional_properties.is_some()); + let files = KotlinOkhttpCodeGenerator::new(Default::default()) + .generate(&ir) + .expect("a valid named dictionary must generate"); + let labels = files + .iter() + .find(|file| file.filename == "Labels.kt") + .unwrap(); + assert!(labels.content.contains("class Labels")); + assert!(!labels.content.contains("data class Labels()")); + let tokens = labels.content.split_whitespace().collect::(); + assert!(tokens.contains("kotlin.collections.MutableMap")); + } +} + +#[test] +fn dictionary_values_keep_semantic_model_container_and_raw_types() { + let ir = + lower(parse_content_yaml(include_str!("fixtures/valid/kotlin-dictionaries.yaml")).unwrap()) + .unwrap(); + let files = KotlinOkhttpCodeGenerator::new(Default::default()) + .generate(&ir) + .unwrap(); + for (name, value) in [ + ("Values.kt", "com.example.sdk.models.NestedValue"), + ( + "Lists.kt", + "kotlin.collections.List", + ), + ( + "NestedMaps.kt", + "kotlin.collections.Map", + ), + ("NullableValues.kt", "com.example.sdk.models.NestedValue?"), + ("JsonValues.kt", "JsonElement"), + ] { + let file = files.iter().find(|file| file.filename == name).unwrap(); + let tokens = file.content.split_whitespace().collect::(); + let expected = format!("kotlin.collections.MutableMap"); + assert!( + tokens.contains(&expected.split_whitespace().collect::()), + "{name}" + ); + } +} + +#[test] +fn dictionary_model_qualification_respects_the_configured_package() { + let ir = lower( + parse_content_yaml(include_str!( + "fixtures/valid/kotlin-dictionary-helper-types.yaml" + )) + .unwrap(), + ) + .unwrap(); + let config = toml::from_str("package_name = 'org.example.dictionary'").unwrap(); + let files = KotlinOkhttpCodeGenerator::new(config) + .generate(&ir) + .unwrap(); + for (name, model) in [ + ("TokenValues.kt", "TypeToken"), + ("ElementValues.kt", "JsonElement"), + ("NullValues.kt", "JsonNull"), + ("GsonValues.kt", "Gson"), + ] { + let file = files.iter().find(|file| file.filename == name).unwrap(); + let tokens = file.content.split_whitespace().collect::(); + assert!( + tokens.contains(&format!( + "kotlin.collections.MutableMap" + )), + "{name} uses the authored model in the configured package" + ); + assert!(!file.content.contains("com.example.sdk")); + } +}