Scala cask api effects (#19936)

* Scala-cask improvements:

 * fixe for grouped methods which have routes containing dashes.

Previously our OperationGroup work-around would potentially
Create methods like ‘foo-bar’, which isn’t a valid function name

 * Fix to not import some.package.Array[Byte] when binary format is specified

 * Fix for grouped operations which contain duplicate query parameters

 * Fix for binary response fields. This can come up with the following example

        "responses" : {
          "200" : {
            "content" : {
              "application/json" : {
                "schema" : {
                  "format" : "binary",
                  "type" : "string"
                }
              }
            },
            "description" : "data"
          },

 * Fix for enum model classes
Extracted complex logic for ‘asData’ and ‘asModel’ transformations for properties

 * Introduced a generic effect F[_] for services

This was done to support composable services
(Service A calls Service B) by using monadic
Effect types (ones which can flatMap)

 * Fixed unique union types for responses, asModel and asData fixes for non-model types

* scala-cask: regenerated samples

* Fix for reserved-word properties in the API

* Fix for null imports and reserved-word enum types

* Fixes for api methods with backticked params

* Fix for duplicate (by name) grouped params

* small syntax fix

* logging response type

* Regenerated samples

* String.format fix
This commit is contained in:
Aaron Pritzlaff 2024-11-06 08:14:31 +00:00 committed by GitHub
parent cded99c3fc
commit b51b18e3ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 1725 additions and 821 deletions

View File

@ -31,6 +31,8 @@ import org.slf4j.LoggerFactory;
import java.io.File; import java.io.File;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -125,7 +127,7 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
// mapped to String as a workaround // mapped to String as a workaround
typeMapping.put("binary", "String"); 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.GROUP_ID, CodegenConstants.GROUP_ID_DESC));
cliOptions.add(new CliOption(CodegenConstants.ARTIFACT_ID, CodegenConstants.ARTIFACT_ID_DESC)); cliOptions.add(new CliOption(CodegenConstants.ARTIFACT_ID, CodegenConstants.ARTIFACT_ID_DESC));
@ -150,8 +152,12 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
@Override @Override
public String getSchemaType(Schema p) { public String getSchemaType(Schema p) {
if (ModelUtils.isFreeFormObject(p, openAPI)) { 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 // We're opinionated in this template to use ujson
return "Value"; return AdditionalPropertiesType;
} }
return super.getSchemaType(p); return super.getSchemaType(p);
} }
@ -246,7 +252,6 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
importMapping.put("LocalDate", "java.time.LocalDate"); importMapping.put("LocalDate", "java.time.LocalDate");
importMapping.put("OffsetDateTime", "java.time.OffsetDateTime"); importMapping.put("OffsetDateTime", "java.time.OffsetDateTime");
importMapping.put("LocalTime", "java.time.LocalTime"); importMapping.put("LocalTime", "java.time.LocalTime");
importMapping.put("Value", "ujson.Value");
importMapping.put(AdditionalPropertiesType, "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 @Override
public String toApiName(String name) { public String toApiName(String name) {
if (name.isEmpty()) { if (name.isEmpty()) {
@ -316,8 +308,7 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
@Override @Override
public String apiFileFolder() { public String apiFileFolder() {
final String folder = outputFolder + "/jvm/" + sourceFolder + "/" + apiPackage().replace('.', File.separatorChar); return outputFolder + "/jvm/" + sourceFolder + "/" + apiPackage().replace('.', File.separatorChar);
return folder;
} }
@Override @Override
@ -329,6 +320,22 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
return outputFolder + "/shared/" + sourceFolder + "/" + apiPackage().replace('.', File.separatorChar); 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) { static String capitalise(String p) {
if (p.length() < 2) { if (p.length() < 2) {
return p.toUpperCase(Locale.ROOT); return p.toUpperCase(Locale.ROOT);
@ -470,14 +477,24 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
* *
* @return the CodegenParameters * @return the CodegenParameters
*/ */
public List<CodegenParameter> getGroupQueryParams() { public Collection<CodegenParameter> getGroupQueryParams() {
List<CodegenParameter> list = operations.stream().flatMap(op -> op.queryParams.stream()).map(p -> {
// wow is this a pain to do in Java ... I just wanted to have distinct items of a list
// based on their name.
Set<String> alreadySeen = new HashSet<>();
List<CodegenParameter> list = operations.stream().flatMap(op -> op.queryParams.stream()).flatMap(p -> {
if (alreadySeen.add(p.paramName)) {
final CodegenParameter copy = p.copy(); final CodegenParameter copy = p.copy();
copy.vendorExtensions.put("x-default-value", defaultValue(p)); 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.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.dataType = asScalaDataType(copy, false, true, true);
copy.defaultValue = defaultValue(copy); copy.defaultValue = defaultValue(copy);
return copy; return Arrays.asList(copy).stream();
} else {
final List<CodegenParameter> empty = Collections.emptyList();
return empty.stream();
}
} }
).collect(Collectors.toList()); ).collect(Collectors.toList());
@ -498,7 +515,7 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
List<String> stripped = Arrays.stream(pathPrefix.split("/", -1)) List<String> stripped = Arrays.stream(pathPrefix.split("/", -1))
.map(ScalaCaskServerCodegen::capitalise).collect(Collectors.toList()); .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) { public void add(CodegenOperation op) {
@ -552,6 +569,7 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
String prefix = nonParamPathPrefix(op); String prefix = nonParamPathPrefix(op);
String key = op.httpMethod + " " + prefix; String key = op.httpMethod + " " + prefix;
if (!op.pathParams.isEmpty()) { if (!op.pathParams.isEmpty()) {
final ScalaCaskServerCodegen.OperationGroup group = groupedByPrefix.getOrDefault(key, new ScalaCaskServerCodegen.OperationGroup(op.httpMethod, prefix)); final ScalaCaskServerCodegen.OperationGroup group = groupedByPrefix.getOrDefault(key, new ScalaCaskServerCodegen.OperationGroup(op.httpMethod, prefix));
group.add(op); group.add(op);
groupedByPrefix.put(key, group); groupedByPrefix.put(key, group);
@ -603,6 +621,25 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
final Map<String, Object> operations = (Map<String, Object>) objs.get("operations"); final Map<String, Object> operations = (Map<String, Object>) objs.get("operations");
final List<CodegenOperation> operationList = (List<CodegenOperation>) operations.get("operation"); final List<CodegenOperation> operationList = (List<CodegenOperation>) operations.get("operation");
/**
* In this case, there is a import set to 'null':
*
* {{{
* ...
* responses:
* "200":
* content:
* application/json:
* schema:
* format: byte
* type: string
* }}}
*/
List<Map<String, String>> imports = (List<Map<String, String>>) 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)); objs.put("route-groups", createRouteGroups(operationList));
operationList.forEach(ScalaCaskServerCodegen::postProcessOperation); operationList.forEach(ScalaCaskServerCodegen::postProcessOperation);
@ -611,6 +648,7 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
@Override @Override
public ModelsMap postProcessModels(ModelsMap objs) { public ModelsMap postProcessModels(ModelsMap objs) {
super.postProcessModels(objs);
objs.getModels().stream().map(ModelMap::getModel).forEach(this::postProcessModel); objs.getModels().stream().map(ModelMap::getModel).forEach(this::postProcessModel);
return objs; return objs;
} }
@ -632,8 +670,8 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
model.getAllVars().forEach(this::setDefaultValueForCodegenProperty); model.getAllVars().forEach(this::setDefaultValueForCodegenProperty);
model.getVars().forEach(this::setDefaultValueForCodegenProperty); model.getVars().forEach(this::setDefaultValueForCodegenProperty);
model.getVars().forEach(ScalaCaskServerCodegen::postProcessProperty); model.getVars().forEach(this::postProcessProperty);
model.getAllVars().forEach(ScalaCaskServerCodegen::postProcessProperty); model.getAllVars().forEach(this::postProcessProperty);
} }
private static void postProcessOperation(CodegenOperation op) { 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' */ /* Put in 'x-consumes-json' and 'x-consumes-xml' */
op.vendorExtensions.put("x-consumes-json", consumesMimetype(op, "application/json")); op.vendorExtensions.put("x-consumes-json", consumesMimetype(op, "application/json"));
op.vendorExtensions.put("x-consumes-xml", consumesMimetype(op, "application/xml")); 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) -> { op.bodyParams.stream().filter((b) -> b.isBodyParam).forEach((p) -> {
p.vendorExtensions.put("x-consumes-json", consumesMimetype(op, "application/json")); 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-cask-path-typed", routeArgs(op));
op.vendorExtensions.put("x-query-args", queryArgs(op)); op.vendorExtensions.put("x-query-args", queryArgs(op));
List<String> responses = op.responses.stream().map(r -> r.dataType).filter(Objects::nonNull).collect(Collectors.toList()); LinkedHashSet<String> responses = op.responses.stream().map(r -> r.dataType)
op.vendorExtensions.put("x-response-type", responses.isEmpty() ? "Unit" : String.join(" | ", responses)); .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<String> 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 <Thing> {
* ...
* def asData = <Thing>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<String> 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 <Thing>Data {
* ...
* def asModel = <Thing>(
* someProp = ... <-- how do we turn this property into a model property?
* )
* }
* }}}
*
* @param p
* @return
*/
private static String asModelCode(final CodegenProperty p, final Set<String> 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-datatype-model", asScalaDataType(p, p.required, false));
p.vendorExtensions.put("x-defaultValue-model", defaultValue(p, p.required, p.defaultValue)); 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-datatype-data", dataTypeData);
p.vendorExtensions.put("x-containertype-data", containerType(dataTypeData)); p.vendorExtensions.put("x-containertype-data", containerType(dataTypeData));
p.vendorExtensions.put("x-defaultValue-data", defaultValueNonOption(p, p.defaultValue)); 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 "<Foo>Data" model for unvalidated data and a "<Foo>" model
// which has passed validation.
//
// The <model> 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 // the 'asModel' logic for modelData.mustache
// //
// if it's optional (not required), then wrap the value in Option() // 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 // ... 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, // 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 // 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 * @return true if the property is numeric
*/ */
private static boolean isNumeric(IJsonSchemaValidationProperties p) { 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) { if (p instanceof CodegenParameter) {
return ((CodegenParameter)p).isNumeric; return ((CodegenParameter)p).isNumeric;
} else if (p instanceof CodegenProperty) { } else if (p instanceof CodegenProperty) {
@ -829,8 +1072,8 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
// Customize type for freeform objects // Customize type for freeform objects
if (ModelUtils.isFreeFormObject(schema, openAPI)) { if (ModelUtils.isFreeFormObject(schema, openAPI)) {
property.dataType = "Value"; property.dataType = AdditionalPropertiesType;
property.baseType = "Value"; property.baseType = AdditionalPropertiesType;
} }
return property; return property;
@ -839,7 +1082,11 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
@Override @Override
public String getTypeDeclaration(Schema schema) { public String getTypeDeclaration(Schema schema) {
if (ModelUtils.isFreeFormObject(schema, openAPI)) { 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); return super.getTypeDeclaration(schema);
} }
@ -850,6 +1097,11 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
if (importMapping.containsKey(name)) { if (importMapping.containsKey(name)) {
result = importMapping.get(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; 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) { 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(); String dataType = (param.getIsModel() && useJason) ? param.getDataType() + "Data" : param.getDataType();
final String dataSuffix = useJason && param.getItems() != null && param.getItems().getIsModel() ? "Data" : ""; final String dataSuffix = useJason && param.getItems() != null && param.getItems().getIsModel() ? "Data" : "";

View File

@ -13,6 +13,16 @@ import scala.reflect.ClassTag
import scala.util.* import scala.util.*
import upickle.default.* 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 // needed for BigDecimal params
given cask.endpoints.QueryParamReader.SimpleParam[BigDecimal](BigDecimal.apply) given cask.endpoints.QueryParamReader.SimpleParam[BigDecimal](BigDecimal.apply)

View File

@ -10,11 +10,12 @@ import {{modelPackage}}.*
import upickle.default.{ReadWriter => RW, macroRW} import upickle.default.{ReadWriter => RW, macroRW}
import upickle.default.* import upickle.default.*
import scala.util.Try
{{#imports}}import {{import}} {{#imports}}import {{import}}
{{/imports}} {{/imports}}
class {{classname}}Routes(service : {{classname}}Service) extends cask.Routes { class {{classname}}Routes(service : {{classname}}Service[Try]) extends cask.Routes {
{{#route-groups}} {{#route-groups}}
// route group for {{methodName}} // route group for {{methodName}}
@ -35,7 +36,7 @@ class {{classname}}Routes(service : {{classname}}Service) extends cask.Routes {
* {{description}} * {{description}}
*/ */
{{vendorExtensions.x-annotation}}("{{vendorExtensions.x-cask-path}}") {{vendorExtensions.x-annotation}}("{{vendorExtensions.x-cask-path}}")
def {{operationId}}({{vendorExtensions.x-cask-path-typed}}) = { def {{operationId}}({{{vendorExtensions.x-cask-path-typed}}}) = {
{{#authMethods}} {{#authMethods}}
// auth method {{name}} : {{type}}, keyParamName: {{keyParamName}} // auth method {{name}} : {{type}}, keyParamName: {{keyParamName}}
{{/authMethods}} {{/authMethods}}

View File

@ -8,30 +8,123 @@ package {{apiPackage}}
{{#imports}}import _root_.{{import}} {{#imports}}import _root_.{{import}}
{{/imports}} {{/imports}}
import scala.util.Failure
import scala.util.Try
import _root_.{{modelPackage}}.* 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 { 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}} {{#operations}}
{{#operation}} {{#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}} {{/operation}}
{{/operations}} {{/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 {{classname}} business-logic
*
*
* The 'asHandler' will return an implementation which allows for easily overriding individual operations.
*
* equally there are "on&lt;Function&gt;" 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}} {{#operations}}
{{#operation}} {{#operation}}
/** {{{summary}}} /** {{{summary}}}
* {{{description}}} * {{{description}}}
* @return {{returnType}} * @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}} {{/operation}}
{{/operations}} {{/operations}}
} }
}
}

View File

@ -2,10 +2,10 @@
//> using lib "com.lihaoyi::cask:0.9.2" //> using lib "com.lihaoyi::cask:0.9.2"
//> using lib "com.lihaoyi::scalatags:0.8.2" //> using lib "com.lihaoyi::scalatags:0.8.2"
{{>licenseInfo}} {{>licenseInfo}}
// this file was generated from app.mustache // this file was generated from app.mustache
package {{packageName}} package {{packageName}}
import scala.util.Try
{{#imports}}import {{import}} {{#imports}}import {{import}}
{{/imports}} {{/imports}}
import _root_.{{modelPackage}}.* import _root_.{{modelPackage}}.*
@ -20,16 +20,17 @@ import _root_.{{apiPackage}}.*
* If you wanted fine-grained control over the routes and services, you could * 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: * extend the cask.MainRoutes and mix in this trait by using this:
* *
* \{\{\{ * ```
* override def allRoutes = appRoutes * override def allRoutes = appRoutes
* \}\}\} * ```
* *
* More typically, however, you would extend the 'BaseApp' class * More typically, however, you would extend the 'BaseApp' class
*/ */
trait AppRoutes { trait AppRoutes {
{{#operations}} {{#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) def routeFor{{classname}} : {{classname}}Routes = {{classname}}Routes(app{{classname}}Service)
{{/operations}} {{/operations}}
def appRoutes = Seq( def appRoutes = Seq(

View File

@ -6,6 +6,7 @@
// this file was generated from app.mustache // this file was generated from app.mustache
package {{packageName}} package {{packageName}}
import scala.util.Try
{{#imports}}import {{import}} {{#imports}}import {{import}}
{{/imports}} {{/imports}}
import _root_.{{modelPackage}}.* import _root_.{{modelPackage}}.*
@ -16,7 +17,7 @@ import _root_.{{apiPackage}}.*
* passing in the custom business logic services * passing in the custom business logic services
*/ */
class BaseApp({{#operations}} class BaseApp({{#operations}}
override val app{{classname}}Service : {{classname}}Service = {{classname}}Service(), override val app{{classname}}Service : {{classname}}Service[Try] = {{classname}}Service(),
{{/operations}} {{/operations}}
override val port : Int = sys.env.get("PORT").map(_.toInt).getOrElse(8080)) extends cask.MainRoutes with AppRoutes { override val port : Int = sys.env.get("PORT").map(_.toInt).getOrElse(8080)) extends cask.MainRoutes with AppRoutes {

View File

@ -3,6 +3,7 @@
package {{modelPackage}} package {{modelPackage}}
{{#imports}}import {{import}} {{#imports}}import {{import}}
{{/imports}} {{/imports}}
import scala.util.control.NonFatal import scala.util.control.NonFatal
// see https://com-lihaoyi.github.io/upickle/ // see https://com-lihaoyi.github.io/upickle/
@ -11,52 +12,13 @@ import upickle.default.*
{{#models}} {{#models}}
{{#model}} {{#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}} {{#isEnum}}
// baseName={{{baseName}}} {{>modelEnum}}
// nameInCamelCase = {{{nameInCamelCase}}} {{/isEnum}}
enum {{datatypeWithEnum}} derives ReadWriter { {{^isEnum}}
{{#_enum}} {{>modelClass}}
case {{.}}
{{/_enum}}
}
{{/isEnum}} {{/isEnum}}
{{/vars}}
}
{{/model}} {{/model}}
{{/models}} {{/models}}

View File

@ -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}}
}

View File

@ -12,250 +12,12 @@ import upickle.default.*
{{#models}} {{#models}}
{{#model}} {{#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}} {{/model}}
{{/models}} {{/models}}

View File

@ -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
}
}
}
}

View File

@ -0,0 +1 @@
type {{classname}}Data = {{classname}}

View File

@ -0,0 +1,6 @@
enum {{classname}} derives RW :
{{#allowableValues}}
{{#values}}
case {{.}}
{{/values}}
{{/allowableValues}}

View File

@ -20,6 +20,8 @@ class {{classname}}Test extends AnyWordSpec with Matchers {
value= {{value}} value= {{value}}
{{/examples}} {{/examples}}
{{/operations}} {{/operations}}
{{^isEnum}}
"{{classname}}.fromJson" should { "{{classname}}.fromJson" should {
"""not parse invalid json""" in { """not parse invalid json""" in {
val Failure(err) = Try({{classname}}Data.fromJsonString("invalid jason")) val Failure(err) = Try({{classname}}Data.fromJsonString("invalid jason"))
@ -31,7 +33,7 @@ class {{classname}}Test extends AnyWordSpec with Matchers {
sys.error("TODO") sys.error("TODO")
} }
} }
{{/isEnum}}
} }
{{/model}} {{/model}}
{{/models}} {{/models}}

View File

@ -53,5 +53,6 @@
{{/vendorExtensions.x-consumes-xml}} {{/vendorExtensions.x-consumes-xml}}
{{/vendorExtensions.x-consumes-json}} {{/vendorExtensions.x-consumes-json}}
{{/bodyParams}} {{/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 } yield result

View File

@ -14,10 +14,10 @@
* https://openapi-generator.tech * https://openapi-generator.tech
*/ */
// this file was generated from app.mustache // this file was generated from app.mustache
package cask.groupId.server package cask.groupId.server
import scala.util.Try
import _root_.sample.cask.model.* import _root_.sample.cask.model.*
import _root_.sample.cask.api.* 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 * 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: * extend the cask.MainRoutes and mix in this trait by using this:
* *
* \{\{\{ * ```
* override def allRoutes = appRoutes * override def allRoutes = appRoutes
* \}\}\} * ```
* *
* More typically, however, you would extend the 'BaseApp' class * More typically, however, you would extend the 'BaseApp' class
*/ */
trait AppRoutes { trait AppRoutes {
def appPetService : PetService = PetService() def appPetService : PetService[Try] = PetService()
def routeForPet : PetRoutes = PetRoutes(appPetService) def routeForPet : PetRoutes = PetRoutes(appPetService)
def appStoreService : StoreService = StoreService()
def appStoreService : StoreService[Try] = StoreService()
def routeForStore : StoreRoutes = StoreRoutes(appStoreService) def routeForStore : StoreRoutes = StoreRoutes(appStoreService)
def appUserService : UserService = UserService()
def appUserService : UserService[Try] = UserService()
def routeForUser : UserRoutes = UserRoutes(appUserService) def routeForUser : UserRoutes = UserRoutes(appUserService)
def appRoutes = Seq( def appRoutes = Seq(
routeForPet , routeForPet ,
routeForStore , routeForStore ,

View File

@ -18,6 +18,7 @@
// this file was generated from app.mustache // this file was generated from app.mustache
package cask.groupId.server package cask.groupId.server
import scala.util.Try
import _root_.sample.cask.model.* import _root_.sample.cask.model.*
import _root_.sample.cask.api.* import _root_.sample.cask.api.*
@ -26,11 +27,11 @@ import _root_.sample.cask.api.*
* passing in the custom business logic services * passing in the custom business logic services
*/ */
class BaseApp( 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 { override val port : Int = sys.env.get("PORT").map(_.toInt).getOrElse(8080)) extends cask.MainRoutes with AppRoutes {
/** routes for the UI /** routes for the UI

View File

@ -22,12 +22,13 @@ import sample.cask.model.*
import upickle.default.{ReadWriter => RW, macroRW} import upickle.default.{ReadWriter => RW, macroRW}
import upickle.default.* import upickle.default.*
import scala.util.Try
import sample.cask.model.ApiResponse import sample.cask.model.ApiResponse
import java.io.File import java.io.File
import sample.cask.model.Pet import sample.cask.model.Pet
class PetRoutes(service : PetService) extends cask.Routes { class PetRoutes(service : PetService[Try]) extends cask.Routes {
// route group for routeWorkAroundForPOSTPet // route group for routeWorkAroundForPOSTPet
@cask.post("/pet", true) @cask.post("/pet", true)
@ -63,7 +64,8 @@ class PetRoutes(service : PetService) extends cask.Routes {
petJson <- Parsed.fromTry(request.bodyAsJson) petJson <- Parsed.fromTry(request.bodyAsJson)
petData <- Parsed.eval(PetData.fromJson(petJson)) /* not array or map */ petData <- Parsed.eval(PetData.fromJson(petJson)) /* not array or map */
pet <- Parsed.fromTry(petData.validated(failFast)) pet <- Parsed.fromTry(petData.validated(failFast))
result <- Parsed.eval(service.addPet(pet)) resultTry <- Parsed.eval(service.addPet(pet))
result <- Parsed.fromTry(resultTry)
} yield result } yield result
(result : @unchecked) match { (result : @unchecked) match {
@ -84,7 +86,8 @@ class PetRoutes(service : PetService) extends cask.Routes {
val result = for { val result = for {
petId <- Parsed(petId) petId <- Parsed(petId)
apiKey <- request.headerSingleValueOptional("apiKey") apiKey <- request.headerSingleValueOptional("apiKey")
result <- Parsed.eval(service.deletePet(petId, apiKey)) resultTry <- Parsed.eval(service.deletePet(petId, apiKey))
result <- Parsed.fromTry(resultTry)
} yield result } yield result
(result : @unchecked) match { (result : @unchecked) match {
@ -102,7 +105,8 @@ class PetRoutes(service : PetService) extends cask.Routes {
def failFast = request.queryParams.keySet.contains("failFast") def failFast = request.queryParams.keySet.contains("failFast")
val result = for { val result = for {
result <- Parsed.eval(service.findPetsByStatus(status)) resultTry <- Parsed.eval(service.findPetsByStatus(status))
result <- Parsed.fromTry(resultTry)
} yield result } yield result
(result : @unchecked) match { (result : @unchecked) match {
@ -121,7 +125,8 @@ class PetRoutes(service : PetService) extends cask.Routes {
def failFast = request.queryParams.keySet.contains("failFast") def failFast = request.queryParams.keySet.contains("failFast")
val result = for { val result = for {
result <- Parsed.eval(service.findPetsByTags(tags)) resultTry <- Parsed.eval(service.findPetsByTags(tags))
result <- Parsed.fromTry(resultTry)
} yield result } yield result
(result : @unchecked) match { (result : @unchecked) match {
@ -141,7 +146,8 @@ class PetRoutes(service : PetService) extends cask.Routes {
val result = for { val result = for {
petId <- Parsed(petId) petId <- Parsed(petId)
result <- Parsed.eval(service.getPetById(petId)) resultTry <- Parsed.eval(service.getPetById(petId))
result <- Parsed.fromTry(resultTry)
} yield result } yield result
(result : @unchecked) match { (result : @unchecked) match {
@ -163,7 +169,8 @@ class PetRoutes(service : PetService) extends cask.Routes {
petJson <- Parsed.fromTry(request.bodyAsJson) petJson <- Parsed.fromTry(request.bodyAsJson)
petData <- Parsed.eval(PetData.fromJson(petJson)) /* not array or map */ petData <- Parsed.eval(PetData.fromJson(petJson)) /* not array or map */
pet <- Parsed.fromTry(petData.validated(failFast)) pet <- Parsed.fromTry(petData.validated(failFast))
result <- Parsed.eval(service.updatePet(pet)) resultTry <- Parsed.eval(service.updatePet(pet))
result <- Parsed.fromTry(resultTry)
} yield result } yield result
(result : @unchecked) match { (result : @unchecked) match {
@ -185,7 +192,8 @@ class PetRoutes(service : PetService) extends cask.Routes {
petId <- Parsed(petId) petId <- Parsed(petId)
name <- request.formSingleValueOptional("name") name <- request.formSingleValueOptional("name")
status <- request.formSingleValueOptional("status") 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 } yield result
(result : @unchecked) match { (result : @unchecked) match {
@ -206,7 +214,8 @@ class PetRoutes(service : PetService) extends cask.Routes {
petId <- Parsed(petId) petId <- Parsed(petId)
additionalMetadata <- request.formSingleValueOptional("additionalMetadata") additionalMetadata <- request.formSingleValueOptional("additionalMetadata")
file <- request.formValueAsFileOptional("file") 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 } yield result
(result : @unchecked) match { (result : @unchecked) match {

View File

@ -22,10 +22,11 @@ import sample.cask.model.*
import upickle.default.{ReadWriter => RW, macroRW} import upickle.default.{ReadWriter => RW, macroRW}
import upickle.default.* import upickle.default.*
import scala.util.Try
import sample.cask.model.Order 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 /** Delete purchase order by ID
@ -38,7 +39,8 @@ class StoreRoutes(service : StoreService) extends cask.Routes {
val result = for { val result = for {
orderId <- Parsed(orderId) orderId <- Parsed(orderId)
result <- Parsed.eval(service.deleteOrder(orderId)) resultTry <- Parsed.eval(service.deleteOrder(orderId))
result <- Parsed.fromTry(resultTry)
} yield result } yield result
(result : @unchecked) match { (result : @unchecked) match {
@ -56,7 +58,8 @@ class StoreRoutes(service : StoreService) extends cask.Routes {
def failFast = request.queryParams.keySet.contains("failFast") def failFast = request.queryParams.keySet.contains("failFast")
val result = for { val result = for {
result <- Parsed.eval(service.getInventory()) resultTry <- Parsed.eval(service.getInventory())
result <- Parsed.fromTry(resultTry)
} yield result } yield result
(result : @unchecked) match { (result : @unchecked) match {
@ -75,7 +78,8 @@ class StoreRoutes(service : StoreService) extends cask.Routes {
val result = for { val result = for {
orderId <- Parsed(orderId) orderId <- Parsed(orderId)
result <- Parsed.eval(service.getOrderById(orderId)) resultTry <- Parsed.eval(service.getOrderById(orderId))
result <- Parsed.fromTry(resultTry)
} yield result } yield result
(result : @unchecked) match { (result : @unchecked) match {
@ -96,7 +100,8 @@ class StoreRoutes(service : StoreService) extends cask.Routes {
orderJson <- Parsed.fromTry(request.bodyAsJson) orderJson <- Parsed.fromTry(request.bodyAsJson)
orderData <- Parsed.eval(OrderData.fromJson(orderJson)) /* not array or map */ orderData <- Parsed.eval(OrderData.fromJson(orderJson)) /* not array or map */
order <- Parsed.fromTry(orderData.validated(failFast)) order <- Parsed.fromTry(orderData.validated(failFast))
result <- Parsed.eval(service.placeOrder(order)) resultTry <- Parsed.eval(service.placeOrder(order))
result <- Parsed.fromTry(resultTry)
} yield result } yield result
(result : @unchecked) match { (result : @unchecked) match {

View File

@ -22,11 +22,12 @@ import sample.cask.model.*
import upickle.default.{ReadWriter => RW, macroRW} import upickle.default.{ReadWriter => RW, macroRW}
import upickle.default.* import upickle.default.*
import scala.util.Try
import java.time.OffsetDateTime import java.time.OffsetDateTime
import sample.cask.model.User import sample.cask.model.User
class UserRoutes(service : UserService) extends cask.Routes { class UserRoutes(service : UserService[Try]) extends cask.Routes {
// route group for routeWorkAroundForGETUser // route group for routeWorkAroundForGETUser
@cask.get("/user", true) @cask.get("/user", true)
@ -52,7 +53,8 @@ class UserRoutes(service : UserService) extends cask.Routes {
userJson <- Parsed.fromTry(request.bodyAsJson) userJson <- Parsed.fromTry(request.bodyAsJson)
userData <- Parsed.eval(UserData.fromJson(userJson)) /* not array or map */ userData <- Parsed.eval(UserData.fromJson(userJson)) /* not array or map */
user <- Parsed.fromTry(userData.validated(failFast)) user <- Parsed.fromTry(userData.validated(failFast))
result <- Parsed.eval(service.createUser(user)) resultTry <- Parsed.eval(service.createUser(user))
result <- Parsed.fromTry(resultTry)
} yield result } yield result
(result : @unchecked) match { (result : @unchecked) match {
@ -71,7 +73,8 @@ class UserRoutes(service : UserService) extends cask.Routes {
val result = for { 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 */ 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 } yield result
(result : @unchecked) match { (result : @unchecked) match {
@ -90,7 +93,8 @@ class UserRoutes(service : UserService) extends cask.Routes {
val result = for { 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 */ 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 } yield result
(result : @unchecked) match { (result : @unchecked) match {
@ -109,7 +113,8 @@ class UserRoutes(service : UserService) extends cask.Routes {
val result = for { val result = for {
username <- Parsed(username) username <- Parsed(username)
result <- Parsed.eval(service.deleteUser(username)) resultTry <- Parsed.eval(service.deleteUser(username))
result <- Parsed.fromTry(resultTry)
} yield result } yield result
(result : @unchecked) match { (result : @unchecked) match {
@ -127,7 +132,8 @@ class UserRoutes(service : UserService) extends cask.Routes {
val result = for { val result = for {
username <- Parsed(username) username <- Parsed(username)
result <- Parsed.eval(service.getUserByName(username)) resultTry <- Parsed.eval(service.getUserByName(username))
result <- Parsed.fromTry(resultTry)
} yield result } yield result
(result : @unchecked) match { (result : @unchecked) match {
@ -145,7 +151,8 @@ class UserRoutes(service : UserService) extends cask.Routes {
def failFast = request.queryParams.keySet.contains("failFast") def failFast = request.queryParams.keySet.contains("failFast")
val result = for { val result = for {
result <- Parsed.eval(service.loginUser(username, password)) resultTry <- Parsed.eval(service.loginUser(username, password))
result <- Parsed.fromTry(resultTry)
} yield result } yield result
(result : @unchecked) match { (result : @unchecked) match {
@ -164,7 +171,8 @@ class UserRoutes(service : UserService) extends cask.Routes {
def failFast = request.queryParams.keySet.contains("failFast") def failFast = request.queryParams.keySet.contains("failFast")
val result = for { val result = for {
result <- Parsed.eval(service.logoutUser()) resultTry <- Parsed.eval(service.logoutUser())
result <- Parsed.fromTry(resultTry)
} yield result } yield result
(result : @unchecked) match { (result : @unchecked) match {
@ -186,7 +194,8 @@ class UserRoutes(service : UserService) extends cask.Routes {
userJson <- Parsed.fromTry(request.bodyAsJson) userJson <- Parsed.fromTry(request.bodyAsJson)
userData <- Parsed.eval(UserData.fromJson(userJson)) /* not array or map */ userData <- Parsed.eval(UserData.fromJson(userJson)) /* not array or map */
user <- Parsed.fromTry(userData.validated(failFast)) 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 } yield result
(result : @unchecked) match { (result : @unchecked) match {

View File

@ -25,6 +25,16 @@ import scala.reflect.ClassTag
import scala.util.* import scala.util.*
import upickle.default.* 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 // needed for BigDecimal params
given cask.endpoints.QueryParamReader.SimpleParam[BigDecimal](BigDecimal.apply) given cask.endpoints.QueryParamReader.SimpleParam[BigDecimal](BigDecimal.apply)

View File

@ -1,6 +1,5 @@
/** /** OpenAPI Petstore This is a sample server Petstore server. For this sample, you can use the api
* OpenAPI Petstore * key `special-key` to test the authorization filters.
* 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 * OpenAPI spec version: 1.0.0
* *

View File

@ -1,6 +1,5 @@
/** /** OpenAPI Petstore This is a sample server Petstore server. For this sample, you can use the api
* OpenAPI Petstore * key `special-key` to test the authorization filters.
* 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 * OpenAPI spec version: 1.0.0
* *

View File

@ -1,6 +1,5 @@
/** /** OpenAPI Petstore This is a sample server Petstore server. For this sample, you can use the api
* OpenAPI Petstore * key `special-key` to test the authorization filters.
* 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 * OpenAPI spec version: 1.0.0
* *

View File

@ -1,6 +1,5 @@
/** /** OpenAPI Petstore This is a sample server Petstore server. For this sample, you can use the api
* OpenAPI Petstore * key `special-key` to test the authorization filters.
* 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 * OpenAPI spec version: 1.0.0
* *
@ -13,8 +12,6 @@
// this model was generated using modelTest.mustache // this model was generated using modelTest.mustache
package sample.cask.model package sample.cask.model
import sample.cask.model.Category
import sample.cask.model.Tag
import org.scalatest.matchers.should.Matchers import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec import org.scalatest.wordspec.AnyWordSpec

View File

@ -1,6 +1,5 @@
/** /** OpenAPI Petstore This is a sample server Petstore server. For this sample, you can use the api
* OpenAPI Petstore * key `special-key` to test the authorization filters.
* 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 * OpenAPI spec version: 1.0.0
* *

View File

@ -1,6 +1,5 @@
/** /** OpenAPI Petstore This is a sample server Petstore server. For this sample, you can use the api
* OpenAPI Petstore * key `special-key` to test the authorization filters.
* 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 * OpenAPI spec version: 1.0.0
* *

View File

@ -21,64 +21,260 @@ package sample.cask.api
import _root_.sample.cask.model.ApiResponse import _root_.sample.cask.model.ApiResponse
import _root_.java.io.File import _root_.java.io.File
import _root_.sample.cask.model.Pet import _root_.sample.cask.model.Pet
import scala.util.Failure
import scala.util.Try
import _root_.sample.cask.model.* 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 { object PetService {
def apply() : PetService = new PetService {
override def addPet(pet : Pet) : Pet = ??? /**
override def deletePet(petId : Long, apiKey : Option[String]) : Unit = ??? * The 'Handler' is an implementation of PetService convenient for delegating or overriding individual functions
override def findPetsByStatus(status : Seq[String]) : List[Pet] = ??? */
override def findPetsByTags(tags : Seq[String]) : List[Pet] = ??? case class Handler[F[_]](
override def getPetById(petId : Long) : Pet = ??? addPetHandler : (pet : Pet) => F[Pet],
override def updatePet(pet : Pet) : Pet = ??? deletePetHandler : (petId : Long, apiKey : Option[String]) => F[Unit],
override def updatePetWithForm(petId : Long, name : Option[String], status : Option[String]) : Unit = ??? findPetsByStatusHandler : (status : Seq[String]) => F[List[Pet]],
override def uploadFile(petId : Long, additionalMetadata : Option[String], file : Option[File]) : ApiResponse = ??? 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 Pet business-logic
*
*
* The 'asHandler' will return an implementation which allows for easily overriding individual operations.
*
* equally there are "on&lt;Function&gt;" 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 /** Add a new pet to the store
* *
* @return Pet * @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 /** Deletes a pet
* *
* @return * @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 /** Finds Pets by status
* *
* @return List[Pet] * @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 /** Finds Pets by tags
* *
* @return List[Pet] * @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 /** Find pet by ID
* *
* @return Pet * @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 /** Update an existing pet
* *
* @return 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 /** Updates a pet in the store with form data
* *
* @return * @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 /** uploads an image
* *
* @return ApiResponse * @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)
}
}
}
} }

View File

@ -19,40 +19,168 @@
package sample.cask.api package sample.cask.api
import _root_.sample.cask.model.Order import _root_.sample.cask.model.Order
import scala.util.Failure
import scala.util.Try
import _root_.sample.cask.model.* 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 { object StoreService {
def apply() : StoreService = new StoreService {
override def deleteOrder(orderId : String) : Unit = ??? /**
override def getInventory() : Map[String, Int] = ??? * The 'Handler' is an implementation of StoreService convenient for delegating or overriding individual functions
override def getOrderById(orderId : Long) : Order = ??? */
override def placeOrder(order : Order) : Order = ??? 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 Store business-logic
*
*
* The 'asHandler' will return an implementation which allows for easily overriding individual operations.
*
* equally there are "on&lt;Function&gt;" 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 /** Delete purchase order by ID
* *
* @return * @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 /** Returns pet inventories by status
* *
* @return Map[String, Int] * @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 /** Find purchase order by ID
* *
* @return Order * @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 /** Place an order for a pet
* *
* @return Order * @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)
}
}
}
} }

View File

@ -20,64 +20,260 @@ package sample.cask.api
import _root_.java.time.OffsetDateTime import _root_.java.time.OffsetDateTime
import _root_.sample.cask.model.User import _root_.sample.cask.model.User
import scala.util.Failure
import scala.util.Try
import _root_.sample.cask.model.* 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 { object UserService {
def apply() : UserService = new UserService {
override def createUser(user : User) : Unit = ??? /**
override def createUsersWithArrayInput(user : Seq[User]) : Unit = ??? * The 'Handler' is an implementation of UserService convenient for delegating or overriding individual functions
override def createUsersWithListInput(user : Seq[User]) : Unit = ??? */
override def deleteUser(username : String) : Unit = ??? case class Handler[F[_]](
override def getUserByName(username : String) : User = ??? createUserHandler : (user : User) => F[Unit],
override def loginUser(username : String, password : String) : String = ??? createUsersWithArrayInputHandler : (user : Seq[User]) => F[Unit],
override def logoutUser() : Unit = ??? createUsersWithListInputHandler : (user : Seq[User]) => F[Unit],
override def updateUser(username : String, user : User) : 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 User business-logic
*
*
* The 'asHandler' will return an implementation which allows for easily overriding individual operations.
*
* equally there are "on&lt;Function&gt;" 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 /** Create user
* *
* @return * @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 /** Creates list of users with given input array
* *
* @return * @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 /** Creates list of users with given input array
* *
* @return * @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 /** Delete user
* *
* @return * @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 /** Get user by user name
* *
* @return User * @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 /** Logs user into the system
* *
* @return String * @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 /** Logs out current logged in user session
* *
* @return * @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 /** Updated user
* *
* @return * @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)
}
}
}
} }

View File

@ -13,12 +13,15 @@
// this model was generated using model.mustache // this model was generated using model.mustache
package sample.cask.model package sample.cask.model
import scala.util.control.NonFatal import scala.util.control.NonFatal
// see https://com-lihaoyi.github.io/upickle/ // see https://com-lihaoyi.github.io/upickle/
import upickle.default.{ReadWriter => RW, macroRW} import upickle.default.{ReadWriter => RW, macroRW}
import upickle.default.* import upickle.default.*
case class ApiResponse( case class ApiResponse(
code: Option[Int] = None , code: Option[Int] = None ,
`type`: Option[String] = None , `type`: Option[String] = None ,
@ -32,9 +35,9 @@ case class ApiResponse(
def asData : ApiResponseData = { def asData : ApiResponseData = {
ApiResponseData( ApiResponseData(
code = code.getOrElse(0), code = code.getOrElse(0) /* 1 */,
`type` = `type`.getOrElse(""), `type` = `type`.getOrElse("") /* 1 */,
message = message.getOrElse("") message = message.getOrElse("") /* 1 */
) )
} }

View File

@ -20,6 +20,7 @@ import scala.util.*
import upickle.default.{ReadWriter => RW, macroRW} import upickle.default.{ReadWriter => RW, macroRW}
import upickle.default.* 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 * It has no validation - there may be nulls, values out of range, etc
*/ */
@ -61,6 +62,9 @@ case class ApiResponseData(
errors.toSeq errors.toSeq
} }
/**
* @return the validated model within a Try (if successful)
*/
def validated(failFast : Boolean = false) : scala.util.Try[ApiResponse] = { def validated(failFast : Boolean = false) : scala.util.Try[ApiResponse] = {
validationErrors(Vector(), failFast) match { validationErrors(Vector(), failFast) match {
case Seq() => Success(asModel) case Seq() => Success(asModel)
@ -71,18 +75,9 @@ case class ApiResponseData(
/** use 'validated' to check validation */ /** use 'validated' to check validation */
def asModel : ApiResponse = { def asModel : ApiResponse = {
ApiResponse( ApiResponse(
code = Option( code = Option(code) /* 1 */,
code `type` = Option(`type`) /* 1 */,
) message = Option(message) /* 1 */
,
`type` = Option(
`type`
)
,
message = Option(
message
)
) )
} }

View File

@ -13,12 +13,15 @@
// this model was generated using model.mustache // this model was generated using model.mustache
package sample.cask.model package sample.cask.model
import scala.util.control.NonFatal import scala.util.control.NonFatal
// see https://com-lihaoyi.github.io/upickle/ // see https://com-lihaoyi.github.io/upickle/
import upickle.default.{ReadWriter => RW, macroRW} import upickle.default.{ReadWriter => RW, macroRW}
import upickle.default.* import upickle.default.*
case class Category( case class Category(
id: Option[Long] = None , id: Option[Long] = None ,
name: Option[String] = None name: Option[String] = None
@ -31,8 +34,8 @@ case class Category(
def asData : CategoryData = { def asData : CategoryData = {
CategoryData( CategoryData(
id = id.getOrElse(0), id = id.getOrElse(0) /* 1 */,
name = name.getOrElse("") name = name.getOrElse("") /* 1 */
) )
} }

View File

@ -20,6 +20,7 @@ import scala.util.*
import upickle.default.{ReadWriter => RW, macroRW} import upickle.default.{ReadWriter => RW, macroRW}
import upickle.default.* 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 * It has no validation - there may be nulls, values out of range, etc
*/ */
@ -60,6 +61,9 @@ case class CategoryData(
errors.toSeq errors.toSeq
} }
/**
* @return the validated model within a Try (if successful)
*/
def validated(failFast : Boolean = false) : scala.util.Try[Category] = { def validated(failFast : Boolean = false) : scala.util.Try[Category] = {
validationErrors(Vector(), failFast) match { validationErrors(Vector(), failFast) match {
case Seq() => Success(asModel) case Seq() => Success(asModel)
@ -70,14 +74,8 @@ case class CategoryData(
/** use 'validated' to check validation */ /** use 'validated' to check validation */
def asModel : Category = { def asModel : Category = {
Category( Category(
id = Option( id = Option(id) /* 1 */,
id name = Option(name) /* 1 */
)
,
name = Option(
name
)
) )
} }

View File

@ -14,12 +14,15 @@
// this model was generated using model.mustache // this model was generated using model.mustache
package sample.cask.model package sample.cask.model
import java.time.OffsetDateTime import java.time.OffsetDateTime
import scala.util.control.NonFatal import scala.util.control.NonFatal
// see https://com-lihaoyi.github.io/upickle/ // see https://com-lihaoyi.github.io/upickle/
import upickle.default.{ReadWriter => RW, macroRW} import upickle.default.{ReadWriter => RW, macroRW}
import upickle.default.* import upickle.default.*
case class Order( case class Order(
id: Option[Long] = None , id: Option[Long] = None ,
petId: Option[Long] = None , petId: Option[Long] = None ,
@ -37,12 +40,12 @@ case class Order(
def asData : OrderData = { def asData : OrderData = {
OrderData( OrderData(
id = id.getOrElse(0), id = id.getOrElse(0) /* 1 */,
petId = petId.getOrElse(0), petId = petId.getOrElse(0) /* 1 */,
quantity = quantity.getOrElse(0), quantity = quantity.getOrElse(0) /* 1 */,
shipDate = shipDate.getOrElse(null), shipDate = shipDate.getOrElse(null) /* 1 */,
status = status.getOrElse(null), status = status.getOrElse(null) /* 1 */,
complete = complete.getOrElse(false) complete = complete.getOrElse(false) /* 1 */
) )
} }

View File

@ -21,6 +21,7 @@ import scala.util.*
import upickle.default.{ReadWriter => RW, macroRW} import upickle.default.{ReadWriter => RW, macroRW}
import upickle.default.* 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 * It has no validation - there may be nulls, values out of range, etc
*/ */
@ -84,6 +85,9 @@ case class OrderData(
errors.toSeq errors.toSeq
} }
/**
* @return the validated model within a Try (if successful)
*/
def validated(failFast : Boolean = false) : scala.util.Try[Order] = { def validated(failFast : Boolean = false) : scala.util.Try[Order] = {
validationErrors(Vector(), failFast) match { validationErrors(Vector(), failFast) match {
case Seq() => Success(asModel) case Seq() => Success(asModel)
@ -94,30 +98,12 @@ case class OrderData(
/** use 'validated' to check validation */ /** use 'validated' to check validation */
def asModel : Order = { def asModel : Order = {
Order( Order(
id = Option( id = Option(id) /* 1 */,
id petId = Option(petId) /* 1 */,
) quantity = Option(quantity) /* 1 */,
, shipDate = Option(shipDate) /* 1 */,
petId = Option( status = Option(status) /* 1 */,
petId complete = Option(complete) /* 1 */
)
,
quantity = Option(
quantity
)
,
shipDate = Option(
shipDate
)
,
status = Option(
status
)
,
complete = Option(
complete
)
) )
} }

View File

@ -13,14 +13,15 @@
// this model was generated using model.mustache // this model was generated using model.mustache
package sample.cask.model package sample.cask.model
import sample.cask.model.Category
import sample.cask.model.Tag
import scala.util.control.NonFatal import scala.util.control.NonFatal
// see https://com-lihaoyi.github.io/upickle/ // see https://com-lihaoyi.github.io/upickle/
import upickle.default.{ReadWriter => RW, macroRW} import upickle.default.{ReadWriter => RW, macroRW}
import upickle.default.* import upickle.default.*
case class Pet( case class Pet(
id: Option[Long] = None , id: Option[Long] = None ,
category: Option[Category] = None , category: Option[Category] = None ,
@ -38,12 +39,12 @@ case class Pet(
def asData : PetData = { def asData : PetData = {
PetData( PetData(
id = id.getOrElse(0), id = id.getOrElse(0) /* 1 */,
category = category.map(_.asData).getOrElse(null), category = category.map(_.asData).getOrElse(null) /* 4 */,
name = name, name = name /* 2 */,
photoUrls = photoUrls, photoUrls = photoUrls /* 2 */,
tags = tags.map(_.asData), tags = tags.map(_.asData) /* 6 */,
status = status.getOrElse(null) status = status.getOrElse(null) /* 1 */
) )
} }

View File

@ -13,8 +13,6 @@
// this model was generated using modelData.mustache // this model was generated using modelData.mustache
package sample.cask.model package sample.cask.model
import sample.cask.model.Category
import sample.cask.model.Tag
import scala.util.control.NonFatal import scala.util.control.NonFatal
import scala.util.* import scala.util.*
@ -22,6 +20,7 @@ import scala.util.*
import upickle.default.{ReadWriter => RW, macroRW} import upickle.default.{ReadWriter => RW, macroRW}
import upickle.default.* 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 * It has no validation - there may be nulls, values out of range, etc
*/ */
@ -101,6 +100,9 @@ case class PetData(
errors.toSeq errors.toSeq
} }
/**
* @return the validated model within a Try (if successful)
*/
def validated(failFast : Boolean = false) : scala.util.Try[Pet] = { def validated(failFast : Boolean = false) : scala.util.Try[Pet] = {
validationErrors(Vector(), failFast) match { validationErrors(Vector(), failFast) match {
case Seq() => Success(asModel) case Seq() => Success(asModel)
@ -111,30 +113,12 @@ case class PetData(
/** use 'validated' to check validation */ /** use 'validated' to check validation */
def asModel : Pet = { def asModel : Pet = {
Pet( Pet(
id = Option( id = Option(id) /* 1 */,
id category = Option(category).map(_.asModel) /* 4 */,
) name = name /* 2 */,
, photoUrls = photoUrls /* 2 */,
category = Option( tags = tags.map(_.asModel) /* 5 */,
category status = Option(status) /* 1 */
)
.map(_.asModel),
name =
name
,
photoUrls =
photoUrls
,
tags =
tags
.map(_.asModel),
status = Option(
status
)
) )
} }

View File

@ -13,12 +13,15 @@
// this model was generated using model.mustache // this model was generated using model.mustache
package sample.cask.model package sample.cask.model
import scala.util.control.NonFatal import scala.util.control.NonFatal
// see https://com-lihaoyi.github.io/upickle/ // see https://com-lihaoyi.github.io/upickle/
import upickle.default.{ReadWriter => RW, macroRW} import upickle.default.{ReadWriter => RW, macroRW}
import upickle.default.* import upickle.default.*
case class Tag( case class Tag(
id: Option[Long] = None , id: Option[Long] = None ,
name: Option[String] = None name: Option[String] = None
@ -31,8 +34,8 @@ case class Tag(
def asData : TagData = { def asData : TagData = {
TagData( TagData(
id = id.getOrElse(0), id = id.getOrElse(0) /* 1 */,
name = name.getOrElse("") name = name.getOrElse("") /* 1 */
) )
} }

View File

@ -20,6 +20,7 @@ import scala.util.*
import upickle.default.{ReadWriter => RW, macroRW} import upickle.default.{ReadWriter => RW, macroRW}
import upickle.default.* 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 * It has no validation - there may be nulls, values out of range, etc
*/ */
@ -54,6 +55,9 @@ case class TagData(
errors.toSeq errors.toSeq
} }
/**
* @return the validated model within a Try (if successful)
*/
def validated(failFast : Boolean = false) : scala.util.Try[Tag] = { def validated(failFast : Boolean = false) : scala.util.Try[Tag] = {
validationErrors(Vector(), failFast) match { validationErrors(Vector(), failFast) match {
case Seq() => Success(asModel) case Seq() => Success(asModel)
@ -64,14 +68,8 @@ case class TagData(
/** use 'validated' to check validation */ /** use 'validated' to check validation */
def asModel : Tag = { def asModel : Tag = {
Tag( Tag(
id = Option( id = Option(id) /* 1 */,
id name = Option(name) /* 1 */
)
,
name = Option(
name
)
) )
} }

View File

@ -13,12 +13,15 @@
// this model was generated using model.mustache // this model was generated using model.mustache
package sample.cask.model package sample.cask.model
import scala.util.control.NonFatal import scala.util.control.NonFatal
// see https://com-lihaoyi.github.io/upickle/ // see https://com-lihaoyi.github.io/upickle/
import upickle.default.{ReadWriter => RW, macroRW} import upickle.default.{ReadWriter => RW, macroRW}
import upickle.default.* import upickle.default.*
case class User( case class User(
id: Option[Long] = None , id: Option[Long] = None ,
username: Option[String] = None , username: Option[String] = None ,
@ -38,14 +41,14 @@ case class User(
def asData : UserData = { def asData : UserData = {
UserData( UserData(
id = id.getOrElse(0), id = id.getOrElse(0) /* 1 */,
username = username.getOrElse(""), username = username.getOrElse("") /* 1 */,
firstName = firstName.getOrElse(""), firstName = firstName.getOrElse("") /* 1 */,
lastName = lastName.getOrElse(""), lastName = lastName.getOrElse("") /* 1 */,
email = email.getOrElse(""), email = email.getOrElse("") /* 1 */,
password = password.getOrElse(""), password = password.getOrElse("") /* 1 */,
phone = phone.getOrElse(""), phone = phone.getOrElse("") /* 1 */,
userStatus = userStatus.getOrElse(0) userStatus = userStatus.getOrElse(0) /* 1 */
) )
} }

View File

@ -20,6 +20,7 @@ import scala.util.*
import upickle.default.{ReadWriter => RW, macroRW} import upickle.default.{ReadWriter => RW, macroRW}
import upickle.default.* 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 * It has no validation - there may be nulls, values out of range, etc
*/ */
@ -97,6 +98,9 @@ case class UserData(
errors.toSeq errors.toSeq
} }
/**
* @return the validated model within a Try (if successful)
*/
def validated(failFast : Boolean = false) : scala.util.Try[User] = { def validated(failFast : Boolean = false) : scala.util.Try[User] = {
validationErrors(Vector(), failFast) match { validationErrors(Vector(), failFast) match {
case Seq() => Success(asModel) case Seq() => Success(asModel)
@ -107,38 +111,14 @@ case class UserData(
/** use 'validated' to check validation */ /** use 'validated' to check validation */
def asModel : User = { def asModel : User = {
User( User(
id = Option( id = Option(id) /* 1 */,
id username = Option(username) /* 1 */,
) firstName = Option(firstName) /* 1 */,
, lastName = Option(lastName) /* 1 */,
username = Option( email = Option(email) /* 1 */,
username password = Option(password) /* 1 */,
) phone = Option(phone) /* 1 */,
, userStatus = Option(userStatus) /* 1 */
firstName = Option(
firstName
)
,
lastName = Option(
lastName
)
,
email = Option(
email
)
,
password = Option(
password
)
,
phone = Option(
phone
)
,
userStatus = Option(
userStatus
)
) )
} }