[scala][http4s] fix codegen for using reserved words in openapi (#21518)

This commit is contained in:
donilg 2025-07-12 19:18:55 +05:00 committed by GitHub
parent 65cb95bef0
commit 7a6be5a3e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 53 additions and 21 deletions

View File

@ -16,10 +16,14 @@
package org.openapitools.codegen.languages; package org.openapitools.codegen.languages;
import com.google.common.collect.ImmutableMap;
import com.samskivert.mustache.Escapers;
import com.samskivert.mustache.Mustache;
import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.media.Schema;
import org.openapitools.codegen.*; import org.openapitools.codegen.*;
import org.openapitools.codegen.meta.features.*; import org.openapitools.codegen.meta.features.*;
import org.openapitools.codegen.model.*; import org.openapitools.codegen.model.*;
import org.openapitools.codegen.templating.mustache.EscapeKeywordLambda;
import org.openapitools.codegen.utils.ModelUtils; import org.openapitools.codegen.utils.ModelUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -140,7 +144,7 @@ public class ScalaHttp4sServerCodegen extends DefaultCodegen implements CodegenC
additionalProperties.put("infoEmail", "team@openapitools.org"); additionalProperties.put("infoEmail", "team@openapitools.org");
additionalProperties.put("licenseInfo", "Apache 2.0"); additionalProperties.put("licenseInfo", "Apache 2.0");
additionalProperties.put("licenseUrl", "http://apache.org/licenses/LICENSE-2.0.html"); additionalProperties.put("licenseUrl", "http://apache.org/licenses/LICENSE-2.0.html");
additionalProperties.put("fnEscapeBacktick", new EscapeBacktickLambda());
languageSpecificPrimitives = new HashSet<>( languageSpecificPrimitives = new HashSet<>(
Arrays.asList( Arrays.asList(
@ -557,7 +561,12 @@ public class ScalaHttp4sServerCodegen extends DefaultCodegen implements CodegenC
@Override @Override
public String escapeReservedWord(String name) { public String escapeReservedWord(String name) {
return "_" + name; if (this.reservedWordsMappings().containsKey(name)) {
return this.reservedWordsMappings().get(name);
}
// Reserved words will be further escaped at the mustache compiler level.
// Scala escaping done here (via `, without compiler escaping) would otherwise be HTML encoded.
return "`" + name + "`";
} }
@Override @Override
@ -807,12 +816,12 @@ public class ScalaHttp4sServerCodegen extends DefaultCodegen implements CodegenC
if (_vendorExtensions.size() == 1) { // only `x-type` if (_vendorExtensions.size() == 1) { // only `x-type`
if ("String".equals(cp.getDataType())) { if ("String".equals(cp.getDataType())) {
return cp.paramName; return escapeReservedWordUnapply(cp.baseName);
} else { } else {
return cp.dataType + "Varr(" + cp.paramName + ")"; return cp.dataType + "Varr(" + escapeReservedWordUnapply(cp.baseName) + ")";
} }
} else { } else {
return cp.baseName + "Varr(" + cp.paramName + ")"; return cp.baseName + "Varr(" + escapeReservedWordUnapply(cp.baseName) + ")";
} }
} }
@ -844,11 +853,34 @@ public class ScalaHttp4sServerCodegen extends DefaultCodegen implements CodegenC
} }
vendorExtensions.putAll(refineProp(cp, imports)); vendorExtensions.putAll(refineProp(cp, imports));
return cp.baseName + "QueryParam(" + cp.paramName + ")"; return cp.baseName + "QueryParam(" + escapeReservedWordUnapply(cp.baseName) + ")";
} }
@Override @Override
public GeneratorLanguage generatorLanguage() { public GeneratorLanguage generatorLanguage() {
return GeneratorLanguage.SCALA; return GeneratorLanguage.SCALA;
} }
@Override
protected ImmutableMap.Builder<String, Mustache.Lambda> addMustacheLambdas() {
return super.addMustacheLambdas()
.put("escapeReservedWordUnapply", new EscapeKeywordLambda(this::escapeReservedWordUnapply));
}
private String escapeReservedWordUnapply(String value) {
// The unapply method doesnt allow you to work with reserved variables via backticks;
// in such cases you should use the variable via a placeholder instead.
return isReservedWord(value) ? "_" + value : value;
}
private static class EscapeBacktickLambda extends AbstractScalaCodegen.CustomLambda {
@Override
public String formatFragment(String fragment) {
if (fragment.startsWith("`") && fragment.endsWith("`")) {
String unescaped = fragment.substring(1, fragment.length() - 1);
return "`" + Escapers.HTML.escape(unescaped) + "`";
}
return Escapers.HTML.escape(fragment);
}
}
} }

