[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
This commit is contained in:
Jim Schubert 2018-01-12 08:14:30 -05:00 committed by William Cheng
parent acf70d0481
commit 75c0180c71
7 changed files with 96 additions and 12 deletions

View File

@ -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

View File

@ -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");

View File

@ -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

View File

@ -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

View File

@ -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
)

View File

@ -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",

View File

@ -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
)