diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaCaskServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaCaskServerCodegen.java index e5a56b02fed..1ac6e0a3fea 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaCaskServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaCaskServerCodegen.java @@ -31,6 +31,8 @@ import org.slf4j.LoggerFactory; import java.io.File; import java.nio.charset.StandardCharsets; import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -125,7 +127,7 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code // mapped to String as a workaround typeMapping.put("binary", "String"); - typeMapping.put("object", "Value"); + typeMapping.put("object", AdditionalPropertiesType); cliOptions.add(new CliOption(CodegenConstants.GROUP_ID, CodegenConstants.GROUP_ID_DESC)); cliOptions.add(new CliOption(CodegenConstants.ARTIFACT_ID, CodegenConstants.ARTIFACT_ID_DESC)); @@ -139,7 +141,7 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code public String toDefaultValue(Schema p) { if (ModelUtils.isMapSchema(p)) { String inner = getSchemaType(ModelUtils.getAdditionalProperties(p)); - return "Map[String, " + inner + "]() "; + return "Map[String, " + inner + "]()"; } else if (ModelUtils.isFreeFormObject(p, openAPI)) { // We're opinionated in this template to use ujson return "ujson.Null"; @@ -150,8 +152,12 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code @Override public String getSchemaType(Schema p) { if (ModelUtils.isFreeFormObject(p, openAPI)) { + if (ModelUtils.isMapSchema(p)) { + final String inner = getSchemaType(ModelUtils.getAdditionalProperties(p)); + return "Map[String, " + inner + "]"; + } // We're opinionated in this template to use ujson - return "Value"; + return AdditionalPropertiesType; } return super.getSchemaType(p); } @@ -246,7 +252,6 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code importMapping.put("LocalDate", "java.time.LocalDate"); importMapping.put("OffsetDateTime", "java.time.OffsetDateTime"); importMapping.put("LocalTime", "java.time.LocalTime"); - importMapping.put("Value", "ujson.Value"); importMapping.put(AdditionalPropertiesType, "ujson.Value"); } @@ -269,19 +274,6 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code } } - - static String formatMap(Map map) { - StringBuilder mapAsString = new StringBuilder("{"); - for (Object key : map.keySet().stream().sorted().collect(Collectors.toList())) { - mapAsString.append(key + " -- " + map.get(key) + ",\n"); - } - if (mapAsString.length() > 1) { - mapAsString.delete(mapAsString.length() - 2, mapAsString.length()); - } - mapAsString.append("}"); - return mapAsString.toString(); - } - @Override public String toApiName(String name) { if (name.isEmpty()) { @@ -316,8 +308,7 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code @Override public String apiFileFolder() { - final String folder = outputFolder + "/jvm/" + sourceFolder + "/" + apiPackage().replace('.', File.separatorChar); - return folder; + return outputFolder + "/jvm/" + sourceFolder + "/" + apiPackage().replace('.', File.separatorChar); } @Override @@ -329,6 +320,22 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code return outputFolder + "/shared/" + sourceFolder + "/" + apiPackage().replace('.', File.separatorChar); } + static String asMethod(final String input) { + // Remove all non-alphanumeric characters using regex + String alphanumeric = input.replaceAll("[^a-zA-Z0-9]", ""); + + // Ensure the method name doesn't start with a digit + if (alphanumeric.isEmpty()) { + throw new IllegalArgumentException("Input string does not contain any valid alphanumeric characters"); + } + + if (Character.isDigit(alphanumeric.charAt(0))) { + alphanumeric = "_" + alphanumeric; + } + + return alphanumeric; + } + static String capitalise(String p) { if (p.length() < 2) { return p.toUpperCase(Locale.ROOT); @@ -470,14 +477,24 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code * * @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; + public Collection getGroupQueryParams() { + + // wow is this a pain to do in Java ... I just wanted to have distinct items of a list + // based on their name. + Set alreadySeen = new HashSet<>(); + + List list = operations.stream().flatMap(op -> op.queryParams.stream()).flatMap(p -> { + if (alreadySeen.add(p.paramName)) { + 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 Arrays.asList(copy).stream(); + } else { + final List empty = Collections.emptyList(); + return empty.stream(); + } } ).collect(Collectors.toList()); @@ -498,7 +515,7 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code List stripped = Arrays.stream(pathPrefix.split("/", -1)) .map(ScalaCaskServerCodegen::capitalise).collect(Collectors.toList()); - methodName = "routeWorkAroundFor" + capitalise(httpMethod) + String.join("", stripped); + methodName = asMethod("routeWorkAroundFor" + capitalise(httpMethod) + String.join("", stripped)); } public void add(CodegenOperation op) { @@ -552,6 +569,7 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code 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); @@ -603,6 +621,25 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code final Map operations = (Map) objs.get("operations"); final List operationList = (List) operations.get("operation"); + + /** + * In this case, there is a import set to 'null': + * + * {{{ + * ... + * responses: + * "200": + * content: + * application/json: + * schema: + * format: byte + * type: string + * }}} + */ + List> imports = (List>) objs.get("imports"); + var filtered = imports.stream().filter(entry -> entry.get("import") != null).collect(Collectors.toList()); + objs.put("imports", filtered); + objs.put("route-groups", createRouteGroups(operationList)); operationList.forEach(ScalaCaskServerCodegen::postProcessOperation); @@ -611,6 +648,7 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code @Override public ModelsMap postProcessModels(ModelsMap objs) { + super.postProcessModels(objs); objs.getModels().stream().map(ModelMap::getModel).forEach(this::postProcessModel); return objs; } @@ -632,8 +670,8 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code model.getAllVars().forEach(this::setDefaultValueForCodegenProperty); model.getVars().forEach(this::setDefaultValueForCodegenProperty); - model.getVars().forEach(ScalaCaskServerCodegen::postProcessProperty); - model.getAllVars().forEach(ScalaCaskServerCodegen::postProcessProperty); + model.getVars().forEach(this::postProcessProperty); + model.getAllVars().forEach(this::postProcessProperty); } private static void postProcessOperation(CodegenOperation op) { @@ -643,6 +681,7 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code /* 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.vendorExtensions.put("x-handlerName", "on" + capitalise(op.operationId)); op.bodyParams.stream().filter((b) -> b.isBodyParam).forEach((p) -> { p.vendorExtensions.put("x-consumes-json", consumesMimetype(op, "application/json")); @@ -666,27 +705,216 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code 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)); + LinkedHashSet responses = op.responses.stream().map(r -> r.dataType) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + var responseType = responses.isEmpty() ? "Unit" : String.join(" | ", responses); + op.vendorExtensions.put("x-response-type", responseType); } - private static void postProcessProperty(CodegenProperty p) { + /** + * primitive or enum types don't have Data representations + * @param p the property + * @return if this property need to have '.asModel' or '.asData' called on it? + */ + private static boolean doesNotNeedMapping(final CodegenProperty p, final Set typesWhichDoNotNeedMapping) { + // ermph. Apparently 'isPrimitive' can be false while 'isNumeric' is true. + + /* + * if dataType == Value then it doesn't need mapping -- this can happen with properties like ths: + * {{{ + * example: + * items: {} + * type: array + * }}} + */ + + // we can't use !p.isModel, since 'isModel' is false (apparently) for models within arrays + return p.isPrimitiveType || p.isEnum || p.isEnumRef || p.isNumeric || isByteArray(p) || typesWhichDoNotNeedMapping.contains(p.dataType); + } + + /** + * There's a weird edge-case where fields can be declared like this: + * + * {{{ + * someField: + * format: byte + * type: string + * }} + */ + private static boolean isByteArray(final CodegenProperty p) { + return "byte".equalsIgnoreCase(p.dataFormat); // && + } + + /** + * this parameter is used to create the function: + * {{{ + * class { + * ... + * def asData = Data( + * someProp = ... <-- how do we turn this property into a model property? + * ) + * } + * }}} + * + * and then back again + */ + private static String asDataCode(final CodegenProperty p, final Set typesWhichDoNotNeedMapping) { + final var wrapInOptional = !p.required && !p.isArray && !p.isMap; + String code = ""; + + String dv = defaultValueNonOption(p, p.defaultValue); + + if (doesNotNeedMapping(p, typesWhichDoNotNeedMapping)) { + if (wrapInOptional) { + code = String.format(Locale.ROOT, "%s.getOrElse(%s) /* 1 */", p.name, dv); + } else { + code = String.format(Locale.ROOT, "%s /* 2 */", p.name); + } + } else { + if (wrapInOptional) { + if (isByteArray(p)) { + code = String.format(Locale.ROOT, "%s.getOrElse(%s) /* 3 */", p.name, dv); + } else { + code = String.format(Locale.ROOT, "%s.map(_.asData).getOrElse(%s) /* 4 */", p.name, dv); + } + } else if (p.isArray) { + if (isByteArray(p)) { + code = String.format(Locale.ROOT, "%s /* 5 */", p.name); + } else { + code = String.format(Locale.ROOT, "%s.map(_.asData) /* 6 */", p.name); + } + } else { + code = String.format(Locale.ROOT, "%s.asData /* 7 */", p.name); + } + } + return code; + } + + /** + * + * {{{ + * class Data { + * ... + * def asModel = ( + * someProp = ... <-- how do we turn this property into a model property? + * ) + * } + * }}} + * + * @param p + * @return + */ + private static String asModelCode(final CodegenProperty p, final Set typesWhichDoNotNeedMapping) { + final var wrapInOptional = !p.required && !p.isArray && !p.isMap; + String code = ""; + + if (doesNotNeedMapping(p, typesWhichDoNotNeedMapping)) { + if (wrapInOptional) { + code = String.format(Locale.ROOT, "Option(%s) /* 1 */", p.name); + } else { + code = String.format(Locale.ROOT, "%s /* 2 */", p.name); + } + } else { + if (wrapInOptional) { + if (isByteArray(p)) { + code = String.format(Locale.ROOT, "Option(%s) /* 3 */", p.name); + } else { + code = String.format(Locale.ROOT, "Option(%s).map(_.asModel) /* 4 */", p.name); + } + } else if (p.isArray) { + code = String.format(Locale.ROOT, "%s.map(_.asModel) /* 5 */", p.name); + } else { + code = String.format(Locale.ROOT, "%s.asModel /* 6 */", p.name); + } + } + return code; + } + + + private static String fixBackTicks(String text) { + // Create a regular expression pattern to find text between backticks + Pattern pattern = Pattern.compile("`([^`]+)`"); + Matcher matcher = pattern.matcher(text); + + // Use a StringBuffer to construct the result + StringBuffer result = new StringBuffer(); + + // Loop through all matches + while (matcher.find()) { + // Extract the text between backticks + String extractedText = matcher.group(1); + + // Replace it with the capitalized version + matcher.appendReplacement(result, capitalise(extractedText)); + } + + // Append the remaining part of the string + matcher.appendTail(result); + + return result.toString(); + } + + private String ensureNonKeyword(String text) { + if (isReservedWord(text)) { + return "`" + text + "`"; + } + return text; + } + + private void postProcessProperty(final 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); + final 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)); + /* + * Fix enum values which may be reserved words + */ + if (p._enum != null) { + p._enum = p._enum.stream().map(this::ensureNonKeyword).collect(Collectors.toList()); + } + + /** + * This is a fix for the enum property "type" declared like this: + * {{{ + * type: + * enum: + * - foo + * type: string + * }}} + */ + if (p.datatypeWithEnum != null && p.datatypeWithEnum.matches(".*[^a-zA-Z0-9_\\]\\[].*")) { + p.datatypeWithEnum = fixBackTicks(p.datatypeWithEnum); + } + + // We have two data models: a "data transfer" model: A "Data" model for unvalidated data and a "" model + // which has passed validation. + // + // The has a '.asData' method, and the data model has a '.asModel' function which we can use to map + // between the two. + // + // annoying, we can't just answer the question "p.isModel" to see if it's one of our modes we need to map. + // instead it seems we have to figure out the answer by determining if it is NOT a type which has to be mapped. + var typesWhichShouldNotBeMapped = importMapping.keySet().stream() + .flatMap(key -> Stream.of( + key, + String.format(Locale.ROOT, "List[%s]", key), + String.format(Locale.ROOT, "Seq[%s]", key), + String.format(Locale.ROOT, "Set[%s]", key) + )).collect(Collectors.toSet()); + typesWhichShouldNotBeMapped.add("byte"); + // 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); + p.vendorExtensions.put("x-asData", asDataCode(p, typesWhichShouldNotBeMapped)); + p.vendorExtensions.put("x-asModel", asModelCode(p, typesWhichShouldNotBeMapped)); // 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 @@ -782,6 +1010,21 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code * @return true if the property is numeric */ private static boolean isNumeric(IJsonSchemaValidationProperties p) { + + + /** + * This is nice. I've seen an example of a property defined as such: + * round: + * maximum: 4294967295 + * minimum: 0 + * type: long + * + * which returns false for isLong and isNumeric, but dataType is set to "Long" + */ + if ("Long".equalsIgnoreCase(p.getDataType())) { + return true; + } + if (p instanceof CodegenParameter) { return ((CodegenParameter)p).isNumeric; } else if (p instanceof CodegenProperty) { @@ -829,8 +1072,8 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code // Customize type for freeform objects if (ModelUtils.isFreeFormObject(schema, openAPI)) { - property.dataType = "Value"; - property.baseType = "Value"; + property.dataType = AdditionalPropertiesType; + property.baseType = AdditionalPropertiesType; } return property; @@ -839,7 +1082,11 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code @Override public String getTypeDeclaration(Schema schema) { if (ModelUtils.isFreeFormObject(schema, openAPI)) { - return "Value"; + if (ModelUtils.isMapSchema(schema)) { + String inner = getSchemaType(ModelUtils.getAdditionalProperties(schema)); + return "Map[String, " + inner + "]"; + } + return AdditionalPropertiesType; } return super.getTypeDeclaration(schema); } @@ -850,6 +1097,11 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code if (importMapping.containsKey(name)) { result = importMapping.get(name); } + // we seem to get a weird 'import foo.bar.Array[Byte]' for fields which are declared as strings w/ format 'byte' + // this test is to "fix" imports which may be e.g. "import foo.bar.Array[Byte]" by removing them + if (name.contains("[")) { + result = null; + } return result; } @@ -887,6 +1139,21 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code } private static String asScalaDataType(final IJsonSchemaValidationProperties param, boolean required, boolean useJason, boolean allowOptional) { + + /* + * BUG: 'getIsModel()' is returning false (GAH!!!) for a nested, in-line property such as this: + * {{{ + * objectValue: + * type: object + * additionalProperties: true + * properties: + * nestedProperty: + * type: string + * example: "Nested example" + * }}} + * Not sure how to go about fixing that ... there doesn't seem to be any obvious properties set on the param + * which would indicate it's actually a model type + */ String dataType = (param.getIsModel() && useJason) ? param.getDataType() + "Data" : param.getDataType(); final String dataSuffix = useJason && param.getItems() != null && param.getItems().getIsModel() ? "Data" : ""; diff --git a/modules/openapi-generator/src/main/resources/scala-cask/apiPackage.mustache b/modules/openapi-generator/src/main/resources/scala-cask/apiPackage.mustache index b315826e170..4e45743a0e8 100644 --- a/modules/openapi-generator/src/main/resources/scala-cask/apiPackage.mustache +++ b/modules/openapi-generator/src/main/resources/scala-cask/apiPackage.mustache @@ -13,6 +13,16 @@ import scala.reflect.ClassTag import scala.util.* import upickle.default.* + +extension (f: java.io.File) { + def bytes: Array[Byte] = java.nio.file.Files.readAllBytes(f.toPath) + def toBase64: String = java.util.Base64.getEncoder.encodeToString(bytes) +} + +given Writer[java.io.File] = new Writer[java.io.File] { + def write0[V](out: upickle.core.Visitor[?, V], v: java.io.File) = out.visitString(v.toBase64, -1) +} + // needed for BigDecimal params given cask.endpoints.QueryParamReader.SimpleParam[BigDecimal](BigDecimal.apply) diff --git a/modules/openapi-generator/src/main/resources/scala-cask/apiRoutes.mustache b/modules/openapi-generator/src/main/resources/scala-cask/apiRoutes.mustache index 293d74466f1..53dc0710494 100644 --- a/modules/openapi-generator/src/main/resources/scala-cask/apiRoutes.mustache +++ b/modules/openapi-generator/src/main/resources/scala-cask/apiRoutes.mustache @@ -10,11 +10,12 @@ import {{modelPackage}}.* import upickle.default.{ReadWriter => RW, macroRW} import upickle.default.* +import scala.util.Try {{#imports}}import {{import}} {{/imports}} -class {{classname}}Routes(service : {{classname}}Service) extends cask.Routes { +class {{classname}}Routes(service : {{classname}}Service[Try]) extends cask.Routes { {{#route-groups}} // route group for {{methodName}} @@ -35,7 +36,7 @@ class {{classname}}Routes(service : {{classname}}Service) extends cask.Routes { * {{description}} */ {{vendorExtensions.x-annotation}}("{{vendorExtensions.x-cask-path}}") - def {{operationId}}({{vendorExtensions.x-cask-path-typed}}) = { + def {{operationId}}({{{vendorExtensions.x-cask-path-typed}}}) = { {{#authMethods}} // auth method {{name}} : {{type}}, keyParamName: {{keyParamName}} {{/authMethods}} diff --git a/modules/openapi-generator/src/main/resources/scala-cask/apiService.mustache b/modules/openapi-generator/src/main/resources/scala-cask/apiService.mustache index 30ea17b60dd..3ba0f0e50f9 100644 --- a/modules/openapi-generator/src/main/resources/scala-cask/apiService.mustache +++ b/modules/openapi-generator/src/main/resources/scala-cask/apiService.mustache @@ -8,30 +8,123 @@ package {{apiPackage}} {{#imports}}import _root_.{{import}} {{/imports}} - +import scala.util.Failure +import scala.util.Try import _root_.{{modelPackage}}.* +/** + * The {{classname}}Service companion object. + * + * Use the {{classname}}Service() companion object to create an instance which returns a 'not implemented' error + * for each operation. + * + */ object {{classname}}Service { - def apply() : {{classname}}Service = new {{classname}}Service { + + /** + * The 'Handler' is an implementation of {{classname}}Service convenient for delegating or overriding individual functions + */ + case class Handler[F[_]]( {{#operations}} {{#operation}} - override def {{operationId}}({{vendorExtensions.x-param-list-typed}}) : {{vendorExtensions.x-response-type}} = ??? + {{operationId}}Handler : ({{{vendorExtensions.x-param-list-typed}}}) => F[{{{vendorExtensions.x-response-type}}}]{{^-last}}, {{/-last}} + {{/operation}} +{{/operations}} + ) extends {{classname}}Service[F] { +{{#operations}} + {{#operation}} + + override def {{operationId}}({{{vendorExtensions.x-param-list-typed}}}) : F[{{{vendorExtensions.x-response-type}}}] = { + {{operationId}}Handler({{{vendorExtensions.x-param-list}}}) + } {{/operation}} {{/operations}} } + + def apply() : {{classname}}Service[Try] = {{classname}}Service.Handler[Try]( +{{#operations}} + {{#operation}} + ({{#allParams}}_{{^-last}}, {{/-last}}{{/allParams}}) => notImplemented("{{operationId}}"){{^-last}}, {{/-last}} + {{/operation}} +{{/operations}} + ) + + private def notImplemented(name : String) = Failure(new Exception(s"TODO: $name not implemented")) } /** * The {{classname}} business-logic + * + * + * The 'asHandler' will return an implementation which allows for easily overriding individual operations. + * + * equally there are "on<Function>" helper methods for easily overriding individual functions + * + * @tparam F the effect type (Future, Try, IO, ID, etc) of the operations */ -trait {{classname}}Service { +trait {{classname}}Service[F[_]] { {{#operations}} {{#operation}} /** {{{summary}}} * {{{description}}} * @return {{returnType}} */ - def {{operationId}}({{vendorExtensions.x-param-list-typed}}) : {{vendorExtensions.x-response-type}} + def {{operationId}}({{{vendorExtensions.x-param-list-typed}}}) : F[{{{vendorExtensions.x-response-type}}}] + + /** + * override {{operationId}} with the given handler + * @return a new implementation of {{classname}}Service[F] with {{operationId}} overridden using the given handler + */ + final def {{vendorExtensions.x-handlerName}}(handler : ({{{vendorExtensions.x-param-list-typed}}}) => F[{{{vendorExtensions.x-response-type}}}]) : {{classname}}Service[F] = { + asHandler.copy({{operationId}}Handler = handler) + } {{/operation}} {{/operations}} + + /** + * @return a Handler implementation of this service + */ + final def asHandler : {{classname}}Service.Handler[F] = this match { + case h : {{classname}}Service.Handler[F] => h + case _ => + {{classname}}Service.Handler[F]( + {{#operations}} + {{#operation}} + ({{{vendorExtensions.x-param-list}}}) => {{operationId}}({{{vendorExtensions.x-param-list}}}){{^-last}}, {{/-last}} + {{/operation}} + {{/operations}} + ) + } + + /** + * This function will change the effect type of this service. + * + * It's not unlike a typical map operation from A => B, except we're not mapping + * a type from A to B, but rather from F[A] => G[A] using the 'changeEffect' function. + * + * For, this could turn an asynchronous service (one which returns Future[_] types) into + * a synchronous one (one which returns Try[_] types) by awaiting on the Future. + * + * It could change an IO type (like cats effect or ZIO) into an ID[A] which is just: + * ``` + * type ID[A] => A + * ``` + * + * @tparam G the new "polymorphic" effect type + * @param changeEffect the "natural transformation" which can change one effect type into another + * @return a new {{classname}}Service service implementation with effect type [G] + */ + final def mapEffect[G[_]](changeEffect : [A] => F[A] => G[A]) : {{classname}}Service[G] = { + val self = this + + new {{classname}}Service[G] { +{{#operations}} + {{#operation}} + override def {{operationId}}({{{vendorExtensions.x-param-list-typed}}}) : G[{{{vendorExtensions.x-response-type}}}] = changeEffect { + self.{{operationId}}({{{vendorExtensions.x-param-list}}}) + } + {{/operation}} +{{/operations}} + } + } } diff --git a/modules/openapi-generator/src/main/resources/scala-cask/appRoutes.mustache b/modules/openapi-generator/src/main/resources/scala-cask/appRoutes.mustache index 5006b467775..09f640f7a93 100644 --- a/modules/openapi-generator/src/main/resources/scala-cask/appRoutes.mustache +++ b/modules/openapi-generator/src/main/resources/scala-cask/appRoutes.mustache @@ -2,10 +2,10 @@ //> 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}} +import scala.util.Try {{#imports}}import {{import}} {{/imports}} import _root_.{{modelPackage}}.* @@ -20,16 +20,17 @@ import _root_.{{apiPackage}}.* * 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 app{{classname}}Service : {{classname}}Service[Try] = {{classname}}Service() def routeFor{{classname}} : {{classname}}Routes = {{classname}}Routes(app{{classname}}Service) + {{/operations}} def appRoutes = Seq( diff --git a/modules/openapi-generator/src/main/resources/scala-cask/baseApp.mustache b/modules/openapi-generator/src/main/resources/scala-cask/baseApp.mustache index b8f019873c5..5e0eca0ed76 100644 --- a/modules/openapi-generator/src/main/resources/scala-cask/baseApp.mustache +++ b/modules/openapi-generator/src/main/resources/scala-cask/baseApp.mustache @@ -6,6 +6,7 @@ // this file was generated from app.mustache package {{packageName}} +import scala.util.Try {{#imports}}import {{import}} {{/imports}} import _root_.{{modelPackage}}.* @@ -16,7 +17,7 @@ import _root_.{{apiPackage}}.* * passing in the custom business logic services */ class BaseApp({{#operations}} - override val app{{classname}}Service : {{classname}}Service = {{classname}}Service(), + override val app{{classname}}Service : {{classname}}Service[Try] = {{classname}}Service(), {{/operations}} override val port : Int = sys.env.get("PORT").map(_.toInt).getOrElse(8080)) extends cask.MainRoutes with AppRoutes { diff --git a/modules/openapi-generator/src/main/resources/scala-cask/model.mustache b/modules/openapi-generator/src/main/resources/scala-cask/model.mustache index a6f60a6e9dc..6e8551c5b05 100644 --- a/modules/openapi-generator/src/main/resources/scala-cask/model.mustache +++ b/modules/openapi-generator/src/main/resources/scala-cask/model.mustache @@ -3,6 +3,7 @@ package {{modelPackage}} {{#imports}}import {{import}} {{/imports}} + import scala.util.control.NonFatal // see https://com-lihaoyi.github.io/upickle/ @@ -11,52 +12,13 @@ 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}} -{{#isAdditionalPropertiesTrue}}, additionalProperties : ujson.Value = ujson.Null{{/isAdditionalPropertiesTrue}} -) { - - def asJsonString: String = asData.asJsonString - def asJson: ujson.Value = 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}} - {{#isAdditionalPropertiesTrue}}, additionalProperties{{/isAdditionalPropertiesTrue}} - ) - } -} - -object {{classname}} { - given RW[{{classname}}] = summon[RW[ujson.Value]].bimap[{{classname}}](_.asJson, json => read[{{classname}}Data](json).asModel) - - enum Fields(val 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}} - -} +{{#isEnum}} +{{>modelEnum}} +{{/isEnum}} +{{^isEnum}} +{{>modelClass}} +{{/isEnum}} {{/model}} {{/models}} diff --git a/modules/openapi-generator/src/main/resources/scala-cask/modelClass.mustache b/modules/openapi-generator/src/main/resources/scala-cask/modelClass.mustache new file mode 100644 index 00000000000..9cf9cf3102f --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-cask/modelClass.mustache @@ -0,0 +1,47 @@ + +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}} + +{{#isAdditionalPropertiesTrue}}, additionalProperties : ujson.Value = ujson.Null{{/isAdditionalPropertiesTrue}} +) { + +def asJsonString: String = asData.asJsonString +def asJson: ujson.Value = asData.asJson + +def asData : {{classname}}Data = { +{{classname}}Data( +{{#vars}} + {{name}} = {{{vendorExtensions.x-asData}}}{{^-last}},{{/-last}} +{{/vars}} +{{#isAdditionalPropertiesTrue}}, additionalProperties{{/isAdditionalPropertiesTrue}} +) +} +} + +object {{classname}} { +given RW[{{classname}}] = summon[RW[ujson.Value]].bimap[{{classname}}](_.asJson, json => read[{{classname}}Data](json).asModel) + +enum Fields(val 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}} + +} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/scala-cask/modelData.mustache b/modules/openapi-generator/src/main/resources/scala-cask/modelData.mustache index ec24792b452..14a8e12ee91 100644 --- a/modules/openapi-generator/src/main/resources/scala-cask/modelData.mustache +++ b/modules/openapi-generator/src/main/resources/scala-cask/modelData.mustache @@ -12,250 +12,12 @@ 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}} - {{#isAdditionalPropertiesTrue}}, additionalProperties : ujson.Value = ujson.Null{{/isAdditionalPropertiesTrue}} - -) derives RW { - - def asJsonString: String = asJson.toString() - - def asJson : ujson.Value = { - val jason = writeJs(this) - {{#isAdditionalPropertiesTrue}} - jason.obj.remove("additionalProperties") - jason.mergeWith(additionalProperties) - {{/isAdditionalPropertiesTrue}} - {{^isAdditionalPropertiesTrue}} - jason - {{/isAdditionalPropertiesTrue}} - } - - def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = { - val errors = scala.collection.mutable.ListBuffer[ValidationError]() - {{#vars}} - // ================== {{name}} validation ================== - {{#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}} - {{#isAdditionalPropertiesTrue}}, additionalProperties{{/isAdditionalPropertiesTrue}} - ) - } -} - -object {{classname}}Data { - - def fromJson(jason : ujson.Value) : {{classname}}Data = try { - val data = read[{{classname}}Data](jason) - {{^isAdditionalPropertiesTrue}} - data - {{/isAdditionalPropertiesTrue}} - {{#isAdditionalPropertiesTrue}} - val obj = jason.obj - {{classname}}.Fields.values.foreach(v => obj.value.subtractOne(v.fieldName)) - data.copy(additionalProperties = obj) - {{/isAdditionalPropertiesTrue}} - } catch { - case NonFatal(e) => sys.error(s"Error creating {{classname}}Data from json '$jason': $e") - } - - def fromJsonString(jason : String) : {{classname}}Data = { - val parsed = try { - read[ujson.Value](jason) - } catch { - case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e") - } - fromJson(parsed) - } - - 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 - } - } - } -} + {{#isEnum}} + {{>modelDataEnum}} + {{/isEnum}} + {{^isEnum}} + {{>modelDataClass}} + {{/isEnum}} {{/model}} {{/models}} diff --git a/modules/openapi-generator/src/main/resources/scala-cask/modelDataClass.mustache b/modules/openapi-generator/src/main/resources/scala-cask/modelDataClass.mustache new file mode 100644 index 00000000000..72017e63563 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-cask/modelDataClass.mustache @@ -0,0 +1,244 @@ +/** {{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}} + {{#isAdditionalPropertiesTrue}}, additionalProperties : ujson.Value = ujson.Null{{/isAdditionalPropertiesTrue}} + +) derives RW { + + def asJsonString: String = asJson.toString() + + def asJson : ujson.Value = { + val jason = writeJs(this) + {{#isAdditionalPropertiesTrue}} + jason.obj.remove("additionalProperties") + jason.mergeWith(additionalProperties) + {{/isAdditionalPropertiesTrue}} + {{^isAdditionalPropertiesTrue}} + jason + {{/isAdditionalPropertiesTrue}} + } + + def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = { + val errors = scala.collection.mutable.ListBuffer[ValidationError]() + {{#vars}} + // ================== {{name}} validation ================== + {{#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 + } + + /** + * @return the validated model within a Try (if successful) + */ + 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-asModel}}}{{^-last}},{{/-last}} + {{/vars}} + {{#isAdditionalPropertiesTrue}}, additionalProperties{{/isAdditionalPropertiesTrue}} + ) + } +} + +object {{classname}}Data { + + def fromJson(jason : ujson.Value) : {{classname}}Data = try { + val data = read[{{classname}}Data](jason) + {{^isAdditionalPropertiesTrue}} + data + {{/isAdditionalPropertiesTrue}} + {{#isAdditionalPropertiesTrue}} + val obj = jason.obj + {{classname}}.Fields.values.foreach(v => obj.value.subtractOne(v.fieldName)) + data.copy(additionalProperties = obj) + {{/isAdditionalPropertiesTrue}} + } catch { + case NonFatal(e) => sys.error(s"Error creating {{classname}}Data from json '$jason': $e") + } + + def fromJsonString(jason : String) : {{classname}}Data = { + val parsed = try { + read[ujson.Value](jason) + } catch { + case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e") + } + fromJson(parsed) + } + + 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 + } + } + } +} diff --git a/modules/openapi-generator/src/main/resources/scala-cask/modelDataEnum.mustache b/modules/openapi-generator/src/main/resources/scala-cask/modelDataEnum.mustache new file mode 100644 index 00000000000..3a46e3d4297 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-cask/modelDataEnum.mustache @@ -0,0 +1 @@ +type {{classname}}Data = {{classname}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/scala-cask/modelEnum.mustache b/modules/openapi-generator/src/main/resources/scala-cask/modelEnum.mustache new file mode 100644 index 00000000000..211717b54a9 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-cask/modelEnum.mustache @@ -0,0 +1,6 @@ +enum {{classname}} derives RW : +{{#allowableValues}} + {{#values}} + case {{.}} + {{/values}} +{{/allowableValues}} \ 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 index cbeae14cecc..7b1d95b5aab 100644 --- a/modules/openapi-generator/src/main/resources/scala-cask/modelTest.mustache +++ b/modules/openapi-generator/src/main/resources/scala-cask/modelTest.mustache @@ -20,6 +20,8 @@ class {{classname}}Test extends AnyWordSpec with Matchers { value= {{value}} {{/examples}} {{/operations}} + +{{^isEnum}} "{{classname}}.fromJson" should { """not parse invalid json""" in { val Failure(err) = Try({{classname}}Data.fromJsonString("invalid jason")) @@ -31,7 +33,7 @@ class {{classname}}Test extends AnyWordSpec with Matchers { sys.error("TODO") } } - +{{/isEnum}} } {{/model}} {{/models}} diff --git a/modules/openapi-generator/src/main/resources/scala-cask/parseHttpParams.mustache b/modules/openapi-generator/src/main/resources/scala-cask/parseHttpParams.mustache index 6da75c69786..ebbd63e2c05 100644 --- a/modules/openapi-generator/src/main/resources/scala-cask/parseHttpParams.mustache +++ b/modules/openapi-generator/src/main/resources/scala-cask/parseHttpParams.mustache @@ -53,5 +53,6 @@ {{/vendorExtensions.x-consumes-xml}} {{/vendorExtensions.x-consumes-json}} {{/bodyParams}} - result <- Parsed.eval(service.{{operationId}}({{vendorExtensions.x-param-list}})) + resultTry <- Parsed.eval(service.{{operationId}}({{{vendorExtensions.x-param-list}}})) + result <- Parsed.fromTry(resultTry) } yield result \ No newline at end of file diff --git a/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/AppRoutes.scala b/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/AppRoutes.scala index 36501557371..f519b3ca30f 100644 --- a/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/AppRoutes.scala +++ b/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/AppRoutes.scala @@ -14,10 +14,10 @@ * https://openapi-generator.tech */ - // this file was generated from app.mustache package cask.groupId.server +import scala.util.Try import _root_.sample.cask.model.* import _root_.sample.cask.api.* @@ -30,20 +30,23 @@ import _root_.sample.cask.api.* * 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 appPetService : PetService[Try] = PetService() def routeForPet : PetRoutes = PetRoutes(appPetService) - def appStoreService : StoreService = StoreService() + + def appStoreService : StoreService[Try] = StoreService() def routeForStore : StoreRoutes = StoreRoutes(appStoreService) - def appUserService : UserService = UserService() + + def appUserService : UserService[Try] = UserService() def routeForUser : UserRoutes = UserRoutes(appUserService) + def appRoutes = Seq( routeForPet , routeForStore , diff --git a/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/BaseApp.scala b/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/BaseApp.scala index c854b216d20..7ed98e88bc9 100644 --- a/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/BaseApp.scala +++ b/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/BaseApp.scala @@ -18,6 +18,7 @@ // this file was generated from app.mustache package cask.groupId.server +import scala.util.Try import _root_.sample.cask.model.* import _root_.sample.cask.api.* @@ -26,11 +27,11 @@ import _root_.sample.cask.api.* * passing in the custom business logic services */ class BaseApp( - override val appPetService : PetService = PetService(), + override val appPetService : PetService[Try] = PetService(), - override val appStoreService : StoreService = StoreService(), + override val appStoreService : StoreService[Try] = StoreService(), - override val appUserService : UserService = UserService(), + override val appUserService : UserService[Try] = UserService(), override val port : Int = sys.env.get("PORT").map(_.toInt).getOrElse(8080)) extends cask.MainRoutes with AppRoutes { /** routes for the UI diff --git a/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/PetRoutes.scala b/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/PetRoutes.scala index e69ef16de01..da02bbb338d 100644 --- a/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/PetRoutes.scala +++ b/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/PetRoutes.scala @@ -22,12 +22,13 @@ import sample.cask.model.* import upickle.default.{ReadWriter => RW, macroRW} import upickle.default.* +import scala.util.Try import sample.cask.model.ApiResponse import java.io.File import sample.cask.model.Pet -class PetRoutes(service : PetService) extends cask.Routes { +class PetRoutes(service : PetService[Try]) extends cask.Routes { // route group for routeWorkAroundForPOSTPet @cask.post("/pet", true) @@ -63,7 +64,8 @@ class PetRoutes(service : PetService) extends cask.Routes { petJson <- Parsed.fromTry(request.bodyAsJson) petData <- Parsed.eval(PetData.fromJson(petJson)) /* not array or map */ pet <- Parsed.fromTry(petData.validated(failFast)) - result <- Parsed.eval(service.addPet(pet)) + resultTry <- Parsed.eval(service.addPet(pet)) + result <- Parsed.fromTry(resultTry) } yield result (result : @unchecked) match { @@ -84,7 +86,8 @@ class PetRoutes(service : PetService) extends cask.Routes { val result = for { petId <- Parsed(petId) apiKey <- request.headerSingleValueOptional("apiKey") - result <- Parsed.eval(service.deletePet(petId, apiKey)) + resultTry <- Parsed.eval(service.deletePet(petId, apiKey)) + result <- Parsed.fromTry(resultTry) } yield result (result : @unchecked) match { @@ -102,7 +105,8 @@ class PetRoutes(service : PetService) extends cask.Routes { def failFast = request.queryParams.keySet.contains("failFast") val result = for { - result <- Parsed.eval(service.findPetsByStatus(status)) + resultTry <- Parsed.eval(service.findPetsByStatus(status)) + result <- Parsed.fromTry(resultTry) } yield result (result : @unchecked) match { @@ -121,7 +125,8 @@ class PetRoutes(service : PetService) extends cask.Routes { def failFast = request.queryParams.keySet.contains("failFast") val result = for { - result <- Parsed.eval(service.findPetsByTags(tags)) + resultTry <- Parsed.eval(service.findPetsByTags(tags)) + result <- Parsed.fromTry(resultTry) } yield result (result : @unchecked) match { @@ -141,7 +146,8 @@ class PetRoutes(service : PetService) extends cask.Routes { val result = for { petId <- Parsed(petId) - result <- Parsed.eval(service.getPetById(petId)) + resultTry <- Parsed.eval(service.getPetById(petId)) + result <- Parsed.fromTry(resultTry) } yield result (result : @unchecked) match { @@ -163,7 +169,8 @@ class PetRoutes(service : PetService) extends cask.Routes { petJson <- Parsed.fromTry(request.bodyAsJson) petData <- Parsed.eval(PetData.fromJson(petJson)) /* not array or map */ pet <- Parsed.fromTry(petData.validated(failFast)) - result <- Parsed.eval(service.updatePet(pet)) + resultTry <- Parsed.eval(service.updatePet(pet)) + result <- Parsed.fromTry(resultTry) } yield result (result : @unchecked) match { @@ -185,7 +192,8 @@ class PetRoutes(service : PetService) extends cask.Routes { petId <- Parsed(petId) name <- request.formSingleValueOptional("name") status <- request.formSingleValueOptional("status") - result <- Parsed.eval(service.updatePetWithForm(petId, name, status)) + resultTry <- Parsed.eval(service.updatePetWithForm(petId, name, status)) + result <- Parsed.fromTry(resultTry) } yield result (result : @unchecked) match { @@ -206,7 +214,8 @@ class PetRoutes(service : PetService) extends cask.Routes { petId <- Parsed(petId) additionalMetadata <- request.formSingleValueOptional("additionalMetadata") file <- request.formValueAsFileOptional("file") - result <- Parsed.eval(service.uploadFile(petId, additionalMetadata, file)) + resultTry <- Parsed.eval(service.uploadFile(petId, additionalMetadata, file)) + result <- Parsed.fromTry(resultTry) } yield result (result : @unchecked) match { diff --git a/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/StoreRoutes.scala b/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/StoreRoutes.scala index b4a550889d7..5c3414564e5 100644 --- a/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/StoreRoutes.scala +++ b/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/StoreRoutes.scala @@ -22,10 +22,11 @@ import sample.cask.model.* import upickle.default.{ReadWriter => RW, macroRW} import upickle.default.* +import scala.util.Try import sample.cask.model.Order -class StoreRoutes(service : StoreService) extends cask.Routes { +class StoreRoutes(service : StoreService[Try]) extends cask.Routes { /** Delete purchase order by ID @@ -38,7 +39,8 @@ class StoreRoutes(service : StoreService) extends cask.Routes { val result = for { orderId <- Parsed(orderId) - result <- Parsed.eval(service.deleteOrder(orderId)) + resultTry <- Parsed.eval(service.deleteOrder(orderId)) + result <- Parsed.fromTry(resultTry) } yield result (result : @unchecked) match { @@ -56,7 +58,8 @@ class StoreRoutes(service : StoreService) extends cask.Routes { def failFast = request.queryParams.keySet.contains("failFast") val result = for { - result <- Parsed.eval(service.getInventory()) + resultTry <- Parsed.eval(service.getInventory()) + result <- Parsed.fromTry(resultTry) } yield result (result : @unchecked) match { @@ -75,7 +78,8 @@ class StoreRoutes(service : StoreService) extends cask.Routes { val result = for { orderId <- Parsed(orderId) - result <- Parsed.eval(service.getOrderById(orderId)) + resultTry <- Parsed.eval(service.getOrderById(orderId)) + result <- Parsed.fromTry(resultTry) } yield result (result : @unchecked) match { @@ -96,7 +100,8 @@ class StoreRoutes(service : StoreService) extends cask.Routes { orderJson <- Parsed.fromTry(request.bodyAsJson) orderData <- Parsed.eval(OrderData.fromJson(orderJson)) /* not array or map */ order <- Parsed.fromTry(orderData.validated(failFast)) - result <- Parsed.eval(service.placeOrder(order)) + resultTry <- Parsed.eval(service.placeOrder(order)) + result <- Parsed.fromTry(resultTry) } yield result (result : @unchecked) match { diff --git a/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/UserRoutes.scala b/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/UserRoutes.scala index 64b82a45d88..63c01ea5583 100644 --- a/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/UserRoutes.scala +++ b/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/UserRoutes.scala @@ -22,11 +22,12 @@ import sample.cask.model.* import upickle.default.{ReadWriter => RW, macroRW} import upickle.default.* +import scala.util.Try import java.time.OffsetDateTime import sample.cask.model.User -class UserRoutes(service : UserService) extends cask.Routes { +class UserRoutes(service : UserService[Try]) extends cask.Routes { // route group for routeWorkAroundForGETUser @cask.get("/user", true) @@ -52,7 +53,8 @@ class UserRoutes(service : UserService) extends cask.Routes { userJson <- Parsed.fromTry(request.bodyAsJson) userData <- Parsed.eval(UserData.fromJson(userJson)) /* not array or map */ user <- Parsed.fromTry(userData.validated(failFast)) - result <- Parsed.eval(service.createUser(user)) + resultTry <- Parsed.eval(service.createUser(user)) + result <- Parsed.fromTry(resultTry) } yield result (result : @unchecked) match { @@ -71,7 +73,8 @@ class UserRoutes(service : UserService) extends cask.Routes { 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)) + resultTry <- Parsed.eval(service.createUsersWithArrayInput(user)) + result <- Parsed.fromTry(resultTry) } yield result (result : @unchecked) match { @@ -90,7 +93,8 @@ class UserRoutes(service : UserService) extends cask.Routes { 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)) + resultTry <- Parsed.eval(service.createUsersWithListInput(user)) + result <- Parsed.fromTry(resultTry) } yield result (result : @unchecked) match { @@ -109,7 +113,8 @@ class UserRoutes(service : UserService) extends cask.Routes { val result = for { username <- Parsed(username) - result <- Parsed.eval(service.deleteUser(username)) + resultTry <- Parsed.eval(service.deleteUser(username)) + result <- Parsed.fromTry(resultTry) } yield result (result : @unchecked) match { @@ -127,7 +132,8 @@ class UserRoutes(service : UserService) extends cask.Routes { val result = for { username <- Parsed(username) - result <- Parsed.eval(service.getUserByName(username)) + resultTry <- Parsed.eval(service.getUserByName(username)) + result <- Parsed.fromTry(resultTry) } yield result (result : @unchecked) match { @@ -145,7 +151,8 @@ class UserRoutes(service : UserService) extends cask.Routes { def failFast = request.queryParams.keySet.contains("failFast") val result = for { - result <- Parsed.eval(service.loginUser(username, password)) + resultTry <- Parsed.eval(service.loginUser(username, password)) + result <- Parsed.fromTry(resultTry) } yield result (result : @unchecked) match { @@ -164,7 +171,8 @@ class UserRoutes(service : UserService) extends cask.Routes { def failFast = request.queryParams.keySet.contains("failFast") val result = for { - result <- Parsed.eval(service.logoutUser()) + resultTry <- Parsed.eval(service.logoutUser()) + result <- Parsed.fromTry(resultTry) } yield result (result : @unchecked) match { @@ -186,7 +194,8 @@ class UserRoutes(service : UserService) extends cask.Routes { userJson <- Parsed.fromTry(request.bodyAsJson) userData <- Parsed.eval(UserData.fromJson(userJson)) /* not array or map */ user <- Parsed.fromTry(userData.validated(failFast)) - result <- Parsed.eval(service.updateUser(username, user)) + resultTry <- Parsed.eval(service.updateUser(username, user)) + result <- Parsed.fromTry(resultTry) } yield result (result : @unchecked) match { diff --git a/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/package.scala b/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/package.scala index 10b838f37aa..87a717a5c34 100644 --- a/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/package.scala +++ b/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/package.scala @@ -25,6 +25,16 @@ import scala.reflect.ClassTag import scala.util.* import upickle.default.* + +extension (f: java.io.File) { + def bytes: Array[Byte] = java.nio.file.Files.readAllBytes(f.toPath) + def toBase64: String = java.util.Base64.getEncoder.encodeToString(bytes) +} + +given Writer[java.io.File] = new Writer[java.io.File] { + def write0[V](out: upickle.core.Visitor[?, V], v: java.io.File) = out.visitString(v.toBase64, -1) +} + // needed for BigDecimal params given cask.endpoints.QueryParamReader.SimpleParam[BigDecimal](BigDecimal.apply) diff --git a/samples/server/petstore/scala-cask/jvm/src/test/scala/sample/cask/model/ApiResponseTest.scala b/samples/server/petstore/scala-cask/jvm/src/test/scala/sample/cask/model/ApiResponseTest.scala index 2cb45b8584c..00f610d291d 100644 --- a/samples/server/petstore/scala-cask/jvm/src/test/scala/sample/cask/model/ApiResponseTest.scala +++ b/samples/server/petstore/scala-cask/jvm/src/test/scala/sample/cask/model/ApiResponseTest.scala @@ -1,15 +1,14 @@ -/** - * 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 - */ +/** 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 modelTest.mustache package sample.cask.model @@ -20,16 +19,16 @@ 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") - } + "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/jvm/src/test/scala/sample/cask/model/CategoryTest.scala b/samples/server/petstore/scala-cask/jvm/src/test/scala/sample/cask/model/CategoryTest.scala index b040f70cd91..3cca352ec89 100644 --- a/samples/server/petstore/scala-cask/jvm/src/test/scala/sample/cask/model/CategoryTest.scala +++ b/samples/server/petstore/scala-cask/jvm/src/test/scala/sample/cask/model/CategoryTest.scala @@ -1,15 +1,14 @@ -/** - * 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 - */ +/** 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 modelTest.mustache package sample.cask.model @@ -20,16 +19,16 @@ 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") - } + "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/jvm/src/test/scala/sample/cask/model/OrderTest.scala b/samples/server/petstore/scala-cask/jvm/src/test/scala/sample/cask/model/OrderTest.scala index 14071dc374a..30c14346347 100644 --- a/samples/server/petstore/scala-cask/jvm/src/test/scala/sample/cask/model/OrderTest.scala +++ b/samples/server/petstore/scala-cask/jvm/src/test/scala/sample/cask/model/OrderTest.scala @@ -1,15 +1,14 @@ -/** - * 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 - */ +/** 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 modelTest.mustache package sample.cask.model @@ -21,16 +20,16 @@ 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") - } + "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/jvm/src/test/scala/sample/cask/model/PetTest.scala b/samples/server/petstore/scala-cask/jvm/src/test/scala/sample/cask/model/PetTest.scala index 7ec3bb037b3..5dfc568f56e 100644 --- a/samples/server/petstore/scala-cask/jvm/src/test/scala/sample/cask/model/PetTest.scala +++ b/samples/server/petstore/scala-cask/jvm/src/test/scala/sample/cask/model/PetTest.scala @@ -1,20 +1,17 @@ -/** - * 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 - */ +/** 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 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 @@ -22,16 +19,16 @@ 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") - } + "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/jvm/src/test/scala/sample/cask/model/TagTest.scala b/samples/server/petstore/scala-cask/jvm/src/test/scala/sample/cask/model/TagTest.scala index 74a5dcf458e..ef1dea19c89 100644 --- a/samples/server/petstore/scala-cask/jvm/src/test/scala/sample/cask/model/TagTest.scala +++ b/samples/server/petstore/scala-cask/jvm/src/test/scala/sample/cask/model/TagTest.scala @@ -1,15 +1,14 @@ -/** - * 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 - */ +/** 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 modelTest.mustache package sample.cask.model @@ -20,16 +19,16 @@ 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") - } + "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/jvm/src/test/scala/sample/cask/model/UserTest.scala b/samples/server/petstore/scala-cask/jvm/src/test/scala/sample/cask/model/UserTest.scala index d01f0adf20e..561bc4dc0e0 100644 --- a/samples/server/petstore/scala-cask/jvm/src/test/scala/sample/cask/model/UserTest.scala +++ b/samples/server/petstore/scala-cask/jvm/src/test/scala/sample/cask/model/UserTest.scala @@ -1,15 +1,14 @@ -/** - * 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 - */ +/** 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 modelTest.mustache package sample.cask.model @@ -20,16 +19,16 @@ 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") - } + "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") + } + } } diff --git a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/api/PetService.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/api/PetService.scala index 7bed5032e2c..56567ecc4c3 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/api/PetService.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/api/PetService.scala @@ -21,64 +21,260 @@ package sample.cask.api import _root_.sample.cask.model.ApiResponse import _root_.java.io.File import _root_.sample.cask.model.Pet - +import scala.util.Failure +import scala.util.Try import _root_.sample.cask.model.* +/** + * The PetService companion object. + * + * Use the PetService() companion object to create an instance which returns a 'not implemented' error + * for each operation. + * + */ 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 'Handler' is an implementation of PetService convenient for delegating or overriding individual functions + */ + case class Handler[F[_]]( + addPetHandler : (pet : Pet) => F[Pet], + deletePetHandler : (petId : Long, apiKey : Option[String]) => F[Unit], + findPetsByStatusHandler : (status : Seq[String]) => F[List[Pet]], + findPetsByTagsHandler : (tags : Seq[String]) => F[List[Pet]], + getPetByIdHandler : (petId : Long) => F[Pet], + updatePetHandler : (pet : Pet) => F[Pet], + updatePetWithFormHandler : (petId : Long, name : Option[String], status : Option[String]) => F[Unit], + uploadFileHandler : (petId : Long, additionalMetadata : Option[String], file : Option[File]) => F[ApiResponse] + ) extends PetService[F] { + + override def addPet(pet : Pet) : F[Pet] = { + addPetHandler(pet) + } + + override def deletePet(petId : Long, apiKey : Option[String]) : F[Unit] = { + deletePetHandler(petId, apiKey) + } + + override def findPetsByStatus(status : Seq[String]) : F[List[Pet]] = { + findPetsByStatusHandler(status) + } + + override def findPetsByTags(tags : Seq[String]) : F[List[Pet]] = { + findPetsByTagsHandler(tags) + } + + override def getPetById(petId : Long) : F[Pet] = { + getPetByIdHandler(petId) + } + + override def updatePet(pet : Pet) : F[Pet] = { + updatePetHandler(pet) + } + + override def updatePetWithForm(petId : Long, name : Option[String], status : Option[String]) : F[Unit] = { + updatePetWithFormHandler(petId, name, status) + } + + override def uploadFile(petId : Long, additionalMetadata : Option[String], file : Option[File]) : F[ApiResponse] = { + uploadFileHandler(petId, additionalMetadata, file) + } } + + def apply() : PetService[Try] = PetService.Handler[Try]( + (_) => notImplemented("addPet"), + (_, _) => notImplemented("deletePet"), + (_) => notImplemented("findPetsByStatus"), + (_) => notImplemented("findPetsByTags"), + (_) => notImplemented("getPetById"), + (_) => notImplemented("updatePet"), + (_, _, _) => notImplemented("updatePetWithForm"), + (_, _, _) => notImplemented("uploadFile") + ) + + private def notImplemented(name : String) = Failure(new Exception(s"TODO: $name not implemented")) } /** * The Pet business-logic + * + * + * The 'asHandler' will return an implementation which allows for easily overriding individual operations. + * + * equally there are "on<Function>" helper methods for easily overriding individual functions + * + * @tparam F the effect type (Future, Try, IO, ID, etc) of the operations */ -trait PetService { +trait PetService[F[_]] { /** Add a new pet to the store * * @return Pet */ - def addPet(pet : Pet) : Pet + def addPet(pet : Pet) : F[Pet] + + /** + * override addPet with the given handler + * @return a new implementation of PetService[F] with addPet overridden using the given handler + */ + final def onAddPet(handler : (pet : Pet) => F[Pet]) : PetService[F] = { + asHandler.copy(addPetHandler = handler) + } /** Deletes a pet * * @return */ - def deletePet(petId : Long, apiKey : Option[String]) : Unit + def deletePet(petId : Long, apiKey : Option[String]) : F[Unit] + + /** + * override deletePet with the given handler + * @return a new implementation of PetService[F] with deletePet overridden using the given handler + */ + final def onDeletePet(handler : (petId : Long, apiKey : Option[String]) => F[Unit]) : PetService[F] = { + asHandler.copy(deletePetHandler = handler) + } /** Finds Pets by status * * @return List[Pet] */ - def findPetsByStatus(status : Seq[String]) : List[Pet] + def findPetsByStatus(status : Seq[String]) : F[List[Pet]] + + /** + * override findPetsByStatus with the given handler + * @return a new implementation of PetService[F] with findPetsByStatus overridden using the given handler + */ + final def onFindPetsByStatus(handler : (status : Seq[String]) => F[List[Pet]]) : PetService[F] = { + asHandler.copy(findPetsByStatusHandler = handler) + } /** Finds Pets by tags * * @return List[Pet] */ - def findPetsByTags(tags : Seq[String]) : List[Pet] + def findPetsByTags(tags : Seq[String]) : F[List[Pet]] + + /** + * override findPetsByTags with the given handler + * @return a new implementation of PetService[F] with findPetsByTags overridden using the given handler + */ + final def onFindPetsByTags(handler : (tags : Seq[String]) => F[List[Pet]]) : PetService[F] = { + asHandler.copy(findPetsByTagsHandler = handler) + } /** Find pet by ID * * @return Pet */ - def getPetById(petId : Long) : Pet + def getPetById(petId : Long) : F[Pet] + + /** + * override getPetById with the given handler + * @return a new implementation of PetService[F] with getPetById overridden using the given handler + */ + final def onGetPetById(handler : (petId : Long) => F[Pet]) : PetService[F] = { + asHandler.copy(getPetByIdHandler = handler) + } /** Update an existing pet * * @return Pet */ - def updatePet(pet : Pet) : Pet + def updatePet(pet : Pet) : F[Pet] + + /** + * override updatePet with the given handler + * @return a new implementation of PetService[F] with updatePet overridden using the given handler + */ + final def onUpdatePet(handler : (pet : Pet) => F[Pet]) : PetService[F] = { + asHandler.copy(updatePetHandler = handler) + } /** Updates a pet in the store with form data * * @return */ - def updatePetWithForm(petId : Long, name : Option[String], status : Option[String]) : Unit + def updatePetWithForm(petId : Long, name : Option[String], status : Option[String]) : F[Unit] + + /** + * override updatePetWithForm with the given handler + * @return a new implementation of PetService[F] with updatePetWithForm overridden using the given handler + */ + final def onUpdatePetWithForm(handler : (petId : Long, name : Option[String], status : Option[String]) => F[Unit]) : PetService[F] = { + asHandler.copy(updatePetWithFormHandler = handler) + } /** uploads an image * * @return ApiResponse */ - def uploadFile(petId : Long, additionalMetadata : Option[String], file : Option[File]) : ApiResponse + def uploadFile(petId : Long, additionalMetadata : Option[String], file : Option[File]) : F[ApiResponse] + + /** + * override uploadFile with the given handler + * @return a new implementation of PetService[F] with uploadFile overridden using the given handler + */ + final def onUploadFile(handler : (petId : Long, additionalMetadata : Option[String], file : Option[File]) => F[ApiResponse]) : PetService[F] = { + asHandler.copy(uploadFileHandler = handler) + } + + /** + * @return a Handler implementation of this service + */ + final def asHandler : PetService.Handler[F] = this match { + case h : PetService.Handler[F] => h + case _ => + PetService.Handler[F]( + (pet) => addPet(pet), + (petId, apiKey) => deletePet(petId, apiKey), + (status) => findPetsByStatus(status), + (tags) => findPetsByTags(tags), + (petId) => getPetById(petId), + (pet) => updatePet(pet), + (petId, name, status) => updatePetWithForm(petId, name, status), + (petId, additionalMetadata, file) => uploadFile(petId, additionalMetadata, file) + ) + } + + /** + * This function will change the effect type of this service. + * + * It's not unlike a typical map operation from A => B, except we're not mapping + * a type from A to B, but rather from F[A] => G[A] using the 'changeEffect' function. + * + * For, this could turn an asynchronous service (one which returns Future[_] types) into + * a synchronous one (one which returns Try[_] types) by awaiting on the Future. + * + * It could change an IO type (like cats effect or ZIO) into an ID[A] which is just: + * ``` + * type ID[A] => A + * ``` + * + * @tparam G the new "polymorphic" effect type + * @param changeEffect the "natural transformation" which can change one effect type into another + * @return a new PetService service implementation with effect type [G] + */ + final def mapEffect[G[_]](changeEffect : [A] => F[A] => G[A]) : PetService[G] = { + val self = this + + new PetService[G] { + override def addPet(pet : Pet) : G[Pet] = changeEffect { + self.addPet(pet) + } + override def deletePet(petId : Long, apiKey : Option[String]) : G[Unit] = changeEffect { + self.deletePet(petId, apiKey) + } + override def findPetsByStatus(status : Seq[String]) : G[List[Pet]] = changeEffect { + self.findPetsByStatus(status) + } + override def findPetsByTags(tags : Seq[String]) : G[List[Pet]] = changeEffect { + self.findPetsByTags(tags) + } + override def getPetById(petId : Long) : G[Pet] = changeEffect { + self.getPetById(petId) + } + override def updatePet(pet : Pet) : G[Pet] = changeEffect { + self.updatePet(pet) + } + override def updatePetWithForm(petId : Long, name : Option[String], status : Option[String]) : G[Unit] = changeEffect { + self.updatePetWithForm(petId, name, status) + } + override def uploadFile(petId : Long, additionalMetadata : Option[String], file : Option[File]) : G[ApiResponse] = changeEffect { + self.uploadFile(petId, additionalMetadata, file) + } + } + } } diff --git a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/api/StoreService.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/api/StoreService.scala index 231f03fc3fe..bc5871153af 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/api/StoreService.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/api/StoreService.scala @@ -19,40 +19,168 @@ package sample.cask.api import _root_.sample.cask.model.Order - +import scala.util.Failure +import scala.util.Try import _root_.sample.cask.model.* +/** + * The StoreService companion object. + * + * Use the StoreService() companion object to create an instance which returns a 'not implemented' error + * for each operation. + * + */ 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 'Handler' is an implementation of StoreService convenient for delegating or overriding individual functions + */ + case class Handler[F[_]]( + deleteOrderHandler : (orderId : String) => F[Unit], + getInventoryHandler : () => F[Map[String, Int]], + getOrderByIdHandler : (orderId : Long) => F[Order], + placeOrderHandler : (order : Order) => F[Order] + ) extends StoreService[F] { + + override def deleteOrder(orderId : String) : F[Unit] = { + deleteOrderHandler(orderId) + } + + override def getInventory() : F[Map[String, Int]] = { + getInventoryHandler() + } + + override def getOrderById(orderId : Long) : F[Order] = { + getOrderByIdHandler(orderId) + } + + override def placeOrder(order : Order) : F[Order] = { + placeOrderHandler(order) + } } + + def apply() : StoreService[Try] = StoreService.Handler[Try]( + (_) => notImplemented("deleteOrder"), + () => notImplemented("getInventory"), + (_) => notImplemented("getOrderById"), + (_) => notImplemented("placeOrder") + ) + + private def notImplemented(name : String) = Failure(new Exception(s"TODO: $name not implemented")) } /** * The Store business-logic + * + * + * The 'asHandler' will return an implementation which allows for easily overriding individual operations. + * + * equally there are "on<Function>" helper methods for easily overriding individual functions + * + * @tparam F the effect type (Future, Try, IO, ID, etc) of the operations */ -trait StoreService { +trait StoreService[F[_]] { /** Delete purchase order by ID * * @return */ - def deleteOrder(orderId : String) : Unit + def deleteOrder(orderId : String) : F[Unit] + + /** + * override deleteOrder with the given handler + * @return a new implementation of StoreService[F] with deleteOrder overridden using the given handler + */ + final def onDeleteOrder(handler : (orderId : String) => F[Unit]) : StoreService[F] = { + asHandler.copy(deleteOrderHandler = handler) + } /** Returns pet inventories by status * * @return Map[String, Int] */ - def getInventory() : Map[String, Int] + def getInventory() : F[Map[String, Int]] + + /** + * override getInventory with the given handler + * @return a new implementation of StoreService[F] with getInventory overridden using the given handler + */ + final def onGetInventory(handler : () => F[Map[String, Int]]) : StoreService[F] = { + asHandler.copy(getInventoryHandler = handler) + } /** Find purchase order by ID * * @return Order */ - def getOrderById(orderId : Long) : Order + def getOrderById(orderId : Long) : F[Order] + + /** + * override getOrderById with the given handler + * @return a new implementation of StoreService[F] with getOrderById overridden using the given handler + */ + final def onGetOrderById(handler : (orderId : Long) => F[Order]) : StoreService[F] = { + asHandler.copy(getOrderByIdHandler = handler) + } /** Place an order for a pet * * @return Order */ - def placeOrder(order : Order) : Order + def placeOrder(order : Order) : F[Order] + + /** + * override placeOrder with the given handler + * @return a new implementation of StoreService[F] with placeOrder overridden using the given handler + */ + final def onPlaceOrder(handler : (order : Order) => F[Order]) : StoreService[F] = { + asHandler.copy(placeOrderHandler = handler) + } + + /** + * @return a Handler implementation of this service + */ + final def asHandler : StoreService.Handler[F] = this match { + case h : StoreService.Handler[F] => h + case _ => + StoreService.Handler[F]( + (orderId) => deleteOrder(orderId), + () => getInventory(), + (orderId) => getOrderById(orderId), + (order) => placeOrder(order) + ) + } + + /** + * This function will change the effect type of this service. + * + * It's not unlike a typical map operation from A => B, except we're not mapping + * a type from A to B, but rather from F[A] => G[A] using the 'changeEffect' function. + * + * For, this could turn an asynchronous service (one which returns Future[_] types) into + * a synchronous one (one which returns Try[_] types) by awaiting on the Future. + * + * It could change an IO type (like cats effect or ZIO) into an ID[A] which is just: + * ``` + * type ID[A] => A + * ``` + * + * @tparam G the new "polymorphic" effect type + * @param changeEffect the "natural transformation" which can change one effect type into another + * @return a new StoreService service implementation with effect type [G] + */ + final def mapEffect[G[_]](changeEffect : [A] => F[A] => G[A]) : StoreService[G] = { + val self = this + + new StoreService[G] { + override def deleteOrder(orderId : String) : G[Unit] = changeEffect { + self.deleteOrder(orderId) + } + override def getInventory() : G[Map[String, Int]] = changeEffect { + self.getInventory() + } + override def getOrderById(orderId : Long) : G[Order] = changeEffect { + self.getOrderById(orderId) + } + override def placeOrder(order : Order) : G[Order] = changeEffect { + self.placeOrder(order) + } + } + } } diff --git a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/api/UserService.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/api/UserService.scala index 4872f31e3c7..b01ec267b1e 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/api/UserService.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/api/UserService.scala @@ -20,64 +20,260 @@ package sample.cask.api import _root_.java.time.OffsetDateTime import _root_.sample.cask.model.User - +import scala.util.Failure +import scala.util.Try import _root_.sample.cask.model.* +/** + * The UserService companion object. + * + * Use the UserService() companion object to create an instance which returns a 'not implemented' error + * for each operation. + * + */ 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 'Handler' is an implementation of UserService convenient for delegating or overriding individual functions + */ + case class Handler[F[_]]( + createUserHandler : (user : User) => F[Unit], + createUsersWithArrayInputHandler : (user : Seq[User]) => F[Unit], + createUsersWithListInputHandler : (user : Seq[User]) => F[Unit], + deleteUserHandler : (username : String) => F[Unit], + getUserByNameHandler : (username : String) => F[User], + loginUserHandler : (username : String, password : String) => F[String], + logoutUserHandler : () => F[Unit], + updateUserHandler : (username : String, user : User) => F[Unit] + ) extends UserService[F] { + + override def createUser(user : User) : F[Unit] = { + createUserHandler(user) + } + + override def createUsersWithArrayInput(user : Seq[User]) : F[Unit] = { + createUsersWithArrayInputHandler(user) + } + + override def createUsersWithListInput(user : Seq[User]) : F[Unit] = { + createUsersWithListInputHandler(user) + } + + override def deleteUser(username : String) : F[Unit] = { + deleteUserHandler(username) + } + + override def getUserByName(username : String) : F[User] = { + getUserByNameHandler(username) + } + + override def loginUser(username : String, password : String) : F[String] = { + loginUserHandler(username, password) + } + + override def logoutUser() : F[Unit] = { + logoutUserHandler() + } + + override def updateUser(username : String, user : User) : F[Unit] = { + updateUserHandler(username, user) + } } + + def apply() : UserService[Try] = UserService.Handler[Try]( + (_) => notImplemented("createUser"), + (_) => notImplemented("createUsersWithArrayInput"), + (_) => notImplemented("createUsersWithListInput"), + (_) => notImplemented("deleteUser"), + (_) => notImplemented("getUserByName"), + (_, _) => notImplemented("loginUser"), + () => notImplemented("logoutUser"), + (_, _) => notImplemented("updateUser") + ) + + private def notImplemented(name : String) = Failure(new Exception(s"TODO: $name not implemented")) } /** * The User business-logic + * + * + * The 'asHandler' will return an implementation which allows for easily overriding individual operations. + * + * equally there are "on<Function>" helper methods for easily overriding individual functions + * + * @tparam F the effect type (Future, Try, IO, ID, etc) of the operations */ -trait UserService { +trait UserService[F[_]] { /** Create user * * @return */ - def createUser(user : User) : Unit + def createUser(user : User) : F[Unit] + + /** + * override createUser with the given handler + * @return a new implementation of UserService[F] with createUser overridden using the given handler + */ + final def onCreateUser(handler : (user : User) => F[Unit]) : UserService[F] = { + asHandler.copy(createUserHandler = handler) + } /** Creates list of users with given input array * * @return */ - def createUsersWithArrayInput(user : Seq[User]) : Unit + def createUsersWithArrayInput(user : Seq[User]) : F[Unit] + + /** + * override createUsersWithArrayInput with the given handler + * @return a new implementation of UserService[F] with createUsersWithArrayInput overridden using the given handler + */ + final def onCreateUsersWithArrayInput(handler : (user : Seq[User]) => F[Unit]) : UserService[F] = { + asHandler.copy(createUsersWithArrayInputHandler = handler) + } /** Creates list of users with given input array * * @return */ - def createUsersWithListInput(user : Seq[User]) : Unit + def createUsersWithListInput(user : Seq[User]) : F[Unit] + + /** + * override createUsersWithListInput with the given handler + * @return a new implementation of UserService[F] with createUsersWithListInput overridden using the given handler + */ + final def onCreateUsersWithListInput(handler : (user : Seq[User]) => F[Unit]) : UserService[F] = { + asHandler.copy(createUsersWithListInputHandler = handler) + } /** Delete user * * @return */ - def deleteUser(username : String) : Unit + def deleteUser(username : String) : F[Unit] + + /** + * override deleteUser with the given handler + * @return a new implementation of UserService[F] with deleteUser overridden using the given handler + */ + final def onDeleteUser(handler : (username : String) => F[Unit]) : UserService[F] = { + asHandler.copy(deleteUserHandler = handler) + } /** Get user by user name * * @return User */ - def getUserByName(username : String) : User + def getUserByName(username : String) : F[User] + + /** + * override getUserByName with the given handler + * @return a new implementation of UserService[F] with getUserByName overridden using the given handler + */ + final def onGetUserByName(handler : (username : String) => F[User]) : UserService[F] = { + asHandler.copy(getUserByNameHandler = handler) + } /** Logs user into the system * * @return String */ - def loginUser(username : String, password : String) : String + def loginUser(username : String, password : String) : F[String] + + /** + * override loginUser with the given handler + * @return a new implementation of UserService[F] with loginUser overridden using the given handler + */ + final def onLoginUser(handler : (username : String, password : String) => F[String]) : UserService[F] = { + asHandler.copy(loginUserHandler = handler) + } /** Logs out current logged in user session * * @return */ - def logoutUser() : Unit + def logoutUser() : F[Unit] + + /** + * override logoutUser with the given handler + * @return a new implementation of UserService[F] with logoutUser overridden using the given handler + */ + final def onLogoutUser(handler : () => F[Unit]) : UserService[F] = { + asHandler.copy(logoutUserHandler = handler) + } /** Updated user * * @return */ - def updateUser(username : String, user : User) : Unit + def updateUser(username : String, user : User) : F[Unit] + + /** + * override updateUser with the given handler + * @return a new implementation of UserService[F] with updateUser overridden using the given handler + */ + final def onUpdateUser(handler : (username : String, user : User) => F[Unit]) : UserService[F] = { + asHandler.copy(updateUserHandler = handler) + } + + /** + * @return a Handler implementation of this service + */ + final def asHandler : UserService.Handler[F] = this match { + case h : UserService.Handler[F] => h + case _ => + UserService.Handler[F]( + (user) => createUser(user), + (user) => createUsersWithArrayInput(user), + (user) => createUsersWithListInput(user), + (username) => deleteUser(username), + (username) => getUserByName(username), + (username, password) => loginUser(username, password), + () => logoutUser(), + (username, user) => updateUser(username, user) + ) + } + + /** + * This function will change the effect type of this service. + * + * It's not unlike a typical map operation from A => B, except we're not mapping + * a type from A to B, but rather from F[A] => G[A] using the 'changeEffect' function. + * + * For, this could turn an asynchronous service (one which returns Future[_] types) into + * a synchronous one (one which returns Try[_] types) by awaiting on the Future. + * + * It could change an IO type (like cats effect or ZIO) into an ID[A] which is just: + * ``` + * type ID[A] => A + * ``` + * + * @tparam G the new "polymorphic" effect type + * @param changeEffect the "natural transformation" which can change one effect type into another + * @return a new UserService service implementation with effect type [G] + */ + final def mapEffect[G[_]](changeEffect : [A] => F[A] => G[A]) : UserService[G] = { + val self = this + + new UserService[G] { + override def createUser(user : User) : G[Unit] = changeEffect { + self.createUser(user) + } + override def createUsersWithArrayInput(user : Seq[User]) : G[Unit] = changeEffect { + self.createUsersWithArrayInput(user) + } + override def createUsersWithListInput(user : Seq[User]) : G[Unit] = changeEffect { + self.createUsersWithListInput(user) + } + override def deleteUser(username : String) : G[Unit] = changeEffect { + self.deleteUser(username) + } + override def getUserByName(username : String) : G[User] = changeEffect { + self.getUserByName(username) + } + override def loginUser(username : String, password : String) : G[String] = changeEffect { + self.loginUser(username, password) + } + override def logoutUser() : G[Unit] = changeEffect { + self.logoutUser() + } + override def updateUser(username : String, user : User) : G[Unit] = changeEffect { + self.updateUser(username, user) + } + } + } } diff --git a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/ApiResponse.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/ApiResponse.scala index 4fb7f930c58..cafa7711645 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/ApiResponse.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/ApiResponse.scala @@ -13,41 +13,44 @@ // 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 + code: Option[Int] = None , + `type`: Option[String] = None , + message: Option[String] = None ) { - def asJsonString: String = asData.asJsonString - def asJson: ujson.Value = asData.asJson +def asJsonString: String = asData.asJsonString +def asJson: ujson.Value = asData.asJson - def asData : ApiResponseData = { - ApiResponseData( - code = code.getOrElse(0), - `type` = `type`.getOrElse(""), - message = message.getOrElse("") - - ) - } +def asData : ApiResponseData = { +ApiResponseData( + code = code.getOrElse(0) /* 1 */, + `type` = `type`.getOrElse("") /* 1 */, + message = message.getOrElse("") /* 1 */ + +) +} } object ApiResponse { - given RW[ApiResponse] = summon[RW[ujson.Value]].bimap[ApiResponse](_.asJson, json => read[ApiResponseData](json).asModel) +given RW[ApiResponse] = summon[RW[ujson.Value]].bimap[ApiResponse](_.asJson, json => read[ApiResponseData](json).asModel) - enum Fields(val fieldName : String) extends Field(fieldName) { - case code extends Fields("code") - case `type` extends Fields("`type`") - case message extends Fields("message") - } +enum Fields(val 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/shared/src/main/scala/sample/cask/model/ApiResponseData.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/ApiResponseData.scala index 9212af8a3df..4c80e1c743f 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/ApiResponseData.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/ApiResponseData.scala @@ -20,7 +20,8 @@ import scala.util.* import upickle.default.{ReadWriter => RW, macroRW} import upickle.default.* -/** ApiResponseData a data transfer object, primarily for simple json serialisation. + + /** 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( @@ -61,6 +62,9 @@ case class ApiResponseData( errors.toSeq } + /** + * @return the validated model within a Try (if successful) + */ def validated(failFast : Boolean = false) : scala.util.Try[ApiResponse] = { validationErrors(Vector(), failFast) match { case Seq() => Success(asModel) @@ -71,18 +75,9 @@ case class ApiResponseData( /** use 'validated' to check validation */ def asModel : ApiResponse = { ApiResponse( - code = Option( - code - ) - , - `type` = Option( - `type` - ) - , - message = Option( - message - ) - + code = Option(code) /* 1 */, + `type` = Option(`type`) /* 1 */, + message = Option(message) /* 1 */ ) } diff --git a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Category.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Category.scala index b143171f9d2..e89cbd7e81e 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Category.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Category.scala @@ -13,38 +13,41 @@ // 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 + id: Option[Long] = None , + name: Option[String] = None ) { - def asJsonString: String = asData.asJsonString - def asJson: ujson.Value = asData.asJson +def asJsonString: String = asData.asJsonString +def asJson: ujson.Value = asData.asJson - def asData : CategoryData = { - CategoryData( - id = id.getOrElse(0), - name = name.getOrElse("") - - ) - } +def asData : CategoryData = { +CategoryData( + id = id.getOrElse(0) /* 1 */, + name = name.getOrElse("") /* 1 */ + +) +} } object Category { - given RW[Category] = summon[RW[ujson.Value]].bimap[Category](_.asJson, json => read[CategoryData](json).asModel) +given RW[Category] = summon[RW[ujson.Value]].bimap[Category](_.asJson, json => read[CategoryData](json).asModel) - enum Fields(val fieldName : String) extends Field(fieldName) { - case id extends Fields("id") - case name extends Fields("name") - } +enum Fields(val fieldName : String) extends Field(fieldName) { + case id extends Fields("id") + case name extends Fields("name") +} } diff --git a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/CategoryData.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/CategoryData.scala index 5c0dcdfc4cc..7eb8a948970 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/CategoryData.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/CategoryData.scala @@ -20,7 +20,8 @@ import scala.util.* import upickle.default.{ReadWriter => RW, macroRW} import upickle.default.* -/** CategoryData a data transfer object, primarily for simple json serialisation. + + /** 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( @@ -60,6 +61,9 @@ case class CategoryData( errors.toSeq } + /** + * @return the validated model within a Try (if successful) + */ def validated(failFast : Boolean = false) : scala.util.Try[Category] = { validationErrors(Vector(), failFast) match { case Seq() => Success(asModel) @@ -70,14 +74,8 @@ case class CategoryData( /** use 'validated' to check validation */ def asModel : Category = { Category( - id = Option( - id - ) - , - name = Option( - name - ) - + id = Option(id) /* 1 */, + name = Option(name) /* 1 */ ) } diff --git a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Order.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Order.scala index 354e8a5e752..77dbcd6793b 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Order.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Order.scala @@ -14,51 +14,54 @@ // 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 + 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 asJsonString: String = asData.asJsonString - def asJson: ujson.Value = asData.asJson +def asJsonString: String = asData.asJsonString +def asJson: ujson.Value = 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) - - ) - } +def asData : OrderData = { +OrderData( + id = id.getOrElse(0) /* 1 */, + petId = petId.getOrElse(0) /* 1 */, + quantity = quantity.getOrElse(0) /* 1 */, + shipDate = shipDate.getOrElse(null) /* 1 */, + status = status.getOrElse(null) /* 1 */, + complete = complete.getOrElse(false) /* 1 */ + +) +} } object Order { - given RW[Order] = summon[RW[ujson.Value]].bimap[Order](_.asJson, json => read[OrderData](json).asModel) +given RW[Order] = summon[RW[ujson.Value]].bimap[Order](_.asJson, json => read[OrderData](json).asModel) - enum Fields(val 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") - } +enum Fields(val 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 diff --git a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/OrderData.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/OrderData.scala index 16d81bce47a..1330c2111c5 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/OrderData.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/OrderData.scala @@ -21,7 +21,8 @@ import scala.util.* import upickle.default.{ReadWriter => RW, macroRW} import upickle.default.* -/** OrderData a data transfer object, primarily for simple json serialisation. + + /** 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( @@ -84,6 +85,9 @@ case class OrderData( errors.toSeq } + /** + * @return the validated model within a Try (if successful) + */ def validated(failFast : Boolean = false) : scala.util.Try[Order] = { validationErrors(Vector(), failFast) match { case Seq() => Success(asModel) @@ -94,30 +98,12 @@ case class OrderData( /** 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 - ) - + id = Option(id) /* 1 */, + petId = Option(petId) /* 1 */, + quantity = Option(quantity) /* 1 */, + shipDate = Option(shipDate) /* 1 */, + status = Option(status) /* 1 */, + complete = Option(complete) /* 1 */ ) } diff --git a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Pet.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Pet.scala index 44c16221842..58725e10dcc 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Pet.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Pet.scala @@ -13,53 +13,54 @@ // 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 + 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 asJsonString: String = asData.asJsonString - def asJson: ujson.Value = asData.asJson +def asJsonString: String = asData.asJsonString +def asJson: ujson.Value = 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) - - ) - } +def asData : PetData = { +PetData( + id = id.getOrElse(0) /* 1 */, + category = category.map(_.asData).getOrElse(null) /* 4 */, + name = name /* 2 */, + photoUrls = photoUrls /* 2 */, + tags = tags.map(_.asData) /* 6 */, + status = status.getOrElse(null) /* 1 */ + +) +} } object Pet { - given RW[Pet] = summon[RW[ujson.Value]].bimap[Pet](_.asJson, json => read[PetData](json).asModel) +given RW[Pet] = summon[RW[ujson.Value]].bimap[Pet](_.asJson, json => read[PetData](json).asModel) - enum Fields(val 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") - } +enum Fields(val 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 diff --git a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/PetData.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/PetData.scala index ea7a34c6193..755d33c288d 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/PetData.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/PetData.scala @@ -13,8 +13,6 @@ // 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.* @@ -22,7 +20,8 @@ import scala.util.* import upickle.default.{ReadWriter => RW, macroRW} import upickle.default.* -/** PetData a data transfer object, primarily for simple json serialisation. + + /** 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( @@ -101,6 +100,9 @@ case class PetData( errors.toSeq } + /** + * @return the validated model within a Try (if successful) + */ def validated(failFast : Boolean = false) : scala.util.Try[Pet] = { validationErrors(Vector(), failFast) match { case Seq() => Success(asModel) @@ -111,30 +113,12 @@ case class PetData( /** 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 - ) - + id = Option(id) /* 1 */, + category = Option(category).map(_.asModel) /* 4 */, + name = name /* 2 */, + photoUrls = photoUrls /* 2 */, + tags = tags.map(_.asModel) /* 5 */, + status = Option(status) /* 1 */ ) } diff --git a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Tag.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Tag.scala index 0406c84c56c..fa6805f6c33 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Tag.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Tag.scala @@ -13,38 +13,41 @@ // 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 + id: Option[Long] = None , + name: Option[String] = None ) { - def asJsonString: String = asData.asJsonString - def asJson: ujson.Value = asData.asJson +def asJsonString: String = asData.asJsonString +def asJson: ujson.Value = asData.asJson - def asData : TagData = { - TagData( - id = id.getOrElse(0), - name = name.getOrElse("") - - ) - } +def asData : TagData = { +TagData( + id = id.getOrElse(0) /* 1 */, + name = name.getOrElse("") /* 1 */ + +) +} } object Tag { - given RW[Tag] = summon[RW[ujson.Value]].bimap[Tag](_.asJson, json => read[TagData](json).asModel) +given RW[Tag] = summon[RW[ujson.Value]].bimap[Tag](_.asJson, json => read[TagData](json).asModel) - enum Fields(val fieldName : String) extends Field(fieldName) { - case id extends Fields("id") - case name extends Fields("name") - } +enum Fields(val fieldName : String) extends Field(fieldName) { + case id extends Fields("id") + case name extends Fields("name") +} } diff --git a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/TagData.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/TagData.scala index e6aa8c81642..972b21dd518 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/TagData.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/TagData.scala @@ -20,7 +20,8 @@ import scala.util.* import upickle.default.{ReadWriter => RW, macroRW} import upickle.default.* -/** TagData a data transfer object, primarily for simple json serialisation. + + /** 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( @@ -54,6 +55,9 @@ case class TagData( errors.toSeq } + /** + * @return the validated model within a Try (if successful) + */ def validated(failFast : Boolean = false) : scala.util.Try[Tag] = { validationErrors(Vector(), failFast) match { case Seq() => Success(asModel) @@ -64,14 +68,8 @@ case class TagData( /** use 'validated' to check validation */ def asModel : Tag = { Tag( - id = Option( - id - ) - , - name = Option( - name - ) - + id = Option(id) /* 1 */, + name = Option(name) /* 1 */ ) } diff --git a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/User.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/User.scala index 5dfb0761b78..55614de4f87 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/User.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/User.scala @@ -13,57 +13,60 @@ // 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 + 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 asJsonString: String = asData.asJsonString - def asJson: ujson.Value = asData.asJson +def asJsonString: String = asData.asJsonString +def asJson: ujson.Value = 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) - - ) - } +def asData : UserData = { +UserData( + id = id.getOrElse(0) /* 1 */, + username = username.getOrElse("") /* 1 */, + firstName = firstName.getOrElse("") /* 1 */, + lastName = lastName.getOrElse("") /* 1 */, + email = email.getOrElse("") /* 1 */, + password = password.getOrElse("") /* 1 */, + phone = phone.getOrElse("") /* 1 */, + userStatus = userStatus.getOrElse(0) /* 1 */ + +) +} } object User { - given RW[User] = summon[RW[ujson.Value]].bimap[User](_.asJson, json => read[UserData](json).asModel) +given RW[User] = summon[RW[ujson.Value]].bimap[User](_.asJson, json => read[UserData](json).asModel) - enum Fields(val 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") - } +enum Fields(val 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/shared/src/main/scala/sample/cask/model/UserData.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/UserData.scala index 42cd2a14f83..07b5cb1de41 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/UserData.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/UserData.scala @@ -20,7 +20,8 @@ import scala.util.* import upickle.default.{ReadWriter => RW, macroRW} import upickle.default.* -/** UserData a data transfer object, primarily for simple json serialisation. + + /** 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( @@ -97,6 +98,9 @@ case class UserData( errors.toSeq } + /** + * @return the validated model within a Try (if successful) + */ def validated(failFast : Boolean = false) : scala.util.Try[User] = { validationErrors(Vector(), failFast) match { case Seq() => Success(asModel) @@ -107,38 +111,14 @@ case class UserData( /** 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 - ) - + id = Option(id) /* 1 */, + username = Option(username) /* 1 */, + firstName = Option(firstName) /* 1 */, + lastName = Option(lastName) /* 1 */, + email = Option(email) /* 1 */, + password = Option(password) /* 1 */, + phone = Option(phone) /* 1 */, + userStatus = Option(userStatus) /* 1 */ ) }