diff --git a/docs/src/java_config.md b/docs/src/java_config.md index c448d10b1..84850414c 100644 --- a/docs/src/java_config.md +++ b/docs/src/java_config.md @@ -49,7 +49,7 @@ not an alias-codec or multi-member `allOf` redesign. Tagged unions and ordinary models keep their existing APIs and codecs. Typed variants retain declared member payloads, not arbitrary unknown fields. -Dictionary/additional-property repairs remain separate. Use raw mode when all +Explicit additional-property storage is described below. Use raw mode when all JSON must be retained, including unknown keys, null-valued object entries, and large numeric literals. Raw writing restores the surrounding Gson writer's null policy; ordinary sibling model fields keep their normal null omission. @@ -60,3 +60,44 @@ The policy can also be set through the CLI: openapi-nexus generate --input api.yaml --output sdk --generators java-okhttp \ --generator-config java-okhttp.untagged_union_strategy=strict ``` + +## Additional properties + +Objects with explicit `additionalProperties` expose typed, mutable storage through +`getAdditionalProperties()`. This includes pure dictionaries with no named fields; +they remain ordinary model classes rather than `Map` subtypes. Existing named +constructor parameters and getters stay unchanged, with an independent empty map +available on each constructed or decoded object. + +```java +MixedMap value = new MixedMap("fixed"); +value.getAdditionalProperties().put("keep_key", new NestedValue("nested")); +// Gson writes {"known_field":"fixed","keep_key":{"wire_value":"nested"}} +``` + +The Gson adapter flattens extensions into the same JSON object, retaining keys +verbatim. Opaque values (`true` or `{}`) use `JsonElement` so null entries and large +numeric literals survive without coercion to `Object`/double. Declared typed +values use their native Gson adapters, including model/enum/nullable values, +containers, recursive references and configured untagged-union policies. + +Storage is transient to the ordinary Gson model delegate. Known fields retain +their configured codecs, native scalar failures and null omission; extension +entries preserve their own nulls without changing surrounding writer settings. +The runtime distinguishes Gson's streaming and tree readers and uses its native +tree writer so their existing coercion and configured numeric output stay intact. +This support depends on the tree reader/writer classes in the unchanged Gson +2.11.0 dependency; native contracts cover both input modes. +Aliases keep their existing `value` wrapper instead of acquiring a different wire +shape. No generated dependency version or general validation policy changes. + +If an authored field/getter already uses the storage name, the storage and getter +receive a numeric suffix, such as `additionalProperties2` and +`getAdditionalProperties2()`. Authored members are not renamed. An extension key +equal to a declared wire key raises `IllegalArgumentException` on serialization, +even if the named field is currently omitted, rather than silently overwriting it. + +Closed objects and those without explicit additional properties keep their +existing behavior. Shared lowering, general inline-composition type loss, +multi-member `allOf`, tagged-wrapper extension storage and generation formatting +are separate concerns, not redesigned by this object-storage repair. diff --git a/src/generators/java/okhttp/additional_properties.rs b/src/generators/java/okhttp/additional_properties.rs new file mode 100644 index 000000000..90d4fc6af --- /dev/null +++ b/src/generators/java/okhttp/additional_properties.rs @@ -0,0 +1,252 @@ +//! Explicit typed extension storage without replacing ordinary Gson field codecs. + +use std::collections::BTreeSet; + +use heck::ToPascalCase; +use sigil_stitch::lang::java::Java; +use sigil_stitch::prelude::*; +use sigil_stitch::spec::modifiers::DeclarationContext; + +use crate::generators::type_application; +use crate::ir::types::{IrObject, IrPrimitive, IrSchema, IrTypeExpr}; + +use super::util::{java_field_name, java_getter_name, java_primitive_boxed}; + +pub(super) struct AdditionalProperties<'a> { + name: String, + getter: String, + object: &'a IrObject, + values: &'a IrTypeExpr, + package: String, + root: TypeName, + factory: String, +} + +impl<'a> AdditionalProperties<'a> { + pub fn new(schema: &IrSchema, object: &'a IrObject, package: &str) -> Option { + let values = object.additional_properties.as_ref()?; + let used = object + .properties + .values() + .flat_map(|property| { + [ + java_field_name(&property.name), + java_getter_name(&property.name), + ] + }) + .collect::>(); + let mut name = "additionalProperties".to_string(); + let mut getter = "getAdditionalProperties".to_string(); + let mut suffix = 2; + while used.contains(&name) || used.contains(&getter) { + name = format!("additionalProperties{suffix}"); + getter = format!("getAdditionalProperties{suffix}"); + suffix += 1; + } + let class = schema.name.to_pascal_case(); + let package = format!("{package}.models"); + Some(Self { + name, + getter, + object, + values, + root: TypeName::qualified(&package, &class), + factory: format!("{class}AdditionalPropertiesAdapterFactory"), + package, + }) + } + + fn map_type(&self) -> TypeName { + type_application( + TypeName::qualified("java.util", "Map"), + vec![ + TypeName::qualified("java.lang", "String"), + value_type(self.values, &self.package), + ], + ) + } + + pub fn annotation(&self) -> AnnotationSpec { + AnnotationSpec::importable(TypeName::qualified( + "com.google.gson.annotations", + "JsonAdapter", + )) + .args([ + format!("value = {}.class", self.factory), + "nullSafe = false".into(), + ]) + } + + pub fn storage(&self) -> Result { + let linked = TypeName::qualified("java.util", "LinkedHashMap"); + let field = FieldSpec::builder(&self.name, self.map_type()) + .initializer(sigil_quote!(Java { new $T(linked)<>() })?.__sigil_trim_trailing_newline()) + .build()? + .emit(&Java::new(), DeclarationContext::Member)?; + // sigil-stitch 0.7.5 has no transient field modifier. Only the static + // modifier prefix is outside FieldSpec; types/imports/initializer stay semantic. + sigil_quote!(Java { private transient $C(field) }) + } + + pub fn getter(&self) -> Result { + FunSpec::builder(&self.getter).visibility(Visibility::Public) + .returns(vec![self.map_type()]) + .body(sigil_quote!(Java { + if (this.$N(&self.name) == null) { + this.$N(&self.name) = new $T(TypeName::qualified("java.util", "LinkedHashMap"))<>(); + } + return this.$N(&self.name); + })?).build() + } + + pub fn factory(&self, package: &str) -> Result { + let value = value_type(self.values, &self.package); + let token = type_application( + TypeName::qualified("com.google.gson.reflect", "TypeToken"), + vec![value.clone()], + ); + let parent = type_application( + TypeName::qualified( + &format!("{package}.runtime"), + "AdditionalPropertiesAdapterFactory", + ), + vec![self.root.clone(), value.clone()], + ); + let constructor = FunSpec::builder(&self.factory).visibility(Visibility::Public) + .body(sigil_quote!(Java { + super($T(self.root.clone()).class, new $T(token)() {}, $+ + new $T(TypeName::qualified("java.lang", "String"))[] { $for(key in self.object.properties.keys(); separator = ", ") { $S(key) } }); + })?).build()?; + let properties = FunSpec::builder("properties") + .visibility(Visibility::Protected) + .annotate(AnnotationSpec::new("Override")) + .add_param(ParameterSpec::of("value", self.root.clone())) + .returns(vec![self.map_type()]) + .body(sigil_quote!(Java { return value.$N(&self.getter)(); })?) + .build()?; + let mut helpers = Vec::new(); + let function = encode_value(self.values, &self.package, &mut helpers)?; + let encode = FunSpec::builder("encode") + .visibility(Visibility::Protected) + .annotate(AnnotationSpec::new("Override")) + .add_param(ParameterSpec::of( + "gson", + TypeName::qualified("com.google.gson", "Gson"), + )) + .add_param(ParameterSpec::of("value", value)) + .returns(vec![TypeName::qualified("com.google.gson", "JsonElement")]) + .body(sigil_quote!(Java { return $N(function)(gson, value); })?) + .build()?; + let mut factory = TypeSpec::builder(&self.factory, TypeKind::Class) + .extends(parent) + .add_method(constructor) + .add_method(properties) + .add_method(encode); + for helper in helpers { + factory = factory.add_method(helper); + } + factory.build() + } +} + +fn value_type(expr: &IrTypeExpr, package: &str) -> TypeName { + match expr.representation() { + IrTypeExpr::Named(name) => TypeName::qualified(package, &name.to_pascal_case()), + IrTypeExpr::Primitive(IrPrimitive::Binary) => TypeName::array(TypeName::primitive("byte")), + IrTypeExpr::Primitive(primitive) => { + TypeName::qualified("java.lang", java_primitive_boxed(primitive)) + } + IrTypeExpr::Array(inner) => type_application( + TypeName::qualified("java.util", "List"), + vec![value_type(inner, package)], + ), + IrTypeExpr::Map(inner) => type_application( + TypeName::qualified("java.util", "Map"), + vec![ + TypeName::qualified("java.lang", "String"), + value_type(inner, package), + ], + ), + IrTypeExpr::Nullable(inner) => value_type(inner, package), + IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => { + TypeName::qualified("java.lang", "String") + } + IrTypeExpr::Any | IrTypeExpr::Union(_) => { + TypeName::qualified("com.google.gson", "JsonElement") + } + IrTypeExpr::ScalarEnum { .. } => unreachable!("scalar representation is unwrapped"), + } +} + +fn encode_value( + expr: &IrTypeExpr, + package: &str, + functions: &mut Vec, +) -> Result { + let expr = expr.representation(); + if let IrTypeExpr::Nullable(inner) = expr { + return encode_value(inner, package, functions); + } + let body = match expr { + IrTypeExpr::Any | IrTypeExpr::Union(_) => sigil_quote!(Java { return value; })?, + IrTypeExpr::Array(inner) => { + let child = encode_value(inner, package, functions)?; + sigil_quote!(Java { + var result = new $T(TypeName::qualified("com.google.gson", "JsonArray"))(); + for (var item : value) { result.add($N(child)(gson, item)); } + return result; + })? + } + IrTypeExpr::Map(inner) => { + let child = encode_value(inner, package, functions)?; + sigil_quote!(Java { + var result = new $T(TypeName::qualified("com.google.gson", "JsonObject"))(); + for (var entry : value.entrySet()) { result.add(entry.getKey(), $N(child)(gson, entry.getValue())); } + return result; + })? + } + _ => { + let token = type_application( + TypeName::qualified("com.google.gson.reflect", "TypeToken"), + vec![value_type(expr, package)], + ); + sigil_quote!(Java { return gson.toJsonTree(value, new $T(token)() {}.getType()); })? + } + }; + let function = format!("additionalValue{}", functions.len()); + functions.push(FunSpec::builder(&function).visibility(Visibility::Private) + .add_param(ParameterSpec::of("gson", TypeName::qualified("com.google.gson", "Gson"))) + .add_param(ParameterSpec::of("value", value_type(expr, package))) + .returns(vec![TypeName::qualified("com.google.gson", "JsonElement")]) + .body(sigil_quote!(Java { + if (value == null) { return $T(TypeName::qualified("com.google.gson", "JsonNull")).INSTANCE; } + $C(body) + })?).build()?); + Ok(function) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ir::types::IrSchemaKind; + use sigil_stitch::assert_rendered; + + #[test] + fn transient_storage_qualifies_types_and_disambiguates_the_authored_getter() { + let source = "openapi: 3.1.0\ninfo: {title: Storage, version: '1'}\npaths: {}\ncomponents:\n schemas:\n Model:\n type: object\n properties:\n additionalProperties: {type: string}\n additionalProperties: {type: integer}\n"; + let ir = + crate::ir::lower::lower(crate::parser::parse_content_yaml(source).unwrap()).unwrap(); + let schema = &ir.schemas["Model"]; + let IrSchemaKind::Object(object) = &schema.kind else { + panic!("object facts retained") + }; + let plan = AdditionalProperties::new(schema, object, "com.example.sdk").unwrap(); + assert_eq!(plan.getter, "getAdditionalProperties2"); + assert_rendered!( + Java::new(), + width = 300, + plan.storage().unwrap(), + "private transient java.util.Map additionalProperties2 = new java.util.LinkedHashMap<>();\n" + ); + } +} diff --git a/src/generators/java/okhttp/codegen.rs b/src/generators/java/okhttp/codegen.rs index 42f5405f2..c940d061a 100644 --- a/src/generators/java/okhttp/codegen.rs +++ b/src/generators/java/okhttp/codegen.rs @@ -57,6 +57,9 @@ impl JavaOkhttpCodeGenerator { &header, &package_name, request_inputs.has_uploads(), + ir.schemas.values().any(|schema| matches!(&schema.kind, + crate::ir::types::IrSchemaKind::Object(object) if object.additional_properties.is_some() + )), )); files.push(build_gradle_file(&package_name, &ir.info)); diff --git a/src/generators/java/okhttp/mod.rs b/src/generators/java/okhttp/mod.rs index 5f41555c4..da493f682 100644 --- a/src/generators/java/okhttp/mod.rs +++ b/src/generators/java/okhttp/mod.rs @@ -1,3 +1,4 @@ +mod additional_properties; pub mod codegen; pub mod config; mod matching; diff --git a/src/generators/java/okhttp/runtime.rs b/src/generators/java/okhttp/runtime.rs index 9f91c0fde..d4ab30610 100644 --- a/src/generators/java/okhttp/runtime.rs +++ b/src/generators/java/okhttp/runtime.rs @@ -8,7 +8,12 @@ const API_KEY_AUTH_JAVA: &str = include_str!("runtime/ApiKeyAuth.java.txt"); const API_KEY_LOCATION_JAVA: &str = include_str!("runtime/ApiKeyLocation.java.txt"); const UPLOAD_FILE_JAVA: &str = include_str!("runtime/UploadFile.java.txt"); -pub fn runtime_files(header: &str, package_name: &str, include_upload_file: bool) -> Vec { +pub fn runtime_files( + header: &str, + package_name: &str, + include_upload_file: bool, + include_additional_properties: bool, +) -> Vec { let runtime_package = format!("{package_name}.runtime"); let mut files = vec![ FileInfo::runtime( @@ -42,6 +47,16 @@ pub fn runtime_files(header: &str, package_name: &str, include_upload_file: bool repackage(header, UPLOAD_FILE_JAVA, &runtime_package), )); } + if include_additional_properties { + files.push(FileInfo::runtime( + "AdditionalPropertiesAdapterFactory.java".to_string(), + repackage( + header, + include_str!("runtime/AdditionalPropertiesAdapterFactory.java.txt"), + &runtime_package, + ), + )); + } files } diff --git a/src/generators/java/okhttp/runtime/AdditionalPropertiesAdapterFactory.java.txt b/src/generators/java/okhttp/runtime/AdditionalPropertiesAdapterFactory.java.txt new file mode 100644 index 000000000..3c4fca9a9 --- /dev/null +++ b/src/generators/java/okhttp/runtime/AdditionalPropertiesAdapterFactory.java.txt @@ -0,0 +1,160 @@ +package runtime; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.annotations.SerializedName; +import com.google.gson.internal.bind.JsonTreeReader; +import com.google.gson.internal.bind.JsonTreeWriter; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +/** Internal flat-object support; ordinary fields retain their configured Gson delegate. */ +public abstract class AdditionalPropertiesAdapterFactory implements TypeAdapterFactory { + private final Class modelClass; + private final TypeToken valueType; + private final Set declaredNames; + + protected AdditionalPropertiesAdapterFactory( + Class modelClass, TypeToken valueType, String... declaredNames) { + this.modelClass = modelClass; + this.valueType = valueType; + this.declaredNames = new HashSet<>(Arrays.asList(declaredNames)); + } + + protected abstract Map properties(M value); + + protected abstract JsonElement encode(Gson gson, V value); + + @Override + @SuppressWarnings("unchecked") + public final TypeAdapter create(Gson gson, TypeToken type) { + if (type.getRawType() != modelClass) { + return null; + } + TypeAdapter delegate = gson.getDelegateAdapter(this, TypeToken.get(modelClass)); + TypeAdapter values = gson.getAdapter(valueType); + TypeAdapter trees = gson.getAdapter(JsonElement.class); + Set known = new HashSet<>(declaredNames); + for (Field field : modelClass.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) || Modifier.isTransient(field.getModifiers())) { + continue; + } + SerializedName annotation = field.getAnnotation(SerializedName.class); + if (annotation == null) { + known.add(gson.fieldNamingStrategy().translateName(field)); + } else { + known.add(annotation.value()); + known.addAll(Arrays.asList(annotation.alternate())); + } + } + TypeAdapter adapter = new TypeAdapter<>() { + @Override + public M read(JsonReader reader) throws IOException { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); + return null; + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw new JsonSyntaxException("Expected " + modelClass.getSimpleName() + " object"); + } + Map extras = new LinkedHashMap<>(); + StringWriter buffer = new StringWriter(); + JsonWriter named = new JsonWriter(buffer); + named.setSerializeNulls(true); + named.setStrictness(reader.getStrictness()); + named.beginObject(); + reader.beginObject(); + while (reader.hasNext()) { + String key = reader.nextName(); + if (known.contains(key)) { + named.name(key); + trees.write(named, trees.read(reader)); + } else { + // Typed extensions consume the original reader: a tree reader + // can silently truncate fractional or overflowing integers. + extras.put(key, values.read(reader)); + } + } + reader.endObject(); + named.endObject(); + JsonReader replay; + // Gson's public tree API uses a different integer coercion policy. + // Preserve that input mode instead of imposing streaming semantics. + if (reader instanceof JsonTreeReader) { + replay = new JsonTreeReader(JsonParser.parseString(buffer.toString())); + } else { + // Text replay retains numeric lexemes and duplicate named fields, + // unlike fromJsonTree, while using the configured streaming delegate. + replay = new JsonReader(new StringReader(buffer.toString())); + } + replay.setStrictness(reader.getStrictness()); + M result = delegate.read(replay); + if (result != null) { + properties(result).putAll(extras); + } + return result; + } + + @Override + public void write(JsonWriter writer, M value) throws IOException { + if (value == null) { + writer.nullValue(); + return; + } + Map extras = properties(value); + for (String key : extras.keySet()) { + if (known.contains(key)) { + throw new IllegalArgumentException("Additional property collides with declared wire field: " + key); + } + } + // Use Gson's native tree writer rather than reparsing JSON text: + // configured non-finite numeric values would otherwise become strings. + JsonTreeWriter named = new JsonTreeWriter(); + named.setSerializeNulls(writer.getSerializeNulls()); + named.setHtmlSafe(writer.isHtmlSafe()); + named.setStrictness(writer.getStrictness()); + delegate.write(named, value); + JsonObject fields = named.get().getAsJsonObject(); + writer.beginObject(); + for (Map.Entry entry : fields.entrySet()) { + writer.name(entry.getKey()); + trees.write(writer, entry.getValue()); + } + for (Map.Entry entry : extras.entrySet()) { + if (fields.has(entry.getKey())) { + throw new IllegalArgumentException("Additional property collides with declared wire field: " + entry.getKey()); + } + JsonElement tree = encode(gson, entry.getValue()); + writer.name(entry.getKey()); + boolean serializeNulls = writer.getSerializeNulls(); + writer.setSerializeNulls(true); + try { + trees.write(writer, tree); + } finally { + writer.setSerializeNulls(serializeNulls); + } + } + writer.endObject(); + } + }; + // The raw-type identity above proves this cast; generated models are not generic. + return (TypeAdapter) adapter; + } +} diff --git a/src/generators/java/okhttp/sigil_emit.rs b/src/generators/java/okhttp/sigil_emit.rs index dda9c836d..8c3ab2857 100644 --- a/src/generators/java/okhttp/sigil_emit.rs +++ b/src/generators/java/okhttp/sigil_emit.rs @@ -167,6 +167,9 @@ fn emit_object( ) -> Result { let name = schema.name.to_pascal_case(); + let additional = + super::additional_properties::AdditionalProperties::new(schema, obj, package_name); + let mut file = FileSpec::builder_with("model.java", Java::new()).header(package_header(package_name)?); @@ -271,7 +274,16 @@ fn emit_object( tb = tb.add_method(build_java_getter(&getter_name, &type_str, &field_name)); } + if let Some(additional) = &additional { + tb = tb + .annotate(additional.annotation()) + .extra_member(additional.storage()?) + .add_method(additional.getter()?); + } file = file.add_type(tb.build()?); + if let Some(additional) = &additional { + file = file.add_type(additional.factory(package_name)?); + } file.build() } diff --git a/tests/fixtures/valid/java-additional-helper-types.yaml b/tests/fixtures/valid/java-additional-helper-types.yaml new file mode 100644 index 000000000..1218e494f --- /dev/null +++ b/tests/fixtures/valid/java-additional-helper-types.yaml @@ -0,0 +1,23 @@ +openapi: 3.1.0 +info: {title: Java additional helper types, version: '1.0.0'} +paths: {} +components: + schemas: + JsonElement: + type: object + required: [wire_value] + properties: + wire_value: {type: string} + Map: + type: object + additionalProperties: + type: array + items: + type: object + additionalProperties: {$ref: '#/components/schemas/JsonElement'} + Integer: + type: object + additionalProperties: {type: integer} + Gson: + type: object + additionalProperties: {type: integer, format: int64} diff --git a/tests/fixtures/valid/java-additional-properties.yaml b/tests/fixtures/valid/java-additional-properties.yaml new file mode 100644 index 000000000..10c33b787 --- /dev/null +++ b/tests/fixtures/valid/java-additional-properties.yaml @@ -0,0 +1,172 @@ +openapi: 3.1.0 +info: {title: Java additional properties, version: '1.0.0'} +paths: + /mixed: + post: + operationId: echoMixed + requestBody: + required: true + content: + application/json: + schema: {$ref: '#/components/schemas/MixedMap'} + responses: + '200': + description: Mixed object + content: + application/json: + schema: {$ref: '#/components/schemas/MixedMap'} + '400': + description: Dictionary error + content: + application/json: + schema: {$ref: '#/components/schemas/TypedMap'} + /dictionary: + post: + operationId: echoDictionary + requestBody: + required: true + content: + application/json: + schema: {$ref: '#/components/schemas/TypedMap'} + responses: + '200': + description: Typed dictionary + content: + application/json: + schema: {$ref: '#/components/schemas/TypedMap'} +components: + schemas: + NestedValue: + type: object + required: [wire_value] + properties: + wire_value: {type: string} + OpaqueMap: + type: object + required: [known_field] + properties: + known_field: {type: string} + additionalProperties: true + MixedMap: + type: object + required: [known_field] + properties: + known_field: {type: string} + additionalProperties: {$ref: '#/components/schemas/NestedValue'} + TypedMap: + type: object + additionalProperties: {$ref: '#/components/schemas/NestedValue'} + RawMap: + type: object + additionalProperties: {} + LongValues: + type: object + additionalProperties: {type: integer, format: int64} + IntegerValues: + type: object + additionalProperties: {type: integer} + Status: + type: string + enum: [active, inactive] + StatusValues: + type: object + additionalProperties: {$ref: '#/components/schemas/Status'} + NullableValues: + type: object + additionalProperties: + anyOf: [{$ref: '#/components/schemas/NestedValue'}, {type: 'null'}] + DeepValues: + type: object + additionalProperties: + type: array + items: + type: object + additionalProperties: {$ref: '#/components/schemas/NestedValue'} + RawContainers: + type: object + additionalProperties: + type: array + items: + type: object + additionalProperties: true + ValueAlias: {$ref: '#/components/schemas/NestedValue'} + AliasValues: + type: object + additionalProperties: {$ref: '#/components/schemas/ValueAlias'} + DictionaryAlias: {$ref: '#/components/schemas/TypedMap'} + Choices: + type: object + additionalProperties: {$ref: '#/components/schemas/ValueChoice'} + ValueChoice: + anyOf: [{$ref: '#/components/schemas/NestedValue'}, {type: integer}] + AmbiguousValues: + type: object + additionalProperties: {$ref: '#/components/schemas/AmbiguousNumber'} + AmbiguousNumber: + anyOf: [{type: integer}, {type: number}] + DictionaryChoice: + anyOf: [{$ref: '#/components/schemas/TypedMap'}, {type: integer}] + MixedChoice: + anyOf: [{$ref: '#/components/schemas/MixedMap'}, {type: string}] + Recursive: + type: object + required: [wire_value] + properties: + wire_value: {type: string} + additionalProperties: {$ref: '#/components/schemas/Recursive'} + PureRecursive: + type: object + additionalProperties: {$ref: '#/components/schemas/PureRecursive'} + 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'} + StorageCollision: + type: object + required: [additionalProperties] + properties: + additionalProperties: {type: string} + additionalProperties: {$ref: '#/components/schemas/NestedValue'} + OptionalKnown: + type: object + properties: + known_field: {type: string} + additionalProperties: true + OpenNumbers: + type: object + required: [count] + properties: + count: {type: integer} + optional_count: {type: integer} + nullable_value: {$ref: '#/components/schemas/NestedValue'} + weight: {type: number} + additionalProperties: true + PlainNumbers: + type: object + required: [count] + properties: + count: {type: integer} + optional_count: {type: integer} + nullable_value: {$ref: '#/components/schemas/NestedValue'} + weight: {type: number} + Closed: + type: object + properties: + known_field: {type: string} + additionalProperties: false + Implicit: + type: object + properties: + known_field: {type: string} + Holder: + type: object + required: [mixed, opaque, dictionaries] + properties: + mixed: {$ref: '#/components/schemas/MixedMap'} + opaque: {$ref: '#/components/schemas/OpaqueMap'} + dictionaries: {type: array, items: {$ref: '#/components/schemas/TypedMap'}} diff --git a/tests/generated-clients/jvm/README.md b/tests/generated-clients/jvm/README.md index d6c8787b9..a95d60fc4 100644 --- a/tests/generated-clients/jvm/README.md +++ b/tests/generated-clients/jvm/README.md @@ -10,6 +10,9 @@ boundaries, raw null preservation, and OkHttp response/error-body integration. Independent helper-type projects also compile recursive unions alongside models named `Integer` and `Boolean`; numeric controls exercise extreme exponents without weakening native delegate errors or finite-value precision. +Java's additional-property consumers cover pure and mixed objects under all +three union policies, typed mutable storage, opaque JSON precision/nulls, native +integer errors, storage/helper collisions, recursion and configured Gson delegates. 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. diff --git a/tests/generated-clients/jvm/java/cases/additional-helper-types/gradle.lockfile b/tests/generated-clients/jvm/java/cases/additional-helper-types/gradle.lockfile new file mode 100644 index 000000000..06e6223df --- /dev/null +++ b/tests/generated-clients/jvm/java/cases/additional-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:additional-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:1.9.10=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:1.9.10=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/java/cases/additional-helper-types/src/test/java/AdditionalHelperTypesTest.java b/tests/generated-clients/jvm/java/cases/additional-helper-types/src/test/java/AdditionalHelperTypesTest.java new file mode 100644 index 000000000..04c2069c0 --- /dev/null +++ b/tests/generated-clients/jvm/java/cases/additional-helper-types/src/test/java/AdditionalHelperTypesTest.java @@ -0,0 +1,30 @@ +import static org.junit.jupiter.api.Assertions.*; + +import com.google.gson.Gson; +import com.google.gson.JsonParser; +import org.junit.jupiter.api.Test; + +class AdditionalHelperTypesTest { + private final Gson gson = new Gson(); + + private T roundTrip(String wire, Class type) { + T value = gson.fromJson(wire, type); + assertEquals(JsonParser.parseString(wire), gson.toJsonTree(value)); + return value; + } + + @Test void nestedAuthoredModelNamesKeepTheirDeclaredIdentity() { + com.example.sdk.models.Map map = roundTrip("{\"list\":[{\"key\":{\"wire_value\":\"nested\"}}]}", com.example.sdk.models.Map.class); + assertInstanceOf(com.example.sdk.models.JsonElement.class, map.getAdditionalProperties().get("list").get(0).get("key")); + } + + @Test void authoredIntegerCannotShadowBoxedPrimitiveValues() { + com.example.sdk.models.Integer integer = roundTrip("{\"count\":2}", com.example.sdk.models.Integer.class); + assertInstanceOf(java.lang.Integer.class, integer.getAdditionalProperties().get("count")); + } + + @Test void authoredGsonCannotShadowItsOwnAdapterTypes() { + com.example.sdk.models.Gson namedGson = roundTrip("{\"large\":9007199254740993}", com.example.sdk.models.Gson.class); + assertInstanceOf(java.lang.Long.class, namedGson.getAdditionalProperties().get("large")); + } +} diff --git a/tests/generated-clients/jvm/java/cases/additional-properties-raw/build.gradle b/tests/generated-clients/jvm/java/cases/additional-properties-raw/build.gradle new file mode 100644 index 000000000..ea2b6c072 --- /dev/null +++ b/tests/generated-clients/jvm/java/cases/additional-properties-raw/build.gradle @@ -0,0 +1 @@ +sourceSets.test.java.srcDir("../../support/additional-properties") diff --git a/tests/generated-clients/jvm/java/cases/additional-properties-raw/gradle.lockfile b/tests/generated-clients/jvm/java/cases/additional-properties-raw/gradle.lockfile new file mode 100644 index 000000000..dfd5cc53c --- /dev/null +++ b/tests/generated-clients/jvm/java/cases/additional-properties-raw/gradle.lockfile @@ -0,0 +1,25 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +# To regenerate this file, run: ./gradlew :cases:additional-properties-raw:dependencies --write-locks +com.google.code.gson:gson:2.11.0=testCompileClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.27.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okhttp3:okhttp:4.12.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okio:okio-jvm:3.6.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okio:okio:3.6.0=testCompileClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10=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:1.9.10=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/java/cases/additional-properties-raw/src/test/java/AdditionalPropertiesTest.java b/tests/generated-clients/jvm/java/cases/additional-properties-raw/src/test/java/AdditionalPropertiesTest.java new file mode 100644 index 000000000..044d569fc --- /dev/null +++ b/tests/generated-clients/jvm/java/cases/additional-properties-raw/src/test/java/AdditionalPropertiesTest.java @@ -0,0 +1 @@ +class AdditionalPropertiesTest extends AdditionalPropertiesContracts {} diff --git a/tests/generated-clients/jvm/java/cases/additional-properties-strict/build.gradle b/tests/generated-clients/jvm/java/cases/additional-properties-strict/build.gradle new file mode 100644 index 000000000..ea2b6c072 --- /dev/null +++ b/tests/generated-clients/jvm/java/cases/additional-properties-strict/build.gradle @@ -0,0 +1 @@ +sourceSets.test.java.srcDir("../../support/additional-properties") diff --git a/tests/generated-clients/jvm/java/cases/additional-properties-strict/gradle.lockfile b/tests/generated-clients/jvm/java/cases/additional-properties-strict/gradle.lockfile new file mode 100644 index 000000000..625da3e8f --- /dev/null +++ b/tests/generated-clients/jvm/java/cases/additional-properties-strict/gradle.lockfile @@ -0,0 +1,25 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +# To regenerate this file, run: ./gradlew :cases:additional-properties-strict:dependencies --write-locks +com.google.code.gson:gson:2.11.0=testCompileClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.27.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okhttp3:okhttp:4.12.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okio:okio-jvm:3.6.0=testCompileClasspath,testRuntimeClasspath +com.squareup.okio:okio:3.6.0=testCompileClasspath,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10=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:1.9.10=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/java/cases/additional-properties-strict/src/test/java/AdditionalPropertiesTest.java b/tests/generated-clients/jvm/java/cases/additional-properties-strict/src/test/java/AdditionalPropertiesTest.java new file mode 100644 index 000000000..76e473031 --- /dev/null +++ b/tests/generated-clients/jvm/java/cases/additional-properties-strict/src/test/java/AdditionalPropertiesTest.java @@ -0,0 +1,12 @@ +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.example.sdk.models.AmbiguousValues; +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import org.junit.jupiter.api.Test; + +class AdditionalPropertiesTest extends AdditionalPropertiesContracts { + @Test void extensionUnionRetainsStrictAmbiguityPolicy() { + assertThrows(JsonParseException.class, () -> new Gson().fromJson("{\"value\":2}", AmbiguousValues.class)); + } +} diff --git a/tests/generated-clients/jvm/java/cases/additional-properties/build.gradle b/tests/generated-clients/jvm/java/cases/additional-properties/build.gradle new file mode 100644 index 000000000..ea2b6c072 --- /dev/null +++ b/tests/generated-clients/jvm/java/cases/additional-properties/build.gradle @@ -0,0 +1 @@ +sourceSets.test.java.srcDir("../../support/additional-properties") diff --git a/tests/generated-clients/jvm/java/cases/additional-properties/gradle.lockfile b/tests/generated-clients/jvm/java/cases/additional-properties/gradle.lockfile new file mode 100644 index 000000000..02f2de7ea --- /dev/null +++ b/tests/generated-clients/jvm/java/cases/additional-properties/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:additional-properties: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:1.9.10=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:1.9.10=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/java/cases/additional-properties/src/test/java/AdditionalPropertiesTest.java b/tests/generated-clients/jvm/java/cases/additional-properties/src/test/java/AdditionalPropertiesTest.java new file mode 100644 index 000000000..044d569fc --- /dev/null +++ b/tests/generated-clients/jvm/java/cases/additional-properties/src/test/java/AdditionalPropertiesTest.java @@ -0,0 +1 @@ +class AdditionalPropertiesTest extends AdditionalPropertiesContracts {} diff --git a/tests/generated-clients/jvm/java/fixtures.txt b/tests/generated-clients/jvm/java/fixtures.txt index c319f39b7..551bc0f1e 100644 --- a/tests/generated-clients/jvm/java/fixtures.txt +++ b/tests/generated-clients/jvm/java/fixtures.txt @@ -5,3 +5,7 @@ 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 +additional-properties valid/java-additional-properties.yaml +additional-properties-strict valid/java-additional-properties.yaml strict +additional-properties-raw valid/java-additional-properties.yaml raw +additional-helper-types valid/java-additional-helper-types.yaml diff --git a/tests/generated-clients/jvm/java/support/additional-properties/AdditionalPropertiesContracts.java b/tests/generated-clients/jvm/java/support/additional-properties/AdditionalPropertiesContracts.java new file mode 100644 index 000000000..15078189e --- /dev/null +++ b/tests/generated-clients/jvm/java/support/additional-properties/AdditionalPropertiesContracts.java @@ -0,0 +1,303 @@ +import static org.junit.jupiter.api.Assertions.*; + +import com.example.sdk.models.*; +import com.google.gson.*; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.example.sdk.apis.DefaultApi; +import com.example.sdk.apis.EchoMixedBadRequest; +import com.example.sdk.apis.EchoMixedException; +import com.example.sdk.runtime.ApiClient; +import java.io.IOException; +import java.io.StringWriter; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Response; +import okhttp3.ResponseBody; + +abstract class AdditionalPropertiesContracts { + final Gson gson = new Gson(); + + 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 opaqueExtensionsKeepVerbatimKeysAndValues() { + roundTrip(""" + {"known_field":"fixed","knownField":"shadow","extra_key":{"nil":null,"large":9007199254740993}} + """, OpaqueMap.class); + } + + @Test void typedExtensionsKeepTheirNestedWireNames() { + MixedMap value = roundTrip(""" + {"known_field":"fixed","extra_key":{"wire_value":"nested"}} + """, MixedMap.class); + assertEquals("nested", value.getAdditionalProperties().get("extra_key").getWireValue()); + } + + @Test void pureDictionaryKeepsItsEntries() { + TypedMap value = roundTrip("{\"extra_key\":{\"wire_value\":\"nested\"}}", TypedMap.class); + assertInstanceOf(NestedValue.class, value.getAdditionalProperties().get("extra_key")); + } + + @Test void defaultsAreMutableAndIndependentWithoutChangingConstructors() { + TypedMap first = new TypedMap(); + TypedMap second = new TypedMap(); + first.getAdditionalProperties().put("new_key", new NestedValue("new")); + assertEquals("{\"new_key\":{\"wire_value\":\"new\"}}", gson.toJson(first)); + assertTrue(second.getAdditionalProperties().isEmpty()); + assertEquals("{\"known_field\":\"fixed\"}", gson.toJson(new MixedMap("fixed"))); + assertTrue(gson.fromJson("{}", TypedMap.class).getAdditionalProperties().isEmpty()); + assertEquals("{}", gson.toJson(new TypedMap())); + } + + @Test void opaqueValuesKeepNullsNumbersAndStorageLookingKeys() { + String wire = """ + {"additionalProperties":null,"": [true,null],"large":9007199254740993,"quote'\\\\key":{"null":null}} + """; + RawMap value = roundTrip(wire, RawMap.class); + assertTrue(value.getAdditionalProperties().get("additionalProperties").isJsonNull()); + assertEquals("9007199254740993", value.getAdditionalProperties().get("large").getAsString()); + } + + @Test void nullableDeepAndRawContainersKeepEntryNulls() { + NullableValues nullable = roundTrip("{\"nil\":null,\"model\":{\"wire_value\":\"nested\"}}", NullableValues.class); + assertNull(nullable.getAdditionalProperties().get("nil")); + assertInstanceOf(NestedValue.class, nullable.getAdditionalProperties().get("model")); + DeepValues deep = roundTrip("{\"list\":[{\"keep_key\":{\"wire_value\":\"nested\"},\"nil\":null},null]}", DeepValues.class); + assertInstanceOf(NestedValue.class, deep.getAdditionalProperties().get("list").get(0).get("keep_key")); + roundTrip("{\"list\":[{\"nil\":null,\"large\":9007199254740993},null]}", RawContainers.class); + } + + @Test void scalarAndEnumValuesKeepTheirDeclaredTypes() { + LongValues longs = roundTrip("{\"large\":9007199254740993,\"negative\":-2}", LongValues.class); + assertEquals(9007199254740993L, longs.getAdditionalProperties().get("large")); + roundTrip("{\"count\":3,\"nil\":null}", IntegerValues.class); + StatusValues status = roundTrip("{\"status\":\"active\"}", StatusValues.class); + assertInstanceOf(Status.class, status.getAdditionalProperties().get("status")); + } + + @Test void nativeScalarErrorsAreNotChangedByTreeConversion() { + for (String invalid : List.of("1.5", "2147483648", "-2147483649", "\"bad\"")) { + RuntimeException direct = assertThrows(RuntimeException.class, () -> gson.fromJson(invalid, java.lang.Integer.class)); + RuntimeException dictionary = assertThrows(RuntimeException.class, () -> gson.fromJson("{\"value\":" + invalid + "}", IntegerValues.class)); + assertEquals(direct.getClass(), dictionary.getClass()); + } + } + + @Test void namedFieldsKeepNativeIntegerErrorsAndDuplicateHandling() { + for (String wire : List.of("{\"count\":1.5}", "{\"count\":2147483648}", "{\"count\":1.5,\"count\":2}")) { + RuntimeException plain = assertThrows(RuntimeException.class, () -> gson.fromJson(wire, PlainNumbers.class)); + RuntimeException opened = assertThrows(RuntimeException.class, () -> gson.fromJson(wire, OpenNumbers.class)); + assertEquals(plain.getClass(), opened.getClass()); + } + assertEquals(2, gson.fromJson("{\"count\":1,\"count\":2}", OpenNumbers.class).getCount()); + } + + @Test void ordinaryTreeReaderCoercionRemainsDifferentFromStreamingReaderCoercion() { + for (String wire : List.of("{\"count\":1.5}", "{\"count\":2147483648}")) { + JsonElement tree = JsonParser.parseString(wire); + PlainNumbers plain = gson.fromJson(tree, PlainNumbers.class); + OpenNumbers opened = gson.fromJson(tree, OpenNumbers.class); + assertEquals(plain.getCount(), opened.getCount()); + } + } + + @Test void configuredSpecialFloatingPointWritingKeepsTheOrdinaryDelegateOutput() { + Gson codec = new GsonBuilder().serializeSpecialFloatingPointValues().create(); + String wire = "{\"count\":2,\"weight\":\"NaN\"}"; + String plain = codec.toJson(codec.fromJson(wire, PlainNumbers.class)); + OpenNumbers opened = codec.fromJson(wire, OpenNumbers.class); + assertEquals(plain, codec.toJson(opened)); + assertEquals(plain, codec.toJson(codec.fromJson(JsonParser.parseString(wire), OpenNumbers.class))); + assertTrue(codec.toJsonTree(opened).getAsJsonObject().get("weight").getAsJsonPrimitive().isNumber()); + opened.getAdditionalProperties().put("nil", JsonNull.INSTANCE); + assertEquals(plain.substring(0, plain.length() - 1) + ",\"nil\":null}", codec.toJson(opened)); + } + + @Test void ordinaryFieldNullPolicyIsUnchangedIncludingSerializeNulls() { + for (Gson codec : List.of(gson, new GsonBuilder().serializeNulls().create())) { + for (String wire : List.of("{}", "{\"count\":null}", "{\"count\":2,\"optional_count\":null,\"nullable_value\":null}")) { + assertEquals(codec.toJsonTree(codec.fromJson(wire, PlainNumbers.class)), + codec.toJsonTree(codec.fromJson(wire, OpenNumbers.class))); + } + } + } + + @Test void extensionEntryNullsDoNotForceNullFieldsInsideDeclaredModelValues() { + TypedMap value = new TypedMap(); + value.getAdditionalProperties().put("model", new NestedValue(null)); + value.getAdditionalProperties().put("nil", null); + assertEquals("{\"model\":{},\"nil\":null}", gson.toJson(value)); + Gson explicitNulls = new GsonBuilder().serializeNulls().create(); + assertEquals("{\"model\":{\"wire_value\":null},\"nil\":null}", explicitNulls.toJson(value)); + } + + @Test void absentDeclaredFieldsCannotBeImpersonatedByExtensions() { + OptionalKnown value = new OptionalKnown(null); + value.getAdditionalProperties().put("known_field", new JsonPrimitive("collision")); + assertThrows(IllegalArgumentException.class, () -> gson.toJson(value)); + MixedMap mixed = new MixedMap("fixed"); + mixed.getAdditionalProperties().put("known_field", new NestedValue("bad")); + assertThrows(IllegalArgumentException.class, () -> gson.toJson(mixed)); + } + + @Test void authoredStorageNamesAreNotRenamedOrOverwritten() { + StorageCollision value = roundTrip("{\"additionalProperties\":\"known\",\"additional_properties\":{\"wire_value\":\"extra\"}}", StorageCollision.class); + assertEquals("known", value.getAdditionalProperties()); + assertEquals("extra", value.getAdditionalProperties2().get("additional_properties").getWireValue()); + } + + @Test void aliasesKeepTheirExistingWrapperRepresentation() { + AliasValues values = roundTrip("{\"key\":{\"value\":{\"wire_value\":\"nested\"}}}", AliasValues.class); + assertEquals("nested", values.getAdditionalProperties().get("key").getValue().getWireValue()); + DictionaryAlias alias = roundTrip("{\"value\":{\"key\":{\"wire_value\":\"nested\"}}}", DictionaryAlias.class); + assertInstanceOf(TypedMap.class, alias.getValue()); + } + + @Test void containingModelsKeepTypedDictionaries() { + String wire = """ + {"mixed":{"known_field":"fixed","key":{"wire_value":"nested"}},"opaque":{"known_field":"fixed","nil":null},"dictionaries":[{"key":{"wire_value":"nested"}}]} + """; + Holder value = roundTrip(wire, Holder.class); + assertInstanceOf(TypedMap.class, value.getDictionaries().get(0)); + } + + @Test void recursiveImportsAndValueAdaptersRemainUsable() { + Recursive recursive = roundTrip("{\"wire_value\":\"root\",\"child\":{\"wire_value\":\"leaf\"}}", Recursive.class); + assertInstanceOf(Recursive.class, recursive.getAdditionalProperties().get("child")); + roundTrip("{\"one\":{\"two\":{}}}", PureRecursive.class); + Nodes nodes = roundTrip("{\"root\":{\"wire_value\":\"root\",\"children\":{\"leaf\":{\"wire_value\":\"leaf\",\"children\":{}}}}}", Nodes.class); + assertInstanceOf(Nodes.class, nodes.getAdditionalProperties().get("root").getChildren()); + } + + @Test void namedUnionsRetainTheDictionaryPayloadAndItsPolicyBoundary() { + roundTrip("{\"model\":{\"wire_value\":\"nested\"},\"scalar\":2}", Choices.class); + roundTrip("{\"key\":{\"wire_value\":\"nested\"}}", DictionaryChoice.class); + roundTrip("{\"known_field\":\"fixed\",\"key\":{\"wire_value\":\"nested\"}}", MixedChoice.class); + } + + @Test void closedAndImplicitObjectsKeepTheirExistingBehavior() { + String wire = "{\"known_field\":\"fixed\",\"extra\":null}"; + assertEquals("{\"known_field\":\"fixed\"}", gson.toJson(gson.fromJson(wire, Closed.class))); + assertEquals("{\"known_field\":\"fixed\"}", gson.toJson(gson.fromJson(wire, Implicit.class))); + } + + @Test void rootNullAndWrongRootKindsKeepNativeFailures() { + assertNull(gson.fromJson("null", TypedMap.class)); + assertEquals("null", gson.toJson(null, TypedMap.class)); + for (String wire : List.of("[]", "2", "\"bad\"")) { + assertThrows(JsonSyntaxException.class, () -> gson.fromJson(wire, TypedMap.class)); + } + } + + @Test void configuredValueAdaptersAndNamedFieldAdaptersAreRetained() { + TypeAdapter custom = new TypeAdapter<>() { + public NestedValue read(JsonReader reader) throws IOException { + return new NestedValue(reader.nextString()); + } + public void write(JsonWriter writer, NestedValue value) throws IOException { + if (value == null) writer.nullValue(); else writer.value(value.getWireValue()); + } + }; + Gson codec = new GsonBuilder().registerTypeAdapter(NestedValue.class, custom).create(); + TypedMap value = codec.fromJson("{\"key\":\"custom\"}", TypedMap.class); + assertEquals("custom", value.getAdditionalProperties().get("key").getWireValue()); + assertEquals("{\"key\":\"custom\"}", codec.toJson(value)); + OpenNumbers named = codec.fromJson("{\"count\":1,\"nullable_value\":\"named\"}", OpenNumbers.class); + assertEquals("named", named.getNullableValue().getWireValue()); + assertEquals("{\"count\":1,\"nullable_value\":\"named\"}", codec.toJson(named)); + } + + @Test void configuredFieldNamesStillBelongToTheNamedFieldDelegate() { + Gson codec = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE).create(); + OpenNumbers value = codec.fromJson("{\"Count\":2,\"extra\":null}", OpenNumbers.class); + assertEquals(2, value.getCount()); + assertEquals(java.util.Set.of("extra"), value.getAdditionalProperties().keySet()); + assertEquals("{\"Count\":2,\"extra\":null}", codec.toJson(value)); + value.getAdditionalProperties().put("Optional_count", JsonNull.INSTANCE); + // SerializedName on optional_count wins over the configured naming policy. + assertTrue(codec.toJson(value).contains("Optional_count")); + value.getAdditionalProperties().put("Count", JsonNull.INSTANCE); + assertThrows(IllegalArgumentException.class, () -> codec.toJson(value)); + } + + private DefaultApi api(String path, int status, String reply, String request) { + OkHttpClient transport = new OkHttpClient.Builder().addInterceptor(chain -> { + assertEquals(path, chain.request().url().encodedPath()); + okio.Buffer buffer = new okio.Buffer(); + chain.request().body().writeTo(buffer); + assertEquals(JsonParser.parseString(request), JsonParser.parseString(buffer.readUtf8())); + return new Response.Builder().request(chain.request()).protocol(Protocol.HTTP_1_1) + .code(status).message(status == 200 ? "OK" : "Bad Request") + .header("Content-Type", "application/json") + .body(ResponseBody.create(reply, MediaType.get("application/json"))).build(); + }).build(); + return new DefaultApi(new ApiClient("https://additional.test", transport, null, Map.of())); + } + + @Test void httpRequestAndSuccessResponseRetainMixedExtensions() throws IOException { + String wire = "{\"known_field\":\"fixed\",\"extra\":{\"wire_value\":\"nested\"}}"; + MixedMap value = api("/mixed", 200, wire, wire).echoMixed(gson.fromJson(wire, MixedMap.class)).getStatus200(); + assertEquals("nested", value.getAdditionalProperties().get("extra").getWireValue()); + assertEquals(JsonParser.parseString(wire), gson.toJsonTree(value)); + } + + @Test void httpRequestAndSuccessResponseRetainPureDictionaryEntries() throws IOException { + String wire = "{\"extra\":{\"wire_value\":\"nested\"}}"; + TypedMap value = api("/dictionary", 200, wire, wire).echoDictionary(gson.fromJson(wire, TypedMap.class)).getStatus200(); + assertEquals(JsonParser.parseString(wire), gson.toJsonTree(value)); + } + + @Test void typedHttpErrorsRetainDictionaryBodiesAndLazyFailures() { + String request = "{\"known_field\":\"fixed\"}"; + String wire = "{\"extra\":{\"wire_value\":\"nested\"}}"; + EchoMixedException error = assertThrows(EchoMixedException.class, + () -> api("/mixed", 400, wire, request).echoMixed(new MixedMap("fixed"))); + EchoMixedBadRequest detail = assertInstanceOf(EchoMixedBadRequest.class, error.detail()); + assertEquals(JsonParser.parseString(wire), gson.toJsonTree(detail.body())); + EchoMixedException malformed = assertThrows(EchoMixedException.class, + () -> api("/mixed", 400, "{\"bad\":false}", request).echoMixed(new MixedMap("fixed"))); + EchoMixedBadRequest bad = assertInstanceOf(EchoMixedBadRequest.class, malformed.detail()); + assertThrows(JsonParseException.class, bad::body); + } + + @Test void extensionNullPreservationDoesNotLeakIntoOrdinaryValues() throws IOException { + TypedMap value = new TypedMap(); + value.getAdditionalProperties().put("nil", null); + StringWriter buffer = new StringWriter(); + JsonWriter writer = new JsonWriter(buffer); + writer.setSerializeNulls(false); + writer.beginObject(); + writer.name("dictionary"); + gson.getAdapter(TypedMap.class).write(writer, value); + assertFalse(writer.getSerializeNulls()); + writer.name("ordinary"); + writer.nullValue(); + writer.endObject(); + assertEquals("{\"dictionary\":{\"nil\":null}}", buffer.toString()); + } + + @Test void writerNullPolicyIsRestoredWhenAnExtensionWriteFails() throws IOException { + TypedMap value = new TypedMap(); + value.getAdditionalProperties().put("nil", null); + JsonWriter writer = new JsonWriter(new StringWriter()) { + @Override public JsonWriter nullValue() throws IOException { + throw new IOException("extension-write"); + } + }; + writer.setSerializeNulls(false); + IOException failure = assertThrows(IOException.class, () -> gson.getAdapter(TypedMap.class).write(writer, value)); + assertEquals("extension-write", failure.getMessage()); + assertFalse(writer.getSerializeNulls()); + } +} diff --git a/tests/golden/java/java-okhttp/comprehensive-schemas/models/ObjectWithAdditionalPropertiesTrue.java.golden b/tests/golden/java/java-okhttp/comprehensive-schemas/models/ObjectWithAdditionalPropertiesTrue.java.golden index 01783a3c2..4d2e76e84 100644 --- a/tests/golden/java/java-okhttp/comprehensive-schemas/models/ObjectWithAdditionalPropertiesTrue.java.golden +++ b/tests/golden/java/java-okhttp/comprehensive-schemas/models/ObjectWithAdditionalPropertiesTrue.java.golden @@ -8,6 +8,7 @@ package com.example.sdk.models; /** * Object allowing any additional properties */ +@com.google.gson.annotations.JsonAdapter(value = ObjectWithAdditionalPropertiesTrueAdditionalPropertiesAdapterFactory.class, nullSafe = false) public class ObjectWithAdditionalPropertiesTrue { private String name; @@ -18,4 +19,40 @@ public class ObjectWithAdditionalPropertiesTrue { public String getName() { return this.name; } + + public java.util.Map getAdditionalProperties() { + if (this.additionalProperties == null) { + this.additionalProperties = new java.util.LinkedHashMap<>(); + } + return this.additionalProperties; + } + private transient java.util.Map additionalProperties = new java.util.LinkedHashMap<>(); +} + +class ObjectWithAdditionalPropertiesTrueAdditionalPropertiesAdapterFactory extends com.example.sdk.runtime.AdditionalPropertiesAdapterFactory { + public ObjectWithAdditionalPropertiesTrueAdditionalPropertiesAdapterFactory() { + super(com.example.sdk.models.ObjectWithAdditionalPropertiesTrue.class, new com.google.gson.reflect.TypeToken() {}, new java.lang.String[] {"name"}); + } + + @Override + protected java.util.Map properties(com.example.sdk.models.ObjectWithAdditionalPropertiesTrue value) { + return value.getAdditionalProperties(); + } + + @Override + protected com.google.gson.JsonElement encode(com.google.gson.Gson gson, + com.google.gson.JsonElement value) { + return additionalValue0(gson, value); + } + + private com.google.gson.JsonElement additionalValue0(com.google.gson.Gson gson, + com.google.gson.JsonElement value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + return value; + } } diff --git a/tests/golden/java/java-okhttp/comprehensive-schemas/models/ObjectWithTypedAdditionalProperties.java.golden b/tests/golden/java/java-okhttp/comprehensive-schemas/models/ObjectWithTypedAdditionalProperties.java.golden index 3726a256a..418fb04fe 100644 --- a/tests/golden/java/java-okhttp/comprehensive-schemas/models/ObjectWithTypedAdditionalProperties.java.golden +++ b/tests/golden/java/java-okhttp/comprehensive-schemas/models/ObjectWithTypedAdditionalProperties.java.golden @@ -8,6 +8,7 @@ package com.example.sdk.models; /** * Object with typed additional properties */ +@com.google.gson.annotations.JsonAdapter(value = ObjectWithTypedAdditionalPropertiesAdditionalPropertiesAdapterFactory.class, nullSafe = false) public class ObjectWithTypedAdditionalProperties { private String name; @@ -18,4 +19,40 @@ public class ObjectWithTypedAdditionalProperties { public String getName() { return this.name; } + + public java.util.Map getAdditionalProperties() { + if (this.additionalProperties == null) { + this.additionalProperties = new java.util.LinkedHashMap<>(); + } + return this.additionalProperties; + } + private transient java.util.Map additionalProperties = new java.util.LinkedHashMap<>(); +} + +class ObjectWithTypedAdditionalPropertiesAdditionalPropertiesAdapterFactory extends com.example.sdk.runtime.AdditionalPropertiesAdapterFactory { + public ObjectWithTypedAdditionalPropertiesAdditionalPropertiesAdapterFactory() { + super(com.example.sdk.models.ObjectWithTypedAdditionalProperties.class, new com.google.gson.reflect.TypeToken() {}, new java.lang.String[] {"name"}); + } + + @Override + protected java.util.Map properties(com.example.sdk.models.ObjectWithTypedAdditionalProperties value) { + return value.getAdditionalProperties(); + } + + @Override + protected com.google.gson.JsonElement encode(com.google.gson.Gson gson, + java.lang.Double value) { + return additionalValue0(gson, value); + } + + private com.google.gson.JsonElement additionalValue0(com.google.gson.Gson gson, + java.lang.Double value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + return gson.toJsonTree(value, new com.google.gson.reflect.TypeToken() {}.getType()); + } } diff --git a/tests/golden/java/java-okhttp/comprehensive-schemas/runtime/AdditionalPropertiesAdapterFactory.java.golden b/tests/golden/java/java-okhttp/comprehensive-schemas/runtime/AdditionalPropertiesAdapterFactory.java.golden new file mode 100644 index 000000000..79dad7335 --- /dev/null +++ b/tests/golden/java/java-okhttp/comprehensive-schemas/runtime/AdditionalPropertiesAdapterFactory.java.golden @@ -0,0 +1,165 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Comprehensive Schema Types Example — 1.0.0 +// Comprehensive test for all OpenAPI v3.1.2 schema types + +package com.example.sdk.runtime; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.annotations.SerializedName; +import com.google.gson.internal.bind.JsonTreeReader; +import com.google.gson.internal.bind.JsonTreeWriter; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +/** Internal flat-object support; ordinary fields retain their configured Gson delegate. */ +public abstract class AdditionalPropertiesAdapterFactory implements TypeAdapterFactory { + private final Class modelClass; + private final TypeToken valueType; + private final Set declaredNames; + + protected AdditionalPropertiesAdapterFactory( + Class modelClass, TypeToken valueType, String... declaredNames) { + this.modelClass = modelClass; + this.valueType = valueType; + this.declaredNames = new HashSet<>(Arrays.asList(declaredNames)); + } + + protected abstract Map properties(M value); + + protected abstract JsonElement encode(Gson gson, V value); + + @Override + @SuppressWarnings("unchecked") + public final TypeAdapter create(Gson gson, TypeToken type) { + if (type.getRawType() != modelClass) { + return null; + } + TypeAdapter delegate = gson.getDelegateAdapter(this, TypeToken.get(modelClass)); + TypeAdapter values = gson.getAdapter(valueType); + TypeAdapter trees = gson.getAdapter(JsonElement.class); + Set known = new HashSet<>(declaredNames); + for (Field field : modelClass.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) || Modifier.isTransient(field.getModifiers())) { + continue; + } + SerializedName annotation = field.getAnnotation(SerializedName.class); + if (annotation == null) { + known.add(gson.fieldNamingStrategy().translateName(field)); + } else { + known.add(annotation.value()); + known.addAll(Arrays.asList(annotation.alternate())); + } + } + TypeAdapter adapter = new TypeAdapter<>() { + @Override + public M read(JsonReader reader) throws IOException { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); + return null; + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw new JsonSyntaxException("Expected " + modelClass.getSimpleName() + " object"); + } + Map extras = new LinkedHashMap<>(); + StringWriter buffer = new StringWriter(); + JsonWriter named = new JsonWriter(buffer); + named.setSerializeNulls(true); + named.setStrictness(reader.getStrictness()); + named.beginObject(); + reader.beginObject(); + while (reader.hasNext()) { + String key = reader.nextName(); + if (known.contains(key)) { + named.name(key); + trees.write(named, trees.read(reader)); + } else { + // Typed extensions consume the original reader: a tree reader + // can silently truncate fractional or overflowing integers. + extras.put(key, values.read(reader)); + } + } + reader.endObject(); + named.endObject(); + JsonReader replay; + // Gson's public tree API uses a different integer coercion policy. + // Preserve that input mode instead of imposing streaming semantics. + if (reader instanceof JsonTreeReader) { + replay = new JsonTreeReader(JsonParser.parseString(buffer.toString())); + } else { + // Text replay retains numeric lexemes and duplicate named fields, + // unlike fromJsonTree, while using the configured streaming delegate. + replay = new JsonReader(new StringReader(buffer.toString())); + } + replay.setStrictness(reader.getStrictness()); + M result = delegate.read(replay); + if (result != null) { + properties(result).putAll(extras); + } + return result; + } + + @Override + public void write(JsonWriter writer, M value) throws IOException { + if (value == null) { + writer.nullValue(); + return; + } + Map extras = properties(value); + for (String key : extras.keySet()) { + if (known.contains(key)) { + throw new IllegalArgumentException("Additional property collides with declared wire field: " + key); + } + } + // Use Gson's native tree writer rather than reparsing JSON text: + // configured non-finite numeric values would otherwise become strings. + JsonTreeWriter named = new JsonTreeWriter(); + named.setSerializeNulls(writer.getSerializeNulls()); + named.setHtmlSafe(writer.isHtmlSafe()); + named.setStrictness(writer.getStrictness()); + delegate.write(named, value); + JsonObject fields = named.get().getAsJsonObject(); + writer.beginObject(); + for (Map.Entry entry : fields.entrySet()) { + writer.name(entry.getKey()); + trees.write(writer, entry.getValue()); + } + for (Map.Entry entry : extras.entrySet()) { + if (fields.has(entry.getKey())) { + throw new IllegalArgumentException("Additional property collides with declared wire field: " + entry.getKey()); + } + JsonElement tree = encode(gson, entry.getValue()); + writer.name(entry.getKey()); + boolean serializeNulls = writer.getSerializeNulls(); + writer.setSerializeNulls(true); + try { + trees.write(writer, tree); + } finally { + writer.setSerializeNulls(serializeNulls); + } + } + writer.endObject(); + } + }; + // The raw-type identity above proves this cast; generated models are not generic. + return (TypeAdapter) adapter; + } +} diff --git a/tests/golden/java/java-okhttp/object-extension-helper-types/README.md.golden b/tests/golden/java/java-okhttp/object-extension-helper-types/README.md.golden new file mode 100644 index 000000000..85b7d2b62 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extension-helper-types/README.md.golden @@ -0,0 +1,7 @@ +# Java additional helper types + +Generated Java SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `java-additional-helper-types`. diff --git a/tests/golden/java/java-okhttp/object-extension-helper-types/build.gradle.golden b/tests/golden/java/java-okhttp/object-extension-helper-types/build.gradle.golden new file mode 100644 index 000000000..4b3a3bddf --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extension-helper-types/build.gradle.golden @@ -0,0 +1,29 @@ +plugins { + id("java-library") +} + +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") +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +sourceSets { + main { + java { + srcDirs("apis", "models", "runtime") + } + } +} diff --git a/tests/golden/java/java-okhttp/object-extension-helper-types/models/Gson.java.golden b/tests/golden/java/java-okhttp/object-extension-helper-types/models/Gson.java.golden new file mode 100644 index 000000000..306935d7d --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extension-helper-types/models/Gson.java.golden @@ -0,0 +1,47 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional helper types — 1.0.0 + +package com.example.sdk.models; + +@com.google.gson.annotations.JsonAdapter(value = GsonAdditionalPropertiesAdapterFactory.class, nullSafe = false) +public class Gson { + public Gson() { + + } + + public java.util.Map getAdditionalProperties() { + if (this.additionalProperties == null) { + this.additionalProperties = new java.util.LinkedHashMap<>(); + } + return this.additionalProperties; + } + private transient java.util.Map additionalProperties = new java.util.LinkedHashMap<>(); +} + +class GsonAdditionalPropertiesAdapterFactory extends com.example.sdk.runtime.AdditionalPropertiesAdapterFactory { + public GsonAdditionalPropertiesAdapterFactory() { + super(com.example.sdk.models.Gson.class, new com.google.gson.reflect.TypeToken() {}, new java.lang.String[] {}); + } + + @Override + protected java.util.Map properties(com.example.sdk.models.Gson value) { + return value.getAdditionalProperties(); + } + + @Override + protected com.google.gson.JsonElement encode(com.google.gson.Gson gson, java.lang.Long value) { + return additionalValue0(gson, value); + } + + private com.google.gson.JsonElement additionalValue0(com.google.gson.Gson gson, + java.lang.Long value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + return gson.toJsonTree(value, new com.google.gson.reflect.TypeToken() {}.getType()); + } +} diff --git a/tests/golden/java/java-okhttp/object-extension-helper-types/models/Integer.java.golden b/tests/golden/java/java-okhttp/object-extension-helper-types/models/Integer.java.golden new file mode 100644 index 000000000..1b1902196 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extension-helper-types/models/Integer.java.golden @@ -0,0 +1,48 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional helper types — 1.0.0 + +package com.example.sdk.models; + +@com.google.gson.annotations.JsonAdapter(value = IntegerAdditionalPropertiesAdapterFactory.class, nullSafe = false) +public class Integer { + public Integer() { + + } + + public java.util.Map getAdditionalProperties() { + if (this.additionalProperties == null) { + this.additionalProperties = new java.util.LinkedHashMap<>(); + } + return this.additionalProperties; + } + private transient java.util.Map additionalProperties = new java.util.LinkedHashMap<>(); +} + +class IntegerAdditionalPropertiesAdapterFactory extends com.example.sdk.runtime.AdditionalPropertiesAdapterFactory { + public IntegerAdditionalPropertiesAdapterFactory() { + super(com.example.sdk.models.Integer.class, new com.google.gson.reflect.TypeToken() {}, new java.lang.String[] {}); + } + + @Override + protected java.util.Map properties(com.example.sdk.models.Integer value) { + return value.getAdditionalProperties(); + } + + @Override + protected com.google.gson.JsonElement encode(com.google.gson.Gson gson, + java.lang.Integer value) { + return additionalValue0(gson, value); + } + + private com.google.gson.JsonElement additionalValue0(com.google.gson.Gson gson, + java.lang.Integer value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + return gson.toJsonTree(value, new com.google.gson.reflect.TypeToken() {}.getType()); + } +} diff --git a/tests/golden/java/java-okhttp/object-extension-helper-types/models/JsonElement.java.golden b/tests/golden/java/java-okhttp/object-extension-helper-types/models/JsonElement.java.golden new file mode 100644 index 000000000..fbdcbf82f --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extension-helper-types/models/JsonElement.java.golden @@ -0,0 +1,20 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional helper types — 1.0.0 + +package com.example.sdk.models; + +import com.google.gson.annotations.SerializedName; + +public class JsonElement { + @SerializedName("wire_value") + private String wireValue; + + public JsonElement(String wireValue) { + this.wireValue = wireValue; + } + + public String getWireValue() { + return this.wireValue; + } +} diff --git a/tests/golden/java/java-okhttp/object-extension-helper-types/models/Map.java.golden b/tests/golden/java/java-okhttp/object-extension-helper-types/models/Map.java.golden new file mode 100644 index 000000000..d3fc8bf48 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extension-helper-types/models/Map.java.golden @@ -0,0 +1,77 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional helper types — 1.0.0 + +package com.example.sdk.models; + +@com.google.gson.annotations.JsonAdapter(value = MapAdditionalPropertiesAdapterFactory.class, nullSafe = false) +public class Map { + public Map() { + + } + + public java.util.Map>> getAdditionalProperties() { + if (this.additionalProperties == null) { + this.additionalProperties = new java.util.LinkedHashMap<>(); + } + return this.additionalProperties; + } + private transient java.util.Map>> additionalProperties = new java.util.LinkedHashMap<>(); +} + +class MapAdditionalPropertiesAdapterFactory extends com.example.sdk.runtime.AdditionalPropertiesAdapterFactory>> { + public MapAdditionalPropertiesAdapterFactory() { + super(com.example.sdk.models.Map.class, new com.google.gson.reflect.TypeToken>>() {}, new java.lang.String[] {}); + } + + @Override + protected java.util.Map>> properties(com.example.sdk.models.Map value) { + return value.getAdditionalProperties(); + } + + @Override + protected com.google.gson.JsonElement encode(com.google.gson.Gson gson, + java.util.List> value) { + return additionalValue2(gson, value); + } + + private com.google.gson.JsonElement additionalValue0(com.google.gson.Gson gson, + com.example.sdk.models.JsonElement value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + return gson.toJsonTree(value, new com.google.gson.reflect.TypeToken() {}.getType()); + } + + private com.google.gson.JsonElement additionalValue1(com.google.gson.Gson gson, + java.util.Map value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + var result = new com.google.gson.JsonObject(); + for (var entry : value.entrySet()) { + result.add(entry.getKey(), additionalValue0(gson, entry.getValue())); + } + return result; + } + + private com.google.gson.JsonElement additionalValue2(com.google.gson.Gson gson, + java.util.List> value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + var result = new com.google.gson.JsonArray(); + for (var item : value) { + result.add(additionalValue1(gson, item)); + } + return result; + } +} diff --git a/tests/golden/java/java-okhttp/object-extension-helper-types/runtime/AdditionalPropertiesAdapterFactory.java.golden b/tests/golden/java/java-okhttp/object-extension-helper-types/runtime/AdditionalPropertiesAdapterFactory.java.golden new file mode 100644 index 000000000..a376343ba --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extension-helper-types/runtime/AdditionalPropertiesAdapterFactory.java.golden @@ -0,0 +1,164 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional helper types — 1.0.0 + +package com.example.sdk.runtime; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.annotations.SerializedName; +import com.google.gson.internal.bind.JsonTreeReader; +import com.google.gson.internal.bind.JsonTreeWriter; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +/** Internal flat-object support; ordinary fields retain their configured Gson delegate. */ +public abstract class AdditionalPropertiesAdapterFactory implements TypeAdapterFactory { + private final Class modelClass; + private final TypeToken valueType; + private final Set declaredNames; + + protected AdditionalPropertiesAdapterFactory( + Class modelClass, TypeToken valueType, String... declaredNames) { + this.modelClass = modelClass; + this.valueType = valueType; + this.declaredNames = new HashSet<>(Arrays.asList(declaredNames)); + } + + protected abstract Map properties(M value); + + protected abstract JsonElement encode(Gson gson, V value); + + @Override + @SuppressWarnings("unchecked") + public final TypeAdapter create(Gson gson, TypeToken type) { + if (type.getRawType() != modelClass) { + return null; + } + TypeAdapter delegate = gson.getDelegateAdapter(this, TypeToken.get(modelClass)); + TypeAdapter values = gson.getAdapter(valueType); + TypeAdapter trees = gson.getAdapter(JsonElement.class); + Set known = new HashSet<>(declaredNames); + for (Field field : modelClass.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) || Modifier.isTransient(field.getModifiers())) { + continue; + } + SerializedName annotation = field.getAnnotation(SerializedName.class); + if (annotation == null) { + known.add(gson.fieldNamingStrategy().translateName(field)); + } else { + known.add(annotation.value()); + known.addAll(Arrays.asList(annotation.alternate())); + } + } + TypeAdapter adapter = new TypeAdapter<>() { + @Override + public M read(JsonReader reader) throws IOException { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); + return null; + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw new JsonSyntaxException("Expected " + modelClass.getSimpleName() + " object"); + } + Map extras = new LinkedHashMap<>(); + StringWriter buffer = new StringWriter(); + JsonWriter named = new JsonWriter(buffer); + named.setSerializeNulls(true); + named.setStrictness(reader.getStrictness()); + named.beginObject(); + reader.beginObject(); + while (reader.hasNext()) { + String key = reader.nextName(); + if (known.contains(key)) { + named.name(key); + trees.write(named, trees.read(reader)); + } else { + // Typed extensions consume the original reader: a tree reader + // can silently truncate fractional or overflowing integers. + extras.put(key, values.read(reader)); + } + } + reader.endObject(); + named.endObject(); + JsonReader replay; + // Gson's public tree API uses a different integer coercion policy. + // Preserve that input mode instead of imposing streaming semantics. + if (reader instanceof JsonTreeReader) { + replay = new JsonTreeReader(JsonParser.parseString(buffer.toString())); + } else { + // Text replay retains numeric lexemes and duplicate named fields, + // unlike fromJsonTree, while using the configured streaming delegate. + replay = new JsonReader(new StringReader(buffer.toString())); + } + replay.setStrictness(reader.getStrictness()); + M result = delegate.read(replay); + if (result != null) { + properties(result).putAll(extras); + } + return result; + } + + @Override + public void write(JsonWriter writer, M value) throws IOException { + if (value == null) { + writer.nullValue(); + return; + } + Map extras = properties(value); + for (String key : extras.keySet()) { + if (known.contains(key)) { + throw new IllegalArgumentException("Additional property collides with declared wire field: " + key); + } + } + // Use Gson's native tree writer rather than reparsing JSON text: + // configured non-finite numeric values would otherwise become strings. + JsonTreeWriter named = new JsonTreeWriter(); + named.setSerializeNulls(writer.getSerializeNulls()); + named.setHtmlSafe(writer.isHtmlSafe()); + named.setStrictness(writer.getStrictness()); + delegate.write(named, value); + JsonObject fields = named.get().getAsJsonObject(); + writer.beginObject(); + for (Map.Entry entry : fields.entrySet()) { + writer.name(entry.getKey()); + trees.write(writer, entry.getValue()); + } + for (Map.Entry entry : extras.entrySet()) { + if (fields.has(entry.getKey())) { + throw new IllegalArgumentException("Additional property collides with declared wire field: " + entry.getKey()); + } + JsonElement tree = encode(gson, entry.getValue()); + writer.name(entry.getKey()); + boolean serializeNulls = writer.getSerializeNulls(); + writer.setSerializeNulls(true); + try { + trees.write(writer, tree); + } finally { + writer.setSerializeNulls(serializeNulls); + } + } + writer.endObject(); + } + }; + // The raw-type identity above proves this cast; generated models are not generic. + return (TypeAdapter) adapter; + } +} diff --git a/tests/golden/java/java-okhttp/object-extension-helper-types/runtime/ApiClient.java.golden b/tests/golden/java/java-okhttp/object-extension-helper-types/runtime/ApiClient.java.golden new file mode 100644 index 000000000..211deb323 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extension-helper-types/runtime/ApiClient.java.golden @@ -0,0 +1,78 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional helper types — 1.0.0 + +package com.example.sdk.runtime; + +import java.io.IOException; +import java.util.Map; + +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class ApiClient { + private static final MediaType JSON = MediaType.get("application/json"); + + private final String baseUrl; + private final OkHttpClient client; + private final Authenticator authenticator; + private final Map defaultHeaders; + + public ApiClient(String baseUrl) { + this(baseUrl, new OkHttpClient(), null, Map.of()); + } + + public ApiClient(String baseUrl, OkHttpClient client, Authenticator authenticator, Map defaultHeaders) { + this.baseUrl = baseUrl; + this.client = client; + this.authenticator = authenticator; + this.defaultHeaders = defaultHeaders; + } + + public Request newRequest(String method, String path, Map query, String body) { + RequestBody requestBody = null; + if (body != null) { + requestBody = RequestBody.create(body, JSON); + } + return buildRequest(method, path, query, requestBody); + } + + public Request newRequestWithBody(String method, String path, Map query, RequestBody body) { + return buildRequest(method, path, query, body); + } + + private Request buildRequest(String method, String path, Map query, RequestBody requestBody) { + String url = baseUrl.replaceAll("/+$", "") + path; + HttpUrl parsed = HttpUrl.parse(url); + if (parsed == null) { + throw new IllegalArgumentException("Invalid URL: " + url); + } + HttpUrl.Builder urlBuilder = parsed.newBuilder(); + if (query != null) { + for (Map.Entry entry : query.entrySet()) { + urlBuilder.addQueryParameter(entry.getKey(), entry.getValue()); + } + } + + Request.Builder builder = new Request.Builder() + .url(urlBuilder.build()) + .method(method, requestBody); + + for (Map.Entry entry : defaultHeaders.entrySet()) { + builder.header(entry.getKey(), entry.getValue()); + } + if (authenticator != null) { + authenticator.authenticate(builder); + } + + return builder.build(); + } + + public Response execute(Request request) throws IOException { + return client.newCall(request).execute(); + } +} diff --git a/tests/golden/java/java-okhttp/object-extension-helper-types/runtime/ApiException.java.golden b/tests/golden/java/java-okhttp/object-extension-helper-types/runtime/ApiException.java.golden new file mode 100644 index 000000000..358e12c36 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extension-helper-types/runtime/ApiException.java.golden @@ -0,0 +1,30 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional helper types — 1.0.0 + +package com.example.sdk.runtime; + +public class ApiException extends RuntimeException { + private final int statusCode; + private final String status; + private final String body; + + public ApiException(int statusCode, String status, String body) { + super("API error " + statusCode + ": " + status); + this.statusCode = statusCode; + this.status = status; + this.body = body; + } + + public int getStatusCode() { + return statusCode; + } + + public String getStatus() { + return status; + } + + public String getBody() { + return body; + } +} diff --git a/tests/golden/java/java-okhttp/object-extension-helper-types/runtime/ApiKeyAuth.java.golden b/tests/golden/java/java-okhttp/object-extension-helper-types/runtime/ApiKeyAuth.java.golden new file mode 100644 index 000000000..2ce00024a --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extension-helper-types/runtime/ApiKeyAuth.java.golden @@ -0,0 +1,41 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional helper types — 1.0.0 + +package com.example.sdk.runtime; + +import java.util.function.Supplier; +import okhttp3.Request; + +public class ApiKeyAuth implements Authenticator { + private final String key; + private final Supplier keyProvider; + private final String name; + private final ApiKeyLocation location; + + public ApiKeyAuth(String key, String name, ApiKeyLocation location) { + this.key = key; + this.keyProvider = null; + this.name = name; + this.location = location; + } + + public ApiKeyAuth(Supplier keyProvider, String name, ApiKeyLocation location) { + this.key = null; + this.keyProvider = keyProvider; + this.name = name; + this.location = location; + } + + @Override + public void authenticate(Request.Builder builder) { + String k = keyProvider != null ? keyProvider.get() : key; + if (location == ApiKeyLocation.HEADER) { + builder.header(name, k); + } else if (location == ApiKeyLocation.QUERY) { + builder.url(builder.build().url().newBuilder() + .addQueryParameter(name, k) + .build()); + } + } +} diff --git a/tests/golden/java/java-okhttp/object-extension-helper-types/runtime/ApiKeyLocation.java.golden b/tests/golden/java/java-okhttp/object-extension-helper-types/runtime/ApiKeyLocation.java.golden new file mode 100644 index 000000000..616f592d2 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extension-helper-types/runtime/ApiKeyLocation.java.golden @@ -0,0 +1,10 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional helper types — 1.0.0 + +package com.example.sdk.runtime; + +public enum ApiKeyLocation { + HEADER, + QUERY +} diff --git a/tests/golden/java/java-okhttp/object-extension-helper-types/runtime/Authenticator.java.golden b/tests/golden/java/java-okhttp/object-extension-helper-types/runtime/Authenticator.java.golden new file mode 100644 index 000000000..667e53552 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extension-helper-types/runtime/Authenticator.java.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional helper types — 1.0.0 + +package com.example.sdk.runtime; + +import okhttp3.Request; + +public interface Authenticator { + void authenticate(Request.Builder builder); +} diff --git a/tests/golden/java/java-okhttp/object-extension-helper-types/runtime/BearerAuth.java.golden b/tests/golden/java/java-okhttp/object-extension-helper-types/runtime/BearerAuth.java.golden new file mode 100644 index 000000000..7fb9a17d4 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extension-helper-types/runtime/BearerAuth.java.golden @@ -0,0 +1,29 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional helper types — 1.0.0 + +package com.example.sdk.runtime; + +import java.util.function.Supplier; +import okhttp3.Request; + +public class BearerAuth implements Authenticator { + private final String token; + private final Supplier tokenProvider; + + public BearerAuth(String token) { + this.token = token; + this.tokenProvider = null; + } + + public BearerAuth(Supplier tokenProvider) { + this.token = null; + this.tokenProvider = tokenProvider; + } + + @Override + public void authenticate(Request.Builder builder) { + String t = tokenProvider != null ? tokenProvider.get() : token; + builder.header("Authorization", "Bearer " + t); + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/README.md.golden b/tests/golden/java/java-okhttp/object-extensions/README.md.golden new file mode 100644 index 000000000..c97b2f06f --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/README.md.golden @@ -0,0 +1,7 @@ +# Java additional properties + +Generated Java SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `java-additional-properties`. diff --git a/tests/golden/java/java-okhttp/object-extensions/apis/DefaultApi.java.golden b/tests/golden/java/java-okhttp/object-extensions/apis/DefaultApi.java.golden new file mode 100644 index 000000000..1d9c764b5 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/apis/DefaultApi.java.golden @@ -0,0 +1,100 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.apis; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +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; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +/** + * DefaultApi groups operations under the default tag. + */ +public class DefaultApi { + private final ApiClient client; + private final Gson gson = new Gson(); + + public DefaultApi(ApiClient client) { + this.client = client; + } + + /** + * echoDictionary POST /dictionary. + */ + public EchoDictionaryResponse echoDictionary(TypedMap body) throws IOException { + String path = "/dictionary"; + Request request; + String jsonBody = gson.toJson(body); + RequestBody requestBody = RequestBody.create(jsonBody, MediaType.get("application/json")); + request = client.newRequestWithBody("POST", path, null, requestBody); + Response response = client.execute(request); + + if (!response.isSuccessful()) { + okhttp3.ResponseBody responseBody = response.body(); + java.nio.charset.Charset responseCharset = responseBody != null && responseBody.contentType() != null ? responseBody.contentType().charset(StandardCharsets.UTF_8) : StandardCharsets.UTF_8; + byte[] responseBytes = responseBody != null ? responseBody.bytes() : new byte[0]; + String responseText = new String(responseBytes, responseCharset); + EchoDictionaryExceptionDetail detail; + switch (response.code()) { + default: + detail = new EchoDictionaryUnexpected(response.code(), response.headers(), responseBytes); + } + throw new EchoDictionaryException(response.code(), response.message(), responseText, response.headers(), responseBytes, detail); + } + byte[] responseBytes = response.body() != null ? response.body().bytes() : new byte[0]; + String responseText = new String(responseBytes, StandardCharsets.UTF_8); + TypedMap status200 = null; + if (response.code() == 200) { + status200 = gson.fromJson(responseText.isEmpty() ? "null" : responseText, new TypeToken() {}.getType()); + } + return new EchoDictionaryResponse(response.code(), response, status200); + } + + /** + * echoMixed POST /mixed. + */ + public EchoMixedResponse echoMixed(MixedMap body) throws IOException { + String path = "/mixed"; + Request request; + String jsonBody = gson.toJson(body); + RequestBody requestBody = RequestBody.create(jsonBody, MediaType.get("application/json")); + request = client.newRequestWithBody("POST", path, null, requestBody); + Response response = client.execute(request); + + if (!response.isSuccessful()) { + okhttp3.ResponseBody responseBody = response.body(); + java.nio.charset.Charset responseCharset = responseBody != null && responseBody.contentType() != null ? responseBody.contentType().charset(StandardCharsets.UTF_8) : StandardCharsets.UTF_8; + byte[] responseBytes = responseBody != null ? responseBody.bytes() : new byte[0]; + String responseText = new String(responseBytes, responseCharset); + EchoMixedExceptionDetail detail; + switch (response.code()) { + case 400: + detail = new EchoMixedBadRequest(response.code(), response.headers(), responseBytes);break; + default: + detail = new EchoMixedUnexpected(response.code(), response.headers(), responseBytes); + } + throw new EchoMixedException(response.code(), response.message(), responseText, response.headers(), responseBytes, detail); + } + byte[] responseBytes = response.body() != null ? response.body().bytes() : new byte[0]; + String responseText = new String(responseBytes, StandardCharsets.UTF_8); + MixedMap status200 = null; + if (response.code() == 200) { + status200 = gson.fromJson(responseText.isEmpty() ? "null" : responseText, new TypeToken() {}.getType()); + } + return new EchoMixedResponse(response.code(), response, status200); + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/apis/EchoDictionaryException.java.golden b/tests/golden/java/java-okhttp/object-extensions/apis/EchoDictionaryException.java.golden new file mode 100644 index 000000000..af68c103e --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/apis/EchoDictionaryException.java.golden @@ -0,0 +1,33 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.apis; + +import com.example.sdk.runtime.ApiException; +import okhttp3.Headers; + +public final class EchoDictionaryException extends ApiException { + private final EchoDictionaryExceptionDetail detail; + private final Headers headers; + private final byte[] rawBody; + + public EchoDictionaryException(int statusCode, String status, String body, Headers headers, byte[] rawBody, EchoDictionaryExceptionDetail detail) { + super(statusCode, status, body); + this.headers = headers; + this.rawBody = rawBody.clone(); + this.detail = detail; + } + + public EchoDictionaryExceptionDetail detail() { + return this.detail; + } + + public Headers headers() { + return this.headers; + } + + public byte[] rawBody() { + return this.rawBody.clone(); + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/apis/EchoDictionaryExceptionDetail.java.golden b/tests/golden/java/java-okhttp/object-extensions/apis/EchoDictionaryExceptionDetail.java.golden new file mode 100644 index 000000000..7c342d112 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/apis/EchoDictionaryExceptionDetail.java.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.apis; + +public sealed interface EchoDictionaryExceptionDetail permits EchoDictionaryUnexpected { + int statusCode(); + okhttp3.Headers headers(); + byte[] rawBody(); +} diff --git a/tests/golden/java/java-okhttp/object-extensions/apis/EchoDictionaryResponse.java.golden b/tests/golden/java/java-okhttp/object-extensions/apis/EchoDictionaryResponse.java.golden new file mode 100644 index 000000000..042a9d12e --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/apis/EchoDictionaryResponse.java.golden @@ -0,0 +1,38 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.apis; + +import java.util.List; +import java.util.Map; + +import com.example.sdk.models.*; +import okhttp3.Response; + +/** + * EchoDictionaryResponse carries the response from echoDictionary. + */ +public class EchoDictionaryResponse { + private final int statusCode; + private final Response raw; + private final TypedMap status200; + + public EchoDictionaryResponse(int statusCode, Response raw, TypedMap status200) { + this.statusCode = statusCode; + this.raw = raw; + this.status200 = status200; + } + + public int getStatusCode() { + return this.statusCode; + } + + public Response getRaw() { + return this.raw; + } + + public TypedMap getStatus200() { + return this.status200; + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/apis/EchoDictionaryUnexpected.java.golden b/tests/golden/java/java-okhttp/object-extensions/apis/EchoDictionaryUnexpected.java.golden new file mode 100644 index 000000000..a49e91ec1 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/apis/EchoDictionaryUnexpected.java.golden @@ -0,0 +1,22 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.apis; + +public final class EchoDictionaryUnexpected implements EchoDictionaryExceptionDetail { + private final int statusCode; + private final okhttp3.Headers headers; + private final byte[] rawBody; + + public EchoDictionaryUnexpected(int statusCode, okhttp3.Headers headers, byte[] rawBody) { + this.statusCode = statusCode; + this.headers = headers; + this.rawBody = rawBody.clone(); + } + + public int statusCode() { return this.statusCode; } + public okhttp3.Headers headers() { return this.headers; } + public byte[] rawBody() { return this.rawBody.clone(); } + public byte[] body() { return this.rawBody.clone(); } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/apis/EchoMixedBadRequest.java.golden b/tests/golden/java/java-okhttp/object-extensions/apis/EchoMixedBadRequest.java.golden new file mode 100644 index 000000000..828d990f4 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/apis/EchoMixedBadRequest.java.golden @@ -0,0 +1,40 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.apis; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +import com.example.sdk.models.*; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +public final class EchoMixedBadRequest implements EchoMixedExceptionDetail { + private final int statusCode; + private final okhttp3.Headers headers; + private final byte[] rawBody; + + public EchoMixedBadRequest(int statusCode, okhttp3.Headers headers, byte[] rawBody) { + this.statusCode = statusCode; + this.headers = headers; + this.rawBody = rawBody.clone(); + } + + public int statusCode() { return this.statusCode; } + public okhttp3.Headers headers() { return this.headers; } + public byte[] rawBody() { return this.rawBody.clone(); } + + private String textBody() { + String contentTypeHeader = this.headers.get("Content-Type"); + okhttp3.MediaType contentType = contentTypeHeader != null ? okhttp3.MediaType.parse(contentTypeHeader) : null; + java.nio.charset.Charset charset = contentType != null ? contentType.charset(StandardCharsets.UTF_8) : StandardCharsets.UTF_8; + return new String(this.rawBody, charset); + } + public TypedMap body() { + String text = textBody(); + return new Gson().fromJson(text.isEmpty() ? "null" : text, new TypeToken() {}.getType()); + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/apis/EchoMixedException.java.golden b/tests/golden/java/java-okhttp/object-extensions/apis/EchoMixedException.java.golden new file mode 100644 index 000000000..dc273ccf0 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/apis/EchoMixedException.java.golden @@ -0,0 +1,33 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.apis; + +import com.example.sdk.runtime.ApiException; +import okhttp3.Headers; + +public final class EchoMixedException extends ApiException { + private final EchoMixedExceptionDetail detail; + private final Headers headers; + private final byte[] rawBody; + + public EchoMixedException(int statusCode, String status, String body, Headers headers, byte[] rawBody, EchoMixedExceptionDetail detail) { + super(statusCode, status, body); + this.headers = headers; + this.rawBody = rawBody.clone(); + this.detail = detail; + } + + public EchoMixedExceptionDetail detail() { + return this.detail; + } + + public Headers headers() { + return this.headers; + } + + public byte[] rawBody() { + return this.rawBody.clone(); + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/apis/EchoMixedExceptionDetail.java.golden b/tests/golden/java/java-okhttp/object-extensions/apis/EchoMixedExceptionDetail.java.golden new file mode 100644 index 000000000..e2cfc67a6 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/apis/EchoMixedExceptionDetail.java.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.apis; + +public sealed interface EchoMixedExceptionDetail permits EchoMixedBadRequest, EchoMixedUnexpected { + int statusCode(); + okhttp3.Headers headers(); + byte[] rawBody(); +} diff --git a/tests/golden/java/java-okhttp/object-extensions/apis/EchoMixedResponse.java.golden b/tests/golden/java/java-okhttp/object-extensions/apis/EchoMixedResponse.java.golden new file mode 100644 index 000000000..01762e9bc --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/apis/EchoMixedResponse.java.golden @@ -0,0 +1,38 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.apis; + +import java.util.List; +import java.util.Map; + +import com.example.sdk.models.*; +import okhttp3.Response; + +/** + * EchoMixedResponse carries the response from echoMixed. + */ +public class EchoMixedResponse { + private final int statusCode; + private final Response raw; + private final MixedMap status200; + + public EchoMixedResponse(int statusCode, Response raw, MixedMap status200) { + this.statusCode = statusCode; + this.raw = raw; + this.status200 = status200; + } + + public int getStatusCode() { + return this.statusCode; + } + + public Response getRaw() { + return this.raw; + } + + public MixedMap getStatus200() { + return this.status200; + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/apis/EchoMixedUnexpected.java.golden b/tests/golden/java/java-okhttp/object-extensions/apis/EchoMixedUnexpected.java.golden new file mode 100644 index 000000000..68db5ed53 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/apis/EchoMixedUnexpected.java.golden @@ -0,0 +1,22 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.apis; + +public final class EchoMixedUnexpected implements EchoMixedExceptionDetail { + private final int statusCode; + private final okhttp3.Headers headers; + private final byte[] rawBody; + + public EchoMixedUnexpected(int statusCode, okhttp3.Headers headers, byte[] rawBody) { + this.statusCode = statusCode; + this.headers = headers; + this.rawBody = rawBody.clone(); + } + + public int statusCode() { return this.statusCode; } + public okhttp3.Headers headers() { return this.headers; } + public byte[] rawBody() { return this.rawBody.clone(); } + public byte[] body() { return this.rawBody.clone(); } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/build.gradle.golden b/tests/golden/java/java-okhttp/object-extensions/build.gradle.golden new file mode 100644 index 000000000..4b3a3bddf --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/build.gradle.golden @@ -0,0 +1,29 @@ +plugins { + id("java-library") +} + +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") +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +sourceSets { + main { + java { + srcDirs("apis", "models", "runtime") + } + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/AliasValues.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/AliasValues.java.golden new file mode 100644 index 000000000..9e3b2a703 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/AliasValues.java.golden @@ -0,0 +1,49 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +@com.google.gson.annotations.JsonAdapter(value = AliasValuesAdditionalPropertiesAdapterFactory.class, nullSafe = false) +public class AliasValues { + public AliasValues() { + + } + + public java.util.Map getAdditionalProperties() { + if (this.additionalProperties == null) { + this.additionalProperties = new java.util.LinkedHashMap<>(); + } + return this.additionalProperties; + } + private transient java.util.Map additionalProperties = new java.util.LinkedHashMap<>(); +} + +class AliasValuesAdditionalPropertiesAdapterFactory extends com.example.sdk.runtime.AdditionalPropertiesAdapterFactory { + public AliasValuesAdditionalPropertiesAdapterFactory() { + super(com.example.sdk.models.AliasValues.class, new com.google.gson.reflect.TypeToken() {}, new java.lang.String[] {}); + } + + @Override + protected java.util.Map properties(com.example.sdk.models.AliasValues value) { + return value.getAdditionalProperties(); + } + + @Override + protected com.google.gson.JsonElement encode(com.google.gson.Gson gson, + com.example.sdk.models.ValueAlias value) { + return additionalValue0(gson, value); + } + + private com.google.gson.JsonElement additionalValue0(com.google.gson.Gson gson, + com.example.sdk.models.ValueAlias value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + return gson.toJsonTree(value, new com.google.gson.reflect.TypeToken() {}.getType()); + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/AmbiguousNumber.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/AmbiguousNumber.java.golden new file mode 100644 index 000000000..8f23f0fdc --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/AmbiguousNumber.java.golden @@ -0,0 +1,89 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 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.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(value = AmbiguousNumberJsonAdapter.class, nullSafe = false) +public sealed interface AmbiguousNumber { + record VariantInteger(int value0) implements AmbiguousNumber {} + + record VariantNumber(double value0) implements AmbiguousNumber {} +} + +final class AmbiguousNumberJsonAdapter extends TypeAdapter { + private final Gson gson = new Gson(); + + @Override + public AmbiguousNumber read(JsonReader reader) throws java.io.IOException { + JsonElement element = JsonParser.parseReader(reader); + if (element.isJsonNull()) { + return null; + } + int selected = -1; + if (selected < 0 && matches0(element)) { + selected = 0; + } + if (selected < 0 && matches1(element)) { + selected = 1; + } + if (selected == 0) { + return new AmbiguousNumber.VariantInteger(gson.fromJson(element, Integer.class)); + } + if (selected == 1) { + return new AmbiguousNumber.VariantNumber(gson.fromJson(element, Double.class)); + } + throw new JsonParseException("unknown AmbiguousNumber union value"); + } + + @Override + public void write(JsonWriter writer, AmbiguousNumber value) throws java.io.IOException { + if (value == null) { + writer.nullValue(); + return; + } + if (value instanceof AmbiguousNumber.VariantInteger) { + gson.toJson(gson.toJsonTree(((AmbiguousNumber.VariantInteger) value).value0()), writer); + return; + } + if (value instanceof AmbiguousNumber.VariantNumber) { + gson.toJson(gson.toJsonTree(((AmbiguousNumber.VariantNumber) value).value0()), writer); + return; + } + } + + private static boolean matches0(JsonElement candidate) { + if (!candidate.isJsonPrimitive() || !candidate.getAsJsonPrimitive().isNumber()) { + return false; + } + java.lang.String literal = candidate.getAsString(); + java.math.BigDecimal significand = numberSignificand(literal); + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale())) >= 0; + } + + private static boolean matches1(JsonElement candidate) { + return candidate.isJsonPrimitive() && candidate.getAsJsonPrimitive().isNumber(); + } + + private static java.math.BigDecimal numberSignificand(java.lang.String literal) { + int exponent = java.lang.Math.max(literal.indexOf('e'), literal.indexOf('E')); + return new java.math.BigDecimal(exponent < 0 ? literal : literal.substring(0, exponent)).stripTrailingZeros(); + } + + private static java.math.BigInteger numberExponent(java.lang.String literal) { + int exponent = java.lang.Math.max(literal.indexOf('e'), literal.indexOf('E')); + return exponent < 0 ? java.math.BigInteger.ZERO : new java.math.BigInteger(literal.substring(exponent + 1)); + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/AmbiguousValues.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/AmbiguousValues.java.golden new file mode 100644 index 000000000..9691951ad --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/AmbiguousValues.java.golden @@ -0,0 +1,49 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +@com.google.gson.annotations.JsonAdapter(value = AmbiguousValuesAdditionalPropertiesAdapterFactory.class, nullSafe = false) +public class AmbiguousValues { + public AmbiguousValues() { + + } + + public java.util.Map getAdditionalProperties() { + if (this.additionalProperties == null) { + this.additionalProperties = new java.util.LinkedHashMap<>(); + } + return this.additionalProperties; + } + private transient java.util.Map additionalProperties = new java.util.LinkedHashMap<>(); +} + +class AmbiguousValuesAdditionalPropertiesAdapterFactory extends com.example.sdk.runtime.AdditionalPropertiesAdapterFactory { + public AmbiguousValuesAdditionalPropertiesAdapterFactory() { + super(com.example.sdk.models.AmbiguousValues.class, new com.google.gson.reflect.TypeToken() {}, new java.lang.String[] {}); + } + + @Override + protected java.util.Map properties(com.example.sdk.models.AmbiguousValues value) { + return value.getAdditionalProperties(); + } + + @Override + protected com.google.gson.JsonElement encode(com.google.gson.Gson gson, + com.example.sdk.models.AmbiguousNumber value) { + return additionalValue0(gson, value); + } + + private com.google.gson.JsonElement additionalValue0(com.google.gson.Gson gson, + com.example.sdk.models.AmbiguousNumber value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + return gson.toJsonTree(value, new com.google.gson.reflect.TypeToken() {}.getType()); + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/Choices.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/Choices.java.golden new file mode 100644 index 000000000..ea02016c2 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/Choices.java.golden @@ -0,0 +1,49 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +@com.google.gson.annotations.JsonAdapter(value = ChoicesAdditionalPropertiesAdapterFactory.class, nullSafe = false) +public class Choices { + public Choices() { + + } + + public java.util.Map getAdditionalProperties() { + if (this.additionalProperties == null) { + this.additionalProperties = new java.util.LinkedHashMap<>(); + } + return this.additionalProperties; + } + private transient java.util.Map additionalProperties = new java.util.LinkedHashMap<>(); +} + +class ChoicesAdditionalPropertiesAdapterFactory extends com.example.sdk.runtime.AdditionalPropertiesAdapterFactory { + public ChoicesAdditionalPropertiesAdapterFactory() { + super(com.example.sdk.models.Choices.class, new com.google.gson.reflect.TypeToken() {}, new java.lang.String[] {}); + } + + @Override + protected java.util.Map properties(com.example.sdk.models.Choices value) { + return value.getAdditionalProperties(); + } + + @Override + protected com.google.gson.JsonElement encode(com.google.gson.Gson gson, + com.example.sdk.models.ValueChoice value) { + return additionalValue0(gson, value); + } + + private com.google.gson.JsonElement additionalValue0(com.google.gson.Gson gson, + com.example.sdk.models.ValueChoice value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + return gson.toJsonTree(value, new com.google.gson.reflect.TypeToken() {}.getType()); + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/Closed.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/Closed.java.golden new file mode 100644 index 000000000..2e7c16f7c --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/Closed.java.golden @@ -0,0 +1,20 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +import com.google.gson.annotations.SerializedName; + +public class Closed { + @SerializedName("known_field") + private String knownField; + + public Closed(String knownField) { + this.knownField = knownField; + } + + public String getKnownField() { + return this.knownField; + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/DeepValues.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/DeepValues.java.golden new file mode 100644 index 000000000..3d93f86c0 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/DeepValues.java.golden @@ -0,0 +1,77 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +@com.google.gson.annotations.JsonAdapter(value = DeepValuesAdditionalPropertiesAdapterFactory.class, nullSafe = false) +public class DeepValues { + public DeepValues() { + + } + + public java.util.Map>> getAdditionalProperties() { + if (this.additionalProperties == null) { + this.additionalProperties = new java.util.LinkedHashMap<>(); + } + return this.additionalProperties; + } + private transient java.util.Map>> additionalProperties = new java.util.LinkedHashMap<>(); +} + +class DeepValuesAdditionalPropertiesAdapterFactory extends com.example.sdk.runtime.AdditionalPropertiesAdapterFactory>> { + public DeepValuesAdditionalPropertiesAdapterFactory() { + super(com.example.sdk.models.DeepValues.class, new com.google.gson.reflect.TypeToken>>() {}, new java.lang.String[] {}); + } + + @Override + protected java.util.Map>> properties(com.example.sdk.models.DeepValues value) { + return value.getAdditionalProperties(); + } + + @Override + protected com.google.gson.JsonElement encode(com.google.gson.Gson gson, + java.util.List> value) { + return additionalValue2(gson, value); + } + + private com.google.gson.JsonElement additionalValue0(com.google.gson.Gson gson, + com.example.sdk.models.NestedValue value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + return gson.toJsonTree(value, new com.google.gson.reflect.TypeToken() {}.getType()); + } + + private com.google.gson.JsonElement additionalValue1(com.google.gson.Gson gson, + java.util.Map value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + var result = new com.google.gson.JsonObject(); + for (var entry : value.entrySet()) { + result.add(entry.getKey(), additionalValue0(gson, entry.getValue())); + } + return result; + } + + private com.google.gson.JsonElement additionalValue2(com.google.gson.Gson gson, + java.util.List> value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + var result = new com.google.gson.JsonArray(); + for (var item : value) { + result.add(additionalValue1(gson, item)); + } + return result; + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/DictionaryAlias.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/DictionaryAlias.java.golden new file mode 100644 index 000000000..ca9f7bb6a --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/DictionaryAlias.java.golden @@ -0,0 +1,17 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +public class DictionaryAlias { + private TypedMap value; + + public DictionaryAlias(TypedMap value) { + this.value = value; + } + + public TypedMap getValue() { + return this.value; + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/DictionaryChoice.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/DictionaryChoice.java.golden new file mode 100644 index 000000000..008d2715c --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/DictionaryChoice.java.golden @@ -0,0 +1,112 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 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.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(value = DictionaryChoiceJsonAdapter.class, nullSafe = false) +public sealed interface DictionaryChoice { + record VariantTypedMap(TypedMap value0) implements DictionaryChoice {} + + record VariantInteger(int value0) implements DictionaryChoice {} +} + +final class DictionaryChoiceJsonAdapter extends TypeAdapter { + private final Gson gson = new Gson(); + + @Override + public DictionaryChoice read(JsonReader reader) throws java.io.IOException { + JsonElement element = JsonParser.parseReader(reader); + if (element.isJsonNull()) { + return null; + } + int selected = -1; + if (selected < 0 && matches0(element)) { + selected = 0; + } + if (selected < 0 && matches3(element)) { + selected = 1; + } + if (selected == 0) { + return new DictionaryChoice.VariantTypedMap(gson.fromJson(element, new TypeToken() {}.getType())); + } + if (selected == 1) { + return new DictionaryChoice.VariantInteger(gson.fromJson(element, Integer.class)); + } + throw new JsonParseException("unknown DictionaryChoice union value"); + } + + @Override + public void write(JsonWriter writer, DictionaryChoice value) throws java.io.IOException { + if (value == null) { + writer.nullValue(); + return; + } + if (value instanceof DictionaryChoice.VariantTypedMap) { + gson.toJson(gson.toJsonTree(((DictionaryChoice.VariantTypedMap) value).value0()), writer); + return; + } + if (value instanceof DictionaryChoice.VariantInteger) { + gson.toJson(gson.toJsonTree(((DictionaryChoice.VariantInteger) value).value0()), writer); + return; + } + } + + private static boolean matches0(JsonElement candidate) { + if (!candidate.isJsonObject()) { + return false; + } + for (JsonElement item : candidate.getAsJsonObject().asMap().values()) { + if (!matches1(item)) { + return false; + } + } + return true; + } + + private static boolean matches1(JsonElement candidate) { + if (!candidate.isJsonObject()) { + return false; + } + JsonObject object = candidate.getAsJsonObject(); + if (!object.has("wire_value") || !matches2(object.get("wire_value"))) { + return false; + } + return true; + } + + private static boolean matches2(JsonElement candidate) { + return candidate.isJsonPrimitive() && candidate.getAsJsonPrimitive().isString(); + } + + private static boolean matches3(JsonElement candidate) { + if (!candidate.isJsonPrimitive() || !candidate.getAsJsonPrimitive().isNumber()) { + return false; + } + java.lang.String literal = candidate.getAsString(); + java.math.BigDecimal significand = numberSignificand(literal); + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale())) >= 0; + } + + private static java.math.BigDecimal numberSignificand(java.lang.String literal) { + int exponent = java.lang.Math.max(literal.indexOf('e'), literal.indexOf('E')); + return new java.math.BigDecimal(exponent < 0 ? literal : literal.substring(0, exponent)).stripTrailingZeros(); + } + + private static java.math.BigInteger numberExponent(java.lang.String literal) { + int exponent = java.lang.Math.max(literal.indexOf('e'), literal.indexOf('E')); + return exponent < 0 ? java.math.BigInteger.ZERO : new java.math.BigInteger(literal.substring(exponent + 1)); + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/Holder.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/Holder.java.golden new file mode 100644 index 000000000..88fb0016c --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/Holder.java.golden @@ -0,0 +1,31 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +import java.util.List; + +public class Holder { + private List dictionaries; + private MixedMap mixed; + private OpaqueMap opaque; + + public Holder(List dictionaries, MixedMap mixed, OpaqueMap opaque) { + this.dictionaries = dictionaries; + this.mixed = mixed; + this.opaque = opaque; + } + + public List getDictionaries() { + return this.dictionaries; + } + + public MixedMap getMixed() { + return this.mixed; + } + + public OpaqueMap getOpaque() { + return this.opaque; + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/Implicit.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/Implicit.java.golden new file mode 100644 index 000000000..9be8c653d --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/Implicit.java.golden @@ -0,0 +1,20 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +import com.google.gson.annotations.SerializedName; + +public class Implicit { + @SerializedName("known_field") + private String knownField; + + public Implicit(String knownField) { + this.knownField = knownField; + } + + public String getKnownField() { + return this.knownField; + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/IntegerValues.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/IntegerValues.java.golden new file mode 100644 index 000000000..320195a00 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/IntegerValues.java.golden @@ -0,0 +1,48 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +@com.google.gson.annotations.JsonAdapter(value = IntegerValuesAdditionalPropertiesAdapterFactory.class, nullSafe = false) +public class IntegerValues { + public IntegerValues() { + + } + + public java.util.Map getAdditionalProperties() { + if (this.additionalProperties == null) { + this.additionalProperties = new java.util.LinkedHashMap<>(); + } + return this.additionalProperties; + } + private transient java.util.Map additionalProperties = new java.util.LinkedHashMap<>(); +} + +class IntegerValuesAdditionalPropertiesAdapterFactory extends com.example.sdk.runtime.AdditionalPropertiesAdapterFactory { + public IntegerValuesAdditionalPropertiesAdapterFactory() { + super(com.example.sdk.models.IntegerValues.class, new com.google.gson.reflect.TypeToken() {}, new java.lang.String[] {}); + } + + @Override + protected java.util.Map properties(com.example.sdk.models.IntegerValues value) { + return value.getAdditionalProperties(); + } + + @Override + protected com.google.gson.JsonElement encode(com.google.gson.Gson gson, + java.lang.Integer value) { + return additionalValue0(gson, value); + } + + private com.google.gson.JsonElement additionalValue0(com.google.gson.Gson gson, + java.lang.Integer value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + return gson.toJsonTree(value, new com.google.gson.reflect.TypeToken() {}.getType()); + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/LongValues.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/LongValues.java.golden new file mode 100644 index 000000000..c42d1a5d5 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/LongValues.java.golden @@ -0,0 +1,47 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +@com.google.gson.annotations.JsonAdapter(value = LongValuesAdditionalPropertiesAdapterFactory.class, nullSafe = false) +public class LongValues { + public LongValues() { + + } + + public java.util.Map getAdditionalProperties() { + if (this.additionalProperties == null) { + this.additionalProperties = new java.util.LinkedHashMap<>(); + } + return this.additionalProperties; + } + private transient java.util.Map additionalProperties = new java.util.LinkedHashMap<>(); +} + +class LongValuesAdditionalPropertiesAdapterFactory extends com.example.sdk.runtime.AdditionalPropertiesAdapterFactory { + public LongValuesAdditionalPropertiesAdapterFactory() { + super(com.example.sdk.models.LongValues.class, new com.google.gson.reflect.TypeToken() {}, new java.lang.String[] {}); + } + + @Override + protected java.util.Map properties(com.example.sdk.models.LongValues value) { + return value.getAdditionalProperties(); + } + + @Override + protected com.google.gson.JsonElement encode(com.google.gson.Gson gson, java.lang.Long value) { + return additionalValue0(gson, value); + } + + private com.google.gson.JsonElement additionalValue0(com.google.gson.Gson gson, + java.lang.Long value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + return gson.toJsonTree(value, new com.google.gson.reflect.TypeToken() {}.getType()); + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/MixedChoice.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/MixedChoice.java.golden new file mode 100644 index 000000000..82e2b3494 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/MixedChoice.java.golden @@ -0,0 +1,106 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 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.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(value = MixedChoiceJsonAdapter.class, nullSafe = false) +public sealed interface MixedChoice { + record VariantMixedMap(MixedMap value0) implements MixedChoice {} + + record VariantString(String value0) implements MixedChoice {} +} + +final class MixedChoiceJsonAdapter extends TypeAdapter { + private final Gson gson = new Gson(); + + @Override + public MixedChoice read(JsonReader reader) throws java.io.IOException { + JsonElement element = JsonParser.parseReader(reader); + if (element.isJsonNull()) { + return null; + } + int selected = -1; + if (selected < 0 && matches0(element)) { + selected = 0; + } + if (selected < 0 && matches2(element)) { + selected = 1; + } + if (selected == 0) { + return new MixedChoice.VariantMixedMap(gson.fromJson(element, new TypeToken() {}.getType())); + } + if (selected == 1) { + return new MixedChoice.VariantString(element.getAsString()); + } + throw new JsonParseException("unknown MixedChoice union value"); + } + + @Override + public void write(JsonWriter writer, MixedChoice value) throws java.io.IOException { + if (value == null) { + writer.nullValue(); + return; + } + if (value instanceof MixedChoice.VariantMixedMap) { + gson.toJson(gson.toJsonTree(((MixedChoice.VariantMixedMap) value).value0()), writer); + return; + } + if (value instanceof MixedChoice.VariantString) { + gson.toJson(gson.toJsonTree(((MixedChoice.VariantString) value).value0()), writer); + return; + } + } + + private static boolean matches0(JsonElement candidate) { + if (!matches1(candidate)) { + return false; + } + java.util.Set declared = java.util.Set.of("known_field"); + for (java.util.Map.Entry item : candidate.getAsJsonObject().entrySet()) { + if (!declared.contains(item.getKey()) && !matches3(item.getValue())) { + return false; + } + } + return true; + } + + private static boolean matches1(JsonElement candidate) { + if (!candidate.isJsonObject()) { + return false; + } + JsonObject object = candidate.getAsJsonObject(); + if (!object.has("known_field") || !matches2(object.get("known_field"))) { + return false; + } + return true; + } + + private static boolean matches2(JsonElement candidate) { + return candidate.isJsonPrimitive() && candidate.getAsJsonPrimitive().isString(); + } + + private static boolean matches3(JsonElement candidate) { + if (!candidate.isJsonObject()) { + return false; + } + JsonObject object = candidate.getAsJsonObject(); + if (!object.has("wire_value") || !matches2(object.get("wire_value"))) { + return false; + } + return true; + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/MixedMap.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/MixedMap.java.golden new file mode 100644 index 000000000..9db12efac --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/MixedMap.java.golden @@ -0,0 +1,58 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +import com.google.gson.annotations.SerializedName; + +@com.google.gson.annotations.JsonAdapter(value = MixedMapAdditionalPropertiesAdapterFactory.class, nullSafe = false) +public class MixedMap { + @SerializedName("known_field") + private String knownField; + + public MixedMap(String knownField) { + this.knownField = knownField; + } + + public String getKnownField() { + return this.knownField; + } + + public java.util.Map getAdditionalProperties() { + if (this.additionalProperties == null) { + this.additionalProperties = new java.util.LinkedHashMap<>(); + } + return this.additionalProperties; + } + private transient java.util.Map additionalProperties = new java.util.LinkedHashMap<>(); +} + +class MixedMapAdditionalPropertiesAdapterFactory extends com.example.sdk.runtime.AdditionalPropertiesAdapterFactory { + public MixedMapAdditionalPropertiesAdapterFactory() { + super(com.example.sdk.models.MixedMap.class, new com.google.gson.reflect.TypeToken() {}, new java.lang.String[] {"known_field"}); + } + + @Override + protected java.util.Map properties(com.example.sdk.models.MixedMap value) { + return value.getAdditionalProperties(); + } + + @Override + protected com.google.gson.JsonElement encode(com.google.gson.Gson gson, + com.example.sdk.models.NestedValue value) { + return additionalValue0(gson, value); + } + + private com.google.gson.JsonElement additionalValue0(com.google.gson.Gson gson, + com.example.sdk.models.NestedValue value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + return gson.toJsonTree(value, new com.google.gson.reflect.TypeToken() {}.getType()); + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/NestedValue.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/NestedValue.java.golden new file mode 100644 index 000000000..0fdbbba37 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/NestedValue.java.golden @@ -0,0 +1,20 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +import com.google.gson.annotations.SerializedName; + +public class NestedValue { + @SerializedName("wire_value") + private String wireValue; + + public NestedValue(String wireValue) { + this.wireValue = wireValue; + } + + public String getWireValue() { + return this.wireValue; + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/Node.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/Node.java.golden new file mode 100644 index 000000000..43e53aa87 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/Node.java.golden @@ -0,0 +1,26 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +import com.google.gson.annotations.SerializedName; + +public class Node { + private Nodes children; + @SerializedName("wire_value") + private String wireValue; + + public Node(Nodes children, String wireValue) { + this.children = children; + this.wireValue = wireValue; + } + + public Nodes getChildren() { + return this.children; + } + + public String getWireValue() { + return this.wireValue; + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/Nodes.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/Nodes.java.golden new file mode 100644 index 000000000..15a40b773 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/Nodes.java.golden @@ -0,0 +1,48 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +@com.google.gson.annotations.JsonAdapter(value = NodesAdditionalPropertiesAdapterFactory.class, nullSafe = false) +public class Nodes { + public Nodes() { + + } + + public java.util.Map getAdditionalProperties() { + if (this.additionalProperties == null) { + this.additionalProperties = new java.util.LinkedHashMap<>(); + } + return this.additionalProperties; + } + private transient java.util.Map additionalProperties = new java.util.LinkedHashMap<>(); +} + +class NodesAdditionalPropertiesAdapterFactory extends com.example.sdk.runtime.AdditionalPropertiesAdapterFactory { + public NodesAdditionalPropertiesAdapterFactory() { + super(com.example.sdk.models.Nodes.class, new com.google.gson.reflect.TypeToken() {}, new java.lang.String[] {}); + } + + @Override + protected java.util.Map properties(com.example.sdk.models.Nodes value) { + return value.getAdditionalProperties(); + } + + @Override + protected com.google.gson.JsonElement encode(com.google.gson.Gson gson, + com.example.sdk.models.Node value) { + return additionalValue0(gson, value); + } + + private com.google.gson.JsonElement additionalValue0(com.google.gson.Gson gson, + com.example.sdk.models.Node value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + return gson.toJsonTree(value, new com.google.gson.reflect.TypeToken() {}.getType()); + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/NullableValues.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/NullableValues.java.golden new file mode 100644 index 000000000..f34451933 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/NullableValues.java.golden @@ -0,0 +1,49 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +@com.google.gson.annotations.JsonAdapter(value = NullableValuesAdditionalPropertiesAdapterFactory.class, nullSafe = false) +public class NullableValues { + public NullableValues() { + + } + + public java.util.Map getAdditionalProperties() { + if (this.additionalProperties == null) { + this.additionalProperties = new java.util.LinkedHashMap<>(); + } + return this.additionalProperties; + } + private transient java.util.Map additionalProperties = new java.util.LinkedHashMap<>(); +} + +class NullableValuesAdditionalPropertiesAdapterFactory extends com.example.sdk.runtime.AdditionalPropertiesAdapterFactory { + public NullableValuesAdditionalPropertiesAdapterFactory() { + super(com.example.sdk.models.NullableValues.class, new com.google.gson.reflect.TypeToken() {}, new java.lang.String[] {}); + } + + @Override + protected java.util.Map properties(com.example.sdk.models.NullableValues value) { + return value.getAdditionalProperties(); + } + + @Override + protected com.google.gson.JsonElement encode(com.google.gson.Gson gson, + com.example.sdk.models.NestedValue value) { + return additionalValue0(gson, value); + } + + private com.google.gson.JsonElement additionalValue0(com.google.gson.Gson gson, + com.example.sdk.models.NestedValue value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + return gson.toJsonTree(value, new com.google.gson.reflect.TypeToken() {}.getType()); + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/OpaqueMap.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/OpaqueMap.java.golden new file mode 100644 index 000000000..295584414 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/OpaqueMap.java.golden @@ -0,0 +1,57 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +import com.google.gson.annotations.SerializedName; + +@com.google.gson.annotations.JsonAdapter(value = OpaqueMapAdditionalPropertiesAdapterFactory.class, nullSafe = false) +public class OpaqueMap { + @SerializedName("known_field") + private String knownField; + + public OpaqueMap(String knownField) { + this.knownField = knownField; + } + + public String getKnownField() { + return this.knownField; + } + + public java.util.Map getAdditionalProperties() { + if (this.additionalProperties == null) { + this.additionalProperties = new java.util.LinkedHashMap<>(); + } + return this.additionalProperties; + } + private transient java.util.Map additionalProperties = new java.util.LinkedHashMap<>(); +} + +class OpaqueMapAdditionalPropertiesAdapterFactory extends com.example.sdk.runtime.AdditionalPropertiesAdapterFactory { + public OpaqueMapAdditionalPropertiesAdapterFactory() { + super(com.example.sdk.models.OpaqueMap.class, new com.google.gson.reflect.TypeToken() {}, new java.lang.String[] {"known_field"}); + } + + @Override + protected java.util.Map properties(com.example.sdk.models.OpaqueMap value) { + return value.getAdditionalProperties(); + } + + @Override + protected com.google.gson.JsonElement encode(com.google.gson.Gson gson, + com.google.gson.JsonElement value) { + return additionalValue0(gson, value); + } + + private com.google.gson.JsonElement additionalValue0(com.google.gson.Gson gson, + com.google.gson.JsonElement value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + return value; + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/OpenNumbers.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/OpenNumbers.java.golden new file mode 100644 index 000000000..74d810c06 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/OpenNumbers.java.golden @@ -0,0 +1,76 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +import com.google.gson.annotations.SerializedName; + +@com.google.gson.annotations.JsonAdapter(value = OpenNumbersAdditionalPropertiesAdapterFactory.class, nullSafe = false) +public class OpenNumbers { + private int count; + @SerializedName("nullable_value") + private NestedValue nullableValue; + @SerializedName("optional_count") + private Integer optionalCount; + private Double weight; + + public OpenNumbers(int count, NestedValue nullableValue, Integer optionalCount, Double weight) { + this.count = count; + this.nullableValue = nullableValue; + this.optionalCount = optionalCount; + this.weight = weight; + } + + public int getCount() { + return this.count; + } + + public NestedValue getNullableValue() { + return this.nullableValue; + } + + public Integer getOptionalCount() { + return this.optionalCount; + } + + public Double getWeight() { + return this.weight; + } + + public java.util.Map getAdditionalProperties() { + if (this.additionalProperties == null) { + this.additionalProperties = new java.util.LinkedHashMap<>(); + } + return this.additionalProperties; + } + private transient java.util.Map additionalProperties = new java.util.LinkedHashMap<>(); +} + +class OpenNumbersAdditionalPropertiesAdapterFactory extends com.example.sdk.runtime.AdditionalPropertiesAdapterFactory { + public OpenNumbersAdditionalPropertiesAdapterFactory() { + super(com.example.sdk.models.OpenNumbers.class, new com.google.gson.reflect.TypeToken() {}, new java.lang.String[] {"count", "nullable_value", "optional_count", "weight"}); + } + + @Override + protected java.util.Map properties(com.example.sdk.models.OpenNumbers value) { + return value.getAdditionalProperties(); + } + + @Override + protected com.google.gson.JsonElement encode(com.google.gson.Gson gson, + com.google.gson.JsonElement value) { + return additionalValue0(gson, value); + } + + private com.google.gson.JsonElement additionalValue0(com.google.gson.Gson gson, + com.google.gson.JsonElement value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + return value; + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/OptionalKnown.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/OptionalKnown.java.golden new file mode 100644 index 000000000..5187a8e24 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/OptionalKnown.java.golden @@ -0,0 +1,57 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +import com.google.gson.annotations.SerializedName; + +@com.google.gson.annotations.JsonAdapter(value = OptionalKnownAdditionalPropertiesAdapterFactory.class, nullSafe = false) +public class OptionalKnown { + @SerializedName("known_field") + private String knownField; + + public OptionalKnown(String knownField) { + this.knownField = knownField; + } + + public String getKnownField() { + return this.knownField; + } + + public java.util.Map getAdditionalProperties() { + if (this.additionalProperties == null) { + this.additionalProperties = new java.util.LinkedHashMap<>(); + } + return this.additionalProperties; + } + private transient java.util.Map additionalProperties = new java.util.LinkedHashMap<>(); +} + +class OptionalKnownAdditionalPropertiesAdapterFactory extends com.example.sdk.runtime.AdditionalPropertiesAdapterFactory { + public OptionalKnownAdditionalPropertiesAdapterFactory() { + super(com.example.sdk.models.OptionalKnown.class, new com.google.gson.reflect.TypeToken() {}, new java.lang.String[] {"known_field"}); + } + + @Override + protected java.util.Map properties(com.example.sdk.models.OptionalKnown value) { + return value.getAdditionalProperties(); + } + + @Override + protected com.google.gson.JsonElement encode(com.google.gson.Gson gson, + com.google.gson.JsonElement value) { + return additionalValue0(gson, value); + } + + private com.google.gson.JsonElement additionalValue0(com.google.gson.Gson gson, + com.google.gson.JsonElement value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + return value; + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/PlainNumbers.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/PlainNumbers.java.golden new file mode 100644 index 000000000..2ce55e058 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/PlainNumbers.java.golden @@ -0,0 +1,40 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +import com.google.gson.annotations.SerializedName; + +public class PlainNumbers { + private int count; + @SerializedName("nullable_value") + private NestedValue nullableValue; + @SerializedName("optional_count") + private Integer optionalCount; + private Double weight; + + public PlainNumbers(int count, NestedValue nullableValue, Integer optionalCount, + Double weight) { + this.count = count; + this.nullableValue = nullableValue; + this.optionalCount = optionalCount; + this.weight = weight; + } + + public int getCount() { + return this.count; + } + + public NestedValue getNullableValue() { + return this.nullableValue; + } + + public Integer getOptionalCount() { + return this.optionalCount; + } + + public Double getWeight() { + return this.weight; + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/PureRecursive.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/PureRecursive.java.golden new file mode 100644 index 000000000..4040f88bf --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/PureRecursive.java.golden @@ -0,0 +1,49 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +@com.google.gson.annotations.JsonAdapter(value = PureRecursiveAdditionalPropertiesAdapterFactory.class, nullSafe = false) +public class PureRecursive { + public PureRecursive() { + + } + + public java.util.Map getAdditionalProperties() { + if (this.additionalProperties == null) { + this.additionalProperties = new java.util.LinkedHashMap<>(); + } + return this.additionalProperties; + } + private transient java.util.Map additionalProperties = new java.util.LinkedHashMap<>(); +} + +class PureRecursiveAdditionalPropertiesAdapterFactory extends com.example.sdk.runtime.AdditionalPropertiesAdapterFactory { + public PureRecursiveAdditionalPropertiesAdapterFactory() { + super(com.example.sdk.models.PureRecursive.class, new com.google.gson.reflect.TypeToken() {}, new java.lang.String[] {}); + } + + @Override + protected java.util.Map properties(com.example.sdk.models.PureRecursive value) { + return value.getAdditionalProperties(); + } + + @Override + protected com.google.gson.JsonElement encode(com.google.gson.Gson gson, + com.example.sdk.models.PureRecursive value) { + return additionalValue0(gson, value); + } + + private com.google.gson.JsonElement additionalValue0(com.google.gson.Gson gson, + com.example.sdk.models.PureRecursive value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + return gson.toJsonTree(value, new com.google.gson.reflect.TypeToken() {}.getType()); + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/RawContainers.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/RawContainers.java.golden new file mode 100644 index 000000000..4f27dcf05 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/RawContainers.java.golden @@ -0,0 +1,77 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +@com.google.gson.annotations.JsonAdapter(value = RawContainersAdditionalPropertiesAdapterFactory.class, nullSafe = false) +public class RawContainers { + public RawContainers() { + + } + + public java.util.Map>> getAdditionalProperties() { + if (this.additionalProperties == null) { + this.additionalProperties = new java.util.LinkedHashMap<>(); + } + return this.additionalProperties; + } + private transient java.util.Map>> additionalProperties = new java.util.LinkedHashMap<>(); +} + +class RawContainersAdditionalPropertiesAdapterFactory extends com.example.sdk.runtime.AdditionalPropertiesAdapterFactory>> { + public RawContainersAdditionalPropertiesAdapterFactory() { + super(com.example.sdk.models.RawContainers.class, new com.google.gson.reflect.TypeToken>>() {}, new java.lang.String[] {}); + } + + @Override + protected java.util.Map>> properties(com.example.sdk.models.RawContainers value) { + return value.getAdditionalProperties(); + } + + @Override + protected com.google.gson.JsonElement encode(com.google.gson.Gson gson, + java.util.List> value) { + return additionalValue2(gson, value); + } + + private com.google.gson.JsonElement additionalValue0(com.google.gson.Gson gson, + com.google.gson.JsonElement value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + return value; + } + + private com.google.gson.JsonElement additionalValue1(com.google.gson.Gson gson, + java.util.Map value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + var result = new com.google.gson.JsonObject(); + for (var entry : value.entrySet()) { + result.add(entry.getKey(), additionalValue0(gson, entry.getValue())); + } + return result; + } + + private com.google.gson.JsonElement additionalValue2(com.google.gson.Gson gson, + java.util.List> value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + var result = new com.google.gson.JsonArray(); + for (var item : value) { + result.add(additionalValue1(gson, item)); + } + return result; + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/RawMap.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/RawMap.java.golden new file mode 100644 index 000000000..b69b67f91 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/RawMap.java.golden @@ -0,0 +1,48 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +@com.google.gson.annotations.JsonAdapter(value = RawMapAdditionalPropertiesAdapterFactory.class, nullSafe = false) +public class RawMap { + public RawMap() { + + } + + public java.util.Map getAdditionalProperties() { + if (this.additionalProperties == null) { + this.additionalProperties = new java.util.LinkedHashMap<>(); + } + return this.additionalProperties; + } + private transient java.util.Map additionalProperties = new java.util.LinkedHashMap<>(); +} + +class RawMapAdditionalPropertiesAdapterFactory extends com.example.sdk.runtime.AdditionalPropertiesAdapterFactory { + public RawMapAdditionalPropertiesAdapterFactory() { + super(com.example.sdk.models.RawMap.class, new com.google.gson.reflect.TypeToken() {}, new java.lang.String[] {}); + } + + @Override + protected java.util.Map properties(com.example.sdk.models.RawMap value) { + return value.getAdditionalProperties(); + } + + @Override + protected com.google.gson.JsonElement encode(com.google.gson.Gson gson, + com.google.gson.JsonElement value) { + return additionalValue0(gson, value); + } + + private com.google.gson.JsonElement additionalValue0(com.google.gson.Gson gson, + com.google.gson.JsonElement value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + return value; + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/Recursive.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/Recursive.java.golden new file mode 100644 index 000000000..c4526ed86 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/Recursive.java.golden @@ -0,0 +1,58 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +import com.google.gson.annotations.SerializedName; + +@com.google.gson.annotations.JsonAdapter(value = RecursiveAdditionalPropertiesAdapterFactory.class, nullSafe = false) +public class Recursive { + @SerializedName("wire_value") + private String wireValue; + + public Recursive(String wireValue) { + this.wireValue = wireValue; + } + + public String getWireValue() { + return this.wireValue; + } + + public java.util.Map getAdditionalProperties() { + if (this.additionalProperties == null) { + this.additionalProperties = new java.util.LinkedHashMap<>(); + } + return this.additionalProperties; + } + private transient java.util.Map additionalProperties = new java.util.LinkedHashMap<>(); +} + +class RecursiveAdditionalPropertiesAdapterFactory extends com.example.sdk.runtime.AdditionalPropertiesAdapterFactory { + public RecursiveAdditionalPropertiesAdapterFactory() { + super(com.example.sdk.models.Recursive.class, new com.google.gson.reflect.TypeToken() {}, new java.lang.String[] {"wire_value"}); + } + + @Override + protected java.util.Map properties(com.example.sdk.models.Recursive value) { + return value.getAdditionalProperties(); + } + + @Override + protected com.google.gson.JsonElement encode(com.google.gson.Gson gson, + com.example.sdk.models.Recursive value) { + return additionalValue0(gson, value); + } + + private com.google.gson.JsonElement additionalValue0(com.google.gson.Gson gson, + com.example.sdk.models.Recursive value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + return gson.toJsonTree(value, new com.google.gson.reflect.TypeToken() {}.getType()); + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/Status.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/Status.java.golden new file mode 100644 index 000000000..a155fd85b --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/Status.java.golden @@ -0,0 +1,24 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +import com.google.gson.annotations.SerializedName; + +public enum Status { + @SerializedName("active") + ACTIVE("active"), + @SerializedName("inactive") + INACTIVE("inactive"); + + private final String value; + + Status(String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/StatusValues.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/StatusValues.java.golden new file mode 100644 index 000000000..c5d7e9dc3 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/StatusValues.java.golden @@ -0,0 +1,49 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +@com.google.gson.annotations.JsonAdapter(value = StatusValuesAdditionalPropertiesAdapterFactory.class, nullSafe = false) +public class StatusValues { + public StatusValues() { + + } + + public java.util.Map getAdditionalProperties() { + if (this.additionalProperties == null) { + this.additionalProperties = new java.util.LinkedHashMap<>(); + } + return this.additionalProperties; + } + private transient java.util.Map additionalProperties = new java.util.LinkedHashMap<>(); +} + +class StatusValuesAdditionalPropertiesAdapterFactory extends com.example.sdk.runtime.AdditionalPropertiesAdapterFactory { + public StatusValuesAdditionalPropertiesAdapterFactory() { + super(com.example.sdk.models.StatusValues.class, new com.google.gson.reflect.TypeToken() {}, new java.lang.String[] {}); + } + + @Override + protected java.util.Map properties(com.example.sdk.models.StatusValues value) { + return value.getAdditionalProperties(); + } + + @Override + protected com.google.gson.JsonElement encode(com.google.gson.Gson gson, + com.example.sdk.models.Status value) { + return additionalValue0(gson, value); + } + + private com.google.gson.JsonElement additionalValue0(com.google.gson.Gson gson, + com.example.sdk.models.Status value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + return gson.toJsonTree(value, new com.google.gson.reflect.TypeToken() {}.getType()); + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/StorageCollision.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/StorageCollision.java.golden new file mode 100644 index 000000000..c626c4afe --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/StorageCollision.java.golden @@ -0,0 +1,55 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +@com.google.gson.annotations.JsonAdapter(value = StorageCollisionAdditionalPropertiesAdapterFactory.class, nullSafe = false) +public class StorageCollision { + private String additionalProperties; + + public StorageCollision(String additionalProperties) { + this.additionalProperties = additionalProperties; + } + + public String getAdditionalProperties() { + return this.additionalProperties; + } + + public java.util.Map getAdditionalProperties2() { + if (this.additionalProperties2 == null) { + this.additionalProperties2 = new java.util.LinkedHashMap<>(); + } + return this.additionalProperties2; + } + private transient java.util.Map additionalProperties2 = new java.util.LinkedHashMap<>(); +} + +class StorageCollisionAdditionalPropertiesAdapterFactory extends com.example.sdk.runtime.AdditionalPropertiesAdapterFactory { + public StorageCollisionAdditionalPropertiesAdapterFactory() { + super(com.example.sdk.models.StorageCollision.class, new com.google.gson.reflect.TypeToken() {}, new java.lang.String[] {"additionalProperties"}); + } + + @Override + protected java.util.Map properties(com.example.sdk.models.StorageCollision value) { + return value.getAdditionalProperties2(); + } + + @Override + protected com.google.gson.JsonElement encode(com.google.gson.Gson gson, + com.example.sdk.models.NestedValue value) { + return additionalValue0(gson, value); + } + + private com.google.gson.JsonElement additionalValue0(com.google.gson.Gson gson, + com.example.sdk.models.NestedValue value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + return gson.toJsonTree(value, new com.google.gson.reflect.TypeToken() {}.getType()); + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/TypedMap.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/TypedMap.java.golden new file mode 100644 index 000000000..67c78bfb6 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/TypedMap.java.golden @@ -0,0 +1,49 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +@com.google.gson.annotations.JsonAdapter(value = TypedMapAdditionalPropertiesAdapterFactory.class, nullSafe = false) +public class TypedMap { + public TypedMap() { + + } + + public java.util.Map getAdditionalProperties() { + if (this.additionalProperties == null) { + this.additionalProperties = new java.util.LinkedHashMap<>(); + } + return this.additionalProperties; + } + private transient java.util.Map additionalProperties = new java.util.LinkedHashMap<>(); +} + +class TypedMapAdditionalPropertiesAdapterFactory extends com.example.sdk.runtime.AdditionalPropertiesAdapterFactory { + public TypedMapAdditionalPropertiesAdapterFactory() { + super(com.example.sdk.models.TypedMap.class, new com.google.gson.reflect.TypeToken() {}, new java.lang.String[] {}); + } + + @Override + protected java.util.Map properties(com.example.sdk.models.TypedMap value) { + return value.getAdditionalProperties(); + } + + @Override + protected com.google.gson.JsonElement encode(com.google.gson.Gson gson, + com.example.sdk.models.NestedValue value) { + return additionalValue0(gson, value); + } + + private com.google.gson.JsonElement additionalValue0(com.google.gson.Gson gson, + com.example.sdk.models.NestedValue value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + return gson.toJsonTree(value, new com.google.gson.reflect.TypeToken() {}.getType()); + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/ValueAlias.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/ValueAlias.java.golden new file mode 100644 index 000000000..b7c9eee54 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/ValueAlias.java.golden @@ -0,0 +1,17 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.models; + +public class ValueAlias { + private NestedValue value; + + public ValueAlias(NestedValue value) { + this.value = value; + } + + public NestedValue getValue() { + return this.value; + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/models/ValueChoice.java.golden b/tests/golden/java/java-okhttp/object-extensions/models/ValueChoice.java.golden new file mode 100644 index 000000000..0d478e6c6 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/models/ValueChoice.java.golden @@ -0,0 +1,100 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 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.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(value = ValueChoiceJsonAdapter.class, nullSafe = false) +public sealed interface ValueChoice { + record VariantNestedValue(NestedValue value0) implements ValueChoice {} + + record VariantInteger(int value0) implements ValueChoice {} +} + +final class ValueChoiceJsonAdapter extends TypeAdapter { + private final Gson gson = new Gson(); + + @Override + public ValueChoice read(JsonReader reader) throws java.io.IOException { + JsonElement element = JsonParser.parseReader(reader); + if (element.isJsonNull()) { + return null; + } + int selected = -1; + if (selected < 0 && matches0(element)) { + selected = 0; + } + if (selected < 0 && matches2(element)) { + selected = 1; + } + if (selected == 0) { + return new ValueChoice.VariantNestedValue(gson.fromJson(element, new TypeToken() {}.getType())); + } + if (selected == 1) { + return new ValueChoice.VariantInteger(gson.fromJson(element, Integer.class)); + } + throw new JsonParseException("unknown ValueChoice union value"); + } + + @Override + public void write(JsonWriter writer, ValueChoice value) throws java.io.IOException { + if (value == null) { + writer.nullValue(); + return; + } + if (value instanceof ValueChoice.VariantNestedValue) { + gson.toJson(gson.toJsonTree(((ValueChoice.VariantNestedValue) value).value0()), writer); + return; + } + if (value instanceof ValueChoice.VariantInteger) { + gson.toJson(gson.toJsonTree(((ValueChoice.VariantInteger) value).value0()), writer); + return; + } + } + + private static boolean matches0(JsonElement candidate) { + if (!candidate.isJsonObject()) { + return false; + } + JsonObject object = candidate.getAsJsonObject(); + if (!object.has("wire_value") || !matches1(object.get("wire_value"))) { + return false; + } + return true; + } + + private static boolean matches1(JsonElement candidate) { + return candidate.isJsonPrimitive() && candidate.getAsJsonPrimitive().isString(); + } + + private static boolean matches2(JsonElement candidate) { + if (!candidate.isJsonPrimitive() || !candidate.getAsJsonPrimitive().isNumber()) { + return false; + } + java.lang.String literal = candidate.getAsString(); + java.math.BigDecimal significand = numberSignificand(literal); + return significand.signum() == 0 || numberExponent(literal).compareTo(java.math.BigInteger.valueOf(significand.scale())) >= 0; + } + + private static java.math.BigDecimal numberSignificand(java.lang.String literal) { + int exponent = java.lang.Math.max(literal.indexOf('e'), literal.indexOf('E')); + return new java.math.BigDecimal(exponent < 0 ? literal : literal.substring(0, exponent)).stripTrailingZeros(); + } + + private static java.math.BigInteger numberExponent(java.lang.String literal) { + int exponent = java.lang.Math.max(literal.indexOf('e'), literal.indexOf('E')); + return exponent < 0 ? java.math.BigInteger.ZERO : new java.math.BigInteger(literal.substring(exponent + 1)); + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/runtime/AdditionalPropertiesAdapterFactory.java.golden b/tests/golden/java/java-okhttp/object-extensions/runtime/AdditionalPropertiesAdapterFactory.java.golden new file mode 100644 index 000000000..44bf50b93 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/runtime/AdditionalPropertiesAdapterFactory.java.golden @@ -0,0 +1,164 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.runtime; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.annotations.SerializedName; +import com.google.gson.internal.bind.JsonTreeReader; +import com.google.gson.internal.bind.JsonTreeWriter; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +/** Internal flat-object support; ordinary fields retain their configured Gson delegate. */ +public abstract class AdditionalPropertiesAdapterFactory implements TypeAdapterFactory { + private final Class modelClass; + private final TypeToken valueType; + private final Set declaredNames; + + protected AdditionalPropertiesAdapterFactory( + Class modelClass, TypeToken valueType, String... declaredNames) { + this.modelClass = modelClass; + this.valueType = valueType; + this.declaredNames = new HashSet<>(Arrays.asList(declaredNames)); + } + + protected abstract Map properties(M value); + + protected abstract JsonElement encode(Gson gson, V value); + + @Override + @SuppressWarnings("unchecked") + public final TypeAdapter create(Gson gson, TypeToken type) { + if (type.getRawType() != modelClass) { + return null; + } + TypeAdapter delegate = gson.getDelegateAdapter(this, TypeToken.get(modelClass)); + TypeAdapter values = gson.getAdapter(valueType); + TypeAdapter trees = gson.getAdapter(JsonElement.class); + Set known = new HashSet<>(declaredNames); + for (Field field : modelClass.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) || Modifier.isTransient(field.getModifiers())) { + continue; + } + SerializedName annotation = field.getAnnotation(SerializedName.class); + if (annotation == null) { + known.add(gson.fieldNamingStrategy().translateName(field)); + } else { + known.add(annotation.value()); + known.addAll(Arrays.asList(annotation.alternate())); + } + } + TypeAdapter adapter = new TypeAdapter<>() { + @Override + public M read(JsonReader reader) throws IOException { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); + return null; + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw new JsonSyntaxException("Expected " + modelClass.getSimpleName() + " object"); + } + Map extras = new LinkedHashMap<>(); + StringWriter buffer = new StringWriter(); + JsonWriter named = new JsonWriter(buffer); + named.setSerializeNulls(true); + named.setStrictness(reader.getStrictness()); + named.beginObject(); + reader.beginObject(); + while (reader.hasNext()) { + String key = reader.nextName(); + if (known.contains(key)) { + named.name(key); + trees.write(named, trees.read(reader)); + } else { + // Typed extensions consume the original reader: a tree reader + // can silently truncate fractional or overflowing integers. + extras.put(key, values.read(reader)); + } + } + reader.endObject(); + named.endObject(); + JsonReader replay; + // Gson's public tree API uses a different integer coercion policy. + // Preserve that input mode instead of imposing streaming semantics. + if (reader instanceof JsonTreeReader) { + replay = new JsonTreeReader(JsonParser.parseString(buffer.toString())); + } else { + // Text replay retains numeric lexemes and duplicate named fields, + // unlike fromJsonTree, while using the configured streaming delegate. + replay = new JsonReader(new StringReader(buffer.toString())); + } + replay.setStrictness(reader.getStrictness()); + M result = delegate.read(replay); + if (result != null) { + properties(result).putAll(extras); + } + return result; + } + + @Override + public void write(JsonWriter writer, M value) throws IOException { + if (value == null) { + writer.nullValue(); + return; + } + Map extras = properties(value); + for (String key : extras.keySet()) { + if (known.contains(key)) { + throw new IllegalArgumentException("Additional property collides with declared wire field: " + key); + } + } + // Use Gson's native tree writer rather than reparsing JSON text: + // configured non-finite numeric values would otherwise become strings. + JsonTreeWriter named = new JsonTreeWriter(); + named.setSerializeNulls(writer.getSerializeNulls()); + named.setHtmlSafe(writer.isHtmlSafe()); + named.setStrictness(writer.getStrictness()); + delegate.write(named, value); + JsonObject fields = named.get().getAsJsonObject(); + writer.beginObject(); + for (Map.Entry entry : fields.entrySet()) { + writer.name(entry.getKey()); + trees.write(writer, entry.getValue()); + } + for (Map.Entry entry : extras.entrySet()) { + if (fields.has(entry.getKey())) { + throw new IllegalArgumentException("Additional property collides with declared wire field: " + entry.getKey()); + } + JsonElement tree = encode(gson, entry.getValue()); + writer.name(entry.getKey()); + boolean serializeNulls = writer.getSerializeNulls(); + writer.setSerializeNulls(true); + try { + trees.write(writer, tree); + } finally { + writer.setSerializeNulls(serializeNulls); + } + } + writer.endObject(); + } + }; + // The raw-type identity above proves this cast; generated models are not generic. + return (TypeAdapter) adapter; + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/runtime/ApiClient.java.golden b/tests/golden/java/java-okhttp/object-extensions/runtime/ApiClient.java.golden new file mode 100644 index 000000000..cfd1c3884 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/runtime/ApiClient.java.golden @@ -0,0 +1,78 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.runtime; + +import java.io.IOException; +import java.util.Map; + +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class ApiClient { + private static final MediaType JSON = MediaType.get("application/json"); + + private final String baseUrl; + private final OkHttpClient client; + private final Authenticator authenticator; + private final Map defaultHeaders; + + public ApiClient(String baseUrl) { + this(baseUrl, new OkHttpClient(), null, Map.of()); + } + + public ApiClient(String baseUrl, OkHttpClient client, Authenticator authenticator, Map defaultHeaders) { + this.baseUrl = baseUrl; + this.client = client; + this.authenticator = authenticator; + this.defaultHeaders = defaultHeaders; + } + + public Request newRequest(String method, String path, Map query, String body) { + RequestBody requestBody = null; + if (body != null) { + requestBody = RequestBody.create(body, JSON); + } + return buildRequest(method, path, query, requestBody); + } + + public Request newRequestWithBody(String method, String path, Map query, RequestBody body) { + return buildRequest(method, path, query, body); + } + + private Request buildRequest(String method, String path, Map query, RequestBody requestBody) { + String url = baseUrl.replaceAll("/+$", "") + path; + HttpUrl parsed = HttpUrl.parse(url); + if (parsed == null) { + throw new IllegalArgumentException("Invalid URL: " + url); + } + HttpUrl.Builder urlBuilder = parsed.newBuilder(); + if (query != null) { + for (Map.Entry entry : query.entrySet()) { + urlBuilder.addQueryParameter(entry.getKey(), entry.getValue()); + } + } + + Request.Builder builder = new Request.Builder() + .url(urlBuilder.build()) + .method(method, requestBody); + + for (Map.Entry entry : defaultHeaders.entrySet()) { + builder.header(entry.getKey(), entry.getValue()); + } + if (authenticator != null) { + authenticator.authenticate(builder); + } + + return builder.build(); + } + + public Response execute(Request request) throws IOException { + return client.newCall(request).execute(); + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/runtime/ApiException.java.golden b/tests/golden/java/java-okhttp/object-extensions/runtime/ApiException.java.golden new file mode 100644 index 000000000..433db4fc0 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/runtime/ApiException.java.golden @@ -0,0 +1,30 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.runtime; + +public class ApiException extends RuntimeException { + private final int statusCode; + private final String status; + private final String body; + + public ApiException(int statusCode, String status, String body) { + super("API error " + statusCode + ": " + status); + this.statusCode = statusCode; + this.status = status; + this.body = body; + } + + public int getStatusCode() { + return statusCode; + } + + public String getStatus() { + return status; + } + + public String getBody() { + return body; + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/runtime/ApiKeyAuth.java.golden b/tests/golden/java/java-okhttp/object-extensions/runtime/ApiKeyAuth.java.golden new file mode 100644 index 000000000..046baf578 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/runtime/ApiKeyAuth.java.golden @@ -0,0 +1,41 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.runtime; + +import java.util.function.Supplier; +import okhttp3.Request; + +public class ApiKeyAuth implements Authenticator { + private final String key; + private final Supplier keyProvider; + private final String name; + private final ApiKeyLocation location; + + public ApiKeyAuth(String key, String name, ApiKeyLocation location) { + this.key = key; + this.keyProvider = null; + this.name = name; + this.location = location; + } + + public ApiKeyAuth(Supplier keyProvider, String name, ApiKeyLocation location) { + this.key = null; + this.keyProvider = keyProvider; + this.name = name; + this.location = location; + } + + @Override + public void authenticate(Request.Builder builder) { + String k = keyProvider != null ? keyProvider.get() : key; + if (location == ApiKeyLocation.HEADER) { + builder.header(name, k); + } else if (location == ApiKeyLocation.QUERY) { + builder.url(builder.build().url().newBuilder() + .addQueryParameter(name, k) + .build()); + } + } +} diff --git a/tests/golden/java/java-okhttp/object-extensions/runtime/ApiKeyLocation.java.golden b/tests/golden/java/java-okhttp/object-extensions/runtime/ApiKeyLocation.java.golden new file mode 100644 index 000000000..6d89e3d46 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/runtime/ApiKeyLocation.java.golden @@ -0,0 +1,10 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.runtime; + +public enum ApiKeyLocation { + HEADER, + QUERY +} diff --git a/tests/golden/java/java-okhttp/object-extensions/runtime/Authenticator.java.golden b/tests/golden/java/java-okhttp/object-extensions/runtime/Authenticator.java.golden new file mode 100644 index 000000000..6e9b22929 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/runtime/Authenticator.java.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.runtime; + +import okhttp3.Request; + +public interface Authenticator { + void authenticate(Request.Builder builder); +} diff --git a/tests/golden/java/java-okhttp/object-extensions/runtime/BearerAuth.java.golden b/tests/golden/java/java-okhttp/object-extensions/runtime/BearerAuth.java.golden new file mode 100644 index 000000000..46a008ea4 --- /dev/null +++ b/tests/golden/java/java-okhttp/object-extensions/runtime/BearerAuth.java.golden @@ -0,0 +1,29 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Java additional properties — 1.0.0 + +package com.example.sdk.runtime; + +import java.util.function.Supplier; +import okhttp3.Request; + +public class BearerAuth implements Authenticator { + private final String token; + private final Supplier tokenProvider; + + public BearerAuth(String token) { + this.token = token; + this.tokenProvider = null; + } + + public BearerAuth(Supplier tokenProvider) { + this.token = null; + this.tokenProvider = tokenProvider; + } + + @Override + public void authenticate(Request.Builder builder) { + String t = tokenProvider != null ? tokenProvider.get() : token; + builder.header("Authorization", "Bearer " + t); + } +} diff --git a/tests/golden/java/java-okhttp/open-tagged-additional-properties/models/EventCompleted.java.golden b/tests/golden/java/java-okhttp/open-tagged-additional-properties/models/EventCompleted.java.golden index 8bb96d574..a256ecfa4 100644 --- a/tests/golden/java/java-okhttp/open-tagged-additional-properties/models/EventCompleted.java.golden +++ b/tests/golden/java/java-okhttp/open-tagged-additional-properties/models/EventCompleted.java.golden @@ -4,6 +4,7 @@ package com.example.sdk.models; +@com.google.gson.annotations.JsonAdapter(value = EventCompletedAdditionalPropertiesAdapterFactory.class, nullSafe = false) public class EventCompleted { private String type; @@ -14,4 +15,40 @@ public class EventCompleted { public String getType() { return this.type; } + + public java.util.Map getAdditionalProperties() { + if (this.additionalProperties == null) { + this.additionalProperties = new java.util.LinkedHashMap<>(); + } + return this.additionalProperties; + } + private transient java.util.Map additionalProperties = new java.util.LinkedHashMap<>(); +} + +class EventCompletedAdditionalPropertiesAdapterFactory extends com.example.sdk.runtime.AdditionalPropertiesAdapterFactory { + public EventCompletedAdditionalPropertiesAdapterFactory() { + super(com.example.sdk.models.EventCompleted.class, new com.google.gson.reflect.TypeToken() {}, new java.lang.String[] {"type"}); + } + + @Override + protected java.util.Map properties(com.example.sdk.models.EventCompleted value) { + return value.getAdditionalProperties(); + } + + @Override + protected com.google.gson.JsonElement encode(com.google.gson.Gson gson, + com.google.gson.JsonElement value) { + return additionalValue0(gson, value); + } + + private com.google.gson.JsonElement additionalValue0(com.google.gson.Gson gson, + com.google.gson.JsonElement value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + return value; + } } diff --git a/tests/golden/java/java-okhttp/open-tagged-additional-properties/models/EventSearching.java.golden b/tests/golden/java/java-okhttp/open-tagged-additional-properties/models/EventSearching.java.golden index f3bd05c37..4fce59181 100644 --- a/tests/golden/java/java-okhttp/open-tagged-additional-properties/models/EventSearching.java.golden +++ b/tests/golden/java/java-okhttp/open-tagged-additional-properties/models/EventSearching.java.golden @@ -6,6 +6,7 @@ package com.example.sdk.models; import com.google.gson.annotations.SerializedName; +@com.google.gson.annotations.JsonAdapter(value = EventSearchingAdditionalPropertiesAdapterFactory.class, nullSafe = false) public class EventSearching { @SerializedName("sequence_number") private int sequenceNumber; @@ -23,4 +24,40 @@ public class EventSearching { public String getType() { return this.type; } + + public java.util.Map getAdditionalProperties() { + if (this.additionalProperties == null) { + this.additionalProperties = new java.util.LinkedHashMap<>(); + } + return this.additionalProperties; + } + private transient java.util.Map additionalProperties = new java.util.LinkedHashMap<>(); +} + +class EventSearchingAdditionalPropertiesAdapterFactory extends com.example.sdk.runtime.AdditionalPropertiesAdapterFactory { + public EventSearchingAdditionalPropertiesAdapterFactory() { + super(com.example.sdk.models.EventSearching.class, new com.google.gson.reflect.TypeToken() {}, new java.lang.String[] {"sequence_number", "type"}); + } + + @Override + protected java.util.Map properties(com.example.sdk.models.EventSearching value) { + return value.getAdditionalProperties(); + } + + @Override + protected com.google.gson.JsonElement encode(com.google.gson.Gson gson, + com.google.gson.JsonElement value) { + return additionalValue0(gson, value); + } + + private com.google.gson.JsonElement additionalValue0(com.google.gson.Gson gson, + com.google.gson.JsonElement value) { + if (value == null) { + return com.google.gson.JsonNull.INSTANCE; + } + return value; + } } diff --git a/tests/golden/java/java-okhttp/open-tagged-additional-properties/runtime/AdditionalPropertiesAdapterFactory.java.golden b/tests/golden/java/java-okhttp/open-tagged-additional-properties/runtime/AdditionalPropertiesAdapterFactory.java.golden new file mode 100644 index 000000000..f920a5d14 --- /dev/null +++ b/tests/golden/java/java-okhttp/open-tagged-additional-properties/runtime/AdditionalPropertiesAdapterFactory.java.golden @@ -0,0 +1,164 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Open tagged additional properties — 1.0.0 + +package com.example.sdk.runtime; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.annotations.SerializedName; +import com.google.gson.internal.bind.JsonTreeReader; +import com.google.gson.internal.bind.JsonTreeWriter; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +/** Internal flat-object support; ordinary fields retain their configured Gson delegate. */ +public abstract class AdditionalPropertiesAdapterFactory implements TypeAdapterFactory { + private final Class modelClass; + private final TypeToken valueType; + private final Set declaredNames; + + protected AdditionalPropertiesAdapterFactory( + Class modelClass, TypeToken valueType, String... declaredNames) { + this.modelClass = modelClass; + this.valueType = valueType; + this.declaredNames = new HashSet<>(Arrays.asList(declaredNames)); + } + + protected abstract Map properties(M value); + + protected abstract JsonElement encode(Gson gson, V value); + + @Override + @SuppressWarnings("unchecked") + public final TypeAdapter create(Gson gson, TypeToken type) { + if (type.getRawType() != modelClass) { + return null; + } + TypeAdapter delegate = gson.getDelegateAdapter(this, TypeToken.get(modelClass)); + TypeAdapter values = gson.getAdapter(valueType); + TypeAdapter trees = gson.getAdapter(JsonElement.class); + Set known = new HashSet<>(declaredNames); + for (Field field : modelClass.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) || Modifier.isTransient(field.getModifiers())) { + continue; + } + SerializedName annotation = field.getAnnotation(SerializedName.class); + if (annotation == null) { + known.add(gson.fieldNamingStrategy().translateName(field)); + } else { + known.add(annotation.value()); + known.addAll(Arrays.asList(annotation.alternate())); + } + } + TypeAdapter adapter = new TypeAdapter<>() { + @Override + public M read(JsonReader reader) throws IOException { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); + return null; + } + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw new JsonSyntaxException("Expected " + modelClass.getSimpleName() + " object"); + } + Map extras = new LinkedHashMap<>(); + StringWriter buffer = new StringWriter(); + JsonWriter named = new JsonWriter(buffer); + named.setSerializeNulls(true); + named.setStrictness(reader.getStrictness()); + named.beginObject(); + reader.beginObject(); + while (reader.hasNext()) { + String key = reader.nextName(); + if (known.contains(key)) { + named.name(key); + trees.write(named, trees.read(reader)); + } else { + // Typed extensions consume the original reader: a tree reader + // can silently truncate fractional or overflowing integers. + extras.put(key, values.read(reader)); + } + } + reader.endObject(); + named.endObject(); + JsonReader replay; + // Gson's public tree API uses a different integer coercion policy. + // Preserve that input mode instead of imposing streaming semantics. + if (reader instanceof JsonTreeReader) { + replay = new JsonTreeReader(JsonParser.parseString(buffer.toString())); + } else { + // Text replay retains numeric lexemes and duplicate named fields, + // unlike fromJsonTree, while using the configured streaming delegate. + replay = new JsonReader(new StringReader(buffer.toString())); + } + replay.setStrictness(reader.getStrictness()); + M result = delegate.read(replay); + if (result != null) { + properties(result).putAll(extras); + } + return result; + } + + @Override + public void write(JsonWriter writer, M value) throws IOException { + if (value == null) { + writer.nullValue(); + return; + } + Map extras = properties(value); + for (String key : extras.keySet()) { + if (known.contains(key)) { + throw new IllegalArgumentException("Additional property collides with declared wire field: " + key); + } + } + // Use Gson's native tree writer rather than reparsing JSON text: + // configured non-finite numeric values would otherwise become strings. + JsonTreeWriter named = new JsonTreeWriter(); + named.setSerializeNulls(writer.getSerializeNulls()); + named.setHtmlSafe(writer.isHtmlSafe()); + named.setStrictness(writer.getStrictness()); + delegate.write(named, value); + JsonObject fields = named.get().getAsJsonObject(); + writer.beginObject(); + for (Map.Entry entry : fields.entrySet()) { + writer.name(entry.getKey()); + trees.write(writer, entry.getValue()); + } + for (Map.Entry entry : extras.entrySet()) { + if (fields.has(entry.getKey())) { + throw new IllegalArgumentException("Additional property collides with declared wire field: " + entry.getKey()); + } + JsonElement tree = encode(gson, entry.getValue()); + writer.name(entry.getKey()); + boolean serializeNulls = writer.getSerializeNulls(); + writer.setSerializeNulls(true); + try { + trees.write(writer, tree); + } finally { + writer.setSerializeNulls(serializeNulls); + } + } + writer.endObject(); + } + }; + // The raw-type identity above proves this cast; generated models are not generic. + return (TypeAdapter) adapter; + } +} diff --git a/tests/golden_tests_java_okhttp.rs b/tests/golden_tests_java_okhttp.rs index 914c1f733..46c1088af 100644 --- a/tests/golden_tests_java_okhttp.rs +++ b/tests/golden_tests_java_okhttp.rs @@ -86,6 +86,8 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("additional-properties", "valid/additional-properties/additional-properties.yaml"), ("open-tagged-additional-properties", "valid/open-tagged-additional-properties.yaml"), + ("object-extensions", "valid/java-additional-properties.yaml"), + ("object-extension-helper-types", "valid/java-additional-helper-types.yaml"), ("enum-repr", "valid/enum-repr/enum-repr.yaml"), ("query-param-enum", "valid/query/query-param-enum.yaml"), ("server-path-prefix", "valid/server-path-prefix.yaml"), @@ -114,6 +116,8 @@ macro_rules! generate_golden_tests { } generate_golden_tests! { + test_object_extensions_golden: "object-extensions", + test_object_extension_helper_types_golden: "object-extension-helper-types", test_union_selection_golden: "union-selection", test_nested_nullable_golden: "nested-nullable", test_petstore_golden: "petstore", diff --git a/tests/java_additional_properties.rs b/tests/java_additional_properties.rs new file mode 100644 index 000000000..c85af7c95 --- /dev/null +++ b/tests/java_additional_properties.rs @@ -0,0 +1,36 @@ +use openapi_nexus::codegen::traits::code_generator::CodeGenerator; +use openapi_nexus::generators::java::okhttp::JavaOkhttpCodeGenerator; +use openapi_nexus::ir::{lower::lower, types::IrSchemaKind}; +use openapi_nexus::parser::parse_content_yaml; + +#[test] +fn explicit_additional_values_retain_ir_facts_and_get_a_flat_gson_codec() { + let source = "openapi: 3.1.0\ninfo: {title: Extensions, version: '1'}\npaths: {}\ncomponents:\n schemas:\n Pure: {type: object, additionalProperties: {type: string}}\n Mixed:\n type: object\n properties:\n known_field: {type: string}\n additionalProperties: true\n"; + for version in ["3.0.3", "3.1.0", "3.2.0"] { + let ir = lower(parse_content_yaml(&source.replacen("3.1.0", version, 1)).unwrap()).unwrap(); + for name in ["Pure", "Mixed"] { + let IrSchemaKind::Object(object) = &ir.schemas[name].kind else { + panic!("explicit additional properties retain object facts"); + }; + assert!(object.additional_properties.is_some()); + } + let files = JavaOkhttpCodeGenerator::new(Default::default()) + .generate(&ir) + .unwrap(); + for name in ["Pure.java", "Mixed.java"] { + let model = files.iter().find(|file| file.filename == name).unwrap(); + assert!(model.content.contains("getAdditionalProperties()")); + assert!(model.content.contains("private transient java.util.Map")); + assert!( + model + .content + .contains("@com.google.gson.annotations.JsonAdapter") + ); + } + assert!( + files + .iter() + .any(|file| file.filename == "AdditionalPropertiesAdapterFactory.java") + ); + } +}