Skip to content

Commit c630432

Browse files
committed
wip
1 parent ab448bb commit c630432

13 files changed

Lines changed: 604 additions & 143 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: 61 additions & 78 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,13 +17,13 @@
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 " +
@@ -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();
@@ -95,7 +100,11 @@ public ScalaSttp4JsoniterClientCodegen() {
95100
apiTemplateFiles.put("api.mustache", ".scala");
96101
embeddedTemplateDir = templateDir = "scala-sttp4-jsoniter";
97102

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

100109
additionalProperties.put(CodegenConstants.GROUP_ID, groupId);
101110
additionalProperties.put(CodegenConstants.ARTIFACT_ID, artifactId);
@@ -110,11 +119,6 @@ public ScalaSttp4JsoniterClientCodegen() {
110119
additionalProperties.put("fnCodecName", new CodecNameLambda());
111120
additionalProperties.put("fnHandleDownload", new HandleDownloadLambda());
112121

113-
// importMapping.remove("Seq");
114-
// importMapping.remove("List");
115-
// importMapping.remove("Set");
116-
// importMapping.remove("Map");
117-
118122
// TODO: there is no specific sttp mapping. All Scala Type mappings should be in
119123
// AbstractScala
120124
typeMapping = new HashMap<>();
@@ -130,17 +134,22 @@ public ScalaSttp4JsoniterClientCodegen() {
130134
typeMapping.put("short", "Short");
131135
typeMapping.put("char", "Char");
132136
typeMapping.put("double", "Double");
133-
typeMapping.put("object", jsonValueClass);
134137
typeMapping.put("file", "File");
135138
typeMapping.put("binary", "File");
136139
typeMapping.put("number", "Double");
137140
typeMapping.put("decimal", "BigDecimal");
138141
typeMapping.put("ByteArray", "Array[Byte]");
142+
143+
// actually, these two *are* jsoniter+circe AST specific
144+
typeMapping.put("object", jsonValueClass);
139145
typeMapping.put("AnyType", jsonValueClass);
140146

141147
instantiationTypes.put("array", "ListBuffer");
142148
instantiationTypes.put("map", "Map");
143149

150+
// remove DATE_LIBRARY option, we don't need it
151+
cliOptions.removeIf(option -> option.getOpt().equals(DATE_LIBRARY));
152+
144153
properties.stream()
145154
.map(Property::toCliOptions)
146155
.flatMap(Collection::stream)
@@ -161,6 +170,8 @@ public void processOpts() {
161170
supportingFiles.add(new SupportingFile("jsonSupport.mustache", invokerFolder, "JsonSupport.scala"));
162171
supportingFiles.add(new SupportingFile("additionalTypeSerializers.mustache", invokerFolder,
163172
"AdditionalTypeSerializers.scala"));
173+
supportingFiles.add(new SupportingFile("helpers.mustache", invokerFolder,
174+
"Helpers.scala"));
164175
supportingFiles.add(new SupportingFile("project/build.properties.mustache", "project", "build.properties"));
165176
}
166177

@@ -188,55 +199,13 @@ public String encodePath(String input) {
188199
return buf.toString();
189200
}
190201

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-
207202
@Override
208203
public CodegenOperation fromOperation(String path,
209204
String httpMethod,
210205
Operation operation,
211206
List<Server> servers) {
212207
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-
208+
op.path = encodePath(path);
240209
return op;
241210
}
242211

@@ -280,7 +249,7 @@ public String toApiName(String name) {
280249
if (!uniqueApiNames.contains(lowerCasedNextGeneratedApiName)) {
281250
uniqueApiNames.add(lowerCasedNextGeneratedApiName);
282251
apiNameMappings.put(name, nextGeneratedApiName);
283-
252+
284253
return nextGeneratedApiName;
285254
}
286255
i++;
@@ -338,13 +307,32 @@ private void postProcessUpdateImports(final Map<String, ModelsMap> models) {
338307
continue;
339308
}
340309
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");
310+
311+
boolean foundJsonImport = false;
312+
313+
for (Map<String, String> anImport : imports) {
314+
String importPath = anImport.get("import");
315+
344316
Map<String, String> item = new HashMap<>();
317+
318+
// remove any imports for io.circe.Json, it's a FQCN
319+
// but on the first encounter, add the import for the
320+
// jsoniter-scala circe AST codec as it will be necessary
321+
// for all places where io.circe.Json is used as request body
322+
// or response body
323+
if (importPath.contains(jsonValueClass)) {
324+
if (!foundJsonImport) {
325+
foundJsonImport = true;
326+
item.put("import", jsonAstCodecImport);
327+
newImports.add(item);
328+
}
329+
330+
continue;
331+
}
332+
345333
if (importPath.startsWith(prefix)) {
346334
if (isEnumClass(importPath, enumRefs)) {
347-
item.put("import", importPath.concat("._"));
335+
item.put("import", importPath.concat(".*"));
348336
newImports.add(item);
349337
}
350338
} else {
@@ -361,7 +349,7 @@ private Map<String, ModelsMap> getEnumRefs(final Map<String, ModelsMap> models)
361349
Map<String, ModelsMap> enums = new HashMap<>();
362350
for (String key : models.keySet()) {
363351
CodegenModel model = ModelUtils.getModelByName(key, models);
364-
if (model.isEnum) {
352+
if (model != null && model.isEnum) {
365353
ModelsMap objs = models.get(key);
366354
enums.put(key, objs);
367355
}
@@ -438,18 +426,18 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
438426
List<Map<String, String>> newImports = new ArrayList<>();
439427
List<Map<String, String>> imports = objs.getImports();
440428
if (imports != null && !imports.isEmpty()) {
441-
Iterator<Map<String, String>> iterator = imports.iterator();
442-
while (iterator.hasNext()) {
443-
String importPath = iterator.next().get("import");
429+
for (Map<String, String> anImport : imports) {
430+
String importPath = anImport.get("import");
444431
Map<String, String> item = new HashMap<>();
445432
if (isEnumClass(importPath, enumRefs)) {
446-
item.put("import", importPath.concat("._"));
433+
item.put("import", importPath.concat(".*"));
447434
} else {
448435
item.put("import", importPath);
449436
}
450437
newImports.add(item);
451438
}
452439
}
440+
453441
objs.setImports(newImports);
454442

455443
return super.postProcessOperationsWithModels(objs, allModels);
@@ -484,8 +472,14 @@ public String toParamName(String name) {
484472
public String toEnumName(CodegenProperty property) {
485473
String identifier = formatIdentifier(property.baseName, true);
486474

487-
// remove backticks because there are no capitalized reserved words in Scala
488475
if (identifier.startsWith("`") && identifier.endsWith("`")) {
476+
// is it numeric?
477+
String unescaped = identifier.substring(1, identifier.length() - 1);
478+
if (StringUtils.isNumeric(unescaped)) {
479+
return identifier; // keep backticks
480+
}
481+
482+
// remove backticks because there are no capitalized reserved words in Scala
489483
return identifier.substring(1, identifier.length() - 1);
490484
} else {
491485
return identifier;
@@ -705,15 +699,4 @@ public String formatFragment(String fragment) {
705699
}
706700
}
707701

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-
719702
}
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()

modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,28 @@ package {{package}}
55
import {{import}}
66
{{/imports}}
77
import {{invokerPackage}}.JsonSupport.{*, given}
8+
import {{invokerPackage}}.Helpers.*
89
import sttp.client4.jsoniter.*
910
import sttp.client4.*
1011
import sttp.model.Method
1112
1213
{{#operations}}
13-
object {{classname}} {
14+
object {{classname}}:
1415
def apply(baseUrl: String = "{{{basePath}}}") = new {{classname}}(baseUrl)
15-
}
16-
17-
class {{classname}}(baseUrl: String) {
1816
17+
class {{classname}}(baseUrl: String):
1918
{{#operation}}
20-
2119
{{#javadocRenderer}}
2220
{{>javadoc}}
2321
{{/javadocRenderer}}
24-
def {{operationId}}({{>methodParameters}}): Request[{{#separateErrorChannel}}Either[ResponseException[String, Exception], {{>operationReturnType}}]{{/separateErrorChannel}}{{^separateErrorChannel}}{{>operationReturnType}}{{/separateErrorChannel}}] =
22+
def {{operationId}}{{>methodParameters}}: Request[{{#separateErrorChannel}}Either[ResponseException[String, Exception], {{>operationReturnType}}]{{/separateErrorChannel}}{{^separateErrorChannel}}{{>operationReturnType}}{{/separateErrorChannel}}] =
23+
val requestURL =
24+
uri"$baseUrl{{{path}}}"{{#queryParams}}
25+
.addParamNamed("{{baseName}}", {{{paramName}}}{{#collectionFormat}}, CollectionFormats.{{collectionFormat.toUpperCase}}{{/collectionFormat}}){{/queryParams}}{{#isApiKey}}{{#isKeyInQuery}}
26+
.addParam("{{keyParamName}}", apiKey.value){{/isKeyInQuery}}{{/isApiKey}}
27+
2528
basicRequest
26-
.method(Method.{{httpMethod.toUpperCase}}, uri"$baseUrl{{{path}}}{{#queryParams.0}}?{{#queryParams}}{{baseName}}=${ {{{paramName}}} }{{^-last}}&{{/-last}}{{/queryParams}}{{/queryParams.0}}{{#isApiKey}}{{#isKeyInQuery}}{{^queryParams.0}}?{{/queryParams.0}}{{#queryParams.0}}&{{/queryParams.0}}{{keyParamName}}=${apiKey.value}&{{/isKeyInQuery}}{{/isApiKey}}")
29+
.method(Method.{{httpMethod.toUpperCase}}, requestURL)
2730
.contentType({{#consumes.0}}"{{{mediaType}}}"{{/consumes.0}}{{^consumes}}"application/json"{{/consumes}}){{#headerParams}}
2831
.header({{>paramCreation}}){{/headerParams}}{{#authMethods}}{{#isBasic}}{{#isBasicBasic}}
2932
.auth.basic(username, password){{/isBasicBasic}}{{#isBasicBearer}}
@@ -43,6 +46,5 @@ class {{classname}}(baseUrl: String) {
4346
.response({{#separateErrorChannel}}{{^returnType}}asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(()))){{/returnType}}{{#fnHandleDownload}}{{#returnType}}asJson[{{>operationReturnType}}]{{/returnType}}{{/fnHandleDownload}}{{/separateErrorChannel}}{{^separateErrorChannel}}{{^returnType}}asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(()))).getRight{{/returnType}}{{#returnType}}asJson[{{>operationReturnType}}].getRight{{/returnType}}{{/separateErrorChannel}})
4447
4548
{{/operation}}
46-
47-
}
4849
{{/operations}}
50+
end {{classname}}

modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/build.sbt.mustache

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,4 @@ scalacOptions := Seq(
1616
"-unchecked",
1717
"-deprecation",
1818
"-feature"
19-
)
20-
21-
// REBUILT
19+
)

0 commit comments

Comments
 (0)