Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions bin/configs/scala-sttp4-circe.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
generatorName: scala-sttp4
outputDir: samples/client/petstore/scala-sttp4-circe
inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore.yaml
templateDir: modules/openapi-generator/src/main/resources/scala-sttp4
additionalProperties:
hideGenerationTimestamp: "true"
jsonLibrary: "circe"
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

public class ScalaSttp4ClientCodegen extends AbstractScalaCodegen implements CodegenConfig {
private static final StringProperty STTP_CLIENT_VERSION = new StringProperty("sttpClientVersion", "The version of " +
"sttp client", "4.0.0-M1");
"sttp client", "4.0.15");
private static final BooleanProperty USE_SEPARATE_ERROR_CHANNEL = new BooleanProperty("separateErrorChannel",
"Whether to return response as " +
"F[Either[ResponseError[ErrorType], ReturnType]]] or to flatten " +
Expand All @@ -33,6 +33,10 @@ public class ScalaSttp4ClientCodegen extends AbstractScalaCodegen implements Cod
"joda-time library", "2.10.13");
private static final StringProperty JSON4S_VERSION = new StringProperty("json4sVersion", "The version of json4s " +
"library", "4.0.6");
private static final StringProperty CIRCE_VERSION = new StringProperty("circeVersion", "The version of circe " +
"library", "0.14.15");
private static final StringProperty CIRCE_EXTRAS_VERSION = new StringProperty("circeExtrasVersion",
"The version of circe-generic-extras library", "0.14.4");

private static final JsonLibraryProperty JSON_LIBRARY_PROPERTY = new JsonLibraryProperty();

Expand All @@ -41,7 +45,7 @@ public class ScalaSttp4ClientCodegen extends AbstractScalaCodegen implements Cod

private static final List<Property<?>> properties = Arrays.asList(
STTP_CLIENT_VERSION, USE_SEPARATE_ERROR_CHANNEL, JODA_TIME_VERSION,
JSON4S_VERSION, JSON_LIBRARY_PROPERTY, PACKAGE_PROPERTY);
JSON4S_VERSION, CIRCE_VERSION, CIRCE_EXTRAS_VERSION, JSON_LIBRARY_PROPERTY, PACKAGE_PROPERTY);

private final Logger LOGGER = LoggerFactory.getLogger(ScalaSttp4ClientCodegen.class);

Expand Down Expand Up @@ -86,15 +90,17 @@ public ScalaSttp4ClientCodegen() {
)
);

// Enable oneOf interface generation
useOneOfInterfaces = true;
supportsMultipleInheritance = true;
supportsInheritance = true;
addOneOfInterfaceImports = true;

outputFolder = "generated-code/scala-sttp4";
modelTemplateFiles.put("model.mustache", ".scala");
apiTemplateFiles.put("api.mustache", ".scala");
embeddedTemplateDir = templateDir = "scala-sttp4";

String jsonLibrary = JSON_LIBRARY_PROPERTY.getValue(additionalProperties);

String jsonValueClass = "circe".equals(jsonLibrary) ? "io.circe.Json" : "org.json4s.JValue";

