From e640397ca07b94a916aa3884393959a104ae81c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Bia=C5=82y?= Date: Mon, 28 Oct 2024 16:31:16 +0100 Subject: [PATCH 01/14] started impl --- .../ScalaSttp4JsoniterClientCodegen.java | 605 ++++++++++++++++++ .../scala-sttp4-jsoniter/README.mustache | 115 ++++ .../additionalTypeSerializers.mustache | 15 + .../scala-sttp4-jsoniter/api.mustache | 42 ++ .../scala-sttp4-jsoniter/build.sbt.mustache | 23 + .../scala-sttp4-jsoniter/javadoc.mustache | 25 + .../scala-sttp4-jsoniter/jsonSupport.mustache | 59 ++ .../scala-sttp4-jsoniter/licenseInfo.mustache | 11 + .../methodParameters.mustache | 1 + .../scala-sttp4-jsoniter/model.mustache | 60 ++ .../operationReturnType.mustache | 1 + .../paramCreation.mustache | 1 + .../paramFormCreation.mustache | 1 + .../paramMultipartCreation.mustache | 16 + .../project/build.properties.mustache | 1 + .../responseState.mustache | 1 + 16 files changed, 977 insertions(+) create mode 100644 modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java create mode 100644 modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/README.mustache create mode 100644 modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/additionalTypeSerializers.mustache create mode 100644 modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache create mode 100644 modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/build.sbt.mustache create mode 100644 modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/javadoc.mustache create mode 100644 modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/jsonSupport.mustache create mode 100644 modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/licenseInfo.mustache create mode 100644 modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/methodParameters.mustache create mode 100644 modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache create mode 100644 modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/operationReturnType.mustache create mode 100644 modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramCreation.mustache create mode 100644 modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramFormCreation.mustache create mode 100644 modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramMultipartCreation.mustache create mode 100644 modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/project/build.properties.mustache create mode 100644 modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/responseState.mustache diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java new file mode 100644 index 000000000000..2314767ee3d3 --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java @@ -0,0 +1,605 @@ +package org.openapitools.codegen.languages; + +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import org.apache.commons.lang3.StringUtils; +import org.openapitools.codegen.*; +import org.openapitools.codegen.meta.GeneratorMetadata; +import org.openapitools.codegen.meta.Stability; +import org.openapitools.codegen.meta.features.*; +import org.openapitools.codegen.model.ModelMap; +import org.openapitools.codegen.model.ModelsMap; +import org.openapitools.codegen.model.OperationMap; +import org.openapitools.codegen.model.OperationsMap; +import org.openapitools.codegen.utils.ModelUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ScalaSttp4JsoniterClientCodegen extends AbstractScalaCodegen implements CodegenConfig { + private static final StringProperty STTP_CLIENT_VERSION = new StringProperty("sttpClientVersion", + "The version of " + + "sttp client", + "4.0.0-M19"); + private static final BooleanProperty USE_SEPARATE_ERROR_CHANNEL = new BooleanProperty("separateErrorChannel", + "Whether to return response as " + + "F[Either[ResponseError[ErrorType], ReturnType]]] or to flatten " + + "response's error raising them through enclosing monad (F[ReturnType]).", + true); + private static final StringProperty JODA_TIME_VERSION = new StringProperty("jodaTimeVersion", "The version of " + + "joda-time library", "2.10.13"); + private static final StringProperty JSONITER_VERSION = new StringProperty("jsoniterVersion", + "The version of jsoniter-scala " + + "library", + "2.31.1"); + + private static final JsonLibraryProperty JSON_LIBRARY_PROPERTY = new JsonLibraryProperty(); + + public static final String DEFAULT_PACKAGE_NAME = "org.openapitools.client"; + private static final PackageProperty PACKAGE_PROPERTY = new PackageProperty(); + + private static final List> properties = Arrays.asList( + STTP_CLIENT_VERSION, USE_SEPARATE_ERROR_CHANNEL, JODA_TIME_VERSION, + JSONITER_VERSION, JSON_LIBRARY_PROPERTY, PACKAGE_PROPERTY); + + private final Logger LOGGER = LoggerFactory.getLogger(ScalaSttp4ClientCodegen.class); + + protected String groupId = "org.openapitools"; + protected String artifactId = "openapi-client"; + protected String artifactVersion = "1.0.0"; + protected boolean registerNonStandardStatusCodes = true; + protected boolean renderJavadoc = true; + protected boolean removeOAuthSecurities = true; + + Map enumRefs = new HashMap<>(); + + public ScalaSttp4JsoniterClientCodegen() { + super(); + generatorMetadata = GeneratorMetadata.newBuilder(generatorMetadata) + .stability(Stability.BETA) + .build(); + + modifyFeatureSet(features -> features + .includeDocumentationFeatures(DocumentationFeature.Readme) + .wireFormatFeatures(EnumSet.of(WireFormatFeature.JSON, WireFormatFeature.XML, WireFormatFeature.Custom)) + .securityFeatures(EnumSet.of( + SecurityFeature.BasicAuth, + SecurityFeature.ApiKey, + SecurityFeature.BearerToken)) + .excludeGlobalFeatures( + GlobalFeature.XMLStructureDefinitions, + GlobalFeature.Callbacks, + GlobalFeature.LinkObjects, + GlobalFeature.ParameterStyling) + .excludeSchemaSupportFeatures( + SchemaSupportFeature.Polymorphism) + .excludeParameterFeatures( + ParameterFeature.Cookie) + .includeClientModificationFeatures( + ClientModificationFeature.BasePath, + ClientModificationFeature.UserAgent)); + + outputFolder = "generated-code/scala-sttp4-jsoniter"; + modelTemplateFiles.put("model.mustache", ".scala"); + apiTemplateFiles.put("api.mustache", ".scala"); + embeddedTemplateDir = templateDir = "scala-sttp4-jsoniter"; + + String jsonLibrary = JSON_LIBRARY_PROPERTY.getValue(additionalProperties); + + String jsonValueClass = "io.circe.Json"; + + additionalProperties.put(CodegenConstants.GROUP_ID, groupId); + additionalProperties.put(CodegenConstants.ARTIFACT_ID, artifactId); + additionalProperties.put(CodegenConstants.ARTIFACT_VERSION, artifactVersion); + if (renderJavadoc) { + additionalProperties.put("javadocRenderer", new JavadocLambda()); + } + additionalProperties.put("fnCapitalize", new CapitalizeLambda()); + additionalProperties.put("fnCamelize", new CamelizeLambda(false)); + additionalProperties.put("fnEnumEntry", new EnumEntryLambda()); + + // importMapping.remove("Seq"); + // importMapping.remove("List"); + // importMapping.remove("Set"); + // importMapping.remove("Map"); + + // TODO: there is no specific sttp mapping. All Scala Type mappings should be in + // AbstractScala + typeMapping = new HashMap<>(); + typeMapping.put("array", "Seq"); + typeMapping.put("set", "Set"); + typeMapping.put("boolean", "Boolean"); + typeMapping.put("string", "String"); + typeMapping.put("int", "Int"); + typeMapping.put("integer", "Int"); + typeMapping.put("long", "Long"); + typeMapping.put("float", "Float"); + typeMapping.put("byte", "Byte"); + typeMapping.put("short", "Short"); + typeMapping.put("char", "Char"); + typeMapping.put("double", "Double"); + typeMapping.put("object", jsonValueClass); + 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); + + instantiationTypes.put("array", "ListBuffer"); + instantiationTypes.put("map", "Map"); + + properties.stream() + .map(Property::toCliOptions) + .flatMap(Collection::stream) + .forEach(option -> cliOptions.add(option)); + } + + @Override + public void processOpts() { + super.processOpts(); + properties.forEach(p -> p.updateAdditionalProperties(additionalProperties)); + invokerPackage = PACKAGE_PROPERTY.getInvokerPackage(additionalProperties); + apiPackage = PACKAGE_PROPERTY.getApiPackage(additionalProperties); + modelPackage = PACKAGE_PROPERTY.getModelPackage(additionalProperties); + + 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); + supportingFiles.add(new SupportingFile("jsonSupport.mustache", invokerFolder, "JsonSupport.scala")); + supportingFiles.add(new SupportingFile("additionalTypeSerializers.mustache", invokerFolder, + "AdditionalTypeSerializers.scala")); + supportingFiles.add(new SupportingFile("project/build.properties.mustache", "project", "build.properties")); + // supportingFiles.add(new SupportingFile("dateSerializers.mustache", + // invokerFolder, "DateSerializers.scala")); + } + + @Override + public String getName() { + return "scala-sttp4-jsoniter"; + } + + @Override + public String getHelp() { + return "Generates a Scala client library (beta) based on Sttp4 and Jsoniter-Scala."; + } + + @Override + public String encodePath(String input) { + String path = super.encodePath(input); + + // The parameter names in the URI must be converted to the same case as + // the method parameter. + StringBuffer buf = new StringBuffer(path.length()); + Matcher matcher = Pattern.compile("[{](.*?)[}]").matcher(path); + while (matcher.find()) { + matcher.appendReplacement(buf, "\\${" + toParamName(matcher.group(0)) + "}"); + } + matcher.appendTail(buf); + return buf.toString(); + } + + @Override + public CodegenOperation fromOperation(String path, + String httpMethod, + Operation operation, + List servers) { + CodegenOperation op = super.fromOperation(path, httpMethod, operation, servers); + op.path = encodePath(path); + return op; + } + + @Override + public CodegenType getTag() { + return CodegenType.CLIENT; + } + + @Override + public String escapeReservedWord(String name) { + if (this.reservedWordsMappings().containsKey(name)) { + return this.reservedWordsMappings().get(name); + } + return "`" + name + "`"; + } + + @Override + public ModelsMap postProcessModels(ModelsMap objs) { + return objs; + } + + /** + * Invoked by {@link DefaultGenerator} after all models have been + * post-processed, + * allowing for a last pass of codegen-specific model cleanup. + * + * @param objs Current state of codegen object model. + * @return An in-place modified state of the codegen object model. + */ + @Override + public Map postProcessAllModels(Map objs) { + final Map processed = super.postProcessAllModels(objs); + postProcessUpdateImports(processed); + return processed; + } + + /** + * Update/clean up model imports + *

+ * append '._" if the import is a Enum class, otherwise + * remove model imports to avoid warnings for importing class in the same + * package in Scala + * + * @param models processed models to be further processed + */ + @SuppressWarnings("unchecked") + private void postProcessUpdateImports(final Map models) { + final String prefix = modelPackage() + "."; + + enumRefs = getEnumRefs(models); + + for (String openAPIName : models.keySet()) { + CodegenModel model = ModelUtils.getModelByName(openAPIName, models); + if (model == null) { + LOGGER.warn( + "Expected to retrieve model {} by name, but no model was found. Check your -Dmodels inclusions.", + openAPIName); + continue; + } + + ModelsMap objs = models.get(openAPIName); + List> imports = objs.getImports(); + if (imports == null || imports.isEmpty()) { + continue; + } + List> newImports = new ArrayList<>(); + Iterator> iterator = imports.iterator(); + while (iterator.hasNext()) { + String importPath = iterator.next().get("import"); + Map item = new HashMap<>(); + if (importPath.startsWith(prefix)) { + if (isEnumClass(importPath, enumRefs)) { + item.put("import", importPath.concat("._")); + newImports.add(item); + } + } else { + item.put("import", importPath); + newImports.add(item); + } + } + // reset imports + objs.setImports(newImports); + } + } + + private Map getEnumRefs(final Map models) { + Map enums = new HashMap<>(); + for (String key : models.keySet()) { + CodegenModel model = ModelUtils.getModelByName(key, models); + if (model.isEnum) { + ModelsMap objs = models.get(key); + enums.put(key, objs); + } + } + return enums; + } + + private boolean isEnumClass(final String importPath, final Map enumModels) { + if (enumModels == null || enumModels.isEmpty()) { + return false; + } + for (ModelsMap objs : enumModels.values()) { + List models = objs.getModels(); + if (models == null || models.isEmpty()) { + continue; + } + for (final Map model : models) { + String enumImportPath = (String) model.get("importPath"); + if (enumImportPath != null && enumImportPath.equals(importPath)) { + return true; + } + } + } + return false; + } + + @Override + public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List allModels) { + if (registerNonStandardStatusCodes) { + try { + OperationMap opsMap = objs.getOperations(); + HashSet unknownCodes = new HashSet<>(); + for (CodegenOperation operation : opsMap.getOperation()) { + for (CodegenResponse response : operation.responses) { + if ("default".equals(response.code)) { + continue; + } + try { + int code = Integer.parseInt(response.code); + if (code >= 600) { + unknownCodes.add(code); + } + } catch (NumberFormatException e) { + LOGGER.error("Status code is not an integer : response.code", e); + } + } + } + if (!unknownCodes.isEmpty()) { + additionalProperties.put("unknownStatusCodes", unknownCodes); + } + } catch (Exception e) { + LOGGER.error("Unable to find operations List", e); + } + } + + // update imports for enum class + List> newImports = new ArrayList<>(); + List> imports = objs.getImports(); + if (imports != null && !imports.isEmpty()) { + Iterator> iterator = imports.iterator(); + while (iterator.hasNext()) { + String importPath = iterator.next().get("import"); + Map item = new HashMap<>(); + if (isEnumClass(importPath, enumRefs)) { + item.put("import", importPath.concat("._")); + } else { + item.put("import", importPath); + } + newImports.add(item); + } + } + objs.setImports(newImports); + + return super.postProcessOperationsWithModels(objs, allModels); + } + + @Override + public List fromSecurity(Map schemes) { + final List codegenSecurities = super.fromSecurity(schemes); + if (!removeOAuthSecurities) { + return codegenSecurities; + } + + // Remove OAuth securities + codegenSecurities.removeIf(security -> security.isOAuth); + if (codegenSecurities.isEmpty()) { + return null; + } + return codegenSecurities; + } + + @Override + public String toParamName(String name) { + // obtain the name from parameterNameMapping directly if provided + if (parameterNameMapping.containsKey(name)) { + return parameterNameMapping.get(name); + } + + return formatIdentifier(name, false); + } + + @Override + public String toEnumName(CodegenProperty property) { + return formatIdentifier(property.baseName, true); + } + + @Override + public String toDefaultValue(Schema p) { + if (p.getRequired() != null && p.getRequired().contains(p.getName())) { + return "None"; + } + + if (ModelUtils.isBooleanSchema(p)) { + return null; + } else if (ModelUtils.isDateSchema(p)) { + return null; + } else if (ModelUtils.isDateTimeSchema(p)) { + return null; + } else if (ModelUtils.isNumberSchema(p)) { + return null; + } else if (ModelUtils.isIntegerSchema(p)) { + return null; + } else if (ModelUtils.isMapSchema(p)) { + String inner = getSchemaType(ModelUtils.getAdditionalProperties(p)); + return "Map[String, " + inner + "].empty "; + } else if (ModelUtils.isArraySchema(p)) { + String inner = getSchemaType(ModelUtils.getSchemaItems(p)); + if (ModelUtils.isSet(p)) { + return "Set[" + inner + "].empty "; + } + return "Seq[" + inner + "].empty "; + } else if (ModelUtils.isStringSchema(p)) { + return null; + } else { + return null; + } + } + + /** + * Update datatypeWithEnum for array container + * + * @param property Codegen property + */ + @Override + protected void updateDataTypeWithEnumForArray(CodegenProperty property) { + CodegenProperty baseItem = property.items; + while (baseItem != null && (Boolean.TRUE.equals(baseItem.isMap) + || Boolean.TRUE.equals(baseItem.isArray))) { + baseItem = baseItem.items; + } + if (baseItem != null) { + // set datetypeWithEnum as only the inner type is enum + property.datatypeWithEnum = toEnumName(baseItem); + // naming the enum with respect to the language enum naming convention + // e.g. remove [], {} from array/map of enum + property.enumName = toEnumName(property); + property._enum = baseItem._enum; + + updateCodegenPropertyEnum(property); + } + } + + public static abstract class Property { + final String name; + final String description; + final T defaultValue; + + public Property(String name, String description, T defaultValue) { + this.name = name; + this.description = description; + this.defaultValue = defaultValue; + } + + public abstract List toCliOptions(); + + public abstract void updateAdditionalProperties(Map additionalProperties); + + public abstract T getValue(Map additionalProperties); + + public void setValue(Map additionalProperties, T value) { + additionalProperties.put(name, value); + } + } + + public static class StringProperty extends Property { + public StringProperty(String name, String description, String defaultValue) { + super(name, description, defaultValue); + } + + @Override + public List toCliOptions() { + return Collections.singletonList(CliOption.newString(name, description).defaultValue(defaultValue)); + } + + @Override + public void updateAdditionalProperties(Map additionalProperties) { + if (!additionalProperties.containsKey(name)) { + additionalProperties.put(name, defaultValue); + } + } + + @Override + public String getValue(Map additionalProperties) { + return additionalProperties.getOrDefault(name, defaultValue).toString(); + } + } + + public static class BooleanProperty extends Property { + public BooleanProperty(String name, String description, Boolean defaultValue) { + super(name, description, defaultValue); + } + + @Override + public List toCliOptions() { + return Collections.singletonList(CliOption.newBoolean(name, description, defaultValue)); + } + + @Override + public void updateAdditionalProperties(Map additionalProperties) { + Boolean value = getValue(additionalProperties); + additionalProperties.put(name, value); + } + + @Override + public Boolean getValue(Map additionalProperties) { + return Boolean.valueOf(additionalProperties.getOrDefault(name, defaultValue.toString()).toString()); + } + } + + public static class JsonLibraryProperty extends StringProperty { + private static final String JSON4S = "json4s"; + private static final String CIRCE = "circe"; + + public JsonLibraryProperty() { + super("jsonLibrary", "Json library to use. Possible values are: json4s and circe.", JSON4S); + } + + @Override + public void updateAdditionalProperties(Map additionalProperties) { + String value = getValue(additionalProperties); + if (CIRCE.equals(value) || JSON4S.equals(value)) { + additionalProperties.put(CIRCE, CIRCE.equals(value)); + additionalProperties.put(JSON4S, JSON4S.equals(value)); + } else { + IllegalArgumentException exception = new IllegalArgumentException( + "Invalid json library: " + value + ". Must be " + CIRCE + " " + + "or " + JSON4S); + throw exception; + } + } + } + + public static class PackageProperty extends StringProperty { + + public PackageProperty() { + super("mainPackage", "Top-level package name, which defines 'apiPackage', 'modelPackage', " + + "'invokerPackage'", DEFAULT_PACKAGE_NAME); + } + + @Override + public void updateAdditionalProperties(Map additionalProperties) { + String mainPackage = getValue(additionalProperties); + if (!additionalProperties.containsKey(CodegenConstants.API_PACKAGE)) { + String apiPackage = mainPackage + ".api"; + additionalProperties.put(CodegenConstants.API_PACKAGE, apiPackage); + } + if (!additionalProperties.containsKey(CodegenConstants.MODEL_PACKAGE)) { + String modelPackage = mainPackage + ".model"; + additionalProperties.put(CodegenConstants.MODEL_PACKAGE, modelPackage); + } + if (!additionalProperties.containsKey(CodegenConstants.INVOKER_PACKAGE)) { + String invokerPackage = mainPackage + ".core"; + additionalProperties.put(CodegenConstants.INVOKER_PACKAGE, invokerPackage); + } + } + + public String getApiPackage(Map additionalProperties) { + return additionalProperties.getOrDefault(CodegenConstants.API_PACKAGE, DEFAULT_PACKAGE_NAME + ".api") + .toString(); + } + + public String getModelPackage(Map additionalProperties) { + return additionalProperties.getOrDefault(CodegenConstants.MODEL_PACKAGE, DEFAULT_PACKAGE_NAME + ".model") + .toString(); + } + + public String getInvokerPackage(Map additionalProperties) { + return additionalProperties.getOrDefault(CodegenConstants.INVOKER_PACKAGE, DEFAULT_PACKAGE_NAME + ".core") + .toString(); + } + } + + private static class JavadocLambda extends CustomLambda { + @Override + public String formatFragment(String fragment) { + final String[] lines = fragment.split("\\r?\\n"); + final StringBuilder sb = new StringBuilder(); + sb.append(" /**\n"); + for (String line : lines) { + sb.append(" * ").append(line).append("\n"); + } + sb.append(" */\n"); + return sb.toString(); + } + } + + private static class CapitalizeLambda extends CustomLambda { + @Override + public String formatFragment(String fragment) { + return StringUtils.capitalize(fragment); + } + } + + private class EnumEntryLambda extends CustomLambda { + @Override + public String formatFragment(String fragment) { + return formatIdentifier(fragment, true); + } + } + +} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/README.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/README.mustache new file mode 100644 index 000000000000..2118a8021718 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/README.mustache @@ -0,0 +1,115 @@ +# {{artifactId}} + +{{appName}} +- API version: {{appVersion}} +{{^hideGenerationTimestamp}} + - Build date: {{generatedDate}} +{{/hideGenerationTimestamp}} + - Generator version: {{generatorVersion}} + +{{{appDescriptionWithNewLines}}} + +{{#infoUrl}} + For more information, please visit [{{{infoUrl}}}]({{{infoUrl}}}) +{{/infoUrl}} + +*Automatically generated by the [OpenAPI Generator](https://openapi-generator.tech)* + +## Requirements + +Building the API client library requires: +1. Java 1.7+ +2. Maven/Gradle/SBT + +## Installation + +To install the API client library to your local Maven repository, simply execute: + +```shell +mvn clean install +``` + +To deploy it to a remote Maven repository instead, configure the settings of the repository and execute: + +```shell +mvn clean deploy +``` + +Refer to the [OSSRH Guide](http://central.sonatype.org/pages/ossrh-guide.html) for more information. + +### Maven users + +Add this dependency to your project's POM: + +```xml + + {{{groupId}}} + {{{artifactId}}} + {{{artifactVersion}}} + compile + +``` + +### Gradle users + +Add this dependency to your project's build file: + +```groovy +compile "{{{groupId}}}:{{{artifactId}}}:{{{artifactVersion}}}" +``` + +### SBT users + +```scala +libraryDependencies += "{{{groupId}}}" % "{{{artifactId}}}" % "{{{artifactVersion}}}" +``` + +## Getting Started + +## Documentation for API Endpoints + +All URIs are relative to *{{basePath}}* + +Class | Method | HTTP request | Description +------------ | ------------- | ------------- | ------------- +{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}*{{classname}}* | **{{operationId}}** | **{{httpMethod}}** {{path}} | {{summary}} +{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} + +## Documentation for Models + +{{#models}}{{#model}} - [{{classname}}]({{modelDocPath}}{{classname}}.md) +{{/model}}{{/models}} + + +## Documentation for Authorization + +{{^authMethods}}Endpoints do not require authorization.{{/authMethods}} +{{#hasAuthMethods}}Authentication schemes defined for the API:{{/hasAuthMethods}} +{{#authMethods}} + + ### {{name}} + + {{#isApiKey}}- **Type**: API key + - **API key parameter name**: {{keyParamName}} + - **Location**: {{#isKeyInQuery}}URL query string{{/isKeyInQuery}}{{#isKeyInHeader}}HTTP header{{/isKeyInHeader}} + {{/isApiKey}} + {{#isBasicBasic}}- **Type**: HTTP basic authentication + {{/isBasicBasic}} + {{#isBasicBearer}}- **Type**: HTTP Bearer Token authentication{{#bearerFormat}} ({{{.}}}){{/bearerFormat}} + {{/isBasicBearer}} + {{#isHttpSignature}}- **Type**: HTTP signature authentication + {{/isHttpSignature}} + {{#isOAuth}}- **Type**: OAuth + - **Flow**: {{flow}} + - **Authorization URL**: {{authorizationUrl}} + - **Scopes**: {{^scopes}}N/A{{/scopes}} + {{#scopes}} - {{scope}}: {{description}} + {{/scopes}} + {{/isOAuth}} + +{{/authMethods}} + +## Author + +{{#apiInfo}}{{#apis}}{{#-last}}{{infoEmail}} +{{/-last}}{{/apis}}{{/apiInfo}} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/additionalTypeSerializers.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/additionalTypeSerializers.mustache new file mode 100644 index 000000000000..62d36137c151 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/additionalTypeSerializers.mustache @@ -0,0 +1,15 @@ +package {{invokerPackage}} + +import java.net.{ URI, URISyntaxException } + +{{#circe}} +trait AdditionalTypeSerializers { + import com.github.plokhotnyuk.jsoniter_scala._ + + implicit final lazy val URICodec: JsonValueCodec[URI] = new JsonValueCodec[URI] { + def nullValue: URI = null + def decodeValue(in: JsonReader, default: URI): URI = ??? + def encodeValue(uri: URI, out: JsonWriter): Unit = ??? + } +} +{{/circe}} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache new file mode 100644 index 000000000000..1635d5bd1c15 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache @@ -0,0 +1,42 @@ +{{>licenseInfo}} +package {{package}} + +{{#imports}} +import {{import}} +{{/imports}} +import {{invokerPackage}}.JsonSupport._ +import sttp.client4._ +import sttp.model.Method + +{{#operations}} +object {{classname}} { + def apply(baseUrl: String = "{{{basePath}}}") = new {{classname}}(baseUrl) +} + +class {{classname}}(baseUrl: String) { + +{{#operation}} +{{#javadocRenderer}} +{{>javadoc}} +{{/javadocRenderer}} + def {{operationId}}({{>methodParameters}}): Request[{{#separateErrorChannel}}Either[ResponseException[String, Exception], {{>operationReturnType}}]{{/separateErrorChannel}}{{^separateErrorChannel}}{{>operationReturnType}}{{/separateErrorChannel}}] = + basicRequest + .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}}") + .contentType({{#consumes.0}}"{{{mediaType}}}"{{/consumes.0}}{{^consumes}}"application/json"{{/consumes}}){{#headerParams}} + .header({{>paramCreation}}){{/headerParams}}{{#authMethods}}{{#isBasic}}{{#isBasicBasic}} + .auth.basic(username, password){{/isBasicBasic}}{{#isBasicBearer}} + .auth.bearer(bearerToken){{/isBasicBearer}}{{/isBasic}}{{#isApiKey}}{{#isKeyInHeader}} + .header("{{keyParamName}}", apiKey){{/isKeyInHeader}}{{#isKeyInCookie}} + .cookie("{{keyParamName}}", apiKey){{/isKeyInCookie}}{{/isApiKey}}{{/authMethods}}{{#formParams.0}}{{^isMultipart}} + .body(Map({{#formParams}} + {{>paramFormCreation}}{{^-last}},{{/-last}}{{/formParams}} + )){{/isMultipart}}{{#isMultipart}} + .multipartBody(Seq({{#formParams}} + {{>paramMultipartCreation}}{{^-last}}, {{/-last}}{{/formParams}} + ).flatten){{/isMultipart}}{{/formParams.0}}{{#bodyParam}} + .body({{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}} +} +{{/operations}} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/build.sbt.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/build.sbt.mustache new file mode 100644 index 000000000000..2e73e53523aa --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/build.sbt.mustache @@ -0,0 +1,23 @@ +version := "{{artifactVersion}}" +name := "{{artifactId}}" +organization := "{{groupId}}" + +scalaVersion := "3.3.4" +crossScalaVersions := Seq(scalaVersion.value, "2.13.15") + +libraryDependencies ++= Seq( + "com.softwaremill.sttp.client4" %% "core" % "{{sttpClientVersion}}", + "com.softwaremill.sttp.client4" %% "jsoniter" % "{{sttpClientVersion}}", +{{#joda}} + "joda-time" % "joda-time" % "{{jodaTimeVersion}}", +{{/joda}} + "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "{{jsoniterVersion}}", + "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "{{jsoniterVersion}}" % "compile-internal", + "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-circe" % "{{jsoniterVersion}}" +) + +scalacOptions := Seq( + "-unchecked", + "-deprecation", + "-feature" +) diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/javadoc.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/javadoc.mustache new file mode 100644 index 000000000000..6acaca14ac3a --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/javadoc.mustache @@ -0,0 +1,25 @@ +{{#notes}} +{{{.}}} + +{{/notes}} +Expected answers: +{{#responses}} + code {{code}} : {{{dataType}}} {{#message}}({{{.}}}){{/message}} + {{#headers}} + {{#-first}} + Headers : + {{/-first}} + {{{baseName}}} - {{{description}}} + {{/headers}} +{{/responses}} +{{#authMethods.0}} + +Available security schemes: +{{#authMethods}} + {{name}} ({{type}}) +{{/authMethods}} +{{/authMethods.0}} + +{{#allParams}} +@param {{{paramName}}} {{{description}}} +{{/allParams}} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/jsonSupport.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/jsonSupport.mustache new file mode 100644 index 000000000000..d8f929c43225 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/jsonSupport.mustache @@ -0,0 +1,59 @@ +{{>licenseInfo}} +package {{invokerPackage}} + +{{#models.0}} +import {{modelPackage}}._ +{{/models.0}} +{{#json4s}} +import org.json4s._ +import sttp.client4.json4s.SttpJson4sApi +import scala.reflect.ClassTag + +object JsonSupport extends SttpJson4sApi { + def enumSerializers: Seq[Serializer[_]] = Seq[Serializer[_]](){{#models}}{{#model}}{{#isEnum}} :+ + new EnumNameSerializer({{classname}}){{/isEnum}}{{#hasEnums}}{{#vars}}{{#isEnum}} :+ + new EnumNameSerializer({{classname}}Enums.{{datatypeWithEnum}}){{/isEnum}}{{/vars}}{{/hasEnums}}{{/model}}{{/models}} + + private class EnumNameSerializer[E <: Enumeration: ClassTag](enumeration: E) extends Serializer[E#Value] { + import JsonDSL._ + val EnumerationClass: Class[E#Value] = classOf[E#Value] + + def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), E#Value] = { + case (t @ TypeInfo(EnumerationClass, _), json) if isValid(json) => + json match { + case JString(value) => enumeration.withName(value) + case value => throw new MappingException(s"Can't convert $value to $EnumerationClass") + } + } + + private[this] def isValid(json: JValue) = json match { + case JString(value) if enumeration.values.exists(_.toString == value) => true + case _ => false + } + + def serialize(implicit format: Formats): PartialFunction[Any, JValue] = { + case i: E#Value => i.toString + } + } + + implicit val format: Formats = DefaultFormats ++ enumSerializers ++ DateSerializers.all ++ AdditionalTypeSerializers.all + implicit val serialization: org.json4s.Serialization = org.json4s.jackson.Serialization +} +{{/json4s}} +{{#circe}} +import io.circe.{Decoder, Encoder} +import io.circe.generic.AutoDerivation +import sttp.client3.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}} +} +{{/circe}} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/licenseInfo.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/licenseInfo.mustache new file mode 100644 index 000000000000..16ba21d203d0 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/licenseInfo.mustache @@ -0,0 +1,11 @@ +/** + * {{{appName}}} + * {{{appDescription}}} + * + * {{#version}}The version of the OpenAPI document: {{{.}}}{{/version}} + * {{#infoEmail}}Contact: {{{.}}}{{/infoEmail}} + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/methodParameters.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/methodParameters.mustache new file mode 100644 index 000000000000..8d056a18a4c9 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/methodParameters.mustache @@ -0,0 +1 @@ +{{#authMethods.0}}{{#authMethods}}{{#isApiKey}}apiKey: String{{/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}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache new file mode 100644 index 000000000000..ecece27a59a5 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache @@ -0,0 +1,60 @@ +{{>licenseInfo}} +package {{package}} + +{{#imports}} +import {{import}} +{{/imports}} + +{{#models}} +{{#model}} +{{#description}} +{{#javadocRenderer}} +{{#title}} +{{{.}}} +{{/title}} +{{{description}}} +{{/javadocRenderer}} +{{/description}} +{{^isEnum}} +case class {{classname}}( + {{#vars}} + {{#description}} + /* {{{.}}} */ + {{/description}} + {{{name}}}: {{^required}}Option[{{/required}}{{^isEnum}}{{dataType}}{{/isEnum}}{{#isEnum}}{{^isArray}}{{classname}}Enums.{{datatypeWithEnum}}{{/isArray}}{{#isArray}}Seq[{{classname}}Enums.{{datatypeWithEnum}}]{{/isArray}}{{/isEnum}}{{^required}}] = None{{/required}}{{^-last}},{{/-last}} + {{/vars}} +) +{{/isEnum}} + +{{#isEnum}} +object {{classname}} extends Enumeration { + type {{classname}} = {{classname}}.Value +{{#allowableValues}} + {{#values}} + val {{#fnEnumEntry}}{{.}}{{/fnEnumEntry}} = Value("{{.}}") + {{/values}} +{{/allowableValues}} +} +{{/isEnum}} +{{#hasEnums}} +object {{classname}}Enums { + + {{#vars}} + {{#isEnum}} + type {{datatypeWithEnum}} = {{datatypeWithEnum}}.Value + {{/isEnum}} + {{/vars}} + {{#vars}} + {{#isEnum}} + object {{datatypeWithEnum}} extends Enumeration { +{{#_enum}} + val {{#fnEnumEntry}}{{.}}{{/fnEnumEntry}} = Value("{{.}}") +{{/_enum}} + } + + {{/isEnum}} + {{/vars}} +} +{{/hasEnums}} +{{/model}} +{{/models}} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/operationReturnType.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/operationReturnType.mustache new file mode 100644 index 000000000000..170935cad639 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/operationReturnType.mustache @@ -0,0 +1 @@ +{{{returnType}}}{{^returnType}}Unit{{/returnType}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramCreation.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramCreation.mustache new file mode 100644 index 000000000000..25ec73e8d5e1 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramCreation.mustache @@ -0,0 +1 @@ +"{{baseName}}", {{#isContainer}}ArrayValues({{{paramName}}}{{#collectionFormat}}, {{collectionFormat.toUpperCase}}{{/collectionFormat}}){{/isContainer}}{{^isContainer}}{{{paramName}}}{{/isContainer}}.toString \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramFormCreation.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramFormCreation.mustache new file mode 100644 index 000000000000..a0f1a65c0746 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramFormCreation.mustache @@ -0,0 +1 @@ +"{{baseName}}" -> {{#isContainer}}ArrayValues({{{paramName}}}{{#collectionFormat}}, {{collectionFormat.toUpperCase}}{{/collectionFormat}}){{/isContainer}}{{^isContainer}}{{{paramName}}}{{/isContainer}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramMultipartCreation.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramMultipartCreation.mustache new file mode 100644 index 000000000000..edfb7b0766b3 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramMultipartCreation.mustache @@ -0,0 +1,16 @@ +{{#required}} + {{#isFile}} + multipartFile("{{baseName}}", {{#isContainer}}ArrayValues({{{paramName}}}{{#collectionFormat}}, {{collectionFormat.toUpperCase}}{{/collectionFormat}}){{/isContainer}}{{^isContainer}}{{{paramName}}}{{/isContainer}}) + {{/isFile}} + {{^isFile}} + multipart("{{baseName}}", {{paramName}}) + {{/isFile}} +{{/required}} +{{^required}} + {{#isFile}} + {{paramName}}.map(multipartFile("{{baseName}}", _)) + {{/isFile}} + {{^isFile}} + {{paramName}}.map(multipart("{{baseName}}", {{#isContainer}}ArrayValues(_{{#collectionFormat}}, {{collectionFormat.toUpperCase}}{{/collectionFormat}}){{/isContainer}}{{^isContainer}}_{{/isContainer}})) + {{/isFile}} +{{/required}} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/project/build.properties.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/project/build.properties.mustache new file mode 100644 index 000000000000..04267b14af69 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/project/build.properties.mustache @@ -0,0 +1 @@ +sbt.version=1.9.9 diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/responseState.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/responseState.mustache new file mode 100644 index 000000000000..d1b3798e6de1 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/responseState.mustache @@ -0,0 +1 @@ +{{#isDefault}}Success{{/isDefault}}{{^isDefault}}Error{{/isDefault}} \ No newline at end of file From 296d0714ef19a8d1984646e0c5f506eeb087b36b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Bia=C5=82y?= Date: Thu, 31 Oct 2024 18:32:12 +0100 Subject: [PATCH 02/14] seems to work --- .../ScalaSttp4JsoniterClientCodegen.java | 155 +++++++++++++----- .../org.openapitools.codegen.CodegenConfig | 1 + .../additionalTypeSerializers.mustache | 16 +- .../scala-sttp4-jsoniter/api.mustache | 16 +- .../scala-sttp4-jsoniter/build.sbt.mustache | 14 +- .../scala-sttp4-jsoniter/jsonSupport.mustache | 61 +------ .../scala-sttp4-jsoniter/model.mustache | 82 +++++++-- .../paramMultipartCreation.mustache | 8 +- 8 files changed, 223 insertions(+), 130 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java index 2314767ee3d3..7add3ed0bbb9 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.servers.Server; +import lombok.Getter; import org.apache.commons.lang3.StringUtils; import org.openapitools.codegen.*; import org.openapitools.codegen.meta.GeneratorMetadata; @@ -32,23 +33,20 @@ public class ScalaSttp4JsoniterClientCodegen extends AbstractScalaCodegen implem "F[Either[ResponseError[ErrorType], ReturnType]]] or to flatten " + "response's error raising them through enclosing monad (F[ReturnType]).", true); - private static final StringProperty JODA_TIME_VERSION = new StringProperty("jodaTimeVersion", "The version of " + - "joda-time library", "2.10.13"); private static final StringProperty JSONITER_VERSION = new StringProperty("jsoniterVersion", "The version of jsoniter-scala " + "library", "2.31.1"); - private static final JsonLibraryProperty JSON_LIBRARY_PROPERTY = new JsonLibraryProperty(); - public static final String DEFAULT_PACKAGE_NAME = "org.openapitools.client"; private static final PackageProperty PACKAGE_PROPERTY = new PackageProperty(); private static final List> properties = Arrays.asList( - STTP_CLIENT_VERSION, USE_SEPARATE_ERROR_CHANNEL, JODA_TIME_VERSION, - JSONITER_VERSION, JSON_LIBRARY_PROPERTY, PACKAGE_PROPERTY); + STTP_CLIENT_VERSION, USE_SEPARATE_ERROR_CHANNEL, JSONITER_VERSION, PACKAGE_PROPERTY); + + private static final Set NO_JSON_CODEC_TYPES = new HashSet<>(Arrays.asList("UUID", "URI", "URL", "File", "Path")); - private final Logger LOGGER = LoggerFactory.getLogger(ScalaSttp4ClientCodegen.class); + private final Logger LOGGER = LoggerFactory.getLogger(ScalaSttp4JsoniterClientCodegen.class); protected String groupId = "org.openapitools"; protected String artifactId = "openapi-client"; @@ -57,6 +55,8 @@ public class ScalaSttp4JsoniterClientCodegen extends AbstractScalaCodegen implem protected boolean renderJavadoc = true; protected boolean removeOAuthSecurities = true; + protected Map jsonCodecNeedingTypes = new HashMap<>(); + Map enumRefs = new HashMap<>(); public ScalaSttp4JsoniterClientCodegen() { @@ -90,19 +90,20 @@ public ScalaSttp4JsoniterClientCodegen() { apiTemplateFiles.put("api.mustache", ".scala"); embeddedTemplateDir = templateDir = "scala-sttp4-jsoniter"; - String jsonLibrary = JSON_LIBRARY_PROPERTY.getValue(additionalProperties); - String jsonValueClass = "io.circe.Json"; additionalProperties.put(CodegenConstants.GROUP_ID, groupId); additionalProperties.put(CodegenConstants.ARTIFACT_ID, artifactId); additionalProperties.put(CodegenConstants.ARTIFACT_VERSION, artifactVersion); + additionalProperties.put("jsonCodecNeedingTypes", jsonCodecNeedingTypes.entrySet()); if (renderJavadoc) { additionalProperties.put("javadocRenderer", new JavadocLambda()); } additionalProperties.put("fnCapitalize", new CapitalizeLambda()); additionalProperties.put("fnCamelize", new CamelizeLambda(false)); additionalProperties.put("fnEnumEntry", new EnumEntryLambda()); + additionalProperties.put("fnCodecName", new CodecNameLambda()); + additionalProperties.put("fnHandleDownload", new HandleDownloadLambda()); // importMapping.remove("Seq"); // importMapping.remove("List"); @@ -156,8 +157,6 @@ public void processOpts() { supportingFiles.add(new SupportingFile("additionalTypeSerializers.mustache", invokerFolder, "AdditionalTypeSerializers.scala")); supportingFiles.add(new SupportingFile("project/build.properties.mustache", "project", "build.properties")); - // supportingFiles.add(new SupportingFile("dateSerializers.mustache", - // invokerFolder, "DateSerializers.scala")); } @Override @@ -173,7 +172,6 @@ public String getHelp() { @Override public String encodePath(String input) { String path = super.encodePath(input); - // The parameter names in the URI must be converted to the same case as // the method parameter. StringBuffer buf = new StringBuffer(path.length()); @@ -185,13 +183,55 @@ public String encodePath(String input) { return buf.toString(); } + private PathMetadata encPath(String input) { + String path = super.encodePath(input); + ArrayList pathParams = new ArrayList<>(); + + // The parameter names in the URI must be converted to the same case as + // the method parameter. + StringBuffer buf = new StringBuffer(path.length()); + Matcher matcher = Pattern.compile("[{](.*?)[}]").matcher(path); + while (matcher.find()) { + matcher.appendReplacement(buf, "\\${" + toParamName(matcher.group(0)) + "}"); + pathParams.add(matcher.group(0)); + } + matcher.appendTail(buf); + return new PathMetadata(buf.toString(), pathParams); + } + @Override public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, List servers) { CodegenOperation op = super.fromOperation(path, httpMethod, operation, servers); - op.path = encodePath(path); + + PathMetadata pathMetadata = encPath(path); + + op.path = pathMetadata.getPath(); + + for (String pathParam : pathMetadata.getPathParams()) { + CodegenParameter param = new CodegenParameter(); + param.isPathParam = true; + param.baseName = pathParam; + param.paramName = toParamName(pathParam); + param.dataType = "String"; + param.required = true; + + boolean alreadyExists = false; + for (CodegenParameter existingParam : op.pathParams) { + if (existingParam.baseName.equals(param.baseName) || existingParam.paramName.equals(param.paramName)) { + alreadyExists = true; + break; + } + } + + if (!alreadyExists) { + op.pathParams.add(param); + op.allParams.add(param); + } + } + return op; } @@ -310,6 +350,30 @@ private boolean isEnumClass(final String importPath, final Map allModels) { + OperationMap ops = objs.getOperations(); + +// allModels.forEach(model -> { +// if (model.getModel().name.equals("PluginStatus")) { +// System.out.println("Found plugin status model"); +// System.out.println(model.getModel()); +// } +// }); + + for (CodegenOperation operation : ops.getOperation()) { + if (operation.returnType != null && !NO_JSON_CODEC_TYPES.contains(operation.returnType)) { + String identifier = formatIdentifier(operation.returnType, false) + "Codec"; + String type = operation.returnType; + jsonCodecNeedingTypes.put(identifier, type); + } + + if (operation.bodyParam != null && !NO_JSON_CODEC_TYPES.contains(operation.bodyParam.dataType)) { + String identifier = formatIdentifier(operation.bodyParam.dataType, false) + "Codec"; + String type = operation.bodyParam.dataType; + + jsonCodecNeedingTypes.put(identifier, type); + } + } + if (registerNonStandardStatusCodes) { try { OperationMap opsMap = objs.getOperations(); @@ -385,7 +449,14 @@ public String toParamName(String name) { @Override public String toEnumName(CodegenProperty property) { - return formatIdentifier(property.baseName, true); + String identifier = formatIdentifier(property.baseName, true); + + // remove backticks because there are no capitalized reserved words in Scala + if (identifier.startsWith("`") && identifier.endsWith("`")) { + return identifier.substring(1, identifier.length() - 1); + } else { + return identifier; + } } @Override @@ -511,29 +582,6 @@ public Boolean getValue(Map additionalProperties) { } } - public static class JsonLibraryProperty extends StringProperty { - private static final String JSON4S = "json4s"; - private static final String CIRCE = "circe"; - - public JsonLibraryProperty() { - super("jsonLibrary", "Json library to use. Possible values are: json4s and circe.", JSON4S); - } - - @Override - public void updateAdditionalProperties(Map additionalProperties) { - String value = getValue(additionalProperties); - if (CIRCE.equals(value) || JSON4S.equals(value)) { - additionalProperties.put(CIRCE, CIRCE.equals(value)); - additionalProperties.put(JSON4S, JSON4S.equals(value)); - } else { - IllegalArgumentException exception = new IllegalArgumentException( - "Invalid json library: " + value + ". Must be " + CIRCE + " " + - "or " + JSON4S); - throw exception; - } - } - } - public static class PackageProperty extends StringProperty { public PackageProperty() { @@ -598,8 +646,41 @@ public String formatFragment(String fragment) { private class EnumEntryLambda extends CustomLambda { @Override public String formatFragment(String fragment) { + if (fragment.isBlank()) { + return "NotPresent"; + } return formatIdentifier(fragment, true); } } + private class CodecNameLambda extends CustomLambda { + @Override + public String formatFragment(String fragment) { + // remove backticks because this is used as prefix for Codec generation + return formatIdentifier(fragment, false).replace("`", "") + "Codec"; + } + } + + private static class HandleDownloadLambda extends CustomLambda { + @Override + public String formatFragment(String fragment) { + if (fragment.equals("asJson[File]")) { + return "asFile(File.createTempFile(\"download\", \".tmp\")).mapLeft(errStr => DeserializationException(errStr, new Exception(errStr)))"; + } else { + return fragment; + } + } + } + + @Getter + private static class PathMetadata { + private final String path; + private final ArrayList pathParams; + + PathMetadata(String path, ArrayList pathParams) { + this.path = path; + this.pathParams = pathParams; + } + } + } diff --git a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig index 2f8c6c6074c2..50295eac842f 100644 --- a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig +++ b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig @@ -131,6 +131,7 @@ org.openapitools.codegen.languages.ScalaLagomServerCodegen org.openapitools.codegen.languages.ScalaPlayFrameworkServerCodegen org.openapitools.codegen.languages.ScalaSttpClientCodegen org.openapitools.codegen.languages.ScalaSttp4ClientCodegen +org.openapitools.codegen.languages.ScalaSttp4JsoniterClientCodegen org.openapitools.codegen.languages.ScalazClientCodegen org.openapitools.codegen.languages.SpringCodegen org.openapitools.codegen.languages.StaticDocCodegen diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/additionalTypeSerializers.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/additionalTypeSerializers.mustache index 62d36137c151..8ac0f80e9ddf 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/additionalTypeSerializers.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/additionalTypeSerializers.mustache @@ -1,15 +1,13 @@ package {{invokerPackage}} import java.net.{ URI, URISyntaxException } +import com.github.plokhotnyuk.jsoniter_scala.core.* -{{#circe}} trait AdditionalTypeSerializers { - import com.github.plokhotnyuk.jsoniter_scala._ - implicit final lazy val URICodec: JsonValueCodec[URI] = new JsonValueCodec[URI] { - def nullValue: URI = null - def decodeValue(in: JsonReader, default: URI): URI = ??? - def encodeValue(uri: URI, out: JsonWriter): Unit = ??? - } -} -{{/circe}} + implicit final lazy val URICodec: JsonValueCodec[URI] = new JsonValueCodec[URI] { + def nullValue: URI = null + def decodeValue(in: JsonReader, default: URI): URI = ??? + def encodeValue(uri: URI, out: JsonWriter): Unit = ??? + } +} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache index 1635d5bd1c15..8c56dd84f1a6 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache @@ -4,8 +4,9 @@ package {{package}} {{#imports}} import {{import}} {{/imports}} -import {{invokerPackage}}.JsonSupport._ -import sttp.client4._ +import {{invokerPackage}}.JsonSupport.{*, given} +import sttp.client4.jsoniter.* +import sttp.client4.* import sttp.model.Method {{#operations}} @@ -16,6 +17,7 @@ object {{classname}} { class {{classname}}(baseUrl: String) { {{#operation}} + {{#javadocRenderer}} {{>javadoc}} {{/javadocRenderer}} @@ -30,13 +32,17 @@ class {{classname}}(baseUrl: String) { .cookie("{{keyParamName}}", apiKey){{/isKeyInCookie}}{{/isApiKey}}{{/authMethods}}{{#formParams.0}}{{^isMultipart}} .body(Map({{#formParams}} {{>paramFormCreation}}{{^-last}},{{/-last}}{{/formParams}} - )){{/isMultipart}}{{#isMultipart}} + ).collect { + case (key, Some(value)) => key -> value.toString + case (key, value) if value != None => key -> value.toString + }){{/isMultipart}}{{#isMultipart}} .multipartBody(Seq({{#formParams}} - {{>paramMultipartCreation}}{{^-last}}, {{/-last}}{{/formParams}} + {{>paramMultipartCreation}}{{/formParams}} ).flatten){{/isMultipart}}{{/formParams.0}}{{#bodyParam}} .body({{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}}) + .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}}) {{/operation}} + } {{/operations}} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/build.sbt.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/build.sbt.mustache index 2e73e53523aa..c6a722edbfd5 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/build.sbt.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/build.sbt.mustache @@ -3,17 +3,13 @@ name := "{{artifactId}}" organization := "{{groupId}}" scalaVersion := "3.3.4" -crossScalaVersions := Seq(scalaVersion.value, "2.13.15") libraryDependencies ++= Seq( - "com.softwaremill.sttp.client4" %% "core" % "{{sttpClientVersion}}", - "com.softwaremill.sttp.client4" %% "jsoniter" % "{{sttpClientVersion}}", -{{#joda}} - "joda-time" % "joda-time" % "{{jodaTimeVersion}}", -{{/joda}} - "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "{{jsoniterVersion}}", + "com.softwaremill.sttp.client4" %% "core" % "{{sttpClientVersion}}", + "com.softwaremill.sttp.client4" %% "jsoniter" % "{{sttpClientVersion}}", + "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "{{jsoniterVersion}}", "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "{{jsoniterVersion}}" % "compile-internal", - "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-circe" % "{{jsoniterVersion}}" + "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-circe" % "{{jsoniterVersion}}" ) scalacOptions := Seq( @@ -21,3 +17,5 @@ scalacOptions := Seq( "-deprecation", "-feature" ) + +// REBUILT \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/jsonSupport.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/jsonSupport.mustache index d8f929c43225..d9b7be35ab47 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/jsonSupport.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/jsonSupport.mustache @@ -2,58 +2,15 @@ package {{invokerPackage}} {{#models.0}} -import {{modelPackage}}._ +import {{modelPackage}}.* {{/models.0}} -{{#json4s}} -import org.json4s._ -import sttp.client4.json4s.SttpJson4sApi -import scala.reflect.ClassTag - -object JsonSupport extends SttpJson4sApi { - def enumSerializers: Seq[Serializer[_]] = Seq[Serializer[_]](){{#models}}{{#model}}{{#isEnum}} :+ - new EnumNameSerializer({{classname}}){{/isEnum}}{{#hasEnums}}{{#vars}}{{#isEnum}} :+ - new EnumNameSerializer({{classname}}Enums.{{datatypeWithEnum}}){{/isEnum}}{{/vars}}{{/hasEnums}}{{/model}}{{/models}} - - private class EnumNameSerializer[E <: Enumeration: ClassTag](enumeration: E) extends Serializer[E#Value] { - import JsonDSL._ - val EnumerationClass: Class[E#Value] = classOf[E#Value] - - def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), E#Value] = { - case (t @ TypeInfo(EnumerationClass, _), json) if isValid(json) => - json match { - case JString(value) => enumeration.withName(value) - case value => throw new MappingException(s"Can't convert $value to $EnumerationClass") - } - } - - private[this] def isValid(json: JValue) = json match { - case JString(value) if enumeration.values.exists(_.toString == value) => true - case _ => false - } - - def serialize(implicit format: Formats): PartialFunction[Any, JValue] = { - case i: E#Value => i.toString - } - } - - implicit val format: Formats = DefaultFormats ++ enumSerializers ++ DateSerializers.all ++ AdditionalTypeSerializers.all - implicit val serialization: org.json4s.Serialization = org.json4s.jackson.Serialization +import com.github.plokhotnyuk.jsoniter_scala.macros.* +import com.github.plokhotnyuk.jsoniter_scala.core.* +import com.github.plokhotnyuk.jsoniter_scala.circe.JsoniterScalaCodec.* + +object JsonSupport extends AdditionalTypeSerializers { +{{#jsonCodecNeedingTypes}} + given {{key}}: JsonValueCodec[{{value}}] = JsonCodecMaker.make +{{/jsonCodecNeedingTypes}} } -{{/json4s}} -{{#circe}} -import io.circe.{Decoder, Encoder} -import io.circe.generic.AutoDerivation -import sttp.client3.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}} -} -{{/circe}} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache index ecece27a59a5..8603701da9a2 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache @@ -27,33 +27,85 @@ case class {{classname}}( {{/isEnum}} {{#isEnum}} -object {{classname}} extends Enumeration { - type {{classname}} = {{classname}}.Value +enum {{classname}} { {{#allowableValues}} - {{#values}} - val {{#fnEnumEntry}}{{.}}{{/fnEnumEntry}} = Value("{{.}}") - {{/values}} + {{#values}} + case {{#fnEnumEntry}}{{.}}{{/fnEnumEntry}} + {{/values}} {{/allowableValues}} } +object {{classname}} { + import com.github.plokhotnyuk.jsoniter_scala.macros.* + import com.github.plokhotnyuk.jsoniter_scala.core.* + {{#isString}} + given CodecMakerConfig = CodecMakerConfig + .withAdtLeafClassNameMapper(x => JsonCodecMaker.simpleClassName(x) match { + {{#values}} + case "{{#fnEnumEntry}}{{.}}{{/fnEnumEntry}}" => "{{.}}" + {{/values}} + }).withDiscriminatorFieldName(None) + + given {{#fnCodecName}}{{datatypeWithEnum}}{{/fnCodecName}}: JsonValueCodec[{{datatypeWithEnum}}] = JsonCodecMaker.make + {{/isString}} + {{#isNumber}} + given {{#fnCodecName}}{{datatypeWithEnum}}{{/fnCodecName}}: JsonValueCodec[{{datatypeWithEnum}}] = new JsonValueCodec[{{datatypeWithEnum}}] { + import scala.util.{Try, Success, Failure} + + override val nullValue: {{datatypeWithEnum}} = null + + override def decodeValue(in: JsonReader, default: {{datatypeWithEnum}}): {{datatypeWithEnum}} = + val x = in.readByte() + Try { {{datatypeWithEnum}}.fromOrdinal(x) } match { + case Success(v) => v + case Failure(_) => in.decodeError(s"unexpected number value: $x") + } + override def encodeValue(x: {{datatypeWithEnum}}, out: JsonWriter): Unit = out.writeVal(x.ordinal) + } + {{/isNumber}} +} {{/isEnum}} {{#hasEnums}} object {{classname}}Enums { - {{#vars}} {{#isEnum}} - type {{datatypeWithEnum}} = {{datatypeWithEnum}}.Value - {{/isEnum}} - {{/vars}} - {{#vars}} - {{#isEnum}} - object {{datatypeWithEnum}} extends Enumeration { -{{#_enum}} - val {{#fnEnumEntry}}{{.}}{{/fnEnumEntry}} = Value("{{.}}") -{{/_enum}} + enum {{datatypeWithEnum}} { + {{#_enum}} + case {{#fnEnumEntry}}{{.}}{{/fnEnumEntry}} + {{/_enum}} } + object {{datatypeWithEnum}} { + import com.github.plokhotnyuk.jsoniter_scala.macros.* + import com.github.plokhotnyuk.jsoniter_scala.core.* + {{#isString}} + given CodecMakerConfig = CodecMakerConfig + .withAdtLeafClassNameMapper(x => JsonCodecMaker.simpleClassName(x) match { + {{#_enum}} + case "{{#fnEnumEntry}}{{.}}{{/fnEnumEntry}}" => "{{.}}" + {{/_enum}} + }).withDiscriminatorFieldName(None) + + given {{#fnCodecName}}{{datatypeWithEnum}}{{/fnCodecName}}: JsonValueCodec[{{datatypeWithEnum}}] = JsonCodecMaker.make + {{/isString}} + {{#isNumber}} + given {{#fnCodecName}}{{datatypeWithEnum}}{{/fnCodecName}}: JsonValueCodec[{{datatypeWithEnum}}] = new JsonValueCodec[{{datatypeWithEnum}}] { + import scala.util.{Try, Success, Failure} + + override val nullValue: {{datatypeWithEnum}} = null + override def decodeValue(in: JsonReader, default: {{datatypeWithEnum}}): {{datatypeWithEnum}} = + val x = in.readByte() + Try { {{datatypeWithEnum}}.fromOrdinal(x) } match { + case Success(v) => v + case Failure(_) => in.decodeError(s"unexpected number value: $x") + } + override def encodeValue(x: {{datatypeWithEnum}}, out: JsonWriter): Unit = out.writeVal(x.ordinal) + } + {{/isNumber}} + + } {{/isEnum}} {{/vars}} + } {{/hasEnums}} {{/model}} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramMultipartCreation.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramMultipartCreation.mustache index edfb7b0766b3..da8a9736b386 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramMultipartCreation.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramMultipartCreation.mustache @@ -1,16 +1,16 @@ {{#required}} {{#isFile}} - multipartFile("{{baseName}}", {{#isContainer}}ArrayValues({{{paramName}}}{{#collectionFormat}}, {{collectionFormat.toUpperCase}}{{/collectionFormat}}){{/isContainer}}{{^isContainer}}{{{paramName}}}{{/isContainer}}) + Some(multipartFile("{{baseName}}", {{#isContainer}}ArrayValues({{{paramName}}}{{#collectionFormat}}, {{collectionFormat.toUpperCase}}{{/collectionFormat}}){{/isContainer}}{{^isContainer}}{{{paramName}}}{{/isContainer}})){{^-last}},{{/-last}} {{/isFile}} {{^isFile}} - multipart("{{baseName}}", {{paramName}}) + Some({{paramName}}).map(_.toString).map(multipart("{{baseName}}", _)){{^-last}},{{/-last}} {{/isFile}} {{/required}} {{^required}} {{#isFile}} - {{paramName}}.map(multipartFile("{{baseName}}", _)) + {{paramName}}.map(multipartFile("{{baseName}}", _)){{^-last}},{{/-last}} {{/isFile}} {{^isFile}} - {{paramName}}.map(multipart("{{baseName}}", {{#isContainer}}ArrayValues(_{{#collectionFormat}}, {{collectionFormat.toUpperCase}}{{/collectionFormat}}){{/isContainer}}{{^isContainer}}_{{/isContainer}})) + {{paramName}}{{^isContainer}}.map(_.toString){{/isContainer}}.map(multipart("{{baseName}}", {{#isContainer}}ArrayValues(_{{#collectionFormat}}, {{collectionFormat.toUpperCase}}{{/collectionFormat}}){{/isContainer}}{{^isContainer}}_{{/isContainer}})){{^-last}},{{/-last}} {{/isFile}} {{/required}} From eebac6d4c93141b44cb1581139feca3d59be8236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Bia=C5=82y?= Date: Thu, 31 Oct 2024 18:41:05 +0100 Subject: [PATCH 03/14] generated docs --- docs/generators/scala-sttp4-jsoniter.md | 251 ++++++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 docs/generators/scala-sttp4-jsoniter.md diff --git a/docs/generators/scala-sttp4-jsoniter.md b/docs/generators/scala-sttp4-jsoniter.md new file mode 100644 index 000000000000..ba89929bc807 --- /dev/null +++ b/docs/generators/scala-sttp4-jsoniter.md @@ -0,0 +1,251 @@ +--- +title: Documentation for the scala-sttp4-jsoniter Generator +--- + +## METADATA + +| Property | Value | Notes | +| -------- | ----- | ----- | +| generator name | scala-sttp4-jsoniter | pass this to the generate command after -g | +| generator stability | BETA | | +| generator type | CLIENT | | +| generator language | Scala | | +| generator default templating engine | mustache | | +| helpTxt | Generates a Scala client library (beta) based on Sttp4 and Jsoniter-Scala. | | + +## CONFIG OPTIONS +These options may be applied as additional-properties (cli) or configOptions (plugins). Refer to [configuration docs](https://openapi-generator.tech/docs/configuration) for more details. + +| Option | Description | Values | Default | +| ------ | ----------- | ------ | ------- | +|allowUnicodeIdentifiers|boolean, toggles whether unicode identifiers are allowed in names or not, default is false| |false| +|apiPackage|package for generated api classes| |null| +|dateLibrary|Option. Date library to use|

**joda**
Joda (for legacy app)
**java8**
Java 8 native JSR310 (preferred for JDK 1.8+)
|java8| +|disallowAdditionalPropertiesIfNotPresent|If false, the 'additionalProperties' implementation (set to true by default) is compliant with the OAS and JSON schema specifications. If true (default), keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default.|
**false**
The 'additionalProperties' implementation is compliant with the OAS and JSON schema specifications.
**true**
Keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default.
|true| +|ensureUniqueParams|Whether to ensure parameter names are unique in an operation (rename parameters that are not).| |true| +|enumUnknownDefaultCase|If the server adds new enum cases, that are unknown by an old spec/client, the client will fail to parse the network response.With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the server sends an enum case that is not known by the client/spec, they can safely fallback to this case.|
**false**
No changes to the enum's are made, this is the default option.
**true**
With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the enum case sent by the server is not known by the client/spec, can safely be decoded to this case.
|false| +|jsoniterVersion|The version of jsoniter-scala library| |2.31.1| +|legacyDiscriminatorBehavior|Set to false for generators with better support for discriminators. (Python, Java, Go, PowerShell, C# have this enabled by default).|
**true**
The mapping in the discriminator includes descendent schemas that allOf inherit from self and the discriminator mapping schemas in the OAS document.
**false**
The mapping in the discriminator includes any descendent schemas that allOf inherit from self, any oneOf schemas, any anyOf schemas, any x-discriminator-values, and the discriminator mapping schemas in the OAS document AND Codegen validates that oneOf and anyOf schemas contain the required discriminator and throws an error if the discriminator is missing.
|true| +|mainPackage|Top-level package name, which defines 'apiPackage', 'modelPackage', 'invokerPackage'| |org.openapitools.client| +|modelPackage|package for generated models| |null| +|modelPropertyNaming|Naming convention for the property: 'camelCase', 'PascalCase', 'snake_case' and 'original', which keeps the original name| |camelCase| +|prependFormOrBodyParameters|Add form or body parameters to the beginning of the parameter list.| |false| +|separateErrorChannel|Whether to return response as F[Either[ResponseError[ErrorType], ReturnType]]] or to flatten response's error raising them through enclosing monad (F[ReturnType]).| |true| +|sortModelPropertiesByRequiredFlag|Sort model properties to place required parameters before optional parameters.| |true| +|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true| +|sourceFolder|source folder for generated code| |null| +|sttpClientVersion|The version of sttp client| |4.0.0-M19| + +## IMPORT MAPPING + +| Type/Alias | Imports | +| ---------- | ------- | +|Array|java.util.List| +|ArrayList|java.util.ArrayList| +|Date|java.util.Date| +|DateTime|org.joda.time.*| +|File|java.io.File| +|HashMap|java.util.HashMap| +|ListBuffer|scala.collection.mutable.ListBuffer| +|ListSet|scala.collection.immutable.ListSet| +|LocalDate|org.joda.time.*| +|LocalDateTime|org.joda.time.*| +|LocalTime|org.joda.time.*| +|Seq|scala.collection.immutable.Seq| +|Set|scala.collection.immutable.Set| +|Timestamp|java.sql.Timestamp| +|URI|java.net.URI| +|UUID|java.util.UUID| + + +## INSTANTIATION TYPES + +| Type/Alias | Instantiated By | +| ---------- | --------------- | +|array|ListBuffer| +|map|Map| +|set|Set| + + +## LANGUAGE PRIMITIVES + +
    +
  • Any
  • +
  • Array
  • +
  • Boolean
  • +
  • Byte
  • +
  • Double
  • +
  • Float
  • +
  • Int
  • +
  • List
  • +
  • Long
  • +
  • Map
  • +
  • Object
  • +
  • Seq
  • +
  • String
  • +
  • boolean
  • +
+ +## RESERVED WORDS + +
    +
  • abstract
  • +
  • case
  • +
  • catch
  • +
  • class
  • +
  • clone
  • +
  • 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
  • +
  • true
  • +
  • try
  • +
  • type
  • +
  • val
  • +
  • var
  • +
  • while
  • +
  • with
  • +
  • yield
  • +
+ +## FEATURE SET + + +### Client Modification Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|BasePath|✓|ToolingExtension +|Authorizations|✗|ToolingExtension +|UserAgent|✓|ToolingExtension +|MockServer|✗|ToolingExtension + +### Data Type Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Custom|✗|OAS2,OAS3 +|Int32|✓|OAS2,OAS3 +|Int64|✓|OAS2,OAS3 +|Float|✓|OAS2,OAS3 +|Double|✓|OAS2,OAS3 +|Decimal|✓|ToolingExtension +|String|✓|OAS2,OAS3 +|Byte|✓|OAS2,OAS3 +|Binary|✓|OAS2,OAS3 +|Boolean|✓|OAS2,OAS3 +|Date|✓|OAS2,OAS3 +|DateTime|✓|OAS2,OAS3 +|Password|✓|OAS2,OAS3 +|File|✓|OAS2 +|Uuid|✗| +|Array|✓|OAS2,OAS3 +|Null|✗|OAS3 +|AnyType|✗|OAS2,OAS3 +|Object|✓|OAS2,OAS3 +|Maps|✓|ToolingExtension +|CollectionFormat|✓|OAS2 +|CollectionFormatMulti|✓|OAS2 +|Enum|✓|OAS2,OAS3 +|ArrayOfEnum|✓|ToolingExtension +|ArrayOfModel|✓|ToolingExtension +|ArrayOfCollectionOfPrimitives|✓|ToolingExtension +|ArrayOfCollectionOfModel|✓|ToolingExtension +|ArrayOfCollectionOfEnum|✓|ToolingExtension +|MapOfEnum|✓|ToolingExtension +|MapOfModel|✓|ToolingExtension +|MapOfCollectionOfPrimitives|✓|ToolingExtension +|MapOfCollectionOfModel|✓|ToolingExtension +|MapOfCollectionOfEnum|✓|ToolingExtension + +### Documentation Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Readme|✓|ToolingExtension +|Model|✓|ToolingExtension +|Api|✓|ToolingExtension + +### Global Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Host|✓|OAS2,OAS3 +|BasePath|✓|OAS2,OAS3 +|Info|✓|OAS2,OAS3 +|Schemes|✗|OAS2,OAS3 +|PartialSchemes|✓|OAS2,OAS3 +|Consumes|✓|OAS2 +|Produces|✓|OAS2 +|ExternalDocumentation|✓|OAS2,OAS3 +|Examples|✓|OAS2,OAS3 +|XMLStructureDefinitions|✗|OAS2,OAS3 +|MultiServer|✗|OAS3 +|ParameterizedServer|✗|OAS3 +|ParameterStyling|✗|OAS3 +|Callbacks|✗|OAS3 +|LinkObjects|✗|OAS3 + +### Parameter Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Path|✓|OAS2,OAS3 +|Query|✓|OAS2,OAS3 +|Header|✓|OAS2,OAS3 +|Body|✓|OAS2 +|FormUnencoded|✓|OAS2 +|FormMultipart|✓|OAS2 +|Cookie|✗|OAS3 + +### Schema Support Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Simple|✓|OAS2,OAS3 +|Composite|✓|OAS2,OAS3 +|Polymorphism|✗|OAS2,OAS3 +|Union|✗|OAS3 +|allOf|✗|OAS2,OAS3 +|anyOf|✗|OAS3 +|oneOf|✗|OAS3 +|not|✗|OAS3 + +### Security Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|BasicAuth|✓|OAS2,OAS3 +|ApiKey|✓|OAS2,OAS3 +|OpenIDConnect|✗|OAS3 +|BearerToken|✓|OAS3 +|OAuth2_Implicit|✗|OAS2,OAS3 +|OAuth2_Password|✗|OAS2,OAS3 +|OAuth2_ClientCredentials|✗|OAS2,OAS3 +|OAuth2_AuthorizationCode|✗|OAS2,OAS3 +|SignatureAuth|✗|OAS3 +|AWSV4Signature|✗|ToolingExtension + +### Wire Format Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|JSON|✓|OAS2,OAS3 +|XML|✓|OAS2,OAS3 +|PROTOBUF|✗|ToolingExtension +|Custom|✓|OAS2,OAS3 From ab448bbc1cdec3d2460210818faf4a13457648a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Bia=C5=82y?= Date: Fri, 1 Nov 2024 14:36:49 +0100 Subject: [PATCH 04/14] fix class name duplicates on case-insensitive filesystems --- .../ScalaSttp4JsoniterClientCodegen.java | 51 +++++++++++++++---- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java index 7add3ed0bbb9..e764854c5f2e 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java @@ -23,6 +23,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static org.openapitools.codegen.utils.StringUtils.camelize; + public class ScalaSttp4JsoniterClientCodegen extends AbstractScalaCodegen implements CodegenConfig { private static final StringProperty STTP_CLIENT_VERSION = new StringProperty("sttpClientVersion", "The version of " + @@ -59,6 +61,9 @@ public class ScalaSttp4JsoniterClientCodegen extends AbstractScalaCodegen implem Map enumRefs = new HashMap<>(); + private Map apiNameMappings = new HashMap<>(); + private Set uniqueApiNames = new HashSet<>(); + public ScalaSttp4JsoniterClientCodegen() { super(); generatorMetadata = GeneratorMetadata.newBuilder(generatorMetadata) @@ -183,7 +188,7 @@ public String encodePath(String input) { return buf.toString(); } - private PathMetadata encPath(String input) { + private PathMetadata parseAndEncodePath(String input) { String path = super.encodePath(input); ArrayList pathParams = new ArrayList<>(); @@ -206,7 +211,7 @@ public CodegenOperation fromOperation(String path, List servers) { CodegenOperation op = super.fromOperation(path, httpMethod, operation, servers); - PathMetadata pathMetadata = encPath(path); + PathMetadata pathMetadata = parseAndEncodePath(path); op.path = pathMetadata.getPath(); @@ -248,6 +253,41 @@ public String escapeReservedWord(String name) { return "`" + name + "`"; } + @Override + public String toApiName(String name) { + // first come, first served + // if a tag name is already mapped, use that mapping + if (apiNameMappings.containsKey(name)) { + return apiNameMappings.get(name); + } + + String generatedApiName = super.toApiName(name); + String lowerCasedApiName = generatedApiName.toLowerCase(Locale.ROOT); + + // check if the name is unique (case-insensitive) + // if it's unique, add it to the mappings and return the generated name + if (!uniqueApiNames.contains(lowerCasedApiName)) { + uniqueApiNames.add(lowerCasedApiName); + apiNameMappings.put(name, generatedApiName); + + return generatedApiName; + } else { + // if the name is not unique, generate a new name with a unique suffix + int i = 0; + while (true) { + String nextGeneratedApiName = super.toApiName(name + i); + String lowerCasedNextGeneratedApiName = nextGeneratedApiName.toLowerCase(Locale.ROOT); + if (!uniqueApiNames.contains(lowerCasedNextGeneratedApiName)) { + uniqueApiNames.add(lowerCasedNextGeneratedApiName); + apiNameMappings.put(name, nextGeneratedApiName); + + return nextGeneratedApiName; + } + i++; + } + } + } + @Override public ModelsMap postProcessModels(ModelsMap objs) { return objs; @@ -352,13 +392,6 @@ private boolean isEnumClass(final String importPath, final Map allModels) { OperationMap ops = objs.getOperations(); -// allModels.forEach(model -> { -// if (model.getModel().name.equals("PluginStatus")) { -// System.out.println("Found plugin status model"); -// System.out.println(model.getModel()); -// } -// }); - for (CodegenOperation operation : ops.getOperation()) { if (operation.returnType != null && !NO_JSON_CODEC_TYPES.contains(operation.returnType)) { String identifier = formatIdentifier(operation.returnType, false) + "Codec"; From c630432e81d77fe812f4645d0a4dd01827ece47d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Bia=C5=82y?= Date: Mon, 3 Feb 2025 17:12:11 +0100 Subject: [PATCH 05/14] wip --- .../languages/AbstractScalaCodegen.java | 11 +- .../ScalaSttp4JsoniterClientCodegen.java | 139 +++---- .../additionalTypeSerializers.mustache | 21 +- .../scala-sttp4-jsoniter/api.mustache | 20 +- .../scala-sttp4-jsoniter/build.sbt.mustache | 4 +- .../scala-sttp4-jsoniter/helpers.mustache | 65 +++ .../scala-sttp4-jsoniter/jsonSupport.mustache | 11 +- .../methodParameters.mustache | 2 +- .../scala-sttp4-jsoniter/model.mustache | 75 ++-- .../paramCreation.mustache | 2 +- .../paramFormCreation.mustache | 2 +- .../paramMultipartCreation.mustache | 4 +- rebuild.sh | 391 ++++++++++++++++++ 13 files changed, 604 insertions(+), 143 deletions(-) create mode 100644 modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/helpers.mustache create mode 100755 rebuild.sh diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractScalaCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractScalaCodegen.java index 2542da515088..fab855819242 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractScalaCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractScalaCodegen.java @@ -530,7 +530,16 @@ protected String formatIdentifier(String name, boolean capitalized) { if (identifier.matches("[a-zA-Z_$][\\w_$]+") && !isReservedWord(identifier)) { return identifier; } - return escapeReservedWord(identifier); + if (identifier.matches("[0-9]*")) { + return escapeReservedWord(identifier); + } + if (!capitalized || StringUtils.isNumeric(name)) { + // starts with a small letter, could be a keyword or a number + return escapeReservedWord(identifier); + } else { + // no keywords start with large letter + return identifier; + } } protected String stripPackageName(String input) { diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java index e764854c5f2e..ee957d69278f 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java @@ -4,7 +4,6 @@ import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.servers.Server; -import lombok.Getter; import org.apache.commons.lang3.StringUtils; import org.openapitools.codegen.*; import org.openapitools.codegen.meta.GeneratorMetadata; @@ -18,13 +17,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static org.openapitools.codegen.languages.AbstractJavaCodegen.DATE_LIBRARY; + import java.io.File; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; -import static org.openapitools.codegen.utils.StringUtils.camelize; - public class ScalaSttp4JsoniterClientCodegen extends AbstractScalaCodegen implements CodegenConfig { private static final StringProperty STTP_CLIENT_VERSION = new StringProperty("sttpClientVersion", "The version of " + @@ -46,7 +45,13 @@ public class ScalaSttp4JsoniterClientCodegen extends AbstractScalaCodegen implem private static final List> properties = Arrays.asList( STTP_CLIENT_VERSION, USE_SEPARATE_ERROR_CHANNEL, JSONITER_VERSION, PACKAGE_PROPERTY); - private static final Set NO_JSON_CODEC_TYPES = new HashSet<>(Arrays.asList("UUID", "URI", "URL", "File", "Path")); + private static final String jsonClassBaseName = "Json"; + private static final String jsonValueClass = "io.circe.Json"; + private static final String jsonAstCodecImport = "com.github.plokhotnyuk.jsoniter_scala.circe.JsoniterScalaCodec.*"; + + private static final Set NO_JSON_CODEC_TYPES = new HashSet<>(Arrays.asList( + "UUID", "URI", "URL", "File", "Path", jsonClassBaseName, jsonValueClass, "BigDecimal" + )); private final Logger LOGGER = LoggerFactory.getLogger(ScalaSttp4JsoniterClientCodegen.class); @@ -61,8 +66,8 @@ public class ScalaSttp4JsoniterClientCodegen extends AbstractScalaCodegen implem Map enumRefs = new HashMap<>(); - private Map apiNameMappings = new HashMap<>(); - private Set uniqueApiNames = new HashSet<>(); + private final Map apiNameMappings = new HashMap<>(); + private final Set uniqueApiNames = new HashSet<>(); public ScalaSttp4JsoniterClientCodegen() { super(); @@ -95,7 +100,11 @@ public ScalaSttp4JsoniterClientCodegen() { apiTemplateFiles.put("api.mustache", ".scala"); embeddedTemplateDir = templateDir = "scala-sttp4-jsoniter"; - String jsonValueClass = "io.circe.Json"; + // Scala 3 reserved words + reservedWords.addAll(Arrays.asList("enum", "export", "given", "then", "using", "Request", "Method", "Either")); + + importMapping.put(jsonValueClass, jsonAstCodecImport); + importMapping.put("BigDecimal", "scala.math.BigDecimal"); additionalProperties.put(CodegenConstants.GROUP_ID, groupId); additionalProperties.put(CodegenConstants.ARTIFACT_ID, artifactId); @@ -110,11 +119,6 @@ public ScalaSttp4JsoniterClientCodegen() { additionalProperties.put("fnCodecName", new CodecNameLambda()); additionalProperties.put("fnHandleDownload", new HandleDownloadLambda()); - // importMapping.remove("Seq"); - // importMapping.remove("List"); - // importMapping.remove("Set"); - // importMapping.remove("Map"); - // TODO: there is no specific sttp mapping. All Scala Type mappings should be in // AbstractScala typeMapping = new HashMap<>(); @@ -130,17 +134,22 @@ public ScalaSttp4JsoniterClientCodegen() { typeMapping.put("short", "Short"); typeMapping.put("char", "Char"); typeMapping.put("double", "Double"); - typeMapping.put("object", jsonValueClass); typeMapping.put("file", "File"); typeMapping.put("binary", "File"); typeMapping.put("number", "Double"); typeMapping.put("decimal", "BigDecimal"); typeMapping.put("ByteArray", "Array[Byte]"); + + // actually, these two *are* jsoniter+circe AST specific + typeMapping.put("object", jsonValueClass); typeMapping.put("AnyType", jsonValueClass); instantiationTypes.put("array", "ListBuffer"); instantiationTypes.put("map", "Map"); + // remove DATE_LIBRARY option, we don't need it + cliOptions.removeIf(option -> option.getOpt().equals(DATE_LIBRARY)); + properties.stream() .map(Property::toCliOptions) .flatMap(Collection::stream) @@ -161,6 +170,8 @@ public void processOpts() { supportingFiles.add(new SupportingFile("jsonSupport.mustache", invokerFolder, "JsonSupport.scala")); supportingFiles.add(new SupportingFile("additionalTypeSerializers.mustache", invokerFolder, "AdditionalTypeSerializers.scala")); + supportingFiles.add(new SupportingFile("helpers.mustache", invokerFolder, + "Helpers.scala")); supportingFiles.add(new SupportingFile("project/build.properties.mustache", "project", "build.properties")); } @@ -188,55 +199,13 @@ public String encodePath(String input) { return buf.toString(); } - private PathMetadata parseAndEncodePath(String input) { - String path = super.encodePath(input); - ArrayList pathParams = new ArrayList<>(); - - // The parameter names in the URI must be converted to the same case as - // the method parameter. - StringBuffer buf = new StringBuffer(path.length()); - Matcher matcher = Pattern.compile("[{](.*?)[}]").matcher(path); - while (matcher.find()) { - matcher.appendReplacement(buf, "\\${" + toParamName(matcher.group(0)) + "}"); - pathParams.add(matcher.group(0)); - } - matcher.appendTail(buf); - return new PathMetadata(buf.toString(), pathParams); - } - @Override public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, List servers) { CodegenOperation op = super.fromOperation(path, httpMethod, operation, servers); - - PathMetadata pathMetadata = parseAndEncodePath(path); - - op.path = pathMetadata.getPath(); - - for (String pathParam : pathMetadata.getPathParams()) { - CodegenParameter param = new CodegenParameter(); - param.isPathParam = true; - param.baseName = pathParam; - param.paramName = toParamName(pathParam); - param.dataType = "String"; - param.required = true; - - boolean alreadyExists = false; - for (CodegenParameter existingParam : op.pathParams) { - if (existingParam.baseName.equals(param.baseName) || existingParam.paramName.equals(param.paramName)) { - alreadyExists = true; - break; - } - } - - if (!alreadyExists) { - op.pathParams.add(param); - op.allParams.add(param); - } - } - + op.path = encodePath(path); return op; } @@ -280,7 +249,7 @@ public String toApiName(String name) { if (!uniqueApiNames.contains(lowerCasedNextGeneratedApiName)) { uniqueApiNames.add(lowerCasedNextGeneratedApiName); apiNameMappings.put(name, nextGeneratedApiName); - + return nextGeneratedApiName; } i++; @@ -338,13 +307,32 @@ private void postProcessUpdateImports(final Map models) { continue; } List> newImports = new ArrayList<>(); - Iterator> iterator = imports.iterator(); - while (iterator.hasNext()) { - String importPath = iterator.next().get("import"); + + boolean foundJsonImport = false; + + for (Map anImport : imports) { + String importPath = anImport.get("import"); + Map item = new HashMap<>(); + + // remove any imports for io.circe.Json, it's a FQCN + // but on the first encounter, add the import for the + // jsoniter-scala circe AST codec as it will be necessary + // for all places where io.circe.Json is used as request body + // or response body + if (importPath.contains(jsonValueClass)) { + if (!foundJsonImport) { + foundJsonImport = true; + item.put("import", jsonAstCodecImport); + newImports.add(item); + } + + continue; + } + if (importPath.startsWith(prefix)) { if (isEnumClass(importPath, enumRefs)) { - item.put("import", importPath.concat("._")); + item.put("import", importPath.concat(".*")); newImports.add(item); } } else { @@ -361,7 +349,7 @@ private Map getEnumRefs(final Map models) Map enums = new HashMap<>(); for (String key : models.keySet()) { CodegenModel model = ModelUtils.getModelByName(key, models); - if (model.isEnum) { + if (model != null && model.isEnum) { ModelsMap objs = models.get(key); enums.put(key, objs); } @@ -438,18 +426,18 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List> newImports = new ArrayList<>(); List> imports = objs.getImports(); if (imports != null && !imports.isEmpty()) { - Iterator> iterator = imports.iterator(); - while (iterator.hasNext()) { - String importPath = iterator.next().get("import"); + for (Map anImport : imports) { + String importPath = anImport.get("import"); Map item = new HashMap<>(); if (isEnumClass(importPath, enumRefs)) { - item.put("import", importPath.concat("._")); + item.put("import", importPath.concat(".*")); } else { item.put("import", importPath); } newImports.add(item); } } + objs.setImports(newImports); return super.postProcessOperationsWithModels(objs, allModels); @@ -484,8 +472,14 @@ public String toParamName(String name) { public String toEnumName(CodegenProperty property) { String identifier = formatIdentifier(property.baseName, true); - // remove backticks because there are no capitalized reserved words in Scala if (identifier.startsWith("`") && identifier.endsWith("`")) { + // is it numeric? + String unescaped = identifier.substring(1, identifier.length() - 1); + if (StringUtils.isNumeric(unescaped)) { + return identifier; // keep backticks + } + + // remove backticks because there are no capitalized reserved words in Scala return identifier.substring(1, identifier.length() - 1); } else { return identifier; @@ -705,15 +699,4 @@ public String formatFragment(String fragment) { } } - @Getter - private static class PathMetadata { - private final String path; - private final ArrayList pathParams; - - PathMetadata(String path, ArrayList pathParams) { - this.path = path; - this.pathParams = pathParams; - } - } - } diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/additionalTypeSerializers.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/additionalTypeSerializers.mustache index 8ac0f80e9ddf..12f01a86d87e 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/additionalTypeSerializers.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/additionalTypeSerializers.mustache @@ -1,13 +1,20 @@ package {{invokerPackage}} -import java.net.{ URI, URISyntaxException } +import java.net.{URI, URISyntaxException} import com.github.plokhotnyuk.jsoniter_scala.core.* -trait AdditionalTypeSerializers { +trait AdditionalTypeSerializers: - implicit final lazy val URICodec: JsonValueCodec[URI] = new JsonValueCodec[URI] { + implicit final lazy val URICodec: JsonValueCodec[URI] = new JsonValueCodec[URI]: def nullValue: URI = null - def decodeValue(in: JsonReader, default: URI): URI = ??? - def encodeValue(uri: URI, out: JsonWriter): Unit = ??? - } -} \ No newline at end of file + def decodeValue(in: JsonReader, default: URI): URI = + try + val uriString = in.readString(null) + if (uriString != null) new URI(uriString) else default + catch + case e: URISyntaxException => + in.decodeError(s"Invalid URI syntax: ${e.getMessage}") + + def encodeValue(uri: URI, out: JsonWriter): Unit = + if (uri != null) out.writeVal(uri.toString) + else out.writeNull() \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache index 8c56dd84f1a6..53d6af6ceaec 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache @@ -5,25 +5,28 @@ package {{package}} import {{import}} {{/imports}} import {{invokerPackage}}.JsonSupport.{*, given} +import {{invokerPackage}}.Helpers.* import sttp.client4.jsoniter.* import sttp.client4.* import sttp.model.Method {{#operations}} -object {{classname}} { +object {{classname}}: def apply(baseUrl: String = "{{{basePath}}}") = new {{classname}}(baseUrl) -} - -class {{classname}}(baseUrl: String) { +class {{classname}}(baseUrl: String): {{#operation}} - {{#javadocRenderer}} {{>javadoc}} {{/javadocRenderer}} - def {{operationId}}({{>methodParameters}}): Request[{{#separateErrorChannel}}Either[ResponseException[String, Exception], {{>operationReturnType}}]{{/separateErrorChannel}}{{^separateErrorChannel}}{{>operationReturnType}}{{/separateErrorChannel}}] = + def {{operationId}}{{>methodParameters}}: Request[{{#separateErrorChannel}}Either[ResponseException[String, Exception], {{>operationReturnType}}]{{/separateErrorChannel}}{{^separateErrorChannel}}{{>operationReturnType}}{{/separateErrorChannel}}] = + val requestURL = + uri"$baseUrl{{{path}}}"{{#queryParams}} + .addParamNamed("{{baseName}}", {{{paramName}}}{{#collectionFormat}}, CollectionFormats.{{collectionFormat.toUpperCase}}{{/collectionFormat}}){{/queryParams}}{{#isApiKey}}{{#isKeyInQuery}} + .addParam("{{keyParamName}}", apiKey.value){{/isKeyInQuery}}{{/isApiKey}} + basicRequest - .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}}") + .method(Method.{{httpMethod.toUpperCase}}, requestURL) .contentType({{#consumes.0}}"{{{mediaType}}}"{{/consumes.0}}{{^consumes}}"application/json"{{/consumes}}){{#headerParams}} .header({{>paramCreation}}){{/headerParams}}{{#authMethods}}{{#isBasic}}{{#isBasicBasic}} .auth.basic(username, password){{/isBasicBasic}}{{#isBasicBearer}} @@ -43,6 +46,5 @@ class {{classname}}(baseUrl: String) { .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}}) {{/operation}} - -} {{/operations}} +end {{classname}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/build.sbt.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/build.sbt.mustache index c6a722edbfd5..18813c0f53db 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/build.sbt.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/build.sbt.mustache @@ -16,6 +16,4 @@ scalacOptions := Seq( "-unchecked", "-deprecation", "-feature" -) - -// REBUILT \ No newline at end of file +) \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/helpers.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/helpers.mustache new file mode 100644 index 000000000000..b613176ba243 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/helpers.mustache @@ -0,0 +1,65 @@ +{{>licenseInfo}} +package {{invokerPackage}} + +import scala.language.implicitConversions + +object Helpers: + implicit def productToMapStringString[A <: Product](a: A): Map[String, String] = + val fields = a.productElementNames.zipWithIndex.map { case (name, index) => + name -> a.productElement(index) + }.map { + case (name, value: Option[_]) => value.map(v => name -> v.toString) + case (name, value) => Some(name -> value.toString) + }.collect { case Some(x) => x } + + fields.toMap + + type Primitive = String | Short | Int | Long | Float | Double | BigDecimal | Boolean + + extension (uri: sttp.model.Uri) + def addParamNamed(name: String, value: Primitive | Option[Primitive] | Map[String, String]): sttp.model.Uri = + value match + case opt: Option[_] => opt.fold(uri) { prim => uri.addParam(name, prim.toString) } + case map: Map[String, String] => map.foldLeft(uri) { case (uri, (k, v)) => uri.addParam(k, v) } + case prim => uri.addParam(name, prim.toString) + + def addParamNamed(name: String, value: Iterable[String], format: CollectionFormat): sttp.model.Uri = + format match + case CollectionFormats.MULTI => value.foldLeft(uri) { case (uri, v) => uri.addParam(name, v) } + case maFormat: MergedArrayFormat => uri.addParam(name, value.mkString(maFormat.separator)) + + /** + * Used for params being arrays + */ + final case class ArrayValues(values: Seq[Any], format: MergedArrayFormat = CollectionFormats.CSV): + override def toString: String = values.mkString(format.separator) + + object ArrayValues: + def apply(values: Option[Seq[Any]], format: MergedArrayFormat): ArrayValues = + ArrayValues(values.getOrElse(Seq.empty), format) + + def apply(values: Option[Seq[Any]]): ArrayValues = ArrayValues(values, CollectionFormats.CSV) + + /** + * Defines how arrays should be rendered in query strings. + */ + sealed trait CollectionFormat + + trait MergedArrayFormat extends CollectionFormat: + def separator: String + + object CollectionFormats: + + case object CSV extends MergedArrayFormat: + override val separator = "," + + case object TSV extends MergedArrayFormat: + override val separator = "\t" + + case object SSV extends MergedArrayFormat: + override val separator = " " + + case object PIPES extends MergedArrayFormat: + override val separator = "|" + + case object MULTI extends CollectionFormat diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/jsonSupport.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/jsonSupport.mustache index d9b7be35ab47..3b3cdf6c5af8 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/jsonSupport.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/jsonSupport.mustache @@ -8,9 +8,14 @@ import com.github.plokhotnyuk.jsoniter_scala.macros.* import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.circe.JsoniterScalaCodec.* -object JsonSupport extends AdditionalTypeSerializers { +object JsonSupport extends AdditionalTypeSerializers: + inline given CodecMakerConfig = CodecMakerConfig.withAllowRecursiveTypes(true) + + inline def deriveJsonCodec[A](using inline config: CodecMakerConfig): JsonValueCodec[A] = + JsonCodecMaker.make(config) + {{#jsonCodecNeedingTypes}} - given {{key}}: JsonValueCodec[{{value}}] = JsonCodecMaker.make + given {{key}}: JsonValueCodec[{{value}}] = deriveJsonCodec {{/jsonCodecNeedingTypes}} -} + diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/methodParameters.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/methodParameters.mustache index 8d056a18a4c9..cf9305d358bc 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/methodParameters.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/methodParameters.mustache @@ -1 +1 @@ -{{#authMethods.0}}{{#authMethods}}{{#isApiKey}}apiKey: String{{/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}} \ No newline at end of file +{{#authMethods.0}}({{#authMethods}}{{#isApiKey}}apiKey: String{{/isApiKey}}{{#isBasic}}{{#isBasicBasic}}username: String, password: String{{/isBasicBasic}}{{#isBasicBearer}}bearerToken: String{{/isBasicBearer}}{{/isBasic}}{{^-last}}, {{/-last}}{{/authMethods}}){{/authMethods.0}}{{#allParams.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}}){{/allParams.0}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache index 8603701da9a2..3b51547b2afb 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache @@ -4,6 +4,7 @@ package {{package}} {{#imports}} import {{import}} {{/imports}} +import com.github.plokhotnyuk.jsoniter_scala.macros.named {{#models}} {{#model}} @@ -21,92 +22,92 @@ case class {{classname}}( {{#description}} /* {{{.}}} */ {{/description}} - {{{name}}}: {{^required}}Option[{{/required}}{{^isEnum}}{{dataType}}{{/isEnum}}{{#isEnum}}{{^isArray}}{{classname}}Enums.{{datatypeWithEnum}}{{/isArray}}{{#isArray}}Seq[{{classname}}Enums.{{datatypeWithEnum}}]{{/isArray}}{{/isEnum}}{{^required}}] = None{{/required}}{{^-last}},{{/-last}} + @named("{{baseName}}") {{{name}}}: {{^required}}Option[{{/required}}{{^isEnum}}{{dataType}}{{/isEnum}}{{#isEnum}}{{^isArray}}{{classname}}Enums.{{datatypeWithEnum}}{{/isArray}}{{#isArray}}Seq[{{classname}}Enums.{{datatypeWithEnum}}]{{/isArray}}{{/isEnum}}{{^required}}] = None{{/required}}{{^-last}},{{/-last}} {{/vars}} ) {{/isEnum}} {{#isEnum}} -enum {{classname}} { +enum {{classname}}: {{#allowableValues}} {{#values}} case {{#fnEnumEntry}}{{.}}{{/fnEnumEntry}} {{/values}} {{/allowableValues}} -} -object {{classname}} { + +object {{classname}}: import com.github.plokhotnyuk.jsoniter_scala.macros.* import com.github.plokhotnyuk.jsoniter_scala.core.* {{#isString}} - given CodecMakerConfig = CodecMakerConfig - .withAdtLeafClassNameMapper(x => JsonCodecMaker.simpleClassName(x) match { - {{#values}} - case "{{#fnEnumEntry}}{{.}}{{/fnEnumEntry}}" => "{{.}}" - {{/values}} - }).withDiscriminatorFieldName(None) - - given {{#fnCodecName}}{{datatypeWithEnum}}{{/fnCodecName}}: JsonValueCodec[{{datatypeWithEnum}}] = JsonCodecMaker.make + given {{#fnCodecName}}{{datatypeWithEnum}}{{/fnCodecName}}: JsonValueCodec[{{datatypeWithEnum}}] = JsonCodecMaker.make { + CodecMakerConfig + .withAdtLeafClassNameMapper { x => + JsonCodecMaker.simpleClassName(x) match + {{#values}} + case "{{#fnEnumEntry}}{{.}}{{/fnEnumEntry}}" => "{{.}}" + {{/values}} + } + .withDiscriminatorFieldName(scala.None) + } {{/isString}} {{#isNumber}} - given {{#fnCodecName}}{{datatypeWithEnum}}{{/fnCodecName}}: JsonValueCodec[{{datatypeWithEnum}}] = new JsonValueCodec[{{datatypeWithEnum}}] { + given {{#fnCodecName}}{{datatypeWithEnum}}{{/fnCodecName}}: JsonValueCodec[{{datatypeWithEnum}}] = new JsonValueCodec[{{datatypeWithEnum}}]: import scala.util.{Try, Success, Failure} override val nullValue: {{datatypeWithEnum}} = null override def decodeValue(in: JsonReader, default: {{datatypeWithEnum}}): {{datatypeWithEnum}} = val x = in.readByte() - Try { {{datatypeWithEnum}}.fromOrdinal(x) } match { + Try { {{datatypeWithEnum}}.fromOrdinal(x) } match case Success(v) => v case Failure(_) => in.decodeError(s"unexpected number value: $x") - } + override def encodeValue(x: {{datatypeWithEnum}}, out: JsonWriter): Unit = out.writeVal(x.ordinal) - } {{/isNumber}} -} +end {{classname}} + {{/isEnum}} {{#hasEnums}} -object {{classname}}Enums { +object {{classname}}Enums: {{#vars}} {{#isEnum}} - enum {{datatypeWithEnum}} { + enum {{datatypeWithEnum}}: {{#_enum}} case {{#fnEnumEntry}}{{.}}{{/fnEnumEntry}} {{/_enum}} - } - object {{datatypeWithEnum}} { + + object {{datatypeWithEnum}}: import com.github.plokhotnyuk.jsoniter_scala.macros.* import com.github.plokhotnyuk.jsoniter_scala.core.* {{#isString}} - given CodecMakerConfig = CodecMakerConfig - .withAdtLeafClassNameMapper(x => JsonCodecMaker.simpleClassName(x) match { - {{#_enum}} - case "{{#fnEnumEntry}}{{.}}{{/fnEnumEntry}}" => "{{.}}" - {{/_enum}} - }).withDiscriminatorFieldName(None) - - given {{#fnCodecName}}{{datatypeWithEnum}}{{/fnCodecName}}: JsonValueCodec[{{datatypeWithEnum}}] = JsonCodecMaker.make + given {{#fnCodecName}}{{datatypeWithEnum}}{{/fnCodecName}}: JsonValueCodec[{{datatypeWithEnum}}] = JsonCodecMaker.make { + CodecMakerConfig + .withAdtLeafClassNameMapper { x => + JsonCodecMaker.simpleClassName(x) match + {{#_enum}} + case "{{#fnEnumEntry}}{{.}}{{/fnEnumEntry}}" => "{{.}}" + {{/_enum}} + } + .withDiscriminatorFieldName(scala.None) + } {{/isString}} {{#isNumber}} - given {{#fnCodecName}}{{datatypeWithEnum}}{{/fnCodecName}}: JsonValueCodec[{{datatypeWithEnum}}] = new JsonValueCodec[{{datatypeWithEnum}}] { + given {{#fnCodecName}}{{datatypeWithEnum}}{{/fnCodecName}}: JsonValueCodec[{{datatypeWithEnum}}] = new JsonValueCodec[{{datatypeWithEnum}}]: import scala.util.{Try, Success, Failure} override val nullValue: {{datatypeWithEnum}} = null override def decodeValue(in: JsonReader, default: {{datatypeWithEnum}}): {{datatypeWithEnum}} = val x = in.readByte() - Try { {{datatypeWithEnum}}.fromOrdinal(x) } match { + Try { {{datatypeWithEnum}}.fromOrdinal(x) } match case Success(v) => v case Failure(_) => in.decodeError(s"unexpected number value: $x") - } + override def encodeValue(x: {{datatypeWithEnum}}, out: JsonWriter): Unit = out.writeVal(x.ordinal) - } {{/isNumber}} - - } {{/isEnum}} {{/vars}} - -} +end {{classname}}Enums {{/hasEnums}} {{/model}} {{/models}} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramCreation.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramCreation.mustache index 25ec73e8d5e1..88aa503ba7a2 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramCreation.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramCreation.mustache @@ -1 +1 @@ -"{{baseName}}", {{#isContainer}}ArrayValues({{{paramName}}}{{#collectionFormat}}, {{collectionFormat.toUpperCase}}{{/collectionFormat}}){{/isContainer}}{{^isContainer}}{{{paramName}}}{{/isContainer}}.toString \ No newline at end of file +"{{baseName}}", {{#isContainer}}Some(ArrayValues({{{paramName}}}{{#collectionFormat}}, CollectionFormats.{{collectionFormat.toUpperCase}}{{/collectionFormat}}).toString()){{/isContainer}}{{^isContainer}}Some({{{paramName}}}.toString()){{/isContainer}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramFormCreation.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramFormCreation.mustache index a0f1a65c0746..1f2352775b15 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramFormCreation.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramFormCreation.mustache @@ -1 +1 @@ -"{{baseName}}" -> {{#isContainer}}ArrayValues({{{paramName}}}{{#collectionFormat}}, {{collectionFormat.toUpperCase}}{{/collectionFormat}}){{/isContainer}}{{^isContainer}}{{{paramName}}}{{/isContainer}} \ No newline at end of file +"{{baseName}}" -> {{#isContainer}}Some(ArrayValues({{{paramName}}}{{#collectionFormat}}, CollectionFormats.{{collectionFormat.toUpperCase}}{{/collectionFormat}}).toString()){{/isContainer}}{{^isContainer}}{{^required}}{{{paramName}}}{{/required}}{{#required}}Some({{{paramName}}}){{/required}}{{/isContainer}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramMultipartCreation.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramMultipartCreation.mustache index da8a9736b386..21bd8c1a1e4a 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramMultipartCreation.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramMultipartCreation.mustache @@ -1,6 +1,6 @@ {{#required}} {{#isFile}} - Some(multipartFile("{{baseName}}", {{#isContainer}}ArrayValues({{{paramName}}}{{#collectionFormat}}, {{collectionFormat.toUpperCase}}{{/collectionFormat}}){{/isContainer}}{{^isContainer}}{{{paramName}}}{{/isContainer}})){{^-last}},{{/-last}} + Some(multipartFile("{{baseName}}", {{#isContainer}}ArrayValues({{{paramName}}}{{#collectionFormat}}, CollectionFormats.{{collectionFormat.toUpperCase}}{{/collectionFormat}}).toString(){{/isContainer}}{{^isContainer}}{{{paramName}}}.toString(){{/isContainer}})){{^-last}},{{/-last}} {{/isFile}} {{^isFile}} Some({{paramName}}).map(_.toString).map(multipart("{{baseName}}", _)){{^-last}},{{/-last}} @@ -11,6 +11,6 @@ {{paramName}}.map(multipartFile("{{baseName}}", _)){{^-last}},{{/-last}} {{/isFile}} {{^isFile}} - {{paramName}}{{^isContainer}}.map(_.toString){{/isContainer}}.map(multipart("{{baseName}}", {{#isContainer}}ArrayValues(_{{#collectionFormat}}, {{collectionFormat.toUpperCase}}{{/collectionFormat}}){{/isContainer}}{{^isContainer}}_{{/isContainer}})){{^-last}},{{/-last}} + {{paramName}}{{^isContainer}}.map(_.toString){{/isContainer}}.map(multipart("{{baseName}}", {{#isContainer}}ArrayValues(_{{#collectionFormat}}, CollectionFormats.{{collectionFormat.toUpperCase}}{{/collectionFormat}}).toString(){{/isContainer}}{{^isContainer}}_{{/isContainer}})){{^-last}},{{/-last}} {{/isFile}} {{/required}} diff --git a/rebuild.sh b/rebuild.sh new file mode 100755 index 000000000000..547814714ad2 --- /dev/null +++ b/rebuild.sh @@ -0,0 +1,391 @@ +#!/bin/bash + +set -euo pipefail + +# Project configurations as associative arrays +declare -A mm_config=( + [PROJECT]="mm-sttp4" + [PROJECT_NAME]="mattermost-scala" + [PROJECT_GROUP_ID]="ma.chinespirit" + [PROJECT_ARTIFACT_ID]="mattermost-scala" + [PROJECT_MAIN_PACKAGE]="ma.chinespirit.mm" + [PROJECT_VERSION]="1.0.0-SNAPSHOT" + [GENERATOR_NAME]="scala-sttp4-jsoniter" + [SKIP_VALIDATE]="true" + [ADDITIONAL_PROPS]="" + [SCHEMA_MAPPINGS]="" + [TYPE_MAPPINGS]="" + [IMPORT_MAPPINGS]="" +) + +declare -A kube_config=( + [PROJECT]="kube-sttp4" + [PROJECT_NAME]="kubeapi-scala" + [PROJECT_GROUP_ID]="ma.chinespirit" + [PROJECT_ARTIFACT_ID]="kubeapi-scala" + [PROJECT_MAIN_PACKAGE]="ma.chinespirit.kube" + [PROJECT_VERSION]="1.0.0-SNAPSHOT" + [GENERATOR_NAME]="scala-sttp4-jsoniter" + [SKIP_VALIDATE]="true" + [ADDITIONAL_PROPS]="" + [SCHEMA_MAPPINGS]="io.k8s.apimachinery.pkg.util.intstr.IntOrString=IntOrString" + [TYPE_MAPPINGS]="IntOrString=IntOrString" + [IMPORT_MAPPINGS]="IntOrString=ma.chinespirit.kube.ext.IntOrString" +) + +declare -A stripe_config=( + [PROJECT]="stripe-sttp4" + [PROJECT_NAME]="stripe-scala" + [PROJECT_GROUP_ID]="ma.chinespirit" + [PROJECT_ARTIFACT_ID]="stripe-scala" + [PROJECT_MAIN_PACKAGE]="ma.chinespirit.stripe" + [PROJECT_VERSION]="1.0.0-SNAPSHOT" + [GENERATOR_NAME]="scala-sttp4-jsoniter" + [SKIP_VALIDATE]="true" + [ADDITIONAL_PROPS]="" + [SCHEMA_MAPPINGS]="" + [TYPE_MAPPINGS]="" + [IMPORT_MAPPINGS]="" +) + +declare -A github_config=( + [PROJECT]="github-sttp4" + [PROJECT_NAME]="github-scala" + [PROJECT_GROUP_ID]="ma.chinespirit" + [PROJECT_ARTIFACT_ID]="github-scala" + [PROJECT_MAIN_PACKAGE]="ma.chinespirit.github" + [PROJECT_VERSION]="1.0.0-SNAPSHOT" + [GENERATOR_NAME]="scala-sttp4-jsoniter" + [SKIP_VALIDATE]="true" + [ADDITIONAL_PROPS]="" + [SCHEMA_MAPPINGS]="" + [TYPE_MAPPINGS]="" + [IMPORT_MAPPINGS]="" +) + +declare -A spotify_config=( + [PROJECT]="spotify-sttp4" + [PROJECT_NAME]="spotify-scala" + [PROJECT_GROUP_ID]="ma.chinespirit" + [PROJECT_ARTIFACT_ID]="spotify-scala" + [PROJECT_MAIN_PACKAGE]="ma.chinespirit.spotify" + [PROJECT_VERSION]="1.0.0-SNAPSHOT" + [GENERATOR_NAME]="scala-sttp4-jsoniter" + [SKIP_VALIDATE]="true" + [ADDITIONAL_PROPS]="" + [SCHEMA_MAPPINGS]="" + [TYPE_MAPPINGS]="" + [IMPORT_MAPPINGS]="" +) + +# Project mapping +declare -A projects=( + [mattermost]="mm_config" + [kubernetes]="kube_config" + [stripe]="stripe_config" + [github]="github_config" + [spotify]="spotify_config" +) + +# Default to mattermost project if no argument is provided +PROJECT_CONFIG=${1:-mm_config} + +# If a project name is provided, look it up in the projects dictionary +if [[ -n "${projects[$1]:-}" ]]; then + PROJECT_CONFIG=${projects[$1]} +fi + +# Dynamically set project variables from the selected configuration +PROJECT="${!PROJECT_CONFIG[PROJECT]}" +PROJECT_NAME="${!PROJECT_CONFIG[PROJECT_NAME]}" +PROJECT_GROUP_ID="${!PROJECT_CONFIG[PROJECT_GROUP_ID]}" +PROJECT_ARTIFACT_ID="${!PROJECT_CONFIG[PROJECT_ARTIFACT_ID]}" +PROJECT_MAIN_PACKAGE="${!PROJECT_CONFIG[PROJECT_MAIN_PACKAGE]}" +PROJECT_VERSION="${!PROJECT_CONFIG[PROJECT_VERSION]}" + +PROJECT_ROOT_PATH="../$PROJECT" + +# Function to clean Maven build +clean_maven() { + ./mvnw clean +} + +# Function to install with Maven, skipping tests and javadocs +install_maven() { + ./mvnw install -DskipTests -Dmaven.javadoc.skip=true +} + +# Function to run openapi-generator-cli with default configuration +run_generator_jsoniter() { + local generator_name="${!PROJECT_CONFIG[GENERATOR_NAME]}" + local additional_props="mainPackage=$PROJECT_MAIN_PACKAGE,groupId=$PROJECT_GROUP_ID,artifactId=$PROJECT_ARTIFACT_ID,artifactVersion=$PROJECT_VERSION" + + # Append any additional properties from the project config + if [[ -n "${!PROJECT_CONFIG[ADDITIONAL_PROPS]}" ]]; then + additional_props+=",${!PROJECT_CONFIG[ADDITIONAL_PROPS]}" + fi + + # Prepare optional flags + local schema_mappings="" + local type_mappings="" + local import_mappings="" + local validate_flag="" + + # Add schema mappings if specified + if [[ -n "${!PROJECT_CONFIG[SCHEMA_MAPPINGS]}" ]]; then + schema_mappings="--schema-mappings ${!PROJECT_CONFIG[SCHEMA_MAPPINGS]}" + fi + + # Add type mappings if specified + if [[ -n "${!PROJECT_CONFIG[TYPE_MAPPINGS]}" ]]; then + type_mappings="--type-mappings ${!PROJECT_CONFIG[TYPE_MAPPINGS]}" + fi + + # Add import mappings if specified + if [[ -n "${!PROJECT_CONFIG[IMPORT_MAPPINGS]}" ]]; then + import_mappings="--import-mappings ${!PROJECT_CONFIG[IMPORT_MAPPINGS]}" + fi + + # Handle validation skipping + if [[ "${!PROJECT_CONFIG[SKIP_VALIDATE]}" == "true" ]]; then + validate_flag="--skip-validate-spec" + fi + + java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate \ + -i $PROJECT_ROOT_PATH/openapi.json \ + --generator-name "$generator_name" \ + -o $PROJECT_ROOT_PATH \ + $validate_flag \ + --additional-properties="$additional_props" \ + $schema_mappings \ + $type_mappings \ + $import_mappings +} + +run_generator_jsoniter_strict() { + local generator_name="${!PROJECT_CONFIG[GENERATOR_NAME]}" + local additional_props="mainPackage=$PROJECT_MAIN_PACKAGE,groupId=$PROJECT_GROUP_ID,artifactId=$PROJECT_ARTIFACT_ID,artifactVersion=$PROJECT_VERSION" + + # Append any additional properties from the project config + if [[ -n "${!PROJECT_CONFIG[ADDITIONAL_PROPS]}" ]]; then + additional_props+=",${!PROJECT_CONFIG[ADDITIONAL_PROPS]}" + fi + + # Prepare optional flags + local schema_mappings="" + local type_mappings="" + local import_mappings="" + + # Add schema mappings if specified + if [[ -n "${!PROJECT_CONFIG[SCHEMA_MAPPINGS]}" ]]; then + schema_mappings="--schema-mappings ${!PROJECT_CONFIG[SCHEMA_MAPPINGS]}" + fi + + # Add type mappings if specified + if [[ -n "${!PROJECT_CONFIG[TYPE_MAPPINGS]}" ]]; then + type_mappings="--type-mappings ${!PROJECT_CONFIG[TYPE_MAPPINGS]}" + fi + + # Add import mappings if specified + if [[ -n "${!PROJECT_CONFIG[IMPORT_MAPPINGS]}" ]]; then + import_mappings="--import-mappings ${!PROJECT_CONFIG[IMPORT_MAPPINGS]}" + fi + + java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate \ + -i $PROJECT_ROOT_PATH/openapi.json \ + --generator-name "$generator_name" \ + -o $PROJECT_ROOT_PATH \ + --additional-properties="$additional_props" \ + $schema_mappings \ + $type_mappings \ + $import_mappings +} + +# Function to run openapi-generator-cli with upstream configuration +run_generator_upstream() { + local generator_name="scala-sttp4" + local additional_props="mainPackage=$PROJECT_MAIN_PACKAGE,groupId=$PROJECT_GROUP_ID,artifactId=$PROJECT_ARTIFACT_ID,artifactVersion=$PROJECT_VERSION,jsonLibrary=circe" + + # Append any additional properties from the project config + if [[ -n "${!PROJECT_CONFIG[ADDITIONAL_PROPS]}" ]]; then + additional_props+=",${!PROJECT_CONFIG[ADDITIONAL_PROPS]}" + fi + + # Prepare optional flags + local schema_mappings="" + local type_mappings="" + local import_mappings="" + local validate_flag="" + + # Add schema mappings if specified + if [[ -n "${!PROJECT_CONFIG[SCHEMA_MAPPINGS]}" ]]; then + schema_mappings="--schema-mappings ${!PROJECT_CONFIG[SCHEMA_MAPPINGS]}" + fi + + # Add type mappings if specified + if [[ -n "${!PROJECT_CONFIG[TYPE_MAPPINGS]}" ]]; then + type_mappings="--type-mappings ${!PROJECT_CONFIG[TYPE_MAPPINGS]}" + fi + + # Add import mappings if specified + if [[ -n "${!PROJECT_CONFIG[IMPORT_MAPPINGS]}" ]]; then + import_mappings="--import-mappings ${!PROJECT_CONFIG[IMPORT_MAPPINGS]}" + fi + + # Handle validation skipping + if [[ "${!PROJECT_CONFIG[SKIP_VALIDATE]}" == "true" ]]; then + validate_flag="--skip-validate-spec" + fi + + # Assumes the alternative openapi-generator-cli.jar is located at a specific path + openapi-generator-cli generate \ + -i $PROJECT_ROOT_PATH/openapi.json \ + --generator-name "$generator_name" \ + -o $PROJECT_ROOT_PATH \ + $validate_flag \ + --additional-properties="$additional_props" \ + $schema_mappings \ + $type_mappings \ + $import_mappings +} + +# Function to clean up specific files and directories in $PROJECT_ROOT_PATH +cleanup_generated_files() { + rm -rf $PROJECT_ROOT_PATH/build.sbt $PROJECT_ROOT_PATH/target $PROJECT_ROOT_PATH/project $PROJECT_ROOT_PATH/README.md + + # Remove generated source files based on the project's main package + local base_package=$(echo "$PROJECT_MAIN_PACKAGE" | tr '.' '/') + rm -rf $PROJECT_ROOT_PATH/src/main/scala/$base_package/api + rm -rf $PROJECT_ROOT_PATH/src/main/scala/$base_package/model + rm -rf $PROJECT_ROOT_PATH/src/main/scala/$base_package/core +} + +notify_failure() { + say "You dun goofed, Mortal" +} + +trap notify_failure ERR + +# Function to run clean for all projects +clean_all_projects() { + for project_name in "${!projects[@]}"; do + PROJECT_CONFIG=${projects[$project_name]} + + # Dynamically set project variables from the selected configuration + PROJECT="${!PROJECT_CONFIG[PROJECT]}" + PROJECT_NAME="${!PROJECT_CONFIG[PROJECT_NAME]}" + PROJECT_ROOT_PATH="../$PROJECT" + PROJECT_MAIN_PACKAGE="${!PROJECT_CONFIG[PROJECT_MAIN_PACKAGE]}" + + echo "Cleaning project: $project_name" + clean_maven + cleanup_generated_files + install_maven + run_generator_jsoniter + done +} + +# Function to generate for all projects +generate_all_projects() { + for project_name in "${!projects[@]}"; do + PROJECT_CONFIG=${projects[$project_name]} + + # Dynamically set project variables from the selected configuration + PROJECT="${!PROJECT_CONFIG[PROJECT]}" + PROJECT_NAME="${!PROJECT_CONFIG[PROJECT_NAME]}" + PROJECT_ROOT_PATH="../$PROJECT" + PROJECT_MAIN_PACKAGE="${!PROJECT_CONFIG[PROJECT_MAIN_PACKAGE]}" + + echo "Generating project: $project_name" + run_generator_jsoniter + done +} + +# Main script logic based on input parameters +case ${1:-} in + clean) + if [[ "${2:-}" == "all" ]]; then + clean_all_projects + else + clean_maven + cleanup_generated_files + install_maven + run_generator_jsoniter + fi + ;; + generate) + if [[ "${2:-}" == "all" ]]; then + generate_all_projects + else + run_generator_jsoniter + fi + ;; + generate-strict) + if [[ "${2:-}" == "all" ]]; then + for project_name in "${!projects[@]}"; do + PROJECT_CONFIG=${projects[$project_name]} + + # Dynamically set project variables from the selected configuration + PROJECT="${!PROJECT_CONFIG[PROJECT]}" + PROJECT_NAME="${!PROJECT_CONFIG[PROJECT_NAME]}" + PROJECT_ROOT_PATH="../$PROJECT" + PROJECT_MAIN_PACKAGE="${!PROJECT_CONFIG[PROJECT_MAIN_PACKAGE]}" + + echo "Generating project (strict): $project_name" + run_generator_jsoniter_strict + done + else + run_generator_jsoniter_strict + fi + ;; + upstream) + if [[ "${2:-}" == "all" ]]; then + for project_name in "${!projects[@]}"; do + PROJECT_CONFIG=${projects[$project_name]} + + # Dynamically set project variables from the selected configuration + PROJECT="${!PROJECT_CONFIG[PROJECT]}" + PROJECT_NAME="${!PROJECT_CONFIG[PROJECT_NAME]}" + PROJECT_ROOT_PATH="../$PROJECT" + PROJECT_MAIN_PACKAGE="${!PROJECT_CONFIG[PROJECT_MAIN_PACKAGE]}" + + echo "Generating project (upstream): $project_name" + cleanup_generated_files + run_generator_upstream + done + else + cleanup_generated_files + run_generator_upstream + fi + ;; + strict) + if [[ "${2:-}" == "all" ]]; then + for project_name in "${!projects[@]}"; do + PROJECT_CONFIG=${projects[$project_name]} + + # Dynamically set project variables from the selected configuration + PROJECT="${!PROJECT_CONFIG[PROJECT]}" + PROJECT_NAME="${!PROJECT_CONFIG[PROJECT_NAME]}" + PROJECT_ROOT_PATH="../$PROJECT" + PROJECT_MAIN_PACKAGE="${!PROJECT_CONFIG[PROJECT_MAIN_PACKAGE]}" + + echo "Cleaning and generating project (strict): $project_name" + clean_maven + cleanup_generated_files + install_maven + run_generator_jsoniter_strict + done + else + clean_maven + cleanup_generated_files + install_maven + run_generator_jsoniter_strict + fi + ;; + *) + install_maven + run_generator_jsoniter + ;; +esac + +say "It's done, Mortal" From 62c24ae858c548085dd11834fedfa29e698d09f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Bia=C5=82y?= Date: Thu, 6 Feb 2025 13:41:34 +0100 Subject: [PATCH 06/14] added script to rebuild all projects --- rebuild.sc | 239 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100755 rebuild.sc diff --git a/rebuild.sc b/rebuild.sc new file mode 100755 index 000000000000..9aaa4908882d --- /dev/null +++ b/rebuild.sc @@ -0,0 +1,239 @@ +#!/usr/bin/env scala +//> using toolkit 0.6.0 +//> using scala 3.3.1 + +import os.* + +object Debug: + var enabled = false + +enum Command: + case Clean, Generate, GenerateStrict, Upstream, Strict, Exit + +enum Target: + case Specific(name: String) + case All + +def say(message: String)(using pwd: Path) = + val command = List("say", message) + if Debug.enabled then println(s"Running command: ${command.mkString(" ")}") + os.proc(command).call(stdout = os.Inherit, stderr = os.Inherit) + +case class Project( + project: String, + projectName: String, + projectGroupId: String, + projectArtifactId: String, + projectMainPackage: String, + projectVersion: String, + generatorName: String, + skipValidate: Boolean, + additionalProps: String = "", + schemaMappings: String = "", + typeMappings: String = "", + importMappings: String = "" +) + +val projects = Map( + "mattermost" -> Project( + project = "mm-sttp4", + projectName = "mattermost-scala", + projectGroupId = "ma.chinespirit", + projectArtifactId = "mattermost-scala", + projectMainPackage = "ma.chinespirit.mm", + projectVersion = "1.0.0-SNAPSHOT", + generatorName = "scala-sttp4-jsoniter", + skipValidate = false + ), + "kubernetes" -> Project( + project = "kube-sttp4", + projectName = "kubeapi-scala", + projectGroupId = "ma.chinespirit", + projectArtifactId = "kubeapi-scala", + projectMainPackage = "ma.chinespirit.kube", + projectVersion = "1.0.0-SNAPSHOT", + generatorName = "scala-sttp4-jsoniter", + skipValidate = false, + schemaMappings = "io.k8s.apimachinery.pkg.util.intstr.IntOrString=IntOrString", + typeMappings = "IntOrString=IntOrString", + importMappings = "IntOrString=ma.chinespirit.kube.ext.IntOrString" + ), + "stripe" -> Project( + project = "stripe-sttp4", + projectName = "stripe-scala", + projectGroupId = "ma.chinespirit", + projectArtifactId = "stripe-scala", + projectMainPackage = "ma.chinespirit.stripe", + projectVersion = "1.0.0-SNAPSHOT", + generatorName = "scala-sttp4-jsoniter", + skipValidate = false + ), + "github" -> Project( + project = "github-sttp4", + projectName = "github-scala", + projectGroupId = "ma.chinespirit", + projectArtifactId = "github-scala", + projectMainPackage = "ma.chinespirit.github", + projectVersion = "1.0.0-SNAPSHOT", + generatorName = "scala-sttp4-jsoniter", + skipValidate = false + ), + "spotify" -> Project( + project = "spotify-sttp4", + projectName = "spotify-scala", + projectGroupId = "ma.chinespirit", + projectArtifactId = "spotify-scala", + projectMainPackage = "ma.chinespirit.spotify", + projectVersion = "1.0.0-SNAPSHOT", + generatorName = "scala-sttp4-jsoniter", + skipValidate = false + ) +) + +def cleanMaven(using pwd: Path) = + val command = List("./mvnw", "clean") + if Debug.enabled then println(s"Running command: ${command.mkString(" ")}") + os.proc(command).call(stdout = os.Inherit, stderr = os.Inherit) + +def installMaven(using pwd: Path) = + val command = List("./mvnw", "install", "-DskipTests", "-Dmaven.javadoc.skip=true") + if Debug.enabled then println(s"Running command: ${command.mkString(" ")}") + os.proc(command).call(stdout = os.Inherit, stderr = os.Inherit) + +def cleanupGeneratedFiles(project: Project, projectRootPath: Path) = + val basePackage = os.SubPath(project.projectMainPackage.replace(".", "/")) + os.remove.all(projectRootPath / "build.sbt") + os.remove.all(projectRootPath / "target") + os.remove.all(projectRootPath / "project") + os.remove.all(projectRootPath / "README.md") + os.remove.all(projectRootPath / "src" / "main" / "scala" / basePackage / "api") + os.remove.all(projectRootPath / "src" / "main" / "scala" / basePackage / "model") + os.remove.all(projectRootPath / "src" / "main" / "scala" / basePackage / "core") + +def runGeneratorJsoniter(project: Project, projectRootPath: Path, strict: Boolean = false)(using pwd: Path) = + val additionalProps = { + val base = s"mainPackage=${project.projectMainPackage},groupId=${project.projectGroupId},artifactId=${project.projectArtifactId},artifactVersion=${project.projectVersion}" + val extended = if project.additionalProps.nonEmpty then s"$base,${project.additionalProps}" else base + + List(s"--additional-properties", extended) + } + val validateFlag = if project.skipValidate && !strict then List("--skip-validate-spec") else Nil + val schemaMappings = if project.schemaMappings.nonEmpty then List("--schema-mappings", project.schemaMappings) else Nil + val typeMappings = if project.typeMappings.nonEmpty then List("--type-mappings", project.typeMappings) else Nil + val importMappings = if project.importMappings.nonEmpty then List("--import-mappings", project.importMappings) else Nil + + val command = List( + List("java", "-jar", "modules/openapi-generator-cli/target/openapi-generator-cli.jar", "generate", + "-i", s"$projectRootPath/openapi.json", + "--generator-name", project.generatorName, + "-o", projectRootPath.toString), + validateFlag, + additionalProps, + schemaMappings, + typeMappings, + importMappings + ).flatten + + if Debug.enabled then println(s"Running command: ${command.mkString(" ")}") + os.proc(command).call(stdout = os.Inherit, stderr = os.Inherit) + +def runGeneratorUpstream(project: Project, projectRootPath: Path)(using pwd: Path) = + val additionalProps = { + val base = s"mainPackage=${project.projectMainPackage},groupId=${project.projectGroupId},artifactId=${project.projectArtifactId},artifactVersion=${project.projectVersion},jsonLibrary=circe" + val extended = if project.additionalProps.nonEmpty then s"$base,${project.additionalProps}" else base + + List("--additional-properties", extended) + } + val validateFlag = if project.skipValidate then List("--skip-validate-spec") else Nil + val schemaMappings = if project.schemaMappings.nonEmpty then List("--schema-mappings", project.schemaMappings) else Nil + val typeMappings = if project.typeMappings.nonEmpty then List("--type-mappings", project.typeMappings) else Nil + val importMappings = if project.importMappings.nonEmpty then List("--import-mappings", project.importMappings) else Nil + + val command = List( + List("openapi-generator-cli", "generate", + "-i", s"$projectRootPath/openapi.json", + "--generator-name", "scala-sttp4", + "-o", projectRootPath.toString), + validateFlag, + additionalProps, + schemaMappings, + typeMappings, + importMappings + ).flatten + + if Debug.enabled then println(s"Running command: ${command.mkString(" ")}") + os.proc(command).call(stdout = os.Inherit, stderr = os.Inherit) + +def processProject(project: Project, cmd: Command)(using pwd: Path) = + val projectRootPath = pwd / os.up / project.project + cmd match + case Command.Clean => + cleanMaven + cleanupGeneratedFiles(project, projectRootPath) + installMaven + runGeneratorJsoniter(project, projectRootPath) + case Command.Generate => + runGeneratorJsoniter(project, projectRootPath) + case Command.GenerateStrict => + runGeneratorJsoniter(project, projectRootPath, strict = true) + case Command.Upstream => + cleanupGeneratedFiles(project, projectRootPath) + runGeneratorUpstream(project, projectRootPath) + case Command.Strict => + cleanMaven + cleanupGeneratedFiles(project, projectRootPath) + installMaven + runGeneratorJsoniter(project, projectRootPath, strict = true) + case Command.Exit => + // Do nothing + +def main(): Unit = + given pwd: Path = os.pwd + + println("Args: " + args.mkString(" ")) + + try + // Handle debug flag + val filteredArgs = args.toList.filter { arg => + if arg == "--debug-script" then + Debug.enabled = true + false + else true + } + + val (cmdStr, target) = filteredArgs match + case cmd :: "all" :: _ => (cmd, Target.All) + case cmd :: name :: _ => (cmd, Target.Specific(name)) + case cmd :: Nil => (cmd, Target.All) + case _ => ("exit", Target.All) + + val cmd = cmdStr match + case "clean" => Command.Clean + case "generate" => Command.Generate + case "generate-strict" => Command.GenerateStrict + case "upstream" => Command.Upstream + case "strict" => Command.Strict + case "exit" => Command.Exit + case _ => Command.Exit + + (target, cmd) match + case (_, Command.Exit) => + println("Usage: scala rebuild.sc [project]") + println("Available commands: clean, generate, generate-strict, upstream, strict") + case (Target.All, _) => + projects.values.foreach(project => processProject(project, cmd)) + say("All done, mortal") + case (Target.Specific(projectName), _) => + projects.get(projectName) match + case Some(project) => + processProject(project, cmd) + say("All done, mortal") + case None => + System.err.println(s"Project '$projectName' not found. Available projects: ${projects.keys.mkString(", ")}") + sys.exit(1) + catch + case e: Exception => + say("You dun goofed, Mortal") + throw e + +main() From a6956e4f9a4210acde33964d801f1fa378de0579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Bia=C5=82y?= Date: Thu, 6 Feb 2025 13:42:14 +0100 Subject: [PATCH 07/14] dropped bash slop --- rebuild.sh | 391 ----------------------------------------------------- 1 file changed, 391 deletions(-) delete mode 100755 rebuild.sh diff --git a/rebuild.sh b/rebuild.sh deleted file mode 100755 index 547814714ad2..000000000000 --- a/rebuild.sh +++ /dev/null @@ -1,391 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -# Project configurations as associative arrays -declare -A mm_config=( - [PROJECT]="mm-sttp4" - [PROJECT_NAME]="mattermost-scala" - [PROJECT_GROUP_ID]="ma.chinespirit" - [PROJECT_ARTIFACT_ID]="mattermost-scala" - [PROJECT_MAIN_PACKAGE]="ma.chinespirit.mm" - [PROJECT_VERSION]="1.0.0-SNAPSHOT" - [GENERATOR_NAME]="scala-sttp4-jsoniter" - [SKIP_VALIDATE]="true" - [ADDITIONAL_PROPS]="" - [SCHEMA_MAPPINGS]="" - [TYPE_MAPPINGS]="" - [IMPORT_MAPPINGS]="" -) - -declare -A kube_config=( - [PROJECT]="kube-sttp4" - [PROJECT_NAME]="kubeapi-scala" - [PROJECT_GROUP_ID]="ma.chinespirit" - [PROJECT_ARTIFACT_ID]="kubeapi-scala" - [PROJECT_MAIN_PACKAGE]="ma.chinespirit.kube" - [PROJECT_VERSION]="1.0.0-SNAPSHOT" - [GENERATOR_NAME]="scala-sttp4-jsoniter" - [SKIP_VALIDATE]="true" - [ADDITIONAL_PROPS]="" - [SCHEMA_MAPPINGS]="io.k8s.apimachinery.pkg.util.intstr.IntOrString=IntOrString" - [TYPE_MAPPINGS]="IntOrString=IntOrString" - [IMPORT_MAPPINGS]="IntOrString=ma.chinespirit.kube.ext.IntOrString" -) - -declare -A stripe_config=( - [PROJECT]="stripe-sttp4" - [PROJECT_NAME]="stripe-scala" - [PROJECT_GROUP_ID]="ma.chinespirit" - [PROJECT_ARTIFACT_ID]="stripe-scala" - [PROJECT_MAIN_PACKAGE]="ma.chinespirit.stripe" - [PROJECT_VERSION]="1.0.0-SNAPSHOT" - [GENERATOR_NAME]="scala-sttp4-jsoniter" - [SKIP_VALIDATE]="true" - [ADDITIONAL_PROPS]="" - [SCHEMA_MAPPINGS]="" - [TYPE_MAPPINGS]="" - [IMPORT_MAPPINGS]="" -) - -declare -A github_config=( - [PROJECT]="github-sttp4" - [PROJECT_NAME]="github-scala" - [PROJECT_GROUP_ID]="ma.chinespirit" - [PROJECT_ARTIFACT_ID]="github-scala" - [PROJECT_MAIN_PACKAGE]="ma.chinespirit.github" - [PROJECT_VERSION]="1.0.0-SNAPSHOT" - [GENERATOR_NAME]="scala-sttp4-jsoniter" - [SKIP_VALIDATE]="true" - [ADDITIONAL_PROPS]="" - [SCHEMA_MAPPINGS]="" - [TYPE_MAPPINGS]="" - [IMPORT_MAPPINGS]="" -) - -declare -A spotify_config=( - [PROJECT]="spotify-sttp4" - [PROJECT_NAME]="spotify-scala" - [PROJECT_GROUP_ID]="ma.chinespirit" - [PROJECT_ARTIFACT_ID]="spotify-scala" - [PROJECT_MAIN_PACKAGE]="ma.chinespirit.spotify" - [PROJECT_VERSION]="1.0.0-SNAPSHOT" - [GENERATOR_NAME]="scala-sttp4-jsoniter" - [SKIP_VALIDATE]="true" - [ADDITIONAL_PROPS]="" - [SCHEMA_MAPPINGS]="" - [TYPE_MAPPINGS]="" - [IMPORT_MAPPINGS]="" -) - -# Project mapping -declare -A projects=( - [mattermost]="mm_config" - [kubernetes]="kube_config" - [stripe]="stripe_config" - [github]="github_config" - [spotify]="spotify_config" -) - -# Default to mattermost project if no argument is provided -PROJECT_CONFIG=${1:-mm_config} - -# If a project name is provided, look it up in the projects dictionary -if [[ -n "${projects[$1]:-}" ]]; then - PROJECT_CONFIG=${projects[$1]} -fi - -# Dynamically set project variables from the selected configuration -PROJECT="${!PROJECT_CONFIG[PROJECT]}" -PROJECT_NAME="${!PROJECT_CONFIG[PROJECT_NAME]}" -PROJECT_GROUP_ID="${!PROJECT_CONFIG[PROJECT_GROUP_ID]}" -PROJECT_ARTIFACT_ID="${!PROJECT_CONFIG[PROJECT_ARTIFACT_ID]}" -PROJECT_MAIN_PACKAGE="${!PROJECT_CONFIG[PROJECT_MAIN_PACKAGE]}" -PROJECT_VERSION="${!PROJECT_CONFIG[PROJECT_VERSION]}" - -PROJECT_ROOT_PATH="../$PROJECT" - -# Function to clean Maven build -clean_maven() { - ./mvnw clean -} - -# Function to install with Maven, skipping tests and javadocs -install_maven() { - ./mvnw install -DskipTests -Dmaven.javadoc.skip=true -} - -# Function to run openapi-generator-cli with default configuration -run_generator_jsoniter() { - local generator_name="${!PROJECT_CONFIG[GENERATOR_NAME]}" - local additional_props="mainPackage=$PROJECT_MAIN_PACKAGE,groupId=$PROJECT_GROUP_ID,artifactId=$PROJECT_ARTIFACT_ID,artifactVersion=$PROJECT_VERSION" - - # Append any additional properties from the project config - if [[ -n "${!PROJECT_CONFIG[ADDITIONAL_PROPS]}" ]]; then - additional_props+=",${!PROJECT_CONFIG[ADDITIONAL_PROPS]}" - fi - - # Prepare optional flags - local schema_mappings="" - local type_mappings="" - local import_mappings="" - local validate_flag="" - - # Add schema mappings if specified - if [[ -n "${!PROJECT_CONFIG[SCHEMA_MAPPINGS]}" ]]; then - schema_mappings="--schema-mappings ${!PROJECT_CONFIG[SCHEMA_MAPPINGS]}" - fi - - # Add type mappings if specified - if [[ -n "${!PROJECT_CONFIG[TYPE_MAPPINGS]}" ]]; then - type_mappings="--type-mappings ${!PROJECT_CONFIG[TYPE_MAPPINGS]}" - fi - - # Add import mappings if specified - if [[ -n "${!PROJECT_CONFIG[IMPORT_MAPPINGS]}" ]]; then - import_mappings="--import-mappings ${!PROJECT_CONFIG[IMPORT_MAPPINGS]}" - fi - - # Handle validation skipping - if [[ "${!PROJECT_CONFIG[SKIP_VALIDATE]}" == "true" ]]; then - validate_flag="--skip-validate-spec" - fi - - java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate \ - -i $PROJECT_ROOT_PATH/openapi.json \ - --generator-name "$generator_name" \ - -o $PROJECT_ROOT_PATH \ - $validate_flag \ - --additional-properties="$additional_props" \ - $schema_mappings \ - $type_mappings \ - $import_mappings -} - -run_generator_jsoniter_strict() { - local generator_name="${!PROJECT_CONFIG[GENERATOR_NAME]}" - local additional_props="mainPackage=$PROJECT_MAIN_PACKAGE,groupId=$PROJECT_GROUP_ID,artifactId=$PROJECT_ARTIFACT_ID,artifactVersion=$PROJECT_VERSION" - - # Append any additional properties from the project config - if [[ -n "${!PROJECT_CONFIG[ADDITIONAL_PROPS]}" ]]; then - additional_props+=",${!PROJECT_CONFIG[ADDITIONAL_PROPS]}" - fi - - # Prepare optional flags - local schema_mappings="" - local type_mappings="" - local import_mappings="" - - # Add schema mappings if specified - if [[ -n "${!PROJECT_CONFIG[SCHEMA_MAPPINGS]}" ]]; then - schema_mappings="--schema-mappings ${!PROJECT_CONFIG[SCHEMA_MAPPINGS]}" - fi - - # Add type mappings if specified - if [[ -n "${!PROJECT_CONFIG[TYPE_MAPPINGS]}" ]]; then - type_mappings="--type-mappings ${!PROJECT_CONFIG[TYPE_MAPPINGS]}" - fi - - # Add import mappings if specified - if [[ -n "${!PROJECT_CONFIG[IMPORT_MAPPINGS]}" ]]; then - import_mappings="--import-mappings ${!PROJECT_CONFIG[IMPORT_MAPPINGS]}" - fi - - java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate \ - -i $PROJECT_ROOT_PATH/openapi.json \ - --generator-name "$generator_name" \ - -o $PROJECT_ROOT_PATH \ - --additional-properties="$additional_props" \ - $schema_mappings \ - $type_mappings \ - $import_mappings -} - -# Function to run openapi-generator-cli with upstream configuration -run_generator_upstream() { - local generator_name="scala-sttp4" - local additional_props="mainPackage=$PROJECT_MAIN_PACKAGE,groupId=$PROJECT_GROUP_ID,artifactId=$PROJECT_ARTIFACT_ID,artifactVersion=$PROJECT_VERSION,jsonLibrary=circe" - - # Append any additional properties from the project config - if [[ -n "${!PROJECT_CONFIG[ADDITIONAL_PROPS]}" ]]; then - additional_props+=",${!PROJECT_CONFIG[ADDITIONAL_PROPS]}" - fi - - # Prepare optional flags - local schema_mappings="" - local type_mappings="" - local import_mappings="" - local validate_flag="" - - # Add schema mappings if specified - if [[ -n "${!PROJECT_CONFIG[SCHEMA_MAPPINGS]}" ]]; then - schema_mappings="--schema-mappings ${!PROJECT_CONFIG[SCHEMA_MAPPINGS]}" - fi - - # Add type mappings if specified - if [[ -n "${!PROJECT_CONFIG[TYPE_MAPPINGS]}" ]]; then - type_mappings="--type-mappings ${!PROJECT_CONFIG[TYPE_MAPPINGS]}" - fi - - # Add import mappings if specified - if [[ -n "${!PROJECT_CONFIG[IMPORT_MAPPINGS]}" ]]; then - import_mappings="--import-mappings ${!PROJECT_CONFIG[IMPORT_MAPPINGS]}" - fi - - # Handle validation skipping - if [[ "${!PROJECT_CONFIG[SKIP_VALIDATE]}" == "true" ]]; then - validate_flag="--skip-validate-spec" - fi - - # Assumes the alternative openapi-generator-cli.jar is located at a specific path - openapi-generator-cli generate \ - -i $PROJECT_ROOT_PATH/openapi.json \ - --generator-name "$generator_name" \ - -o $PROJECT_ROOT_PATH \ - $validate_flag \ - --additional-properties="$additional_props" \ - $schema_mappings \ - $type_mappings \ - $import_mappings -} - -# Function to clean up specific files and directories in $PROJECT_ROOT_PATH -cleanup_generated_files() { - rm -rf $PROJECT_ROOT_PATH/build.sbt $PROJECT_ROOT_PATH/target $PROJECT_ROOT_PATH/project $PROJECT_ROOT_PATH/README.md - - # Remove generated source files based on the project's main package - local base_package=$(echo "$PROJECT_MAIN_PACKAGE" | tr '.' '/') - rm -rf $PROJECT_ROOT_PATH/src/main/scala/$base_package/api - rm -rf $PROJECT_ROOT_PATH/src/main/scala/$base_package/model - rm -rf $PROJECT_ROOT_PATH/src/main/scala/$base_package/core -} - -notify_failure() { - say "You dun goofed, Mortal" -} - -trap notify_failure ERR - -# Function to run clean for all projects -clean_all_projects() { - for project_name in "${!projects[@]}"; do - PROJECT_CONFIG=${projects[$project_name]} - - # Dynamically set project variables from the selected configuration - PROJECT="${!PROJECT_CONFIG[PROJECT]}" - PROJECT_NAME="${!PROJECT_CONFIG[PROJECT_NAME]}" - PROJECT_ROOT_PATH="../$PROJECT" - PROJECT_MAIN_PACKAGE="${!PROJECT_CONFIG[PROJECT_MAIN_PACKAGE]}" - - echo "Cleaning project: $project_name" - clean_maven - cleanup_generated_files - install_maven - run_generator_jsoniter - done -} - -# Function to generate for all projects -generate_all_projects() { - for project_name in "${!projects[@]}"; do - PROJECT_CONFIG=${projects[$project_name]} - - # Dynamically set project variables from the selected configuration - PROJECT="${!PROJECT_CONFIG[PROJECT]}" - PROJECT_NAME="${!PROJECT_CONFIG[PROJECT_NAME]}" - PROJECT_ROOT_PATH="../$PROJECT" - PROJECT_MAIN_PACKAGE="${!PROJECT_CONFIG[PROJECT_MAIN_PACKAGE]}" - - echo "Generating project: $project_name" - run_generator_jsoniter - done -} - -# Main script logic based on input parameters -case ${1:-} in - clean) - if [[ "${2:-}" == "all" ]]; then - clean_all_projects - else - clean_maven - cleanup_generated_files - install_maven - run_generator_jsoniter - fi - ;; - generate) - if [[ "${2:-}" == "all" ]]; then - generate_all_projects - else - run_generator_jsoniter - fi - ;; - generate-strict) - if [[ "${2:-}" == "all" ]]; then - for project_name in "${!projects[@]}"; do - PROJECT_CONFIG=${projects[$project_name]} - - # Dynamically set project variables from the selected configuration - PROJECT="${!PROJECT_CONFIG[PROJECT]}" - PROJECT_NAME="${!PROJECT_CONFIG[PROJECT_NAME]}" - PROJECT_ROOT_PATH="../$PROJECT" - PROJECT_MAIN_PACKAGE="${!PROJECT_CONFIG[PROJECT_MAIN_PACKAGE]}" - - echo "Generating project (strict): $project_name" - run_generator_jsoniter_strict - done - else - run_generator_jsoniter_strict - fi - ;; - upstream) - if [[ "${2:-}" == "all" ]]; then - for project_name in "${!projects[@]}"; do - PROJECT_CONFIG=${projects[$project_name]} - - # Dynamically set project variables from the selected configuration - PROJECT="${!PROJECT_CONFIG[PROJECT]}" - PROJECT_NAME="${!PROJECT_CONFIG[PROJECT_NAME]}" - PROJECT_ROOT_PATH="../$PROJECT" - PROJECT_MAIN_PACKAGE="${!PROJECT_CONFIG[PROJECT_MAIN_PACKAGE]}" - - echo "Generating project (upstream): $project_name" - cleanup_generated_files - run_generator_upstream - done - else - cleanup_generated_files - run_generator_upstream - fi - ;; - strict) - if [[ "${2:-}" == "all" ]]; then - for project_name in "${!projects[@]}"; do - PROJECT_CONFIG=${projects[$project_name]} - - # Dynamically set project variables from the selected configuration - PROJECT="${!PROJECT_CONFIG[PROJECT]}" - PROJECT_NAME="${!PROJECT_CONFIG[PROJECT_NAME]}" - PROJECT_ROOT_PATH="../$PROJECT" - PROJECT_MAIN_PACKAGE="${!PROJECT_CONFIG[PROJECT_MAIN_PACKAGE]}" - - echo "Cleaning and generating project (strict): $project_name" - clean_maven - cleanup_generated_files - install_maven - run_generator_jsoniter_strict - done - else - clean_maven - cleanup_generated_files - install_maven - run_generator_jsoniter_strict - fi - ;; - *) - install_maven - run_generator_jsoniter - ;; -esac - -say "It's done, Mortal" From 147acb38bd485d39fed408db1ef86cd3220c332a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Bia=C5=82y?= Date: Thu, 6 Feb 2025 14:19:48 +0100 Subject: [PATCH 08/14] fixed directory structure --- rebuild.sc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rebuild.sc b/rebuild.sc index 9aaa4908882d..ac304ede871c 100755 --- a/rebuild.sc +++ b/rebuild.sc @@ -36,7 +36,7 @@ case class Project( val projects = Map( "mattermost" -> Project( - project = "mm-sttp4", + project = "mattermost-scala", projectName = "mattermost-scala", projectGroupId = "ma.chinespirit", projectArtifactId = "mattermost-scala", @@ -46,7 +46,7 @@ val projects = Map( skipValidate = false ), "kubernetes" -> Project( - project = "kube-sttp4", + project = "kubeapi-scala", projectName = "kubeapi-scala", projectGroupId = "ma.chinespirit", projectArtifactId = "kubeapi-scala", @@ -59,7 +59,7 @@ val projects = Map( importMappings = "IntOrString=ma.chinespirit.kube.ext.IntOrString" ), "stripe" -> Project( - project = "stripe-sttp4", + project = "stripe-scala", projectName = "stripe-scala", projectGroupId = "ma.chinespirit", projectArtifactId = "stripe-scala", @@ -69,7 +69,7 @@ val projects = Map( skipValidate = false ), "github" -> Project( - project = "github-sttp4", + project = "github-scala", projectName = "github-scala", projectGroupId = "ma.chinespirit", projectArtifactId = "github-scala", @@ -79,7 +79,7 @@ val projects = Map( skipValidate = false ), "spotify" -> Project( - project = "spotify-sttp4", + project = "spotify-scala", projectName = "spotify-scala", projectGroupId = "ma.chinespirit", projectArtifactId = "spotify-scala", From 8edb63d39e6355b2b4580e1965ea911d575f0491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Bia=C5=82y?= Date: Thu, 6 Feb 2025 16:36:23 +0100 Subject: [PATCH 09/14] ok, now it makes sense --- rebuild.sc | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/rebuild.sc b/rebuild.sc index ac304ede871c..77667bf07ae6 100755 --- a/rebuild.sc +++ b/rebuild.sc @@ -8,7 +8,7 @@ object Debug: var enabled = false enum Command: - case Clean, Generate, GenerateStrict, Upstream, Strict, Exit + case Clean, Generate, GenerateStrict, GenerateOldGen, Strict, Exit enum Target: case Specific(name: String) @@ -137,7 +137,7 @@ def runGeneratorJsoniter(project: Project, projectRootPath: Path, strict: Boolea if Debug.enabled then println(s"Running command: ${command.mkString(" ")}") os.proc(command).call(stdout = os.Inherit, stderr = os.Inherit) -def runGeneratorUpstream(project: Project, projectRootPath: Path)(using pwd: Path) = +def runGeneratorOld(project: Project, projectRootPath: Path)(using pwd: Path) = val additionalProps = { val base = s"mainPackage=${project.projectMainPackage},groupId=${project.projectGroupId},artifactId=${project.projectArtifactId},artifactVersion=${project.projectVersion},jsonLibrary=circe" val extended = if project.additionalProps.nonEmpty then s"$base,${project.additionalProps}" else base @@ -170,15 +170,13 @@ def processProject(project: Project, cmd: Command)(using pwd: Path) = case Command.Clean => cleanMaven cleanupGeneratedFiles(project, projectRootPath) - installMaven - runGeneratorJsoniter(project, projectRootPath) case Command.Generate => runGeneratorJsoniter(project, projectRootPath) case Command.GenerateStrict => runGeneratorJsoniter(project, projectRootPath, strict = true) - case Command.Upstream => + case Command.GenerateOldGen => cleanupGeneratedFiles(project, projectRootPath) - runGeneratorUpstream(project, projectRootPath) + runGeneratorOld(project, projectRootPath) case Command.Strict => cleanMaven cleanupGeneratedFiles(project, projectRootPath) @@ -211,7 +209,7 @@ def main(): Unit = case "clean" => Command.Clean case "generate" => Command.Generate case "generate-strict" => Command.GenerateStrict - case "upstream" => Command.Upstream + case "generate-old-gen" => Command.GenerateOldGen case "strict" => Command.Strict case "exit" => Command.Exit case _ => Command.Exit @@ -222,18 +220,18 @@ def main(): Unit = println("Available commands: clean, generate, generate-strict, upstream, strict") case (Target.All, _) => projects.values.foreach(project => processProject(project, cmd)) - say("All done, mortal") + say("It is done") case (Target.Specific(projectName), _) => projects.get(projectName) match case Some(project) => processProject(project, cmd) - say("All done, mortal") + say("It is done") case None => System.err.println(s"Project '$projectName' not found. Available projects: ${projects.keys.mkString(", ")}") sys.exit(1) catch case e: Exception => - say("You dun goofed, Mortal") + say("You dun goofed") throw e main() From 5887936eede73bbbb8ffb065a6bc2e43f4331b6a Mon Sep 17 00:00:00 2001 From: Kamil-Lontkowski Date: Tue, 11 Feb 2025 14:18:37 +0100 Subject: [PATCH 10/14] update sttp, serialize query params, fix enums serialization, fix not required files params --- .../ScalaSttp4JsoniterClientCodegen.java | 5 +- .../scala-sttp4-jsoniter/api.mustache | 10 +- .../scala-sttp4-jsoniter/helpers.mustache | 179 ++++++++++++------ .../scala-sttp4-jsoniter/model.mustache | 6 +- .../paramFormCreation.mustache | 2 +- 5 files changed, 134 insertions(+), 68 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java index ee957d69278f..bacd2a1bd718 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java @@ -28,7 +28,7 @@ public class ScalaSttp4JsoniterClientCodegen extends AbstractScalaCodegen implem private static final StringProperty STTP_CLIENT_VERSION = new StringProperty("sttpClientVersion", "The version of " + "sttp client", - "4.0.0-M19"); + "4.0.0-RC1"); private static final BooleanProperty USE_SEPARATE_ERROR_CHANNEL = new BooleanProperty("separateErrorChannel", "Whether to return response as " + "F[Either[ResponseError[ErrorType], ReturnType]]] or to flatten " + @@ -85,8 +85,7 @@ public ScalaSttp4JsoniterClientCodegen() { .excludeGlobalFeatures( GlobalFeature.XMLStructureDefinitions, GlobalFeature.Callbacks, - GlobalFeature.LinkObjects, - GlobalFeature.ParameterStyling) + GlobalFeature.LinkObjects) .excludeSchemaSupportFeatures( SchemaSupportFeature.Polymorphism) .excludeParameterFeatures( diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache index 53d6af6ceaec..d3e051709534 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache @@ -5,6 +5,8 @@ package {{package}} import {{import}} {{/imports}} import {{invokerPackage}}.JsonSupport.{*, given} +import {{invokerPackage}}.FormSerializable +import {{invokerPackage}}.FormStyleFormat import {{invokerPackage}}.Helpers.* import sttp.client4.jsoniter.* import sttp.client4.* @@ -19,11 +21,11 @@ class {{classname}}(baseUrl: String): {{#javadocRenderer}} {{>javadoc}} {{/javadocRenderer}} - def {{operationId}}{{>methodParameters}}: Request[{{#separateErrorChannel}}Either[ResponseException[String, Exception], {{>operationReturnType}}]{{/separateErrorChannel}}{{^separateErrorChannel}}{{>operationReturnType}}{{/separateErrorChannel}}] = + def {{operationId}}{{>methodParameters}}: sttp.client4.Request[{{#separateErrorChannel}}Either[ResponseException[String, Exception], {{>operationReturnType}}]{{/separateErrorChannel}}{{^separateErrorChannel}}{{>operationReturnType}}{{/separateErrorChannel}}] = val requestURL = uri"$baseUrl{{{path}}}"{{#queryParams}} - .addParamNamed("{{baseName}}", {{{paramName}}}{{#collectionFormat}}, CollectionFormats.{{collectionFormat.toUpperCase}}{{/collectionFormat}}){{/queryParams}}{{#isApiKey}}{{#isKeyInQuery}} - .addParam("{{keyParamName}}", apiKey.value){{/isKeyInQuery}}{{/isApiKey}} + .addParams(FormSerializable.serialize("{{baseName}}", {{{paramName}}}{{#style}}, FormStyleFormat.{{style.toUpperCase}}{{/style}}{{^style}}, FormStyleFormat.FORM{{/style}}, {{isExplode}})){{/queryParams}}{{#isApiKey}}{{#isKeyInQuery}} + .addParams("{{keyParamName}}" -> apiKey.value){{/isKeyInQuery}}{{/isApiKey}} basicRequest .method(Method.{{httpMethod.toUpperCase}}, requestURL) @@ -42,7 +44,7 @@ class {{classname}}(baseUrl: String): .multipartBody(Seq({{#formParams}} {{>paramMultipartCreation}}{{/formParams}} ).flatten){{/isMultipart}}{{/formParams.0}}{{#bodyParam}} - .body({{paramName}}){{/bodyParam}} + {{^isFile}}.body({{paramName}}){{/isFile}}{{#isFile}}.fileBody({{paramName}}){{/isFile}}{{/bodyParam}} .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}}) {{/operation}} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/helpers.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/helpers.mustache index b613176ba243..2993206f0add 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/helpers.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/helpers.mustache @@ -1,65 +1,130 @@ {{>licenseInfo}} package {{invokerPackage}} -import scala.language.implicitConversions +import scala.deriving.* +import scala.compiletime.* +import java.io.File -object Helpers: - implicit def productToMapStringString[A <: Product](a: A): Map[String, String] = - val fields = a.productElementNames.zipWithIndex.map { case (name, index) => - name -> a.productElement(index) - }.map { - case (name, value: Option[_]) => value.map(v => name -> v.toString) - case (name, value) => Some(name -> value.toString) - }.collect { case Some(x) => x } - - fields.toMap +enum FormStyleFormat: + case FORM + case SPACEDELIMITED + case PIPEDELIMITED + case DEEPOBJECT +object FormSerializable: type Primitive = String | Short | Int | Long | Float | Double | BigDecimal | Boolean - extension (uri: sttp.model.Uri) - def addParamNamed(name: String, value: Primitive | Option[Primitive] | Map[String, String]): sttp.model.Uri = - value match - case opt: Option[_] => opt.fold(uri) { prim => uri.addParam(name, prim.toString) } - case map: Map[String, String] => map.foldLeft(uri) { case (uri, (k, v)) => uri.addParam(k, v) } - case prim => uri.addParam(name, prim.toString) - - def addParamNamed(name: String, value: Iterable[String], format: CollectionFormat): sttp.model.Uri = - format match - case CollectionFormats.MULTI => value.foldLeft(uri) { case (uri, v) => uri.addParam(name, v) } - case maFormat: MergedArrayFormat => uri.addParam(name, value.mkString(maFormat.separator)) - - /** - * Used for params being arrays - */ - final case class ArrayValues(values: Seq[Any], format: MergedArrayFormat = CollectionFormats.CSV): - override def toString: String = values.mkString(format.separator) - - object ArrayValues: - def apply(values: Option[Seq[Any]], format: MergedArrayFormat): ArrayValues = - ArrayValues(values.getOrElse(Seq.empty), format) - - def apply(values: Option[Seq[Any]]): ArrayValues = ArrayValues(values, CollectionFormats.CSV) - - /** - * Defines how arrays should be rendered in query strings. - */ - sealed trait CollectionFormat + inline def serialize[T]( + name: String, + obj: T, + inline format: FormStyleFormat = FormStyleFormat.FORM, + inline explode: Boolean = true + ): Map[String, String] = + inline obj match + case primitive: Primitive => serializePrimitive(name, primitive, format, explode) + case array: Seq[Primitive] => serializeArray(name, array, format, explode) + case optPrimitive: Option[Primitive] => optPrimitive.map(value => serializePrimitive(name, value, format, explode)).getOrElse(Map.empty[String, String]) + case optArray: Option[Seq[Primitive]] => optArray.map(serializeArray(name, _, format, explode)).getOrElse(Map.empty[String, String]) + case freeObj: Map[String, Primitive] => freeObj.map((key, value) => (key, value.toString)) + case optObj: Option[t] => + inline summonInline[Mirror.Of[t]] match + case mirror: Mirror.ProductOf[t] => + checkFields[mirror.MirroredElemTypes] + val labels = allLabels[mirror.MirroredElemLabels] + optObj.map{ obj => + val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive]]).toSeq + serializeModel(name, keyVals, format, explode) + }.getOrElse(Map.empty[String, String]) + case _ => error("ERROR") // TODO - support for enums + case obj => + inline summonInline[Mirror.Of[T]] match + case _: Mirror.SumOf[T] => + error("ERROR") // TODO - support for enums + case mirror: Mirror.ProductOf[T] => + checkFields[mirror.MirroredElemTypes] // Stripe ma IDGAF bo używają deepObject np. tak lines[0][tax_amounts][0][amount] - mimo tego że spec na to nie pozwala + val labels = allLabels[mirror.MirroredElemLabels] + val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive]]).toSeq + serializeModel(name, keyVals, format, explode) + + + + private inline def allLabels[T <: Tuple]: List[String] = + constValueTuple[T].toList.asInstanceOf[List[String]] + + private inline def checkFields[T <: Tuple]: Unit = + inline erasedValue[T] match { + case _: EmptyTuple => () + case _: (t *: ts) => + inline erasedValue[t] match + case _: Primitive => checkFields[ts] + case _ => + error( + "Cannot derive structure, structure must consist only of primitive fields" + ) + } + + private inline def serializePrimitive( + paramName: String, + value: Primitive, + inline format: FormStyleFormat, + inline explode: Boolean + ): Map[String, String] = { + inline format match + case FormStyleFormat.FORM => + Map(paramName -> value.toString) // for primitve values explode does not change anything + case FormStyleFormat.SPACEDELIMITED => + error( + "FormStyleFormat.SpaceDelimited does not support primitive values" + ) + case FormStyleFormat.PIPEDELIMITED => + error("FormStyleFormat.PipeDelimited does not support primitive values") + case FormStyleFormat.DEEPOBJECT => + error("FormStyleFormat.DeepObject does not support primitive values") + + } + private inline def serializeArray( + paramName: String, + values: Seq[Primitive], + inline format: FormStyleFormat, + inline explode: Boolean + ): Map[String, String] = { + inline format match + case FormStyleFormat.FORM => + inline if explode then values.map(s => (paramName, s.toString)).toMap + else Map(paramName -> values.mkString(",")) + case FormStyleFormat.SPACEDELIMITED => + inline if explode then values.map(s => (paramName, s.toString)).toMap + else Map(paramName -> values.mkString(" ")) // Sttp will encode space as +, from https://swagger.io/docs/specification/v3_0/serialization/#query-parameters it is not clear if it should be + or %20 + case FormStyleFormat.PIPEDELIMITED => + inline if explode then values.map(s => (paramName, s.toString)).toMap + else Map(paramName -> values.mkString("|")) + case FormStyleFormat.DEEPOBJECT => + error("FormStyleFormat.DeepObject does not support arrays") + } + inline def serializeModel( + paramName: String, + keyValPairs: Seq[(String, Primitive)], + inline format: FormStyleFormat, + inline explode: Boolean + ): Map[String, String] = { + inline format match + case FormStyleFormat.FORM => + inline if explode then keyValPairs.map((key, value) => (key, value.toString)).toMap + else Map(paramName -> keyValPairs.flatMap((key,value) => Seq(key, value.toString)).mkString(",")) + case FormStyleFormat.SPACEDELIMITED => + error("FormStyleFormat.SpaceDelimited does not support objects") + case FormStyleFormat.PIPEDELIMITED => + error("FormStyleFormat.PipeDelimited does not support objects") + case FormStyleFormat.DEEPOBJECT => + inline if explode then + keyValPairs.map((key, value) => (s"$paramName[$key]", value.toString)).toMap + else error("FormStyleFormat.DeepObject does not support explode=false") + } +end FormSerializable - trait MergedArrayFormat extends CollectionFormat: - def separator: String - - object CollectionFormats: - - case object CSV extends MergedArrayFormat: - override val separator = "," - - case object TSV extends MergedArrayFormat: - override val separator = "\t" - - case object SSV extends MergedArrayFormat: - override val separator = " " - - case object PIPES extends MergedArrayFormat: - override val separator = "|" - - case object MULTI extends CollectionFormat +object Helpers: + extension (request: sttp.client4.Request[?]) + def fileBody(file: Option[File] | File): sttp.client4.Request[?] = + file match + case f: File => request.body(f) + case f: Option[File] => f.map(request.body(_)).getOrElse(request) \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache index 3b51547b2afb..624bca2beaf3 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache @@ -39,14 +39,14 @@ object {{classname}}: import com.github.plokhotnyuk.jsoniter_scala.macros.* import com.github.plokhotnyuk.jsoniter_scala.core.* {{#isString}} - given {{#fnCodecName}}{{datatypeWithEnum}}{{/fnCodecName}}: JsonValueCodec[{{datatypeWithEnum}}] = JsonCodecMaker.make { - CodecMakerConfig + given {{#fnCodecName}}{{classname}}{{/fnCodecName}}: JsonValueCodec[{{classname}}] = JsonCodecMaker.make { + CodecMakerConfig{{#allowableValues}} .withAdtLeafClassNameMapper { x => JsonCodecMaker.simpleClassName(x) match {{#values}} case "{{#fnEnumEntry}}{{.}}{{/fnEnumEntry}}" => "{{.}}" {{/values}} - } + }{{/allowableValues}} .withDiscriminatorFieldName(scala.None) } {{/isString}} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramFormCreation.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramFormCreation.mustache index 1f2352775b15..6b4bbe255a4f 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramFormCreation.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramFormCreation.mustache @@ -1 +1 @@ -"{{baseName}}" -> {{#isContainer}}Some(ArrayValues({{{paramName}}}{{#collectionFormat}}, CollectionFormats.{{collectionFormat.toUpperCase}}{{/collectionFormat}}).toString()){{/isContainer}}{{^isContainer}}{{^required}}{{{paramName}}}{{/required}}{{#required}}Some({{{paramName}}}){{/required}}{{/isContainer}} \ No newline at end of file +"{{baseName}}" -> {{#isContainer}}FormStyleFormat.serialize({{{paramName}}}{{#collectionFormat}}, FormStyleFormat.{{style.toUpperCase}}{{/collectionFormat}}){{/isContainer}}{{^isContainer}}{{^required}}{{{paramName}}}{{/required}}{{#required}}Some({{{paramName}}}){{/required}}{{/isContainer}} \ No newline at end of file From 135ecbd7c9adf2ec39ecb63510bdb41f2fcde132 Mon Sep 17 00:00:00 2001 From: Kamil-Lontkowski Date: Fri, 14 Feb 2025 15:41:12 +0100 Subject: [PATCH 11/14] fix compile errors after sttp upgrade, fix missing enum import in operations, fix file response. change serialization from Map to Seq to avoid deleting duplicate keys --- .../ScalaSttp4JsoniterClientCodegen.java | 8 +++- .../scala-sttp4-jsoniter/api.mustache | 16 ++++---- .../scala-sttp4-jsoniter/helpers.mustache | 39 +++++++++---------- .../scala-sttp4-jsoniter/jsonSupport.mustache | 1 + .../methodParameters.mustache | 2 +- .../scala-sttp4-jsoniter/model.mustache | 2 +- .../paramFormCreation.mustache | 2 +- 7 files changed, 36 insertions(+), 34 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java index bacd2a1bd718..f6614b3b6287 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java @@ -430,10 +430,14 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List item = new HashMap<>(); if (isEnumClass(importPath, enumRefs)) { item.put("import", importPath.concat(".*")); + Map enumClassImport = new HashMap<>(); + enumClassImport.put("import", importPath); + newImports.add(item); + newImports.add(enumClassImport); } else { item.put("import", importPath); + newImports.add(item); } - newImports.add(item); } } @@ -691,7 +695,7 @@ private static class HandleDownloadLambda extends CustomLambda { @Override public String formatFragment(String fragment) { if (fragment.equals("asJson[File]")) { - return "asFile(File.createTempFile(\"download\", \".tmp\")).mapLeft(errStr => DeserializationException(errStr, new Exception(errStr)))"; + return "asFile(File.createTempFile(\"download\", \".tmp\")).mapWithMetadata((result, metadata) => result.left.map(errStr => ResponseException.DeserializationException(errStr, new Exception(errStr), metadata)))"; } else { return fragment; } diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache index d3e051709534..067322389f99 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache @@ -21,10 +21,10 @@ class {{classname}}(baseUrl: String): {{#javadocRenderer}} {{>javadoc}} {{/javadocRenderer}} - def {{operationId}}{{>methodParameters}}: sttp.client4.Request[{{#separateErrorChannel}}Either[ResponseException[String, Exception], {{>operationReturnType}}]{{/separateErrorChannel}}{{^separateErrorChannel}}{{>operationReturnType}}{{/separateErrorChannel}}] = + def {{operationId}}{{>methodParameters}}: sttp.client4.Request[{{#separateErrorChannel}}Either[ResponseException[String], {{>operationReturnType}}]{{/separateErrorChannel}}{{^separateErrorChannel}}{{>operationReturnType}}{{/separateErrorChannel}}] = val requestURL = uri"$baseUrl{{{path}}}"{{#queryParams}} - .addParams(FormSerializable.serialize("{{baseName}}", {{{paramName}}}{{#style}}, FormStyleFormat.{{style.toUpperCase}}{{/style}}{{^style}}, FormStyleFormat.FORM{{/style}}, {{isExplode}})){{/queryParams}}{{#isApiKey}}{{#isKeyInQuery}} + .addParams(FormSerializable.serialize("{{baseName}}", {{{paramName}}}{{#style}}, FormStyleFormat.{{style.toUpperCase}}{{/style}}{{^style}}, FormStyleFormat.FORM{{/style}}, {{isExplode}}): _*){{/queryParams}}{{#isApiKey}}{{#isKeyInQuery}} .addParams("{{keyParamName}}" -> apiKey.value){{/isKeyInQuery}}{{/isApiKey}} basicRequest @@ -35,16 +35,14 @@ class {{classname}}(baseUrl: String): .auth.bearer(bearerToken){{/isBasicBearer}}{{/isBasic}}{{#isApiKey}}{{#isKeyInHeader}} .header("{{keyParamName}}", apiKey){{/isKeyInHeader}}{{#isKeyInCookie}} .cookie("{{keyParamName}}", apiKey){{/isKeyInCookie}}{{/isApiKey}}{{/authMethods}}{{#formParams.0}}{{^isMultipart}} - .body(Map({{#formParams}} - {{>paramFormCreation}}{{^-last}},{{/-last}}{{/formParams}} - ).collect { - case (key, Some(value)) => key -> value.toString - case (key, value) if value != None => key -> value.toString - }){{/isMultipart}}{{#isMultipart}} + .body({{#formParams}} + {{>paramFormCreation}}{{^-last}} ++ {{/-last}}{{/formParams}}, + "utf-8" + ){{/isMultipart}}{{#isMultipart}} .multipartBody(Seq({{#formParams}} {{>paramMultipartCreation}}{{/formParams}} ).flatten){{/isMultipart}}{{/formParams.0}}{{#bodyParam}} - {{^isFile}}.body({{paramName}}){{/isFile}}{{#isFile}}.fileBody({{paramName}}){{/isFile}}{{/bodyParam}} + {{^isFile}}.body(asJson({{paramName}})){{/isFile}}{{#isFile}}.fileBody({{paramName}}){{/isFile}}{{/bodyParam}} .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}}) {{/operation}} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/helpers.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/helpers.mustache index 2993206f0add..2ffb69f467cb 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/helpers.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/helpers.mustache @@ -19,13 +19,13 @@ object FormSerializable: obj: T, inline format: FormStyleFormat = FormStyleFormat.FORM, inline explode: Boolean = true - ): Map[String, String] = + ): Seq[(String, String)] = inline obj match case primitive: Primitive => serializePrimitive(name, primitive, format, explode) case array: Seq[Primitive] => serializeArray(name, array, format, explode) - case optPrimitive: Option[Primitive] => optPrimitive.map(value => serializePrimitive(name, value, format, explode)).getOrElse(Map.empty[String, String]) - case optArray: Option[Seq[Primitive]] => optArray.map(serializeArray(name, _, format, explode)).getOrElse(Map.empty[String, String]) - case freeObj: Map[String, Primitive] => freeObj.map((key, value) => (key, value.toString)) + case optPrimitive: Option[Primitive] => optPrimitive.map(value => serializePrimitive(name, value, format, explode)).getOrElse(Seq.empty[(String, String)]) + case optArray: Option[Seq[Primitive]] => optArray.map(serializeArray(name, _, format, explode)).getOrElse(Seq.empty[(String, String)]) + case freeObj: Map[String, Primitive] => freeObj.map((key, value) => (key, value.toString)).toSeq case optObj: Option[t] => inline summonInline[Mirror.Of[t]] match case mirror: Mirror.ProductOf[t] => @@ -34,7 +34,7 @@ object FormSerializable: optObj.map{ obj => val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive]]).toSeq serializeModel(name, keyVals, format, explode) - }.getOrElse(Map.empty[String, String]) + }.getOrElse(Seq.empty[(String, String)]) case _ => error("ERROR") // TODO - support for enums case obj => inline summonInline[Mirror.Of[T]] match @@ -47,7 +47,6 @@ object FormSerializable: serializeModel(name, keyVals, format, explode) - private inline def allLabels[T <: Tuple]: List[String] = constValueTuple[T].toList.asInstanceOf[List[String]] @@ -68,10 +67,10 @@ object FormSerializable: value: Primitive, inline format: FormStyleFormat, inline explode: Boolean - ): Map[String, String] = { + ): Seq[(String, String)] = { inline format match case FormStyleFormat.FORM => - Map(paramName -> value.toString) // for primitve values explode does not change anything + Seq(paramName -> value.toString) // for primitve values explode does not change anything case FormStyleFormat.SPACEDELIMITED => error( "FormStyleFormat.SpaceDelimited does not support primitive values" @@ -87,37 +86,37 @@ object FormSerializable: values: Seq[Primitive], inline format: FormStyleFormat, inline explode: Boolean - ): Map[String, String] = { + ): Seq[(String, String)] = { inline format match case FormStyleFormat.FORM => - inline if explode then values.map(s => (paramName, s.toString)).toMap - else Map(paramName -> values.mkString(",")) + inline if explode then values.map(s => (paramName, s.toString)) + else Seq(paramName -> values.mkString(",")) case FormStyleFormat.SPACEDELIMITED => - inline if explode then values.map(s => (paramName, s.toString)).toMap - else Map(paramName -> values.mkString(" ")) // Sttp will encode space as +, from https://swagger.io/docs/specification/v3_0/serialization/#query-parameters it is not clear if it should be + or %20 + inline if explode then values.map(s => (paramName, s.toString)) + else Seq(paramName -> values.mkString(" ")) // Sttp will encode space as +, from https://swagger.io/docs/specification/v3_0/serialization/#query-parameters it is not clear if it should be + or %20 case FormStyleFormat.PIPEDELIMITED => - inline if explode then values.map(s => (paramName, s.toString)).toMap - else Map(paramName -> values.mkString("|")) + inline if explode then values.map(s => (paramName, s.toString)) + else Seq(paramName -> values.mkString("|")) case FormStyleFormat.DEEPOBJECT => error("FormStyleFormat.DeepObject does not support arrays") } - inline def serializeModel( + private inline def serializeModel( paramName: String, keyValPairs: Seq[(String, Primitive)], inline format: FormStyleFormat, inline explode: Boolean - ): Map[String, String] = { + ): Seq[(String, String)] = { inline format match case FormStyleFormat.FORM => - inline if explode then keyValPairs.map((key, value) => (key, value.toString)).toMap - else Map(paramName -> keyValPairs.flatMap((key,value) => Seq(key, value.toString)).mkString(",")) + inline if explode then keyValPairs.map((key, value) => (key, value.toString)) + else Seq(paramName -> keyValPairs.flatMap((key,value) => Seq(key, value.toString)).mkString(",")) case FormStyleFormat.SPACEDELIMITED => error("FormStyleFormat.SpaceDelimited does not support objects") case FormStyleFormat.PIPEDELIMITED => error("FormStyleFormat.PipeDelimited does not support objects") case FormStyleFormat.DEEPOBJECT => inline if explode then - keyValPairs.map((key, value) => (s"$paramName[$key]", value.toString)).toMap + keyValPairs.map((key, value) => (s"$paramName[$key]", value.toString)) else error("FormStyleFormat.DeepObject does not support explode=false") } end FormSerializable diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/jsonSupport.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/jsonSupport.mustache index 3b3cdf6c5af8..0ff02c740c9c 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/jsonSupport.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/jsonSupport.mustache @@ -4,6 +4,7 @@ package {{invokerPackage}} {{#models.0}} import {{modelPackage}}.* {{/models.0}} +import java.time.* import com.github.plokhotnyuk.jsoniter_scala.macros.* import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.circe.JsoniterScalaCodec.* diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/methodParameters.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/methodParameters.mustache index cf9305d358bc..0a6be0c7fe15 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/methodParameters.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/methodParameters.mustache @@ -1 +1 @@ -{{#authMethods.0}}({{#authMethods}}{{#isApiKey}}apiKey: String{{/isApiKey}}{{#isBasic}}{{#isBasicBasic}}username: String, password: String{{/isBasicBasic}}{{#isBasicBearer}}bearerToken: String{{/isBasicBearer}}{{/isBasic}}{{^-last}}, {{/-last}}{{/authMethods}}){{/authMethods.0}}{{#allParams.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}}){{/allParams.0}} \ No newline at end of file +{{#authMethods.0}}({{#authMethods}}{{#isApiKey}}apiKey: String{{/isApiKey}}{{#isBasic}}{{#isBasicBasic}}username: String, password: String{{/isBasicBasic}}{{#isBasicBearer}}bearerToken: String{{/isBasicBearer}}{{/isBasic}}{{^-last}}, {{/-last}}{{/authMethods}}){{/authMethods.0}}{{#allParams.0}}({{#allParams}}{{paramName}}: {{#required}}{{dataType}}{{/required}}{{^required}}{{#isContainer}}{{dataType}}{{/isContainer}}{{^isContainer}}Option[{{dataType}}]{{/isContainer}}{{/required}}{{^defaultValue}}{{^required}}{{^isContainer}} = scala.None{{/isContainer}}{{/required}}{{/defaultValue}}{{^-last}}, {{/-last}}{{/allParams}}){{/allParams.0}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache index 624bca2beaf3..7ca532648e94 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache @@ -22,7 +22,7 @@ case class {{classname}}( {{#description}} /* {{{.}}} */ {{/description}} - @named("{{baseName}}") {{{name}}}: {{^required}}Option[{{/required}}{{^isEnum}}{{dataType}}{{/isEnum}}{{#isEnum}}{{^isArray}}{{classname}}Enums.{{datatypeWithEnum}}{{/isArray}}{{#isArray}}Seq[{{classname}}Enums.{{datatypeWithEnum}}]{{/isArray}}{{/isEnum}}{{^required}}] = None{{/required}}{{^-last}},{{/-last}} + @named("{{baseName}}") {{{name}}}: {{^required}}Option[{{/required}}{{^isEnum}}{{dataType}}{{/isEnum}}{{#isEnum}}{{^isArray}}{{classname}}Enums.{{datatypeWithEnum}}{{/isArray}}{{#isArray}}Seq[{{classname}}Enums.{{datatypeWithEnum}}]{{/isArray}}{{/isEnum}}{{^required}}] = scala.None{{/required}}{{^-last}},{{/-last}} {{/vars}} ) {{/isEnum}} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramFormCreation.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramFormCreation.mustache index 6b4bbe255a4f..cab5c89dbc6f 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramFormCreation.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramFormCreation.mustache @@ -1 +1 @@ -"{{baseName}}" -> {{#isContainer}}FormStyleFormat.serialize({{{paramName}}}{{#collectionFormat}}, FormStyleFormat.{{style.toUpperCase}}{{/collectionFormat}}){{/isContainer}}{{^isContainer}}{{^required}}{{{paramName}}}{{/required}}{{#required}}Some({{{paramName}}}){{/required}}{{/isContainer}} \ No newline at end of file +FormSerializable.serialize("{{baseName}}", {{{paramName}}}{{#style}}, FormStyleFormat.{{style.toUpperCase}}{{/style}}{{^style}}, FormStyleFormat.FORM{{/style}}, {{isExplode}}) \ No newline at end of file From fb93494399b18f9e484613f60c8207eee852bc7d Mon Sep 17 00:00:00 2001 From: Kamil-Lontkowski Date: Mon, 17 Feb 2025 14:50:35 +0100 Subject: [PATCH 12/14] auth support --- .../scala-sttp4-jsoniter/api.mustache | 34 ++++++++++++++----- .../scala-sttp4-jsoniter/helpers.mustache | 26 +++++++++++++- .../methodParameters.mustache | 2 +- 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache index 067322389f99..23260c7a4bd9 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache @@ -14,9 +14,29 @@ import sttp.model.Method {{#operations}} object {{classname}}: - def apply(baseUrl: String = "{{{basePath}}}") = new {{classname}}(baseUrl) + def apply(baseUrl: String = "{{{basePath}}}"): {{classname}}[{{invokerPackage}}.Authorization.NoAuthorization.type] = {{classname}}(baseUrl, {{invokerPackage}}.Authorization.NoAuthorization) + def withBasicAuth(baseUrl: String, username: String, password: String): {{classname}}[{{invokerPackage}}.Authorization.BasicAuth] = + {{classname}}(baseUrl, {{invokerPackage}}.Authorization.BasicAuth(username, password)) + + def withApiKeyAuth(baseUrl: String, apiKey: String): {{classname}}[{{invokerPackage}}.Authorization.ApiKey] = + {{classname}}(baseUrl, {{invokerPackage}}.Authorization.ApiKey(apiKey)) + + def withBearerTokenAuth(baseUrl: String, token: String): {{classname}}[{{invokerPackage}}.Authorization.BearerToken] = + {{classname}}(baseUrl, {{invokerPackage}}.Authorization.BearerToken(token)) + +case class {{classname}}[Auth <: {{invokerPackage}}.Authorization] private (baseUrl: String, authConfig: {{invokerPackage}}.Authorization): + def withBasicAuth(username: String, password: String): {{classname}}[{{invokerPackage}}.Authorization.BasicAuth] = + copy(authConfig = {{invokerPackage}}.Authorization.BasicAuth(username, password)) + + def withApiKeyAuth(apiKey: String): {{classname}}[{{invokerPackage}}.Authorization.ApiKey] = + copy(authConfig = {{invokerPackage}}.Authorization.ApiKey(apiKey)) + + def withNoAuth: {{classname}}[{{invokerPackage}}.Authorization.NoAuthorization.type] = + copy(authConfig = {{invokerPackage}}.Authorization.NoAuthorization) + + def withBearerTokenAuth(token: String): {{classname}}[{{invokerPackage}}.Authorization.BearerToken] = + copy(authConfig = {{invokerPackage}}.Authorization.BearerToken(token)) -class {{classname}}(baseUrl: String): {{#operation}} {{#javadocRenderer}} {{>javadoc}} @@ -24,17 +44,13 @@ class {{classname}}(baseUrl: String): def {{operationId}}{{>methodParameters}}: sttp.client4.Request[{{#separateErrorChannel}}Either[ResponseException[String], {{>operationReturnType}}]{{/separateErrorChannel}}{{^separateErrorChannel}}{{>operationReturnType}}{{/separateErrorChannel}}] = val requestURL = uri"$baseUrl{{{path}}}"{{#queryParams}} - .addParams(FormSerializable.serialize("{{baseName}}", {{{paramName}}}{{#style}}, FormStyleFormat.{{style.toUpperCase}}{{/style}}{{^style}}, FormStyleFormat.FORM{{/style}}, {{isExplode}}): _*){{/queryParams}}{{#isApiKey}}{{#isKeyInQuery}} - .addParams("{{keyParamName}}" -> apiKey.value){{/isKeyInQuery}}{{/isApiKey}} + .addParams(FormSerializable.serialize("{{baseName}}", {{{paramName}}}{{#style}}, FormStyleFormat.{{style.toUpperCase}}{{/style}}{{^style}}, FormStyleFormat.FORM{{/style}}, {{isExplode}}): _*){{/queryParams}} basicRequest .method(Method.{{httpMethod.toUpperCase}}, requestURL) .contentType({{#consumes.0}}"{{{mediaType}}}"{{/consumes.0}}{{^consumes}}"application/json"{{/consumes}}){{#headerParams}} - .header({{>paramCreation}}){{/headerParams}}{{#authMethods}}{{#isBasic}}{{#isBasicBasic}} - .auth.basic(username, password){{/isBasicBasic}}{{#isBasicBearer}} - .auth.bearer(bearerToken){{/isBasicBearer}}{{/isBasic}}{{#isApiKey}}{{#isKeyInHeader}} - .header("{{keyParamName}}", apiKey){{/isKeyInHeader}}{{#isKeyInCookie}} - .cookie("{{keyParamName}}", apiKey){{/isKeyInCookie}}{{/isApiKey}}{{/authMethods}}{{#formParams.0}}{{^isMultipart}} + .header({{>paramCreation}}){{/headerParams}}{{#authMethods}} + .auth(authConfig{{#isApiKey}}, {{invokerPackage}}.{{#isKeyInQuery}}QUERY{{/isKeyInQuery}}{{#isKeyInHeader}}HEADER{{/isKeyInHeader}}{{#isKeyInCookie}}COOKIE{{/isKeyInCookie}}, "{{keyParamName}}"{{/isApiKey}}){{/authMethods}}{{#formParams.0}}{{^isMultipart}} .body({{#formParams}} {{>paramFormCreation}}{{^-last}} ++ {{/-last}}{{/formParams}}, "utf-8" diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/helpers.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/helpers.mustache index 2ffb69f467cb..19010a3324d3 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/helpers.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/helpers.mustache @@ -5,6 +5,18 @@ import scala.deriving.* import scala.compiletime.* import java.io.File +enum ApiKeyLocation: + case HEADER + case COOKIE + case QUERY + case NOAPIKEY + +enum Authorization: + case NoAuthorization + case BasicAuth(username: String, password: String) + case ApiKey(apiKey: String) + case BearerToken(token: String) + enum FormStyleFormat: case FORM case SPACEDELIMITED @@ -126,4 +138,16 @@ object Helpers: def fileBody(file: Option[File] | File): sttp.client4.Request[?] = file match case f: File => request.body(f) - case f: Option[File] => f.map(request.body(_)).getOrElse(request) \ No newline at end of file + case f: Option[File] => f.map(request.body(_)).getOrElse(request) + + def auth(authConfig: Authorization, location: ApiKeyLocation = ApiKeyLocation.NOAPIKEY, keyParamName: String = ""): sttp.client4.Request[?] = + // multiple calls to auth with the same config are not idempotent only for APIKey with COOKIE and QUERY - that's why we have NOAPIKEY as default + authConfig match + case Authorization.NoAuthorization => request + case Authorization.BasicAuth(username, password) => request.auth.basic(username, password) + case Authorization.BearerToken(token) => request.auth.bearer(token) + case Authorization.ApiKey(apiKey) =>location match + case ApiKeyLocation.HEADER => request.header(keyParamName, apiKey) + case ApiKeyLocation.COOKIE => request.cookie(keyParamName, apiKey) + case ApiKeyLocation.QUERY => request.copy(uri = request.uri.addParam(keyParamName, apiKey)) + case ApiKeyLocation.NOAPIKEY => request // since it can be called multiple times in request (when there are for example 2 auth methods) we want to make this call idempotent diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/methodParameters.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/methodParameters.mustache index 0a6be0c7fe15..fafca2a986a0 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/methodParameters.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/methodParameters.mustache @@ -1 +1 @@ -{{#authMethods.0}}({{#authMethods}}{{#isApiKey}}apiKey: String{{/isApiKey}}{{#isBasic}}{{#isBasicBasic}}username: String, password: String{{/isBasicBasic}}{{#isBasicBearer}}bearerToken: String{{/isBasicBearer}}{{/isBasic}}{{^-last}}, {{/-last}}{{/authMethods}}){{/authMethods.0}}{{#allParams.0}}({{#allParams}}{{paramName}}: {{#required}}{{dataType}}{{/required}}{{^required}}{{#isContainer}}{{dataType}}{{/isContainer}}{{^isContainer}}Option[{{dataType}}]{{/isContainer}}{{/required}}{{^defaultValue}}{{^required}}{{^isContainer}} = scala.None{{/isContainer}}{{/required}}{{/defaultValue}}{{^-last}}, {{/-last}}{{/allParams}}){{/allParams.0}} \ No newline at end of file +{{#allParams.0}}({{#allParams}}{{paramName}}: {{#required}}{{dataType}}{{/required}}{{^required}}{{#isContainer}}{{dataType}}{{/isContainer}}{{^isContainer}}Option[{{dataType}}]{{/isContainer}}{{/required}}{{^defaultValue}}{{^required}}{{^isContainer}} = scala.None{{/isContainer}}{{/required}}{{/defaultValue}}{{^-last}}, {{/-last}}{{/allParams}}){{/allParams.0}}{{#authMethods.0}}(using Auth <:< {{#authMethods}}{{#isApiKey}}{{invokerPackage}}.Authorization.ApiKey{{/isApiKey}}{{#isBasic}}{{#isBasicBasic}}{{invokerPackage}}.Authorization.BasicAuth{{/isBasicBasic}}{{#isBasicBearer}}{{invokerPackage}}.Authorization.BearerToken{{/isBasicBearer}}{{/isBasic}}{{^-last}} | {{/-last}}{{/authMethods}}){{/authMethods.0}} \ No newline at end of file From 05706ee0fd24cf2ba263ad5d6ad559f02c19dacf Mon Sep 17 00:00:00 2001 From: Kamil-Lontkowski Date: Thu, 20 Feb 2025 12:33:53 +0100 Subject: [PATCH 13/14] fix enums entries, multipart support(partially), header, path and cookie serialization support --- .../ScalaSttp4JsoniterClientCodegen.java | 19 +- .../scala-sttp4-jsoniter/api.mustache | 12 +- .../scala-sttp4-jsoniter/helpers.mustache | 335 +++++++++++++++--- .../scala-sttp4-jsoniter/model.mustache | 2 +- .../paramCreation.mustache | 1 - .../paramMultipartCreation.mustache | 16 +- rebuild.sc | 6 +- 7 files changed, 314 insertions(+), 77 deletions(-) delete mode 100644 modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramCreation.mustache diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java index f6614b3b6287..9a3d7ecd5fa9 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4JsoniterClientCodegen.java @@ -88,8 +88,6 @@ public ScalaSttp4JsoniterClientCodegen() { GlobalFeature.LinkObjects) .excludeSchemaSupportFeatures( SchemaSupportFeature.Polymorphism) - .excludeParameterFeatures( - ParameterFeature.Cookie) .includeClientModificationFeatures( ClientModificationFeature.BasePath, ClientModificationFeature.UserAgent)); @@ -117,6 +115,7 @@ public ScalaSttp4JsoniterClientCodegen() { additionalProperties.put("fnEnumEntry", new EnumEntryLambda()); additionalProperties.put("fnCodecName", new CodecNameLambda()); additionalProperties.put("fnHandleDownload", new HandleDownloadLambda()); + additionalProperties.put("fnEnumLeaf", new EnumLeafLambda()); // TODO: there is no specific sttp mapping. All Scala Type mappings should be in // AbstractScala @@ -192,7 +191,7 @@ public String encodePath(String input) { StringBuffer buf = new StringBuffer(path.length()); Matcher matcher = Pattern.compile("[{](.*?)[}]").matcher(path); while (matcher.find()) { - matcher.appendReplacement(buf, "\\${" + toParamName(matcher.group(0)) + "}"); + matcher.appendReplacement(buf, "\\${" + toParamName(matcher.group(0)).replace("`", "") + "PathParam}"); } matcher.appendTail(buf); return buf.toString(); @@ -673,13 +672,23 @@ public String formatFragment(String fragment) { } } - private class EnumEntryLambda extends CustomLambda { + private static class EnumEntryLambda extends CustomLambda { @Override public String formatFragment(String fragment) { if (fragment.isBlank()) { return "NotPresent"; } - return formatIdentifier(fragment, true); + return "`" + fragment + "`"; + } + } + + private static class EnumLeafLambda extends CustomLambda { + @Override + public String formatFragment(String fragment) { + if (fragment.isBlank()) { + return "NotPresent"; + } + return fragment.replace("`", ""); } } diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache index 23260c7a4bd9..f71426b2872b 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/api.mustache @@ -7,6 +7,11 @@ import {{import}} import {{invokerPackage}}.JsonSupport.{*, given} import {{invokerPackage}}.FormSerializable import {{invokerPackage}}.FormStyleFormat +import {{invokerPackage}}.HeaderSerializable +import {{invokerPackage}}.ApiKeyLocation +import {{invokerPackage}}.PathStyleFormat +import {{invokerPackage}}.PathSerializable +import {{invokerPackage}}.CookieSerializable import {{invokerPackage}}.Helpers.* import sttp.client4.jsoniter.* import sttp.client4.* @@ -42,6 +47,8 @@ case class {{classname}}[Auth <: {{invokerPackage}}.Authorization] private (base {{>javadoc}} {{/javadocRenderer}} def {{operationId}}{{>methodParameters}}: sttp.client4.Request[{{#separateErrorChannel}}Either[ResponseException[String], {{>operationReturnType}}]{{/separateErrorChannel}}{{^separateErrorChannel}}{{>operationReturnType}}{{/separateErrorChannel}}] = +{{#pathParams}} val {{#fnEnumLeaf}}{{paramName}}PathParam{{/fnEnumLeaf}} = PathSerializable.serialize("{{baseName}}", {{{paramName}}}{{#style}}, PathStyleFormat.{{style.toUpperCase}}{{/style}}{{^style}}, PathStyleFormat.SIMPLE{{/style}}, {{isExplode}}) + {{/pathParams}} val requestURL = uri"$baseUrl{{{path}}}"{{#queryParams}} .addParams(FormSerializable.serialize("{{baseName}}", {{{paramName}}}{{#style}}, FormStyleFormat.{{style.toUpperCase}}{{/style}}{{^style}}, FormStyleFormat.FORM{{/style}}, {{isExplode}}): _*){{/queryParams}} @@ -49,8 +56,9 @@ case class {{classname}}[Auth <: {{invokerPackage}}.Authorization] private (base basicRequest .method(Method.{{httpMethod.toUpperCase}}, requestURL) .contentType({{#consumes.0}}"{{{mediaType}}}"{{/consumes.0}}{{^consumes}}"application/json"{{/consumes}}){{#headerParams}} - .header({{>paramCreation}}){{/headerParams}}{{#authMethods}} - .auth(authConfig{{#isApiKey}}, {{invokerPackage}}.{{#isKeyInQuery}}QUERY{{/isKeyInQuery}}{{#isKeyInHeader}}HEADER{{/isKeyInHeader}}{{#isKeyInCookie}}COOKIE{{/isKeyInCookie}}, "{{keyParamName}}"{{/isApiKey}}){{/authMethods}}{{#formParams.0}}{{^isMultipart}} + .headers(HeaderSerializable.serialize("{{baseName}}", {{paramName}}, {{isExplode}})){{/headerParams}}{{#authMethods}}{{#cookieParams}} + .cookies(CookieSerializable.serialize("{{baseName}}", {{paramName}}, {{isExplode}})){{/cookieParams}} + .auth(authConfig{{#isApiKey}}, {{invokerPackage}}.ApiKeyLocation.{{#isKeyInQuery}}QUERY{{/isKeyInQuery}}{{#isKeyInHeader}}HEADER{{/isKeyInHeader}}{{#isKeyInCookie}}COOKIE{{/isKeyInCookie}}, "{{keyParamName}}"{{/isApiKey}}){{/authMethods}}{{#formParams.0}}{{^isMultipart}} .body({{#formParams}} {{>paramFormCreation}}{{^-last}} ++ {{/-last}}{{/formParams}}, "utf-8" diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/helpers.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/helpers.mustache index 19010a3324d3..d63c42e87de8 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/helpers.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/helpers.mustache @@ -4,12 +4,10 @@ package {{invokerPackage}} import scala.deriving.* import scala.compiletime.* import java.io.File +import com.github.plokhotnyuk.jsoniter_scala.core.{JsonValueCodec, writeToString} -enum ApiKeyLocation: - case HEADER - case COOKIE - case QUERY - case NOAPIKEY +type Primitive = String | Short | Int | Long | Float | Double | BigDecimal | + Boolean enum Authorization: case NoAuthorization @@ -17,61 +15,86 @@ enum Authorization: case ApiKey(apiKey: String) case BearerToken(token: String) +enum ApiKeyLocation: + case HEADER + case COOKIE + case QUERY + case NOAPIKEY + enum FormStyleFormat: case FORM case SPACEDELIMITED case PIPEDELIMITED case DEEPOBJECT -object FormSerializable: - type Primitive = String | Short | Int | Long | Float | Double | BigDecimal | Boolean +enum PathStyleFormat: + case SIMPLE + case LABEL + case MATRIX + +inline def allLabels[T <: Tuple]: List[String] = + constValueTuple[T].toList.asInstanceOf[List[String]] + +inline def checkFields[T <: Tuple]: Unit = + inline erasedValue[T] match { + case _: EmptyTuple => () + case _: (t *: ts) => + inline erasedValue[t] match + case _: Primitive => checkFields[ts] + case _ => error("Cannot derive structure, structure must consist only of primitive fields") + } + +trait FormSerializable[T]: + inline def serialize( + name: String, + obj: T, + inline format: FormStyleFormat = FormStyleFormat.FORM, + inline explode: Boolean = true + ): Seq[(String, String)] + +object FormSerializable: inline def serialize[T]( name: String, obj: T, inline format: FormStyleFormat = FormStyleFormat.FORM, inline explode: Boolean = true - ): Seq[(String, String)] = - inline obj match - case primitive: Primitive => serializePrimitive(name, primitive, format, explode) - case array: Seq[Primitive] => serializeArray(name, array, format, explode) - case optPrimitive: Option[Primitive] => optPrimitive.map(value => serializePrimitive(name, value, format, explode)).getOrElse(Seq.empty[(String, String)]) - case optArray: Option[Seq[Primitive]] => optArray.map(serializeArray(name, _, format, explode)).getOrElse(Seq.empty[(String, String)]) - case freeObj: Map[String, Primitive] => freeObj.map((key, value) => (key, value.toString)).toSeq - case optObj: Option[t] => - inline summonInline[Mirror.Of[t]] match - case mirror: Mirror.ProductOf[t] => - checkFields[mirror.MirroredElemTypes] - val labels = allLabels[mirror.MirroredElemLabels] - optObj.map{ obj => - val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive]]).toSeq - serializeModel(name, keyVals, format, explode) - }.getOrElse(Seq.empty[(String, String)]) - case _ => error("ERROR") // TODO - support for enums - case obj => - inline summonInline[Mirror.Of[T]] match - case _: Mirror.SumOf[T] => - error("ERROR") // TODO - support for enums - case mirror: Mirror.ProductOf[T] => - checkFields[mirror.MirroredElemTypes] // Stripe ma IDGAF bo używają deepObject np. tak lines[0][tax_amounts][0][amount] - mimo tego że spec na to nie pozwala - val labels = allLabels[mirror.MirroredElemLabels] - val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive]]).toSeq - serializeModel(name, keyVals, format, explode) - - - private inline def allLabels[T <: Tuple]: List[String] = - constValueTuple[T].toList.asInstanceOf[List[String]] - - private inline def checkFields[T <: Tuple]: Unit = - inline erasedValue[T] match { - case _: EmptyTuple => () - case _: (t *: ts) => - inline erasedValue[t] match - case _: Primitive => checkFields[ts] - case _ => - error( - "Cannot derive structure, structure must consist only of primitive fields" - ) + ): Seq[(String, String)] = + summonFrom { + case t: FormSerializable[T] => t.serialize(name, obj, format, explode) + case _ => + inline obj match + case primitive: Primitive => + serializePrimitive(name, primitive, format, explode) + case array: Seq[Primitive] => + serializeArray(name, array, format, explode) + case optPrimitive: Option[Primitive] => + optPrimitive.map(value => serializePrimitive(name, value, format, explode)) + .getOrElse(Seq.empty[(String, String)]) + case optArray: Option[Seq[Primitive]] => + optArray.map(serializeArray(name, _, format, explode)) + .getOrElse(Seq.empty[(String, String)]) + case freeObj: Map[String, Primitive] => + freeObj.map((key, value) => (key, value.toString)).toSeq + case optObj: Option[t] => + inline summonInline[Mirror.Of[t]] match + case mirror: Mirror.ProductOf[t] => + checkFields[mirror.MirroredElemTypes] + val labels = allLabels[mirror.MirroredElemLabels] + optObj.map { obj => + val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive]]) + serializeModel(name, keyVals, format, explode) + }.getOrElse(Seq.empty[(String, String)]) + case mirror: Mirror.SumOf[t] => optObj.map(v => (name, writeToString(v)(summonInline[JsonValueCodec[mirror.MirroredMonoType]]))).toSeq + case obj => + inline summonInline[Mirror.Of[T]] match + case _: Mirror.SumOf[T] => + Seq((name, writeToString(obj)(summonInline[JsonValueCodec[T]]))) + case mirror: Mirror.ProductOf[T] => + checkFields[mirror.MirroredElemTypes] // Stripe ma IDGAF bo używają deepObject np. tak lines[0][tax_amounts][0][amount] - mimo tego że spec na to nie pozwala + val labels = allLabels[mirror.MirroredElemLabels] + val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive]]).toSeq + serializeModel(name, keyVals, format, explode) } private inline def serializePrimitive( @@ -84,9 +107,7 @@ object FormSerializable: case FormStyleFormat.FORM => Seq(paramName -> value.toString) // for primitve values explode does not change anything case FormStyleFormat.SPACEDELIMITED => - error( - "FormStyleFormat.SpaceDelimited does not support primitive values" - ) + error("FormStyleFormat.SpaceDelimited does not support primitive values") case FormStyleFormat.PIPEDELIMITED => error("FormStyleFormat.PipeDelimited does not support primitive values") case FormStyleFormat.DEEPOBJECT => @@ -98,7 +119,7 @@ object FormSerializable: values: Seq[Primitive], inline format: FormStyleFormat, inline explode: Boolean - ): Seq[(String, String)] = { + ): Seq[(String, String)] = { inline format match case FormStyleFormat.FORM => inline if explode then values.map(s => (paramName, s.toString)) @@ -117,31 +138,229 @@ object FormSerializable: keyValPairs: Seq[(String, Primitive)], inline format: FormStyleFormat, inline explode: Boolean - ): Seq[(String, String)] = { + ): Seq[(String, String)] = { inline format match case FormStyleFormat.FORM => inline if explode then keyValPairs.map((key, value) => (key, value.toString)) - else Seq(paramName -> keyValPairs.flatMap((key,value) => Seq(key, value.toString)).mkString(",")) + else Seq(paramName -> keyValPairs.flatMap((key, value) => Seq(key, value.toString)).mkString(",")) case FormStyleFormat.SPACEDELIMITED => error("FormStyleFormat.SpaceDelimited does not support objects") case FormStyleFormat.PIPEDELIMITED => error("FormStyleFormat.PipeDelimited does not support objects") case FormStyleFormat.DEEPOBJECT => - inline if explode then - keyValPairs.map((key, value) => (s"$paramName[$key]", value.toString)) + inline if explode then keyValPairs.map((key, value) => (s"$paramName[$key]", value.toString)) else error("FormStyleFormat.DeepObject does not support explode=false") } end FormSerializable +trait HeaderSerializable[T]: + inline def serialize( + name: String, + obj: T, + inline explode: Boolean = true + ): Map[String, String] + +object HeaderSerializable: + inline def serialize[T]( + name: String, + obj: T, + inline explode: Boolean = true + ): Map[String, String] = + summonFrom { + case t: HeaderSerializable[T] => t.serialize(name, obj, explode) + case _ => inline obj match + case primitive: Primitive => Map(name -> primitive.toString) + case optPrimitive: Option[Primitive] => optPrimitive.map(v => Map(name -> v.toString)).getOrElse(Map.empty[String, String]) + case seqPrimitive: Seq[Primitive] => Map(name -> seqPrimitive.map(_.toString).mkString(",")) + case optSeqPrimitive: Option[Seq[Primitive]] => optSeqPrimitive.map(v => Map(name -> v.map(_.toString).mkString(","))).getOrElse(Map.empty[String, String]) + case mapPrimitive: Map[String, Primitive] => mapPrimitive.map((k, v) => (k, v.toString)) + case optObj: Option[t] => + inline summonInline[Mirror.Of[t]] match + case mirror: Mirror.ProductOf[t] => + checkFields[mirror.MirroredElemTypes] + val labels = allLabels[mirror.MirroredElemLabels] + optObj.map { obj => + val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive]].map(_.toString)) + inline if explode then + Map(name ->keyVals.map((k, v) => s"$k=$v").mkString(",")) + else + Map(name -> keyVals.flatMap((k, v) => Seq(k, v)).mkString(",")) + }.getOrElse(Map.empty[String, String]) + case mirror: Mirror.SumOf[t] => optObj.map(v => Map(name -> writeToString(v)(summonInline[JsonValueCodec[mirror.MirroredMonoType]]))).getOrElse(Map.empty[String, String]) + case obj: T => + inline summonInline[Mirror.Of[T]] match + case mirror: Mirror.ProductOf[T] => + checkFields[mirror.MirroredElemTypes] + val labels = allLabels[mirror.MirroredElemLabels] + val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive]].map(_.toString)) + inline if explode then + Map(name ->keyVals.map((k, v) => s"$k=$v").mkString(",")) + else + Map(name -> keyVals.flatMap((k, v) => Seq(k, v)).mkString(",")) + case mirror: Mirror.SumOf[T] => Map(name -> writeToString(obj)(summonInline[JsonValueCodec[mirror.MirroredMonoType]])) + } +end HeaderSerializable + +trait PathSerializer[T]: + inline def serialize[T](name: String, obj: T, inline style: PathStyleFormat, inline explode: Boolean): String + +object PathSerializable: + inline def serialize[T](name: String, obj: T, inline style: PathStyleFormat, inline explode: Boolean): String = + summonFrom { + case t: PathSerializer[T] => t.serialize(name, obj, style, explode) + case _ => + inline obj match + case primitive: Primitive => + serializePrimitive(name, primitive, style, explode) + case array: Seq[Primitive] => + serializeArray(name, array, style, explode) + case optPrimitive: Option[Primitive] => + optPrimitive.map(value => serializePrimitive(name, value, style, explode)) + .getOrElse("") + case optArray: Option[Seq[Primitive]] => + optArray.map(serializeArray(name, _, style, explode)) + .getOrElse("") + case freeObj: Map[String, Primitive] => + serializeModel(name, freeObj.map((key, value) => (key, value.toString)).toSeq, style, explode) + case optObj: Option[t] => + inline summonInline[Mirror.Of[t]] match + case mirror: Mirror.ProductOf[t] => + checkFields[mirror.MirroredElemTypes] + val labels = allLabels[mirror.MirroredElemLabels] + optObj.map { obj => + val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive]]) + serializeModel(name, keyVals, style, explode) + }.getOrElse("") + case mirror: Mirror.SumOf[t] => optObj.map(writeToString(_)(summonInline[JsonValueCodec[mirror.MirroredMonoType]])).getOrElse("") + case obj => + inline summonInline[Mirror.Of[T]] match + case _: Mirror.SumOf[T] => + writeToString(obj)(summonInline[JsonValueCodec[T]]) + case mirror: Mirror.ProductOf[T] => + checkFields[mirror.MirroredElemTypes] + val labels = allLabels[mirror.MirroredElemLabels] + val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive]]).toSeq + serializeModel(name, keyVals, style, explode) + } + + private inline def serializePrimitive( + paramName: String, + value: Primitive, + inline format: PathStyleFormat, + inline explode: Boolean + ): String = inline format match + case PathStyleFormat.SIMPLE => value.toString + case PathStyleFormat.LABEL => s".${value.toString}" + case PathStyleFormat.MATRIX => s";$paramName=${value.toString}" + + private inline def serializeArray( + paramName: String, + values: Seq[Primitive], + inline format: PathStyleFormat, + inline explode: Boolean + ): String = inline format match + case PathStyleFormat.SIMPLE => values.map(_.toString).mkString(",") + case PathStyleFormat.LABEL => inline if explode then values.map(_.toString).mkString(".", ".", "") else values.map(_.toString).mkString(".", ",", "") + case PathStyleFormat.MATRIX => inline if explode then values.map(v => s";$paramName=${v.toString}").mkString else s";$paramName=" + values.map(_.toString).mkString(",") + + private inline def serializeModel( + paramName: String, + keyValPairs: Seq[(String, Primitive)], + inline format: PathStyleFormat, + inline explode: Boolean + ): String = inline format match + case PathStyleFormat.SIMPLE => + inline if explode then keyValPairs.map((k, v) => s"$k=${v.toString}").mkString(",") + else keyValPairs.map((k, v) => s"$k,${v.toString}").mkString(",") + case PathStyleFormat.LABEL => + inline if explode then keyValPairs.map((k, v) => s"$k=${v.toString}").mkString(".", ".", "") + else keyValPairs.map((k, v) => s"$k,${v.toString}").mkString(".", ",", "") + case PathStyleFormat.MATRIX => + inline if explode then keyValPairs.map((k, v) => s";$k=${v.toString}").mkString + else keyValPairs.map((k, v) => s"$k,${v.toString}").mkString(s";$paramName=", ",", "") +end PathSerializable + +trait CookieSerializable[T]: + inline def serialize( + name: String, + obj: T, + inline explode: Boolean = true + ): Seq[(String, String)] + +object CookieSerializable: + inline def serialize[T]( + name: String, + obj: T, + inline explode: Boolean = true + ): Seq[(String, String)] = + summonFrom { + case t: CookieSerializable[T] => t.serialize(name, obj, explode) + case _ => + inline obj match + case primitive: Primitive => + serializePrimitive(name, primitive, explode) + case array: Seq[Primitive] => + serializeArray(name, array, explode) + case optPrimitive: Option[Primitive] => + optPrimitive.map(value => serializePrimitive(name, value, explode)) + .getOrElse(Seq.empty[(String, String)]) + case optArray: Option[Seq[Primitive]] => + optArray.map(serializeArray(name, _, explode)) + .getOrElse(Seq.empty[(String, String)]) + case freeObj: Map[String, Primitive] => + serializeModel(name, freeObj.map((key, value) => (key, value.toString)).toSeq, explode) + case optObj: Option[t] => + inline summonInline[Mirror.Of[t]] match + case mirror: Mirror.ProductOf[t] => + checkFields[mirror.MirroredElemTypes] + val labels = allLabels[mirror.MirroredElemLabels] + optObj.map { obj => + val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive]]) + serializeModel(name, keyVals, explode) + }.getOrElse(Seq.empty[(String, String)]) + case mirror: Mirror.SumOf[t] => optObj.map(v => (name, writeToString(v)(summonInline[JsonValueCodec[mirror.MirroredMonoType]]))).toSeq + case obj => + inline summonInline[Mirror.Of[T]] match + case _: Mirror.SumOf[T] => + Seq(name -> writeToString(obj)(summonInline[JsonValueCodec[T]])) + case mirror: Mirror.ProductOf[T] => + checkFields[mirror.MirroredElemTypes] + val labels = allLabels[mirror.MirroredElemLabels] + val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive]]).toSeq + serializeModel(name, keyVals, explode) + } + + private inline def serializePrimitive( + paramName: String, + value: Primitive, + inline explode: Boolean + ): Seq[(String, String)] = Seq(paramName -> value.toString) + + private inline def serializeArray( + paramName: String, + values: Seq[Primitive], + inline explode: Boolean + ): Seq[(String, String)] = + inline if explode then error("Not supported") + else Seq(paramName -> values.map(_.toString).mkString(",")) + + private inline def serializeModel( + paramName: String, + keyValPairs: Seq[(String, Primitive)], + inline explode: Boolean + ): Seq[(String, String)] = + inline if explode then error("Not supported") + else Seq(paramName -> keyValPairs.map((k, v) => s"$k,$v").mkString(",")) +end CookieSerializable + object Helpers: extension (request: sttp.client4.Request[?]) def fileBody(file: Option[File] | File): sttp.client4.Request[?] = file match - case f: File => request.body(f) + case f: File => request.body(f) case f: Option[File] => f.map(request.body(_)).getOrElse(request) def auth(authConfig: Authorization, location: ApiKeyLocation = ApiKeyLocation.NOAPIKEY, keyParamName: String = ""): sttp.client4.Request[?] = - // multiple calls to auth with the same config are not idempotent only for APIKey with COOKIE and QUERY - that's why we have NOAPIKEY as default authConfig match case Authorization.NoAuthorization => request case Authorization.BasicAuth(username, password) => request.auth.basic(username, password) diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache index 7ca532648e94..2fd923d042c3 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache @@ -85,7 +85,7 @@ object {{classname}}Enums: .withAdtLeafClassNameMapper { x => JsonCodecMaker.simpleClassName(x) match {{#_enum}} - case "{{#fnEnumEntry}}{{.}}{{/fnEnumEntry}}" => "{{.}}" + case "{{#fnEnumLeaf}}{{.}}{{/fnEnumLeaf}}" => "{{.}}" {{/_enum}} } .withDiscriminatorFieldName(scala.None) diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramCreation.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramCreation.mustache deleted file mode 100644 index 88aa503ba7a2..000000000000 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramCreation.mustache +++ /dev/null @@ -1 +0,0 @@ -"{{baseName}}", {{#isContainer}}Some(ArrayValues({{{paramName}}}{{#collectionFormat}}, CollectionFormats.{{collectionFormat.toUpperCase}}{{/collectionFormat}}).toString()){{/isContainer}}{{^isContainer}}Some({{{paramName}}}.toString()){{/isContainer}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramMultipartCreation.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramMultipartCreation.mustache index 21bd8c1a1e4a..1f43245d3fe1 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramMultipartCreation.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/paramMultipartCreation.mustache @@ -1,16 +1,18 @@ {{#required}} {{#isFile}} - Some(multipartFile("{{baseName}}", {{#isContainer}}ArrayValues({{{paramName}}}{{#collectionFormat}}, CollectionFormats.{{collectionFormat.toUpperCase}}{{/collectionFormat}}).toString(){{/isContainer}}{{^isContainer}}{{{paramName}}}.toString(){{/isContainer}})){{^-last}},{{/-last}} + Some(multipartFile("{{baseName}}", {{paramName}})) {{/isFile}} - {{^isFile}} - Some({{paramName}}).map(_.toString).map(multipart("{{baseName}}", _)){{^-last}},{{/-last}} + {{^isFile}}{{#isFormParam}}{{^isPrimitiveType}} + Some(multipart("{{baseName}}", FormSerializable.serialize("{{baseName}}", {{{paramName}}}{{#style}}, FormStyleFormat.{{style.toUpperCase}}{{/style}}{{^style}}, FormStyleFormat.FORM{{/style}}, {{isExplode}}))){{/isPrimitiveType}}{{/isFormParam}}{{#isPrimitiveType}} + Some(multipart("{{baseName}}", {{paramName}}.toString)){{/isPrimitiveType}} {{/isFile}} {{/required}} {{^required}} {{#isFile}} - {{paramName}}.map(multipartFile("{{baseName}}", _)){{^-last}},{{/-last}} + {{paramName}}.map(multipartFile("{{baseName}}", _)) {{/isFile}} - {{^isFile}} - {{paramName}}{{^isContainer}}.map(_.toString){{/isContainer}}.map(multipart("{{baseName}}", {{#isContainer}}ArrayValues(_{{#collectionFormat}}, CollectionFormats.{{collectionFormat.toUpperCase}}{{/collectionFormat}}).toString(){{/isContainer}}{{^isContainer}}_{{/isContainer}})){{^-last}},{{/-last}} + {{^isFile}}{{#isFormParam}}{{^isPrimitiveType}} + {{paramName}}.map(value => multipart("{{baseName}}", FormSerializable.serialize("{{baseName}}", value{{#style}}, FormStyleFormat.{{style.toUpperCase}}{{/style}}{{^style}}, FormStyleFormat.FORM{{/style}}, {{isExplode}}))){{/isPrimitiveType}}{{/isFormParam}}{{#isPrimitiveType}} + {{paramName}}.map(value => multipart("{{baseName}}", value.toString)){{/isPrimitiveType}} {{/isFile}} -{{/required}} +{{/required}}{{^-last}},{{/-last}} diff --git a/rebuild.sc b/rebuild.sc index 77667bf07ae6..0b066ab27a44 100755 --- a/rebuild.sc +++ b/rebuild.sc @@ -54,9 +54,9 @@ val projects = Map( projectVersion = "1.0.0-SNAPSHOT", generatorName = "scala-sttp4-jsoniter", skipValidate = false, - schemaMappings = "io.k8s.apimachinery.pkg.util.intstr.IntOrString=IntOrString", - typeMappings = "IntOrString=IntOrString", - importMappings = "IntOrString=ma.chinespirit.kube.ext.IntOrString" + schemaMappings = "", + typeMappings = "", + importMappings = "" ), "stripe" -> Project( project = "stripe-scala", From 0a9c2a8e9b92dc5fb992053bb6bdb767a4d65918 Mon Sep 17 00:00:00 2001 From: Kamil-Lontkowski Date: Mon, 17 Mar 2025 13:12:02 +0100 Subject: [PATCH 14/14] support of option fields --- .../scala-sttp4-jsoniter/helpers.mustache | 40 ++++++++++++++----- .../scala-sttp4-jsoniter/model.mustache | 2 +- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/helpers.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/helpers.mustache index d63c42e87de8..48967ba5b3d2 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/helpers.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/helpers.mustache @@ -35,15 +35,21 @@ enum PathStyleFormat: inline def allLabels[T <: Tuple]: List[String] = constValueTuple[T].toList.asInstanceOf[List[String]] -inline def checkFields[T <: Tuple]: Unit = +private inline def checkFields[T <: Tuple]: Unit = inline erasedValue[T] match { case _: EmptyTuple => () case _: (t *: ts) => inline erasedValue[t] match case _: Primitive => checkFields[ts] + case _: Option[Primitive] => checkFields[ts] case _ => error("Cannot derive structure, structure must consist only of primitive fields") } +private val flattenKeyVals: Primitive | Option[Primitive] => Option[Primitive] = { + case p: Primitive => Some(p) + case opt: Option[Primitive] => opt +} + trait FormSerializable[T]: inline def serialize( name: String, @@ -82,7 +88,9 @@ object FormSerializable: checkFields[mirror.MirroredElemTypes] val labels = allLabels[mirror.MirroredElemLabels] optObj.map { obj => - val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive]]) + val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive | Option[Primitive]]].map(flattenKeyVals)) + .filter((_, v) => v.isDefined) + .map((k, v) => (k, v.get)) serializeModel(name, keyVals, format, explode) }.getOrElse(Seq.empty[(String, String)]) case mirror: Mirror.SumOf[t] => optObj.map(v => (name, writeToString(v)(summonInline[JsonValueCodec[mirror.MirroredMonoType]]))).toSeq @@ -93,7 +101,9 @@ object FormSerializable: case mirror: Mirror.ProductOf[T] => checkFields[mirror.MirroredElemTypes] // Stripe ma IDGAF bo używają deepObject np. tak lines[0][tax_amounts][0][amount] - mimo tego że spec na to nie pozwala val labels = allLabels[mirror.MirroredElemLabels] - val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive]]).toSeq + val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive | Option[Primitive]]].map(flattenKeyVals)) + .filter((_, v) => v.isDefined) + .map((k, v) => (k, v.get)) serializeModel(name, keyVals, format, explode) } @@ -180,7 +190,9 @@ object HeaderSerializable: checkFields[mirror.MirroredElemTypes] val labels = allLabels[mirror.MirroredElemLabels] optObj.map { obj => - val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive]].map(_.toString)) + val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive | Option[Primitive]]].map(flattenKeyVals)) + .filter((_, v) => v.isDefined) + .map((k, v) => (k, v.get.toString)) inline if explode then Map(name ->keyVals.map((k, v) => s"$k=$v").mkString(",")) else @@ -192,7 +204,9 @@ object HeaderSerializable: case mirror: Mirror.ProductOf[T] => checkFields[mirror.MirroredElemTypes] val labels = allLabels[mirror.MirroredElemLabels] - val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive]].map(_.toString)) + val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive | Option[Primitive]]].map(flattenKeyVals)) + .filter((_, v) => v.isDefined) + .map((k, v) => (k, v.get.toString)) inline if explode then Map(name ->keyVals.map((k, v) => s"$k=$v").mkString(",")) else @@ -228,7 +242,9 @@ object PathSerializable: checkFields[mirror.MirroredElemTypes] val labels = allLabels[mirror.MirroredElemLabels] optObj.map { obj => - val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive]]) + val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive | Option[Primitive]]].map(flattenKeyVals)) + .filter((_, v) => v.isDefined) + .map((k, v) => (k, v.get)) serializeModel(name, keyVals, style, explode) }.getOrElse("") case mirror: Mirror.SumOf[t] => optObj.map(writeToString(_)(summonInline[JsonValueCodec[mirror.MirroredMonoType]])).getOrElse("") @@ -239,7 +255,9 @@ object PathSerializable: case mirror: Mirror.ProductOf[T] => checkFields[mirror.MirroredElemTypes] val labels = allLabels[mirror.MirroredElemLabels] - val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive]]).toSeq + val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive | Option[Primitive]]].map(flattenKeyVals)) + .filter((_, v) => v.isDefined) + .map((k, v) => (k, v.get)) serializeModel(name, keyVals, style, explode) } @@ -315,7 +333,9 @@ object CookieSerializable: checkFields[mirror.MirroredElemTypes] val labels = allLabels[mirror.MirroredElemLabels] optObj.map { obj => - val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive]]) + val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive | Option[Primitive]]].map(flattenKeyVals)) + .filter((_, v) => v.isDefined) + .map((k, v) => (k, v.get)) serializeModel(name, keyVals, explode) }.getOrElse(Seq.empty[(String, String)]) case mirror: Mirror.SumOf[t] => optObj.map(v => (name, writeToString(v)(summonInline[JsonValueCodec[mirror.MirroredMonoType]]))).toSeq @@ -326,7 +346,9 @@ object CookieSerializable: case mirror: Mirror.ProductOf[T] => checkFields[mirror.MirroredElemTypes] val labels = allLabels[mirror.MirroredElemLabels] - val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive]]).toSeq + val keyVals = labels.zip(obj.asInstanceOf[Product].productIterator.toSeq.asInstanceOf[Seq[Primitive | Option[Primitive]]].map(flattenKeyVals)) + .filter((_, v) => v.isDefined) + .map((k, v) => (k, v.get)) serializeModel(name, keyVals, explode) } diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache index 2fd923d042c3..26e90554e735 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4-jsoniter/model.mustache @@ -44,7 +44,7 @@ object {{classname}}: .withAdtLeafClassNameMapper { x => JsonCodecMaker.simpleClassName(x) match {{#values}} - case "{{#fnEnumEntry}}{{.}}{{/fnEnumEntry}}" => "{{.}}" + case "{{#fnEnumLeaf}}{{.}}{{/fnEnumLeaf}}" => "{{.}}" {{/values}} }{{/allowableValues}} .withDiscriminatorFieldName(scala.None)