View File

@ -1,21 +1,21 @@
{{#pathParams}} {{#pathParams}}
{{paramName}}: {{{vendorExtensions.x-type}}}, {{#fnEscapeBacktick}}{{{paramName}}}{{/fnEscapeBacktick}}: {{{vendorExtensions.x-type}}},
{{/pathParams}} {{/pathParams}}
{{#queryParams}} {{#queryParams}}
{{#isArray}} {{#isArray}}
{{#required}} {{#required}}
{{paramName}}: List[{{{items.vendorExtensions.x-type}}}], {{#fnEscapeBacktick}}{{{paramName}}}{{/fnEscapeBacktick}}: List[{{{items.vendorExtensions.x-type}}}],
{{/required}} {{/required}}
{{^required}} {{^required}}
{{paramName}}: Option[List[{{{items.vendorExtensions.x-type}}}]], {{#fnEscapeBacktick}}{{{paramName}}}{{/fnEscapeBacktick}}: Option[List[{{{items.vendorExtensions.x-type}}}]],
{{/required}} {{/required}}
{{/isArray}} {{/isArray}}
{{^isArray}} {{^isArray}}
{{#required}} {{#required}}
{{paramName}}: {{{vendorExtensions.x-type}}}, {{#fnEscapeBacktick}}{{{paramName}}}{{/fnEscapeBacktick}}: {{{vendorExtensions.x-type}}},
{{/required}} {{/required}}
{{^required}} {{^required}}
{{paramName}}: Option[{{{vendorExtensions.x-type}}}], {{#fnEscapeBacktick}}{{{paramName}}}{{/fnEscapeBacktick}}: Option[{{{vendorExtensions.x-type}}}],
{{/required}} {{/required}}
{{/isArray}} {{/isArray}}
{{/queryParams}} {{/queryParams}}

View File

@ -1,6 +1,6 @@
{{^authName}} {{^authName}}
delegate.{{operationId}}.handle(req, {{#pathParams}}{{paramName}}, {{/pathParams}}{{#queryParams}}{{paramName}}, {{/queryParams}}responses) delegate.{{operationId}}.handle(req, {{#pathParams}}{{#lambda.escapeReservedWordUnapply}}{{baseName}}{{/lambda.escapeReservedWordUnapply}}, {{/pathParams}}{{#queryParams}}{{#lambda.escapeReservedWordUnapply}}{{baseName}}{{/lambda.escapeReservedWordUnapply}}, {{/queryParams}}responses)
{{/authName}} {{/authName}}
{{#authName}} {{#authName}}
delegate.{{operationId}}.handle_{{authName}}(auth, req, {{#pathParams}}{{paramName}}, {{/pathParams}}{{#queryParams}}{{paramName}}, {{/queryParams}}responses) delegate.{{operationId}}.handle_{{authName}}(auth, req, {{#pathParams}}{{#lambda.escapeReservedWordUnapply}}{{baseName}}{{/lambda.escapeReservedWordUnapply}}, {{/pathParams}}{{#queryParams}}{{#lambda.escapeReservedWordUnapply}}{{baseName}}{{/lambda.escapeReservedWordUnapply}}, {{/queryParams}}responses)
{{/authName}} {{/authName}}

View File

@ -1,6 +1,6 @@
{{^authName}} {{^authName}}
delegate.{{operationId}}.handle(req, req.asJsonDecode[{{{bodyParam.dataType}}}] , {{#pathParams}}{{paramName}}, {{/pathParams}}{{#queryParams}}{{paramName}}, {{/queryParams}}responses) delegate.{{operationId}}.handle(req, req.asJsonDecode[{{{bodyParam.dataType}}}] , {{#pathParams}}{{#lambda.escapeReservedWordUnapply}}{{baseName}}{{/lambda.escapeReservedWordUnapply}}, {{/pathParams}}{{#queryParams}}{{#lambda.escapeReservedWordUnapply}}{{baseName}}{{/lambda.escapeReservedWordUnapply}}, {{/queryParams}}responses)
{{/authName}} {{/authName}}
{{#authName}} {{#authName}}
delegate.{{operationId}}.handle_{{authName}}(auth, req, req.asJsonDecode[{{{bodyParam.dataType}}}] , {{#pathParams}}{{paramName}}, {{/pathParams}}{{#queryParams}}{{paramName}}, {{/queryParams}}responses) delegate.{{operationId}}.handle_{{authName}}(auth, req, req.asJsonDecode[{{{bodyParam.dataType}}}] , {{#pathParams}}{{#lambda.escapeReservedWordUnapply}}{{baseName}}{{/lambda.escapeReservedWordUnapply}}, {{/pathParams}}{{#queryParams}}{{#lambda.escapeReservedWordUnapply}}{{baseName}}{{/lambda.escapeReservedWordUnapply}}, {{/queryParams}}responses)
{{/authName}} {{/authName}}

View File

@ -23,7 +23,7 @@ import {{modelPackage}}.{{classname}}.{{classname}}
/** /**
* {{{description}}} * {{{description}}}
{{#vars}} {{#vars}}
* @param {{name}} {{{description}}} * @param {{#fnEscapeBacktick}}{{{name}}}{{/fnEscapeBacktick}} {{{description}}}
{{/vars}} {{/vars}}
*/ */
{{#vendorExtensions.x-isSealedTrait}} {{#vendorExtensions.x-isSealedTrait}}
@ -104,7 +104,7 @@ object {{classname}} extends Enumeration {
{{#vendorExtensions.x-another}} {{#vendorExtensions.x-another}}
case class {{classname}}( case class {{classname}}(
{{#vars}} {{#vars}}
{{name}}: {{^required}}Option[{{{vendorExtensions.x-type}}}]{{/required}}{{#required}}{{{vendorExtensions.x-type}}}{{/required}}{{^-last}},{{/-last}} {{#fnEscapeBacktick}}{{{name}}}{{/fnEscapeBacktick}}: {{^required}}Option[{{{vendorExtensions.x-type}}}]{{/required}}{{#required}}{{{vendorExtensions.x-type}}}{{/required}}{{^-last}},{{/-last}}
{{/vars}} {{/vars}}
){{#vendorExtensions.x-extends}} extends {{.}}{{/vendorExtensions.x-extends}}{{#vendorExtensions.x-extendsWith}} with {{.}}{{/vendorExtensions.x-extendsWith}} ){{#vendorExtensions.x-extends}} extends {{.}}{{/vendorExtensions.x-extends}}{{#vendorExtensions.x-extendsWith}} with {{.}}{{/vendorExtensions.x-extendsWith}}
object {{classname}} { object {{classname}} {

View File

@ -57,8 +57,8 @@ trait FakeApiDelegate[F[_]] {
def handle( def handle(
req: Request[F], req: Request[F],
_type: String, `type`: String,
_var: Option[String], `var`: Option[String],
responses: reservedWordsResponses[F] responses: reservedWordsResponses[F]
): F[Response[F]] ): F[Response[F]]

View File

@ -15,13 +15,13 @@ import java.time.ZonedDateTime
/** /**
* Describes the result of uploading an image resource * Describes the result of uploading an image resource
* @param code * @param code
* @param _type * @param `type`
* @param message * @param message
*/ */
case class ApiResponse( case class ApiResponse(
code: Option[Int], code: Option[Int],
_type: Option[String], `type`: Option[String],
message: Option[String] message: Option[String]
) )
object ApiResponse { object ApiResponse {