diff --git a/.github/workflows/samples-scala.yaml b/.github/workflows/samples-scala.yaml index 584cb7ea3b1..9e9eb8c249e 100644 --- a/.github/workflows/samples-scala.yaml +++ b/.github/workflows/samples-scala.yaml @@ -31,6 +31,7 @@ jobs: - samples/server/petstore/scala-pekko-http-server - samples/server/petstore/scalatra - samples/server/petstore/scala-finch # cannot be tested with jdk11 + - samples/server/petstore/scala-http4s-server steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 diff --git a/bin/configs/scala-http4s-server.yaml b/bin/configs/scala-http4s-server.yaml new file mode 100644 index 00000000000..20435c43769 --- /dev/null +++ b/bin/configs/scala-http4s-server.yaml @@ -0,0 +1,6 @@ +generatorName: scala-http4s-server +outputDir: samples/server/petstore/scala-http4s-server +inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore.yaml +templateDir: modules/openapi-generator/src/main/resources/scala-http4s-server +additionalProperties: + artifactId: openapi-scala-http4s-server \ No newline at end of file diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaHttp4sServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaHttp4sServerCodegen.java new file mode 100644 index 00000000000..6eee8734975 --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaHttp4sServerCodegen.java @@ -0,0 +1,852 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openapitools.codegen.languages; + +import io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.media.Schema; +import org.openapitools.codegen.*; +import org.openapitools.codegen.meta.features.*; +import org.openapitools.codegen.model.*; +import org.openapitools.codegen.utils.ModelUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.*; +import java.util.stream.Collectors; + +public class ScalaHttp4sServerCodegen extends DefaultCodegen implements CodegenConfig { + private final Logger LOGGER = LoggerFactory.getLogger(ScalaHttp4sServerCodegen.class); + protected String artifactId = "http4s-server"; + protected String artifactVersion = "1.0.0"; + protected String sourceFolder = "scala"; + protected String sourceSubFolder = "main"; + private String packageName = "org.openapitools"; + + public static final String EXCLUDE_SBT = "excludeSbt"; // generate as whole project + public static final String SOURCE_SUBFOLDER = "sourceSubfolder"; // generate as whole project + + public ScalaHttp4sServerCodegen() { + super(); + + modifyFeatureSet(features -> features + .wireFormatFeatures(EnumSet.of(WireFormatFeature.JSON, WireFormatFeature.XML, WireFormatFeature.Custom)) + .securityFeatures(EnumSet.noneOf(SecurityFeature.class)) + .excludeGlobalFeatures( + GlobalFeature.XMLStructureDefinitions, + GlobalFeature.Callbacks, + GlobalFeature.LinkObjects, + GlobalFeature.ParameterStyling + ) + .excludeSchemaSupportFeatures( + SchemaSupportFeature.Polymorphism + ) + .excludeParameterFeatures( + ParameterFeature.Cookie + ) + ); + + embeddedTemplateDir = templateDir = "scala-http4s-server"; + + apiPackage = packageName + ".apis"; + modelPackage = packageName + ".models"; + useOneOfInterfaces = true; + supportsMultipleInheritance = true; + supportsInheritance = true; + supportsMixins = true; + addOneOfInterfaceImports =true; + + + setReservedWordsLowerCase( + Arrays.asList( + // Scala + "abstract", "case", "catch", "class", "def", + "do", "else", "extends", "false", "final", + "finally", "for", "forSome", "if", "implicit", + "import", "lazy", "match", "new", "null", + "object", "override", "package", "private", "protected", + "return", "sealed", "super", "this", "throw", + "trait", "try", "true", "type", "val", + "var", "while", "with", "yield", + // Scala-interop languages keywords + "abstract", "continue", "switch", "assert", + "default", "synchronized", "goto", + "break", "double", "implements", "byte", + "public", "throws", "enum", "instanceof", "transient", + "int", "short", "char", "interface", "static", + "void", "finally", "long", "strictfp", "volatile", "const", "float", + "native") + ); + + defaultIncludes = new HashSet<>( + Arrays.asList("double", + "Int", + "Long", + "Float", + "Double", + "char", + "float", + "String", + "boolean", + "Boolean", + "Double", + "Integer", + "Long", + "Float", + "List", + "Set", + "Map") + ); + + typeMapping = new HashMap<>(); + typeMapping.put("string", "String"); + typeMapping.put("boolean", "Boolean"); + typeMapping.put("integer", "Int"); + typeMapping.put("long", "Long"); + typeMapping.put("float", "Float"); + typeMapping.put("double", "Double"); + typeMapping.put("number", "BigDecimal"); + typeMapping.put("decimal", "BigDecimal"); + typeMapping.put("date-time", "ZonedDateTime"); + typeMapping.put("offset-date-time", "OffsetDateTime"); + typeMapping.put("date", "LocalDate"); + typeMapping.put("file", "File"); + typeMapping.put("array", "List"); + typeMapping.put("list", "List"); + typeMapping.put("map", "Map"); + typeMapping.put("object", "Object"); + typeMapping.put("binary", "Array[Byte]"); + typeMapping.put("Date", "LocalDate"); + typeMapping.put("DateTime", "ZonedDateTime"); + typeMapping.put("OffsetDateTime", "OffsetDateTime"); + typeMapping.put("uuid", "UUID"); + + additionalProperties.put("modelPackage", modelPackage()); + additionalProperties.put("apiPackage", apiPackage()); + additionalProperties.put("infoUrl", "http://org.openapitools"); + additionalProperties.put("infoEmail", "team@openapitools.org"); + additionalProperties.put("licenseInfo", "Apache 2.0"); + additionalProperties.put("licenseUrl", "http://apache.org/licenses/LICENSE-2.0.html"); + + + + languageSpecificPrimitives = new HashSet<>( + Arrays.asList( + "String", + "Boolean", + "Double", + "Int", + "Integer", + "Long", + "Float", + "Any", + "AnyVal", + "AnyRef", + "Object", + "BigDecimal" + ) + ); + instantiationTypes.put("array", "ArrayList"); + instantiationTypes.put("map", "HashMap"); + + importMapping = new HashMap<>(); + importMapping.put("UUID", "java.util.UUID"); + importMapping.put("URI", "java.net.URI"); + importMapping.put("File", "java.io.File"); + importMapping.put("Date", "java.util.Date"); + importMapping.put("Timestamp", "java.sql.Timestamp"); + importMapping.put("Map", "scala.collection.immutable.Map"); + importMapping.put("HashMap", "scala.collection.immutable.HashMap"); + importMapping.put("Seq", "scala.collection.immutable.Seq"); + importMapping.put("ArrayBuffer", "scala.collection.mutable.ArrayBuffer"); + importMapping.put("DateTime", "java.time.LocalDateTime"); + importMapping.put("LocalDateTime", "java.time.LocalDateTime"); + importMapping.put("LocalDate", "java.time.LocalDate"); + importMapping.put("LocalTime", "java.time.LocalTime"); + importMapping.put("ZonedDateTime", "java.time.ZonedDateTime"); + importMapping.put("OffsetDateTime", "java.time.OffsetDateTime"); + //refined + importMapping.put("Refined", "eu.timepit.refined.api.Refined"); + importMapping.put("And", "eu.timepit.refined.boolean.And"); + importMapping.put("MinSize", "eu.timepit.refined.collection.MinSize"); + importMapping.put("MaxSize", "eu.timepit.refined.collection.MaxSize"); + importMapping.put("MatchesRegex", "eu.timepit.refined.string.MatchesRegex"); + importMapping.put("Greater", "eu.timepit.refined.numeric.Greater"); + importMapping.put("GreaterEqual", "eu.timepit.refined.numeric.GreaterEqual"); + importMapping.put("Less", "eu.timepit.refined.numeric.Less"); + importMapping.put("LessEqual", "eu.timepit.refined.numeric.LessEqual"); + + + cliOptions.add(new CliOption(EXCLUDE_SBT, "exclude sbt from generation")); + cliOptions.add(new CliOption(SOURCE_SUBFOLDER, "name of subfolder, for example to generate code in src/scala/generated")); + + inlineSchemaOption.put("SKIP_SCHEMA_REUSE", "true"); + inlineSchemaOption.put("REFACTOR_ALLOF_INLINE_SCHEMAS", "true"); + } + + private final static Map locationStatusToResponse = new HashMap<>(); + static { + locationStatusToResponse.put("300", "MultipleChoices"); + locationStatusToResponse.put("301", "MovedPermanently"); + locationStatusToResponse.put("302", "Found"); + locationStatusToResponse.put("303", "SeeOther"); + locationStatusToResponse.put("307", "TemporaryRedirect"); + locationStatusToResponse.put("308", "PermanentRedirect"); + } + + private final static Map wwwAuthStatusToResponse = new HashMap<>(); + static { + wwwAuthStatusToResponse.put("401", "Unauthorized"); + } + + private final static Map allowStatusToResponse = new HashMap<>(); + static { + allowStatusToResponse.put("405", "MethodNotAllowed"); + } + + private final static Map proxyAuthStatusToResponse = new HashMap<>(); + static { + proxyAuthStatusToResponse.put("407", "ProxyAuthenticationRequired"); + } + + private final static Map statusToResponse = new HashMap<>(); + + static { + statusToResponse.put("100", "Continue"); + + statusToResponse.put("101", "SwitchingProtocols"); + + statusToResponse.put("102", "Processing"); + statusToResponse.put("103", "EarlyHints"); + + statusToResponse.put("200", "Ok"); + statusToResponse.put("201", "Created"); + statusToResponse.put("202", "Accepted"); + statusToResponse.put("203", "NonAuthoritativeInformation"); + statusToResponse.put("204", "NoContent"); + statusToResponse.put("205", "ResetContent"); + statusToResponse.put("206", "PartialContent"); + statusToResponse.put("207", "MultiStatus"); + statusToResponse.put("208", "AlreadyReported"); + statusToResponse.put("226", "IMUsed"); + + statusToResponse.put("304", "NotModified"); + statusToResponse.put("305", "UseProxy"); + + statusToResponse.put("400", "BadRequest"); + statusToResponse.put("402", "PaymentRequired"); + statusToResponse.put("403", "Forbidden"); + statusToResponse.put("404", "NotFound"); + statusToResponse.put("406", "NotAcceptable"); + statusToResponse.put("408", "RequestTimeout"); + statusToResponse.put("409", "Conflict"); + statusToResponse.put("410", "Gone"); + statusToResponse.put("411", "LengthRequired"); + statusToResponse.put("412", "PreconditionFailed"); + statusToResponse.put("413", "PayloadTooLarge"); + statusToResponse.put("414", "UriTooLong"); + statusToResponse.put("415", "UnsupportedMediaType"); + statusToResponse.put("416", "RangeNotSatisfiable"); + statusToResponse.put("417", "ExpectationFailed"); + statusToResponse.put("418", "ImATeapot"); + statusToResponse.put("421", "MisdirectedRequest"); + statusToResponse.put("422", "UnprocessableEntity"); + statusToResponse.put("423", "Locked"); + statusToResponse.put("424", "FailedDependency"); + statusToResponse.put("425", "TooEarly"); + statusToResponse.put("426", "UpgradeRequired"); + statusToResponse.put("428", "PreconditionRequired"); + statusToResponse.put("429", "TooManyRequests"); + statusToResponse.put("431", "RequestHeaderFieldsTooLarge"); + statusToResponse.put("451", "UnavailableForLegalReasons"); + + statusToResponse.put("500", "InternalServerError"); + statusToResponse.put("501", "NotImplemented"); + statusToResponse.put("502", "BadGateway"); + statusToResponse.put("503", "ServiceUnavailable"); + statusToResponse.put("504", "GatewayTimeout"); + statusToResponse.put("505", "HttpVersionNotSupported"); + statusToResponse.put("506", "VariantAlsoNegotiates"); + statusToResponse.put("507", "InsufficientStorage"); + statusToResponse.put("508", "LoopDetected"); + statusToResponse.put("510", "NotExtended"); + statusToResponse.put("511", "NetworkAuthenticationRequired"); + } + + @Override + public void processOpts() { + super.processOpts(); + if (additionalProperties.containsKey(CodegenConstants.PACKAGE_NAME)) { + packageName = (String) additionalProperties.get(CodegenConstants.PACKAGE_NAME); + + setApiPackage(packageName + ".apis"); + additionalProperties.put(CodegenConstants.API_PACKAGE, apiPackage()); + + setModelPackage(packageName + ".models"); + additionalProperties.put(CodegenConstants.PACKAGE_NAME, modelPackage()); + } else { + additionalProperties.put(CodegenConstants.PACKAGE_NAME, packageName); + } + + if (additionalProperties.containsKey(SOURCE_SUBFOLDER)) { + sourceSubFolder = (String) additionalProperties.get(SOURCE_SUBFOLDER); + } + + sourceFolder = "src" + File.separator + sourceSubFolder + File.separator + sourceFolder; + + supportingFiles.add(new SupportingFile("types.mustache", modelFileFolderRelative(), "types.scala")); + supportingFiles.add(new SupportingFile("path.mustache", apiFileFolderRelative(), "path.scala")); + supportingFiles.add(new SupportingFile("query.mustache", apiFileFolderRelative(), "query.scala")); + + supportingFiles.add(new SupportingFile("apis.mustache", packageFileFolderRelative(), "api.scala")); + + apiTemplateFiles.put("api.mustache", ".scala"); + + if (!additionalProperties.containsKey(EXCLUDE_SBT) && !Boolean.parseBoolean((String)additionalProperties.get(EXCLUDE_SBT))) { + supportingFiles.add(new SupportingFile("build.sbt", "", "build.sbt")); + supportingFiles.add(new SupportingFile("build.properties", "project", "build.properties")); + } + } + + @Override + public Map inlineSchemaOption() { + return super.inlineSchemaOption(); + } + + + @Override + public boolean isEnablePostProcessFile() { + return true; + } + + @Override + public void postProcessFile(File file, String fileType) { + System.out.println("postprocess " + file.toString()); + super.postProcessFile(file, fileType); + } + + @Override + public Map postProcessAllModels(Map objs) { + Map modelsMap = super.postProcessAllModels(objs); + + for (ModelsMap mm : modelsMap.values()) { + for (ModelMap model : mm.getModels()) { + // model oneOf as sealed trait + + CodegenModel cModel = model.getModel(); + cModel.getVendorExtensions().put("x-isSealedTrait", !cModel.oneOf.isEmpty()); + + if (cModel.discriminator != null) { + cModel.getVendorExtensions().put("x-use-discr", true); + + if (cModel.discriminator.getMapping() != null) { + cModel.getVendorExtensions().put("x-use-discr-mapping", true); + } + } + // + try { + List exts = (List) cModel.getVendorExtensions().get("x-implements"); + if (exts != null) { + cModel.getVendorExtensions().put("x-extends", exts.subList(0, 1)); + cModel.getVendorExtensions().put("x-extendsWith", exts.subList(1, exts.size())); + } + } catch (IndexOutOfBoundsException ignored) { + } + + // add refined constraints + + for (CodegenProperty prop: cModel.vars) { + Set imports = new TreeSet<>(); + + prop.getVendorExtensions().putAll(refineProp(prop, imports)); + + cModel.imports.addAll(imports); + } + } + } + return modelsMap; + } + + private Map makeRefiined(Set imports, String dataType, ArrayList refined) { + Map vendorExtensions = new HashMap<>(); + if (!refined.isEmpty()) { + imports.add("And"); + imports.add("Refined"); + + String refinedRgt = String.join(" And ", refined); + + vendorExtensions.put("x-type", "Refined[" + dataType + ", " + refinedRgt + "]"); + vendorExtensions.put("x-refined-lft", dataType); + vendorExtensions.put("x-refined-rgt", refinedRgt); + vendorExtensions.put("x-refined", true); + } else { + vendorExtensions.put("x-type", dataType); + } + + return vendorExtensions; + } + + private Map refineProp(IJsonSchemaValidationProperties prop, Set imports) { + Map vendorExtensions = new HashMap<>(); + + vendorExtensions.put("x-type", prop.getDataType()); + + if (prop.getIsString()) { + ArrayList refined = new ArrayList<>(); + + if (prop.getMinLength() != null) { + refined.add("MinSize[" + prop.getMinLength() + "]"); + imports.add("MinSize"); + } + if (prop.getMaxLength() != null) { + refined.add("MaxSize[" + prop.getMaxLength() + "]"); + imports.add("MaxSize"); + } + if (prop.getPattern() != null) { + try { + String fixedPattern = prop.getPattern().substring(1, prop.getPattern().length() - 1); + refined.add("MatchesRegex[\"" + fixedPattern + "\"]"); + imports.add("MatchesRegex"); + } catch (IndexOutOfBoundsException ignored) { + } + } + vendorExtensions.putAll(makeRefiined(imports, prop.getDataType(), refined)); + } + + if ("Int".equals(prop.getDataType()) + || "Long".equals(prop.getDataType()) + || "Float".equals(prop.getDataType()) + || "Double".equals(prop.getDataType()) + || "BigDecimal".equals(prop.getDataType()) + ) { + ArrayList refined = new ArrayList<>(); + + if (prop.getMinimum() != null) { + if (prop.getExclusiveMinimum()) { + refined.add("Greater[" + prop.getMinimum() + "]"); + imports.add("Greater"); + } else { + refined.add("GreaterEqual[" + prop.getMinimum() + "]"); + imports.add("GreaterEqual"); + } + } + if (prop.getMaximum() != null) { + if (prop.getExclusiveMaximum()) { + refined.add("Less[" + prop.getMaximum() + "]"); + imports.add("Less"); + } else { + refined.add("LessEqual[" + prop.getMaximum() + "]"); + imports.add("LessEqual"); + } + } + vendorExtensions.putAll(makeRefiined(imports, prop.getDataType(), refined)); + } + + if (prop.getIsUuid() || "Uuid".equals(prop.getDataType())) { + prop.setDataType("UUID"); + } + + if (prop.getIsArray() && prop.getItems() != null) { + Map subVendorExtensions = refineProp(prop.getItems(), imports); + prop.getItems().getVendorExtensions().putAll(subVendorExtensions); + + ArrayList refined = new ArrayList<>(); + if (prop.getMinItems() != null) { + refined.add("MinSize[" + prop.getMinItems() + "]"); + imports.add("MinSize"); + } + if (prop.getMaxItems() != null) { + refined.add("MaxSize[" + prop.getMaxItems() + "]"); + imports.add("MaxSize"); + } + + vendorExtensions.putAll(makeRefiined(imports, prop.getDataType(), refined)); + } + + return vendorExtensions; + } + + + @Override + public Map postProcessSupportingFileData(Map objs) { + Map bundle = super.postProcessSupportingFileData(objs); + + List models = (List) bundle.get("models"); + TreeSet allImports = new TreeSet<>(); + for (ModelMap mm: models) { + for (String nextImport : mm.getModel().imports) { + String mapping = importMapping().get(nextImport); + if (mapping != null && !defaultIncludes().contains(mapping)) { + allImports.add(mapping); + } + // add instantiation types + mapping = instantiationTypes().get(nextImport); + if (mapping != null && !defaultIncludes().contains(mapping)) { + allImports.add(mapping); + } + } + } + bundle.put("imports", allImports); + bundle.put("packageName", packageName); + + + ApiInfoMap apiInfoMap = (ApiInfoMap) bundle.get("apiInfo"); + Map> authToOperationMap = new TreeMap<>(); + for (OperationsMap op: apiInfoMap.getApis()) { + List> opsByAuth = (List>) op.get("operationsByAuth"); + for (HashMap auth: opsByAuth) { + String autName = (String) auth.get("auth"); + String classname = (String) op.get("classname"); + List classnames = authToOperationMap.computeIfAbsent(autName, k -> new ArrayList<>()); + classnames.add(classname); + } + } + + bundle.put("authToOperationMap", + authToOperationMap.entrySet().stream().map(ent -> { + Map tuple = new HashMap<>(); + String auth = ent.getKey(); + tuple.put("auth", auth); + tuple.put("ops", ent.getValue()); + tuple.put("addMiddleware", !"".equals(auth)); + return tuple; + }).collect(Collectors.toList()) + ); + return bundle; + } + + @Override + public CodegenType getTag() { + return CodegenType.SERVER; + } + + @Override + public String getName() { + return "scala-http4s-server"; + } + + @Override + public String getHelp() { + return "Generates a Scala http4s server bindings."; + } + + @Override + public String escapeReservedWord(String name) { + return "_" + name; + } + + @Override + public String apiFileFolder() { + return outputFolder + File.separator + apiFileFolderRelative() ; + } + + private String apiFileFolderRelative() { + return sourceFolder + File.separator + apiPackage().replace('.', File.separatorChar); + } + + @Override + public String modelFileFolder() { + return outputFolder + File.separator + modelFileFolderRelative(); + } + + public String modelFileFolderRelative() { + return sourceFolder + File.separator + modelPackage().replace('.', File.separatorChar); + } + + public String packageFileFolderRelative() { + return sourceFolder + File.separator + packageName.replace('.', File.separatorChar); + } + + @Override + public OperationsMap postProcessOperationsWithModels(OperationsMap objsI, List allModels) { + OperationsMap objs = super.postProcessOperationsWithModels(objsI, allModels); + OperationMap operations = objs.getOperations(); + + List operationList = operations.getOperation(); + Set allAuth = new HashSet<>(); + Map> opsByAuth = new HashMap<>(); + + for (CodegenOperation op : operationList) { + + // Converts GET /foo/bar => GET / foo / bar => + generateScalaPath(op); + + // :? fooQueryParam + generateQueryParameters(op); + + // decide wat methods do we need in delegate: + if (op.consumes == null || op.consumes.size() == 0) { + op.vendorExtensions.put("x-generic-body", true); + } else { + if (op.consumes.stream().anyMatch(x -> x.containsKey("isJson"))) { + op.vendorExtensions.put("x-json-body", true); + } + if (op.consumes.stream().anyMatch(x -> !x.containsKey("isJson"))) { + op.vendorExtensions.put("x-generic-body", true); + } + } + + // decide wat methods do we need in responses: + for (CodegenResponse resp: op.responses) { + if (resp.code.equals("0")) + resp.code = "200"; // 200 by default + + String responseName; + + responseName = locationStatusToResponse.get(resp.code); + if (responseName != null) { + resp.vendorExtensions.put("x-response-location", true); + } else { + responseName = wwwAuthStatusToResponse.get(resp.code); + if (responseName != null) { + resp.vendorExtensions.put("x-response-www-auth", true); + } else { + responseName = allowStatusToResponse.get(resp.code); + if (responseName != null) { + resp.vendorExtensions.put("x-response-allow", true); + } else { + responseName = proxyAuthStatusToResponse.get(resp.code); + if (responseName != null) { + resp.vendorExtensions.put("x-response-proxy-auth", true); + } else { + responseName = statusToResponse.get(resp.code); + if (responseName != null) { + resp.vendorExtensions.put("x-response-standard", true); + } else { + throw new IllegalArgumentException("unsupported status " + resp.code); + } + } + } + } + } + + resp.vendorExtensions.put("x-response", responseName); + + if (resp.getContent() == null) { + resp.vendorExtensions.put("x-generic-response", true); // non json resp + } else { + if (resp.getContent().containsKey("application/json")) { + resp.vendorExtensions.put("x-json-response", true); // json resp + } else { + resp.vendorExtensions.put("x-generic-response", true); // non json resp + } + if (resp.getContent().size() > 1) { + resp.vendorExtensions.put("x-generic-response", true); // non json resp + } + } + } + + if (op.authMethods != null) { + for (CodegenSecurity cs: op.authMethods) { + allAuth.add(cs.name); + } + List> authDup = new ArrayList<>(); + for (CodegenSecurity authMeth: op.authMethods) { + Map vals = new HashMap<>(); + vals.put("authName", authMeth.name); + vals.put("operation", op); + authDup.add(vals); + + opsByAuth.computeIfAbsent(authMeth.name, k -> new ArrayList<>()).add(op.operationId); + } + op.vendorExtensions.put("x-authed", authDup); + } else { + opsByAuth.computeIfAbsent("", k -> new ArrayList<>()).add(op.operationId); + } + } + + TreeSet allImports = new TreeSet<>(); + List currentImports = objs.getImports().stream().flatMap(m -> m.values().stream()).collect(Collectors.toList()); + for (CodegenOperation op : operationList) { + for (String nextImport : op.imports) { + String mapping = importMapping().get(nextImport); + if (mapping != null && !defaultIncludes().contains(mapping)) { + if (!currentImports.contains(mapping)) { + allImports.add(mapping); + } + } + // add instantiation types + mapping = instantiationTypes().get(nextImport); + if (mapping != null && !currentImports.contains(mapping)) { + if (!currentImports.contains(mapping)) { + allImports.add(mapping); + } + } + } + } + + + objs.put("operationsByAuth", opsByAuth.entrySet().stream().map(ent -> { + HashMap tuple = new HashMap<>(); + tuple.put("auth", ent.getKey()); + tuple.put("ops", ent.getValue()); + return tuple; + } + ).collect(Collectors.toList())); + objs.put("extraImports", allImports); + objs.put("allAuth", allAuth); + + return objs; + } + + + @SuppressWarnings("Duplicates") + @Override + public String getTypeDeclaration(Schema p) { + if (ModelUtils.isArraySchema(p)) { + ArraySchema ap = (ArraySchema) p; + Schema inner = ap.getItems(); + return getSchemaType(p) + "[" + getTypeDeclaration(inner) + "]"; + } else if (ModelUtils.isMapSchema(p)) { + Schema inner = ModelUtils.getAdditionalProperties(p); + + return getSchemaType(p) + "[String, " + getTypeDeclaration(inner) + "]"; + } + return super.getTypeDeclaration(p); + } + + @Override + public String getSchemaType(Schema p) { + String schemaType = super.getSchemaType(p); + String type; + if (typeMapping.containsKey(schemaType)) { + type = typeMapping.get(schemaType); + if (languageSpecificPrimitives.contains(type)) { + return toModelName(type); + } + } else { + type = schemaType; + } + return toModelName(type); + } + + @Override + public String escapeQuotationMark(String input) { + // remove " to avoid code injection + return input.replace("\"", ""); + } + + @Override + public String escapeUnsafeCharacters(String input) { + return input.replace("*/", "*_/").replace("/*", "/_*"); + } + + private void generateScalaPath(CodegenOperation op) { + Set imports = new HashSet<>(); + + String path = op.path; + + // remove first / + if (path.startsWith("/")) { + path = path.substring(1); + } + + // remove last / + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + + String[] items = path.split("/", -1); + String scalaPath = ""; + int pathParamIndex = 0; + + for (String item : items) { + + if (item.matches("^\\{(.*)}$")) { // wrap in {} + // find the datatype of the parameter + final CodegenParameter cp = op.pathParams.get(pathParamIndex); + + // TODO: Handle non-primitives… + scalaPath = scalaPath + " / " + cpToPathParameter(cp, imports, cp.vendorExtensions); + + pathParamIndex++; + } else { + scalaPath = scalaPath + " / " + "\"" + item + "\""; + } + } + + op.vendorExtensions.put("x-codegen-path", scalaPath); + op.imports.addAll(imports); + } + + private String cpToPathParameter(CodegenParameter cp, Set imports, Map vendorExtensions) { + // don't support containers and arrays yet, reset to string + if (cp.isContainer || cp.isArray) { + cp.setDataType("String"); + cp.setIsArray(false); + cp.setIsString(true); + cp.isContainer = false; + } + + Map _vendorExtensions = refineProp(cp, imports); + vendorExtensions.putAll(_vendorExtensions); + + if (_vendorExtensions.size() == 1) { // only `x-type` + if ("String".equals(cp.getDataType())) { + return cp.baseName; + } else { + return cp.dataType + "Varr(" + cp.baseName + ")"; + } + } else { + return cp.baseName + "Varr(" + cp.baseName + ")"; + } + } + + private void generateQueryParameters(CodegenOperation op) { + Set imports = new HashSet<>(); + String queryString = ""; + + for (CodegenParameter cp : op.queryParams) { + if (queryString.isEmpty()) { + queryString = queryString + " :? "; + } else { + queryString = queryString + " +& "; + } + + queryString = queryString + cpToQueryParameter(cp, imports, cp.vendorExtensions); + } + + op.vendorExtensions.put("x-codegen-query", queryString); + op.imports.addAll(imports); + } + + private String cpToQueryParameter(CodegenParameter cp, Set imports, Map vendorExtensions) { + // don't support containers and arrays yet, reset to string + if (cp.isContainer && !cp.isArray) { + cp.setDataType("String"); + cp.setIsArray(false); + cp.setIsString(true); + cp.isContainer = false; + } + + vendorExtensions.putAll(refineProp(cp, imports)); + return cp.baseName + "QueryParam(" + cp.baseName + ")"; + } + + @Override + public void postProcess() { + System.out.println("################################################################################"); + System.out.println("# Thanks for using OpenAPI Generator. #"); + System.out.println("# Please consider donation to help us maintain this project \uD83D\uDE4F #"); + System.out.println("# https://opencollective.com/openapi_generator/donate #"); + System.out.println("# #"); + System.out.println("# This generator's contributed by Jim Schubert (https://github.com/jimschubert)#"); + System.out.println("# Please support his work directly via https://patreon.com/jimschubert \uD83D\uDE4F #"); + System.out.println("################################################################################"); + } + + @Override + public GeneratorLanguage generatorLanguage() { return GeneratorLanguage.SCALA; } +} 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 e605d387806..16298388143 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 @@ -118,6 +118,7 @@ org.openapitools.codegen.languages.ScalaPekkoClientCodegen org.openapitools.codegen.languages.ScalaAkkaHttpServerCodegen org.openapitools.codegen.languages.ScalaFinchServerCodegen org.openapitools.codegen.languages.ScalaGatlingCodegen +org.openapitools.codegen.languages.ScalaHttp4sServerCodegen org.openapitools.codegen.languages.ScalaLagomServerCodegen org.openapitools.codegen.languages.ScalaPlayFrameworkServerCodegen org.openapitools.codegen.languages.ScalaSttpClientCodegen diff --git a/modules/openapi-generator/src/main/resources/scala-http4s-server/api.mustache b/modules/openapi-generator/src/main/resources/scala-http4s-server/api.mustache new file mode 100644 index 00000000000..2cb353b03c6 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-http4s-server/api.mustache @@ -0,0 +1,255 @@ +package {{apiPackage}} + +import {{apiPackage}}.path._ +import {{apiPackage}}.query._ + +{{#imports}}import {{import}} +{{/imports}} + +{{#extraImports}}import {{.}} +{{/extraImports}} + +import cats.Monad +import cats.syntax.all._ + +import org.http4s._ +import org.http4s.circe._ +import org.http4s.server._ +import org.http4s.headers._ +import org.http4s.dsl.Http4sDsl +import org.http4s.circe.CirceEntityEncoder._ + +final case class {{classname}}Routes[ + F[_]: JsonDecoder: Monad{{#allAuth}}, {{.}}{{/allAuth}} +](delegate: {{classname}}Delegate[F{{#allAuth}}, {{.}}{{/allAuth}}]) extends Http4sDsl[F] { +{{#operations}} +{{#operation}} + object {{operationId}} { + import {{classname}}Delegate.{{operationId}}Responses + + {{#pathParams}} + {{#vendorExtensions.x-refined}} + object {{baseName}}Varr extends RefinedVarr[{{vendorExtensions.x-refined-lft}}, {{{vendorExtensions.x-refined-rgt}}}] + {{/vendorExtensions.x-refined}} + {{/pathParams}} + {{#queryParams}} + {{#isArray}} + {{#required}} + object {{baseName}}QueryParam extends QuerySeqParamDecoderMatcher[{{{items.vendorExtensions.x-type}}}]("{{baseName}}") + {{/required}} + {{^required}} + object {{baseName}}QueryParam extends OptionalQuerySeqParamDecoderMatcher[{{{items.vendorExtensions.x-type}}}]("{{baseName}}") + {{/required}} + {{/isArray}} + {{^isArray}} + {{#required}} + object {{baseName}}QueryParam extends QueryParamDecoderMatcher[{{{vendorExtensions.x-type}}}]("{{baseName}}") + {{/required}} + {{^required}} + object {{baseName}}QueryParam extends OptionalQueryParamDecoderMatcher[{{{vendorExtensions.x-type}}}]("{{baseName}}") + {{/required}} + {{/isArray}} + {{/queryParams}} + +{{^vendorExtensions.x-authed}} + val route = HttpRoutes.of[F] { + case req @ {{{httpMethod}}} -> Root{{{vendorExtensions.x-codegen-path}}}{{{vendorExtensions.x-codegen-query}}} => + {{#vendorExtensions.x-json-body}} + {{#vendorExtensions.x-generic-body}} + req.contentType match { + case Some(`Content-Type`(MediaType.application.json, _)) => + {{>delegateCallJson}} + case _ => + {{>delegateCallGeneric}} + } + {{/vendorExtensions.x-generic-body}} + {{^vendorExtensions.x-generic-body}} + {{>delegateCallJson}} + {{/vendorExtensions.x-generic-body}} + {{/vendorExtensions.x-json-body}} + {{^vendorExtensions.x-json-body}} + {{>delegateCallGeneric}} + {{/vendorExtensions.x-json-body}} + } + +{{/vendorExtensions.x-authed}} +{{#vendorExtensions.x-authed}} + val route{{authName}} = AuthedRoutes.of[{{authName}}, F] { + case (req @ {{{httpMethod}}} -> Root{{{vendorExtensions.x-codegen-path}}}{{{vendorExtensions.x-codegen-query}}}) as auth => + {{#vendorExtensions.x-json-body}} + {{#vendorExtensions.x-generic-body}} + req.contentType match { + case Some(`Content-Type`(MediaType.application.json, _)) => + {{>delegateCallJson}} + case _ => + {{>delegateCallGeneric}} + } + {{/vendorExtensions.x-generic-body}} + {{^vendorExtensions.x-generic-body}} + {{>delegateCallJson}} + {{/vendorExtensions.x-generic-body}} + {{/vendorExtensions.x-json-body}} + {{^vendorExtensions.x-json-body}} + {{>delegateCallGeneric}} + {{/vendorExtensions.x-json-body}} + } +{{/vendorExtensions.x-authed}} + + val responses: {{operationId}}Responses[F] = new {{operationId}}Responses[F] { + {{#responses}} + {{#vendorExtensions.x-response-location}} + {{#vendorExtensions.x-json-response}} + def resp{{code}}(location: Location, value: {{{dataType}}}): F[Response[F]] = {{vendorExtensions.x-response}}(location, value) + {{/vendorExtensions.x-json-response}} + {{#vendorExtensions.x-generic-response}} + def resp{{code}}(location: Location): F[Response[F]] = {{vendorExtensions.x-response}}(location) + {{/vendorExtensions.x-generic-response}} + {{/vendorExtensions.x-response-location}} + {{#vendorExtensions.x-response-www-auth}} + {{#vendorExtensions.x-json-response}} + def resp{{code}}(authenticate: `WWW-Authenticate`, value: {{{dataType}}}): F[Response[F]] = {{vendorExtensions.x-response}}(authenticate, value) + {{/vendorExtensions.x-json-response}} + {{#vendorExtensions.x-generic-response}} + def resp{{code}}(authenticate: `WWW-Authenticate`): F[Response[F]] = {{vendorExtensions.x-response}}(authenticate) + {{/vendorExtensions.x-generic-response}} + {{/vendorExtensions.x-response-www-auth}} + {{#vendorExtensions.x-response-allow}} + {{#vendorExtensions.x-json-response}} + def resp{{code}}(allow: Allow, value: {{{dataType}}}): F[Response[F]] = {{vendorExtensions.x-response}}(allow, value) + {{/vendorExtensions.x-json-response}} + {{#vendorExtensions.x-generic-response}} + def resp{{code}}(allow: Allow): F[Response[F]] = {{vendorExtensions.x-response}}(allow) + {{/vendorExtensions.x-generic-response}} + {{/vendorExtensions.x-response-allow}} + {{#vendorExtensions.x-response-proxy-auth}} + {{#vendorExtensions.x-json-response}} + def resp{{code}}(authenticate: `Proxy-Authenticate`, value: {{{dataType}}}): F[Response[F]] = {{vendorExtensions.x-response}}(value, authenticate) + {{/vendorExtensions.x-json-response}} + {{#vendorExtensions.x-generic-response}} + def resp{{code}}(authenticate: `Proxy-Authenticate`): F[Response[F]] = {{vendorExtensions.x-response}}(authenticate) + {{/vendorExtensions.x-generic-response}} + {{/vendorExtensions.x-response-proxy-auth}} + {{#vendorExtensions.x-response-standard}} + {{#vendorExtensions.x-json-response}} + def resp{{code}}(value: {{{dataType}}}): F[Response[F]] = {{vendorExtensions.x-response}}(value) + {{/vendorExtensions.x-json-response}} + {{#vendorExtensions.x-generic-response}} + def resp{{code}}(): F[Response[F]] = {{vendorExtensions.x-response}}() + {{/vendorExtensions.x-generic-response}} + {{/vendorExtensions.x-response-standard}} + {{/responses}} + } + } +{{/operation}} +{{/operations}} + +{{#operationsByAuth}} + val routes{{auth}} = + {{#ops}} + {{.}}.route{{auth}}{{^-last}} <+>{{/-last}} + {{/ops}} +{{/operationsByAuth}} +} + +object {{classname}}Delegate { +{{#operations}} +{{#operation}} + trait {{operationId}}Responses[F[_]] { + {{#responses}} + {{#vendorExtensions.x-response-location}} + {{#vendorExtensions.x-json-response}} + def resp{{code}}(location: Location, value: {{{dataType}}}): F[Response[F]] + {{/vendorExtensions.x-json-response}} + {{#vendorExtensions.x-generic-response}} + def resp{{code}}(location: Location): F[Response[F]] + {{/vendorExtensions.x-generic-response}} + {{/vendorExtensions.x-response-location}} + {{#vendorExtensions.x-response-www-auth}} + {{#vendorExtensions.x-json-response}} + def resp{{code}}(authenticate: `WWW-Authenticate`, value: {{{dataType}}}): F[Response[F]] + {{/vendorExtensions.x-json-response}} + {{#vendorExtensions.x-generic-response}} + def resp{{code}}(authenticate: `WWW-Authenticate`): F[Response[F]] + {{/vendorExtensions.x-generic-response}} + {{/vendorExtensions.x-response-www-auth}} + {{#vendorExtensions.x-response-allow}} + {{#vendorExtensions.x-json-response}} + def resp{{code}}(allow: Allow, value: {{{dataType}}}): F[Response[F]] + {{/vendorExtensions.x-json-response}} + {{#vendorExtensions.x-generic-response}} + def resp{{code}}(allow: Allow): F[Response[F]] + {{/vendorExtensions.x-generic-response}} + {{/vendorExtensions.x-response-allow}} + {{#vendorExtensions.x-response-proxy-auth}} + {{#vendorExtensions.x-json-response}} + def resp{{code}}(authenticate: `Proxy-Authenticate`, value: {{{dataType}}}): F[Response[F]] + {{/vendorExtensions.x-json-response}} + {{#vendorExtensions.x-generic-response}} + def resp{{code}}(authenticate: `Proxy-Authenticate`): F[Response[F]] + {{/vendorExtensions.x-generic-response}} + {{/vendorExtensions.x-response-proxy-auth}} + {{#vendorExtensions.x-response-standard}} + {{#vendorExtensions.x-json-response}} + def resp{{code}}(value: {{{dataType}}}): F[Response[F]] + {{/vendorExtensions.x-json-response}} + {{#vendorExtensions.x-generic-response}} + def resp{{code}}(): F[Response[F]] + {{/vendorExtensions.x-generic-response}} + {{/vendorExtensions.x-response-standard}} + {{/responses}} + } + +{{/operation}} +{{/operations}} +} + +trait {{classname}}Delegate[F[_]{{#allAuth}}, {{.}}{{/allAuth}}] { +{{#operations}} +{{#operation}} + + trait {{operationId}} { + import {{classname}}Delegate.{{operationId}}Responses + {{#vendorExtensions.x-json-body}} + +{{^vendorExtensions.x-authed}} + def handle( + req: Request[F], + {{operationId}}: F[{{{bodyParam.dataType}}}], +{{> delegateArgs}} responses: {{operationId}}Responses[F] + ): F[Response[F]] +{{/vendorExtensions.x-authed}} + +{{#vendorExtensions.x-authed}} + def handle_{{authName}}( + auth: {{authName}}, + req: Request[F], + {{operationId}}: F[{{{bodyParam.dataType}}}], +{{> delegateArgs}} responses: {{operationId}}Responses[F] + ): F[Response[F]] + +{{/vendorExtensions.x-authed}} + {{/vendorExtensions.x-json-body}} + + {{#vendorExtensions.x-generic-body}} +{{^vendorExtensions.x-authed}} + def handle( + req: Request[F], +{{> delegateArgs}} responses: {{operationId}}Responses[F] + ): F[Response[F]] +{{/vendorExtensions.x-authed}} + +{{#vendorExtensions.x-authed}} + def handle_{{authName}}( + auth: {{authName}}, + req: Request[F], +{{> delegateArgs}} responses: {{operationId}}Responses[F] + ): F[Response[F]] + +{{/vendorExtensions.x-authed}} + {{/vendorExtensions.x-generic-body}} + } + def {{operationId}}: {{operationId}} + +{{/operation}} +{{/operations}} +} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/scala-http4s-server/apis.mustache b/modules/openapi-generator/src/main/resources/scala-http4s-server/apis.mustache new file mode 100644 index 00000000000..98737814e14 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-http4s-server/apis.mustache @@ -0,0 +1,53 @@ +package {{packageName}} + +import org.http4s.circe._ +import cats.Monad +import cats.syntax.all._ +import cats.data.OptionT +import cats.data.Kleisli +import org.http4s._ +import org.http4s.server._ + +import {{apiPackage}}._ + +final case class API [ + F[_]: JsonDecoder: Monad{{#authMethods}}, {{name}}{{/authMethods}} +]( +{{#authMethods}} + {{#lambda.camelcase}}{{name}}{{/lambda.camelcase}}: Kleisli[OptionT[F, *], Request[F], {{name}}], +{{/authMethods}} +)( +{{#apiInfo}} +{{#apis}} +{{#operations}} + delegate{{classname}}: {{classname}}Delegate[F{{#allAuth}}, {{.}}{{/allAuth}}], +{{/operations}} +{{/apis}} +{{/apiInfo}} +){ +{{#authToOperationMap}} +{{#addMiddleware}} + val {{#lambda.camelcase}}{{auth}}{{/lambda.camelcase}}Middleware = AuthMiddleware{{^-last}}.withFallThrough{{/-last}}({{#lambda.camelcase}}{{auth}}{{/lambda.camelcase}}) +{{/addMiddleware}} +{{/authToOperationMap}} + +{{#apiInfo}} +{{#apis}} +{{#operations}} + val {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}Routes = new {{classname}}Routes(delegate{{classname}}) +{{/operations}} +{{/apis}} +{{/apiInfo}} + +{{#authToOperationMap}} + val routes{{auth}} = {{#addMiddleware}}{{#lambda.camelcase}}{{auth}}{{/lambda.camelcase}}Middleware({{/addMiddleware}} + {{#ops}} + {{#lambda.camelcase}}{{.}}{{/lambda.camelcase}}Routes.routes{{auth}}{{^-last}} <+>{{/-last}} + {{/ops}}{{#addMiddleware}}){{/addMiddleware}} +{{/authToOperationMap}} + + val routesAll = +{{#authToOperationMap}} + routes{{auth}}{{^-last}} <+>{{/-last}} +{{/authToOperationMap}} +} diff --git a/modules/openapi-generator/src/main/resources/scala-http4s-server/build.properties b/modules/openapi-generator/src/main/resources/scala-http4s-server/build.properties new file mode 100644 index 00000000000..875272df2d3 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-http4s-server/build.properties @@ -0,0 +1 @@ +sbt.version=1.8.2 \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/scala-http4s-server/build.sbt b/modules/openapi-generator/src/main/resources/scala-http4s-server/build.sbt new file mode 100644 index 00000000000..70c61651640 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-http4s-server/build.sbt @@ -0,0 +1,27 @@ +scalaVersion := "2.13.11" +scalacOptions += "-Ymacro-annotations" + +val circeVersion = "0.14.5" +def circe(artifact: String): ModuleID = "io.circe" %% s"circe-$artifact" % circeVersion + +val http4sVersion = "0.23.23" +def http4s(artifact: String): ModuleID = "org.http4s" %% s"http4s-$artifact" % http4sVersion + +val refinedVersion = "0.9.29" +val refined = Seq( + "eu.timepit" %% "refined" % refinedVersion, + "eu.timepit" %% "refined-cats" % refinedVersion +) + +val catsVersion = "2.10.0" +val cats = Seq("org.typelevel" %% "cats-core" % catsVersion) + +lazy val compilerPlugins = Seq( + compilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1"), + compilerPlugin("org.typelevel" %% "kind-projector" % "0.13.2" cross CrossVersion.full) +) + +libraryDependencies ++= (Seq( + http4s("core"), http4s("ember-server"), http4s("circe"), http4s("dsl"), + circe("core"), circe("generic"), circe("parser"), circe("refined") +) ++ refined ++ cats ++ compilerPlugins) diff --git a/modules/openapi-generator/src/main/resources/scala-http4s-server/delegateArgs.mustache b/modules/openapi-generator/src/main/resources/scala-http4s-server/delegateArgs.mustache new file mode 100644 index 00000000000..327a6a96545 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-http4s-server/delegateArgs.mustache @@ -0,0 +1,21 @@ +{{#pathParams}} + {{baseName}}: {{{vendorExtensions.x-type}}}, +{{/pathParams}} +{{#queryParams}} + {{#isArray}} + {{#required}} + {{baseName}}: List[{{{items.vendorExtensions.x-type}}}], + {{/required}} + {{^required}} + {{baseName}}: Option[List[{{{items.vendorExtensions.x-type}}}]], + {{/required}} + {{/isArray}} + {{^isArray}} + {{#required}} + {{baseName}}: {{{vendorExtensions.x-type}}}, + {{/required}} + {{^required}} + {{baseName}}: Option[{{{vendorExtensions.x-type}}}], + {{/required}} + {{/isArray}} +{{/queryParams}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/scala-http4s-server/delegateCallGeneric.mustache b/modules/openapi-generator/src/main/resources/scala-http4s-server/delegateCallGeneric.mustache new file mode 100644 index 00000000000..d5b1ba61c57 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-http4s-server/delegateCallGeneric.mustache @@ -0,0 +1,6 @@ +{{^authName}} +delegate.{{operationId}}.handle(req, {{#pathParams}}{{baseName}}, {{/pathParams}}{{#queryParams}}{{baseName}}, {{/queryParams}}responses) +{{/authName}} +{{#authName}} +delegate.{{operationId}}.handle_{{authName}}(auth, req, {{#pathParams}}{{baseName}}, {{/pathParams}}{{#queryParams}}{{baseName}}, {{/queryParams}}responses) +{{/authName}} diff --git a/modules/openapi-generator/src/main/resources/scala-http4s-server/delegateCallJson.mustache b/modules/openapi-generator/src/main/resources/scala-http4s-server/delegateCallJson.mustache new file mode 100644 index 00000000000..e023b906240 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-http4s-server/delegateCallJson.mustache @@ -0,0 +1,6 @@ +{{^authName}} + delegate.{{operationId}}.handle(req, req.asJsonDecode[{{{bodyParam.dataType}}}] , {{#pathParams}}{{baseName}}, {{/pathParams}}{{#queryParams}}{{baseName}}, {{/queryParams}}responses) +{{/authName}} +{{#authName}} + delegate.{{operationId}}.handle_{{authName}}(auth, req, req.asJsonDecode[{{{bodyParam.dataType}}}] , {{#pathParams}}{{baseName}}, {{/pathParams}}{{#queryParams}}{{baseName}}, {{/queryParams}}responses) +{{/authName}} diff --git a/modules/openapi-generator/src/main/resources/scala-http4s-server/path.mustache b/modules/openapi-generator/src/main/resources/scala-http4s-server/path.mustache new file mode 100644 index 00000000000..c35abc8d48c --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-http4s-server/path.mustache @@ -0,0 +1,62 @@ +package {{apiPackage}} + +import cats.syntax.all._ +import cats.data.ValidatedNel + +import eu.timepit.refined._ +import eu.timepit.refined.api.Refined +import eu.timepit.refined.api.Validate + +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.ZonedDateTime +import java.util.UUID + +import scala.util.Try + +object path { + trait Varrr[T] { + def unapply(str: String): Option[T] + } + + implicit val LocalDateVarr: Varrr[LocalDate] = new Varrr[LocalDate] { + def unapply(str: String): Option[LocalDate] = Try(LocalDate.parse(str)).toOption + } + + implicit val LocalDateTimeVarr: Varrr[LocalDateTime] = new Varrr[LocalDateTime] { + def unapply(str: String): Option[LocalDateTime] = Try(LocalDateTime.parse(str)).toOption + } + + implicit val ZonedDateTimeVarr: Varrr[ZonedDateTime] = new Varrr[ZonedDateTime] { + def unapply(str: String): Option[ZonedDateTime] = Try(ZonedDateTime.parse(str)).toOption + } + + implicit val UUIDVarr: Varrr[UUID] = new Varrr[UUID] { + def unapply(str: String): Option[UUID] = Try(java.util.UUID.fromString(str)).toOption + } + + implicit val IntVarr: Varrr[Int] = new Varrr[Int] { + def unapply(str: String): Option[Int] = Try(str.toInt).toOption + } + + implicit val LongVarr: Varrr[Long] = new Varrr[Long] { + def unapply(str: String): Option[Long] = Try(str.toLong).toOption + } + + implicit val DoubleVarr: Varrr[Double] = new Varrr[Double] { + def unapply(str: String): Option[Double] = Try(str.toDouble).toOption + } + + implicit val BigDecimalVarr: Varrr[BigDecimal] = new Varrr[BigDecimal] { + def unapply(str: String): Option[BigDecimal] = Try(BigDecimal(str)).toOption + } + + implicit val StringVarr: Varrr[String] = new Varrr[String] { + def unapply(str: String): Option[String] = str.some + } + + abstract class RefinedVarr[T, P](implicit varrr: Varrr[T], validate: Validate[T, P]) extends Varrr[Refined[T, P]] { + def unapply(str: String): Option[Refined[T, P]] = + varrr.unapply(str).flatMap(x => refineV(x).toOption) + } +} diff --git a/modules/openapi-generator/src/main/resources/scala-http4s-server/query.mustache b/modules/openapi-generator/src/main/resources/scala-http4s-server/query.mustache new file mode 100644 index 00000000000..52f5fa9ce97 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-http4s-server/query.mustache @@ -0,0 +1,59 @@ +package {{apiPackage}} + +import cats.data.NonEmptyList +import cats.data.ValidatedNel +import cats.syntax.all._ + +import eu.timepit.refined._ +import eu.timepit.refined.api.Refined +import eu.timepit.refined.api.Validate + +import org.http4s.ParseFailure +import org.http4s.QueryParamDecoder +import org.http4s.QueryParameterValue + +import java.time._ +import java.util.UUID + +object query { + implicit def rrrefinedQueryParamDecoder[T, P]( + implicit tDecoder: QueryParamDecoder[T], validate: Validate[T, P] + ): QueryParamDecoder[Refined[T, P]] = new QueryParamDecoder[Refined[T, P]] { + def decode(value: QueryParameterValue): ValidatedNel[ParseFailure,Refined[T, P]] = + tDecoder.decode(value).withEither(t => t.flatMap(x => + refineV(x).leftMap(err => NonEmptyList.one(ParseFailure(err, err))) + )) + } + + abstract class QuerySeqParamDecoderMatcher[T: QueryParamDecoder](name: String) { + def unapply(params: Map[String, Seq[String]]): Option[List[T]] = + params + .get(name) + .flatMap(values => + values.toList.traverse(s => QueryParamDecoder[T].decode(QueryParameterValue(s)).toOption)) + } + + abstract class OptionalQuerySeqParamDecoderMatcher[T: QueryParamDecoder](name: String) { + def unapply(params: Map[String, List[String]]): Option[Option[List[T]]] = + params + .get(name) + .flatMap(values => + values.toList.traverse(s => QueryParamDecoder[T].decode(QueryParameterValue(s)).toOption)) + .fold(List.empty[T].some.some)(_.some.some) + } + + implicit lazy val BigDecimalQueryParamDecoder: QueryParamDecoder[BigDecimal] = + QueryParamDecoder.fromUnsafeCast[BigDecimal](x => BigDecimal(x.value))("BigDecimal") + + implicit lazy val LocalDateTimeQueryParamDecoder: QueryParamDecoder[LocalDateTime] = + QueryParamDecoder.fromUnsafeCast[LocalDateTime](x => LocalDateTime.parse(x.value))("LocalDateTime") + + implicit lazy val LocalDateQueryParamDecoder: QueryParamDecoder[LocalDate] = + QueryParamDecoder.fromUnsafeCast[LocalDate](x => LocalDate.parse(x.value))("LocalDateTime") + + implicit lazy val ZonedDateTimeQueryParamDecoder: QueryParamDecoder[ZonedDateTime] = + QueryParamDecoder.fromUnsafeCast[ZonedDateTime](x => ZonedDateTime.parse(x.value))("ZonedDateTime") + + implicit lazy val UUIDQueryParamDecoder: QueryParamDecoder[UUID] = + QueryParamDecoder.fromUnsafeCast[UUID](x => UUID.fromString(x.value))("UUID") +} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/scala-http4s-server/types.mustache b/modules/openapi-generator/src/main/resources/scala-http4s-server/types.mustache new file mode 100644 index 00000000000..53b5851b99e --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-http4s-server/types.mustache @@ -0,0 +1,98 @@ +package {{modelPackage}} + +import java.time._ + +import io.circe.refined._ +import io.circe.syntax._ +import io.circe.{ Decoder, Encoder } +import io.circe.generic.semiauto.{ deriveDecoder, deriveEncoder } + +{{#imports}} +import {{.}} +{{/imports}} + +{{#models}} +{{#model}} +/** +* {{{description}}} +{{#vars}} +* @param {{name}} {{{description}}} +{{/vars}} +*/ +{{#vendorExtensions.x-isSealedTrait}} +sealed trait {{classname}} +object {{classname}} { + import io.circe.{ Decoder, Encoder } + import io.circe.syntax._ + import cats.syntax.functor._ + +{{^vendorExtensions.x-use-discr}} +// no discriminator + implicit val {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}Encoder: Encoder[{{classname}}] = Encoder.instance { +{{#oneOf}} + case {{#lambda.camelcase}}{{.}}{{/lambda.camelcase}}: {{.}} => {{#lambda.camelcase}}{{.}}{{/lambda.camelcase}}.asJson +{{/oneOf}} + } + + implicit val {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}Decoder: Decoder[{{classname}}] = + List[Decoder[{{classname}}]]( +{{#oneOf}} + Decoder[{{.}}].widen, +{{/oneOf}} + ).reduceLeft(_ or _) +{{/vendorExtensions.x-use-discr}} +{{#vendorExtensions.x-use-discr}} +{{^vendorExtensions.x-use-discr-mapping}} +// no discriminator mapping + implicit val {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}Encoder: Encoder[{{classname}}] = Encoder.instance { +{{#oneOf}} + case {{#lambda.camelcase}}{{.}}{{/lambda.camelcase}}: {{.}} => {{#lambda.camelcase}}{{.}}{{/lambda.camelcase}}.asJson.mapObject(("{{discriminator.propertyName}}" -> "{{.}}".asJson) +: _) +{{/oneOf}} + } + + implicit val {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}Decoder: Decoder[{{classname}}] = Decoder.instance { cursor => + cursor.downField("{{discriminator.propertyName}}").as[String].flatMap { + {{#oneOf}} + case "{{.}}" => + cursor.as[{{.}}] + {{/oneOf}} + } + } +{{/vendorExtensions.x-use-discr-mapping}} +{{#vendorExtensions.x-use-discr-mapping}} +// use discriminator mapping +{{#discriminator}} + implicit val {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}Encoder: Encoder[{{classname}}] = Encoder.instance { +{{#mappedModels}} + case {{#lambda.camelcase}}{{model.classname}}{{/lambda.camelcase}}: {{model.classname}} => {{#lambda.camelcase}}{{model.classname}}{{/lambda.camelcase}}.asJson.mapObject(("{{propertyName}}" -> "{{mappingName}}".asJson) +: _) +{{/mappedModels}} + } + + implicit val {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}Decoder: Decoder[{{classname}}] = Decoder.instance { cursor => + cursor.downField("{{propertyName}}").as[String].flatMap { + {{#mappedModels}} + case "{{mappingName}}" => + cursor.as[{{model.classname}}] + {{/mappedModels}} + } + } +{{/discriminator}} +{{/vendorExtensions.x-use-discr-mapping}} +{{/vendorExtensions.x-use-discr}} +} +{{/vendorExtensions.x-isSealedTrait}} + +{{^vendorExtensions.x-isSealedTrait}} +case class {{classname}}( +{{#vars}} + {{name}}: {{^required}}Option[{{{vendorExtensions.x-type}}}]{{/required}}{{#required}}{{{vendorExtensions.x-type}}}{{/required}}{{^-last}},{{/-last}} +{{/vars}} +){{#vendorExtensions.x-extends}} extends {{.}}{{/vendorExtensions.x-extends}}{{#vendorExtensions.x-extendsWith}} with {{.}}{{/vendorExtensions.x-extendsWith}} +object {{classname}} { + implicit val encoder{{classname}}: Encoder[{{classname}}] = deriveEncoder[{{classname}}].mapJson(_.dropNullValues) + implicit val decoder{{classname}}: Decoder[{{classname}}] = deriveDecoder[{{classname}}] +} +{{/vendorExtensions.x-isSealedTrait}} + +{{/model}} +{{/models}} diff --git a/samples/server/petstore/scala-http4s-server/.openapi-generator-ignore b/samples/server/petstore/scala-http4s-server/.openapi-generator-ignore new file mode 100644 index 00000000000..7484ee590a3 --- /dev/null +++ b/samples/server/petstore/scala-http4s-server/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/samples/server/petstore/scala-http4s-server/.openapi-generator/FILES b/samples/server/petstore/scala-http4s-server/.openapi-generator/FILES new file mode 100644 index 00000000000..db5888af483 --- /dev/null +++ b/samples/server/petstore/scala-http4s-server/.openapi-generator/FILES @@ -0,0 +1,9 @@ +build.sbt +project/build.properties +src/main/scala/org/openapitools/api.scala +src/main/scala/org/openapitools/apis/PetApi.scala +src/main/scala/org/openapitools/apis/StoreApi.scala +src/main/scala/org/openapitools/apis/UserApi.scala +src/main/scala/org/openapitools/apis/path.scala +src/main/scala/org/openapitools/apis/query.scala +src/main/scala/org/openapitools/models/types.scala diff --git a/samples/server/petstore/scala-http4s-server/.openapi-generator/VERSION b/samples/server/petstore/scala-http4s-server/.openapi-generator/VERSION new file mode 100644 index 00000000000..717311e32e3 --- /dev/null +++ b/samples/server/petstore/scala-http4s-server/.openapi-generator/VERSION @@ -0,0 +1 @@ +unset \ No newline at end of file diff --git a/samples/server/petstore/scala-http4s-server/build.sbt b/samples/server/petstore/scala-http4s-server/build.sbt new file mode 100644 index 00000000000..70c61651640 --- /dev/null +++ b/samples/server/petstore/scala-http4s-server/build.sbt @@ -0,0 +1,27 @@ +scalaVersion := "2.13.11" +scalacOptions += "-Ymacro-annotations" + +val circeVersion = "0.14.5" +def circe(artifact: String): ModuleID = "io.circe" %% s"circe-$artifact" % circeVersion + +val http4sVersion = "0.23.23" +def http4s(artifact: String): ModuleID = "org.http4s" %% s"http4s-$artifact" % http4sVersion + +val refinedVersion = "0.9.29" +val refined = Seq( + "eu.timepit" %% "refined" % refinedVersion, + "eu.timepit" %% "refined-cats" % refinedVersion +) + +val catsVersion = "2.10.0" +val cats = Seq("org.typelevel" %% "cats-core" % catsVersion) + +lazy val compilerPlugins = Seq( + compilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1"), + compilerPlugin("org.typelevel" %% "kind-projector" % "0.13.2" cross CrossVersion.full) +) + +libraryDependencies ++= (Seq( + http4s("core"), http4s("ember-server"), http4s("circe"), http4s("dsl"), + circe("core"), circe("generic"), circe("parser"), circe("refined") +) ++ refined ++ cats ++ compilerPlugins) diff --git a/samples/server/petstore/scala-http4s-server/pom.xml b/samples/server/petstore/scala-http4s-server/pom.xml new file mode 100644 index 00000000000..2bcbf0232eb --- /dev/null +++ b/samples/server/petstore/scala-http4s-server/pom.xml @@ -0,0 +1,34 @@ + + 4.0.0 + org.openapitools + scala-http4s-server + pom + 1.0-SNAPSHOT + Scala http4s server + + + + org.codehaus.mojo + exec-maven-plugin + 1.5.0 + + + sbt-test + integration-test + + exec + + + sbt + + -ivy + ${user.home}/.ivy2 + test + + + + + + + + diff --git a/samples/server/petstore/scala-http4s-server/project/build.properties b/samples/server/petstore/scala-http4s-server/project/build.properties new file mode 100644 index 00000000000..875272df2d3 --- /dev/null +++ b/samples/server/petstore/scala-http4s-server/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.8.2 \ No newline at end of file diff --git a/samples/server/petstore/scala-http4s-server/src/main/scala/org/openapitools/api.scala b/samples/server/petstore/scala-http4s-server/src/main/scala/org/openapitools/api.scala new file mode 100644 index 00000000000..70d45a221b4 --- /dev/null +++ b/samples/server/petstore/scala-http4s-server/src/main/scala/org/openapitools/api.scala @@ -0,0 +1,47 @@ +package org.openapitools + +import org.http4s.circe._ +import cats.Monad +import cats.syntax.all._ +import cats.data.OptionT +import cats.data.Kleisli +import org.http4s._ +import org.http4s.server._ + +import org.openapitools.apis._ + +final case class API [ + F[_]: JsonDecoder: Monad, petstore_auth, api_key +]( + petstoreAuth: Kleisli[OptionT[F, *], Request[F], petstore_auth], + apiKey: Kleisli[OptionT[F, *], Request[F], api_key], +)( + delegatePetApi: PetApiDelegate[F, petstore_auth, api_key], + delegateStoreApi: StoreApiDelegate[F, api_key], + delegateUserApi: UserApiDelegate[F, api_key], +){ + val apiKeyMiddleware = AuthMiddleware.withFallThrough(apiKey) + val petstoreAuthMiddleware = AuthMiddleware(petstoreAuth) + + val petApiRoutes = new PetApiRoutes(delegatePetApi) + val storeApiRoutes = new StoreApiRoutes(delegateStoreApi) + val userApiRoutes = new UserApiRoutes(delegateUserApi) + + val routes = + storeApiRoutes.routes <+> + userApiRoutes.routes + + val routesapi_key = apiKeyMiddleware( + petApiRoutes.routesapi_key <+> + storeApiRoutes.routesapi_key <+> + userApiRoutes.routesapi_key + ) + val routespetstore_auth = petstoreAuthMiddleware( + petApiRoutes.routespetstore_auth + ) + + val routesAll = + routes <+> + routesapi_key <+> + routespetstore_auth +} diff --git a/samples/server/petstore/scala-http4s-server/src/main/scala/org/openapitools/apis/PetApi.scala b/samples/server/petstore/scala-http4s-server/src/main/scala/org/openapitools/apis/PetApi.scala new file mode 100644 index 00000000000..0a3c44fa3ed --- /dev/null +++ b/samples/server/petstore/scala-http4s-server/src/main/scala/org/openapitools/apis/PetApi.scala @@ -0,0 +1,361 @@ +package org.openapitools.apis + +import org.openapitools.apis.path._ +import org.openapitools.apis.query._ + +import org.openapitools.models.ApiResponse +import java.io.File +import org.openapitools.models.Pet + + +import cats.Monad +import cats.syntax.all._ + +import org.http4s._ +import org.http4s.circe._ +import org.http4s.server._ +import org.http4s.headers._ +import org.http4s.dsl.Http4sDsl +import org.http4s.circe.CirceEntityEncoder._ + +final case class PetApiRoutes[ + F[_]: JsonDecoder: Monad, petstore_auth, api_key +](delegate: PetApiDelegate[F, petstore_auth, api_key]) extends Http4sDsl[F] { + object addPet { + import PetApiDelegate.addPetResponses + + + val routepetstore_auth = AuthedRoutes.of[petstore_auth, F] { + case (req @ POST -> Root / "pet") as auth => + req.contentType match { + case Some(`Content-Type`(MediaType.application.json, _)) => + delegate.addPet.handle_petstore_auth(auth, req, req.asJsonDecode[Pet] , responses) + + case _ => + delegate.addPet.handle_petstore_auth(auth, req, responses) + + } + } + + val responses: addPetResponses[F] = new addPetResponses[F] { + def resp200(value: Pet): F[Response[F]] = Ok(value) + def resp200(): F[Response[F]] = Ok() + def resp405(allow: Allow): F[Response[F]] = MethodNotAllowed(allow) + } + } + object deletePet { + import PetApiDelegate.deletePetResponses + + + val routepetstore_auth = AuthedRoutes.of[petstore_auth, F] { + case (req @ DELETE -> Root / "pet" / LongVarr(petId)) as auth => + delegate.deletePet.handle_petstore_auth(auth, req, petId, responses) + + } + + val responses: deletePetResponses[F] = new deletePetResponses[F] { + def resp400(): F[Response[F]] = BadRequest() + } + } + object findPetsByStatus { + import PetApiDelegate.findPetsByStatusResponses + + object statusQueryParam extends QuerySeqParamDecoderMatcher[String]("status") + + val routepetstore_auth = AuthedRoutes.of[petstore_auth, F] { + case (req @ GET -> Root / "pet" / "findByStatus" :? statusQueryParam(status)) as auth => + delegate.findPetsByStatus.handle_petstore_auth(auth, req, status, responses) + + } + + val responses: findPetsByStatusResponses[F] = new findPetsByStatusResponses[F] { + def resp200(value: List[Pet]): F[Response[F]] = Ok(value) + def resp200(): F[Response[F]] = Ok() + def resp400(): F[Response[F]] = BadRequest() + } + } + object findPetsByTags { + import PetApiDelegate.findPetsByTagsResponses + + object tagsQueryParam extends QuerySeqParamDecoderMatcher[String]("tags") + + val routepetstore_auth = AuthedRoutes.of[petstore_auth, F] { + case (req @ GET -> Root / "pet" / "findByTags" :? tagsQueryParam(tags)) as auth => + delegate.findPetsByTags.handle_petstore_auth(auth, req, tags, responses) + + } + + val responses: findPetsByTagsResponses[F] = new findPetsByTagsResponses[F] { + def resp200(value: List[Pet]): F[Response[F]] = Ok(value) + def resp200(): F[Response[F]] = Ok() + def resp400(): F[Response[F]] = BadRequest() + } + } + object getPetById { + import PetApiDelegate.getPetByIdResponses + + + val routeapi_key = AuthedRoutes.of[api_key, F] { + case (req @ GET -> Root / "pet" / LongVarr(petId)) as auth => + delegate.getPetById.handle_api_key(auth, req, petId, responses) + + } + + val responses: getPetByIdResponses[F] = new getPetByIdResponses[F] { + def resp200(value: Pet): F[Response[F]] = Ok(value) + def resp200(): F[Response[F]] = Ok() + def resp400(): F[Response[F]] = BadRequest() + def resp404(): F[Response[F]] = NotFound() + } + } + object updatePet { + import PetApiDelegate.updatePetResponses + + + val routepetstore_auth = AuthedRoutes.of[petstore_auth, F] { + case (req @ PUT -> Root / "pet") as auth => + req.contentType match { + case Some(`Content-Type`(MediaType.application.json, _)) => + delegate.updatePet.handle_petstore_auth(auth, req, req.asJsonDecode[Pet] , responses) + + case _ => + delegate.updatePet.handle_petstore_auth(auth, req, responses) + + } + } + + val responses: updatePetResponses[F] = new updatePetResponses[F] { + def resp200(value: Pet): F[Response[F]] = Ok(value) + def resp200(): F[Response[F]] = Ok() + def resp400(): F[Response[F]] = BadRequest() + def resp404(): F[Response[F]] = NotFound() + def resp405(allow: Allow): F[Response[F]] = MethodNotAllowed(allow) + } + } + object updatePetWithForm { + import PetApiDelegate.updatePetWithFormResponses + + + val routepetstore_auth = AuthedRoutes.of[petstore_auth, F] { + case (req @ POST -> Root / "pet" / LongVarr(petId)) as auth => + delegate.updatePetWithForm.handle_petstore_auth(auth, req, petId, responses) + + } + + val responses: updatePetWithFormResponses[F] = new updatePetWithFormResponses[F] { + def resp405(allow: Allow): F[Response[F]] = MethodNotAllowed(allow) + } + } + object uploadFile { + import PetApiDelegate.uploadFileResponses + + + val routepetstore_auth = AuthedRoutes.of[petstore_auth, F] { + case (req @ POST -> Root / "pet" / LongVarr(petId) / "uploadImage") as auth => + delegate.uploadFile.handle_petstore_auth(auth, req, petId, responses) + + } + + val responses: uploadFileResponses[F] = new uploadFileResponses[F] { + def resp200(value: ApiResponse): F[Response[F]] = Ok(value) + } + } + + val routespetstore_auth = + addPet.routepetstore_auth <+> + deletePet.routepetstore_auth <+> + findPetsByStatus.routepetstore_auth <+> + findPetsByTags.routepetstore_auth <+> + updatePet.routepetstore_auth <+> + updatePetWithForm.routepetstore_auth <+> + uploadFile.routepetstore_auth + val routesapi_key = + getPetById.routeapi_key +} + +object PetApiDelegate { + trait addPetResponses[F[_]] { + def resp200(value: Pet): F[Response[F]] + def resp200(): F[Response[F]] + def resp405(allow: Allow): F[Response[F]] + } + + trait deletePetResponses[F[_]] { + def resp400(): F[Response[F]] + } + + trait findPetsByStatusResponses[F[_]] { + def resp200(value: List[Pet]): F[Response[F]] + def resp200(): F[Response[F]] + def resp400(): F[Response[F]] + } + + trait findPetsByTagsResponses[F[_]] { + def resp200(value: List[Pet]): F[Response[F]] + def resp200(): F[Response[F]] + def resp400(): F[Response[F]] + } + + trait getPetByIdResponses[F[_]] { + def resp200(value: Pet): F[Response[F]] + def resp200(): F[Response[F]] + def resp400(): F[Response[F]] + def resp404(): F[Response[F]] + } + + trait updatePetResponses[F[_]] { + def resp200(value: Pet): F[Response[F]] + def resp200(): F[Response[F]] + def resp400(): F[Response[F]] + def resp404(): F[Response[F]] + def resp405(allow: Allow): F[Response[F]] + } + + trait updatePetWithFormResponses[F[_]] { + def resp405(allow: Allow): F[Response[F]] + } + + trait uploadFileResponses[F[_]] { + def resp200(value: ApiResponse): F[Response[F]] + } + +} + +trait PetApiDelegate[F[_], petstore_auth, api_key] { + + trait addPet { + import PetApiDelegate.addPetResponses + + + def handle_petstore_auth( + auth: petstore_auth, + req: Request[F], + addPet: F[Pet], + responses: addPetResponses[F] + ): F[Response[F]] + + + + def handle_petstore_auth( + auth: petstore_auth, + req: Request[F], + responses: addPetResponses[F] + ): F[Response[F]] + + } + def addPet: addPet + + + trait deletePet { + import PetApiDelegate.deletePetResponses + + + def handle_petstore_auth( + auth: petstore_auth, + req: Request[F], + petId: Long, + responses: deletePetResponses[F] + ): F[Response[F]] + + } + def deletePet: deletePet + + + trait findPetsByStatus { + import PetApiDelegate.findPetsByStatusResponses + + + def handle_petstore_auth( + auth: petstore_auth, + req: Request[F], + status: List[String], + responses: findPetsByStatusResponses[F] + ): F[Response[F]] + + } + def findPetsByStatus: findPetsByStatus + + + trait findPetsByTags { + import PetApiDelegate.findPetsByTagsResponses + + + def handle_petstore_auth( + auth: petstore_auth, + req: Request[F], + tags: List[String], + responses: findPetsByTagsResponses[F] + ): F[Response[F]] + + } + def findPetsByTags: findPetsByTags + + + trait getPetById { + import PetApiDelegate.getPetByIdResponses + + + def handle_api_key( + auth: api_key, + req: Request[F], + petId: Long, + responses: getPetByIdResponses[F] + ): F[Response[F]] + + } + def getPetById: getPetById + + + trait updatePet { + import PetApiDelegate.updatePetResponses + + + def handle_petstore_auth( + auth: petstore_auth, + req: Request[F], + updatePet: F[Pet], + responses: updatePetResponses[F] + ): F[Response[F]] + + + + def handle_petstore_auth( + auth: petstore_auth, + req: Request[F], + responses: updatePetResponses[F] + ): F[Response[F]] + + } + def updatePet: updatePet + + + trait updatePetWithForm { + import PetApiDelegate.updatePetWithFormResponses + + + def handle_petstore_auth( + auth: petstore_auth, + req: Request[F], + petId: Long, + responses: updatePetWithFormResponses[F] + ): F[Response[F]] + + } + def updatePetWithForm: updatePetWithForm + + + trait uploadFile { + import PetApiDelegate.uploadFileResponses + + + def handle_petstore_auth( + auth: petstore_auth, + req: Request[F], + petId: Long, + responses: uploadFileResponses[F] + ): F[Response[F]] + + } + def uploadFile: uploadFile + +} \ No newline at end of file diff --git a/samples/server/petstore/scala-http4s-server/src/main/scala/org/openapitools/apis/StoreApi.scala b/samples/server/petstore/scala-http4s-server/src/main/scala/org/openapitools/apis/StoreApi.scala new file mode 100644 index 00000000000..2b7925d9607 --- /dev/null +++ b/samples/server/petstore/scala-http4s-server/src/main/scala/org/openapitools/apis/StoreApi.scala @@ -0,0 +1,181 @@ +package org.openapitools.apis + +import org.openapitools.apis.path._ +import org.openapitools.apis.query._ + +import org.openapitools.models.Order + +import eu.timepit.refined.api.Refined +import eu.timepit.refined.boolean.And +import eu.timepit.refined.numeric.GreaterEqual +import eu.timepit.refined.numeric.LessEqual + +import cats.Monad +import cats.syntax.all._ + +import org.http4s._ +import org.http4s.circe._ +import org.http4s.server._ +import org.http4s.headers._ +import org.http4s.dsl.Http4sDsl +import org.http4s.circe.CirceEntityEncoder._ + +final case class StoreApiRoutes[ + F[_]: JsonDecoder: Monad, api_key +](delegate: StoreApiDelegate[F, api_key]) extends Http4sDsl[F] { + object deleteOrder { + import StoreApiDelegate.deleteOrderResponses + + + val route = HttpRoutes.of[F] { + case req @ DELETE -> Root / "store" / "order" / orderId => + delegate.deleteOrder.handle(req, orderId, responses) + + } + + + val responses: deleteOrderResponses[F] = new deleteOrderResponses[F] { + def resp400(): F[Response[F]] = BadRequest() + def resp404(): F[Response[F]] = NotFound() + } + } + object getInventory { + import StoreApiDelegate.getInventoryResponses + + + val routeapi_key = AuthedRoutes.of[api_key, F] { + case (req @ GET -> Root / "store" / "inventory") as auth => + delegate.getInventory.handle_api_key(auth, req, responses) + + } + + val responses: getInventoryResponses[F] = new getInventoryResponses[F] { + def resp200(value: Map[String, Int]): F[Response[F]] = Ok(value) + } + } + object getOrderById { + import StoreApiDelegate.getOrderByIdResponses + + object orderIdVarr extends RefinedVarr[Long, GreaterEqual[1] And LessEqual[5]] + + val route = HttpRoutes.of[F] { + case req @ GET -> Root / "store" / "order" / orderIdVarr(orderId) => + delegate.getOrderById.handle(req, orderId, responses) + + } + + + val responses: getOrderByIdResponses[F] = new getOrderByIdResponses[F] { + def resp200(value: Order): F[Response[F]] = Ok(value) + def resp200(): F[Response[F]] = Ok() + def resp400(): F[Response[F]] = BadRequest() + def resp404(): F[Response[F]] = NotFound() + } + } + object placeOrder { + import StoreApiDelegate.placeOrderResponses + + + val route = HttpRoutes.of[F] { + case req @ POST -> Root / "store" / "order" => + delegate.placeOrder.handle(req, req.asJsonDecode[Order] , responses) + + } + + + val responses: placeOrderResponses[F] = new placeOrderResponses[F] { + def resp200(value: Order): F[Response[F]] = Ok(value) + def resp200(): F[Response[F]] = Ok() + def resp400(): F[Response[F]] = BadRequest() + } + } + + val routes = + deleteOrder.route <+> + getOrderById.route <+> + placeOrder.route + val routesapi_key = + getInventory.routeapi_key +} + +object StoreApiDelegate { + trait deleteOrderResponses[F[_]] { + def resp400(): F[Response[F]] + def resp404(): F[Response[F]] + } + + trait getInventoryResponses[F[_]] { + def resp200(value: Map[String, Int]): F[Response[F]] + } + + trait getOrderByIdResponses[F[_]] { + def resp200(value: Order): F[Response[F]] + def resp200(): F[Response[F]] + def resp400(): F[Response[F]] + def resp404(): F[Response[F]] + } + + trait placeOrderResponses[F[_]] { + def resp200(value: Order): F[Response[F]] + def resp200(): F[Response[F]] + def resp400(): F[Response[F]] + } + +} + +trait StoreApiDelegate[F[_], api_key] { + + trait deleteOrder { + import StoreApiDelegate.deleteOrderResponses + + def handle( + req: Request[F], + orderId: String, + responses: deleteOrderResponses[F] + ): F[Response[F]] + + } + def deleteOrder: deleteOrder + + + trait getInventory { + import StoreApiDelegate.getInventoryResponses + + + def handle_api_key( + auth: api_key, + req: Request[F], + responses: getInventoryResponses[F] + ): F[Response[F]] + + } + def getInventory: getInventory + + + trait getOrderById { + import StoreApiDelegate.getOrderByIdResponses + + def handle( + req: Request[F], + orderId: Refined[Long, GreaterEqual[1] And LessEqual[5]], + responses: getOrderByIdResponses[F] + ): F[Response[F]] + + } + def getOrderById: getOrderById + + + trait placeOrder { + import StoreApiDelegate.placeOrderResponses + + def handle( + req: Request[F], + placeOrder: F[Order], + responses: placeOrderResponses[F] + ): F[Response[F]] + + + } + def placeOrder: placeOrder + +} \ No newline at end of file diff --git a/samples/server/petstore/scala-http4s-server/src/main/scala/org/openapitools/apis/UserApi.scala b/samples/server/petstore/scala-http4s-server/src/main/scala/org/openapitools/apis/UserApi.scala new file mode 100644 index 00000000000..bbee7dca5de --- /dev/null +++ b/samples/server/petstore/scala-http4s-server/src/main/scala/org/openapitools/apis/UserApi.scala @@ -0,0 +1,326 @@ +package org.openapitools.apis + +import org.openapitools.apis.path._ +import org.openapitools.apis.query._ + +import org.openapitools.models.User +import java.time.ZonedDateTime + +import eu.timepit.refined.api.Refined +import eu.timepit.refined.boolean.And +import eu.timepit.refined.string.MatchesRegex + +import cats.Monad +import cats.syntax.all._ + +import org.http4s._ +import org.http4s.circe._ +import org.http4s.server._ +import org.http4s.headers._ +import org.http4s.dsl.Http4sDsl +import org.http4s.circe.CirceEntityEncoder._ + +final case class UserApiRoutes[ + F[_]: JsonDecoder: Monad, api_key +](delegate: UserApiDelegate[F, api_key]) extends Http4sDsl[F] { + object createUser { + import UserApiDelegate.createUserResponses + + + val routeapi_key = AuthedRoutes.of[api_key, F] { + case (req @ POST -> Root / "user") as auth => + delegate.createUser.handle_api_key(auth, req, req.asJsonDecode[User] , responses) + + } + + val responses: createUserResponses[F] = new createUserResponses[F] { + def resp200(): F[Response[F]] = Ok() + } + } + object createUsersWithArrayInput { + import UserApiDelegate.createUsersWithArrayInputResponses + + + val routeapi_key = AuthedRoutes.of[api_key, F] { + case (req @ POST -> Root / "user" / "createWithArray") as auth => + delegate.createUsersWithArrayInput.handle_api_key(auth, req, req.asJsonDecode[List[User]] , responses) + + } + + val responses: createUsersWithArrayInputResponses[F] = new createUsersWithArrayInputResponses[F] { + def resp200(): F[Response[F]] = Ok() + } + } + object createUsersWithListInput { + import UserApiDelegate.createUsersWithListInputResponses + + + val routeapi_key = AuthedRoutes.of[api_key, F] { + case (req @ POST -> Root / "user" / "createWithList") as auth => + delegate.createUsersWithListInput.handle_api_key(auth, req, req.asJsonDecode[List[User]] , responses) + + } + + val responses: createUsersWithListInputResponses[F] = new createUsersWithListInputResponses[F] { + def resp200(): F[Response[F]] = Ok() + } + } + object deleteUser { + import UserApiDelegate.deleteUserResponses + + + val routeapi_key = AuthedRoutes.of[api_key, F] { + case (req @ DELETE -> Root / "user" / username) as auth => + delegate.deleteUser.handle_api_key(auth, req, username, responses) + + } + + val responses: deleteUserResponses[F] = new deleteUserResponses[F] { + def resp400(): F[Response[F]] = BadRequest() + def resp404(): F[Response[F]] = NotFound() + } + } + object getUserByName { + import UserApiDelegate.getUserByNameResponses + + + val route = HttpRoutes.of[F] { + case req @ GET -> Root / "user" / username => + delegate.getUserByName.handle(req, username, responses) + + } + + + val responses: getUserByNameResponses[F] = new getUserByNameResponses[F] { + def resp200(value: User): F[Response[F]] = Ok(value) + def resp200(): F[Response[F]] = Ok() + def resp400(): F[Response[F]] = BadRequest() + def resp404(): F[Response[F]] = NotFound() + } + } + object loginUser { + import UserApiDelegate.loginUserResponses + + object usernameQueryParam extends QueryParamDecoderMatcher[Refined[String, MatchesRegex["^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$"]]]("username") + object passwordQueryParam extends QueryParamDecoderMatcher[String]("password") + + val route = HttpRoutes.of[F] { + case req @ GET -> Root / "user" / "login" :? usernameQueryParam(username) +& passwordQueryParam(password) => + delegate.loginUser.handle(req, username, password, responses) + + } + + + val responses: loginUserResponses[F] = new loginUserResponses[F] { + def resp200(value: String): F[Response[F]] = Ok(value) + def resp200(): F[Response[F]] = Ok() + def resp400(): F[Response[F]] = BadRequest() + } + } + object logoutUser { + import UserApiDelegate.logoutUserResponses + + + val routeapi_key = AuthedRoutes.of[api_key, F] { + case (req @ GET -> Root / "user" / "logout") as auth => + delegate.logoutUser.handle_api_key(auth, req, responses) + + } + + val responses: logoutUserResponses[F] = new logoutUserResponses[F] { + def resp200(): F[Response[F]] = Ok() + } + } + object updateUser { + import UserApiDelegate.updateUserResponses + + + val routeapi_key = AuthedRoutes.of[api_key, F] { + case (req @ PUT -> Root / "user" / username) as auth => + delegate.updateUser.handle_api_key(auth, req, req.asJsonDecode[User] , username, responses) + + } + + val responses: updateUserResponses[F] = new updateUserResponses[F] { + def resp400(): F[Response[F]] = BadRequest() + def resp404(): F[Response[F]] = NotFound() + } + } + + val routes = + getUserByName.route <+> + loginUser.route + val routesapi_key = + createUser.routeapi_key <+> + createUsersWithArrayInput.routeapi_key <+> + createUsersWithListInput.routeapi_key <+> + deleteUser.routeapi_key <+> + logoutUser.routeapi_key <+> + updateUser.routeapi_key +} + +object UserApiDelegate { + trait createUserResponses[F[_]] { + def resp200(): F[Response[F]] + } + + trait createUsersWithArrayInputResponses[F[_]] { + def resp200(): F[Response[F]] + } + + trait createUsersWithListInputResponses[F[_]] { + def resp200(): F[Response[F]] + } + + trait deleteUserResponses[F[_]] { + def resp400(): F[Response[F]] + def resp404(): F[Response[F]] + } + + trait getUserByNameResponses[F[_]] { + def resp200(value: User): F[Response[F]] + def resp200(): F[Response[F]] + def resp400(): F[Response[F]] + def resp404(): F[Response[F]] + } + + trait loginUserResponses[F[_]] { + def resp200(value: String): F[Response[F]] + def resp200(): F[Response[F]] + def resp400(): F[Response[F]] + } + + trait logoutUserResponses[F[_]] { + def resp200(): F[Response[F]] + } + + trait updateUserResponses[F[_]] { + def resp400(): F[Response[F]] + def resp404(): F[Response[F]] + } + +} + +trait UserApiDelegate[F[_], api_key] { + + trait createUser { + import UserApiDelegate.createUserResponses + + + def handle_api_key( + auth: api_key, + req: Request[F], + createUser: F[User], + responses: createUserResponses[F] + ): F[Response[F]] + + + } + def createUser: createUser + + + trait createUsersWithArrayInput { + import UserApiDelegate.createUsersWithArrayInputResponses + + + def handle_api_key( + auth: api_key, + req: Request[F], + createUsersWithArrayInput: F[List[User]], + responses: createUsersWithArrayInputResponses[F] + ): F[Response[F]] + + + } + def createUsersWithArrayInput: createUsersWithArrayInput + + + trait createUsersWithListInput { + import UserApiDelegate.createUsersWithListInputResponses + + + def handle_api_key( + auth: api_key, + req: Request[F], + createUsersWithListInput: F[List[User]], + responses: createUsersWithListInputResponses[F] + ): F[Response[F]] + + + } + def createUsersWithListInput: createUsersWithListInput + + + trait deleteUser { + import UserApiDelegate.deleteUserResponses + + + def handle_api_key( + auth: api_key, + req: Request[F], + username: String, + responses: deleteUserResponses[F] + ): F[Response[F]] + + } + def deleteUser: deleteUser + + + trait getUserByName { + import UserApiDelegate.getUserByNameResponses + + def handle( + req: Request[F], + username: String, + responses: getUserByNameResponses[F] + ): F[Response[F]] + + } + def getUserByName: getUserByName + + + trait loginUser { + import UserApiDelegate.loginUserResponses + + def handle( + req: Request[F], + username: Refined[String, MatchesRegex["^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$"]], + password: String, + responses: loginUserResponses[F] + ): F[Response[F]] + + } + def loginUser: loginUser + + + trait logoutUser { + import UserApiDelegate.logoutUserResponses + + + def handle_api_key( + auth: api_key, + req: Request[F], + responses: logoutUserResponses[F] + ): F[Response[F]] + + } + def logoutUser: logoutUser + + + trait updateUser { + import UserApiDelegate.updateUserResponses + + + def handle_api_key( + auth: api_key, + req: Request[F], + updateUser: F[User], + username: String, + responses: updateUserResponses[F] + ): F[Response[F]] + + + } + def updateUser: updateUser + +} \ No newline at end of file diff --git a/samples/server/petstore/scala-http4s-server/src/main/scala/org/openapitools/apis/path.scala b/samples/server/petstore/scala-http4s-server/src/main/scala/org/openapitools/apis/path.scala new file mode 100644 index 00000000000..ea838424fec --- /dev/null +++ b/samples/server/petstore/scala-http4s-server/src/main/scala/org/openapitools/apis/path.scala @@ -0,0 +1,62 @@ +package org.openapitools.apis + +import cats.syntax.all._ +import cats.data.ValidatedNel + +import eu.timepit.refined._ +import eu.timepit.refined.api.Refined +import eu.timepit.refined.api.Validate + +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.ZonedDateTime +import java.util.UUID + +import scala.util.Try + +object path { + trait Varrr[T] { + def unapply(str: String): Option[T] + } + + implicit val LocalDateVarr: Varrr[LocalDate] = new Varrr[LocalDate] { + def unapply(str: String): Option[LocalDate] = Try(LocalDate.parse(str)).toOption + } + + implicit val LocalDateTimeVarr: Varrr[LocalDateTime] = new Varrr[LocalDateTime] { + def unapply(str: String): Option[LocalDateTime] = Try(LocalDateTime.parse(str)).toOption + } + + implicit val ZonedDateTimeVarr: Varrr[ZonedDateTime] = new Varrr[ZonedDateTime] { + def unapply(str: String): Option[ZonedDateTime] = Try(ZonedDateTime.parse(str)).toOption + } + + implicit val UUIDVarr: Varrr[UUID] = new Varrr[UUID] { + def unapply(str: String): Option[UUID] = Try(java.util.UUID.fromString(str)).toOption + } + + implicit val IntVarr: Varrr[Int] = new Varrr[Int] { + def unapply(str: String): Option[Int] = Try(str.toInt).toOption + } + + implicit val LongVarr: Varrr[Long] = new Varrr[Long] { + def unapply(str: String): Option[Long] = Try(str.toLong).toOption + } + + implicit val DoubleVarr: Varrr[Double] = new Varrr[Double] { + def unapply(str: String): Option[Double] = Try(str.toDouble).toOption + } + + implicit val BigDecimalVarr: Varrr[BigDecimal] = new Varrr[BigDecimal] { + def unapply(str: String): Option[BigDecimal] = Try(BigDecimal(str)).toOption + } + + implicit val StringVarr: Varrr[String] = new Varrr[String] { + def unapply(str: String): Option[String] = str.some + } + + abstract class RefinedVarr[T, P](implicit varrr: Varrr[T], validate: Validate[T, P]) extends Varrr[Refined[T, P]] { + def unapply(str: String): Option[Refined[T, P]] = + varrr.unapply(str).flatMap(x => refineV(x).toOption) + } +} diff --git a/samples/server/petstore/scala-http4s-server/src/main/scala/org/openapitools/apis/query.scala b/samples/server/petstore/scala-http4s-server/src/main/scala/org/openapitools/apis/query.scala new file mode 100644 index 00000000000..efc86b959e0 --- /dev/null +++ b/samples/server/petstore/scala-http4s-server/src/main/scala/org/openapitools/apis/query.scala @@ -0,0 +1,59 @@ +package org.openapitools.apis + +import cats.data.NonEmptyList +import cats.data.ValidatedNel +import cats.syntax.all._ + +import eu.timepit.refined._ +import eu.timepit.refined.api.Refined +import eu.timepit.refined.api.Validate + +import org.http4s.ParseFailure +import org.http4s.QueryParamDecoder +import org.http4s.QueryParameterValue + +import java.time._ +import java.util.UUID + +object query { + implicit def rrrefinedQueryParamDecoder[T, P]( + implicit tDecoder: QueryParamDecoder[T], validate: Validate[T, P] + ): QueryParamDecoder[Refined[T, P]] = new QueryParamDecoder[Refined[T, P]] { + def decode(value: QueryParameterValue): ValidatedNel[ParseFailure,Refined[T, P]] = + tDecoder.decode(value).withEither(t => t.flatMap(x => + refineV(x).leftMap(err => NonEmptyList.one(ParseFailure(err, err))) + )) + } + + abstract class QuerySeqParamDecoderMatcher[T: QueryParamDecoder](name: String) { + def unapply(params: Map[String, Seq[String]]): Option[List[T]] = + params + .get(name) + .flatMap(values => + values.toList.traverse(s => QueryParamDecoder[T].decode(QueryParameterValue(s)).toOption)) + } + + abstract class OptionalQuerySeqParamDecoderMatcher[T: QueryParamDecoder](name: String) { + def unapply(params: Map[String, List[String]]): Option[Option[List[T]]] = + params + .get(name) + .flatMap(values => + values.toList.traverse(s => QueryParamDecoder[T].decode(QueryParameterValue(s)).toOption)) + .fold(List.empty[T].some.some)(_.some.some) + } + + implicit lazy val BigDecimalQueryParamDecoder: QueryParamDecoder[BigDecimal] = + QueryParamDecoder.fromUnsafeCast[BigDecimal](x => BigDecimal(x.value))("BigDecimal") + + implicit lazy val LocalDateTimeQueryParamDecoder: QueryParamDecoder[LocalDateTime] = + QueryParamDecoder.fromUnsafeCast[LocalDateTime](x => LocalDateTime.parse(x.value))("LocalDateTime") + + implicit lazy val LocalDateQueryParamDecoder: QueryParamDecoder[LocalDate] = + QueryParamDecoder.fromUnsafeCast[LocalDate](x => LocalDate.parse(x.value))("LocalDateTime") + + implicit lazy val ZonedDateTimeQueryParamDecoder: QueryParamDecoder[ZonedDateTime] = + QueryParamDecoder.fromUnsafeCast[ZonedDateTime](x => ZonedDateTime.parse(x.value))("ZonedDateTime") + + implicit lazy val UUIDQueryParamDecoder: QueryParamDecoder[UUID] = + QueryParamDecoder.fromUnsafeCast[UUID](x => UUID.fromString(x.value))("UUID") +} \ No newline at end of file diff --git a/samples/server/petstore/scala-http4s-server/src/main/scala/org/openapitools/models/types.scala b/samples/server/petstore/scala-http4s-server/src/main/scala/org/openapitools/models/types.scala new file mode 100644 index 00000000000..4db7ce57363 --- /dev/null +++ b/samples/server/petstore/scala-http4s-server/src/main/scala/org/openapitools/models/types.scala @@ -0,0 +1,134 @@ +package org.openapitools.models + +import java.time._ + +import io.circe.refined._ +import io.circe.syntax._ +import io.circe.{ Decoder, Encoder } +import io.circe.generic.semiauto.{ deriveDecoder, deriveEncoder } + +import eu.timepit.refined.api.Refined +import eu.timepit.refined.boolean.And +import eu.timepit.refined.string.MatchesRegex +import java.time.ZonedDateTime + +/** +* Describes the result of uploading an image resource +* @param code +* @param _type +* @param message +*/ + +case class ApiResponse( + code: Option[Int], + _type: Option[String], + message: Option[String] +) +object ApiResponse { + implicit val encoderApiResponse: Encoder[ApiResponse] = deriveEncoder[ApiResponse].mapJson(_.dropNullValues) + implicit val decoderApiResponse: Decoder[ApiResponse] = deriveDecoder[ApiResponse] +} + +/** +* A category for a pet +* @param id +* @param name +*/ + +case class Category( + id: Option[Long], + name: Option[Refined[String, MatchesRegex["^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$"]]] +) +object Category { + implicit val encoderCategory: Encoder[Category] = deriveEncoder[Category].mapJson(_.dropNullValues) + implicit val decoderCategory: Decoder[Category] = deriveDecoder[Category] +} + +/** +* An order for a pets from the pet store +* @param id +* @param petId +* @param quantity +* @param shipDate +* @param status Order Status +* @param complete +*/ + +case class Order( + id: Option[Long], + petId: Option[Long], + quantity: Option[Int], + shipDate: Option[ZonedDateTime], + status: Option[String], + complete: Option[Boolean] +) +object Order { + implicit val encoderOrder: Encoder[Order] = deriveEncoder[Order].mapJson(_.dropNullValues) + implicit val decoderOrder: Decoder[Order] = deriveDecoder[Order] +} + +/** +* A pet for sale in the pet store +* @param id +* @param category +* @param name +* @param photoUrls +* @param tags +* @param status pet status in the store +*/ + +case class Pet( + id: Option[Long], + category: Option[Category], + name: String, + photoUrls: List[String], + tags: Option[List[Tag]], + status: Option[String] +) +object Pet { + implicit val encoderPet: Encoder[Pet] = deriveEncoder[Pet].mapJson(_.dropNullValues) + implicit val decoderPet: Decoder[Pet] = deriveDecoder[Pet] +} + +/** +* A tag for a pet +* @param id +* @param name +*/ + +case class Tag( + id: Option[Long], + name: Option[String] +) +object Tag { + implicit val encoderTag: Encoder[Tag] = deriveEncoder[Tag].mapJson(_.dropNullValues) + implicit val decoderTag: Decoder[Tag] = deriveDecoder[Tag] +} + +/** +* A User who is purchasing from the pet store +* @param id +* @param username +* @param firstName +* @param lastName +* @param email +* @param password +* @param phone +* @param userStatus User Status +*/ + +case class User( + id: Option[Long], + username: Option[String], + firstName: Option[String], + lastName: Option[String], + email: Option[String], + password: Option[String], + phone: Option[String], + userStatus: Option[Int] +) +object User { + implicit val encoderUser: Encoder[User] = deriveEncoder[User].mapJson(_.dropNullValues) + implicit val decoderUser: Decoder[User] = deriveDecoder[User] +} +