parts = Arrays.stream(src.split("/", -1)).collect(Collectors.toList());
+ if (parts.isEmpty()) {
+ return "resources";
+ } else {
+ String srcMain = String.join("/", parts.subList(0, parts.size() - 1));
+ return srcMain + "/resources";
+ }
+ }
+
+ @Override
+ public void processOpenAPI(OpenAPI openAPI) {
+ String jsonOpenAPI = SerializerUtils.toJsonString(openAPI);
+
+ try {
+ String outputFile = getOutputDir() + "/" + getResourceFolder() + "/openapi.json";
+ FileUtils.writeStringToFile(new File(outputFile), jsonOpenAPI, StandardCharsets.UTF_8);
+ LOGGER.info("wrote file to {}", outputFile);
+ } catch (Exception e) {
+ LOGGER.error(e.getMessage(), e);
+ }
+ }
+
+
+ public static class ParamPart {
+ final CodegenParameter param;
+ final String name;
+ final boolean isParam;
+
+ // flag for if there are more path parts
+ boolean hasMore;
+ // flag for if there are more path parts which are parameters
+ boolean hasMoreParams;
+
+ final String conversion;
+
+ public ParamPart(String name, CodegenParameter param) {
+ this.name = name;
+ this.param = param;
+ this.isParam = param != null;
+ this.hasMore = true;
+ this.conversion = !isParam || param.isString ? "" : ".to" + param.dataType;
+ }
+ }
+
+ /**
+ * Cask will compile but 'initialize' can throw a route overlap exception:
+ *
+ * {{{
+ * Routes overlap with wildcards: get /user/logout, get /user/:username, get /user/login
+ * }}}
+ *
+ * Note: The same error persists even if the suffixes are unique:
+ * {{{
+ * Routes overlap with wildcards: get /user/logout/3, get /user/:username/1, get /user/login/2
+ * }}}
+ *
+ * To fix this, we need to identify and resolve conflicts in our generated code.
+ *
+ * # How do we identify conflicts?
+ *
+ *
+ * 1. group routes by their non-param prefixes.
+ *
+ * 2. add an "x-annotation" vendor extension for operations
+ *
+ * 3. add a list of "RouteGroups" which can manually delegate as per below
+ *
+ *
+ * # How do we resolve conflicts?
+ *
+ * We leave out the cask route annotation on the conflicting operations, e.g. :
+ * {{{
+ * //conflict: @cask.get("/user/:username")
+ * def getUserByName(username: String, request: cask.Request) = ...
+ * }}}
+ *
+ * and we introduce a new discriminator function to "manually" call those conflicts:
+ * {{{
+ *
+ * @cask.get("/user", subpath = true)
+ * def userRouteDescriminator(request: cask.Request) = {
+ * request.remainingPathSegments match {
+ * case Seq("logout") => logoutUser(request)
+ * case Seq("login") => loginUser(request)
+ * case Seq(param) => getUserByName(param, request)
+ * }
+ * }
+ * }}}
+ */
+ public static class OperationGroup {
+ List operations = new ArrayList<>();
+ final String pathPrefix;
+ final String httpMethod;
+ final String caskAnnotation;
+ final String methodName;
+
+ // TODO - multiple operations may have the same query params, so we'll need to somehow merge them (and take the right type)
+ public boolean hasGroupQueryParams() {
+ return operations.stream().flatMap(op -> op.queryParams.stream()).count() > 0;
+ }
+
+ /**
+ * This is invoked from `scala-cask/apiRoutesQueryParamsTyped.mustache`
+ *
+ * @return the CodegenParameters
+ */
+ public List getGroupQueryParams() {
+ List list = operations.stream().flatMap(op -> op.queryParams.stream()).map(p -> {
+ final CodegenParameter copy = p.copy();
+ copy.vendorExtensions.put("x-default-value", defaultValue(p));
+ copy.required = false; // all our query params are optional for our work-around as it's a super-set of a few different routes
+ copy.dataType = asScalaDataType(copy, false, true, true);
+ copy.defaultValue = defaultValue(copy);
+ return copy;
+ }
+ ).collect(Collectors.toList());
+
+ return list;
+ }
+
+ @Override
+ public String toString() {
+ List ops = operations.stream().map(o -> o.path + "\n").collect(Collectors.toList());
+ return httpMethod + " " + pathPrefix + " w/ " + operations.size() + " operations:\n" + String.join("", ops);
+ }
+
+ public OperationGroup(String httpMethod, String pathPrefix) {
+ this.httpMethod = httpMethod;
+ this.pathPrefix = pathPrefix;
+ caskAnnotation = "@cask." + httpMethod.toLowerCase(Locale.ROOT);
+
+ List stripped = Arrays.stream(pathPrefix.split("/", -1))
+ .map(ScalaCaskServerCodegen::capitalise).collect(Collectors.toList());
+
+ methodName = "routeWorkAroundFor" + capitalise(httpMethod) + String.join("", stripped);
+ }
+
+ public void add(CodegenOperation op) {
+ if (!op.path.startsWith(pathPrefix)) {
+ throw new IllegalArgumentException("inconsistent path: " + pathPrefix);
+ }
+ if (!op.httpMethod.equals(httpMethod)) {
+ throw new IllegalArgumentException("inconsistent method: " + httpMethod);
+ }
+
+ final List pathParts = new ArrayList<>();
+ final List parts = Arrays.stream(op.path.substring(pathPrefix.length()).split("/", -1)).filter(p -> !p.isEmpty()).collect(Collectors.toList());
+ for (int i = 0; i < parts.size(); i++) {
+ String p = parts.get(i);
+ ScalaCaskServerCodegen.ParamPart pp = hasBrackets(p) ? new ScalaCaskServerCodegen.ParamPart(chompBrackets(p), pathParamForName(op, chompBrackets(p))) : new ScalaCaskServerCodegen.ParamPart(p, null);
+ pathParts.add(pp);
+ }
+
+ List paramPathParts = pathParts.stream().filter(p -> p.isParam).collect(Collectors.toList());
+ if (!paramPathParts.isEmpty()) {
+ final String lastParamName = paramPathParts.get(paramPathParts.size() - 1).name;
+ paramPathParts.forEach(p -> p.hasMoreParams = !p.name.equals(lastParamName));
+ }
+ if (!pathParts.isEmpty()) {
+ pathParts.get(pathParts.size() - 1).hasMore = false;
+ }
+
+ op.vendorExtensions.put("x-path-remaining", pathParts);
+ op.vendorExtensions.put("x-has-path-remaining", !paramPathParts.isEmpty());
+ operations.add(op);
+ }
+
+ public boolean contains(CodegenOperation op) {
+ return operations.contains(op);
+ }
+
+ public void updateAnnotations() {
+ operations.forEach(op -> {
+ String annotation = op.vendorExtensions.get("x-annotation").toString();
+ String conflicts = String.join(", ", operations.stream().map(o -> o.path).collect(Collectors.toList()));
+ op.vendorExtensions.put("x-annotation", "// conflicts with [" + conflicts + "] after" + pathPrefix + ", ignoring " + annotation);
+ });
+ operations = operations.stream().sorted((a, b) -> a.pathParams.size() - b.pathParams.size()).collect(Collectors.toList());
+ }
+ }
+
+
+ static List group(List operationList) {
+ Map groupedByPrefix = new HashMap<>();
+ operationList.forEach(op -> {
+ String prefix = nonParamPathPrefix(op);
+ String key = op.httpMethod + " " + prefix;
+ if (!op.pathParams.isEmpty()) {
+ final ScalaCaskServerCodegen.OperationGroup group = groupedByPrefix.getOrDefault(key, new ScalaCaskServerCodegen.OperationGroup(op.httpMethod, prefix));
+ group.add(op);
+ groupedByPrefix.put(key, group);
+ }
+ });
+ return groupedByPrefix.values().stream().collect(Collectors.toList());
+ }
+
+ static String nonParamPathPrefix(CodegenOperation op) {
+ if (op.pathParams.isEmpty()) {
+ return op.path;
+ }
+
+ final String firstParam = op.pathParams.stream().findFirst().get().paramName;
+ final int i = op.path.indexOf(firstParam);
+ final String path = chompSuffix(op.path.substring(0, i - 1), "/");
+ return path;
+ }
+
+ static List createRouteGroups(List operationList) {
+
+ List groups = group(operationList);
+ operationList.forEach((op) -> {
+ for (final ScalaCaskServerCodegen.OperationGroup group : groups) {
+ // for the usage/call site
+ final String scalaPath = pathWithBracketPlaceholdersRemovedAndXPathIndexAdded(op);
+ op.vendorExtensions.put("x-cask-path", scalaPath);
+
+ final String annotation = "@cask." + op.httpMethod.toLowerCase(Locale.ROOT);
+ op.vendorExtensions.put("x-annotation", annotation);
+ if (!group.contains(op)) {
+ if (op.path.startsWith(group.pathPrefix) && op.httpMethod.equalsIgnoreCase(group.httpMethod)) {
+ group.add(op);
+ }
+ }
+ }
+ });
+
+ List trimmed = groups.stream().filter(g -> g.operations.size() > 1).map(g -> {
+ g.updateAnnotations();
+ return g;
+ }).collect(Collectors.toList());
+ return trimmed;
+ }
+
+ @Override
+ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List allModels) {
+ final Map operations = (Map) objs.get("operations");
+ final List operationList = (List) operations.get("operation");
+
+ objs.put("route-groups", createRouteGroups(operationList));
+
+ operationList.forEach(ScalaCaskServerCodegen::postProcessOperation);
+ return objs;
+ }
+
+ @Override
+ public ModelsMap postProcessModels(ModelsMap objs) {
+ objs.getModels().stream().map(ModelMap::getModel).forEach(this::postProcessModel);
+ return objs;
+ }
+
+ private void setDefaultValueForCodegenProperty(CodegenProperty p) {
+
+ if (p.defaultValue == null || p.defaultValue.trim().isEmpty()) {
+ if (p.getIsEnumOrRef()) {
+ p.defaultValue = "null";
+ } else {
+ p.defaultValue = defaultValueNonOption(p);
+ }
+ } else if (p.defaultValue.contains("Seq.empty")) {
+ p.defaultValue = "Nil";
+ }
+ }
+
+ private void postProcessModel(CodegenModel model) {
+ model.getAllVars().forEach(this::setDefaultValueForCodegenProperty);
+ model.getVars().forEach(this::setDefaultValueForCodegenProperty);
+
+ model.getVars().forEach(ScalaCaskServerCodegen::postProcessProperty);
+ model.getAllVars().forEach(ScalaCaskServerCodegen::postProcessProperty);
+ }
+
+ private static void postProcessOperation(CodegenOperation op) {
+ // force http method to lower case
+ op.httpMethod = op.httpMethod.toLowerCase(Locale.ROOT);
+
+ /* Put in 'x-consumes-json' and 'x-consumes-xml' */
+ op.vendorExtensions.put("x-consumes-json", consumesMimetype(op, "application/json"));
+ op.vendorExtensions.put("x-consumes-xml", consumesMimetype(op, "application/xml"));
+
+ op.bodyParams.stream().filter((b) -> b.isBodyParam).forEach((p) -> {
+ p.vendorExtensions.put("x-consumes-json", consumesMimetype(op, "application/json"));
+ p.vendorExtensions.put("x-consumes-xml", consumesMimetype(op, "application/xml"));
+ });
+
+ /* put in 'x-container-type' to help with unmarshalling from json */
+ op.allParams.forEach((p) -> p.vendorExtensions.put("x-container-type", containerType(p.dataType)));
+ op.bodyParams.forEach((p) -> p.vendorExtensions.put("x-container-type", containerType(p.dataType)));
+
+ final String paramList = op.allParams.stream().map((p) -> p.paramName).collect(Collectors.joining(", "));
+ op.vendorExtensions.put("x-param-list", paramList);
+
+ final Stream typed = op.allParams.stream().map((p) -> p.paramName + " : " + asScalaDataType(p, p.required, false));
+ op.vendorExtensions.put("x-param-list-typed", String.join(", ", typed.collect(Collectors.toList())));
+
+ final Stream typedJson = op.allParams.stream().map((p) -> p.paramName + " : " + asScalaDataType(p, p.required, true));
+ op.vendorExtensions.put("x-param-list-typed-json", String.join(", ", typedJson.collect(Collectors.toList())));
+
+ // for the declaration site
+ op.vendorExtensions.put("x-cask-path-typed", routeArgs(op));
+ op.vendorExtensions.put("x-query-args", queryArgs(op));
+
+ List responses = op.responses.stream().map(r -> r.dataType).filter(Objects::nonNull).collect(Collectors.toList());
+ op.vendorExtensions.put("x-response-type", responses.isEmpty() ? "Unit" : String.join(" | ", responses));
+ }
+
+ private static void postProcessProperty(CodegenProperty p) {
+ p.vendorExtensions.put("x-datatype-model", asScalaDataType(p, p.required, false));
+ p.vendorExtensions.put("x-defaultValue-model", defaultValue(p, p.required, p.defaultValue));
+ String dataTypeData = asScalaDataType(p, p.required, true);
+ p.vendorExtensions.put("x-datatype-data", dataTypeData);
+
+
+ p.vendorExtensions.put("x-containertype-data", containerType(dataTypeData));
+
+ p.vendorExtensions.put("x-defaultValue-data", defaultValueNonOption(p, p.defaultValue));
+
+ // the 'asModel' logic for modelData.mustache
+ //
+ // if it's optional (not required), then wrap the value in Option()
+ // ... unless it's a map or array, in which case it can just be empty
+ //
+ p.vendorExtensions.put("x-wrap-in-optional", !p.required && !p.isArray && !p.isMap);
+
+ // if it's an array or optional, we need to map it as a model -- unless it's a map,
+ // in which case we have to map the values
+ boolean hasItemModel = p.items != null && p.items.isModel;
+ boolean isObjectArray = p.isArray && hasItemModel;
+ boolean isOptionalObj = !p.required && p.isModel;
+ p.vendorExtensions.put("x-map-asModel", (isOptionalObj || isObjectArray) && !p.isMap);
+
+ // when deserialising map objects, the logic is tricky.
+ p.vendorExtensions.put("x-deserialize-asModelMap", p.isMap && hasItemModel);
+
+ // for some reason, an openapi spec with pattern field like this:
+ // pattern: '^[A-Za-z]+$'
+ // will result in the pattern property text of
+ // pattern: '/^[A-Za-z]+$/'
+ if (p.pattern != null && p.pattern.startsWith("/") && p.pattern.endsWith("/")) {
+ p.pattern = p.pattern.substring(1, p.pattern.length() - 1);
+ }
+
+ }
+
+
+ /**
+ * Cask path params use the :pathParam syntax rather than the {pathParam} syntax
+ *
+ * @param op
+ * @return
+ */
+ private static String pathWithBracketPlaceholdersRemovedAndXPathIndexAdded(CodegenOperation op) {
+ String[] items = op.path.split("/", -1);
+ String scalaPath = "";
+ for (int i = 0; i < items.length; ++i) {
+ final String nextPart = hasBrackets(items[i]) ? ":" + chompBrackets(items[i]) : items[i];
+ if (i != items.length - 1) {
+ scalaPath = scalaPath + nextPart + "/";
+ } else {
+ scalaPath = scalaPath + nextPart;
+ }
+ }
+ return scalaPath;
+ }
+
+ private static CodegenParameter pathParamForName(CodegenOperation op, String pathParam) {
+ final CodegenParameter param = op.pathParams.stream().filter(p -> p.paramName.equals(pathParam)).findFirst().get();
+ if (param == null) {
+ throw new RuntimeException("Bug: path param " + pathParam + " not found");
+ }
+ return param;
+ }
+
+ /**
+ * The path placeholders as well as query parameters
+ *
+ * @param op the codegen operations
+ * @return a list of both the path and query parameters as typed arguments (e.g. "aPathArg : Int, request: cask.Request, aQueryArg : Option[Long]")
+ */
+ private static String routeArgs(CodegenOperation op) {
+ final Stream pathParamNames = Arrays.stream(op.path.split("/", -1)).filter(ScalaCaskServerCodegen::hasBrackets).map(p -> {
+ final CodegenParameter param = pathParamForName(op, chompBrackets(p));
+ return param.paramName + " : " + asScalaDataType(param, param.required, true);
+ });
+
+
+ final List pathList = pathParamNames.collect(Collectors.toList());
+
+ // we always include the cask request
+ pathList.add("request: cask.Request");
+
+ final Stream queryParams = op.queryParams.stream().map(p -> {
+ p.vendorExtensions.put("x-default-value", defaultValue(p));
+ return p.paramName + " : " + asScalaDataType(p, p.required, true, true);
+ });
+ pathList.addAll(queryParams.collect(Collectors.toList()));
+ return pathList.isEmpty() ? "" : (String.join(", ", pathList));
+ }
+
+ private static String defaultValue(CodegenParameter p) {
+ return defaultValue(p, p.required, p.defaultValue);
+ }
+
+ private static String defaultValue(IJsonSchemaValidationProperties p, boolean required, String fallbackDefaultValue) {
+ if (!required && !(p.getIsArray() || p.getIsMap())) {
+ return "None";
+ }
+ return defaultValueNonOption(p, fallbackDefaultValue);
+ }
+
+ private static String defaultValueNonOption(IJsonSchemaValidationProperties p, String fallbackDefaultValue) {
+ if (p.getIsArray()) {
+ if (p.getUniqueItems()) {
+ return "Set.empty";
+ }
+ return "Nil";
+ }
+ if (p.getIsMap()) {
+ return "Map.empty";
+ }
+ if (p.getIsNumber()) {
+ return "0";
+ }
+ if (p.getIsEnum()) {
+ return fallbackDefaultValue;
+ }
+ if (p.getIsBoolean()) {
+ return "false";
+ }
+ if (p.getIsUuid()) {
+ return "java.util.UUID.randomUUID()";
+ }
+ if (p.getIsString()) {
+ return "\"\"";
+ }
+ return fallbackDefaultValue;
+ }
+
+ private static String defaultValueNonOption(CodegenProperty p) {
+ if (p.getIsArray()) {
+ return "Nil";
+ }
+ if (p.getIsMap()) {
+ return "Map.empty";
+ }
+ if (p.isNumber || p.isNumeric) {
+ return "0";
+ }
+ if (p.isBoolean) {
+ return "false";
+ }
+ if (p.isUuid) {
+ return "java.util.UUID.randomUUID()";
+ }
+ if (p.isModel) {
+ return "null";
+ }
+ if (p.isDate || p.isDateTime) {
+ return "null";
+ }
+ if (p.isString) {
+ return "\"\"";
+ }
+ return p.defaultValue;
+ }
+
+ private static String queryArgs(final CodegenOperation op) {
+ final List list = op.queryParams.stream().map(p -> p.paramName).collect(Collectors.toList());
+ final String prefix = list.isEmpty() ? "" : ", ";
+ return prefix + String.join(", ", list);
+ }
+
+ /**
+ * For our model classes, we have two variants:
+ *
+ * 1) a {model}.scala one which is a validated, model class
+ * 2) a {model}Data.scala one which is just our data-transfer-object (DTO) which is written primarily for e.g. json serialisation
+ *
+ * The data variant can have nulls and other non-scala things, but they know how to create validated model objects.
+ *
+ * This 'asScalaDataType' is used to ensure the type hierarchy is correct for both the model and data varients.
+ *
+ * e.g. consider this example:
+ * ```
+ * case class Foo(bar : Bar, bazz :List[Bazz])
+ * case class Bar(x : Option[String] = None)
+ * case class Bazz(y : Int)
+ *
+ * // vs
+ *
+ * case class FooData(bar : BarData, bazz :List[BazzData])
+ * case class BarData(x : String = "")
+ * case class BazzData(y : Int)
+ * ```
+ */
+ private static String asScalaDataType(final IJsonSchemaValidationProperties param, boolean required, boolean useJason) {
+ return asScalaDataType(param, required, useJason, !useJason);
+ }
+
+ private static String asScalaDataType(final IJsonSchemaValidationProperties param, boolean required, boolean useJason, boolean allowOptional) {
+ String dataType = (param.getIsModel() && useJason) ? param.getDataType() + "Data" : param.getDataType();
+
+ final String dataSuffix = useJason && param.getItems() != null && param.getItems().getIsModel() ? "Data" : "";
+ if (dataType.startsWith("List[")) {
+ dataType = dataType.replace("List[", "Seq[");
+ dataType = dataType.replace("]", dataSuffix + "]");
+ } else if (dataType.startsWith("Set[")) {
+ dataType = dataType.replace("]", dataSuffix + "]");
+ } else if (!required && allowOptional) {
+ dataType = "Option[" + dataType + "]";
+ }
+ return dataType;
+ }
+
+ private static String chompBrackets(String str) {
+ return str.replace("{", "").replace("}", "");
+ }
+
+ private static String chompSuffix(String str, String suffix) {
+ return str.endsWith(suffix) ? chompSuffix(str.substring(0, str.length() - suffix.length()), suffix) : str;
+ }
+
+ private static boolean hasBrackets(String str) {
+ return str.matches("^\\{(.*)\\}$");
+ }
+
+ static String containerType(String dataType) {
+ String fixedForList = dataType.replaceAll(".*\\[(.*)\\]", "$1");
+
+ // if it is a map, we want the value type
+ final String[] parts = fixedForList.split(",");
+ return parts[parts.length - 1];
+ }
+}
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 11c50f7c170..9849aa702f3 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
@@ -115,6 +115,7 @@ org.openapitools.codegen.languages.RustClientCodegen
org.openapitools.codegen.languages.RustServerCodegen
org.openapitools.codegen.languages.ScalatraServerCodegen
org.openapitools.codegen.languages.ScalaAkkaClientCodegen
+org.openapitools.codegen.languages.ScalaCaskServerCodegen
org.openapitools.codegen.languages.ScalaPekkoClientCodegen
org.openapitools.codegen.languages.ScalaAkkaHttpServerCodegen
org.openapitools.codegen.languages.ScalaFinchServerCodegen
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/.scalafmt.conf.mustache b/modules/openapi-generator/src/main/resources/scala-cask/.scalafmt.conf.mustache
new file mode 100644
index 00000000000..e150d4c409d
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/.scalafmt.conf.mustache
@@ -0,0 +1,4 @@
+version = 3.6.1
+align.preset = more // For pretty alignment.
+maxColumn = 100
+runner.dialect = scala3
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/Dockerfile.mustache b/modules/openapi-generator/src/main/resources/scala-cask/Dockerfile.mustache
new file mode 100644
index 00000000000..0c548753973
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/Dockerfile.mustache
@@ -0,0 +1,13 @@
+FROM virtuslab/scala-cli:latest as build
+WORKDIR /app
+COPY ./Server.scala /app/
+# note: this assumes a published server stub jar.
+# If you've published this locally, you would need to copy those into this image,
+# perhaps by using coursier fetch
+RUN scala-cli --power package /app/Server.scala --assembly -o app.jar
+
+# The main image
+FROM openjdk:23-slim
+WORKDIR /app
+COPY --from=build /app/app.jar /app/
+ENTRYPOINT ["java", "-jar", "/app/app.jar"]
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/README.mustache b/modules/openapi-generator/src/main/resources/scala-cask/README.mustache
new file mode 100644
index 00000000000..39e65480193
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/README.mustache
@@ -0,0 +1,96 @@
+# REST Service
+
+This project contains the data models and REST services, generated from the [openapi-generator](https://github.com/OpenAPITools/openapi-generator) project.
+
+The server implementation is based on Li's excellent [cask](https://com-lihaoyi.github.io/cask/) library.
+
+# How to use this code
+
+This code was designed so that it can be packaged up and semantically-versioned alongside your open-api schema.
+
+That approach supports having separate "contract" repositories for microservice projects, where the pipeline for the
+contract repo might produce versioned jar artefacts which can then easily be brought in / referenced by a separate service project which simply
+implements the business logic.
+
+You can read more about this approach [here](https://github.com/kindservices/contract-first-rest)
+
+# How to implement your business logic
+There are a few options for using this code/applying your business logic for the services.
+
+## Option 1 (preferred): Package and publish this boilerplate
+Typically, OpenApi templates are written to generate code which co-exists alongside the handwritten business logic.
+
+While that works, it's also not ideal:
+ * You have to ensure the generated code isn't checked in
+ * Team members, build pipelines, etc all have to regenerate and recompile the same boilerplate code over and over
+ * People can encounter IDE issues with generated code
+
+Instead, you have the option of simply packaging/publishing this generated code, and then allowing service implementations
+to simply bring in the published code as a dependency.
+
+The steps to do that are:
+
+### Build/Publish
+This project is built using [sbt](https://www.scala-sbt.org/download/), so you can run `sbt publish` (or `sbt publishLocal`)
+
+Or, for a zero-install docker build:
+```bash
+docker run -it --rm -v $(pwd):/app -w /app sbtscala/scala-sbt:eclipse-temurin-17.0.4_1.7.1_3.2.0 sbt publishLocal
+```
+
+### Create a new separate implementation project
+Once published, you can create your server implementation in a new, clean, separate project based on [the example](./example)
+
+This means all the boilerplate endpoint and model code is brought in as "just another jar", and you're free to
+create a greenfield project in whatever language (scala, java, kotlin) and build system of your choosing.
+
+We show a simple, minimalistic example of a starting point in [the example project](./example)
+
+## Option 2: Extend this generated example
+You can configure this project (for instance, setting up your own .gitignore rules and scripts) to leave the generated code as-is
+and provide your implementation alongside the generated code.
+
+The place to start is by providing your own implementation of the Services defined in the `api` package -
+perhaps by creating your 'MyService.scala' code in a new `impl` package.
+
+You then have several options for how to wire those in:
+
+1) Create a new BaseApp instance to create your own Main entry point
+Follow the pattern in App.scala, but by passing your own implementations to BaseApp,
+ensuring you call `start` to start the server
+
+```bash
+@main def run() = BaseApp(/* your services here/*).start()
+```
+
+2) Extend either BaseApp class or mix in the AppRoutes trait
+You can create your own main entry point with further control of the main cask app by extending
+the BaseApp or otherwise creating your own CaskApp which mixes in the AppRoutes
+
+```bash
+object MyApp extends BaseApp(/* your services here/*) {
+ // any overrides, new routes, etc here
+ start()
+}
+```
+
+
+# Customising the generated code
+
+A typical config.yml used to alter the generated code may look like this:
+```
+groupId: "ex.amp.le"
+artifactId: "pets-test"
+apiPackage: "ex.ample.api"
+modelPackage: "ex.ample.model"
+```
+
+Which you would then pass to the generator like this:
+```
+docker run --rm \
+-v ${PWD}:/local openapitools/openapi-generator-cli generate \
+-i https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3.0/petstore.yaml \
+-g scala-cask \
+-c /local/config.yml \
+-o /local/path/to/output_dir
+```
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/api.mustache b/modules/openapi-generator/src/main/resources/scala-cask/api.mustache
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/apiPackage.mustache b/modules/openapi-generator/src/main/resources/scala-cask/apiPackage.mustache
new file mode 100644
index 00000000000..6acc47b3100
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/apiPackage.mustache
@@ -0,0 +1,155 @@
+{{>licenseInfo}}
+package {{apiPackage}}
+
+
+import cask.FormEntry
+import io.undertow.server.handlers.form.{FormData, FormParserFactory}
+
+import java.io.File
+import scala.jdk.CollectionConverters.*
+import java.time.LocalDate
+import java.util.UUID
+import scala.reflect.ClassTag
+import scala.util.*
+
+// needed for BigDecimal params
+given cask.endpoints.QueryParamReader.SimpleParam[BigDecimal](BigDecimal.apply)
+
+// a parsed value from an HTTP request
+opaque type Parsed[A] = Either[String, A]
+
+object Parsed {
+ def apply[A](value: A): Parsed[A] = Right(value)
+
+ def eval[A](value: => A): Parsed[A] = Try(value) match {
+ case Failure(exp) => Left(s"Error: ${exp.getMessage}")
+ case Success(ok) => Right(ok)
+ }
+
+ def fromTry[A](value : Try[A]) = value match {
+ case Failure(err) => Left(err.getMessage)
+ case Success(ok) => Right(ok)
+ }
+
+ def fail[A](msg: String): Parsed[A] = Left(msg)
+
+ def optionalValue(map: Map[String, collection.Seq[String]], key: String): Parsed[Option[String]] = {
+ map.get(key) match {
+ case Some(Seq(only: String)) => Parsed(Option(only))
+ case Some(Seq()) => Parsed(None)
+ case Some(many) => Parsed.fail(s"${many.size} values set for '$key'")
+ case None => Parsed(None)
+ }
+ }
+
+ def singleValue(map: Map[String, collection.Seq[String]], key : String): Parsed[String] = {
+ map.get(key) match {
+ case Some(Seq(only : String)) => Parsed(only)
+ case Some(Seq()) => Parsed("")
+ case Some(many) => Parsed.fail(s"${many.size} values set for '$key'")
+ case None => Parsed.fail(s"required '$key' was not set")
+ }
+ }
+
+ def manyValues(map: Map[String, collection.Seq[String]], key : String, required: Boolean): Parsed[List[String]] = {
+ map.get(key) match {
+ case Some(many) => Parsed(many.toList)
+ case None if required => Parsed.fail(s"required '$key' was not set")
+ case None => Parsed(Nil)
+ }
+ }
+}
+
+extension[A] (parsed: Parsed[A]) {
+ def toEither: Either[String, A] = parsed
+
+ def asLong(using ev : A =:= String): Parsed[Long] = as[Long](_.toLongOption)
+ def asBoolean(using ev : A =:= String): Parsed[Boolean] = as[Boolean](_.toBooleanOption)
+ def asInt(using ev : A =:= String): Parsed[Int] = as[Int](_.toIntOption)
+ def asByte(using ev : A =:= String): Parsed[Byte] = as[Byte](_.toByteOption)
+ def asUuid(using ev : A =:= String): Parsed[UUID] = as[UUID](x => Try(UUID.fromString(x)).toOption)
+ def asFloat(using ev : A =:= String): Parsed[Float] = as[Float](_.toFloatOption)
+ def asDouble(using ev : A =:= String): Parsed[Double] = as[Double](_.toDoubleOption)
+ def asDate(using ev: A =:= String): Parsed[LocalDate] = as[LocalDate](x => Try(LocalDate.parse(x)).toOption)
+
+ private def as[B : ClassTag](f : String => Option[B])(using ev : A =:= String): Parsed[B] = parsed.flatMap { str =>
+ f(ev(str)) match {
+ case None => Parsed.fail(s"'$str' cannot be parsed as a ${implicitly[ClassTag[B]].runtimeClass}")
+ case Some(x) => Parsed(x)
+ }
+ }
+
+
+ def mapError(f : String => String) : Parsed[A] = parsed match {
+ case Left(msg) => Left(f(msg))
+ case right => right
+ }
+
+ def map[B](f: A => B): Parsed[B] = parsed match {
+ case Right(value) => Right(f(value))
+ case Left(err) => Left(err)
+ }
+ def flatMap[B](f : A => Parsed[B]): Parsed[B] = parsed match {
+ case Right(value) => f(value)
+ case Left(err) => Left(err)
+ }
+}
+
+
+extension (request: cask.Request) {
+
+ def formSingleValueRequired(paramName: String): Parsed[String] = {
+ val data = formDataForKey(paramName).map(_.getValue).toSeq
+ Parsed.singleValue(Map(paramName -> data), paramName)
+ }
+ def formSingleValueOptional(paramName: String): Parsed[Option[String]] = {
+ val data = formDataForKey(paramName).map(_.getValue).toSeq
+ Parsed.optionalValue(Map(paramName -> data), paramName)
+ }
+
+ def formValueAsFileOptional(paramName: String): Parsed[Option[File]] = {
+ val data = formDataForKey(paramName)
+ data.map(_.getFileItem.getFile.toFile).toSeq match {
+ case Seq() => Parsed(None)
+ case Seq(file) => Parsed(Option(file))
+ case many => Parsed.fail(s"${many.size} file values set for '$paramName'")
+ }
+ }
+
+ def formValueAsFileRequired(paramName: String): Parsed[File] = {
+ val data = formDataForKey(paramName)
+ data.map(_.getFileItem.getFile.toFile).toSeq match {
+ case Seq() => Parsed.fail(s"No file form data was submitted for '$paramName'. The submitted form keys were: ${formDataKeys.mkString(",")}")
+ case Seq(file) => Parsed(file)
+ case many => Parsed.fail(s"${many.size} file values set for '$paramName'")
+ }
+ }
+
+ def formManyValues(paramName: String, required: Boolean): Parsed[List[String]] = {
+ val data = formDataForKey(paramName).map(_.getValue).toSeq
+ Parsed.manyValues(Map(paramName -> data), paramName, required)
+ }
+
+ def formData: FormData = FormParserFactory.builder().build().createParser(request.exchange).parseBlocking()
+
+ def formDataKeys: Iterator[String] = formData.iterator().asScala
+
+ def formDataForKey(paramName: String): Iterable[FormData.FormValue] = formData.get(paramName).asScala
+
+ def headerSingleValueOptional(paramName: String): Parsed[Option[String]] = Parsed.optionalValue(request.headers, paramName)
+ def headerSingleValueRequired(paramName: String): Parsed[String] = Parsed.singleValue(request.headers, paramName)
+
+ def headerManyValues(paramName: String, required: Boolean): Parsed[List[String]] = Parsed.manyValues(request.headers, paramName, required)
+
+ def bodyAsString = new String(request.readAllBytes(), "UTF-8")
+
+ def pathValue(index: Int, paramName: String, required : Boolean): Parsed[String] = {
+ request
+ .remainingPathSegments
+ .lift(index) match {
+ case Some(value) => Right(value)
+ case None if required => Left(s"'$paramName'' is a required path parameter at path position $index")
+ case None => Right("")
+ }
+ }
+}
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/apiRoutes.mustache b/modules/openapi-generator/src/main/resources/scala-cask/apiRoutes.mustache
new file mode 100644
index 00000000000..0d1ca45d0d5
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/apiRoutes.mustache
@@ -0,0 +1,65 @@
+//> using scala "3.3.1"
+//> using lib "com.lihaoyi::cask:0.8.3"
+//> using lib "com.lihaoyi::scalatags:0.12.0"
+{{>licenseInfo}}
+
+// this is generated from apiRoutes.mustache
+package {{apiPackage}}
+
+import {{modelPackage}}.*
+
+import upickle.default.{ReadWriter => RW, macroRW}
+import upickle.default.*
+
+{{#imports}}import {{import}}
+{{/imports}}
+
+class {{classname}}Routes(service : {{classname}}Service) extends cask.Routes {
+
+{{#route-groups}}
+ // route group for {{methodName}}
+ {{caskAnnotation}}("{{pathPrefix}}", true)
+ def {{methodName}}(request: cask.Request{{>apiRoutesQueryParamsTyped}}) = {
+ request.remainingPathSegments match {
+ {{#operations}}
+ case Seq({{>pathExtractor}}) => {{operationId}}({{>pathExtractorParams}}request{{>queryParams}})
+ {{/operations}}
+ case _ => cask.Response("Not Found", statusCode = 404)
+ }
+ }
+{{/route-groups}}
+
+{{#operations}}
+ {{#operation}}
+ /** {{summary}}
+ * {{description}}
+ */
+ {{vendorExtensions.x-annotation}}("{{vendorExtensions.x-cask-path}}")
+ def {{operationId}}({{vendorExtensions.x-cask-path-typed}}) = {
+ {{#authMethods}}
+ // auth method {{name}} : {{type}}, keyParamName: {{keyParamName}}
+ {{/authMethods}}
+
+ def failFast = request.queryParams.keySet.contains("failFast")
+
+ val result = {{>parseHttpParams}}
+
+ result match {
+ case Left(error) => cask.Response(error, 500)
+ {{#vendorExtensions.x-has-response-types}}
+ {{#responses}}
+ {{#dataType}}
+ case Right(value : {{dataType}}) => cask.Response(data = write(value), {{code}}, headers = Seq("Content-Type" -> "application/json"))
+ {{/dataType}}
+ {{/responses}}
+ {{/vendorExtensions.x-has-response-types}}
+ {{^vendorExtensions.x-has-response-types}}
+ case Right(_) => cask.Response("", 200)
+ {{/vendorExtensions.x-has-response-types}}
+ }
+ }
+ {{/operation}}
+{{/operations}}
+
+ initialize()
+}
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/apiRoutesQueryParamsTyped.mustache b/modules/openapi-generator/src/main/resources/scala-cask/apiRoutesQueryParamsTyped.mustache
new file mode 100644
index 00000000000..8356421c485
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/apiRoutesQueryParamsTyped.mustache
@@ -0,0 +1 @@
+{{#hasGroupQueryParams}},{{/hasGroupQueryParams}}{{#groupQueryParams}}{{paramName}} : {{dataType}} = {{defaultValue}}{{^-last}},{{/-last}}{{/groupQueryParams}}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/apiService.mustache b/modules/openapi-generator/src/main/resources/scala-cask/apiService.mustache
new file mode 100644
index 00000000000..30ea17b60dd
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/apiService.mustache
@@ -0,0 +1,37 @@
+//> using scala "3.3.1"
+//> using lib "com.lihaoyi::cask:0.8.3"
+//> using lib "com.lihaoyi::scalatags:0.12.0"
+{{>licenseInfo}}
+
+// generated from apiService.mustache
+package {{apiPackage}}
+
+{{#imports}}import _root_.{{import}}
+{{/imports}}
+
+import _root_.{{modelPackage}}.*
+
+object {{classname}}Service {
+ def apply() : {{classname}}Service = new {{classname}}Service {
+{{#operations}}
+ {{#operation}}
+ override def {{operationId}}({{vendorExtensions.x-param-list-typed}}) : {{vendorExtensions.x-response-type}} = ???
+ {{/operation}}
+{{/operations}}
+ }
+}
+
+/**
+ * The {{classname}} business-logic
+ */
+trait {{classname}}Service {
+{{#operations}}
+{{#operation}}
+ /** {{{summary}}}
+ * {{{description}}}
+ * @return {{returnType}}
+ */
+ def {{operationId}}({{vendorExtensions.x-param-list-typed}}) : {{vendorExtensions.x-response-type}}
+{{/operation}}
+{{/operations}}
+}
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/appPackage.mustache b/modules/openapi-generator/src/main/resources/scala-cask/appPackage.mustache
new file mode 100644
index 00000000000..614e06bb9e1
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/appPackage.mustache
@@ -0,0 +1,12 @@
+{{>licenseInfo}}
+package {{packageName}}
+
+def box(str: String): String = {
+ val lines = str.linesIterator.toList
+ val maxLen = (0 +: lines.map(_.length)).max
+ val boxed = lines.map { line =>
+ s" | ${line.padTo(maxLen, ' ')} |"
+ }
+ val bar = " +-" + ("-" * maxLen) + "-+"
+ (bar +: boxed :+ bar).mkString("\n")
+}
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/appRoutes.mustache b/modules/openapi-generator/src/main/resources/scala-cask/appRoutes.mustache
new file mode 100644
index 00000000000..5006b467775
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/appRoutes.mustache
@@ -0,0 +1,40 @@
+//> using scala "3.3.1"
+//> using lib "com.lihaoyi::cask:0.9.2"
+//> using lib "com.lihaoyi::scalatags:0.8.2"
+{{>licenseInfo}}
+
+// this file was generated from app.mustache
+package {{packageName}}
+
+{{#imports}}import {{import}}
+{{/imports}}
+import _root_.{{modelPackage}}.*
+import _root_.{{apiPackage}}.*
+
+/**
+ * This trait encapsulates the business logic (services) and the
+ * http routes which handle the http requests sent to those services.
+ *
+ * There are default 'not implemented' implementations for the service.
+ *
+ * If you wanted fine-grained control over the routes and services, you could
+ * extend the cask.MainRoutes and mix in this trait by using this:
+ *
+ * \{\{\{
+ * override def allRoutes = appRoutes
+ * \}\}\}
+ *
+ * More typically, however, you would extend the 'BaseApp' class
+ */
+trait AppRoutes {
+{{#operations}}
+ def app{{classname}}Service : {{classname}}Service = {{classname}}Service()
+ def routeFor{{classname}} : {{classname}}Routes = {{classname}}Routes(app{{classname}}Service)
+{{/operations}}
+
+ def appRoutes = Seq(
+{{#operations}}
+ routeFor{{classname}} {{^-last}},{{/-last}}
+{{/operations}}
+ )
+}
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/baseApp.mustache b/modules/openapi-generator/src/main/resources/scala-cask/baseApp.mustache
new file mode 100644
index 00000000000..b8f019873c5
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/baseApp.mustache
@@ -0,0 +1,49 @@
+//> using scala "3.3.1"
+//> using lib "com.lihaoyi::cask:0.9.2"
+//> using lib "com.lihaoyi::scalatags:0.8.2"
+{{>licenseInfo}}
+
+// this file was generated from app.mustache
+package {{packageName}}
+
+{{#imports}}import {{import}}
+{{/imports}}
+import _root_.{{modelPackage}}.*
+import _root_.{{apiPackage}}.*
+
+/**
+ * This class was created with the intention of being extended by some runnable object,
+ * passing in the custom business logic services
+ */
+class BaseApp({{#operations}}
+ override val app{{classname}}Service : {{classname}}Service = {{classname}}Service(),
+{{/operations}}
+ override val port : Int = sys.env.get("PORT").map(_.toInt).getOrElse(8080)) extends cask.MainRoutes with AppRoutes {
+
+ /** routes for the UI
+ * Subclasses can override to turn this off
+ */
+ def openApiRoute: Option[cask.Routes] = Option(OpenApiRoutes(port))
+
+ override def allRoutes = appRoutes ++ openApiRoute
+
+ {{^operations}}
+ // no operations!
+ {{/operations}}
+
+ override def host: String = "0.0.0.0"
+
+ def start() = locally {
+ initialize()
+ println(box(s""" 🚀 browse to localhost:$port 🚀
+ | host : $host
+ | port : $port
+ | verbose : $verbose
+ | debugMode : $debugMode
+ |""".stripMargin))
+
+ // if java.awt.Desktop.isDesktopSupported then {
+ // java.awt.Desktop.getDesktop.browse(new java.net.URI(s"http://localhost:${port}"))
+ // }
+ }
+}
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/build.sbt.mustache b/modules/openapi-generator/src/main/resources/scala-cask/build.sbt.mustache
new file mode 100644
index 00000000000..ae0a5ae12e5
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/build.sbt.mustache
@@ -0,0 +1,29 @@
+name := "{{artifactId}}"
+organization:="{{groupId}}"
+version := "0.0.1-SNAPSHOT"
+scalaVersion := "3.3.1"
+scalafmtOnCompile := true
+libraryDependencies ++= Seq(
+ "com.lihaoyi" %% "cask" % "0.9.2" ,
+ "com.lihaoyi" %% "upickle" % "3.2.0",
+ "org.scalatest" %% "scalatest" % "3.2.18" % Test
+)
+
+publishMavenStyle := true
+
+val githubUser = "{{{gitUserId}}}"
+val githubRepo = "{{{gitRepoId}}}"
+publishTo := Some("GitHub Package Registry" at s"https://maven.pkg.github.com/$githubUser/$githubRepo")
+
+sys.env.get("GITHUB_TOKEN") match {
+ case Some(token) if !token.isEmpty =>
+ credentials += Credentials(
+ "GitHub Package Registry",
+ "maven.pkg.github.com",
+ githubUser,
+ token
+ )
+ case _ =>
+ println("\n\t\tGITHUB_TOKEN not set - assuming a local build\n\n")
+ credentials ++= Nil
+}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/build.sc.mustache b/modules/openapi-generator/src/main/resources/scala-cask/build.sc.mustache
new file mode 100644
index 00000000000..a31414db150
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/build.sc.mustache
@@ -0,0 +1,43 @@
+import mill._, scalalib._, scalafmt._, publish._
+
+// Mill build file (see https://mill-build.com/mill/Intro_to_Mill.html).
+// run with:
+//
+// mill _.compile
+// mill _.reformat
+// mill _.publishLocal
+// mill _.test.test
+object {{artifactId}} extends SbtModule with ScalafmtModule with PublishModule {
+ def scalaVersion = "3.3.1"
+
+ def pomSettings = PomSettings(
+ description = "{{artifactId}}",
+ organization = "{{groupId}}",
+ url = "https://github.com//{{artifactId}}",
+ licenses = Seq(License.MIT),
+ versionControl = VersionControl.github("", "{{artifactId}}"),
+ developers = Seq(
+ // Developer("", "", "https://github.com/")
+ )
+ )
+
+ def publishVersion: mill.T[String] = T("0.0.1-SNAPSHOT")
+
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::cask:0.9.2" ,
+ ivy"com.lihaoyi::upickle:3.2.0"
+ )
+
+ override def sources = T.sources(millSourcePath / os.up / "src" / "main" / "scala")
+ override def resources = T.sources(millSourcePath / os.up / "src" / "main" / "resources")
+
+ object test extends SbtModuleTests {
+ def ivyDeps = Agg(
+ ivy"org.scalactic::scalactic:3.2.18",
+ ivy"org.scalatest::scalatest:3.2.18"
+ )
+
+ def testFramework = "org.scalatest.tools.Framework"
+ override def sources = T.sources(millSourcePath / os.up / "src" / "test" / "scala")
+ }
+}
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/bulidAndPublish.yml.mustache b/modules/openapi-generator/src/main/resources/scala-cask/bulidAndPublish.yml.mustache
new file mode 100644
index 00000000000..ead5b4c0dcb
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/bulidAndPublish.yml.mustache
@@ -0,0 +1,41 @@
+name: Scala CI with sbt
+
+on:
+ push:
+ branches: [ main, master ]
+ pull_request:
+ branches: [ main, master ]
+
+jobs:
+ build-and-publish:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Set up JDK 11
+ uses: actions/setup-java@v1
+ with:
+ java-version: 11
+
+ - name: Cache sbt dependencies
+ uses: actions/cache@v2
+ with:
+ path: |
+ ~/.ivy2/cache
+ ~/.sbt
+ ~/.m2
+ key: ${{{openbrackets}}} runner.os {{{closebrackets}}}-sbt-${{{openbrackets}}} hashFiles('**/*.sbt') {{{closebrackets}}}
+ restore-keys: |
+ ${{{openbrackets}}} runner.os {{{closebrackets}}}-sbt-
+
+ - name: Build with sbt
+ run: sbt clean compile
+
+ - name: Test with sbt
+ run: sbt test
+
+ - name: Publish to GitHub Packages
+ run: sbt publish
+ env:
+ GITHUB_TOKEN: ${{{openbrackets}}} secrets.GITHUB_TOKEN {{{closebrackets}}}
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/example.mustache b/modules/openapi-generator/src/main/resources/scala-cask/example.mustache
new file mode 100644
index 00000000000..3d6b5cca779
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/example.mustache
@@ -0,0 +1,60 @@
+//> using scala "3.3.1"
+//> using lib "{{groupId}}::{{artifactId}}:0.0.1-SNAPSHOT"
+//> using repositories https://maven.pkg.github.com/{{{gitUserId}}}/{{{gitRepoId}}}
+
+
+/**
+* This single file can contain the business logic for a REST service.
+* ====================================
+* == zero-install build with docker ==
+* ====================================
+*
+*
+* ```
+* docker build . -t {{artifactId}}:latest
+* ```
+* ======================
+* == Building Locally ==
+* ======================
+* This project can be built using [[scala-clit][https://scala-cli.virtuslab.org]]
+*
+* To simply run the project
+* ```
+* scala-cli Server.scala
+* ```
+*
+* To create a runnable jar, run:
+* ```
+* scala-cli --power package Server.scala -o app-assembly --assembly
+* ```
+*
+* To produce a docker image (no need for the Dockerfile), run:
+* ```
+* scala-cli --power package --docker Server.scala --docker-image-repository app-docker
+* ```
+*
+* To generate an IDE project:
+* ```
+* scala-cli setup-ide . --scala 3.3
+* ```
+*/
+package app
+
+import {{packageName}}.BaseApp
+import {{apiPackage}}.*
+import {{modelPackage}}.*
+
+import java.io.File
+
+// TODO - write your business logic for your services here (the defaults all return 'not implemented'):
+{{#operations}}
+val my{{classname}}Service : {{classname}}Service = {{classname}}Service() // <-- replace this with your implementation
+{{/operations}}
+
+/** This is your main entry point for your REST service
+ * It extends BaseApp which defines the business logic for your services
+ */
+object Server extends BaseApp({{#operations}}app{{classname}}Service = my{{classname}}Service{{^-last}},
+{{/-last}}{{/operations}}):
+ start()
+
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/exampleApp.mustache b/modules/openapi-generator/src/main/resources/scala-cask/exampleApp.mustache
new file mode 100644
index 00000000000..f1247210dac
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/exampleApp.mustache
@@ -0,0 +1,21 @@
+//> using scala "3.3.1"
+//> using lib "com.lihaoyi::cask:0.9.2"
+//> using lib "com.lihaoyi::scalatags:0.8.2"
+{{>licenseInfo}}
+
+// this file was generated from app.mustache
+package {{packageName}}
+
+{{#imports}}import {{import}}
+{{/imports}}
+import _root_.{{modelPackage}}.*
+import _root_.{{apiPackage}}.*
+
+/**
+ * This is an example of how you might extends BaseApp for a runnable application.
+ *
+ * See the README.md for how to create your own app
+ */
+object ExampleApp extends BaseApp() {
+ start()
+}
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/gitignore.mustache b/modules/openapi-generator/src/main/resources/scala-cask/gitignore.mustache
new file mode 100644
index 00000000000..56f2b272a08
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/gitignore.mustache
@@ -0,0 +1,25 @@
+# scala specific
+*.class
+*.log
+
+# sbt specific
+.cache
+.history
+.lib/
+dist/*
+target/
+lib_managed/
+src_managed/
+project/boot/
+project/plugins/project/
+
+# Scala-IDE specific
+.scala_dependencies
+.worksheet
+
+# Mill specific
+out
+
+# IntelliJ specific
+.idea
+*.iml
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/licenseInfo.mustache b/modules/openapi-generator/src/main/resources/scala-cask/licenseInfo.mustache
new file mode 100644
index 00000000000..02c79936aa5
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/licenseInfo.mustache
@@ -0,0 +1,16 @@
+/**
+ * {{{appName}}}
+ * {{{appDescription}}}
+ *
+ {{#version}}
+ * OpenAPI spec version: {{{version}}}
+ *
+ {{/version}}
+ {{#infoEmail}}
+ * Contact: {{{infoEmail}}}
+ *
+ {{/infoEmail}}
+ * NOTE: This class is auto generated by OpenAPI Generator.
+ *
+ * https://openapi-generator.tech
+ */
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/model.mustache b/modules/openapi-generator/src/main/resources/scala-cask/model.mustache
new file mode 100644
index 00000000000..c4b430b7ced
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/model.mustache
@@ -0,0 +1,60 @@
+{{>licenseInfo}}
+// this model was generated using model.mustache
+package {{modelPackage}}
+{{#imports}}import {{import}}
+{{/imports}}
+import scala.util.control.NonFatal
+
+// see https://com-lihaoyi.github.io/upickle/
+import upickle.default.{ReadWriter => RW, macroRW}
+import upickle.default.*
+
+{{#models}}
+{{#model}}
+case class {{classname}}(
+ {{#vars}}
+ {{#description}}
+/* {{{description}}} */
+ {{/description}}
+ {{name}}: {{#isEnum}}{{^required}}Option[{{/required}}{{classname}}.{{datatypeWithEnum}}{{^required}}]{{/required}}{{/isEnum}}{{^isEnum}}{{{vendorExtensions.x-datatype-model}}}{{/isEnum}}{{^required}} = {{{vendorExtensions.x-defaultValue-model}}} {{/required}}{{^-last}},{{/-last}}
+
+ {{/vars}}) {
+
+ def asJson: String = asData.asJson
+
+ def asData : {{classname}}Data = {
+ {{classname}}Data(
+ {{#vars}}
+ {{name}} = {{name}}{{#vendorExtensions.x-map-asModel}}.map(_.asData){{/vendorExtensions.x-map-asModel}}{{#vendorExtensions.x-wrap-in-optional}}.getOrElse({{{defaultValue}}}){{/vendorExtensions.x-wrap-in-optional}}{{^-last}},{{/-last}}
+ {{/vars}}
+ )
+ }
+
+}
+
+object {{classname}}{
+
+ given RW[{{classname}}] = {{classname}}Data.readWriter.bimap[{{classname}}](_.asData, _.asModel)
+
+ enum Fields(fieldName : String) extends Field(fieldName) {
+ {{#vars}}
+ case {{name}} extends Fields("{{name}}")
+ {{/vars}}
+ }
+
+{{#vars}}
+ {{#isEnum}}
+ // baseName={{{baseName}}}
+ // nameInCamelCase = {{{nameInCamelCase}}}
+ enum {{datatypeWithEnum}} derives ReadWriter {
+ {{#_enum}}
+ case {{.}}
+ {{/_enum}}
+ }
+ {{/isEnum}}
+{{/vars}}
+
+}
+
+{{/model}}
+{{/models}}
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/modelData.mustache b/modules/openapi-generator/src/main/resources/scala-cask/modelData.mustache
new file mode 100644
index 00000000000..8dfdef358f6
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/modelData.mustache
@@ -0,0 +1,250 @@
+{{>licenseInfo}}
+// this model was generated using modelData.mustache
+package {{modelPackage}}
+{{#imports}}import {{import}}
+{{/imports}}
+import scala.util.control.NonFatal
+import scala.util.*
+
+// see https://com-lihaoyi.github.io/upickle/
+import upickle.default.{ReadWriter => RW, macroRW}
+import upickle.default.*
+
+{{#models}}
+{{#model}}
+/** {{classname}}Data a data transfer object, primarily for simple json serialisation.
+ * It has no validation - there may be nulls, values out of range, etc
+ */
+case class {{classname}}Data(
+ {{#vars}}
+ {{#description}}
+/* {{{description}}} */
+ {{/description}}
+ {{name}}: {{#isEnum}}{{classname}}.{{datatypeWithEnum}}{{/isEnum}}{{^isEnum}}{{{vendorExtensions.x-datatype-data}}}{{/isEnum}}{{^required}} = {{{vendorExtensions.x-defaultValue-data}}} {{/required}}{{^-last}},{{/-last}}
+
+ {{/vars}}) {
+
+ def asJson: String = write(this)
+
+ def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = {
+ val errors = scala.collection.mutable.ListBuffer[ValidationError]()
+ {{#vars}}
+ // ==================
+ // {{name}}
+ {{#pattern}}
+ // validate against pattern '{{{pattern}}}'
+ if (errors.isEmpty || !failFast) {
+ val regex = """{{{pattern}}}"""
+ if {{name}} == null || !regex.r.matches({{name}}) then
+ errors += ValidationError(path :+ {{classname}}.Fields.{{name}}, s"value '${{name}}' doesn't match pattern $regex")
+ }
+ {{/pattern}}
+
+ {{#minimum}}
+ // validate against {{#exclusiveMinimum}}exclusive {{/exclusiveMinimum}}minimum {{minimum}}
+ if (errors.isEmpty || !failFast) {
+ if !({{name}} >{{^exclusiveMinimum}}={{/exclusiveMinimum}} {{minimum}}) then
+ errors += ValidationError(path :+ {{classname}}.Fields.{{name}}, s"value '${{name}}' is not greater than the {{#exclusiveMinimum}}exclusive {{/exclusiveMinimum}}minimum value {{minimum}}")
+ }
+ {{/minimum}}
+
+ {{#maximum}}
+ // validate against {{#exclusiveMaximum}}exclusive {{/exclusiveMaximum}}maximum {{maximum}}
+ if (errors.isEmpty || !failFast) {
+ if !({{name}} <{{^exclusiveMaximum}}={{/exclusiveMaximum}} {{maximum}}) then
+ errors += ValidationError(path :+ {{classname}}.Fields.{{name}}, s"value '${{name}}' is not greater than the {{#exclusiveMaximum}}exclusive {{/exclusiveMaximum}}maximum value {{maximum}}")
+ }
+ {{/maximum}}
+
+ {{#minLength}}
+ // validate min length {{minLength}}
+ if (errors.isEmpty || !failFast) {
+ val len = if {{name}} == null then 0 else {{name}}.length
+ if (len < {{minLength}}) {
+ errors += ValidationError(path :+ {{classname}}.Fields.{{name}}, s"length $len is shorter than the min length {{minLength}}")
+ }
+ }
+ {{/minLength}}
+
+ {{#maxLength}}
+ // validate max length {{maxLength}}
+ if (errors.isEmpty || !failFast) {
+ val len = if {{name}} == null then 0 else {{name}}.length
+ if (len < {{maxLength}}) {
+ errors += ValidationError(path :+ {{classname}}.Fields.{{name}}, s"length $len is longer than the max length {{maxLength}}")
+ }
+ }
+ {{/maxLength}}
+
+ {{#isEmail}}
+ // validate {{name}} is a valid email address
+ if (errors.isEmpty || !failFast) {
+ val emailRegex = """^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"""
+ // validate {{name}} is email
+ if ({{name}} == null || !emailRegex.r.matches({{name}})) {
+ errors += ValidationError(path :+ {{classname}}.Fields.{{name}}, s"${{name}} is not a valid email address according to the pattern $emailRegex")
+ }
+ }
+ {{/isEmail}}
+
+ {{#required}}{{^isPrimitiveType}}
+ if (errors.isEmpty || !failFast) {
+ if ({{name}} == null) {
+ errors += ValidationError(path :+ {{classname}}.Fields.{{name}}, "{{name}} is a required field and cannot be null")
+ }
+ }
+ {{/isPrimitiveType}}{{/required}}
+
+ {{#uniqueItems}}
+ // validate {{name}} has unique items
+ if (errors.isEmpty || !failFast) {
+ if ({{name}} != null) {
+ {{name}}.foldLeft(Set[{{{vendorExtensions.x-containertype-data}}}]()) {
+ case (set, next) if set.contains(next) =>
+ errors += ValidationError(
+ path :+ {{classname}}.Fields.{{name}},
+ s"duplicate value: $next"
+ )
+ set + next
+ case (set, next) => set + next
+ }
+ }
+ }
+ {{/uniqueItems}}
+
+ {{#multipleOf}}
+ if (errors.isEmpty || !failFast) {
+ // validate {{name}} multiple of {{multipleOf}}
+ if ({{name}} % {{multipleOf}} != 0) {
+ errors += ValidationError(
+ path :+ {{classname}}.Fields.{{name}},
+ s"${{name}} is not a multiple of {{multipleOf}}"
+ )
+ }
+ }
+ {{/multipleOf}}
+
+ {{#minItems}}
+ // validate min items {{minItems}}
+ if (errors.isEmpty || !failFast) {
+ val len = if {{name}} == null then 0 else {{name}}.size
+ if (len < {{minItems}}) {
+ errors += ValidationError(path :+ {{classname}}.Fields.{{name}}, s"{{name}} has $len, which is less than the min items {{minItems}}")
+ }
+ }
+ {{/minItems}}
+
+ {{#maxItems}}
+ // validate min items {{maxItems}}
+ if (errors.isEmpty || !failFast) {
+ val len = if {{name}} == null then 0 else {{name}}.size
+ if (len > {{maxItems}}) {
+ errors += ValidationError(path :+ {{classname}}.Fields.{{name}}, s"{{name}} has $len, which is greater than the max items {{maxItems}}")
+ }
+ }
+ {{/maxItems}}
+
+ {{#minProperties}}
+ TODO - minProperties
+ {{/minProperties}}
+
+ {{#maxProperties}}
+ TODO - maxProperties
+ {{/maxProperties}}
+
+ {{#items}}{{#isModel}}
+ if (errors.isEmpty || !failFast) {
+ if ({{name}} != null) {
+ {{name}}.zipWithIndex.foreach {
+ case (value, i) if errors.isEmpty || !failFast =>
+ errors ++= value.validationErrors(
+ path :+ {{classname}}.Fields.{{name}} :+ Field(i.toString),
+ failFast)
+ case (value, i) =>
+ }
+ }
+ }
+ {{/isModel}}{{/items}}
+ {{#isModel}}
+ // validating {{name}}
+ if (errors.isEmpty || !failFast) {
+ if {{name}} != null then errors ++= {{name}}.validationErrors(path :+ {{classname}}.Fields.{{name}}, failFast)
+ }
+ {{/isModel}}
+
+ {{/vars}}
+ errors.toSeq
+ }
+
+ def validated(failFast : Boolean = false) : scala.util.Try[{{classname}}] = {
+ validationErrors(Vector(), failFast) match {
+ case Seq() => Success(asModel)
+ case first +: theRest => Failure(ValidationErrors(first, theRest))
+ }
+ }
+
+ /** use 'validated' to check validation */
+ def asModel : {{classname}} = {
+ {{classname}}(
+ {{#vars}}
+ {{name}} = {{#vendorExtensions.x-wrap-in-optional}}Option({{/vendorExtensions.x-wrap-in-optional}}
+ {{name}}
+ {{#vendorExtensions.x-wrap-in-optional}}){{/vendorExtensions.x-wrap-in-optional}}
+ {{#vendorExtensions.x-map-asModel}}.map(_.asModel){{/vendorExtensions.x-map-asModel}}{{^-last}},{{/-last}}
+ {{/vars}}
+ )
+ }
+}
+
+object {{classname}}Data {
+
+ given readWriter : RW[{{classname}}Data] = macroRW
+
+ def fromJsonString(jason : String) : {{classname}}Data = try {
+ read[{{classname}}Data](jason)
+ } catch {
+ case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e")
+ }
+
+ def manyFromJsonString(jason : String) : Seq[{{classname}}Data] = try {
+ read[List[{{classname}}Data]](jason)
+ } catch {
+ case NonFatal(e) => sys.error(s"Error parsing json '$jason' as list: $e")
+ }
+
+ def manyFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Seq[{{classname}}]] = {
+ Try(manyFromJsonString(jason)).flatMap { list =>
+ list.zipWithIndex.foldLeft(Try(Vector[{{classname}}]())) {
+ case (Success(list), (next, i)) =>
+ next.validated(failFast) match {
+ case Success(ok) => Success(list :+ ok)
+ case Failure(err) => Failure(new Exception(s"Validation error on element $i: ${err.getMessage}", err))
+ }
+ case (fail, _) => fail
+ }
+ }
+ }
+
+ def mapFromJsonString(jason : String) : Map[String, {{classname}}Data] = try {
+ read[Map[String, {{classname}}Data]](jason)
+ } catch {
+ case NonFatal(e) => sys.error(s"Error parsing json '$jason' as map: $e")
+ }
+
+
+ def mapFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Map[String, {{classname}}]] = {
+ Try(mapFromJsonString(jason)).flatMap { map =>
+ map.foldLeft(Try(Map[String, {{classname}}]())) {
+ case (Success(map), (key, next)) =>
+ next.validated(failFast) match {
+ case Success(ok) => Success(map.updated(key, ok))
+ case Failure(err) => Failure(new Exception(s"Validation error on element $key: ${err.getMessage}", err))
+ }
+ case (fail, _) => fail
+ }
+ }
+ }
+}
+
+{{/model}}
+{{/models}}
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/modelPackage.mustache b/modules/openapi-generator/src/main/resources/scala-cask/modelPackage.mustache
new file mode 100644
index 00000000000..f582c3e9fb1
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/modelPackage.mustache
@@ -0,0 +1,53 @@
+{{>licenseInfo}}
+package {{modelPackage}}
+
+// model package
+import upickle.default._
+import java.time.*
+import java.time.format.DateTimeFormatter
+
+/**
+ * This base class lets us refer to fields in exceptions
+ */
+class Field(val name : String)
+
+final case class ValidationErrors(
+ first: ValidationError,
+ remaining: Seq[ValidationError],
+ message: String
+) extends Exception(message)
+
+object ValidationErrors {
+ def apply(first: ValidationError, remaining: Seq[ValidationError]) = {
+ val noun = if remaining.isEmpty then "error" else "errors"
+ new ValidationErrors(
+ first,
+ remaining,
+ remaining.mkString(s"${remaining.size + 1} $noun found: ${first}", "\n\t", "")
+ )
+ }
+}
+
+
+final case class ValidationError(path : Seq[Field], message : String) extends Exception(message) {
+ override def toString = s"ValidationError for ${path.mkString(".")}: $message"
+}
+
+given ReadWriter[ZonedDateTime] = readwriter[String].bimap[ZonedDateTime](
+ zonedDateTime => DateTimeFormatter.ISO_INSTANT.format(zonedDateTime),
+ str => ZonedDateTime.parse(str, DateTimeFormatter.ISO_INSTANT))
+
+given ReadWriter[LocalDateTime] = readwriter[String].bimap[LocalDateTime](
+ zonedDateTime => DateTimeFormatter.ISO_INSTANT.format(zonedDateTime),
+ str => LocalDateTime.parse(str, DateTimeFormatter.ISO_INSTANT))
+
+given ReadWriter[LocalDate] = readwriter[String].bimap[LocalDate](
+ zonedDateTime => DateTimeFormatter.ISO_INSTANT.format(zonedDateTime),
+ str => LocalDate.parse(str, DateTimeFormatter.ISO_INSTANT))
+
+given ReadWriter[OffsetDateTime] = readwriter[String].bimap[OffsetDateTime](
+ zonedDateTime => DateTimeFormatter.ISO_INSTANT.format(zonedDateTime),
+ str => scala.util.Try(OffsetDateTime.parse(str, DateTimeFormatter.ISO_DATE_TIME)).getOrElse(
+ OffsetDateTime.parse(str, DateTimeFormatter.ISO_INSTANT)
+ )
+)
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/modelTest.mustache b/modules/openapi-generator/src/main/resources/scala-cask/modelTest.mustache
new file mode 100644
index 00000000000..cbeae14cecc
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/modelTest.mustache
@@ -0,0 +1,37 @@
+{{>licenseInfo}}
+// this model was generated using modelTest.mustache
+package {{modelPackage}}
+{{#imports}}import {{import}}
+{{/imports}}
+
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+import scala.util.*
+
+{{#models}}
+{{#model}}
+class {{classname}}Test extends AnyWordSpec with Matchers {
+
+{{#operations}}
+ // operation {{classname}}
+ //
+ {{#examples}}
+ key = {{key}}
+ value= {{value}}
+ {{/examples}}
+{{/operations}}
+ "{{classname}}.fromJson" should {
+ """not parse invalid json""" in {
+ val Failure(err) = Try({{classname}}Data.fromJsonString("invalid jason"))
+ err.getMessage should startWith ("Error parsing json 'invalid jason'")
+ }
+ """parse {{example}}""" ignore {
+ val Failure(err : ValidationErrors) = {{classname}}Data.fromJsonString("""{{example}}""").validated()
+
+ sys.error("TODO")
+ }
+ }
+
+}
+{{/model}}
+{{/models}}
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/openapiRoute.mustache b/modules/openapi-generator/src/main/resources/scala-cask/openapiRoute.mustache
new file mode 100644
index 00000000000..58c35063555
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/openapiRoute.mustache
@@ -0,0 +1,116 @@
+{{>licenseInfo}}
+
+// generated from openapiRoute.mustache
+package {{apiPackage}}
+
+import cask.model.Response
+
+import java.nio.file.{Files, Path, Paths}
+
+/**
+ * This code will try and download the swagger UI static files on startup
+ *
+ * That behaviour can be altered by:
+ * - setting the environment variable SWAGGER_ON to false
+ * - setting the environment variable SWAGGER_UI_URL to either the URL of a swagger UI zip or setting it to the empty string
+ *
+ */
+object OpenApiRoutes {
+
+ def swaggerUIUrl: Option[String] = {
+ // flag to turn SWAGGER off
+ def useSwaggerUI = sys.env.get("SWAGGER_ON").map(_.toBoolean).getOrElse(true)
+
+ val defaultUrl = "https://github.com/swagger-api/swagger-ui/archive/refs/tags/v5.11.9.zip"
+ Option(sys.env.getOrElse("SWAGGER_UI_URL", defaultUrl))
+ .map(_.trim)
+ .filterNot(_.isEmpty)
+ .filter(_ => useSwaggerUI)
+ }
+ def apply(localPort: Int) = new OpenApiRoutes(localPort, swaggerUIUrl)
+}
+
+class OpenApiRoutes(localPort: Int, swaggerUrl: Option[String]) extends cask.Routes {
+
+ def openApiDir = "ui"
+
+ @cask.get("/")
+ def index() = cask.Redirect("/ui/index.html")
+
+ @cask.staticFiles("/ui")
+ def staticUI() = openApiDir
+
+ @cask.staticResources("/openapi.json")
+ def staticOpenApi() = "openapi.json"
+
+ /** This code will try and download the swagger UI artefacts to a local directory to serve up
+ */
+ object extract {
+
+ def openApiDirPath: Path = Paths.get(openApiDir)
+ def hasSwagger = Files.exists(openApiDirPath) && Files.isDirectory(openApiDirPath)
+
+ import java.io.{BufferedInputStream, FileOutputStream, InputStream}
+ import java.net.URL
+ import java.util.zip.{ZipEntry, ZipInputStream}
+ import scala.util.Using
+
+ def apply(url: String) = {
+ if !hasSwagger then downloadAndExtractZip(url, openApiDir)
+ }
+
+ def downloadAndExtractZip(url: String, outputDir: String): Unit = {
+ val urlConn = new URL(url).openConnection()
+ urlConn.setRequestProperty("User-Agent", "Mozilla/5.0")
+
+ Using(urlConn.getInputStream) { inputStream =>
+ val zipIn = new ZipInputStream(new BufferedInputStream(inputStream))
+ LazyList.continually(zipIn.getNextEntry).takeWhile(_ != null).foreach { entry =>
+
+ def isDist = entry.getName.contains("/dist/")
+ def isNotMap = !entry.getName.endsWith(".map")
+
+ if (!entry.isDirectory && isDist && isNotMap) {
+ val fileName = entry.getName.split("/").last
+ extractFile(entry.getName, zipIn, s"$outputDir/$fileName")
+ }
+ zipIn.closeEntry()
+ }
+ }
+ }
+
+ def extractFile(name: String, zipIn: ZipInputStream, filePath: String): Unit = {
+ val fullPath = Paths.get(filePath).toAbsolutePath
+ if !Files.exists(fullPath.getParent) then {
+ Files.createDirectories(fullPath.getParent)
+ }
+
+ // config hack - we replace the default url from this swagger conf to use our localhost
+ //
+ if name.endsWith("swagger-initializer.js") then {
+ val textLines = scala.io.Source.fromInputStream(zipIn).getLines().map {
+ case line if line.contains("url:") =>
+ s""" url: "http://localhost:$localPort/openapi.json","""
+ case line => line
+ }
+
+ // keeping this compatible for java 8, where this is from >= java 11:
+ // Files.writeString(fullPath, textLines.mkString("\n"))
+ scala.util.Using(new java.io.PrintWriter(fullPath.toFile))(_.write(textLines.mkString("\n")))
+ } else {
+ Using(new FileOutputStream(filePath)) { outputStream =>
+ val buffer = new Array[Byte](1024)
+ LazyList
+ .continually(zipIn.read(buffer))
+ .takeWhile(_ != -1)
+ .foreach(outputStream.write(buffer, 0, _))
+ }
+ }
+ }
+ }
+
+ // extract the swagger UI resources to our local directory
+ swaggerUrl.foreach(url => extract(url))
+
+ initialize()
+}
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/parseHttpParams.mustache b/modules/openapi-generator/src/main/resources/scala-cask/parseHttpParams.mustache
new file mode 100644
index 00000000000..18193e0606c
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/parseHttpParams.mustache
@@ -0,0 +1,56 @@
+ for {
+{{#pathParams}}
+ {{#isString}}
+ {{paramName}} <- Parsed({{paramName}})
+ {{/isString}}
+ {{^isString}}
+ {{paramName}} <- Parsed({{paramName}})
+ {{/isString}}
+{{/pathParams}}
+{{#headerParams}}
+ {{#required}}
+ {{paramName}} <- request.headerSingleValueRequired("{{paramName}}")
+ {{/required}}
+ {{^required}}
+ {{paramName}} <- request.headerSingleValueOptional("{{paramName}}")
+ {{/required}}
+{{/headerParams}}
+{{#formParams}}
+ {{#required}}
+ {{paramName}} <- {{#isFile}}request.formValueAsFileRequired("{{paramName}}"){{/isFile}}{{^isFile}}request.formSingleValueRequired("{{paramName}}"){{/isFile}}
+ {{/required}}
+ {{^required}}
+ {{paramName}} <- {{#isFile}}request.formValueAsFileOptional("{{paramName}}"){{/isFile}}{{^isFile}}request.formSingleValueOptional("{{paramName}}"){{/isFile}}
+ {{/required}}
+{{/formParams}}
+{{#bodyParams}}
+ {{#vendorExtensions.x-consumes-json}}
+ {{#isArray}}
+ {{paramName}} <- Parsed.fromTry({{vendorExtensions.x-container-type}}Data.manyFromJsonStringValidated(request.bodyAsString)).mapError(e => s"Error parsing json as an array of {{vendorExtensions.x-container-type}} from >${request.bodyAsString}< : ${e}") /* array */
+ {{/isArray}}
+ {{^isArray}}
+ {{#isMap}}
+ {{#vendorExtensions.x-deserialize-asModelMap}}
+ {{paramName}} <- Parsed.eval(read[Map[String, {{vendorExtensions.x-container-type}}](request.bodyAsString)).mapError(e => s"Error parsing json as a string map of {{vendorExtensions.x-container-type}} from >${request.bodyAsString}< : ${e}") /* x-deserialize-asModelMap */
+ {{/vendorExtensions.x-deserialize-asModelMap}}
+ {{^vendorExtensions.x-deserialize-asModelMap}}
+ {{paramName}} <- Parsed.eval(read[Map[String, {{vendorExtensions.x-container-type}}]](request.bodyAsString)).mapError(e => s"Error parsing json as a string map of {{vendorExtensions.x-container-type}} from >${request.bodyAsString}< : ${e}") /* not x-deserialize-asModelMap */
+ {{/vendorExtensions.x-deserialize-asModelMap}}
+ {{/isMap}}
+ {{^isMap}}
+ {{paramName}}Data <- Parsed.eval({{vendorExtensions.x-container-type}}Data.fromJsonString(request.bodyAsString)).mapError(e => s"Error parsing json as {{vendorExtensions.x-container-type}} from >${request.bodyAsString}< : ${e}") /* not array or map */
+ {{paramName}} <- Parsed.fromTry({{paramName}}Data.validated(failFast))
+ {{/isMap}}
+ {{/isArray}}
+ {{/vendorExtensions.x-consumes-json}}
+ {{^vendorExtensions.x-consumes-json}}
+ {{#vendorExtensions.x-consumes-xml}}
+ {{paramName}} <- Parsed.fail("TODO - xml deserialisation not yet supported. see src/main/resources/scala-cask/parseHttpParams.mustache in https://github.com/OpenAPITools/openapi-generator")
+ {{/vendorExtensions.x-consumes-xml}}
+ {{^vendorExtensions.x-consumes-xml}}
+ {{paramName}} <- Parsed.fail("TODO - content deserialisation. see src/main/resources/scala-cask/parseHttpParams.mustache in https://github.com/OpenAPITools/openapi-generator")
+ {{/vendorExtensions.x-consumes-xml}}
+ {{/vendorExtensions.x-consumes-json}}
+{{/bodyParams}}
+ result <- Parsed.eval(service.{{operationId}}({{vendorExtensions.x-param-list}}))
+ } yield result
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/pathExtractor.mustache b/modules/openapi-generator/src/main/resources/scala-cask/pathExtractor.mustache
new file mode 100644
index 00000000000..9890a8865a2
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/pathExtractor.mustache
@@ -0,0 +1 @@
+{{#vendorExtensions.x-path-remaining}}{{#isParam}}{{name}}{{/isParam}}{{^isParam}}"{{name}}"{{/isParam}}{{^-last}},{{/-last}}{{/vendorExtensions.x-path-remaining}}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/pathExtractorParams.mustache b/modules/openapi-generator/src/main/resources/scala-cask/pathExtractorParams.mustache
new file mode 100644
index 00000000000..89a51b83b40
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/pathExtractorParams.mustache
@@ -0,0 +1 @@
+{{#vendorExtensions.x-path-remaining}}{{#isParam}}{{name}}{{conversion}}{{/isParam}}{{#hasMoreParams}}, {{/hasMoreParams}}{{/vendorExtensions.x-path-remaining}}{{#vendorExtensions.x-has-path-remaining}},{{/vendorExtensions.x-has-path-remaining}}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/project/build.properties b/modules/openapi-generator/src/main/resources/scala-cask/project/build.properties
new file mode 100644
index 00000000000..04267b14af6
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/project/build.properties
@@ -0,0 +1 @@
+sbt.version=1.9.9
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/project/plugins.sbt b/modules/openapi-generator/src/main/resources/scala-cask/project/plugins.sbt
new file mode 100644
index 00000000000..4f3f02c2de4
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/project/plugins.sbt
@@ -0,0 +1,3 @@
+addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6")
+
+addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6")
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/scala-cask/queryParams.mustache b/modules/openapi-generator/src/main/resources/scala-cask/queryParams.mustache
new file mode 100644
index 00000000000..fc9a800ed2b
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-cask/queryParams.mustache
@@ -0,0 +1 @@
+{{#hasQueryParams}},{{/hasQueryParams}}{{#queryParams}}{{paramName}}{{#required}}{{^isMap}}{{^isArray}}.getOrElse({{#isString}}""{{/isString}}{{#isNumber}}0{{/isNumber}}{{#isBoolean}}false{{/isBoolean}}){{/isArray}}{{/isMap}}{{/required}}{{^-last}}, {{/-last}}{{/queryParams}}
\ No newline at end of file
diff --git a/samples/server/petstore/scala-cask/.github/workflows/bulidAndPublish.yml b/samples/server/petstore/scala-cask/.github/workflows/bulidAndPublish.yml
new file mode 100644
index 00000000000..fef794246b0
--- /dev/null
+++ b/samples/server/petstore/scala-cask/.github/workflows/bulidAndPublish.yml
@@ -0,0 +1,41 @@
+name: Scala CI with sbt
+
+on:
+ push:
+ branches: [ main, master ]
+ pull_request:
+ branches: [ main, master ]
+
+jobs:
+ build-and-publish:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Set up JDK 11
+ uses: actions/setup-java@v1
+ with:
+ java-version: 11
+
+ - name: Cache sbt dependencies
+ uses: actions/cache@v2
+ with:
+ path: |
+ ~/.ivy2/cache
+ ~/.sbt
+ ~/.m2
+ key: ${{ runner.os }}-sbt-${{ hashFiles('**/*.sbt') }}
+ restore-keys: |
+ ${{ runner.os }}-sbt-
+
+ - name: Build with sbt
+ run: sbt clean compile
+
+ - name: Test with sbt
+ run: sbt test
+
+ - name: Publish to GitHub Packages
+ run: sbt publish
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/samples/server/petstore/scala-cask/.gitignore b/samples/server/petstore/scala-cask/.gitignore
new file mode 100644
index 00000000000..56f2b272a08
--- /dev/null
+++ b/samples/server/petstore/scala-cask/.gitignore
@@ -0,0 +1,25 @@
+# scala specific
+*.class
+*.log
+
+# sbt specific
+.cache
+.history
+.lib/
+dist/*
+target/
+lib_managed/
+src_managed/
+project/boot/
+project/plugins/project/
+
+# Scala-IDE specific
+.scala_dependencies
+.worksheet
+
+# Mill specific
+out
+
+# IntelliJ specific
+.idea
+*.iml
diff --git a/samples/server/petstore/scala-cask/.openapi-generator-ignore b/samples/server/petstore/scala-cask/.openapi-generator-ignore
new file mode 100644
index 00000000000..7484ee590a3
--- /dev/null
+++ b/samples/server/petstore/scala-cask/.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-cask/.openapi-generator/FILES b/samples/server/petstore/scala-cask/.openapi-generator/FILES
new file mode 100644
index 00000000000..e65ebab478f
--- /dev/null
+++ b/samples/server/petstore/scala-cask/.openapi-generator/FILES
@@ -0,0 +1,39 @@
+.github/workflows/bulidAndPublish.yml
+.gitignore
+.scalafmt.conf
+README.md
+README.md
+build.sbt
+build.sc
+example/Dockerfile
+example/Server.scala
+project/build.properties
+project/plugins.sbt
+src/main/scala/sample/cask/AppRoutes.scala
+src/main/scala/sample/cask/BaseApp.scala
+src/main/scala/sample/cask/ExampleApp.scala
+src/main/scala/sample/cask/api/OpenApiRoutes.scala
+src/main/scala/sample/cask/api/PetRoutes.scala
+src/main/scala/sample/cask/api/PetRoutes.scala
+src/main/scala/sample/cask/api/PetService.scala
+src/main/scala/sample/cask/api/StoreRoutes.scala
+src/main/scala/sample/cask/api/StoreRoutes.scala
+src/main/scala/sample/cask/api/StoreService.scala
+src/main/scala/sample/cask/api/UserRoutes.scala
+src/main/scala/sample/cask/api/UserRoutes.scala
+src/main/scala/sample/cask/api/UserService.scala
+src/main/scala/sample/cask/api/package.scala
+src/main/scala/sample/cask/model/ApiResponse.scala
+src/main/scala/sample/cask/model/ApiResponseData.scala
+src/main/scala/sample/cask/model/Category.scala
+src/main/scala/sample/cask/model/CategoryData.scala
+src/main/scala/sample/cask/model/Order.scala
+src/main/scala/sample/cask/model/OrderData.scala
+src/main/scala/sample/cask/model/Pet.scala
+src/main/scala/sample/cask/model/PetData.scala
+src/main/scala/sample/cask/model/Tag.scala
+src/main/scala/sample/cask/model/TagData.scala
+src/main/scala/sample/cask/model/User.scala
+src/main/scala/sample/cask/model/UserData.scala
+src/main/scala/sample/cask/model/package.scala
+src/main/scala/sample/cask/package.scala
diff --git a/samples/server/petstore/scala-cask/.openapi-generator/VERSION b/samples/server/petstore/scala-cask/.openapi-generator/VERSION
new file mode 100644
index 00000000000..08bfd0643b8
--- /dev/null
+++ b/samples/server/petstore/scala-cask/.openapi-generator/VERSION
@@ -0,0 +1 @@
+7.5.0-SNAPSHOT
diff --git a/samples/server/petstore/scala-cask/.scalafmt.conf b/samples/server/petstore/scala-cask/.scalafmt.conf
new file mode 100644
index 00000000000..e150d4c409d
--- /dev/null
+++ b/samples/server/petstore/scala-cask/.scalafmt.conf
@@ -0,0 +1,4 @@
+version = 3.6.1
+align.preset = more // For pretty alignment.
+maxColumn = 100
+runner.dialect = scala3
\ No newline at end of file
diff --git a/samples/server/petstore/scala-cask/README.md b/samples/server/petstore/scala-cask/README.md
new file mode 100644
index 00000000000..39e65480193
--- /dev/null
+++ b/samples/server/petstore/scala-cask/README.md
@@ -0,0 +1,96 @@
+# REST Service
+
+This project contains the data models and REST services, generated from the [openapi-generator](https://github.com/OpenAPITools/openapi-generator) project.
+
+The server implementation is based on Li's excellent [cask](https://com-lihaoyi.github.io/cask/) library.
+
+# How to use this code
+
+This code was designed so that it can be packaged up and semantically-versioned alongside your open-api schema.
+
+That approach supports having separate "contract" repositories for microservice projects, where the pipeline for the
+contract repo might produce versioned jar artefacts which can then easily be brought in / referenced by a separate service project which simply
+implements the business logic.
+
+You can read more about this approach [here](https://github.com/kindservices/contract-first-rest)
+
+# How to implement your business logic
+There are a few options for using this code/applying your business logic for the services.
+
+## Option 1 (preferred): Package and publish this boilerplate
+Typically, OpenApi templates are written to generate code which co-exists alongside the handwritten business logic.
+
+While that works, it's also not ideal:
+ * You have to ensure the generated code isn't checked in
+ * Team members, build pipelines, etc all have to regenerate and recompile the same boilerplate code over and over
+ * People can encounter IDE issues with generated code
+
+Instead, you have the option of simply packaging/publishing this generated code, and then allowing service implementations
+to simply bring in the published code as a dependency.
+
+The steps to do that are:
+
+### Build/Publish
+This project is built using [sbt](https://www.scala-sbt.org/download/), so you can run `sbt publish` (or `sbt publishLocal`)
+
+Or, for a zero-install docker build:
+```bash
+docker run -it --rm -v $(pwd):/app -w /app sbtscala/scala-sbt:eclipse-temurin-17.0.4_1.7.1_3.2.0 sbt publishLocal
+```
+
+### Create a new separate implementation project
+Once published, you can create your server implementation in a new, clean, separate project based on [the example](./example)
+
+This means all the boilerplate endpoint and model code is brought in as "just another jar", and you're free to
+create a greenfield project in whatever language (scala, java, kotlin) and build system of your choosing.
+
+We show a simple, minimalistic example of a starting point in [the example project](./example)
+
+## Option 2: Extend this generated example
+You can configure this project (for instance, setting up your own .gitignore rules and scripts) to leave the generated code as-is
+and provide your implementation alongside the generated code.
+
+The place to start is by providing your own implementation of the Services defined in the `api` package -
+perhaps by creating your 'MyService.scala' code in a new `impl` package.
+
+You then have several options for how to wire those in:
+
+1) Create a new BaseApp instance to create your own Main entry point
+Follow the pattern in App.scala, but by passing your own implementations to BaseApp,
+ensuring you call `start` to start the server
+
+```bash
+@main def run() = BaseApp(/* your services here/*).start()
+```
+
+2) Extend either BaseApp class or mix in the AppRoutes trait
+You can create your own main entry point with further control of the main cask app by extending
+the BaseApp or otherwise creating your own CaskApp which mixes in the AppRoutes
+
+```bash
+object MyApp extends BaseApp(/* your services here/*) {
+ // any overrides, new routes, etc here
+ start()
+}
+```
+
+
+# Customising the generated code
+
+A typical config.yml used to alter the generated code may look like this:
+```
+groupId: "ex.amp.le"
+artifactId: "pets-test"
+apiPackage: "ex.ample.api"
+modelPackage: "ex.ample.model"
+```
+
+Which you would then pass to the generator like this:
+```
+docker run --rm \
+-v ${PWD}:/local openapitools/openapi-generator-cli generate \
+-i https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3.0/petstore.yaml \
+-g scala-cask \
+-c /local/config.yml \
+-o /local/path/to/output_dir
+```
diff --git a/samples/server/petstore/scala-cask/build.sbt b/samples/server/petstore/scala-cask/build.sbt
new file mode 100644
index 00000000000..4062a2fa45a
--- /dev/null
+++ b/samples/server/petstore/scala-cask/build.sbt
@@ -0,0 +1,29 @@
+name := "scala-cask-petstore"
+organization:="cask.groupId"
+version := "0.0.1-SNAPSHOT"
+scalaVersion := "3.3.1"
+scalafmtOnCompile := true
+libraryDependencies ++= Seq(
+ "com.lihaoyi" %% "cask" % "0.9.2" ,
+ "com.lihaoyi" %% "upickle" % "3.2.0",
+ "org.scalatest" %% "scalatest" % "3.2.18" % Test
+)
+
+publishMavenStyle := true
+
+val githubUser = "GIT_USER_ID"
+val githubRepo = "GIT_REPO_ID"
+publishTo := Some("GitHub Package Registry" at s"https://maven.pkg.github.com/$githubUser/$githubRepo")
+
+sys.env.get("GITHUB_TOKEN") match {
+ case Some(token) if !token.isEmpty =>
+ credentials += Credentials(
+ "GitHub Package Registry",
+ "maven.pkg.github.com",
+ githubUser,
+ token
+ )
+ case _ =>
+ println("\n\t\tGITHUB_TOKEN not set - assuming a local build\n\n")
+ credentials ++= Nil
+}
\ No newline at end of file
diff --git a/samples/server/petstore/scala-cask/build.sc b/samples/server/petstore/scala-cask/build.sc
new file mode 100644
index 00000000000..b2ca2988987
--- /dev/null
+++ b/samples/server/petstore/scala-cask/build.sc
@@ -0,0 +1,43 @@
+import mill._, scalalib._, scalafmt._, publish._
+
+// Mill build file (see https://mill-build.com/mill/Intro_to_Mill.html).
+// run with:
+//
+// mill _.compile
+// mill _.reformat
+// mill _.publishLocal
+// mill _.test.test
+object scala-cask-petstore extends SbtModule with ScalafmtModule with PublishModule {
+ def scalaVersion = "3.3.1"
+
+ def pomSettings = PomSettings(
+ description = "scala-cask-petstore",
+ organization = "cask.groupId",
+ url = "https://github.com//scala-cask-petstore",
+ licenses = Seq(License.MIT),
+ versionControl = VersionControl.github("", "scala-cask-petstore"),
+ developers = Seq(
+ // Developer("", "", "https://github.com/")
+ )
+ )
+
+ def publishVersion: mill.T[String] = T("0.0.1-SNAPSHOT")
+
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::cask:0.9.2" ,
+ ivy"com.lihaoyi::upickle:3.2.0"
+ )
+
+ override def sources = T.sources(millSourcePath / os.up / "src" / "main" / "scala")
+ override def resources = T.sources(millSourcePath / os.up / "src" / "main" / "resources")
+
+ object test extends SbtModuleTests {
+ def ivyDeps = Agg(
+ ivy"org.scalactic::scalactic:3.2.18",
+ ivy"org.scalatest::scalatest:3.2.18"
+ )
+
+ def testFramework = "org.scalatest.tools.Framework"
+ override def sources = T.sources(millSourcePath / os.up / "src" / "test" / "scala")
+ }
+}
diff --git a/samples/server/petstore/scala-cask/example/Dockerfile b/samples/server/petstore/scala-cask/example/Dockerfile
new file mode 100644
index 00000000000..0c548753973
--- /dev/null
+++ b/samples/server/petstore/scala-cask/example/Dockerfile
@@ -0,0 +1,13 @@
+FROM virtuslab/scala-cli:latest as build
+WORKDIR /app
+COPY ./Server.scala /app/
+# note: this assumes a published server stub jar.
+# If you've published this locally, you would need to copy those into this image,
+# perhaps by using coursier fetch
+RUN scala-cli --power package /app/Server.scala --assembly -o app.jar
+
+# The main image
+FROM openjdk:23-slim
+WORKDIR /app
+COPY --from=build /app/app.jar /app/
+ENTRYPOINT ["java", "-jar", "/app/app.jar"]
diff --git a/samples/server/petstore/scala-cask/example/Server.scala b/samples/server/petstore/scala-cask/example/Server.scala
new file mode 100644
index 00000000000..77e65913712
--- /dev/null
+++ b/samples/server/petstore/scala-cask/example/Server.scala
@@ -0,0 +1,61 @@
+//> using scala "3.3.1"
+//> using lib "cask.groupId::scala-cask-petstore:0.0.1-SNAPSHOT"
+//> using repositories https://maven.pkg.github.com/GIT_USER_ID/GIT_REPO_ID
+
+
+/**
+* This single file can contain the business logic for a REST service.
+* ====================================
+* == zero-install build with docker ==
+* ====================================
+*
+*
+* ```
+* docker build . -t scala-cask-petstore:latest
+* ```
+* ======================
+* == Building Locally ==
+* ======================
+* This project can be built using [[scala-clit][https://scala-cli.virtuslab.org]]
+*
+* To simply run the project
+* ```
+* scala-cli Server.scala
+* ```
+*
+* To create a runnable jar, run:
+* ```
+* scala-cli --power package Server.scala -o app-assembly --assembly
+* ```
+*
+* To produce a docker image (no need for the Dockerfile), run:
+* ```
+* scala-cli --power package --docker Server.scala --docker-image-repository app-docker
+* ```
+*
+* To generate an IDE project:
+* ```
+* scala-cli setup-ide . --scala 3.3
+* ```
+*/
+package app
+
+import cask.groupId.server.BaseApp
+import sample.cask.api.*
+import sample.cask.model.*
+
+import java.io.File
+
+// TODO - write your business logic for your services here (the defaults all return 'not implemented'):
+val myPetService : PetService = PetService() // <-- replace this with your implementation
+val myStoreService : StoreService = StoreService() // <-- replace this with your implementation
+val myUserService : UserService = UserService() // <-- replace this with your implementation
+
+/** This is your main entry point for your REST service
+ * It extends BaseApp which defines the business logic for your services
+ */
+object Server extends BaseApp(appPetService = myPetService,
+appStoreService = myStoreService,
+appUserService = myUserService):
+ start()
+
diff --git a/samples/server/petstore/scala-cask/project/build.properties b/samples/server/petstore/scala-cask/project/build.properties
new file mode 100644
index 00000000000..04267b14af6
--- /dev/null
+++ b/samples/server/petstore/scala-cask/project/build.properties
@@ -0,0 +1 @@
+sbt.version=1.9.9
diff --git a/samples/server/petstore/scala-cask/project/plugins.sbt b/samples/server/petstore/scala-cask/project/plugins.sbt
new file mode 100644
index 00000000000..4f3f02c2de4
--- /dev/null
+++ b/samples/server/petstore/scala-cask/project/plugins.sbt
@@ -0,0 +1,3 @@
+addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6")
+
+addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6")
\ No newline at end of file
diff --git a/samples/server/petstore/scala-cask/src/main/resources/openapi.json b/samples/server/petstore/scala-cask/src/main/resources/openapi.json
new file mode 100644
index 00000000000..5c3f8dec9aa
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/main/resources/openapi.json
@@ -0,0 +1,1032 @@
+{
+ "openapi" : "3.0.0",
+ "info" : {
+ "description" : "This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.",
+ "license" : {
+ "name" : "Apache-2.0",
+ "url" : "https://www.apache.org/licenses/LICENSE-2.0.html"
+ },
+ "title" : "OpenAPI Petstore",
+ "version" : "1.0.0"
+ },
+ "externalDocs" : {
+ "description" : "Find out more about Swagger",
+ "url" : "http://swagger.io"
+ },
+ "servers" : [ {
+ "url" : "http://petstore.swagger.io/v2"
+ } ],
+ "tags" : [ {
+ "description" : "Everything about your Pets",
+ "name" : "pet"
+ }, {
+ "description" : "Access to Petstore orders",
+ "name" : "store"
+ }, {
+ "description" : "Operations about user",
+ "name" : "user"
+ } ],
+ "paths" : {
+ "/pet" : {
+ "post" : {
+ "description" : "",
+ "operationId" : "addPet",
+ "requestBody" : {
+ "$ref" : "#/components/requestBodies/Pet"
+ },
+ "responses" : {
+ "200" : {
+ "content" : {
+ "application/xml" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/Pet"
+ }
+ },
+ "application/json" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/Pet"
+ }
+ }
+ },
+ "description" : "successful operation"
+ },
+ "405" : {
+ "description" : "Invalid input"
+ }
+ },
+ "security" : [ {
+ "petstore_auth" : [ "write:pets", "read:pets" ]
+ } ],
+ "summary" : "Add a new pet to the store",
+ "tags" : [ "pet" ]
+ },
+ "put" : {
+ "description" : "",
+ "externalDocs" : {
+ "description" : "API documentation for the updatePet operation",
+ "url" : "http://petstore.swagger.io/v2/doc/updatePet"
+ },
+ "operationId" : "updatePet",
+ "requestBody" : {
+ "$ref" : "#/components/requestBodies/Pet"
+ },
+ "responses" : {
+ "200" : {
+ "content" : {
+ "application/xml" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/Pet"
+ }
+ },
+ "application/json" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/Pet"
+ }
+ }
+ },
+ "description" : "successful operation"
+ },
+ "400" : {
+ "description" : "Invalid ID supplied"
+ },
+ "404" : {
+ "description" : "Pet not found"
+ },
+ "405" : {
+ "description" : "Validation exception"
+ }
+ },
+ "security" : [ {
+ "petstore_auth" : [ "write:pets", "read:pets" ]
+ } ],
+ "summary" : "Update an existing pet",
+ "tags" : [ "pet" ]
+ }
+ },
+ "/pet/findByStatus" : {
+ "get" : {
+ "description" : "Multiple status values can be provided with comma separated strings",
+ "operationId" : "findPetsByStatus",
+ "parameters" : [ {
+ "deprecated" : true,
+ "description" : "Status values that need to be considered for filter",
+ "explode" : false,
+ "in" : "query",
+ "name" : "status",
+ "required" : true,
+ "schema" : {
+ "items" : {
+ "default" : "available",
+ "enum" : [ "available", "pending", "sold" ],
+ "type" : "string"
+ },
+ "type" : "array"
+ },
+ "style" : "form"
+ } ],
+ "responses" : {
+ "200" : {
+ "content" : {
+ "application/xml" : {
+ "schema" : {
+ "items" : {
+ "$ref" : "#/components/schemas/Pet"
+ },
+ "type" : "array"
+ }
+ },
+ "application/json" : {
+ "schema" : {
+ "items" : {
+ "$ref" : "#/components/schemas/Pet"
+ },
+ "type" : "array"
+ }
+ }
+ },
+ "description" : "successful operation"
+ },
+ "400" : {
+ "description" : "Invalid status value"
+ }
+ },
+ "security" : [ {
+ "petstore_auth" : [ "read:pets" ]
+ } ],
+ "summary" : "Finds Pets by status",
+ "tags" : [ "pet" ]
+ }
+ },
+ "/pet/findByTags" : {
+ "get" : {
+ "deprecated" : true,
+ "description" : "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.",
+ "operationId" : "findPetsByTags",
+ "parameters" : [ {
+ "description" : "Tags to filter by",
+ "explode" : false,
+ "in" : "query",
+ "name" : "tags",
+ "required" : true,
+ "schema" : {
+ "items" : {
+ "type" : "string"
+ },
+ "type" : "array"
+ },
+ "style" : "form"
+ } ],
+ "responses" : {
+ "200" : {
+ "content" : {
+ "application/xml" : {
+ "schema" : {
+ "items" : {
+ "$ref" : "#/components/schemas/Pet"
+ },
+ "type" : "array"
+ }
+ },
+ "application/json" : {
+ "schema" : {
+ "items" : {
+ "$ref" : "#/components/schemas/Pet"
+ },
+ "type" : "array"
+ }
+ }
+ },
+ "description" : "successful operation"
+ },
+ "400" : {
+ "description" : "Invalid tag value"
+ }
+ },
+ "security" : [ {
+ "petstore_auth" : [ "read:pets" ]
+ } ],
+ "summary" : "Finds Pets by tags",
+ "tags" : [ "pet" ]
+ }
+ },
+ "/pet/{petId}" : {
+ "delete" : {
+ "description" : "",
+ "operationId" : "deletePet",
+ "parameters" : [ {
+ "explode" : false,
+ "in" : "header",
+ "name" : "api_key",
+ "required" : false,
+ "schema" : {
+ "type" : "string"
+ },
+ "style" : "simple"
+ }, {
+ "description" : "Pet id to delete",
+ "explode" : false,
+ "in" : "path",
+ "name" : "petId",
+ "required" : true,
+ "schema" : {
+ "format" : "int64",
+ "type" : "integer"
+ },
+ "style" : "simple"
+ } ],
+ "responses" : {
+ "400" : {
+ "description" : "Invalid pet value"
+ }
+ },
+ "security" : [ {
+ "petstore_auth" : [ "write:pets", "read:pets" ]
+ } ],
+ "summary" : "Deletes a pet",
+ "tags" : [ "pet" ]
+ },
+ "get" : {
+ "description" : "Returns a single pet",
+ "operationId" : "getPetById",
+ "parameters" : [ {
+ "description" : "ID of pet to return",
+ "explode" : false,
+ "in" : "path",
+ "name" : "petId",
+ "required" : true,
+ "schema" : {
+ "format" : "int64",
+ "type" : "integer"
+ },
+ "style" : "simple"
+ } ],
+ "responses" : {
+ "200" : {
+ "content" : {
+ "application/xml" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/Pet"
+ }
+ },
+ "application/json" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/Pet"
+ }
+ }
+ },
+ "description" : "successful operation"
+ },
+ "400" : {
+ "description" : "Invalid ID supplied"
+ },
+ "404" : {
+ "description" : "Pet not found"
+ }
+ },
+ "security" : [ {
+ "api_key" : [ ]
+ } ],
+ "summary" : "Find pet by ID",
+ "tags" : [ "pet" ]
+ },
+ "post" : {
+ "description" : "",
+ "operationId" : "updatePetWithForm",
+ "parameters" : [ {
+ "description" : "ID of pet that needs to be updated",
+ "explode" : false,
+ "in" : "path",
+ "name" : "petId",
+ "required" : true,
+ "schema" : {
+ "format" : "int64",
+ "type" : "integer"
+ },
+ "style" : "simple"
+ } ],
+ "requestBody" : {
+ "content" : {
+ "application/x-www-form-urlencoded" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/updatePetWithForm_request"
+ }
+ }
+ }
+ },
+ "responses" : {
+ "405" : {
+ "description" : "Invalid input"
+ }
+ },
+ "security" : [ {
+ "petstore_auth" : [ "write:pets", "read:pets" ]
+ } ],
+ "summary" : "Updates a pet in the store with form data",
+ "tags" : [ "pet" ]
+ }
+ },
+ "/pet/{petId}/uploadImage" : {
+ "post" : {
+ "description" : "",
+ "operationId" : "uploadFile",
+ "parameters" : [ {
+ "description" : "ID of pet to update",
+ "explode" : false,
+ "in" : "path",
+ "name" : "petId",
+ "required" : true,
+ "schema" : {
+ "format" : "int64",
+ "type" : "integer"
+ },
+ "style" : "simple"
+ } ],
+ "requestBody" : {
+ "content" : {
+ "multipart/form-data" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/uploadFile_request"
+ }
+ }
+ }
+ },
+ "responses" : {
+ "200" : {
+ "content" : {
+ "application/json" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/ApiResponse"
+ }
+ }
+ },
+ "description" : "successful operation"
+ }
+ },
+ "security" : [ {
+ "petstore_auth" : [ "write:pets", "read:pets" ]
+ } ],
+ "summary" : "uploads an image",
+ "tags" : [ "pet" ]
+ }
+ },
+ "/store/inventory" : {
+ "get" : {
+ "description" : "Returns a map of status codes to quantities",
+ "operationId" : "getInventory",
+ "responses" : {
+ "200" : {
+ "content" : {
+ "application/json" : {
+ "schema" : {
+ "additionalProperties" : {
+ "format" : "int32",
+ "type" : "integer"
+ },
+ "type" : "object"
+ }
+ }
+ },
+ "description" : "successful operation"
+ }
+ },
+ "security" : [ {
+ "api_key" : [ ]
+ } ],
+ "summary" : "Returns pet inventories by status",
+ "tags" : [ "store" ]
+ }
+ },
+ "/store/order" : {
+ "post" : {
+ "description" : "",
+ "operationId" : "placeOrder",
+ "requestBody" : {
+ "content" : {
+ "application/json" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/Order"
+ }
+ }
+ },
+ "description" : "order placed for purchasing the pet",
+ "required" : true
+ },
+ "responses" : {
+ "200" : {
+ "content" : {
+ "application/xml" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/Order"
+ }
+ },
+ "application/json" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/Order"
+ }
+ }
+ },
+ "description" : "successful operation"
+ },
+ "400" : {
+ "description" : "Invalid Order"
+ }
+ },
+ "summary" : "Place an order for a pet",
+ "tags" : [ "store" ]
+ }
+ },
+ "/store/order/{orderId}" : {
+ "delete" : {
+ "description" : "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors",
+ "operationId" : "deleteOrder",
+ "parameters" : [ {
+ "description" : "ID of the order that needs to be deleted",
+ "explode" : false,
+ "in" : "path",
+ "name" : "orderId",
+ "required" : true,
+ "schema" : {
+ "type" : "string"
+ },
+ "style" : "simple"
+ } ],
+ "responses" : {
+ "400" : {
+ "description" : "Invalid ID supplied"
+ },
+ "404" : {
+ "description" : "Order not found"
+ }
+ },
+ "summary" : "Delete purchase order by ID",
+ "tags" : [ "store" ]
+ },
+ "get" : {
+ "description" : "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions",
+ "operationId" : "getOrderById",
+ "parameters" : [ {
+ "description" : "ID of pet that needs to be fetched",
+ "explode" : false,
+ "in" : "path",
+ "name" : "orderId",
+ "required" : true,
+ "schema" : {
+ "format" : "int64",
+ "maximum" : 5,
+ "minimum" : 1,
+ "type" : "integer"
+ },
+ "style" : "simple"
+ } ],
+ "responses" : {
+ "200" : {
+ "content" : {
+ "application/xml" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/Order"
+ }
+ },
+ "application/json" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/Order"
+ }
+ }
+ },
+ "description" : "successful operation"
+ },
+ "400" : {
+ "description" : "Invalid ID supplied"
+ },
+ "404" : {
+ "description" : "Order not found"
+ }
+ },
+ "summary" : "Find purchase order by ID",
+ "tags" : [ "store" ]
+ }
+ },
+ "/user" : {
+ "post" : {
+ "description" : "This can only be done by the logged in user.",
+ "operationId" : "createUser",
+ "requestBody" : {
+ "content" : {
+ "application/json" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/User"
+ }
+ }
+ },
+ "description" : "Created user object",
+ "required" : true
+ },
+ "responses" : {
+ "default" : {
+ "description" : "successful operation"
+ }
+ },
+ "security" : [ {
+ "api_key" : [ ]
+ } ],
+ "summary" : "Create user",
+ "tags" : [ "user" ]
+ }
+ },
+ "/user/createWithArray" : {
+ "post" : {
+ "description" : "",
+ "operationId" : "createUsersWithArrayInput",
+ "requestBody" : {
+ "$ref" : "#/components/requestBodies/UserArray"
+ },
+ "responses" : {
+ "default" : {
+ "description" : "successful operation"
+ }
+ },
+ "security" : [ {
+ "api_key" : [ ]
+ } ],
+ "summary" : "Creates list of users with given input array",
+ "tags" : [ "user" ]
+ }
+ },
+ "/user/createWithList" : {
+ "post" : {
+ "description" : "",
+ "operationId" : "createUsersWithListInput",
+ "requestBody" : {
+ "$ref" : "#/components/requestBodies/UserArray"
+ },
+ "responses" : {
+ "default" : {
+ "description" : "successful operation"
+ }
+ },
+ "security" : [ {
+ "api_key" : [ ]
+ } ],
+ "summary" : "Creates list of users with given input array",
+ "tags" : [ "user" ]
+ }
+ },
+ "/user/login" : {
+ "get" : {
+ "description" : "",
+ "operationId" : "loginUser",
+ "parameters" : [ {
+ "description" : "The user name for login",
+ "explode" : true,
+ "in" : "query",
+ "name" : "username",
+ "required" : true,
+ "schema" : {
+ "pattern" : "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$",
+ "type" : "string"
+ },
+ "style" : "form"
+ }, {
+ "description" : "The password for login in clear text",
+ "explode" : true,
+ "in" : "query",
+ "name" : "password",
+ "required" : true,
+ "schema" : {
+ "type" : "string"
+ },
+ "style" : "form"
+ } ],
+ "responses" : {
+ "200" : {
+ "content" : {
+ "application/xml" : {
+ "schema" : {
+ "type" : "string"
+ }
+ },
+ "application/json" : {
+ "schema" : {
+ "type" : "string"
+ }
+ }
+ },
+ "description" : "successful operation",
+ "headers" : {
+ "Set-Cookie" : {
+ "description" : "Cookie authentication key for use with the `api_key` apiKey authentication.",
+ "explode" : false,
+ "schema" : {
+ "example" : "AUTH_KEY=abcde12345; Path=/; HttpOnly",
+ "type" : "string"
+ },
+ "style" : "simple"
+ },
+ "X-Rate-Limit" : {
+ "description" : "calls per hour allowed by the user",
+ "explode" : false,
+ "schema" : {
+ "format" : "int32",
+ "type" : "integer"
+ },
+ "style" : "simple"
+ },
+ "X-Expires-After" : {
+ "description" : "date in UTC when token expires",
+ "explode" : false,
+ "schema" : {
+ "format" : "date-time",
+ "type" : "string"
+ },
+ "style" : "simple"
+ }
+ }
+ },
+ "400" : {
+ "description" : "Invalid username/password supplied"
+ }
+ },
+ "summary" : "Logs user into the system",
+ "tags" : [ "user" ]
+ }
+ },
+ "/user/logout" : {
+ "get" : {
+ "description" : "",
+ "operationId" : "logoutUser",
+ "responses" : {
+ "default" : {
+ "description" : "successful operation"
+ }
+ },
+ "security" : [ {
+ "api_key" : [ ]
+ } ],
+ "summary" : "Logs out current logged in user session",
+ "tags" : [ "user" ]
+ }
+ },
+ "/user/{username}" : {
+ "delete" : {
+ "description" : "This can only be done by the logged in user.",
+ "operationId" : "deleteUser",
+ "parameters" : [ {
+ "description" : "The name that needs to be deleted",
+ "explode" : false,
+ "in" : "path",
+ "name" : "username",
+ "required" : true,
+ "schema" : {
+ "type" : "string"
+ },
+ "style" : "simple"
+ } ],
+ "responses" : {
+ "400" : {
+ "description" : "Invalid username supplied"
+ },
+ "404" : {
+ "description" : "User not found"
+ }
+ },
+ "security" : [ {
+ "api_key" : [ ]
+ } ],
+ "summary" : "Delete user",
+ "tags" : [ "user" ]
+ },
+ "get" : {
+ "description" : "",
+ "operationId" : "getUserByName",
+ "parameters" : [ {
+ "description" : "The name that needs to be fetched. Use user1 for testing.",
+ "explode" : false,
+ "in" : "path",
+ "name" : "username",
+ "required" : true,
+ "schema" : {
+ "type" : "string"
+ },
+ "style" : "simple"
+ } ],
+ "responses" : {
+ "200" : {
+ "content" : {
+ "application/xml" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/User"
+ }
+ },
+ "application/json" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/User"
+ }
+ }
+ },
+ "description" : "successful operation"
+ },
+ "400" : {
+ "description" : "Invalid username supplied"
+ },
+ "404" : {
+ "description" : "User not found"
+ }
+ },
+ "summary" : "Get user by user name",
+ "tags" : [ "user" ]
+ },
+ "put" : {
+ "description" : "This can only be done by the logged in user.",
+ "operationId" : "updateUser",
+ "parameters" : [ {
+ "description" : "name that need to be deleted",
+ "explode" : false,
+ "in" : "path",
+ "name" : "username",
+ "required" : true,
+ "schema" : {
+ "type" : "string"
+ },
+ "style" : "simple"
+ } ],
+ "requestBody" : {
+ "content" : {
+ "application/json" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/User"
+ }
+ }
+ },
+ "description" : "Updated user object",
+ "required" : true
+ },
+ "responses" : {
+ "400" : {
+ "description" : "Invalid user supplied"
+ },
+ "404" : {
+ "description" : "User not found"
+ }
+ },
+ "security" : [ {
+ "api_key" : [ ]
+ } ],
+ "summary" : "Updated user",
+ "tags" : [ "user" ]
+ }
+ }
+ },
+ "components" : {
+ "requestBodies" : {
+ "UserArray" : {
+ "content" : {
+ "application/json" : {
+ "schema" : {
+ "items" : {
+ "$ref" : "#/components/schemas/User"
+ },
+ "type" : "array"
+ }
+ }
+ },
+ "description" : "List of user object",
+ "required" : true
+ },
+ "Pet" : {
+ "content" : {
+ "application/json" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/Pet"
+ }
+ },
+ "application/xml" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/Pet"
+ }
+ }
+ },
+ "description" : "Pet object that needs to be added to the store",
+ "required" : true
+ }
+ },
+ "schemas" : {
+ "Order" : {
+ "description" : "An order for a pets from the pet store",
+ "properties" : {
+ "id" : {
+ "format" : "int64",
+ "type" : "integer"
+ },
+ "petId" : {
+ "format" : "int64",
+ "type" : "integer"
+ },
+ "quantity" : {
+ "format" : "int32",
+ "type" : "integer"
+ },
+ "shipDate" : {
+ "format" : "date-time",
+ "type" : "string"
+ },
+ "status" : {
+ "description" : "Order Status",
+ "enum" : [ "placed", "approved", "delivered" ],
+ "type" : "string"
+ },
+ "complete" : {
+ "default" : false,
+ "type" : "boolean"
+ }
+ },
+ "title" : "Pet Order",
+ "type" : "object",
+ "xml" : {
+ "name" : "Order"
+ }
+ },
+ "Category" : {
+ "description" : "A category for a pet",
+ "properties" : {
+ "id" : {
+ "format" : "int64",
+ "type" : "integer"
+ },
+ "name" : {
+ "pattern" : "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$",
+ "type" : "string"
+ }
+ },
+ "title" : "Pet category",
+ "type" : "object",
+ "xml" : {
+ "name" : "Category"
+ }
+ },
+ "User" : {
+ "description" : "A User who is purchasing from the pet store",
+ "properties" : {
+ "id" : {
+ "format" : "int64",
+ "type" : "integer"
+ },
+ "username" : {
+ "type" : "string"
+ },
+ "firstName" : {
+ "type" : "string"
+ },
+ "lastName" : {
+ "type" : "string"
+ },
+ "email" : {
+ "type" : "string"
+ },
+ "password" : {
+ "type" : "string"
+ },
+ "phone" : {
+ "type" : "string"
+ },
+ "userStatus" : {
+ "description" : "User Status",
+ "format" : "int32",
+ "type" : "integer"
+ }
+ },
+ "title" : "a User",
+ "type" : "object",
+ "xml" : {
+ "name" : "User"
+ }
+ },
+ "Tag" : {
+ "description" : "A tag for a pet",
+ "properties" : {
+ "id" : {
+ "format" : "int64",
+ "type" : "integer"
+ },
+ "name" : {
+ "type" : "string"
+ }
+ },
+ "title" : "Pet Tag",
+ "type" : "object",
+ "xml" : {
+ "name" : "Tag"
+ }
+ },
+ "Pet" : {
+ "description" : "A pet for sale in the pet store",
+ "properties" : {
+ "id" : {
+ "format" : "int64",
+ "type" : "integer"
+ },
+ "category" : {
+ "$ref" : "#/components/schemas/Category"
+ },
+ "name" : {
+ "example" : "doggie",
+ "type" : "string"
+ },
+ "photoUrls" : {
+ "items" : {
+ "type" : "string"
+ },
+ "type" : "array",
+ "xml" : {
+ "name" : "photoUrl",
+ "wrapped" : true
+ }
+ },
+ "tags" : {
+ "items" : {
+ "$ref" : "#/components/schemas/Tag"
+ },
+ "type" : "array",
+ "xml" : {
+ "name" : "tag",
+ "wrapped" : true
+ }
+ },
+ "status" : {
+ "deprecated" : true,
+ "description" : "pet status in the store",
+ "enum" : [ "available", "pending", "sold" ],
+ "type" : "string"
+ }
+ },
+ "required" : [ "name", "photoUrls" ],
+ "title" : "a Pet",
+ "type" : "object",
+ "xml" : {
+ "name" : "Pet"
+ }
+ },
+ "ApiResponse" : {
+ "description" : "Describes the result of uploading an image resource",
+ "properties" : {
+ "code" : {
+ "format" : "int32",
+ "type" : "integer"
+ },
+ "type" : {
+ "type" : "string"
+ },
+ "message" : {
+ "type" : "string"
+ }
+ },
+ "title" : "An uploaded response",
+ "type" : "object"
+ },
+ "updatePetWithForm_request" : {
+ "properties" : {
+ "name" : {
+ "description" : "Updated name of the pet",
+ "type" : "string"
+ },
+ "status" : {
+ "description" : "Updated status of the pet",
+ "type" : "string"
+ }
+ },
+ "type" : "object"
+ },
+ "uploadFile_request" : {
+ "properties" : {
+ "additionalMetadata" : {
+ "description" : "Additional data to pass to server",
+ "type" : "string"
+ },
+ "file" : {
+ "description" : "file to upload",
+ "format" : "binary",
+ "type" : "string"
+ }
+ },
+ "type" : "object"
+ }
+ },
+ "securitySchemes" : {
+ "petstore_auth" : {
+ "flows" : {
+ "implicit" : {
+ "authorizationUrl" : "http://petstore.swagger.io/api/oauth/dialog",
+ "scopes" : {
+ "write:pets" : "modify pets in your account",
+ "read:pets" : "read your pets"
+ }
+ }
+ },
+ "type" : "oauth2"
+ },
+ "api_key" : {
+ "in" : "header",
+ "name" : "api_key",
+ "type" : "apiKey"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/server/petstore/scala-cask/src/main/scala/sample/cask/AppRoutes.scala b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/AppRoutes.scala
new file mode 100644
index 00000000000..36501557371
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/AppRoutes.scala
@@ -0,0 +1,52 @@
+//> using scala "3.3.1"
+//> using lib "com.lihaoyi::cask:0.9.2"
+//> using lib "com.lihaoyi::scalatags:0.8.2"
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator.
+ *
+ * https://openapi-generator.tech
+ */
+
+
+// this file was generated from app.mustache
+package cask.groupId.server
+
+import _root_.sample.cask.model.*
+import _root_.sample.cask.api.*
+
+/**
+ * This trait encapsulates the business logic (services) and the
+ * http routes which handle the http requests sent to those services.
+ *
+ * There are default 'not implemented' implementations for the service.
+ *
+ * If you wanted fine-grained control over the routes and services, you could
+ * extend the cask.MainRoutes and mix in this trait by using this:
+ *
+ * \{\{\{
+ * override def allRoutes = appRoutes
+ * \}\}\}
+ *
+ * More typically, however, you would extend the 'BaseApp' class
+ */
+trait AppRoutes {
+ def appPetService : PetService = PetService()
+ def routeForPet : PetRoutes = PetRoutes(appPetService)
+ def appStoreService : StoreService = StoreService()
+ def routeForStore : StoreRoutes = StoreRoutes(appStoreService)
+ def appUserService : UserService = UserService()
+ def routeForUser : UserRoutes = UserRoutes(appUserService)
+
+ def appRoutes = Seq(
+ routeForPet ,
+ routeForStore ,
+ routeForUser
+ )
+}
diff --git a/samples/server/petstore/scala-cask/src/main/scala/sample/cask/BaseApp.scala b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/BaseApp.scala
new file mode 100644
index 00000000000..c854b216d20
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/BaseApp.scala
@@ -0,0 +1,59 @@
+//> using scala "3.3.1"
+//> using lib "com.lihaoyi::cask:0.9.2"
+//> using lib "com.lihaoyi::scalatags:0.8.2"
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator.
+ *
+ * https://openapi-generator.tech
+ */
+
+
+// this file was generated from app.mustache
+package cask.groupId.server
+
+import _root_.sample.cask.model.*
+import _root_.sample.cask.api.*
+
+/**
+ * This class was created with the intention of being extended by some runnable object,
+ * passing in the custom business logic services
+ */
+class BaseApp(
+ override val appPetService : PetService = PetService(),
+
+ override val appStoreService : StoreService = StoreService(),
+
+ override val appUserService : UserService = UserService(),
+ override val port : Int = sys.env.get("PORT").map(_.toInt).getOrElse(8080)) extends cask.MainRoutes with AppRoutes {
+
+ /** routes for the UI
+ * Subclasses can override to turn this off
+ */
+ def openApiRoute: Option[cask.Routes] = Option(OpenApiRoutes(port))
+
+ override def allRoutes = appRoutes ++ openApiRoute
+
+
+ override def host: String = "0.0.0.0"
+
+ def start() = locally {
+ initialize()
+ println(box(s""" 🚀 browse to localhost:$port 🚀
+ | host : $host
+ | port : $port
+ | verbose : $verbose
+ | debugMode : $debugMode
+ |""".stripMargin))
+
+ // if java.awt.Desktop.isDesktopSupported then {
+ // java.awt.Desktop.getDesktop.browse(new java.net.URI(s"http://localhost:${port}"))
+ // }
+ }
+}
diff --git a/samples/server/petstore/scala-cask/src/main/scala/sample/cask/ExampleApp.scala b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/ExampleApp.scala
new file mode 100644
index 00000000000..9c5e733b73e
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/ExampleApp.scala
@@ -0,0 +1,31 @@
+//> using scala "3.3.1"
+//> using lib "com.lihaoyi::cask:0.9.2"
+//> using lib "com.lihaoyi::scalatags:0.8.2"
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator.
+ *
+ * https://openapi-generator.tech
+ */
+
+
+// this file was generated from app.mustache
+package cask.groupId.server
+
+import _root_.sample.cask.model.*
+import _root_.sample.cask.api.*
+
+/**
+ * This is an example of how you might extends BaseApp for a runnable application.
+ *
+ * See the README.md for how to create your own app
+ */
+object ExampleApp extends BaseApp() {
+ start()
+}
diff --git a/samples/server/petstore/scala-cask/src/main/scala/sample/cask/api/OpenApiRoutes.scala b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/api/OpenApiRoutes.scala
new file mode 100644
index 00000000000..a991ce2aaf4
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/api/OpenApiRoutes.scala
@@ -0,0 +1,128 @@
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator.
+ *
+ * https://openapi-generator.tech
+ */
+
+
+// generated from openapiRoute.mustache
+package sample.cask.api
+
+import cask.model.Response
+
+import java.nio.file.{Files, Path, Paths}
+
+/**
+ * This code will try and download the swagger UI static files on startup
+ *
+ * That behaviour can be altered by:
+ * - setting the environment variable SWAGGER_ON to false
+ * - setting the environment variable SWAGGER_UI_URL to either the URL of a swagger UI zip or setting it to the empty string
+ *
+ */
+object OpenApiRoutes {
+
+ def swaggerUIUrl: Option[String] = {
+ // flag to turn SWAGGER off
+ def useSwaggerUI = sys.env.get("SWAGGER_ON").map(_.toBoolean).getOrElse(true)
+
+ val defaultUrl = "https://github.com/swagger-api/swagger-ui/archive/refs/tags/v5.11.9.zip"
+ Option(sys.env.getOrElse("SWAGGER_UI_URL", defaultUrl))
+ .map(_.trim)
+ .filterNot(_.isEmpty)
+ .filter(_ => useSwaggerUI)
+ }
+ def apply(localPort: Int) = new OpenApiRoutes(localPort, swaggerUIUrl)
+}
+
+class OpenApiRoutes(localPort: Int, swaggerUrl: Option[String]) extends cask.Routes {
+
+ def openApiDir = "ui"
+
+ @cask.get("/")
+ def index() = cask.Redirect("/ui/index.html")
+
+ @cask.staticFiles("/ui")
+ def staticUI() = openApiDir
+
+ @cask.staticResources("/openapi.json")
+ def staticOpenApi() = "openapi.json"
+
+ /** This code will try and download the swagger UI artefacts to a local directory to serve up
+ */
+ object extract {
+
+ def openApiDirPath: Path = Paths.get(openApiDir)
+ def hasSwagger = Files.exists(openApiDirPath) && Files.isDirectory(openApiDirPath)
+
+ import java.io.{BufferedInputStream, FileOutputStream, InputStream}
+ import java.net.URL
+ import java.util.zip.{ZipEntry, ZipInputStream}
+ import scala.util.Using
+
+ def apply(url: String) = {
+ if !hasSwagger then downloadAndExtractZip(url, openApiDir)
+ }
+
+ def downloadAndExtractZip(url: String, outputDir: String): Unit = {
+ val urlConn = new URL(url).openConnection()
+ urlConn.setRequestProperty("User-Agent", "Mozilla/5.0")
+
+ Using(urlConn.getInputStream) { inputStream =>
+ val zipIn = new ZipInputStream(new BufferedInputStream(inputStream))
+ LazyList.continually(zipIn.getNextEntry).takeWhile(_ != null).foreach { entry =>
+
+ def isDist = entry.getName.contains("/dist/")
+ def isNotMap = !entry.getName.endsWith(".map")
+
+ if (!entry.isDirectory && isDist && isNotMap) {
+ val fileName = entry.getName.split("/").last
+ extractFile(entry.getName, zipIn, s"$outputDir/$fileName")
+ }
+ zipIn.closeEntry()
+ }
+ }
+ }
+
+ def extractFile(name: String, zipIn: ZipInputStream, filePath: String): Unit = {
+ val fullPath = Paths.get(filePath).toAbsolutePath
+ if !Files.exists(fullPath.getParent) then {
+ Files.createDirectories(fullPath.getParent)
+ }
+
+ // config hack - we replace the default url from this swagger conf to use our localhost
+ //
+ if name.endsWith("swagger-initializer.js") then {
+ val textLines = scala.io.Source.fromInputStream(zipIn).getLines().map {
+ case line if line.contains("url:") =>
+ s""" url: "http://localhost:$localPort/openapi.json","""
+ case line => line
+ }
+
+ // keeping this compatible for java 8, where this is from >= java 11:
+ // Files.writeString(fullPath, textLines.mkString("\n"))
+ scala.util.Using(new java.io.PrintWriter(fullPath.toFile))(_.write(textLines.mkString("\n")))
+ } else {
+ Using(new FileOutputStream(filePath)) { outputStream =>
+ val buffer = new Array[Byte](1024)
+ LazyList
+ .continually(zipIn.read(buffer))
+ .takeWhile(_ != -1)
+ .foreach(outputStream.write(buffer, 0, _))
+ }
+ }
+ }
+ }
+
+ // extract the swagger UI resources to our local directory
+ swaggerUrl.foreach(url => extract(url))
+
+ initialize()
+}
diff --git a/samples/server/petstore/scala-cask/src/main/scala/sample/cask/api/PetRoutes.scala b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/api/PetRoutes.scala
new file mode 100644
index 00000000000..4224e229493
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/api/PetRoutes.scala
@@ -0,0 +1,212 @@
+//> using scala "3.3.1"
+//> using lib "com.lihaoyi::cask:0.8.3"
+//> using lib "com.lihaoyi::scalatags:0.12.0"
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator.
+ *
+ * https://openapi-generator.tech
+ */
+
+
+// this is generated from apiRoutes.mustache
+package sample.cask.api
+
+import sample.cask.model.*
+
+import upickle.default.{ReadWriter => RW, macroRW}
+import upickle.default.*
+
+import sample.cask.model.ApiResponse
+import java.io.File
+import sample.cask.model.Pet
+
+class PetRoutes(service : PetService) extends cask.Routes {
+
+ // route group for routeWorkAroundForPOSTPet
+ @cask.post("/pet", true)
+ def routeWorkAroundForPOSTPet(request: cask.Request) = {
+ request.remainingPathSegments match {
+ case Seq() => addPet(request)
+ case Seq(petId) => updatePetWithForm(petId.toLong,request)
+ case Seq(petId,"uploadImage") => uploadFile(petId.toLong,request)
+ case _ => cask.Response("Not Found", statusCode = 404)
+ }
+ }
+ // route group for routeWorkAroundForGETPet
+ @cask.get("/pet", true)
+ def routeWorkAroundForGETPet(request: cask.Request,status : Seq[String] = Nil,tags : Seq[String] = Nil) = {
+ request.remainingPathSegments match {
+ case Seq("findByStatus") => findPetsByStatus(request,status)
+ case Seq("findByTags") => findPetsByTags(request,tags)
+ case Seq(petId) => getPetById(petId.toLong,request)
+ case _ => cask.Response("Not Found", statusCode = 404)
+ }
+ }
+
+ /** Add a new pet to the store
+ *
+ */
+ // conflicts with [/pet/{petId}, /pet/{petId}/uploadImage, /pet] after/pet, ignoring @cask.post("/pet")
+ def addPet(request: cask.Request) = {
+ // auth method petstore_auth : oauth2, keyParamName:
+
+ def failFast = request.queryParams.keySet.contains("failFast")
+
+ val result = for {
+ petData <- Parsed.eval(PetData.fromJsonString(request.bodyAsString)).mapError(e => s"Error parsing json as Pet from >${request.bodyAsString}< : ${e}") /* not array or map */
+ pet <- Parsed.fromTry(petData.validated(failFast))
+ result <- Parsed.eval(service.addPet(pet))
+ } yield result
+
+ result match {
+ case Left(error) => cask.Response(error, 500)
+ case Right(_) => cask.Response("", 200)
+ }
+ }
+ /** Deletes a pet
+ *
+ */
+ @cask.delete("/pet/:petId")
+ def deletePet(petId : Long, request: cask.Request) = {
+ // auth method petstore_auth : oauth2, keyParamName:
+
+ def failFast = request.queryParams.keySet.contains("failFast")
+
+ val result = for {
+ petId <- Parsed(petId)
+ apiKey <- request.headerSingleValueOptional("apiKey")
+ result <- Parsed.eval(service.deletePet(petId, apiKey))
+ } yield result
+
+ result match {
+ case Left(error) => cask.Response(error, 500)
+ case Right(_) => cask.Response("", 200)
+ }
+ }
+ /** Finds Pets by status
+ *
+ */
+ // conflicts with [/pet/{petId}, /pet/findByStatus, /pet/findByTags] after/pet, ignoring @cask.get("/pet/findByStatus")
+ def findPetsByStatus(request: cask.Request, status : Seq[String]) = {
+ // auth method petstore_auth : oauth2, keyParamName:
+
+ def failFast = request.queryParams.keySet.contains("failFast")
+
+ val result = for {
+ result <- Parsed.eval(service.findPetsByStatus(status))
+ } yield result
+
+ result match {
+ case Left(error) => cask.Response(error, 500)
+ case Right(_) => cask.Response("", 200)
+ }
+ }
+ /** Finds Pets by tags
+ *
+ */
+ // conflicts with [/pet/{petId}, /pet/findByStatus, /pet/findByTags] after/pet, ignoring @cask.get("/pet/findByTags")
+ def findPetsByTags(request: cask.Request, tags : Seq[String]) = {
+ // auth method petstore_auth : oauth2, keyParamName:
+
+ def failFast = request.queryParams.keySet.contains("failFast")
+
+ val result = for {
+ result <- Parsed.eval(service.findPetsByTags(tags))
+ } yield result
+
+ result match {
+ case Left(error) => cask.Response(error, 500)
+ case Right(_) => cask.Response("", 200)
+ }
+ }
+ /** Find pet by ID
+ *
+ */
+ // conflicts with [/pet/{petId}, /pet/findByStatus, /pet/findByTags] after/pet, ignoring @cask.get("/pet/:petId")
+ def getPetById(petId : Long, request: cask.Request) = {
+ // auth method api_key : apiKey, keyParamName: api_key
+
+ def failFast = request.queryParams.keySet.contains("failFast")
+
+ val result = for {
+ petId <- Parsed(petId)
+ result <- Parsed.eval(service.getPetById(petId))
+ } yield result
+
+ result match {
+ case Left(error) => cask.Response(error, 500)
+ case Right(_) => cask.Response("", 200)
+ }
+ }
+ /** Update an existing pet
+ *
+ */
+ @cask.put("/pet")
+ def updatePet(request: cask.Request) = {
+ // auth method petstore_auth : oauth2, keyParamName:
+
+ def failFast = request.queryParams.keySet.contains("failFast")
+
+ val result = for {
+ petData <- Parsed.eval(PetData.fromJsonString(request.bodyAsString)).mapError(e => s"Error parsing json as Pet from >${request.bodyAsString}< : ${e}") /* not array or map */
+ pet <- Parsed.fromTry(petData.validated(failFast))
+ result <- Parsed.eval(service.updatePet(pet))
+ } yield result
+
+ result match {
+ case Left(error) => cask.Response(error, 500)
+ case Right(_) => cask.Response("", 200)
+ }
+ }
+ /** Updates a pet in the store with form data
+ *
+ */
+ // conflicts with [/pet/{petId}, /pet/{petId}/uploadImage, /pet] after/pet, ignoring @cask.post("/pet/:petId")
+ def updatePetWithForm(petId : Long, request: cask.Request) = {
+ // auth method petstore_auth : oauth2, keyParamName:
+
+ def failFast = request.queryParams.keySet.contains("failFast")
+
+ val result = for {
+ petId <- Parsed(petId)
+ name <- request.formSingleValueOptional("name")
+ status <- request.formSingleValueOptional("status")
+ result <- Parsed.eval(service.updatePetWithForm(petId, name, status))
+ } yield result
+
+ result match {
+ case Left(error) => cask.Response(error, 500)
+ case Right(_) => cask.Response("", 200)
+ }
+ }
+ /** uploads an image
+ *
+ */
+ // conflicts with [/pet/{petId}, /pet/{petId}/uploadImage, /pet] after/pet, ignoring @cask.post("/pet/:petId/uploadImage")
+ def uploadFile(petId : Long, request: cask.Request) = {
+ // auth method petstore_auth : oauth2, keyParamName:
+
+ def failFast = request.queryParams.keySet.contains("failFast")
+
+ val result = for {
+ petId <- Parsed(petId)
+ additionalMetadata <- request.formSingleValueOptional("additionalMetadata")
+ file <- request.formValueAsFileOptional("file")
+ result <- Parsed.eval(service.uploadFile(petId, additionalMetadata, file))
+ } yield result
+
+ result match {
+ case Left(error) => cask.Response(error, 500)
+ case Right(_) => cask.Response("", 200)
+ }
+ }
+
+ initialize()
+}
diff --git a/samples/server/petstore/scala-cask/src/main/scala/sample/cask/api/PetService.scala b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/api/PetService.scala
new file mode 100644
index 00000000000..7bed5032e2c
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/api/PetService.scala
@@ -0,0 +1,84 @@
+//> using scala "3.3.1"
+//> using lib "com.lihaoyi::cask:0.8.3"
+//> using lib "com.lihaoyi::scalatags:0.12.0"
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator.
+ *
+ * https://openapi-generator.tech
+ */
+
+
+// generated from apiService.mustache
+package sample.cask.api
+
+import _root_.sample.cask.model.ApiResponse
+import _root_.java.io.File
+import _root_.sample.cask.model.Pet
+
+import _root_.sample.cask.model.*
+
+object PetService {
+ def apply() : PetService = new PetService {
+ override def addPet(pet : Pet) : Pet = ???
+ override def deletePet(petId : Long, apiKey : Option[String]) : Unit = ???
+ override def findPetsByStatus(status : Seq[String]) : List[Pet] = ???
+ override def findPetsByTags(tags : Seq[String]) : List[Pet] = ???
+ override def getPetById(petId : Long) : Pet = ???
+ override def updatePet(pet : Pet) : Pet = ???
+ override def updatePetWithForm(petId : Long, name : Option[String], status : Option[String]) : Unit = ???
+ override def uploadFile(petId : Long, additionalMetadata : Option[String], file : Option[File]) : ApiResponse = ???
+ }
+}
+
+/**
+ * The Pet business-logic
+ */
+trait PetService {
+ /** Add a new pet to the store
+ *
+ * @return Pet
+ */
+ def addPet(pet : Pet) : Pet
+ /** Deletes a pet
+ *
+ * @return
+ */
+ def deletePet(petId : Long, apiKey : Option[String]) : Unit
+ /** Finds Pets by status
+ *
+ * @return List[Pet]
+ */
+ def findPetsByStatus(status : Seq[String]) : List[Pet]
+ /** Finds Pets by tags
+ *
+ * @return List[Pet]
+ */
+ def findPetsByTags(tags : Seq[String]) : List[Pet]
+ /** Find pet by ID
+ *
+ * @return Pet
+ */
+ def getPetById(petId : Long) : Pet
+ /** Update an existing pet
+ *
+ * @return Pet
+ */
+ def updatePet(pet : Pet) : Pet
+ /** Updates a pet in the store with form data
+ *
+ * @return
+ */
+ def updatePetWithForm(petId : Long, name : Option[String], status : Option[String]) : Unit
+ /** uploads an image
+ *
+ * @return ApiResponse
+ */
+ def uploadFile(petId : Long, additionalMetadata : Option[String], file : Option[File]) : ApiResponse
+}
diff --git a/samples/server/petstore/scala-cask/src/main/scala/sample/cask/api/StoreRoutes.scala b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/api/StoreRoutes.scala
new file mode 100644
index 00000000000..1452575b38c
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/api/StoreRoutes.scala
@@ -0,0 +1,106 @@
+//> using scala "3.3.1"
+//> using lib "com.lihaoyi::cask:0.8.3"
+//> using lib "com.lihaoyi::scalatags:0.12.0"
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator.
+ *
+ * https://openapi-generator.tech
+ */
+
+
+// this is generated from apiRoutes.mustache
+package sample.cask.api
+
+import sample.cask.model.*
+
+import upickle.default.{ReadWriter => RW, macroRW}
+import upickle.default.*
+
+import sample.cask.model.Order
+
+class StoreRoutes(service : StoreService) extends cask.Routes {
+
+
+ /** Delete purchase order by ID
+ *
+ */
+ @cask.delete("/store/order/:orderId")
+ def deleteOrder(orderId : String, request: cask.Request) = {
+
+ def failFast = request.queryParams.keySet.contains("failFast")
+
+ val result = for {
+ orderId <- Parsed(orderId)
+ result <- Parsed.eval(service.deleteOrder(orderId))
+ } yield result
+
+ result match {
+ case Left(error) => cask.Response(error, 500)
+ case Right(_) => cask.Response("", 200)
+ }
+ }
+ /** Returns pet inventories by status
+ *
+ */
+ @cask.get("/store/inventory")
+ def getInventory(request: cask.Request) = {
+ // auth method api_key : apiKey, keyParamName: api_key
+
+ def failFast = request.queryParams.keySet.contains("failFast")
+
+ val result = for {
+ result <- Parsed.eval(service.getInventory())
+ } yield result
+
+ result match {
+ case Left(error) => cask.Response(error, 500)
+ case Right(_) => cask.Response("", 200)
+ }
+ }
+ /** Find purchase order by ID
+ *
+ */
+ @cask.get("/store/order/:orderId")
+ def getOrderById(orderId : Long, request: cask.Request) = {
+
+ def failFast = request.queryParams.keySet.contains("failFast")
+
+ val result = for {
+ orderId <- Parsed(orderId)
+ result <- Parsed.eval(service.getOrderById(orderId))
+ } yield result
+
+ result match {
+ case Left(error) => cask.Response(error, 500)
+ case Right(_) => cask.Response("", 200)
+ }
+ }
+ /** Place an order for a pet
+ *
+ */
+ @cask.post("/store/order")
+ def placeOrder(request: cask.Request) = {
+
+ def failFast = request.queryParams.keySet.contains("failFast")
+
+ val result = for {
+ orderData <- Parsed.eval(OrderData.fromJsonString(request.bodyAsString)).mapError(e => s"Error parsing json as Order from >${request.bodyAsString}< : ${e}") /* not array or map */
+ order <- Parsed.fromTry(orderData.validated(failFast))
+ result <- Parsed.eval(service.placeOrder(order))
+ } yield result
+
+ result match {
+ case Left(error) => cask.Response(error, 500)
+ case Right(_) => cask.Response("", 200)
+ }
+ }
+
+ initialize()
+}
diff --git a/samples/server/petstore/scala-cask/src/main/scala/sample/cask/api/StoreService.scala b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/api/StoreService.scala
new file mode 100644
index 00000000000..231f03fc3fe
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/api/StoreService.scala
@@ -0,0 +1,58 @@
+//> using scala "3.3.1"
+//> using lib "com.lihaoyi::cask:0.8.3"
+//> using lib "com.lihaoyi::scalatags:0.12.0"
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator.
+ *
+ * https://openapi-generator.tech
+ */
+
+
+// generated from apiService.mustache
+package sample.cask.api
+
+import _root_.sample.cask.model.Order
+
+import _root_.sample.cask.model.*
+
+object StoreService {
+ def apply() : StoreService = new StoreService {
+ override def deleteOrder(orderId : String) : Unit = ???
+ override def getInventory() : Map[String, Int] = ???
+ override def getOrderById(orderId : Long) : Order = ???
+ override def placeOrder(order : Order) : Order = ???
+ }
+}
+
+/**
+ * The Store business-logic
+ */
+trait StoreService {
+ /** Delete purchase order by ID
+ *
+ * @return
+ */
+ def deleteOrder(orderId : String) : Unit
+ /** Returns pet inventories by status
+ *
+ * @return Map[String, Int]
+ */
+ def getInventory() : Map[String, Int]
+ /** Find purchase order by ID
+ *
+ * @return Order
+ */
+ def getOrderById(orderId : Long) : Order
+ /** Place an order for a pet
+ *
+ * @return Order
+ */
+ def placeOrder(order : Order) : Order
+}
diff --git a/samples/server/petstore/scala-cask/src/main/scala/sample/cask/api/UserRoutes.scala b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/api/UserRoutes.scala
new file mode 100644
index 00000000000..7987883da6b
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/api/UserRoutes.scala
@@ -0,0 +1,195 @@
+//> using scala "3.3.1"
+//> using lib "com.lihaoyi::cask:0.8.3"
+//> using lib "com.lihaoyi::scalatags:0.12.0"
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator.
+ *
+ * https://openapi-generator.tech
+ */
+
+
+// this is generated from apiRoutes.mustache
+package sample.cask.api
+
+import sample.cask.model.*
+
+import upickle.default.{ReadWriter => RW, macroRW}
+import upickle.default.*
+
+import java.time.OffsetDateTime
+import sample.cask.model.User
+
+class UserRoutes(service : UserService) extends cask.Routes {
+
+ // route group for routeWorkAroundForGETUser
+ @cask.get("/user", true)
+ def routeWorkAroundForGETUser(request: cask.Request,username : Option[String] = None,password : Option[String] = None) = {
+ request.remainingPathSegments match {
+ case Seq("login") => loginUser(request,username.getOrElse(""), password.getOrElse(""))
+ case Seq("logout") => logoutUser(request)
+ case Seq(username) => getUserByName(username,request)
+ case _ => cask.Response("Not Found", statusCode = 404)
+ }
+ }
+
+ /** Create user
+ *
+ */
+ @cask.post("/user")
+ def createUser(request: cask.Request) = {
+ // auth method api_key : apiKey, keyParamName: api_key
+
+ def failFast = request.queryParams.keySet.contains("failFast")
+
+ val result = for {
+ userData <- Parsed.eval(UserData.fromJsonString(request.bodyAsString)).mapError(e => s"Error parsing json as User from >${request.bodyAsString}< : ${e}") /* not array or map */
+ user <- Parsed.fromTry(userData.validated(failFast))
+ result <- Parsed.eval(service.createUser(user))
+ } yield result
+
+ result match {
+ case Left(error) => cask.Response(error, 500)
+ case Right(_) => cask.Response("", 200)
+ }
+ }
+ /** Creates list of users with given input array
+ *
+ */
+ @cask.post("/user/createWithArray")
+ def createUsersWithArrayInput(request: cask.Request) = {
+ // auth method api_key : apiKey, keyParamName: api_key
+
+ def failFast = request.queryParams.keySet.contains("failFast")
+
+ val result = for {
+ user <- Parsed.fromTry(UserData.manyFromJsonStringValidated(request.bodyAsString)).mapError(e => s"Error parsing json as an array of User from >${request.bodyAsString}< : ${e}") /* array */
+ result <- Parsed.eval(service.createUsersWithArrayInput(user))
+ } yield result
+
+ result match {
+ case Left(error) => cask.Response(error, 500)
+ case Right(_) => cask.Response("", 200)
+ }
+ }
+ /** Creates list of users with given input array
+ *
+ */
+ @cask.post("/user/createWithList")
+ def createUsersWithListInput(request: cask.Request) = {
+ // auth method api_key : apiKey, keyParamName: api_key
+
+ def failFast = request.queryParams.keySet.contains("failFast")
+
+ val result = for {
+ user <- Parsed.fromTry(UserData.manyFromJsonStringValidated(request.bodyAsString)).mapError(e => s"Error parsing json as an array of User from >${request.bodyAsString}< : ${e}") /* array */
+ result <- Parsed.eval(service.createUsersWithListInput(user))
+ } yield result
+
+ result match {
+ case Left(error) => cask.Response(error, 500)
+ case Right(_) => cask.Response("", 200)
+ }
+ }
+ /** Delete user
+ *
+ */
+ @cask.delete("/user/:username")
+ def deleteUser(username : String, request: cask.Request) = {
+ // auth method api_key : apiKey, keyParamName: api_key
+
+ def failFast = request.queryParams.keySet.contains("failFast")
+
+ val result = for {
+ username <- Parsed(username)
+ result <- Parsed.eval(service.deleteUser(username))
+ } yield result
+
+ result match {
+ case Left(error) => cask.Response(error, 500)
+ case Right(_) => cask.Response("", 200)
+ }
+ }
+ /** Get user by user name
+ *
+ */
+ // conflicts with [/user/{username}, /user/login, /user/logout] after/user, ignoring @cask.get("/user/:username")
+ def getUserByName(username : String, request: cask.Request) = {
+
+ def failFast = request.queryParams.keySet.contains("failFast")
+
+ val result = for {
+ username <- Parsed(username)
+ result <- Parsed.eval(service.getUserByName(username))
+ } yield result
+
+ result match {
+ case Left(error) => cask.Response(error, 500)
+ case Right(_) => cask.Response("", 200)
+ }
+ }
+ /** Logs user into the system
+ *
+ */
+ // conflicts with [/user/{username}, /user/login, /user/logout] after/user, ignoring @cask.get("/user/login")
+ def loginUser(request: cask.Request, username : String, password : String) = {
+
+ def failFast = request.queryParams.keySet.contains("failFast")
+
+ val result = for {
+ result <- Parsed.eval(service.loginUser(username, password))
+ } yield result
+
+ result match {
+ case Left(error) => cask.Response(error, 500)
+ case Right(_) => cask.Response("", 200)
+ }
+ }
+ /** Logs out current logged in user session
+ *
+ */
+ // conflicts with [/user/{username}, /user/login, /user/logout] after/user, ignoring @cask.get("/user/logout")
+ def logoutUser(request: cask.Request) = {
+ // auth method api_key : apiKey, keyParamName: api_key
+
+ def failFast = request.queryParams.keySet.contains("failFast")
+
+ val result = for {
+ result <- Parsed.eval(service.logoutUser())
+ } yield result
+
+ result match {
+ case Left(error) => cask.Response(error, 500)
+ case Right(_) => cask.Response("", 200)
+ }
+ }
+ /** Updated user
+ *
+ */
+ @cask.put("/user/:username")
+ def updateUser(username : String, request: cask.Request) = {
+ // auth method api_key : apiKey, keyParamName: api_key
+
+ def failFast = request.queryParams.keySet.contains("failFast")
+
+ val result = for {
+ username <- Parsed(username)
+ userData <- Parsed.eval(UserData.fromJsonString(request.bodyAsString)).mapError(e => s"Error parsing json as User from >${request.bodyAsString}< : ${e}") /* not array or map */
+ user <- Parsed.fromTry(userData.validated(failFast))
+ result <- Parsed.eval(service.updateUser(username, user))
+ } yield result
+
+ result match {
+ case Left(error) => cask.Response(error, 500)
+ case Right(_) => cask.Response("", 200)
+ }
+ }
+
+ initialize()
+}
diff --git a/samples/server/petstore/scala-cask/src/main/scala/sample/cask/api/UserService.scala b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/api/UserService.scala
new file mode 100644
index 00000000000..4872f31e3c7
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/api/UserService.scala
@@ -0,0 +1,83 @@
+//> using scala "3.3.1"
+//> using lib "com.lihaoyi::cask:0.8.3"
+//> using lib "com.lihaoyi::scalatags:0.12.0"
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator.
+ *
+ * https://openapi-generator.tech
+ */
+
+
+// generated from apiService.mustache
+package sample.cask.api
+
+import _root_.java.time.OffsetDateTime
+import _root_.sample.cask.model.User
+
+import _root_.sample.cask.model.*
+
+object UserService {
+ def apply() : UserService = new UserService {
+ override def createUser(user : User) : Unit = ???
+ override def createUsersWithArrayInput(user : Seq[User]) : Unit = ???
+ override def createUsersWithListInput(user : Seq[User]) : Unit = ???
+ override def deleteUser(username : String) : Unit = ???
+ override def getUserByName(username : String) : User = ???
+ override def loginUser(username : String, password : String) : String = ???
+ override def logoutUser() : Unit = ???
+ override def updateUser(username : String, user : User) : Unit = ???
+ }
+}
+
+/**
+ * The User business-logic
+ */
+trait UserService {
+ /** Create user
+ *
+ * @return
+ */
+ def createUser(user : User) : Unit
+ /** Creates list of users with given input array
+ *
+ * @return
+ */
+ def createUsersWithArrayInput(user : Seq[User]) : Unit
+ /** Creates list of users with given input array
+ *
+ * @return
+ */
+ def createUsersWithListInput(user : Seq[User]) : Unit
+ /** Delete user
+ *
+ * @return
+ */
+ def deleteUser(username : String) : Unit
+ /** Get user by user name
+ *
+ * @return User
+ */
+ def getUserByName(username : String) : User
+ /** Logs user into the system
+ *
+ * @return String
+ */
+ def loginUser(username : String, password : String) : String
+ /** Logs out current logged in user session
+ *
+ * @return
+ */
+ def logoutUser() : Unit
+ /** Updated user
+ *
+ * @return
+ */
+ def updateUser(username : String, user : User) : Unit
+}
diff --git a/samples/server/petstore/scala-cask/src/main/scala/sample/cask/api/package.scala b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/api/package.scala
new file mode 100644
index 00000000000..ab25a84875a
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/api/package.scala
@@ -0,0 +1,167 @@
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator.
+ *
+ * https://openapi-generator.tech
+ */
+
+package sample.cask.api
+
+
+import cask.FormEntry
+import io.undertow.server.handlers.form.{FormData, FormParserFactory}
+
+import java.io.File
+import scala.jdk.CollectionConverters.*
+import java.time.LocalDate
+import java.util.UUID
+import scala.reflect.ClassTag
+import scala.util.*
+
+// needed for BigDecimal params
+given cask.endpoints.QueryParamReader.SimpleParam[BigDecimal](BigDecimal.apply)
+
+// a parsed value from an HTTP request
+opaque type Parsed[A] = Either[String, A]
+
+object Parsed {
+ def apply[A](value: A): Parsed[A] = Right(value)
+
+ def eval[A](value: => A): Parsed[A] = Try(value) match {
+ case Failure(exp) => Left(s"Error: ${exp.getMessage}")
+ case Success(ok) => Right(ok)
+ }
+
+ def fromTry[A](value : Try[A]) = value match {
+ case Failure(err) => Left(err.getMessage)
+ case Success(ok) => Right(ok)
+ }
+
+ def fail[A](msg: String): Parsed[A] = Left(msg)
+
+ def optionalValue(map: Map[String, collection.Seq[String]], key: String): Parsed[Option[String]] = {
+ map.get(key) match {
+ case Some(Seq(only: String)) => Parsed(Option(only))
+ case Some(Seq()) => Parsed(None)
+ case Some(many) => Parsed.fail(s"${many.size} values set for '$key'")
+ case None => Parsed(None)
+ }
+ }
+
+ def singleValue(map: Map[String, collection.Seq[String]], key : String): Parsed[String] = {
+ map.get(key) match {
+ case Some(Seq(only : String)) => Parsed(only)
+ case Some(Seq()) => Parsed("")
+ case Some(many) => Parsed.fail(s"${many.size} values set for '$key'")
+ case None => Parsed.fail(s"required '$key' was not set")
+ }
+ }
+
+ def manyValues(map: Map[String, collection.Seq[String]], key : String, required: Boolean): Parsed[List[String]] = {
+ map.get(key) match {
+ case Some(many) => Parsed(many.toList)
+ case None if required => Parsed.fail(s"required '$key' was not set")
+ case None => Parsed(Nil)
+ }
+ }
+}
+
+extension[A] (parsed: Parsed[A]) {
+ def toEither: Either[String, A] = parsed
+
+ def asLong(using ev : A =:= String): Parsed[Long] = as[Long](_.toLongOption)
+ def asBoolean(using ev : A =:= String): Parsed[Boolean] = as[Boolean](_.toBooleanOption)
+ def asInt(using ev : A =:= String): Parsed[Int] = as[Int](_.toIntOption)
+ def asByte(using ev : A =:= String): Parsed[Byte] = as[Byte](_.toByteOption)
+ def asUuid(using ev : A =:= String): Parsed[UUID] = as[UUID](x => Try(UUID.fromString(x)).toOption)
+ def asFloat(using ev : A =:= String): Parsed[Float] = as[Float](_.toFloatOption)
+ def asDouble(using ev : A =:= String): Parsed[Double] = as[Double](_.toDoubleOption)
+ def asDate(using ev: A =:= String): Parsed[LocalDate] = as[LocalDate](x => Try(LocalDate.parse(x)).toOption)
+
+ private def as[B : ClassTag](f : String => Option[B])(using ev : A =:= String): Parsed[B] = parsed.flatMap { str =>
+ f(ev(str)) match {
+ case None => Parsed.fail(s"'$str' cannot be parsed as a ${implicitly[ClassTag[B]].runtimeClass}")
+ case Some(x) => Parsed(x)
+ }
+ }
+
+
+ def mapError(f : String => String) : Parsed[A] = parsed match {
+ case Left(msg) => Left(f(msg))
+ case right => right
+ }
+
+ def map[B](f: A => B): Parsed[B] = parsed match {
+ case Right(value) => Right(f(value))
+ case Left(err) => Left(err)
+ }
+ def flatMap[B](f : A => Parsed[B]): Parsed[B] = parsed match {
+ case Right(value) => f(value)
+ case Left(err) => Left(err)
+ }
+}
+
+
+extension (request: cask.Request) {
+
+ def formSingleValueRequired(paramName: String): Parsed[String] = {
+ val data = formDataForKey(paramName).map(_.getValue).toSeq
+ Parsed.singleValue(Map(paramName -> data), paramName)
+ }
+ def formSingleValueOptional(paramName: String): Parsed[Option[String]] = {
+ val data = formDataForKey(paramName).map(_.getValue).toSeq
+ Parsed.optionalValue(Map(paramName -> data), paramName)
+ }
+
+ def formValueAsFileOptional(paramName: String): Parsed[Option[File]] = {
+ val data = formDataForKey(paramName)
+ data.map(_.getFileItem.getFile.toFile).toSeq match {
+ case Seq() => Parsed(None)
+ case Seq(file) => Parsed(Option(file))
+ case many => Parsed.fail(s"${many.size} file values set for '$paramName'")
+ }
+ }
+
+ def formValueAsFileRequired(paramName: String): Parsed[File] = {
+ val data = formDataForKey(paramName)
+ data.map(_.getFileItem.getFile.toFile).toSeq match {
+ case Seq() => Parsed.fail(s"No file form data was submitted for '$paramName'. The submitted form keys were: ${formDataKeys.mkString(",")}")
+ case Seq(file) => Parsed(file)
+ case many => Parsed.fail(s"${many.size} file values set for '$paramName'")
+ }
+ }
+
+ def formManyValues(paramName: String, required: Boolean): Parsed[List[String]] = {
+ val data = formDataForKey(paramName).map(_.getValue).toSeq
+ Parsed.manyValues(Map(paramName -> data), paramName, required)
+ }
+
+ def formData: FormData = FormParserFactory.builder().build().createParser(request.exchange).parseBlocking()
+
+ def formDataKeys: Iterator[String] = formData.iterator().asScala
+
+ def formDataForKey(paramName: String): Iterable[FormData.FormValue] = formData.get(paramName).asScala
+
+ def headerSingleValueOptional(paramName: String): Parsed[Option[String]] = Parsed.optionalValue(request.headers, paramName)
+ def headerSingleValueRequired(paramName: String): Parsed[String] = Parsed.singleValue(request.headers, paramName)
+
+ def headerManyValues(paramName: String, required: Boolean): Parsed[List[String]] = Parsed.manyValues(request.headers, paramName, required)
+
+ def bodyAsString = new String(request.readAllBytes(), "UTF-8")
+
+ def pathValue(index: Int, paramName: String, required : Boolean): Parsed[String] = {
+ request
+ .remainingPathSegments
+ .lift(index) match {
+ case Some(value) => Right(value)
+ case None if required => Left(s"'$paramName'' is a required path parameter at path position $index")
+ case None => Right("")
+ }
+ }
+}
diff --git a/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/ApiResponse.scala b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/ApiResponse.scala
new file mode 100644
index 00000000000..ff5d8abaa14
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/ApiResponse.scala
@@ -0,0 +1,55 @@
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator.
+ *
+ * https://openapi-generator.tech
+ */
+
+// this model was generated using model.mustache
+package sample.cask.model
+import scala.util.control.NonFatal
+
+// see https://com-lihaoyi.github.io/upickle/
+import upickle.default.{ReadWriter => RW, macroRW}
+import upickle.default.*
+
+case class ApiResponse(
+ code: Option[Int] = None ,
+
+ `type`: Option[String] = None ,
+
+ message: Option[String] = None
+
+ ) {
+
+ def asJson: String = asData.asJson
+
+ def asData : ApiResponseData = {
+ ApiResponseData(
+ code = code.getOrElse(0),
+ `type` = `type`.getOrElse(""),
+ message = message.getOrElse("")
+ )
+ }
+
+}
+
+object ApiResponse{
+
+ given RW[ApiResponse] = ApiResponseData.readWriter.bimap[ApiResponse](_.asData, _.asModel)
+
+ enum Fields(fieldName : String) extends Field(fieldName) {
+ case code extends Fields("code")
+ case `type` extends Fields("`type`")
+ case message extends Fields("message")
+ }
+
+
+}
+
diff --git a/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/ApiResponseData.scala b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/ApiResponseData.scala
new file mode 100644
index 00000000000..b93b7eb44ef
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/ApiResponseData.scala
@@ -0,0 +1,171 @@
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator.
+ *
+ * https://openapi-generator.tech
+ */
+
+// this model was generated using modelData.mustache
+package sample.cask.model
+import scala.util.control.NonFatal
+import scala.util.*
+
+// see https://com-lihaoyi.github.io/upickle/
+import upickle.default.{ReadWriter => RW, macroRW}
+import upickle.default.*
+
+/** ApiResponseData a data transfer object, primarily for simple json serialisation.
+ * It has no validation - there may be nulls, values out of range, etc
+ */
+case class ApiResponseData(
+ code: Int = 0 ,
+
+ `type`: String = "" ,
+
+ message: String = ""
+
+ ) {
+
+ def asJson: String = write(this)
+
+ def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = {
+ val errors = scala.collection.mutable.ListBuffer[ValidationError]()
+ // ==================
+ // code
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ // ==================
+ // `type`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ // ==================
+ // message
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ errors.toSeq
+ }
+
+ def validated(failFast : Boolean = false) : scala.util.Try[ApiResponse] = {
+ validationErrors(Vector(), failFast) match {
+ case Seq() => Success(asModel)
+ case first +: theRest => Failure(ValidationErrors(first, theRest))
+ }
+ }
+
+ /** use 'validated' to check validation */
+ def asModel : ApiResponse = {
+ ApiResponse(
+ code = Option(
+ code
+ )
+ ,
+ `type` = Option(
+ `type`
+ )
+ ,
+ message = Option(
+ message
+ )
+
+ )
+ }
+}
+
+object ApiResponseData {
+
+ given readWriter : RW[ApiResponseData] = macroRW
+
+ def fromJsonString(jason : String) : ApiResponseData = try {
+ read[ApiResponseData](jason)
+ } catch {
+ case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e")
+ }
+
+ def manyFromJsonString(jason : String) : Seq[ApiResponseData] = try {
+ read[List[ApiResponseData]](jason)
+ } catch {
+ case NonFatal(e) => sys.error(s"Error parsing json '$jason' as list: $e")
+ }
+
+ def manyFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Seq[ApiResponse]] = {
+ Try(manyFromJsonString(jason)).flatMap { list =>
+ list.zipWithIndex.foldLeft(Try(Vector[ApiResponse]())) {
+ case (Success(list), (next, i)) =>
+ next.validated(failFast) match {
+ case Success(ok) => Success(list :+ ok)
+ case Failure(err) => Failure(new Exception(s"Validation error on element $i: ${err.getMessage}", err))
+ }
+ case (fail, _) => fail
+ }
+ }
+ }
+
+ def mapFromJsonString(jason : String) : Map[String, ApiResponseData] = try {
+ read[Map[String, ApiResponseData]](jason)
+ } catch {
+ case NonFatal(e) => sys.error(s"Error parsing json '$jason' as map: $e")
+ }
+
+
+ def mapFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Map[String, ApiResponse]] = {
+ Try(mapFromJsonString(jason)).flatMap { map =>
+ map.foldLeft(Try(Map[String, ApiResponse]())) {
+ case (Success(map), (key, next)) =>
+ next.validated(failFast) match {
+ case Success(ok) => Success(map.updated(key, ok))
+ case Failure(err) => Failure(new Exception(s"Validation error on element $key: ${err.getMessage}", err))
+ }
+ case (fail, _) => fail
+ }
+ }
+ }
+}
+
diff --git a/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/Category.scala b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/Category.scala
new file mode 100644
index 00000000000..d0bf01a2861
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/Category.scala
@@ -0,0 +1,51 @@
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator.
+ *
+ * https://openapi-generator.tech
+ */
+
+// this model was generated using model.mustache
+package sample.cask.model
+import scala.util.control.NonFatal
+
+// see https://com-lihaoyi.github.io/upickle/
+import upickle.default.{ReadWriter => RW, macroRW}
+import upickle.default.*
+
+case class Category(
+ id: Option[Long] = None ,
+
+ name: Option[String] = None
+
+ ) {
+
+ def asJson: String = asData.asJson
+
+ def asData : CategoryData = {
+ CategoryData(
+ id = id.getOrElse(0),
+ name = name.getOrElse("")
+ )
+ }
+
+}
+
+object Category{
+
+ given RW[Category] = CategoryData.readWriter.bimap[Category](_.asData, _.asModel)
+
+ enum Fields(fieldName : String) extends Field(fieldName) {
+ case id extends Fields("id")
+ case name extends Fields("name")
+ }
+
+
+}
+
diff --git a/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/CategoryData.scala b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/CategoryData.scala
new file mode 100644
index 00000000000..77a834683a8
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/CategoryData.scala
@@ -0,0 +1,153 @@
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator.
+ *
+ * https://openapi-generator.tech
+ */
+
+// this model was generated using modelData.mustache
+package sample.cask.model
+import scala.util.control.NonFatal
+import scala.util.*
+
+// see https://com-lihaoyi.github.io/upickle/
+import upickle.default.{ReadWriter => RW, macroRW}
+import upickle.default.*
+
+/** CategoryData a data transfer object, primarily for simple json serialisation.
+ * It has no validation - there may be nulls, values out of range, etc
+ */
+case class CategoryData(
+ id: Long = 0 ,
+
+ name: String = ""
+
+ ) {
+
+ def asJson: String = write(this)
+
+ def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = {
+ val errors = scala.collection.mutable.ListBuffer[ValidationError]()
+ // ==================
+ // id
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ // ==================
+ // name
+ // validate against pattern '^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$'
+ if (errors.isEmpty || !failFast) {
+ val regex = """^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$"""
+ if name == null || !regex.r.matches(name) then
+ errors += ValidationError(path :+ Category.Fields.name, s"value '$name' doesn't match pattern $regex")
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ errors.toSeq
+ }
+
+ def validated(failFast : Boolean = false) : scala.util.Try[Category] = {
+ validationErrors(Vector(), failFast) match {
+ case Seq() => Success(asModel)
+ case first +: theRest => Failure(ValidationErrors(first, theRest))
+ }
+ }
+
+ /** use 'validated' to check validation */
+ def asModel : Category = {
+ Category(
+ id = Option(
+ id
+ )
+ ,
+ name = Option(
+ name
+ )
+
+ )
+ }
+}
+
+object CategoryData {
+
+ given readWriter : RW[CategoryData] = macroRW
+
+ def fromJsonString(jason : String) : CategoryData = try {
+ read[CategoryData](jason)
+ } catch {
+ case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e")
+ }
+
+ def manyFromJsonString(jason : String) : Seq[CategoryData] = try {
+ read[List[CategoryData]](jason)
+ } catch {
+ case NonFatal(e) => sys.error(s"Error parsing json '$jason' as list: $e")
+ }
+
+ def manyFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Seq[Category]] = {
+ Try(manyFromJsonString(jason)).flatMap { list =>
+ list.zipWithIndex.foldLeft(Try(Vector[Category]())) {
+ case (Success(list), (next, i)) =>
+ next.validated(failFast) match {
+ case Success(ok) => Success(list :+ ok)
+ case Failure(err) => Failure(new Exception(s"Validation error on element $i: ${err.getMessage}", err))
+ }
+ case (fail, _) => fail
+ }
+ }
+ }
+
+ def mapFromJsonString(jason : String) : Map[String, CategoryData] = try {
+ read[Map[String, CategoryData]](jason)
+ } catch {
+ case NonFatal(e) => sys.error(s"Error parsing json '$jason' as map: $e")
+ }
+
+
+ def mapFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Map[String, Category]] = {
+ Try(mapFromJsonString(jason)).flatMap { map =>
+ map.foldLeft(Try(Map[String, Category]())) {
+ case (Success(map), (key, next)) =>
+ next.validated(failFast) match {
+ case Success(ok) => Success(map.updated(key, ok))
+ case Failure(err) => Failure(new Exception(s"Validation error on element $key: ${err.getMessage}", err))
+ }
+ case (fail, _) => fail
+ }
+ }
+ }
+}
+
diff --git a/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/Order.scala b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/Order.scala
new file mode 100644
index 00000000000..85bda97b815
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/Order.scala
@@ -0,0 +1,76 @@
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator.
+ *
+ * https://openapi-generator.tech
+ */
+
+// this model was generated using model.mustache
+package sample.cask.model
+import java.time.OffsetDateTime
+import scala.util.control.NonFatal
+
+// see https://com-lihaoyi.github.io/upickle/
+import upickle.default.{ReadWriter => RW, macroRW}
+import upickle.default.*
+
+case class Order(
+ id: Option[Long] = None ,
+
+ petId: Option[Long] = None ,
+
+ quantity: Option[Int] = None ,
+
+ shipDate: Option[OffsetDateTime] = None ,
+
+ /* Order Status */
+ status: Option[Order.StatusEnum] = None ,
+
+ complete: Option[Boolean] = None
+
+ ) {
+
+ def asJson: String = asData.asJson
+
+ def asData : OrderData = {
+ OrderData(
+ id = id.getOrElse(0),
+ petId = petId.getOrElse(0),
+ quantity = quantity.getOrElse(0),
+ shipDate = shipDate.getOrElse(null),
+ status = status.getOrElse(null),
+ complete = complete.getOrElse(false)
+ )
+ }
+
+}
+
+object Order{
+
+ given RW[Order] = OrderData.readWriter.bimap[Order](_.asData, _.asModel)
+
+ enum Fields(fieldName : String) extends Field(fieldName) {
+ case id extends Fields("id")
+ case petId extends Fields("petId")
+ case quantity extends Fields("quantity")
+ case shipDate extends Fields("shipDate")
+ case status extends Fields("status")
+ case complete extends Fields("complete")
+ }
+
+ // baseName=status
+ // nameInCamelCase = status
+ enum StatusEnum derives ReadWriter {
+ case placed
+ case approved
+ case delivered
+ }
+
+}
+
diff --git a/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/OrderData.scala b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/OrderData.scala
new file mode 100644
index 00000000000..0de58a7f0b3
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/OrderData.scala
@@ -0,0 +1,245 @@
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator.
+ *
+ * https://openapi-generator.tech
+ */
+
+// this model was generated using modelData.mustache
+package sample.cask.model
+import java.time.OffsetDateTime
+import scala.util.control.NonFatal
+import scala.util.*
+
+// see https://com-lihaoyi.github.io/upickle/
+import upickle.default.{ReadWriter => RW, macroRW}
+import upickle.default.*
+
+/** OrderData a data transfer object, primarily for simple json serialisation.
+ * It has no validation - there may be nulls, values out of range, etc
+ */
+case class OrderData(
+ id: Long = 0 ,
+
+ petId: Long = 0 ,
+
+ quantity: Int = 0 ,
+
+ shipDate: OffsetDateTime = null ,
+
+ /* Order Status */
+ status: Order.StatusEnum = null ,
+
+ complete: Boolean = false
+
+ ) {
+
+ def asJson: String = write(this)
+
+ def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = {
+ val errors = scala.collection.mutable.ListBuffer[ValidationError]()
+ // ==================
+ // id
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ // ==================
+ // petId
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ // ==================
+ // quantity
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ // ==================
+ // shipDate
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ // ==================
+ // status
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ // ==================
+ // complete
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ errors.toSeq
+ }
+
+ def validated(failFast : Boolean = false) : scala.util.Try[Order] = {
+ validationErrors(Vector(), failFast) match {
+ case Seq() => Success(asModel)
+ case first +: theRest => Failure(ValidationErrors(first, theRest))
+ }
+ }
+
+ /** use 'validated' to check validation */
+ def asModel : Order = {
+ Order(
+ id = Option(
+ id
+ )
+ ,
+ petId = Option(
+ petId
+ )
+ ,
+ quantity = Option(
+ quantity
+ )
+ ,
+ shipDate = Option(
+ shipDate
+ )
+ ,
+ status = Option(
+ status
+ )
+ ,
+ complete = Option(
+ complete
+ )
+
+ )
+ }
+}
+
+object OrderData {
+
+ given readWriter : RW[OrderData] = macroRW
+
+ def fromJsonString(jason : String) : OrderData = try {
+ read[OrderData](jason)
+ } catch {
+ case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e")
+ }
+
+ def manyFromJsonString(jason : String) : Seq[OrderData] = try {
+ read[List[OrderData]](jason)
+ } catch {
+ case NonFatal(e) => sys.error(s"Error parsing json '$jason' as list: $e")
+ }
+
+ def manyFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Seq[Order]] = {
+ Try(manyFromJsonString(jason)).flatMap { list =>
+ list.zipWithIndex.foldLeft(Try(Vector[Order]())) {
+ case (Success(list), (next, i)) =>
+ next.validated(failFast) match {
+ case Success(ok) => Success(list :+ ok)
+ case Failure(err) => Failure(new Exception(s"Validation error on element $i: ${err.getMessage}", err))
+ }
+ case (fail, _) => fail
+ }
+ }
+ }
+
+ def mapFromJsonString(jason : String) : Map[String, OrderData] = try {
+ read[Map[String, OrderData]](jason)
+ } catch {
+ case NonFatal(e) => sys.error(s"Error parsing json '$jason' as map: $e")
+ }
+
+
+ def mapFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Map[String, Order]] = {
+ Try(mapFromJsonString(jason)).flatMap { map =>
+ map.foldLeft(Try(Map[String, Order]())) {
+ case (Success(map), (key, next)) =>
+ next.validated(failFast) match {
+ case Success(ok) => Success(map.updated(key, ok))
+ case Failure(err) => Failure(new Exception(s"Validation error on element $key: ${err.getMessage}", err))
+ }
+ case (fail, _) => fail
+ }
+ }
+ }
+}
+
diff --git a/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/Pet.scala b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/Pet.scala
new file mode 100644
index 00000000000..068f593303c
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/Pet.scala
@@ -0,0 +1,77 @@
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator.
+ *
+ * https://openapi-generator.tech
+ */
+
+// this model was generated using model.mustache
+package sample.cask.model
+import sample.cask.model.Category
+import sample.cask.model.Tag
+import scala.util.control.NonFatal
+
+// see https://com-lihaoyi.github.io/upickle/
+import upickle.default.{ReadWriter => RW, macroRW}
+import upickle.default.*
+
+case class Pet(
+ id: Option[Long] = None ,
+
+ category: Option[Category] = None ,
+
+ name: String,
+
+ photoUrls: Seq[String],
+
+ tags: Seq[Tag] = Nil ,
+
+ /* pet status in the store */
+ status: Option[Pet.StatusEnum] = None
+
+ ) {
+
+ def asJson: String = asData.asJson
+
+ def asData : PetData = {
+ PetData(
+ id = id.getOrElse(0),
+ category = category.map(_.asData).getOrElse(null),
+ name = name,
+ photoUrls = photoUrls,
+ tags = tags.map(_.asData),
+ status = status.getOrElse(null)
+ )
+ }
+
+}
+
+object Pet{
+
+ given RW[Pet] = PetData.readWriter.bimap[Pet](_.asData, _.asModel)
+
+ enum Fields(fieldName : String) extends Field(fieldName) {
+ case id extends Fields("id")
+ case category extends Fields("category")
+ case name extends Fields("name")
+ case photoUrls extends Fields("photoUrls")
+ case tags extends Fields("tags")
+ case status extends Fields("status")
+ }
+
+ // baseName=status
+ // nameInCamelCase = status
+ enum StatusEnum derives ReadWriter {
+ case available
+ case pending
+ case sold
+ }
+
+}
+
diff --git a/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/PetData.scala b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/PetData.scala
new file mode 100644
index 00000000000..db6a3e1a512
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/PetData.scala
@@ -0,0 +1,262 @@
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator.
+ *
+ * https://openapi-generator.tech
+ */
+
+// this model was generated using modelData.mustache
+package sample.cask.model
+import sample.cask.model.Category
+import sample.cask.model.Tag
+import scala.util.control.NonFatal
+import scala.util.*
+
+// see https://com-lihaoyi.github.io/upickle/
+import upickle.default.{ReadWriter => RW, macroRW}
+import upickle.default.*
+
+/** PetData a data transfer object, primarily for simple json serialisation.
+ * It has no validation - there may be nulls, values out of range, etc
+ */
+case class PetData(
+ id: Long = 0 ,
+
+ category: CategoryData = null ,
+
+ name: String,
+
+ photoUrls: Seq[String],
+
+ tags: Seq[TagData] = Nil ,
+
+ /* pet status in the store */
+ status: Pet.StatusEnum = null
+
+ ) {
+
+ def asJson: String = write(this)
+
+ def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = {
+ val errors = scala.collection.mutable.ListBuffer[ValidationError]()
+ // ==================
+ // id
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ // ==================
+ // category
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ // validating category
+ if (errors.isEmpty || !failFast) {
+ if category != null then errors ++= category.validationErrors(path :+ Pet.Fields.category, failFast)
+ }
+
+ // ==================
+ // name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ // ==================
+ // photoUrls
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ // ==================
+ // tags
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ if (errors.isEmpty || !failFast) {
+ if (tags != null) {
+ tags.zipWithIndex.foreach {
+ case (value, i) if errors.isEmpty || !failFast =>
+ errors ++= value.validationErrors(
+ path :+ Pet.Fields.tags :+ Field(i.toString),
+ failFast)
+ case (value, i) =>
+ }
+ }
+ }
+
+
+ // ==================
+ // status
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ errors.toSeq
+ }
+
+ def validated(failFast : Boolean = false) : scala.util.Try[Pet] = {
+ validationErrors(Vector(), failFast) match {
+ case Seq() => Success(asModel)
+ case first +: theRest => Failure(ValidationErrors(first, theRest))
+ }
+ }
+
+ /** use 'validated' to check validation */
+ def asModel : Pet = {
+ Pet(
+ id = Option(
+ id
+ )
+ ,
+ category = Option(
+ category
+ )
+ .map(_.asModel),
+ name =
+ name
+
+ ,
+ photoUrls =
+ photoUrls
+
+ ,
+ tags =
+ tags
+
+ .map(_.asModel),
+ status = Option(
+ status
+ )
+
+ )
+ }
+}
+
+object PetData {
+
+ given readWriter : RW[PetData] = macroRW
+
+ def fromJsonString(jason : String) : PetData = try {
+ read[PetData](jason)
+ } catch {
+ case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e")
+ }
+
+ def manyFromJsonString(jason : String) : Seq[PetData] = try {
+ read[List[PetData]](jason)
+ } catch {
+ case NonFatal(e) => sys.error(s"Error parsing json '$jason' as list: $e")
+ }
+
+ def manyFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Seq[Pet]] = {
+ Try(manyFromJsonString(jason)).flatMap { list =>
+ list.zipWithIndex.foldLeft(Try(Vector[Pet]())) {
+ case (Success(list), (next, i)) =>
+ next.validated(failFast) match {
+ case Success(ok) => Success(list :+ ok)
+ case Failure(err) => Failure(new Exception(s"Validation error on element $i: ${err.getMessage}", err))
+ }
+ case (fail, _) => fail
+ }
+ }
+ }
+
+ def mapFromJsonString(jason : String) : Map[String, PetData] = try {
+ read[Map[String, PetData]](jason)
+ } catch {
+ case NonFatal(e) => sys.error(s"Error parsing json '$jason' as map: $e")
+ }
+
+
+ def mapFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Map[String, Pet]] = {
+ Try(mapFromJsonString(jason)).flatMap { map =>
+ map.foldLeft(Try(Map[String, Pet]())) {
+ case (Success(map), (key, next)) =>
+ next.validated(failFast) match {
+ case Success(ok) => Success(map.updated(key, ok))
+ case Failure(err) => Failure(new Exception(s"Validation error on element $key: ${err.getMessage}", err))
+ }
+ case (fail, _) => fail
+ }
+ }
+ }
+}
+
diff --git a/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/Tag.scala b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/Tag.scala
new file mode 100644
index 00000000000..a8bd2a35866
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/Tag.scala
@@ -0,0 +1,51 @@
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator.
+ *
+ * https://openapi-generator.tech
+ */
+
+// this model was generated using model.mustache
+package sample.cask.model
+import scala.util.control.NonFatal
+
+// see https://com-lihaoyi.github.io/upickle/
+import upickle.default.{ReadWriter => RW, macroRW}
+import upickle.default.*
+
+case class Tag(
+ id: Option[Long] = None ,
+
+ name: Option[String] = None
+
+ ) {
+
+ def asJson: String = asData.asJson
+
+ def asData : TagData = {
+ TagData(
+ id = id.getOrElse(0),
+ name = name.getOrElse("")
+ )
+ }
+
+}
+
+object Tag{
+
+ given RW[Tag] = TagData.readWriter.bimap[Tag](_.asData, _.asModel)
+
+ enum Fields(fieldName : String) extends Field(fieldName) {
+ case id extends Fields("id")
+ case name extends Fields("name")
+ }
+
+
+}
+
diff --git a/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/TagData.scala b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/TagData.scala
new file mode 100644
index 00000000000..e8c66334bcb
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/TagData.scala
@@ -0,0 +1,147 @@
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator.
+ *
+ * https://openapi-generator.tech
+ */
+
+// this model was generated using modelData.mustache
+package sample.cask.model
+import scala.util.control.NonFatal
+import scala.util.*
+
+// see https://com-lihaoyi.github.io/upickle/
+import upickle.default.{ReadWriter => RW, macroRW}
+import upickle.default.*
+
+/** TagData a data transfer object, primarily for simple json serialisation.
+ * It has no validation - there may be nulls, values out of range, etc
+ */
+case class TagData(
+ id: Long = 0 ,
+
+ name: String = ""
+
+ ) {
+
+ def asJson: String = write(this)
+
+ def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = {
+ val errors = scala.collection.mutable.ListBuffer[ValidationError]()
+ // ==================
+ // id
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ // ==================
+ // name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ errors.toSeq
+ }
+
+ def validated(failFast : Boolean = false) : scala.util.Try[Tag] = {
+ validationErrors(Vector(), failFast) match {
+ case Seq() => Success(asModel)
+ case first +: theRest => Failure(ValidationErrors(first, theRest))
+ }
+ }
+
+ /** use 'validated' to check validation */
+ def asModel : Tag = {
+ Tag(
+ id = Option(
+ id
+ )
+ ,
+ name = Option(
+ name
+ )
+
+ )
+ }
+}
+
+object TagData {
+
+ given readWriter : RW[TagData] = macroRW
+
+ def fromJsonString(jason : String) : TagData = try {
+ read[TagData](jason)
+ } catch {
+ case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e")
+ }
+
+ def manyFromJsonString(jason : String) : Seq[TagData] = try {
+ read[List[TagData]](jason)
+ } catch {
+ case NonFatal(e) => sys.error(s"Error parsing json '$jason' as list: $e")
+ }
+
+ def manyFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Seq[Tag]] = {
+ Try(manyFromJsonString(jason)).flatMap { list =>
+ list.zipWithIndex.foldLeft(Try(Vector[Tag]())) {
+ case (Success(list), (next, i)) =>
+ next.validated(failFast) match {
+ case Success(ok) => Success(list :+ ok)
+ case Failure(err) => Failure(new Exception(s"Validation error on element $i: ${err.getMessage}", err))
+ }
+ case (fail, _) => fail
+ }
+ }
+ }
+
+ def mapFromJsonString(jason : String) : Map[String, TagData] = try {
+ read[Map[String, TagData]](jason)
+ } catch {
+ case NonFatal(e) => sys.error(s"Error parsing json '$jason' as map: $e")
+ }
+
+
+ def mapFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Map[String, Tag]] = {
+ Try(mapFromJsonString(jason)).flatMap { map =>
+ map.foldLeft(Try(Map[String, Tag]())) {
+ case (Success(map), (key, next)) =>
+ next.validated(failFast) match {
+ case Success(ok) => Success(map.updated(key, ok))
+ case Failure(err) => Failure(new Exception(s"Validation error on element $key: ${err.getMessage}", err))
+ }
+ case (fail, _) => fail
+ }
+ }
+ }
+}
+
diff --git a/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/User.scala b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/User.scala
new file mode 100644
index 00000000000..286cdb3b652
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/User.scala
@@ -0,0 +1,76 @@
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator.
+ *
+ * https://openapi-generator.tech
+ */
+
+// this model was generated using model.mustache
+package sample.cask.model
+import scala.util.control.NonFatal
+
+// see https://com-lihaoyi.github.io/upickle/
+import upickle.default.{ReadWriter => RW, macroRW}
+import upickle.default.*
+
+case class User(
+ id: Option[Long] = None ,
+
+ username: Option[String] = None ,
+
+ firstName: Option[String] = None ,
+
+ lastName: Option[String] = None ,
+
+ email: Option[String] = None ,
+
+ password: Option[String] = None ,
+
+ phone: Option[String] = None ,
+
+ /* User Status */
+ userStatus: Option[Int] = None
+
+ ) {
+
+ def asJson: String = asData.asJson
+
+ def asData : UserData = {
+ UserData(
+ id = id.getOrElse(0),
+ username = username.getOrElse(""),
+ firstName = firstName.getOrElse(""),
+ lastName = lastName.getOrElse(""),
+ email = email.getOrElse(""),
+ password = password.getOrElse(""),
+ phone = phone.getOrElse(""),
+ userStatus = userStatus.getOrElse(0)
+ )
+ }
+
+}
+
+object User{
+
+ given RW[User] = UserData.readWriter.bimap[User](_.asData, _.asModel)
+
+ enum Fields(fieldName : String) extends Field(fieldName) {
+ case id extends Fields("id")
+ case username extends Fields("username")
+ case firstName extends Fields("firstName")
+ case lastName extends Fields("lastName")
+ case email extends Fields("email")
+ case password extends Fields("password")
+ case phone extends Fields("phone")
+ case userStatus extends Fields("userStatus")
+ }
+
+
+}
+
diff --git a/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/UserData.scala b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/UserData.scala
new file mode 100644
index 00000000000..8b8ca7908ab
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/UserData.scala
@@ -0,0 +1,292 @@
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator.
+ *
+ * https://openapi-generator.tech
+ */
+
+// this model was generated using modelData.mustache
+package sample.cask.model
+import scala.util.control.NonFatal
+import scala.util.*
+
+// see https://com-lihaoyi.github.io/upickle/
+import upickle.default.{ReadWriter => RW, macroRW}
+import upickle.default.*
+
+/** UserData a data transfer object, primarily for simple json serialisation.
+ * It has no validation - there may be nulls, values out of range, etc
+ */
+case class UserData(
+ id: Long = 0 ,
+
+ username: String = "" ,
+
+ firstName: String = "" ,
+
+ lastName: String = "" ,
+
+ email: String = "" ,
+
+ password: String = "" ,
+
+ phone: String = "" ,
+
+ /* User Status */
+ userStatus: Int = 0
+
+ ) {
+
+ def asJson: String = write(this)
+
+ def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = {
+ val errors = scala.collection.mutable.ListBuffer[ValidationError]()
+ // ==================
+ // id
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ // ==================
+ // username
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ // ==================
+ // firstName
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ // ==================
+ // lastName
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ // ==================
+ // email
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ // ==================
+ // password
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ // ==================
+ // phone
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ // ==================
+ // userStatus
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ errors.toSeq
+ }
+
+ def validated(failFast : Boolean = false) : scala.util.Try[User] = {
+ validationErrors(Vector(), failFast) match {
+ case Seq() => Success(asModel)
+ case first +: theRest => Failure(ValidationErrors(first, theRest))
+ }
+ }
+
+ /** use 'validated' to check validation */
+ def asModel : User = {
+ User(
+ id = Option(
+ id
+ )
+ ,
+ username = Option(
+ username
+ )
+ ,
+ firstName = Option(
+ firstName
+ )
+ ,
+ lastName = Option(
+ lastName
+ )
+ ,
+ email = Option(
+ email
+ )
+ ,
+ password = Option(
+ password
+ )
+ ,
+ phone = Option(
+ phone
+ )
+ ,
+ userStatus = Option(
+ userStatus
+ )
+
+ )
+ }
+}
+
+object UserData {
+
+ given readWriter : RW[UserData] = macroRW
+
+ def fromJsonString(jason : String) : UserData = try {
+ read[UserData](jason)
+ } catch {
+ case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e")
+ }
+
+ def manyFromJsonString(jason : String) : Seq[UserData] = try {
+ read[List[UserData]](jason)
+ } catch {
+ case NonFatal(e) => sys.error(s"Error parsing json '$jason' as list: $e")
+ }
+
+ def manyFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Seq[User]] = {
+ Try(manyFromJsonString(jason)).flatMap { list =>
+ list.zipWithIndex.foldLeft(Try(Vector[User]())) {
+ case (Success(list), (next, i)) =>
+ next.validated(failFast) match {
+ case Success(ok) => Success(list :+ ok)
+ case Failure(err) => Failure(new Exception(s"Validation error on element $i: ${err.getMessage}", err))
+ }
+ case (fail, _) => fail
+ }
+ }
+ }
+
+ def mapFromJsonString(jason : String) : Map[String, UserData] = try {
+ read[Map[String, UserData]](jason)
+ } catch {
+ case NonFatal(e) => sys.error(s"Error parsing json '$jason' as map: $e")
+ }
+
+
+ def mapFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Map[String, User]] = {
+ Try(mapFromJsonString(jason)).flatMap { map =>
+ map.foldLeft(Try(Map[String, User]())) {
+ case (Success(map), (key, next)) =>
+ next.validated(failFast) match {
+ case Success(ok) => Success(map.updated(key, ok))
+ case Failure(err) => Failure(new Exception(s"Validation error on element $key: ${err.getMessage}", err))
+ }
+ case (fail, _) => fail
+ }
+ }
+ }
+}
+
diff --git a/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/package.scala b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/package.scala
new file mode 100644
index 00000000000..b0c893da671
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/model/package.scala
@@ -0,0 +1,65 @@
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator.
+ *
+ * https://openapi-generator.tech
+ */
+
+package sample.cask.model
+
+// model package
+import upickle.default._
+import java.time.*
+import java.time.format.DateTimeFormatter
+
+/**
+ * This base class lets us refer to fields in exceptions
+ */
+class Field(val name : String)
+
+final case class ValidationErrors(
+ first: ValidationError,
+ remaining: Seq[ValidationError],
+ message: String
+) extends Exception(message)
+
+object ValidationErrors {
+ def apply(first: ValidationError, remaining: Seq[ValidationError]) = {
+ val noun = if remaining.isEmpty then "error" else "errors"
+ new ValidationErrors(
+ first,
+ remaining,
+ remaining.mkString(s"${remaining.size + 1} $noun found: ${first}", "\n\t", "")
+ )
+ }
+}
+
+
+final case class ValidationError(path : Seq[Field], message : String) extends Exception(message) {
+ override def toString = s"ValidationError for ${path.mkString(".")}: $message"
+}
+
+given ReadWriter[ZonedDateTime] = readwriter[String].bimap[ZonedDateTime](
+ zonedDateTime => DateTimeFormatter.ISO_INSTANT.format(zonedDateTime),
+ str => ZonedDateTime.parse(str, DateTimeFormatter.ISO_INSTANT))
+
+given ReadWriter[LocalDateTime] = readwriter[String].bimap[LocalDateTime](
+ zonedDateTime => DateTimeFormatter.ISO_INSTANT.format(zonedDateTime),
+ str => LocalDateTime.parse(str, DateTimeFormatter.ISO_INSTANT))
+
+given ReadWriter[LocalDate] = readwriter[String].bimap[LocalDate](
+ zonedDateTime => DateTimeFormatter.ISO_INSTANT.format(zonedDateTime),
+ str => LocalDate.parse(str, DateTimeFormatter.ISO_INSTANT))
+
+given ReadWriter[OffsetDateTime] = readwriter[String].bimap[OffsetDateTime](
+ zonedDateTime => DateTimeFormatter.ISO_INSTANT.format(zonedDateTime),
+ str => scala.util.Try(OffsetDateTime.parse(str, DateTimeFormatter.ISO_DATE_TIME)).getOrElse(
+ OffsetDateTime.parse(str, DateTimeFormatter.ISO_INSTANT)
+ )
+)
\ No newline at end of file
diff --git a/samples/server/petstore/scala-cask/src/main/scala/sample/cask/package.scala b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/package.scala
new file mode 100644
index 00000000000..f4c86e310e0
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/main/scala/sample/cask/package.scala
@@ -0,0 +1,24 @@
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator.
+ *
+ * https://openapi-generator.tech
+ */
+
+package cask.groupId.server
+
+def box(str: String): String = {
+ val lines = str.linesIterator.toList
+ val maxLen = (0 +: lines.map(_.length)).max
+ val boxed = lines.map { line =>
+ s" | ${line.padTo(maxLen, ' ')} |"
+ }
+ val bar = " +-" + ("-" * maxLen) + "-+"
+ (bar +: boxed :+ bar).mkString("\n")
+}
diff --git a/samples/server/petstore/scala-cask/src/test/scala/sample/cask/model/ApiResponseTest.scala b/samples/server/petstore/scala-cask/src/test/scala/sample/cask/model/ApiResponseTest.scala
new file mode 100644
index 00000000000..18906d1f384
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/test/scala/sample/cask/model/ApiResponseTest.scala
@@ -0,0 +1,33 @@
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ */
+
+// this model was generated using modelTest.mustache
+package sample.cask.model
+
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+import scala.util.*
+
+class ApiResponseTest extends AnyWordSpec with Matchers {
+
+ "ApiResponse.fromJson" should {
+ """not parse invalid json""" in {
+ val Failure(err) = Try(ApiResponseData.fromJsonString("invalid jason"))
+ err.getMessage should startWith ("Error parsing json 'invalid jason'")
+ }
+ """parse """ ignore {
+ val Failure(err : ValidationErrors) = ApiResponseData.fromJsonString("""""").validated()
+
+ sys.error("TODO")
+ }
+ }
+
+}
diff --git a/samples/server/petstore/scala-cask/src/test/scala/sample/cask/model/CategoryTest.scala b/samples/server/petstore/scala-cask/src/test/scala/sample/cask/model/CategoryTest.scala
new file mode 100644
index 00000000000..a2d9d6c6251
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/test/scala/sample/cask/model/CategoryTest.scala
@@ -0,0 +1,33 @@
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ */
+
+// this model was generated using modelTest.mustache
+package sample.cask.model
+
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+import scala.util.*
+
+class CategoryTest extends AnyWordSpec with Matchers {
+
+ "Category.fromJson" should {
+ """not parse invalid json""" in {
+ val Failure(err) = Try(CategoryData.fromJsonString("invalid jason"))
+ err.getMessage should startWith ("Error parsing json 'invalid jason'")
+ }
+ """parse """ ignore {
+ val Failure(err : ValidationErrors) = CategoryData.fromJsonString("""""").validated()
+
+ sys.error("TODO")
+ }
+ }
+
+}
diff --git a/samples/server/petstore/scala-cask/src/test/scala/sample/cask/model/OrderTest.scala b/samples/server/petstore/scala-cask/src/test/scala/sample/cask/model/OrderTest.scala
new file mode 100644
index 00000000000..3f536010113
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/test/scala/sample/cask/model/OrderTest.scala
@@ -0,0 +1,34 @@
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ */
+
+// this model was generated using modelTest.mustache
+package sample.cask.model
+import java.time.OffsetDateTime
+
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+import scala.util.*
+
+class OrderTest extends AnyWordSpec with Matchers {
+
+ "Order.fromJson" should {
+ """not parse invalid json""" in {
+ val Failure(err) = Try(OrderData.fromJsonString("invalid jason"))
+ err.getMessage should startWith ("Error parsing json 'invalid jason'")
+ }
+ """parse """ ignore {
+ val Failure(err : ValidationErrors) = OrderData.fromJsonString("""""").validated()
+
+ sys.error("TODO")
+ }
+ }
+
+}
diff --git a/samples/server/petstore/scala-cask/src/test/scala/sample/cask/model/PetTest.scala b/samples/server/petstore/scala-cask/src/test/scala/sample/cask/model/PetTest.scala
new file mode 100644
index 00000000000..1ab24f2e926
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/test/scala/sample/cask/model/PetTest.scala
@@ -0,0 +1,35 @@
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ */
+
+// this model was generated using modelTest.mustache
+package sample.cask.model
+import sample.cask.model.Category
+import sample.cask.model.Tag
+
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+import scala.util.*
+
+class PetTest extends AnyWordSpec with Matchers {
+
+ "Pet.fromJson" should {
+ """not parse invalid json""" in {
+ val Failure(err) = Try(PetData.fromJsonString("invalid jason"))
+ err.getMessage should startWith ("Error parsing json 'invalid jason'")
+ }
+ """parse """ ignore {
+ val Failure(err : ValidationErrors) = PetData.fromJsonString("""""").validated()
+
+ sys.error("TODO")
+ }
+ }
+
+}
diff --git a/samples/server/petstore/scala-cask/src/test/scala/sample/cask/model/TagTest.scala b/samples/server/petstore/scala-cask/src/test/scala/sample/cask/model/TagTest.scala
new file mode 100644
index 00000000000..664e43e63c9
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/test/scala/sample/cask/model/TagTest.scala
@@ -0,0 +1,33 @@
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ */
+
+// this model was generated using modelTest.mustache
+package sample.cask.model
+
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+import scala.util.*
+
+class TagTest extends AnyWordSpec with Matchers {
+
+ "Tag.fromJson" should {
+ """not parse invalid json""" in {
+ val Failure(err) = Try(TagData.fromJsonString("invalid jason"))
+ err.getMessage should startWith ("Error parsing json 'invalid jason'")
+ }
+ """parse """ ignore {
+ val Failure(err : ValidationErrors) = TagData.fromJsonString("""""").validated()
+
+ sys.error("TODO")
+ }
+ }
+
+}
diff --git a/samples/server/petstore/scala-cask/src/test/scala/sample/cask/model/UserTest.scala b/samples/server/petstore/scala-cask/src/test/scala/sample/cask/model/UserTest.scala
new file mode 100644
index 00000000000..622fad87b0d
--- /dev/null
+++ b/samples/server/petstore/scala-cask/src/test/scala/sample/cask/model/UserTest.scala
@@ -0,0 +1,33 @@
+/**
+ * OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * OpenAPI spec version: 1.0.0
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ */
+
+// this model was generated using modelTest.mustache
+package sample.cask.model
+
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+import scala.util.*
+
+class UserTest extends AnyWordSpec with Matchers {
+
+ "User.fromJson" should {
+ """not parse invalid json""" in {
+ val Failure(err) = Try(UserData.fromJsonString("invalid jason"))
+ err.getMessage should startWith ("Error parsing json 'invalid jason'")
+ }
+ """parse """ ignore {
+ val Failure(err : ValidationErrors) = UserData.fromJsonString("""""").validated()
+
+ sys.error("TODO")
+ }
+ }
+
+}