additionalProperties.put(CodegenConstants.GROUP_ID, groupId);
additionalProperties.put(CodegenConstants.ARTIFACT_ID, artifactId);
additionalProperties.put(CodegenConstants.ARTIFACT_VERSION, artifactVersion);
Expand Down Expand Up @@ -124,13 +130,12 @@ public ScalaSttp4ClientCodegen() {
typeMapping.put("short", "Short");
typeMapping.put("char", "Char");
typeMapping.put("double", "Double");
typeMapping.put("object", "Any");
typeMapping.put("file", "File");
typeMapping.put("binary", "File");
typeMapping.put("number", "Double");
typeMapping.put("decimal", "BigDecimal");
typeMapping.put("ByteArray", "Array[Byte]");
typeMapping.put("AnyType", jsonValueClass);
// AnyType and object mapping will be set in processOpts() based on jsonLibrary

instantiationTypes.put("array", "ListBuffer");
instantiationTypes.put("map", "Map");
Expand All @@ -149,6 +154,20 @@ public void processOpts() {
apiPackage = PACKAGE_PROPERTY.getApiPackage(additionalProperties);
modelPackage = PACKAGE_PROPERTY.getModelPackage(additionalProperties);

// Set AnyType and object mapping based on jsonLibrary
String jsonLibrary = JSON_LIBRARY_PROPERTY.getValue(additionalProperties);
if ("circe".equals(jsonLibrary)) {
typeMapping.put("AnyType", "io.circe.Json");
typeMapping.put("object", "io.circe.JsonObject");
importMapping.put("io.circe.Json", "io.circe.Json");
importMapping.put("io.circe.JsonObject", "io.circe.JsonObject");
} else {
typeMapping.put("AnyType", "org.json4s.JValue");
typeMapping.put("object", "org.json4s.JObject");
importMapping.put("org.json4s.JValue", "org.json4s.JValue");
importMapping.put("org.json4s.JObject", "org.json4s.JObject");
}

supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
supportingFiles.add(new SupportingFile("build.sbt.mustache", "", "build.sbt"));
final String invokerFolder = (sourceFolder + File.separator + invokerPackage).replace(".", File.separator);
Expand Down Expand Up @@ -211,6 +230,16 @@ public ModelsMap postProcessModels(ModelsMap objs) {
return objs;
}

private void setParameterDefaults(CodegenParameter param) {
// Set default values for optional parameters
// Template will handle Option[] wrapping, so all defaults should be None
if (!param.required) {
param.defaultValue = "None";
}
}



/**
* Invoked by {@link DefaultGenerator} after all models have been post-processed,
* allowing for a last pass of codegen-specific model cleanup.
Expand All @@ -221,6 +250,104 @@ public ModelsMap postProcessModels(ModelsMap objs) {
@Override
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
final Map<String, ModelsMap> processed = super.postProcessAllModels(objs);

// First pass: count how many oneOf parents each model has
Map<String, Integer> oneOfMemberCount = new HashMap<>();
for (ModelsMap mm : processed.values()) {
for (ModelMap model : mm.getModels()) {
CodegenModel cModel = model.getModel();
if (!cModel.oneOf.isEmpty()) {
for (String childName : cModel.oneOf) {
oneOfMemberCount.put(childName, oneOfMemberCount.getOrDefault(childName, 0) + 1);
}
}
}
}

// Second pass: process models
for (ModelsMap mm : processed.values()) {
for (ModelMap model : mm.getModels()) {
CodegenModel cModel = model.getModel();

if (!cModel.oneOf.isEmpty()) {
cModel.getVendorExtensions().put("x-isSealedTrait", true);

// Collect child models for inline generation
// Only inline if they are used exclusively by this oneOf parent
List<CodegenModel> childModels = new ArrayList<>();

for (String childName : cModel.oneOf) {
CodegenModel childModel = ModelUtils.getModelByName(childName, processed);
if (childModel != null && oneOfMemberCount.getOrDefault(childName, 0) == 1) {
// This child is only used by this parent - can be inlined
childModel.getVendorExtensions().put("x-isOneOfMember", true);
childModel.getVendorExtensions().put("x-oneOfParent", cModel.classname);

// Add discriminator mapping value if present
if (cModel.discriminator != null) {
String discriminatorName = cModel.discriminator.getPropertyName();

// Find the mapping value for this child model
String discriminatorValue = null;
if (cModel.discriminator.getMappedModels() != null) {
for (CodegenDiscriminator.MappedModel mappedModel : cModel.discriminator.getMappedModels()) {
if (mappedModel.getModelName().equals(childName)) {
discriminatorValue = mappedModel.getMappingName();
break;
}
}
}

if (discriminatorValue != null) {
childModel.getVendorExtensions().put("x-discriminator-value", discriminatorValue);
}

// Remove discriminator field from child
// (circe-generic-extras adds it automatically)
childModel.vars.removeIf(prop -> prop.baseName.equals(discriminatorName));
childModel.allVars.removeIf(prop -> prop.baseName.equals(discriminatorName));
childModel.requiredVars.removeIf(prop -> prop.baseName.equals(discriminatorName));
childModel.optionalVars.removeIf(prop -> prop.baseName.equals(discriminatorName));
}

childModels.add(childModel);
}
}
cModel.getVendorExtensions().put("x-oneOfMembers", childModels);
} else if (cModel.isEnum) {
cModel.getVendorExtensions().put("x-isEnum", true);
} else {
cModel.getVendorExtensions().put("x-isRegularModel", true);
}

if (cModel.discriminator != null) {
cModel.getVendorExtensions().put("x-use-discr", true);

if (cModel.discriminator.getMapping() != null) {
cModel.getVendorExtensions().put("x-use-discr-mapping", true);
}
}

// Remove discriminator property from models that extend a oneOf parent
// (circe-generic-extras adds it automatically)
if (cModel.parent != null && cModel.parentModel != null && cModel.parentModel.discriminator != null) {
String discriminatorName = cModel.parentModel.discriminator.getPropertyName();
cModel.vars.removeIf(prop -> prop.baseName.equals(discriminatorName));
cModel.allVars.removeIf(prop -> prop.baseName.equals(discriminatorName));
cModel.requiredVars.removeIf(prop -> prop.baseName.equals(discriminatorName));
cModel.optionalVars.removeIf(prop -> prop.baseName.equals(discriminatorName));
}
}
}

// Third pass: remove oneOf members from the map to skip file generation
// (they are already inlined in their parent sealed trait)
processed.entrySet().removeIf(entry -> {
ModelsMap mm = entry.getValue();
return mm.getModels().stream()
.anyMatch(model -> model.getModel().getVendorExtensions().containsKey("x-isOneOfMember"));
});

postProcessUpdateImports(processed);
return processed;
}
Expand Down Expand Up @@ -349,6 +476,14 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
}
objs.setImports(newImports);

// Fix parameter types and defaults
OperationMap opsMap = objs.getOperations();
for (CodegenOperation operation : opsMap.getOperation()) {
for (CodegenParameter param : operation.allParams) {
setParameterDefaults(param);
}
}

return super.postProcessOperationsWithModels(objs, allModels);
}

Expand Down Expand Up @@ -402,11 +537,11 @@ public String toDefaultValue(Schema p) {
String inner = getSchemaType(ModelUtils.getAdditionalProperties(p));
return "Map[String, " + inner + "].empty ";
} else if (ModelUtils.isArraySchema(p)) {
String inner = getSchemaType(ModelUtils.getSchemaItems(p));
// Use simple Seq.empty for cleaner code
if (ModelUtils.isSet(p)) {
return "Set[" + inner + "].empty ";
return "Set.empty";
}
return "Seq[" + inner + "].empty ";
return "Seq.empty";
} else if (ModelUtils.isStringSchema(p)) {
return null;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class {{classname}}(baseUrl: String) {
{{>javadoc}}

{{/javadocRenderer}}
def {{operationId}}({{>methodParameters}}): Request[{{#separateErrorChannel}}Either[ResponseException[String, Exception], {{>operationReturnType}}]{{/separateErrorChannel}}{{^separateErrorChannel}}{{>operationReturnType}}{{/separateErrorChannel}}] =
def {{operationId}}({{>methodParameters}}): Request[{{#separateErrorChannel}}Either[ResponseException[String], {{>operationReturnType}}]{{/separateErrorChannel}}{{^separateErrorChannel}}{{>operationReturnType}}{{/separateErrorChannel}}] =
basicRequest
.method(Method.{{httpMethod.toUpperCase}}, uri"$baseUrl{{{path}}}{{#queryParams.0}}?{{/queryParams.0}}{{#queryParams}}{{baseName}}=${ {{paramName}} }{{^-last}}&{{/-last}}{{/queryParams}}{{#authMethods}}{{#isApiKey}}{{#isKeyInQuery}}{{#queryParams.0}}&{{/queryParams.0}}{{^queryParams.0}}?{{/queryParams.0}}{{keyParamName}}=${apiKeyQuery}{{/isKeyInQuery}}{{/isApiKey}}{{/authMethods}}")
.contentType({{#consumes.0}}"{{{mediaType}}}"{{/consumes.0}}{{^consumes}}"application/json"{{/consumes}}){{#headerParams}}
Expand All @@ -35,7 +35,7 @@ class {{classname}}(baseUrl: String) {
.multipartBody(Seq({{#formParams}}
{{>paramMultipartCreation}}{{^-last}}, {{/-last}}{{/formParams}}
).flatten){{/isMultipart}}{{/formParams.0}}{{#bodyParam}}
.body({{paramName}}){{/bodyParam}}
.body(asJson({{paramName}})){{/bodyParam}}
.response({{#separateErrorChannel}}{{^returnType}}asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(()))){{/returnType}}{{#returnType}}asJson[{{>operationReturnType}}]{{/returnType}}{{/separateErrorChannel}}{{^separateErrorChannel}}{{^returnType}}asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(()))).getRight{{/returnType}}{{#returnType}}asJson[{{>operationReturnType}}].getRight{{/returnType}}{{/separateErrorChannel}})

{{/operation}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ libraryDependencies ++= Seq(
"org.json4s" %% "json4s-jackson" % "{{json4sVersion}}"
{{/json4s}}
{{#circe}}
"com.softwaremill.sttp.client4" %% "circe" % "{{sttpClientVersion}}"
"com.softwaremill.sttp.client4" %% "circe" % "{{sttpClientVersion}}",
"io.circe" %% "circe-generic" % "{{circeVersion}}",
"io.circe" %% "circe-generic-extras" % "{{circeExtrasVersion}}",
{{/circe}}
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,9 @@ object JsonSupport extends SttpJson4sApi {
{{#circe}}
import io.circe.{Decoder, Encoder}
import io.circe.generic.AutoDerivation
import sttp.client3.circe.SttpCirceApi
import sttp.client4.circe.SttpCirceApi

object JsonSupport extends SttpCirceApi with AutoDerivation with DateSerializers with AdditionalTypeSerializers {

{{#models}}
{{#model}}
{{#isEnum}}
implicit val {{classname}}Decoder: Decoder[{{classname}}.{{classname}}] = Decoder.decodeEnumeration({{classname}})
implicit val {{classname}}Encoder: Encoder[{{classname}}.{{classname}}] = Encoder.encodeEnumeration({{classname}})
{{/isEnum}}
{{/model}}
{{/models}}
// Enum encoders/decoders are defined in their respective companion objects
}
{{/circe}}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{#authMethods.0}}{{#authMethods}}{{#isApiKey}}{{#isKeyInHeader}}apiKeyHeader: String{{/isKeyInHeader}}{{#isKeyInQuery}}apiKeyQuery: String{{/isKeyInQuery}}{{#isKeyInCookie}}apiKeyCookie: String{{/isKeyInCookie}}{{/isApiKey}}{{#isBasic}}{{#isBasicBasic}}username: String, password: String{{/isBasicBasic}}{{#isBasicBearer}}bearerToken: String{{/isBasicBearer}}{{/isBasic}}{{^-last}}, {{/-last}}{{/authMethods}})({{/authMethods.0}}{{#allParams}}{{paramName}}: {{#required}}{{dataType}}{{/required}}{{^required}}{{#isContainer}}{{dataType}}{{/isContainer}}{{^isContainer}}Option[{{dataType}}]{{/isContainer}}{{/required}}{{^defaultValue}}{{^required}}{{^isContainer}} = None{{/isContainer}}{{/required}}{{/defaultValue}}{{^-last}}, {{/-last}}{{/allParams}}
{{#authMethods.0}}{{#authMethods}}{{#isApiKey}}{{#isKeyInHeader}}apiKeyHeader: String{{/isKeyInHeader}}{{#isKeyInQuery}}apiKeyQuery: String{{/isKeyInQuery}}{{#isKeyInCookie}}apiKeyCookie: String{{/isKeyInCookie}}{{/isApiKey}}{{#isBasic}}{{#isBasicBasic}}username: String, password: String{{/isBasicBasic}}{{#isBasicBearer}}bearerToken: String{{/isBasicBearer}}{{/isBasic}}{{^-last}}, {{/-last}}{{/authMethods}})({{/authMethods.0}}{{#allParams}}{{paramName}}: {{#required}}{{dataType}}{{/required}}{{^required}}Option[{{dataType}}]{{/required}}{{#defaultValue}} = {{{defaultValue}}}{{/defaultValue}}{{^-last}}, {{/-last}}{{/allParams}}
Loading