[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;
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 org.openapitools.codegen.*;
import org.openapitools.codegen.meta.features.*;
import org.openapitools.codegen.model.*;
import org.openapitools.codegen.templating.mustache.EscapeKeywordLambda;
import org.openapitools.codegen.utils.ModelUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -140,7 +144,7 @@ public class ScalaHttp4sServerCodegen extends DefaultCodegen implements CodegenC
additionalProperties.put("infoEmail", "team@openapitools.org");
additionalProperties.put("licenseInfo", "Apache 2.0");
additionalProperties.put("licenseUrl", "http://apache.org/licenses/LICENSE-2.0.html");
additionalProperties.put("fnEscapeBacktick", new EscapeBacktickLambda());
languageSpecificPrimitives = new HashSet<>(
Arrays.asList(
@ -557,7 +561,12 @@ public class ScalaHttp4sServerCodegen extends DefaultCodegen implements CodegenC
@Override
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
@ -807,12 +816,12 @@ public class ScalaHttp4sServerCodegen extends DefaultCodegen implements CodegenC
if (_vendorExtensions.size() == 1) { // only `x-type`
if ("String".equals(cp.getDataType())) {
return cp.paramName;
return escapeReservedWordUnapply(cp.baseName);
} else {
return cp.dataType + "Varr(" + cp.paramName + ")";
return cp.dataType + "Varr(" + escapeReservedWordUnapply(cp.baseName) + ")";
}
} 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));
return cp.baseName + "QueryParam(" + cp.paramName + ")";
return cp.baseName + "QueryParam(" + escapeReservedWordUnapply(cp.baseName) + ")";
}
@Override
public GeneratorLanguage generatorLanguage() {
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}}
{{paramName}}: {{{vendorExtensions.x-type}}},
{{#fnEscapeBacktick}}{{{paramName}}}{{/fnEscapeBacktick}}: {{{vendorExtensions.x-type}}},
{{/pathParams}}
{{#queryParams}}
{{#isArray}}
{{#required}}
{{paramName}}: List[{{{items.vendorExtensions.x-type}}}],
{{#fnEscapeBacktick}}{{{paramName}}}{{/fnEscapeBacktick}}: List[{{{items.vendorExtensions.x-type}}}],
{{/required}}
{{^required}}
{{paramName}}: Option[List[{{{items.vendorExtensions.x-type}}}]],
{{#fnEscapeBacktick}}{{{paramName}}}{{/fnEscapeBacktick}}: Option[List[{{{items.vendorExtensions.x-type}}}]],
{{/required}}
{{/isArray}}
{{^isArray}}
{{#required}}
{{paramName}}: {{{vendorExtensions.x-type}}},
{{#fnEscapeBacktick}}{{{paramName}}}{{/fnEscapeBacktick}}: {{{vendorExtensions.x-type}}},
{{/required}}
{{^required}}
{{paramName}}: Option[{{{vendorExtensions.x-type}}}],
{{#fnEscapeBacktick}}{{{paramName}}}{{/fnEscapeBacktick}}: Option[{{{vendorExtensions.x-type}}}],
{{/required}}
{{/isArray}}
{{/queryParams}}

View File

@ -1,6 +1,6 @@
{{^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}}
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}}

View File

@ -1,6 +1,6 @@
{{^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}}
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}}

View File

@ -23,7 +23,7 @@ import {{modelPackage}}.{{classname}}.{{classname}}
/**
* {{{description}}}
{{#vars}}
* @param {{name}} {{{description}}}
* @param {{#fnEscapeBacktick}}{{{name}}}{{/fnEscapeBacktick}} {{{description}}}
{{/vars}}
*/
{{#vendorExtensions.x-isSealedTrait}}
@ -104,7 +104,7 @@ object {{classname}} extends Enumeration {
{{#vendorExtensions.x-another}}
case class {{classname}}(
{{#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}}
){{#vendorExtensions.x-extends}} extends {{.}}{{/vendorExtensions.x-extends}}{{#vendorExtensions.x-extendsWith}} with {{.}}{{/vendorExtensions.x-extendsWith}}
object {{classname}} {

View File

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

View File

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