From 75c0180c7145367cb0a7109b1f5469c8b244e45d Mon Sep 17 00:00:00 2001 From: Jim Schubert Date: Fri, 12 Jan 2018 08:14:30 -0500 Subject: [PATCH] [scala] Escape reserved words, support Array[Byte] (#7378) * [scala] Escape reserved words, support Array[Byte] Previously, Array[Byte] was compiling to ArrayByte. This provides a type mapping to output the correct type. This also escapes reserved words with grave accents, as is most common in Scala. Escaping with an underscore prefix breaks serialization (in Jackson, for example) unless templates are modified manually. Escaping using grave accent should unblock most serializers from requiring template modifications. * [scala] Regenerate integration test outputs * [scala] Regenerate samples * [scala] Remove unused imports in related codegen files --- .../languages/AbstractScalaCodegen.java | 72 ++++++++++++++++++- .../codegen/languages/ScalaClientCodegen.java | 2 +- .../languages/AbstractScalaCodegenTest.java | 3 +- .../io/swagger/client/api/HobbiesApi.scala | 19 +++-- .../scala/io/swagger/client/model/Hobby.scala | 2 +- .../client/required-attributes-spec.json | 8 +++ .../io/swagger/client/model/ApiResponse.scala | 2 +- 7 files changed, 96 insertions(+), 12 deletions(-) diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/AbstractScalaCodegen.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/AbstractScalaCodegen.java index 3ef8b2e06bb..85afe5d16ca 100644 --- a/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/AbstractScalaCodegen.java +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/AbstractScalaCodegen.java @@ -6,6 +6,8 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import com.samskivert.mustache.Escapers; +import com.samskivert.mustache.Mustache; import io.swagger.codegen.CliOption; import io.swagger.codegen.CodegenConstants; import io.swagger.codegen.DefaultCodegen; @@ -43,7 +45,50 @@ public abstract class AbstractScalaCodegen extends DefaultCodegen { "Any", "List", "Seq", - "Map")); + "Map", + "Array")); + + reservedWords.addAll(Arrays.asList( + "abstract", + "case", + "catch", + "class", + "def", + "do", + "else", + "extends", + "false", + "final", + "finally", + "for", + "forSome", + "if", + "implicit", + "import", + "lazy", + "match", + "new", + "null", + "object", + "override", + "package", + "private", + "protected", + "return", + "sealed", + "super", + "this", + "throw", + "trait", + "try", + "true", + "type", + "val", + "var", + "while", + "with", + "yield" + )); cliOptions.add(new CliOption(CodegenConstants.MODEL_PACKAGE, CodegenConstants.MODEL_PACKAGE_DESC)); cliOptions.add(new CliOption(CodegenConstants.API_PACKAGE, CodegenConstants.API_PACKAGE_DESC)); @@ -72,7 +117,30 @@ public abstract class AbstractScalaCodegen extends DefaultCodegen { if (this.reservedWordsMappings().containsKey(name)) { return this.reservedWordsMappings().get(name); } - return "_" + 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 + public Mustache.Compiler processCompiler(Mustache.Compiler compiler) { + Mustache.Escaper SCALA = new Mustache.Escaper() { + @Override public String escape (String text) { + // Fix included as suggested by akkie in #6393 + // The given text is a reserved word which is escaped by enclosing it with grave accents. If we would + // escape that with the default Mustache `HTML` escaper, then the escaper would also escape our grave + // accents. So we remove the grave accents before the escaping and add it back after the escaping. + if (text.startsWith("`") && text.endsWith("`")) { + String unescaped = text.substring(1, text.length() - 1); + return "`" + Escapers.HTML.escape(unescaped) + "`"; + } + + // All none reserved words will be escaped with the default Mustache `HTML` escaper + return Escapers.HTML.escape(text); + } + }; + + return compiler.withEscaper(SCALA); } @Override diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/ScalaClientCodegen.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/ScalaClientCodegen.java index 8316e5563ba..d84e7bddae3 100644 --- a/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/ScalaClientCodegen.java +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/ScalaClientCodegen.java @@ -5,7 +5,6 @@ import io.swagger.codegen.*; import java.io.File; import java.util.Arrays; import java.util.HashMap; -import java.util.Map; import org.apache.commons.lang3.StringUtils; @@ -96,6 +95,7 @@ public class ScalaClientCodegen extends AbstractScalaCodegen implements CodegenC typeMapping.put("file", "File"); typeMapping.put("binary", "Array[Byte]"); typeMapping.put("ByteArray", "Array[Byte]"); + typeMapping.put("ArrayByte", "Array[Byte]"); typeMapping.put("date-time", "Date"); typeMapping.put("DateTime", "Date"); diff --git a/modules/swagger-codegen/src/test/java/io/swagger/codegen/languages/AbstractScalaCodegenTest.java b/modules/swagger-codegen/src/test/java/io/swagger/codegen/languages/AbstractScalaCodegenTest.java index 415f346c1d4..e29b9fd09c2 100644 --- a/modules/swagger-codegen/src/test/java/io/swagger/codegen/languages/AbstractScalaCodegenTest.java +++ b/modules/swagger-codegen/src/test/java/io/swagger/codegen/languages/AbstractScalaCodegenTest.java @@ -37,7 +37,8 @@ public class AbstractScalaCodegenTest { String result = abstractScalaCodegen.formatIdentifier(className, true); - Assert.assertTrue("_ReservedWord".equals(result)); + // NOTE: reserved words are further escaped at the compiler level. + Assert.assertTrue("`ReservedWord`".equals(result)); } @Test diff --git a/modules/swagger-codegen/src/test/resources/integrationtests/scala/client/required-attributes-expected/src/main/scala/io/swagger/client/api/HobbiesApi.scala b/modules/swagger-codegen/src/test/resources/integrationtests/scala/client/required-attributes-expected/src/main/scala/io/swagger/client/api/HobbiesApi.scala index 325b0069bbb..3f8ca7c4880 100644 --- a/modules/swagger-codegen/src/test/resources/integrationtests/scala/client/required-attributes-expected/src/main/scala/io/swagger/client/api/HobbiesApi.scala +++ b/modules/swagger-codegen/src/test/resources/integrationtests/scala/client/required-attributes-expected/src/main/scala/io/swagger/client/api/HobbiesApi.scala @@ -86,6 +86,7 @@ class HobbiesApi( * Query hobbies with some additional optional meaningless parameters * * @param s a string (optional, default to some string) + * @param `class` a string, testing keyword escaping (optional, default to some string) * @param i an integer (optional, default to 1) * @param l a long (optional, default to 2) * @param bool a bool (optional, default to true) @@ -97,8 +98,8 @@ class HobbiesApi( * @param bin an octet string (optional, default to DEADBEEF) * @return Hobby */ - def getHobbies(s: Option[String] = Option("some string"), i: Option[Integer] = Option(1), l: Option[Long] = Option(2), bool: Option[Boolean] = Option(true), f: Option[Float] = Option(0.1), d: Option[Double] = Option(10.005), datetime: Option[Date] = Option(dateTimeFormatter.parse("2018-01-01T08:30:00Z-04:00")), date: Option[Date] = Option(dateFormatter.parse("2018-01-01")), b: Option[ArrayByte] = Option("c3dhZ2dlciBjb2RlZ2Vu".getBytes), bin: Option[ArrayByte] = Option("DEADBEEF".getBytes)): Option[Hobby] = { - val await = Try(Await.result(getHobbiesAsync(s, i, l, bool, f, d, datetime, date, b, bin), Duration.Inf)) + def getHobbies(s: Option[String] = Option("some string"), `class`: Option[String] = Option("some string"), i: Option[Integer] = Option(1), l: Option[Long] = Option(2), bool: Option[Boolean] = Option(true), f: Option[Float] = Option(0.1), d: Option[Double] = Option(10.005), datetime: Option[Date] = Option(dateTimeFormatter.parse("2018-01-01T08:30:00Z-04:00")), date: Option[Date] = Option(dateFormatter.parse("2018-01-01")), b: Option[Array[Byte]] = Option("c3dhZ2dlciBjb2RlZ2Vu".getBytes), bin: Option[Array[Byte]] = Option("DEADBEEF".getBytes)): Option[Hobby] = { + val await = Try(Await.result(getHobbiesAsync(s, `class`, i, l, bool, f, d, datetime, date, b, bin), Duration.Inf)) await match { case Success(i) => Some(await.get) case Failure(t) => None @@ -110,6 +111,7 @@ class HobbiesApi( * Query hobbies with some additional optional meaningless parameters * * @param s a string (optional, default to some string) + * @param `class` a string, testing keyword escaping (optional, default to some string) * @param i an integer (optional, default to 1) * @param l a long (optional, default to 2) * @param bool a bool (optional, default to true) @@ -121,8 +123,8 @@ class HobbiesApi( * @param bin an octet string (optional, default to DEADBEEF) * @return Future(Hobby) */ - def getHobbiesAsync(s: Option[String] = Option("some string"), i: Option[Integer] = Option(1), l: Option[Long] = Option(2), bool: Option[Boolean] = Option(true), f: Option[Float] = Option(0.1), d: Option[Double] = Option(10.005), datetime: Option[Date] = Option(dateTimeFormatter.parse("2018-01-01T08:30:00Z-04:00")), date: Option[Date] = Option(dateFormatter.parse("2018-01-01")), b: Option[ArrayByte] = Option("c3dhZ2dlciBjb2RlZ2Vu".getBytes), bin: Option[ArrayByte] = Option("DEADBEEF".getBytes)): Future[Hobby] = { - helper.getHobbies(s, i, l, bool, f, d, datetime, date, b, bin) + def getHobbiesAsync(s: Option[String] = Option("some string"), `class`: Option[String] = Option("some string"), i: Option[Integer] = Option(1), l: Option[Long] = Option(2), bool: Option[Boolean] = Option(true), f: Option[Float] = Option(0.1), d: Option[Double] = Option(10.005), datetime: Option[Date] = Option(dateTimeFormatter.parse("2018-01-01T08:30:00Z-04:00")), date: Option[Date] = Option(dateFormatter.parse("2018-01-01")), b: Option[Array[Byte]] = Option("c3dhZ2dlciBjb2RlZ2Vu".getBytes), bin: Option[Array[Byte]] = Option("DEADBEEF".getBytes)): Future[Hobby] = { + helper.getHobbies(s, `class`, i, l, bool, f, d, datetime, date, b, bin) } } @@ -130,6 +132,7 @@ class HobbiesApi( class HobbiesApiAsyncHelper(client: TransportClient, config: SwaggerConfig) extends ApiClient(client, config) { def getHobbies(s: Option[String] = Option("some string"), + `class`: Option[String] = Option("some string"), i: Option[Integer] = Option(1), l: Option[Long] = Option(2), bool: Option[Boolean] = Option(true), @@ -137,8 +140,8 @@ class HobbiesApiAsyncHelper(client: TransportClient, config: SwaggerConfig) exte d: Option[Double] = Option(10.005), datetime: Option[Date] = Option(dateTimeFormatter.parse("2018-01-01T08:30:00Z-04:00")), date: Option[Date] = Option(dateFormatter.parse("2018-01-01")), - b: Option[ArrayByte] = Option("c3dhZ2dlciBjb2RlZ2Vu".getBytes), - bin: Option[ArrayByte] = Option("DEADBEEF".getBytes) + b: Option[Array[Byte]] = Option("c3dhZ2dlciBjb2RlZ2Vu".getBytes), + bin: Option[Array[Byte]] = Option("DEADBEEF".getBytes) )(implicit reader: ClientResponseReader[Hobby]): Future[Hobby] = { // create path and map variables val path = (addFmt("/hobbies")) @@ -151,6 +154,10 @@ class HobbiesApiAsyncHelper(client: TransportClient, config: SwaggerConfig) exte case Some(param) => queryParams += "s" -> param.toString case _ => queryParams } + `class` match { + case Some(param) => queryParams += "class" -> param.toString + case _ => queryParams + } i match { case Some(param) => queryParams += "i" -> param.toString case _ => queryParams diff --git a/modules/swagger-codegen/src/test/resources/integrationtests/scala/client/required-attributes-expected/src/main/scala/io/swagger/client/model/Hobby.scala b/modules/swagger-codegen/src/test/resources/integrationtests/scala/client/required-attributes-expected/src/main/scala/io/swagger/client/model/Hobby.scala index 30782d0119f..02f64681fa4 100644 --- a/modules/swagger-codegen/src/test/resources/integrationtests/scala/client/required-attributes-expected/src/main/scala/io/swagger/client/model/Hobby.scala +++ b/modules/swagger-codegen/src/test/resources/integrationtests/scala/client/required-attributes-expected/src/main/scala/io/swagger/client/model/Hobby.scala @@ -23,7 +23,7 @@ case class Hobby ( enabled: Option[Boolean] = None, created: Option[Date] = None, timestamp: Option[Date] = None, - bytes: Option[ArrayByte] = None, + bytes: Option[Array[Byte]] = None, binary: Option[String] = None ) diff --git a/modules/swagger-codegen/src/test/resources/integrationtests/scala/client/required-attributes-spec.json b/modules/swagger-codegen/src/test/resources/integrationtests/scala/client/required-attributes-spec.json index 00829eb2655..db23005df4a 100644 --- a/modules/swagger-codegen/src/test/resources/integrationtests/scala/client/required-attributes-spec.json +++ b/modules/swagger-codegen/src/test/resources/integrationtests/scala/client/required-attributes-spec.json @@ -167,6 +167,14 @@ "in": "query", "default": "some string" }, + { + "type": "string", + "description": "a string, testing keyword escaping", + "name": "class", + "required": false, + "in": "query", + "default": "some string" + }, { "type": "integer", "format": "int32", diff --git a/samples/client/petstore/scala/src/main/scala/io/swagger/client/model/ApiResponse.scala b/samples/client/petstore/scala/src/main/scala/io/swagger/client/model/ApiResponse.scala index 220a5e48d9e..2717e1bcb72 100644 --- a/samples/client/petstore/scala/src/main/scala/io/swagger/client/model/ApiResponse.scala +++ b/samples/client/petstore/scala/src/main/scala/io/swagger/client/model/ApiResponse.scala @@ -15,7 +15,7 @@ package io.swagger.client.model case class ApiResponse ( code: Option[Integer] = None, - _type: Option[String] = None, + `type`: Option[String] = None, message: Option[String] = None )