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));
@ -139,7 +141,7 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
public String toDefaultValue(Schema p) { public String toDefaultValue(Schema p) {
if (ModelUtils.isMapSchema(p)) { if (ModelUtils.isMapSchema(p)) {
String inner = getSchemaType(ModelUtils.getAdditionalProperties(p)); String inner = getSchemaType(ModelUtils.getAdditionalProperties(p));
return "Map[String, " + inner + "]() "; return "Map[String, " + inner + "]()";
} else if (ModelUtils.isFreeFormObject(p, openAPI)) { } else if (ModelUtils.isFreeFormObject(p, openAPI)) {
// We're opinionated in this template to use ujson // We're opinionated in this template to use ujson
return "ujson.Null"; return "ujson.Null";
@ -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 -> {
final CodegenParameter copy = p.copy(); // wow is this a pain to do in Java ... I just wanted to have distinct items of a list
copy.vendorExtensions.put("x-default-value", defaultValue(p)); // based on their name.
copy.required = false; // all our query params are optional for our work-around as it's a super-set of a few different routes Set<String> alreadySeen = new HashSet<>();
copy.dataType = asScalaDataType(copy, false, true, true);
copy.defaultValue = defaultValue(copy); List<CodegenParameter> list = operations.stream().flatMap(op -> op.queryParams.stream()).flatMap(p -> {
return copy; if (alreadySeen.add(p.paramName)) {
final CodegenParameter copy = p.copy();
copy.vendorExtensions.put("x-default-value", defaultValue(p));
copy.required = false; // all our query params are optional for our work-around as it's a super-set of a few different routes
copy.dataType = asScalaDataType(copy, false, true, true);
copy.defaultValue = defaultValue(copy);
return Arrays.asList(copy).stream();
} else {
final List<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}} {{/operation}}
{{/operations}} {{/operations}}
/**
* @return a Handler implementation of this service
*/
final def asHandler : {{classname}}Service.Handler[F] = this match {
case h : {{classname}}Service.Handler[F] => h
case _ =>
{{classname}}Service.Handler[F](
{{#operations}}
{{#operation}}
({{{vendorExtensions.x-param-list}}}) => {{operationId}}({{{vendorExtensions.x-param-list}}}){{^-last}}, {{/-last}}
{{/operation}}
{{/operations}}
)
}
/**
* This function will change the effect type of this service.
*
* It's not unlike a typical map operation from A => B, except we're not mapping
* a type from A to B, but rather from F[A] => G[A] using the 'changeEffect' function.
*
* For, this could turn an asynchronous service (one which returns Future[_] types) into
* a synchronous one (one which returns Try[_] types) by awaiting on the Future.
*
* It could change an IO type (like cats effect or ZIO) into an ID[A] which is just:
* ```
* type ID[A] => A
* ```
*
* @tparam G the new "polymorphic" effect type
* @param changeEffect the "natural transformation" which can change one effect type into another
* @return a new {{classname}}Service service implementation with effect type [G]
*/
final def mapEffect[G[_]](changeEffect : [A] => F[A] => G[A]) : {{classname}}Service[G] = {
val self = this
new {{classname}}Service[G] {
{{#operations}}
{{#operation}}
override def {{operationId}}({{{vendorExtensions.x-param-list-typed}}}) : G[{{{vendorExtensions.x-response-type}}}] = changeEffect {
self.{{operationId}}({{{vendorExtensions.x-param-list}}})
}
{{/operation}}
{{/operations}}
}
}
} }

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}} {{#isEnum}}
) { {{>modelEnum}}
{{/isEnum}}
def asJsonString: String = asData.asJsonString {{^isEnum}}
def asJson: ujson.Value = asData.asJson {{>modelClass}}
{{/isEnum}}
def asData : {{classname}}Data = {
{{classname}}Data(
{{#vars}}
{{name}} = {{name}}{{#vendorExtensions.x-map-asModel}}.map(_.asData){{/vendorExtensions.x-map-asModel}}{{#vendorExtensions.x-wrap-in-optional}}.getOrElse({{{defaultValue}}}){{/vendorExtensions.x-wrap-in-optional}}{{^-last}},{{/-last}}
{{/vars}}
{{#isAdditionalPropertiesTrue}}, additionalProperties{{/isAdditionalPropertiesTrue}}
)
}
}
object {{classname}} {
given RW[{{classname}}] = summon[RW[ujson.Value]].bimap[{{classname}}](_.asJson, json => read[{{classname}}Data](json).asModel)
enum Fields(val fieldName : String) extends Field(fieldName) {
{{#vars}}
case {{name}} extends Fields("{{name}}")
{{/vars}}
}
{{#vars}}
{{#isEnum}}
// baseName={{{baseName}}}
// nameInCamelCase = {{{nameInCamelCase}}}
enum {{datatypeWithEnum}} derives ReadWriter {
{{#_enum}}
case {{.}}
{{/_enum}}
}
{{/isEnum}}
{{/vars}}
}
{{/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,15 +1,14 @@
/** /** 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 *
* * Contact: team@openapitools.org
* Contact: team@openapitools.org *
* * NOTE: This class is auto generated by OpenAPI Generator.
* NOTE: This class is auto generated by OpenAPI Generator. *
* * https://openapi-generator.tech
* https://openapi-generator.tech */
*/
// this model was generated using modelTest.mustache // this model was generated using modelTest.mustache
package sample.cask.model package sample.cask.model
@ -20,16 +19,16 @@ import scala.util.*
class ApiResponseTest extends AnyWordSpec with Matchers { class ApiResponseTest extends AnyWordSpec with Matchers {
"ApiResponse.fromJson" should { "ApiResponse.fromJson" should {
"""not parse invalid json""" in { """not parse invalid json""" in {
val Failure(err) = Try(ApiResponseData.fromJsonString("invalid jason")) val Failure(err) = Try(ApiResponseData.fromJsonString("invalid jason"))
err.getMessage should startWith ("Error parsing json 'invalid jason'") err.getMessage should startWith("Error parsing json 'invalid jason'")
}
"""parse """ ignore {
val Failure(err : ValidationErrors) = ApiResponseData.fromJsonString("""""").validated()
sys.error("TODO")
}
} }
"""parse """ ignore {
val Failure(err: ValidationErrors) = ApiResponseData.fromJsonString("""""").validated()
sys.error("TODO")
}
}
} }

View File

@ -1,15 +1,14 @@
/** /** 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 *
* * Contact: team@openapitools.org
* Contact: team@openapitools.org *
* * NOTE: This class is auto generated by OpenAPI Generator.
* NOTE: This class is auto generated by OpenAPI Generator. *
* * https://openapi-generator.tech
* https://openapi-generator.tech */
*/
// this model was generated using modelTest.mustache // this model was generated using modelTest.mustache
package sample.cask.model package sample.cask.model
@ -20,16 +19,16 @@ import scala.util.*
class CategoryTest extends AnyWordSpec with Matchers { class CategoryTest extends AnyWordSpec with Matchers {
"Category.fromJson" should { "Category.fromJson" should {
"""not parse invalid json""" in { """not parse invalid json""" in {
val Failure(err) = Try(CategoryData.fromJsonString("invalid jason")) val Failure(err) = Try(CategoryData.fromJsonString("invalid jason"))
err.getMessage should startWith ("Error parsing json 'invalid jason'") err.getMessage should startWith("Error parsing json 'invalid jason'")
}
"""parse """ ignore {
val Failure(err : ValidationErrors) = CategoryData.fromJsonString("""""").validated()
sys.error("TODO")
}
} }
"""parse """ ignore {
val Failure(err: ValidationErrors) = CategoryData.fromJsonString("""""").validated()
sys.error("TODO")
}
}
} }

View File

@ -1,15 +1,14 @@
/** /** 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 *
* * Contact: team@openapitools.org
* Contact: team@openapitools.org *
* * NOTE: This class is auto generated by OpenAPI Generator.
* NOTE: This class is auto generated by OpenAPI Generator. *
* * https://openapi-generator.tech
* https://openapi-generator.tech */
*/
// this model was generated using modelTest.mustache // this model was generated using modelTest.mustache
package sample.cask.model package sample.cask.model
@ -21,16 +20,16 @@ import scala.util.*
class OrderTest extends AnyWordSpec with Matchers { class OrderTest extends AnyWordSpec with Matchers {
"Order.fromJson" should { "Order.fromJson" should {
"""not parse invalid json""" in { """not parse invalid json""" in {
val Failure(err) = Try(OrderData.fromJsonString("invalid jason")) val Failure(err) = Try(OrderData.fromJsonString("invalid jason"))
err.getMessage should startWith ("Error parsing json 'invalid jason'") err.getMessage should startWith("Error parsing json 'invalid jason'")
}
"""parse """ ignore {
val Failure(err : ValidationErrors) = OrderData.fromJsonString("""""").validated()
sys.error("TODO")
}
} }
"""parse """ ignore {
val Failure(err: ValidationErrors) = OrderData.fromJsonString("""""").validated()
sys.error("TODO")
}
}
} }

View File

@ -1,20 +1,17 @@
/** /** 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 *
* * Contact: team@openapitools.org
* Contact: team@openapitools.org *
* * NOTE: This class is auto generated by OpenAPI Generator.
* NOTE: This class is auto generated by OpenAPI Generator. *
* * https://openapi-generator.tech
* https://openapi-generator.tech */
*/
// 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
@ -22,16 +19,16 @@ import scala.util.*
class PetTest extends AnyWordSpec with Matchers { class PetTest extends AnyWordSpec with Matchers {
"Pet.fromJson" should { "Pet.fromJson" should {
"""not parse invalid json""" in { """not parse invalid json""" in {
val Failure(err) = Try(PetData.fromJsonString("invalid jason")) val Failure(err) = Try(PetData.fromJsonString("invalid jason"))
err.getMessage should startWith ("Error parsing json 'invalid jason'") err.getMessage should startWith("Error parsing json 'invalid jason'")
}
"""parse """ ignore {
val Failure(err : ValidationErrors) = PetData.fromJsonString("""""").validated()
sys.error("TODO")
}
} }
"""parse """ ignore {
val Failure(err: ValidationErrors) = PetData.fromJsonString("""""").validated()
sys.error("TODO")
}
}
} }

View File

@ -1,15 +1,14 @@
/** /** 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 *
* * Contact: team@openapitools.org
* Contact: team@openapitools.org *
* * NOTE: This class is auto generated by OpenAPI Generator.
* NOTE: This class is auto generated by OpenAPI Generator. *
* * https://openapi-generator.tech
* https://openapi-generator.tech */
*/
// this model was generated using modelTest.mustache // this model was generated using modelTest.mustache
package sample.cask.model package sample.cask.model
@ -20,16 +19,16 @@ import scala.util.*
class TagTest extends AnyWordSpec with Matchers { class TagTest extends AnyWordSpec with Matchers {
"Tag.fromJson" should { "Tag.fromJson" should {
"""not parse invalid json""" in { """not parse invalid json""" in {
val Failure(err) = Try(TagData.fromJsonString("invalid jason")) val Failure(err) = Try(TagData.fromJsonString("invalid jason"))
err.getMessage should startWith ("Error parsing json 'invalid jason'") err.getMessage should startWith("Error parsing json 'invalid jason'")
}
"""parse """ ignore {
val Failure(err : ValidationErrors) = TagData.fromJsonString("""""").validated()
sys.error("TODO")
}
} }
"""parse """ ignore {
val Failure(err: ValidationErrors) = TagData.fromJsonString("""""").validated()
sys.error("TODO")
}
}
} }

View File

@ -1,15 +1,14 @@
/** /** 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 *
* * Contact: team@openapitools.org
* Contact: team@openapitools.org *
* * NOTE: This class is auto generated by OpenAPI Generator.
* NOTE: This class is auto generated by OpenAPI Generator. *
* * https://openapi-generator.tech
* https://openapi-generator.tech */
*/
// this model was generated using modelTest.mustache // this model was generated using modelTest.mustache
package sample.cask.model package sample.cask.model
@ -20,16 +19,16 @@ import scala.util.*
class UserTest extends AnyWordSpec with Matchers { class UserTest extends AnyWordSpec with Matchers {
"User.fromJson" should { "User.fromJson" should {
"""not parse invalid json""" in { """not parse invalid json""" in {
val Failure(err) = Try(UserData.fromJsonString("invalid jason")) val Failure(err) = Try(UserData.fromJsonString("invalid jason"))
err.getMessage should startWith ("Error parsing json 'invalid jason'") err.getMessage should startWith("Error parsing json 'invalid jason'")
}
"""parse """ ignore {
val Failure(err : ValidationErrors) = UserData.fromJsonString("""""").validated()
sys.error("TODO")
}
} }
"""parse """ ignore {
val Failure(err: ValidationErrors) = UserData.fromJsonString("""""").validated()
sys.error("TODO")
}
}
} }

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,41 +13,44 @@
// 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 ,
message: Option[String] = None message: Option[String] = None
) { ) {
def asJsonString: String = asData.asJsonString def asJsonString: String = asData.asJsonString
def asJson: ujson.Value = asData.asJson def asJson: ujson.Value = asData.asJson
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 */
) )
} }
} }
object ApiResponse { object ApiResponse {
given RW[ApiResponse] = summon[RW[ujson.Value]].bimap[ApiResponse](_.asJson, json => read[ApiResponseData](json).asModel) given RW[ApiResponse] = summon[RW[ujson.Value]].bimap[ApiResponse](_.asJson, json => read[ApiResponseData](json).asModel)
enum Fields(val fieldName : String) extends Field(fieldName) { enum Fields(val fieldName : String) extends Field(fieldName) {
case code extends Fields("code") case code extends Fields("code")
case `type` extends Fields("`type`") case `type` extends Fields("`type`")
case message extends Fields("message") case message extends Fields("message")
} }
} }

View File

@ -20,7 +20,8 @@ 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
*/ */
case class ApiResponseData( case class ApiResponseData(
@ -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,38 +13,41 @@
// 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
) { ) {
def asJsonString: String = asData.asJsonString def asJsonString: String = asData.asJsonString
def asJson: ujson.Value = asData.asJson def asJson: ujson.Value = asData.asJson
def asData : CategoryData = { def asData : CategoryData = {
CategoryData( CategoryData(
id = id.getOrElse(0), id = id.getOrElse(0) /* 1 */,
name = name.getOrElse("") name = name.getOrElse("") /* 1 */
) )
} }
} }
object Category { object Category {
given RW[Category] = summon[RW[ujson.Value]].bimap[Category](_.asJson, json => read[CategoryData](json).asModel) given RW[Category] = summon[RW[ujson.Value]].bimap[Category](_.asJson, json => read[CategoryData](json).asModel)
enum Fields(val fieldName : String) extends Field(fieldName) { enum Fields(val fieldName : String) extends Field(fieldName) {
case id extends Fields("id") case id extends Fields("id")
case name extends Fields("name") case name extends Fields("name")
} }
} }

View File

@ -20,7 +20,8 @@ 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
*/ */
case class CategoryData( case class CategoryData(
@ -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,51 +14,54 @@
// 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 ,
quantity: Option[Int] = None , quantity: Option[Int] = None ,
shipDate: Option[OffsetDateTime] = None , shipDate: Option[OffsetDateTime] = None ,
/* Order Status */ /* Order Status */
status: Option[Order.StatusEnum] = None , status: Option[Order.StatusEnum] = None ,
complete: Option[Boolean] = None complete: Option[Boolean] = None
) { ) {
def asJsonString: String = asData.asJsonString def asJsonString: String = asData.asJsonString
def asJson: ujson.Value = asData.asJson def asJson: ujson.Value = asData.asJson
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 */
) )
} }
} }
object Order { object Order {
given RW[Order] = summon[RW[ujson.Value]].bimap[Order](_.asJson, json => read[OrderData](json).asModel) given RW[Order] = summon[RW[ujson.Value]].bimap[Order](_.asJson, json => read[OrderData](json).asModel)
enum Fields(val fieldName : String) extends Field(fieldName) { enum Fields(val fieldName : String) extends Field(fieldName) {
case id extends Fields("id") case id extends Fields("id")
case petId extends Fields("petId") case petId extends Fields("petId")
case quantity extends Fields("quantity") case quantity extends Fields("quantity")
case shipDate extends Fields("shipDate") case shipDate extends Fields("shipDate")
case status extends Fields("status") case status extends Fields("status")
case complete extends Fields("complete") case complete extends Fields("complete")
} }
// baseName=status // baseName=status
// nameInCamelCase = status // nameInCamelCase = status

View File

@ -21,7 +21,8 @@ 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
*/ */
case class OrderData( case class OrderData(
@ -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,53 +13,54 @@
// 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 ,
name: String, name: String,
photoUrls: Seq[String], photoUrls: Seq[String],
tags: Seq[Tag] = Nil , tags: Seq[Tag] = Nil ,
/* pet status in the store */ /* pet status in the store */
status: Option[Pet.StatusEnum] = None status: Option[Pet.StatusEnum] = None
) { ) {
def asJsonString: String = asData.asJsonString def asJsonString: String = asData.asJsonString
def asJson: ujson.Value = asData.asJson def asJson: ujson.Value = asData.asJson
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 */
) )
} }
} }
object Pet { object Pet {
given RW[Pet] = summon[RW[ujson.Value]].bimap[Pet](_.asJson, json => read[PetData](json).asModel) given RW[Pet] = summon[RW[ujson.Value]].bimap[Pet](_.asJson, json => read[PetData](json).asModel)
enum Fields(val fieldName : String) extends Field(fieldName) { enum Fields(val fieldName : String) extends Field(fieldName) {
case id extends Fields("id") case id extends Fields("id")
case category extends Fields("category") case category extends Fields("category")
case name extends Fields("name") case name extends Fields("name")
case photoUrls extends Fields("photoUrls") case photoUrls extends Fields("photoUrls")
case tags extends Fields("tags") case tags extends Fields("tags")
case status extends Fields("status") case status extends Fields("status")
} }
// baseName=status // baseName=status
// nameInCamelCase = status // nameInCamelCase = status

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,7 +20,8 @@ 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
*/ */
case class PetData( case class PetData(
@ -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,38 +13,41 @@
// 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
) { ) {
def asJsonString: String = asData.asJsonString def asJsonString: String = asData.asJsonString
def asJson: ujson.Value = asData.asJson def asJson: ujson.Value = asData.asJson
def asData : TagData = { def asData : TagData = {
TagData( TagData(
id = id.getOrElse(0), id = id.getOrElse(0) /* 1 */,
name = name.getOrElse("") name = name.getOrElse("") /* 1 */
) )
} }
} }
object Tag { object Tag {
given RW[Tag] = summon[RW[ujson.Value]].bimap[Tag](_.asJson, json => read[TagData](json).asModel) given RW[Tag] = summon[RW[ujson.Value]].bimap[Tag](_.asJson, json => read[TagData](json).asModel)
enum Fields(val fieldName : String) extends Field(fieldName) { enum Fields(val fieldName : String) extends Field(fieldName) {
case id extends Fields("id") case id extends Fields("id")
case name extends Fields("name") case name extends Fields("name")
} }
} }

View File

@ -20,7 +20,8 @@ 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
*/ */
case class TagData( case class TagData(
@ -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,57 +13,60 @@
// 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 ,
firstName: Option[String] = None , firstName: Option[String] = None ,
lastName: Option[String] = None , lastName: Option[String] = None ,
email: Option[String] = None , email: Option[String] = None ,
password: Option[String] = None , password: Option[String] = None ,
phone: Option[String] = None , phone: Option[String] = None ,
/* User Status */ /* User Status */
userStatus: Option[Int] = None userStatus: Option[Int] = None
) { ) {
def asJsonString: String = asData.asJsonString def asJsonString: String = asData.asJsonString
def asJson: ujson.Value = asData.asJson def asJson: ujson.Value = asData.asJson
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 */
) )
} }
} }
object User { object User {
given RW[User] = summon[RW[ujson.Value]].bimap[User](_.asJson, json => read[UserData](json).asModel) given RW[User] = summon[RW[ujson.Value]].bimap[User](_.asJson, json => read[UserData](json).asModel)
enum Fields(val fieldName : String) extends Field(fieldName) { enum Fields(val fieldName : String) extends Field(fieldName) {
case id extends Fields("id") case id extends Fields("id")
case username extends Fields("username") case username extends Fields("username")
case firstName extends Fields("firstName") case firstName extends Fields("firstName")
case lastName extends Fields("lastName") case lastName extends Fields("lastName")
case email extends Fields("email") case email extends Fields("email")
case password extends Fields("password") case password extends Fields("password")
case phone extends Fields("phone") case phone extends Fields("phone")
case userStatus extends Fields("userStatus") case userStatus extends Fields("userStatus")
} }
} }

View File

@ -20,7 +20,8 @@ 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
*/ */
case class UserData( case class UserData(
@ -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
)
) )
} }