Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[kotlin] better oneOf, anyOf support #18382

Merged
merged 17 commits into from
May 31, 2024
1 change: 1 addition & 0 deletions bin/configs/kotlin-model-prefix-type-mapping.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ additionalProperties:
library: jvm-retrofit2
enumPropertyNaming: UPPERCASE
serializationLibrary: gson
enableOneOfWrapper: true
openapiNormalizer:
SIMPLIFY_ONEOF_ANYOF: false
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.samskivert.mustache.Mustache;
import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.CliOption;
import org.openapitools.codegen.CodegenConstants;
Expand Down Expand Up @@ -513,6 +514,14 @@ public void processOpts() {
supportingFiles.add(new SupportingFile("auth/HttpBasicAuth.kt.mustache", authFolder, "HttpBasicAuth.kt"));
}
}

additionalProperties.put("sanitizeGeneric", (Mustache.Lambda) (fragment, writer) -> {
String content = fragment.execute();
for (final String s: List.of("<", ">", ",", " ")) {
content = content.replace(s, "");
}
writer.write(content);
});
}

private void processDateLibrary() {
Expand Down Expand Up @@ -869,10 +878,11 @@ public ModelsMap postProcessModels(ModelsMap objs) {

for (ModelMap mo : objects.getModels()) {
CodegenModel cm = mo.getModel();
if (getGenerateRoomModels()) {
if (getGenerateRoomModels() || Boolean.parseBoolean(String.valueOf(additionalProperties.get("enableOneOfWrapper")))) {
cm.vendorExtensions.put("x-has-data-class-body", true);
}


// escape the variable base name for use as a string literal
List<CodegenProperty> vars = Stream.of(
cm.vars,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ This runs all tests and packages the library.

All URIs are relative to *{{{basePath}}}*

Class | Method | HTTP request | Description
------------ | ------------- | ------------- | -------------
{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}*{{classname}}* | [**{{operationId}}**]({{apiDocPath}}{{classname}}.md#{{operationIdLowerCase}}) | **{{httpMethod}}** {{path}} | {{{summary}}}
| Class | Method | HTTP request | Description |
| ------------ | ------------- | ------------- | ------------- |
{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}| *{{classname}}* | [**{{operationId}}**]({{apiDocPath}}{{classname}}.md#{{operationIdLowerCase}}) | **{{httpMethod}}** {{path}} | {{{summary}}} |
{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}}
{{/generateApiDocs}}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

All URIs are relative to *{{basePath}}*

Method | HTTP request | Description
------------- | ------------- | -------------
{{#operations}}{{#operation}}[**{{operationId}}**]({{classname}}.md#{{operationId}}) | **{{httpMethod}}** {{path}} | {{summary}}
| Method | HTTP request | Description |
| ------------- | ------------- | ------------- |
{{#operations}}{{#operation}}| [**{{operationId}}**]({{classname}}.md#{{operationId}}) | **{{httpMethod}}** {{path}} | {{summary}} |
{{/operation}}{{/operations}}

{{#operations}}
Expand Down Expand Up @@ -42,10 +42,15 @@ try {
```

### Parameters
{{^allParams}}This endpoint does not need any parameter.{{/allParams}}{{#allParams}}{{#-last}}
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------{{/-last}}{{/allParams}}
{{#allParams}} **{{paramName}}** | {{#isPrimitiveType}}**{{dataType}}**{{/isPrimitiveType}}{{^isPrimitiveType}}{{#isFile}}**{{dataType}}**{{/isFile}}{{^isFile}}{{#generateModelDocs}}[**{{dataType}}**]({{baseType}}.md){{/generateModelDocs}}{{^generateModelDocs}}**{{dataType}}**{{/generateModelDocs}}{{/isFile}}{{/isPrimitiveType}}| {{description}} |{{^required}} [optional]{{/required}}{{#defaultValue}} [default to {{.}}]{{/defaultValue}}{{#allowableValues}} [enum: {{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}]{{/allowableValues}}
{{^allParams}}
This endpoint does not need any parameter.
{{/allParams}}
{{#allParams}}
{{#-last}}
| Name | Type | Description | Notes |
| ------------- | ------------- | ------------- | ------------- |
{{/-last}}
| **{{paramName}}** | {{#isPrimitiveType}}**{{dataType}}**{{/isPrimitiveType}}{{^isPrimitiveType}}{{#isFile}}**{{dataType}}**{{/isFile}}{{^isFile}}{{#generateModelDocs}}[**{{dataType}}**]({{baseType}}.md){{/generateModelDocs}}{{^generateModelDocs}}**{{dataType}}**{{/generateModelDocs}}{{/isFile}}{{/isPrimitiveType}}| {{description}} |{{^required}} [optional]{{/required}}{{#defaultValue}} [default to {{.}}]{{/defaultValue}}{{#allowableValues}} [enum: {{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}]{{/allowableValues}} |
{{/allParams}}

### Return type
Expand Down Expand Up @@ -84,4 +89,4 @@ Configure {{name}}:
- **Accept**: {{#produces}}{{{mediaType}}}{{^-last}}, {{/-last}}{{/produces}}{{^produces}}Not defined{{/produces}}

{{/operation}}
{{/operations}}
{{/operations}}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ version '{{artifactVersion}}'
{{^omitGradleWrapper}}

wrapper {
gradleVersion = '7.5'
gradleVersion = '7.6.4'
distributionUrl = "https://services.gradle.org/distributions/gradle-$gradleVersion-all.zip"
}
{{/omitGradleWrapper}}

buildscript {
ext.kotlin_version = '1.8.10'
ext.kotlin_version = '1.9.10'
{{#jvm-ktor}}
ext.ktor_version = '2.3.9'
{{/jvm-ktor}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# {{classname}}

## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
{{#vars}}**{{name}}** | {{#isEnum}}[**inline**](#{{datatypeWithEnum}}){{/isEnum}}{{^isEnum}}{{#isPrimitiveType}}**{{dataType}}**{{/isPrimitiveType}}{{^isPrimitiveType}}[**{{dataType}}**]({{complexType}}.md){{/isPrimitiveType}}{{/isEnum}} | {{description}} | {{^required}} [optional]{{/required}}{{#isReadOnly}} [readonly]{{/isReadOnly}}
| Name | Type | Description | Notes |
| ------------ | ------------- | ------------- | ------------- |
{{#vars}}| **{{name}}** | {{#isEnum}}[**inline**](#{{datatypeWithEnum}}){{/isEnum}}{{^isEnum}}{{#isPrimitiveType}}**{{dataType}}**{{/isPrimitiveType}}{{^isPrimitiveType}}[**{{dataType}}**]({{complexType}}.md){{/isPrimitiveType}}{{/isEnum}} | {{description}} | {{^required}} [optional]{{/required}}{{#isReadOnly}} [readonly]{{/isReadOnly}} |
{{/vars}}
{{#vars}}{{#isEnum}}

<a id="{{{datatypeWithEnum}}}"></a>{{!NOTE: see java's resources "pojo_doc.mustache" once enums are fully implemented}}
## Enum: {{baseName}}
Name | Value
---- | -----{{#allowableValues}}
{{name}} | {{#values}}{{.}}{{^-last}}, {{/-last}}{{/values}}{{/allowableValues}}
| Name | Value |
| ---- | ----- |{{#allowableValues}}
| {{name}} | {{#values}}{{.}}{{^-last}}, {{/-last}}{{/values}}{{/allowableValues}} |
{{/isEnum}}{{/vars}}
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
{{^multiplatform}}
{{#gson}}
{{#enableOneOfWrapper}}
import com.google.gson.Gson
import com.google.gson.JsonElement
import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import com.google.gson.annotations.JsonAdapter
{{/enableOneOfWrapper}}
import com.google.gson.annotations.SerializedName
{{/gson}}
{{#moshi}}
Expand Down Expand Up @@ -47,6 +57,8 @@ import java.io.Serializable
import {{roomModelPackage}}.{{classname}}RoomModel
import {{packageName}}.infrastructure.ITransformForStorage
{{/generateRoomModels}}
import java.io.IOException


/**
* {{{description}}}
Expand Down Expand Up @@ -128,7 +140,9 @@ import {{packageName}}.infrastructure.ITransformForStorage
{{/multiplatform}}
{{/enumVars}}
{{/allowableValues}}
}{{#kotlinx_serialization}}{{#enumUnknownDefaultCase}}
}
{{#kotlinx_serialization}}
{{#enumUnknownDefaultCase}}

@Serializer(forClass = {{{nameInPascalCase}}}::class)
internal object {{nameInPascalCase}}Serializer : KSerializer<{{nameInPascalCase}}> {
Expand All @@ -143,10 +157,217 @@ import {{packageName}}.infrastructure.ITransformForStorage
override fun serialize(encoder: Encoder, value: {{nameInPascalCase}}) {
encoder.encodeSerializableValue({{{dataType}}}.serializer(), value.value)
}
}{{/enumUnknownDefaultCase}}{{/kotlinx_serialization}}
}
{{/enumUnknownDefaultCase}}
{{/kotlinx_serialization}}
{{/isEnum}}
{{/vars}}
{{/hasEnums}}
{{#enableOneOfWrapper}}

class CustomTypeAdapterFactory : TypeAdapterFactory {
override fun <T> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (!{{classname}}::class.java.isAssignableFrom(type.rawType)) {
return null // this class only serializes '{{classname}}' and its subtypes
}
val elementAdapter = gson.getAdapter(JsonElement::class.java)
val thisAdapter = gson.getDelegateAdapter(this, TypeToken.get({{classname}}::class.java))

@Suppress("UNCHECKED_CAST")
return object : TypeAdapter<{{classname}}>() {
@Throws(IOException::class)
override fun write(out: JsonWriter, value: {{classname}}) {
val obj = thisAdapter.toJsonTree(value).getAsJsonObject()
elementAdapter.write(out, obj)
}

@Throws(IOException::class)
override fun read(jsonReader: JsonReader): {{classname}} {
val jsonElement = elementAdapter.read(jsonReader)
validateJsonElement(jsonElement)
return thisAdapter.fromJsonTree(jsonElement)
}
}.nullSafe() as TypeAdapter<T>
}
}

companion object {
var openapiFields: HashSet<String>? = null
var openapiRequiredFields: HashSet<String>? = null

init {
// a set of all properties/fields (JSON key names)
openapiFields = HashSet()
{{#allVars}}
openapiFields!!.add("{{baseName}}")
{{/allVars}}

// a set of required properties/fields (JSON key names)
openapiRequiredFields = HashSet()
{{#requiredVars}}
openapiRequiredFields!!.add("{{baseName}}")
{{/requiredVars}}
}

/**
* Validates the JSON Element and throws an exception if issues found
*
* @param jsonElement JSON Element
* @throws IOException if the JSON Element is invalid with respect to {{classname}}
*/
@Throws(IOException::class)
fun validateJsonElement(jsonElement: JsonElement?) {
if (jsonElement == null) {
require(openapiRequiredFields!!.isEmpty()) { // has required fields but JSON element is null
String.format("The required field(s) %s in {{{classname}}} is not found in the empty JSON string", {{classname}}.openapiRequiredFields.toString())
}
}
{{^hasChildren}}
{{^isAdditionalPropertiesTrue}}

// TODO
//val entries = jsonElement!!.getAsJsonObject().entrySet()
// check to see if the JSON string contains additional fields
//for ((key) in entries) {
// require(openapiFields!!.contains(key)) {
// String.format("The field `%s` in the JSON string is not defined in the `{{classname}}` properties. JSON: %s", key, jsonElement.toString())
// }
//}
{{/isAdditionalPropertiesTrue}}
{{#requiredVars}}
{{#-first}}

// check to make sure all required properties/fields are present in the JSON string
for (requiredField in openapiRequiredFields!!) {
requireNotNull(jsonElement!!.getAsJsonObject()[requiredField]) {
String.format("The required field `%s` is not found in the JSON string: %s", requiredField, jsonElement.toString())
}
}
{{/-first}}
{{/requiredVars}}
{{/hasChildren}}
{{^discriminator}}
{{#hasVars}}
val jsonObj = jsonElement!!.getAsJsonObject()
{{/hasVars}}
{{#vars}}
{{#isArray}}
{{#items.isModel}}
{{#required}}
// ensure the json data is an array
if (!jsonObj.get("{{{baseName}}}").isJsonArray) {
throw new IllegalArgumentException(String.format("Expected the field `{{{baseName}}}` to be an array in the JSON string but got `%s`", jsonObj["{{{baseName}}}"].toString()))
}

JsonArray jsonArray{{name}} = jsonObj.getAsJsonArray("{{{baseName}}}")
// validate the required field `{{{baseName}}}` (array)
for (i in 0 until jsonArray{{name}}.size()) {
{{{items.dataType}}}.validateJsonElement(jsonArray{{name}}.get(i))
}
{{/required}}
{{^required}}
if (jsonObj["{{{baseName}}}"] != null && !jsonObj["{{{baseName}}}"].isJsonNull) {
val jsonArray{{name}} = jsonObj.getAsJsonArray("{{{baseName}}}")
if (jsonArray{{name}} != null) {
// ensure the json data is an array
require(jsonObj["{{{baseName}}}"].isJsonArray) {
String.format("Expected the field `{{{baseName}}}` to be an array in the JSON string but got `%s`", jsonObj["{{{baseName}}}"].toString())
}

// validate the optional field `{{{baseName}}}` (array)
for (i in 0 until jsonArray{{name}}.size()) {
{{{items.dataType}}}.validateJsonElement(jsonArray{{name}}[i])
}
}
}
{{/required}}
{{/items.isModel}}
{{^items.isModel}}
{{^required}}
// ensure the optional json data is an array if present
if (jsonObj["{{{baseName}}}"] != null && !jsonObj["{{{baseName}}}"].isJsonNull) {
require(jsonObj["{{{baseName}}}"].isJsonArray()) {
String.format("Expected the field `{{{baseName}}}` to be an array in the JSON string but got `%s`", jsonObj["{{{baseName}}}"].toString())
}
}
{{/required}}
{{#required}}
// ensure the required json array is present
requireNotNull(jsonObj["{{{baseName}}}"]) {
"Expected the field `{{{baseName}}}` to be an array in the JSON string but got `null`"
}
require(jsonObj["{{{baseName}}}"].isJsonArray()) {
String.format("Expected the field `{{{baseName}}}` to be an array in the JSON string but got `%s`", jsonObj["{{{baseName}}}"].toString())
}
{{/required}}
{{/items.isModel}}
{{/isArray}}
{{^isContainer}}
{{#isString}}
{{#notRequiredOrIsNullable}}
if (jsonObj["{{{baseName}}}"] != null && !jsonObj["{{{baseName}}}"].isJsonNull) {
require(jsonObj.get("{{{baseName}}}").isJsonPrimitive) {
String.format("Expected the field `{{{baseName}}}` to be a primitive type in the JSON string but got `%s`", jsonObj["{{{baseName}}}"].toString())
}
}
{{/notRequiredOrIsNullable}}
{{^notRequiredOrIsNullable}}
require(jsonObj["{{{baseName}}}"].isJsonPrimitive) {
String.format("Expected the field `{{{baseName}}}` to be a primitive type in the JSON string but got `%s`", jsonObj["{{{baseName}}}"].toString())
}
{{/notRequiredOrIsNullable}}
{{/isString}}
{{#isModel}}
{{#required}}
// validate the required field `{{{baseName}}}`
{{{dataType}}}.validateJsonElement(jsonObj["{{{baseName}}}"])
{{/required}}
{{^required}}
// validate the optional field `{{{baseName}}}`
if (jsonObj["{{{baseName}}}"] != null && !jsonObj["{{{baseName}}}"].isJsonNull) {
{{{dataType}}}.validateJsonElement(jsonObj["{{{baseName}}}"])
}
{{/required}}
{{/isModel}}
{{#isEnum}}
{{#required}}
// validate the required field `{{{baseName}}}`
require({{{datatypeWithEnum}}}.values().any { it.value == jsonObj["{{{baseName}}}"].asString }) {
String.format("Expected the field `{{{baseName}}}` to be valid `{{{datatypeWithEnum}}}` enum value in the JSON string but got `%s`", jsonObj["{{{baseName}}}"].toString())
}
{{/required}}
{{^required}}
// validate the optional field `{{{baseName}}}`
if (jsonObj["{{{baseName}}}"] != null && !jsonObj["{{{baseName}}}"].isJsonNull) {
require({{{datatypeWithEnum}}}.values().any { it.value == jsonObj["{{{baseName}}}"].asString }) {
String.format("Expected the field `{{{baseName}}}` to be valid `{{{datatypeWithEnum}}}` enum value in the JSON string but got `%s`", jsonObj["{{{baseName}}}"].toString())
}
}
{{/required}}
{{/isEnum}}
{{#isEnumRef}}
{{#required}}
// validate the required field `{{{baseName}}}`
require({{{dataType}}}.values().any { it.value == jsonObj["{{{baseName}}}"].asString }) {
String.format("Expected the field `{{{baseName}}}` to be valid `{{{dataType}}}` enum value in the JSON string but got `%s`", jsonObj["{{{baseName}}}"].toString())
}
{{/required}}
{{^required}}
// validate the optional field `{{{baseName}}}`
if (jsonObj["{{{baseName}}}"] != null && !jsonObj["{{{baseName}}}"].isJsonNull) {
require({{{dataType}}}.values().any { it.value == jsonObj["{{{baseName}}}"].asString }) {
String.format("Expected the field `{{{baseName}}}` to be valid `{{{dataType}}}` enum value in the JSON string but got `%s`", jsonObj["{{{baseName}}}"].toString())
}
}
{{/required}}
{{/isEnumRef}}
{{/isContainer}}
{{/vars}}
{{/discriminator}}
}
}
{{/enableOneOfWrapper}}

{{#vendorExtensions.x-has-data-class-body}}
}
{{/vendorExtensions.x-has-data-class-body}}
Loading
Loading