Skip to content

Commit 1a630aa

Browse files
authored
Merge pull request #1 from lbialy/scala-sttp4-jsoniter-wip
Scala sttp4 jsoniter wip
2 parents ab448bb + 0a9c2a8 commit 1a630aa

13 files changed

Lines changed: 844 additions & 170 deletions

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractScalaCodegen.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,16 @@ protected String formatIdentifier(String name, boolean capitalized) {
530530
if (identifier.matches("[a-zA-Z_$][\\w_$]+") && !isReservedWord(identifier)) {
531531
return identifier;
532532
}
533-
return escapeReservedWord(identifier);
533+
if (identifier.matches("[0-9]*")) {
534+
return escapeReservedWord(identifier);
535+
}
536+
if (!capitalized || StringUtils.isNumeric(name)) {
537+
// starts with a small letter, could be a keyword or a number
538+
return escapeReservedWord(identifier);
539+
} else {
540+
// no keywords start with large letter
541+
return identifier;
542+
}
534543
}
535544

536545
protected String stripPackageName(String input) {

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java

Lines changed: 83 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import io.swagger.v3.oas.models.media.Schema;
55
import io.swagger.v3.oas.models.security.SecurityScheme;
66
import io.swagger.v3.oas.models.servers.Server;
7-
import lombok.Getter;
87
import org.apache.commons.lang3.StringUtils;
98
import org.openapitools.codegen.*;
109
import org.openapitools.codegen.meta.GeneratorMetadata;
@@ -18,18 +17,18 @@
1817
import org.slf4j.Logger;
1918
import org.slf4j.LoggerFactory;
2019

20+
import static org.openapitools.codegen.languages.AbstractJavaCodegen.DATE_LIBRARY;
21+
2122
import java.io.File;
2223
import java.util.*;
2324
import java.util.regex.Matcher;
2425
import java.util.regex.Pattern;
2526

26-
import static org.openapitools.codegen.utils.StringUtils.camelize;
27-
2827
public class ScalaSttp4JsoniterClientCodegen extends AbstractScalaCodegen implements CodegenConfig {
2928
private static final StringProperty STTP_CLIENT_VERSION = new StringProperty("sttpClientVersion",
3029
"The version of " +
3130
"sttp client",
32-
"4.0.0-M19");
31+
"4.0.0-RC1");
3332
private static final BooleanProperty USE_SEPARATE_ERROR_CHANNEL = new BooleanProperty("separateErrorChannel",
3433
"Whether to return response as " +
3534
"F[Either[ResponseError[ErrorType], ReturnType]]] or to flatten " +
@@ -46,7 +45,13 @@ public class ScalaSttp4JsoniterClientCodegen extends AbstractScalaCodegen implem
4645
private static final List<Property<?>> properties = Arrays.asList(
4746
STTP_CLIENT_VERSION, USE_SEPARATE_ERROR_CHANNEL, JSONITER_VERSION, PACKAGE_PROPERTY);
4847

49-
private static final Set<String> NO_JSON_CODEC_TYPES = new HashSet<>(Arrays.asList("UUID", "URI", "URL", "File", "Path"));
48+
private static final String jsonClassBaseName = "Json";
49+
private static final String jsonValueClass = "io.circe.Json";
50+
private static final String jsonAstCodecImport = "com.github.plokhotnyuk.jsoniter_scala.circe.JsoniterScalaCodec.*";
51+
52+
private static final Set<String> NO_JSON_CODEC_TYPES = new HashSet<>(Arrays.asList(
53+
"UUID", "URI", "URL", "File", "Path", jsonClassBaseName, jsonValueClass, "BigDecimal"
54+
));
5055

5156
private final Logger LOGGER = LoggerFactory.getLogger(ScalaSttp4JsoniterClientCodegen.class);
5257

@@ -61,8 +66,8 @@ public class ScalaSttp4JsoniterClientCodegen extends AbstractScalaCodegen implem
6166

6267
Map<String, ModelsMap> enumRefs = new HashMap<>();
6368

64-
private Map<String, String> apiNameMappings = new HashMap<>();
65-
private Set<String> uniqueApiNames = new HashSet<>();
69+
private final Map<String, String> apiNameMappings = new HashMap<>();
70+
private final Set<String> uniqueApiNames = new HashSet<>();
6671

6772
public ScalaSttp4JsoniterClientCodegen() {
6873
super();
@@ -80,12 +85,9 @@ public ScalaSttp4JsoniterClientCodegen() {
8085
.excludeGlobalFeatures(
8186
GlobalFeature.XMLStructureDefinitions,
8287
GlobalFeature.Callbacks,
83-
GlobalFeature.LinkObjects,
84-
GlobalFeature.ParameterStyling)
88+
GlobalFeature.LinkObjects)
8589
.excludeSchemaSupportFeatures(
8690
SchemaSupportFeature.Polymorphism)
87-
.excludeParameterFeatures(
88-
ParameterFeature.Cookie)
8991
.includeClientModificationFeatures(
9092
ClientModificationFeature.BasePath,
9193
ClientModificationFeature.UserAgent));
@@ -95,7 +97,11 @@ public ScalaSttp4JsoniterClientCodegen() {
9597
apiTemplateFiles.put("api.mustache", ".scala");
9698
embeddedTemplateDir = templateDir = "scala-sttp4-jsoniter";
9799

98-
String jsonValueClass = "io.circe.Json";
100+
// Scala 3 reserved words
101+
reservedWords.addAll(Arrays.asList("enum", "export", "given", "then", "using", "Request", "Method", "Either"));
102+
103+
importMapping.put(jsonValueClass, jsonAstCodecImport);
104+
importMapping.put("BigDecimal", "scala.math.BigDecimal");
99105

100106
additionalProperties.put(CodegenConstants.GROUP_ID, groupId);
101107
additionalProperties.put(CodegenConstants.ARTIFACT_ID, artifactId);
@@ -109,11 +115,7 @@ public ScalaSttp4JsoniterClientCodegen() {
109115
additionalProperties.put("fnEnumEntry", new EnumEntryLambda());
110116
additionalProperties.put("fnCodecName", new CodecNameLambda());
111117
additionalProperties.put("fnHandleDownload", new HandleDownloadLambda());
112-
113-
// importMapping.remove("Seq");
114-
// importMapping.remove("List");
115-
// importMapping.remove("Set");
116-
// importMapping.remove("Map");
118+
additionalProperties.put("fnEnumLeaf", new EnumLeafLambda());
117119

118120
// TODO: there is no specific sttp mapping. All Scala Type mappings should be in
119121
// AbstractScala
@@ -130,17 +132,22 @@ public ScalaSttp4JsoniterClientCodegen() {
130132
typeMapping.put("short", "Short");
131133
typeMapping.put("char", "Char");
132134
typeMapping.put("double", "Double");
133-
typeMapping.put("object", jsonValueClass);
134135
typeMapping.put("file", "File");
135136
typeMapping.put("binary", "File");
136137
typeMapping.put("number", "Double");
137138
typeMapping.put("decimal", "BigDecimal");
138139
typeMapping.put("ByteArray", "Array[Byte]");
140+
141+
// actually, these two *are* jsoniter+circe AST specific
142+
typeMapping.put("object", jsonValueClass);
139143
typeMapping.put("AnyType", jsonValueClass);
140144

141145
instantiationTypes.put("array", "ListBuffer");
142146
instantiationTypes.put("map", "Map");
143147

148+
// remove DATE_LIBRARY option, we don't need it
149+
cliOptions.removeIf(option -> option.getOpt().equals(DATE_LIBRARY));
150+
144151
properties.stream()
145152
.map(Property::toCliOptions)
146153
.flatMap(Collection::stream)
@@ -161,6 +168,8 @@ public void processOpts() {
161168
supportingFiles.add(new SupportingFile("jsonSupport.mustache", invokerFolder, "JsonSupport.scala"));
162169
supportingFiles.add(new SupportingFile("additionalTypeSerializers.mustache", invokerFolder,
163170
"AdditionalTypeSerializers.scala"));
171+
supportingFiles.add(new SupportingFile("helpers.mustache", invokerFolder,
172+
"Helpers.scala"));
164173
supportingFiles.add(new SupportingFile("project/build.properties.mustache", "project", "build.properties"));
165174
}
166175

@@ -182,61 +191,19 @@ public String encodePath(String input) {
182191
StringBuffer buf = new StringBuffer(path.length());
183192
Matcher matcher = Pattern.compile("[{](.*?)[}]").matcher(path);
184193
while (matcher.find()) {
185-
matcher.appendReplacement(buf, "\\${" + toParamName(matcher.group(0)) + "}");
194+
matcher.appendReplacement(buf, "\\${" + toParamName(matcher.group(0)).replace("`", "") + "PathParam}");
186195
}
187196
matcher.appendTail(buf);
188197
return buf.toString();
189198
}
190199

191-
private PathMetadata parseAndEncodePath(String input) {
192-
String path = super.encodePath(input);
193-
ArrayList<String> pathParams = new ArrayList<>();
194-
195-
// The parameter names in the URI must be converted to the same case as
196-
// the method parameter.
197-
StringBuffer buf = new StringBuffer(path.length());
198-
Matcher matcher = Pattern.compile("[{](.*?)[}]").matcher(path);
199-
while (matcher.find()) {
200-
matcher.appendReplacement(buf, "\\${" + toParamName(matcher.group(0)) + "}");
201-
pathParams.add(matcher.group(0));
202-
}
203-
matcher.appendTail(buf);
204-
return new PathMetadata(buf.toString(), pathParams);
205-
}
206-
207200
@Override
208201
public CodegenOperation fromOperation(String path,
209202
String httpMethod,
210203
Operation operation,
211204
List<Server> servers) {
212205
CodegenOperation op = super.fromOperation(path, httpMethod, operation, servers);
213-
214-
PathMetadata pathMetadata = parseAndEncodePath(path);
215-
216-
op.path = pathMetadata.getPath();
217-
218-
for (String pathParam : pathMetadata.getPathParams()) {
219-
CodegenParameter param = new CodegenParameter();
220-
param.isPathParam = true;
221-
param.baseName = pathParam;
222-
param.paramName = toParamName(pathParam);
223-
param.dataType = "String";
224-
param.required = true;
225-
226-
boolean alreadyExists = false;
227-
for (CodegenParameter existingParam : op.pathParams) {
228-
if (existingParam.baseName.equals(param.baseName) || existingParam.paramName.equals(param.paramName)) {
229-
alreadyExists = true;
230-
break;
231-
}
232-
}
233-
234-
if (!alreadyExists) {
235-
op.pathParams.add(param);
236-
op.allParams.add(param);
237-
}
238-
}
239-
206+
op.path = encodePath(path);
240207
return op;
241208
}
242209

@@ -280,7 +247,7 @@ public String toApiName(String name) {
280247
if (!uniqueApiNames.contains(lowerCasedNextGeneratedApiName)) {
281248
uniqueApiNames.add(lowerCasedNextGeneratedApiName);
282249
apiNameMappings.put(name, nextGeneratedApiName);
283-
250+
284251
return nextGeneratedApiName;
285252
}
286253
i++;
@@ -338,13 +305,32 @@ private void postProcessUpdateImports(final Map<String, ModelsMap> models) {
338305
continue;
339306
}
340307
List<Map<String, String>> newImports = new ArrayList<>();
341-
Iterator<Map<String, String>> iterator = imports.iterator();
342-
while (iterator.hasNext()) {
343-
String importPath = iterator.next().get("import");
308+
309+
boolean foundJsonImport = false;
310+
311+
for (Map<String, String> anImport : imports) {
312+
String importPath = anImport.get("import");
313+
344314
Map<String, String> item = new HashMap<>();
315+
316+
// remove any imports for io.circe.Json, it's a FQCN
317+
// but on the first encounter, add the import for the
318+
// jsoniter-scala circe AST codec as it will be necessary
319+
// for all places where io.circe.Json is used as request body
320+
// or response body
321+
if (importPath.contains(jsonValueClass)) {
322+
if (!foundJsonImport) {
323+
foundJsonImport = true;
324+
item.put("import", jsonAstCodecImport);
325+
newImports.add(item);
326+
}
327+
328+
continue;
329+
}
330+
345331
if (importPath.startsWith(prefix)) {
346332
if (isEnumClass(importPath, enumRefs)) {
347-
item.put("import", importPath.concat("._"));
333+
item.put("import", importPath.concat(".*"));
348334
newImports.add(item);
349335
}
350336
} else {
@@ -361,7 +347,7 @@ private Map<String, ModelsMap> getEnumRefs(final Map<String, ModelsMap> models)
361347
Map<String, ModelsMap> enums = new HashMap<>();
362348
for (String key : models.keySet()) {
363349
CodegenModel model = ModelUtils.getModelByName(key, models);
364-
if (model.isEnum) {
350+
if (model != null && model.isEnum) {
365351
ModelsMap objs = models.get(key);
366352
enums.put(key, objs);
367353
}
@@ -438,18 +424,22 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
438424
List<Map<String, String>> newImports = new ArrayList<>();
439425
List<Map<String, String>> imports = objs.getImports();
440426
if (imports != null && !imports.isEmpty()) {
441-
Iterator<Map<String, String>> iterator = imports.iterator();
442-
while (iterator.hasNext()) {
443-
String importPath = iterator.next().get("import");
427+
for (Map<String, String> anImport : imports) {
428+
String importPath = anImport.get("import");
444429
Map<String, String> item = new HashMap<>();
445430
if (isEnumClass(importPath, enumRefs)) {
446-
item.put("import", importPath.concat("._"));
431+
item.put("import", importPath.concat(".*"));
432+
Map<String, String> enumClassImport = new HashMap<>();
433+
enumClassImport.put("import", importPath);
434+
newImports.add(item);
435+
newImports.add(enumClassImport);
447436
} else {
448437
item.put("import", importPath);
438+
newImports.add(item);
449439
}
450-
newImports.add(item);
451440
}
452441
}
442+
453443
objs.setImports(newImports);
454444

455445
return super.postProcessOperationsWithModels(objs, allModels);
@@ -484,8 +474,14 @@ public String toParamName(String name) {
484474
public String toEnumName(CodegenProperty property) {
485475
String identifier = formatIdentifier(property.baseName, true);
486476

487-
// remove backticks because there are no capitalized reserved words in Scala
488477
if (identifier.startsWith("`") && identifier.endsWith("`")) {
478+
// is it numeric?
479+
String unescaped = identifier.substring(1, identifier.length() - 1);
480+
if (StringUtils.isNumeric(unescaped)) {
481+
return identifier; // keep backticks
482+
}
483+
484+
// remove backticks because there are no capitalized reserved words in Scala
489485
return identifier.substring(1, identifier.length() - 1);
490486
} else {
491487
return identifier;
@@ -676,13 +672,23 @@ public String formatFragment(String fragment) {
676672
}
677673
}
678674

679-
private class EnumEntryLambda extends CustomLambda {
675+
private static class EnumEntryLambda extends CustomLambda {
676+
@Override
677+
public String formatFragment(String fragment) {
678+
if (fragment.isBlank()) {
679+
return "NotPresent";
680+
}
681+
return "`" + fragment + "`";
682+
}
683+
}
684+
685+
private static class EnumLeafLambda extends CustomLambda {
680686
@Override
681687
public String formatFragment(String fragment) {
682688
if (fragment.isBlank()) {
683689
return "NotPresent";
684690
}
685-
return formatIdentifier(fragment, true);
691+
return fragment.replace("`", "");
686692
}
687693
}
688694

@@ -698,22 +704,11 @@ private static class HandleDownloadLambda extends CustomLambda {
698704
@Override
699705
public String formatFragment(String fragment) {
700706
if (fragment.equals("asJson[File]")) {
701-
return "asFile(File.createTempFile(\"download\", \".tmp\")).mapLeft(errStr => DeserializationException(errStr, new Exception(errStr)))";
707+
return "asFile(File.createTempFile(\"download\", \".tmp\")).mapWithMetadata((result, metadata) => result.left.map(errStr => ResponseException.DeserializationException(errStr, new Exception(errStr), metadata)))";
702708
} else {
703709
return fragment;
704710
}
705711
}
706712
}
707713

708-
@Getter
709-
private static class PathMetadata {
710-
private final String path;
711-
private final ArrayList<String> pathParams;
712-
713-
PathMetadata(String path, ArrayList<String> pathParams) {
714-
this.path = path;
715-
this.pathParams = pathParams;
716-
}
717-
}
718-
719714
}
Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
package {{invokerPackage}}
22

3-
import java.net.{ URI, URISyntaxException }
3+
import java.net.{URI, URISyntaxException}
44
import com.github.plokhotnyuk.jsoniter_scala.core.*
55

6-
trait AdditionalTypeSerializers {
6+
trait AdditionalTypeSerializers:
77

8-
implicit final lazy val URICodec: JsonValueCodec[URI] = new JsonValueCodec[URI] {
8+
implicit final lazy val URICodec: JsonValueCodec[URI] = new JsonValueCodec[URI]:
99
def nullValue: URI = null
10-
def decodeValue(in: JsonReader, default: URI): URI = ???
11-
def encodeValue(uri: URI, out: JsonWriter): Unit = ???
12-
}
13-
}
10+
def decodeValue(in: JsonReader, default: URI): URI =
11+
try
12+
val uriString = in.readString(null)
13+
if (uriString != null) new URI(uriString) else default
14+
catch
15+
case e: URISyntaxException =>
16+
in.decodeError(s"Invalid URI syntax: ${e.getMessage}")
17+
18+
def encodeValue(uri: URI, out: JsonWriter): Unit =
19+
if (uri != null) out.writeVal(uri.toString)
20+
else out.writeNull()

0 commit comments

Comments
 (0)