scala-cask fix: Added support for 'additionalProperties:true' (#19767)

* Added support for 'additionalProperties:true' to scala-cask generator

additionalProperties means the request can contain arbitrary
additional properties, and so this change adds an 'additionalProperties'
field to request objects which is a json type.

* fixed warning in example scala-cli project

* updated samples

* addressed codegen comments
This commit is contained in:
Aaron Pritzlaff 2024-10-09 04:15:25 +01:00 committed by GitHub
parent d60200de38
commit 31be9b9207
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 512 additions and 678 deletions

View File

@ -3158,7 +3158,7 @@ public class DefaultCodegen implements CodegenConfig {
additionalPropertiesIsAnyType = true; additionalPropertiesIsAnyType = true;
} }
} else { } else {
// if additioanl properties is set (e.g. free form object, any type, string, etc) // if additional properties is set (e.g. free form object, any type, string, etc)
addPropProp = fromProperty(getAdditionalPropertiesName(), (Schema) schema.getAdditionalProperties(), false); addPropProp = fromProperty(getAdditionalPropertiesName(), (Schema) schema.getAdditionalProperties(), false);
additionalPropertiesIsAnyType = true; additionalPropertiesIsAnyType = true;
} }

View File

@ -39,6 +39,10 @@ import static org.openapitools.codegen.utils.StringUtils.camelize;
public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements CodegenConfig { public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements CodegenConfig {
public static final String PROJECT_NAME = "projectName"; public static final String PROJECT_NAME = "projectName";
// this is our opinionated json type - ujson.Value - which is a first-class
// citizen of cask
private static final String AdditionalPropertiesType = "Value";
private final Logger LOGGER = LoggerFactory.getLogger(ScalaCaskServerCodegen.class); private final Logger LOGGER = LoggerFactory.getLogger(ScalaCaskServerCodegen.class);
@Override @Override
@ -115,6 +119,8 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
typeMapping.put("integer", "Int"); typeMapping.put("integer", "Int");
typeMapping.put("long", "Long"); typeMapping.put("long", "Long");
typeMapping.put("AnyType", AdditionalPropertiesType);
//TODO binary should be mapped to byte array //TODO binary should be mapped to byte array
// mapped to String as a workaround // mapped to String as a workaround
typeMapping.put("binary", "String"); typeMapping.put("binary", "String");
@ -241,6 +247,7 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
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("Value", "ujson.Value");
importMapping.put(AdditionalPropertiesType, "ujson.Value");
} }
static boolean consumesMimetype(CodegenOperation op, String mimetype) { static boolean consumesMimetype(CodegenOperation op, String mimetype) {
@ -614,7 +621,7 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
if (p.getIsEnumOrRef()) { if (p.getIsEnumOrRef()) {
p.defaultValue = "null"; p.defaultValue = "null";
} else { } else {
p.defaultValue = defaultValueNonOption(p); p.defaultValue = defaultValueNonOption(p, "null");
} }
} else if (p.defaultValue.contains("Seq.empty")) { } else if (p.defaultValue.contains("Seq.empty")) {
p.defaultValue = "Nil"; p.defaultValue = "Nil";
@ -767,6 +774,23 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
return defaultValueNonOption(p, fallbackDefaultValue); return defaultValueNonOption(p, fallbackDefaultValue);
} }
/**
* the subtypes of IJsonSchemaValidationProperties have an 'isNumeric', but that's not a method on IJsonSchemaValidationProperties.
*
* This helper method tries to isolate that noisy logic in a safe way so we can ask 'is this IJsonSchemaValidationProperties numeric'?
* @param p the property
* @return true if the property is numeric
*/
private static boolean isNumeric(IJsonSchemaValidationProperties p) {
if (p instanceof CodegenParameter) {
return ((CodegenParameter)p).isNumeric;
} else if (p instanceof CodegenProperty) {
return ((CodegenProperty)p).isNumeric;
} else {
return p.getIsNumber() || p.getIsFloat() || p.getIsDecimal() || p.getIsDouble() || p.getIsInteger() || p.getIsLong() || p.getIsUnboundedInteger();
}
}
private static String defaultValueNonOption(IJsonSchemaValidationProperties p, String fallbackDefaultValue) { private static String defaultValueNonOption(IJsonSchemaValidationProperties p, String fallbackDefaultValue) {
if (p.getIsArray()) { if (p.getIsArray()) {
if (p.getUniqueItems()) { if (p.getUniqueItems()) {
@ -777,7 +801,7 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
if (p.getIsMap()) { if (p.getIsMap()) {
return "Map.empty"; return "Map.empty";
} }
if (p.getIsNumber()) { if (isNumeric(p)) {
return "0"; return "0";
} }
if (p.getIsEnum()) { if (p.getIsEnum()) {
@ -792,37 +816,12 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
if (p.getIsString()) { if (p.getIsString()) {
return "\"\""; return "\"\"";
} }
if (fallbackDefaultValue != null && !fallbackDefaultValue.trim().isEmpty()) {
return fallbackDefaultValue; return fallbackDefaultValue;
} }
private static String defaultValueNonOption(CodegenProperty p) {
if (p.getIsArray()) {
return "Nil";
}
if (p.getIsMap()) {
return "Map.empty";
}
if (p.isNumber || p.isNumeric) {
return "0";
}
if (p.isBoolean) {
return "false";
}
if (p.isUuid) {
return "java.util.UUID.randomUUID()";
}
if (p.isModel) {
return "null"; return "null";
} }
if (p.isDate || p.isDateTime) {
return "null";
}
if (p.isString) {
return "\"\"";
}
return p.defaultValue;
}
@Override @Override
public CodegenProperty fromProperty(String name, Schema schema) { public CodegenProperty fromProperty(String name, Schema schema) {
@ -847,9 +846,9 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
@Override @Override
public String toModelImport(String name) { public String toModelImport(String name) {
final String result = super.toModelImport(name); String result = super.toModelImport(name);
if (importMapping.containsKey(name)) { if (importMapping.containsKey(name)) {
return importMapping.get(name); result = importMapping.get(name);
} }
return result; return result;
} }

View File

@ -11,6 +11,7 @@ import java.time.LocalDate
import java.util.UUID import java.util.UUID
import scala.reflect.ClassTag import scala.reflect.ClassTag
import scala.util.* import scala.util.*
import upickle.default.*
// needed for BigDecimal params // needed for BigDecimal params
given cask.endpoints.QueryParamReader.SimpleParam[BigDecimal](BigDecimal.apply) given cask.endpoints.QueryParamReader.SimpleParam[BigDecimal](BigDecimal.apply)
@ -143,6 +144,15 @@ extension (request: cask.Request) {
def bodyAsString = new String(request.readAllBytes(), "UTF-8") def bodyAsString = new String(request.readAllBytes(), "UTF-8")
def bodyAsJson : Try[ujson.Value] = {
val jason = bodyAsString
try {
Success(read[ujson.Value](jason))
} catch {
case scala.util.control.NonFatal(e) => sys.error(s"Error parsing json '$jason': $e")
}
}
def pathValue(index: Int, paramName: String, required : Boolean): Parsed[String] = { def pathValue(index: Int, paramName: String, required : Boolean): Parsed[String] = {
request request
.remainingPathSegments .remainingPathSegments

View File

@ -44,7 +44,7 @@ class {{classname}}Routes(service : {{classname}}Service) extends cask.Routes {
val result = {{>parseHttpParams}} val result = {{>parseHttpParams}}
result match { (result : @unchecked) match {
case Left(error) => cask.Response(error, 500) case Left(error) => cask.Response(error, 500)
{{#responses}} {{#responses}}
{{#dataType}} {{#dataType}}

View File

@ -1,6 +1,6 @@
//> using scala "3.3.1" //> using scala "3.3.1"
//> using lib "com.lihaoyi::cask:0.9.2" //> using dep "com.lihaoyi::cask:0.9.2"
//> using lib "com.lihaoyi::scalatags:0.8.2" //> using dep "com.lihaoyi::scalatags:0.8.2"
{{>licenseInfo}} {{>licenseInfo}}
// this file was generated from app.mustache // this file was generated from app.mustache
@ -11,11 +11,21 @@ package {{packageName}}
import _root_.{{modelPackage}}.* import _root_.{{modelPackage}}.*
import _root_.{{apiPackage}}.* import _root_.{{apiPackage}}.*
/** an example of how you can add your own additional routes to your app */
object MoreRoutes extends cask.Routes {
@cask.get("/echo")
def more(request: cask.Request) = s"request was ${request.bodyAsString}"
initialize()
}
/** /**
* This is an example of how you might extends BaseApp for a runnable application. * This is an example of how you might extends BaseApp for a runnable application.
* *
* See the README.md for how to create your own app * See the README.md for how to create your own app
*/ */
object ExampleApp extends BaseApp() { object ExampleApp extends BaseApp() {
// override to include our additional route
override def allRoutes = super.allRoutes ++ Option(MoreRoutes)
start() start()
} }

View File

@ -17,26 +17,28 @@ case class {{classname}}(
/* {{{description}}} */ /* {{{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}} {{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}}
{{/vars}}) { {{#isAdditionalPropertiesTrue}}, additionalProperties : ujson.Value = ujson.Null{{/isAdditionalPropertiesTrue}}
) {
def asJson: String = asData.asJson def asJsonString: String = asData.asJsonString
def asJson: ujson.Value = asData.asJson
def asData : {{classname}}Data = { def asData : {{classname}}Data = {
{{classname}}Data( {{classname}}Data(
{{#vars}} {{#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}} {{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}} {{/vars}}
{{#isAdditionalPropertiesTrue}}, additionalProperties{{/isAdditionalPropertiesTrue}}
) )
} }
} }
object {{classname}} { object {{classname}} {
given RW[{{classname}}] = summon[RW[ujson.Value]].bimap[{{classname}}](_.asJson, json => read[{{classname}}Data](json).asModel)
given RW[{{classname}}] = {{classname}}Data.readWriter.bimap[{{classname}}](_.asData, _.asModel) enum Fields(val fieldName : String) extends Field(fieldName) {
enum Fields(fieldName : String) extends Field(fieldName) {
{{#vars}} {{#vars}}
case {{name}} extends Fields("{{name}}") case {{name}} extends Fields("{{name}}")
{{/vars}} {{/vars}}

View File

@ -21,16 +21,28 @@ case class {{classname}}Data(
/* {{{description}}} */ /* {{{description}}} */
{{/description}} {{/description}}
{{name}}: {{#isEnum}}{{classname}}.{{datatypeWithEnum}}{{/isEnum}}{{^isEnum}}{{{vendorExtensions.x-datatype-data}}}{{/isEnum}}{{^required}} = {{{vendorExtensions.x-defaultValue-data}}} {{/required}}{{^-last}},{{/-last}} {{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}}
{{/vars}}) { ) derives RW {
def asJson: String = write(this) 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] = { def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = {
val errors = scala.collection.mutable.ListBuffer[ValidationError]() val errors = scala.collection.mutable.ListBuffer[ValidationError]()
{{#vars}} {{#vars}}
// ================== // ================== {{name}} validation ==================
// {{name}}
{{#pattern}} {{#pattern}}
// validate against pattern '{{{pattern}}}' // validate against pattern '{{{pattern}}}'
if (errors.isEmpty || !failFast) { if (errors.isEmpty || !failFast) {
@ -39,7 +51,6 @@ case class {{classname}}Data(
errors += ValidationError(path :+ {{classname}}.Fields.{{name}}, s"value '${{name}}' doesn't match pattern $regex") errors += ValidationError(path :+ {{classname}}.Fields.{{name}}, s"value '${{name}}' doesn't match pattern $regex")
} }
{{/pattern}} {{/pattern}}
{{#minimum}} {{#minimum}}
// validate against {{#exclusiveMinimum}}exclusive {{/exclusiveMinimum}}minimum {{minimum}} // validate against {{#exclusiveMinimum}}exclusive {{/exclusiveMinimum}}minimum {{minimum}}
if (errors.isEmpty || !failFast) { if (errors.isEmpty || !failFast) {
@ -47,7 +58,6 @@ case class {{classname}}Data(
errors += ValidationError(path :+ {{classname}}.Fields.{{name}}, s"value '${{name}}' is not greater than the {{#exclusiveMinimum}}exclusive {{/exclusiveMinimum}}minimum value {{minimum}}") errors += ValidationError(path :+ {{classname}}.Fields.{{name}}, s"value '${{name}}' is not greater than the {{#exclusiveMinimum}}exclusive {{/exclusiveMinimum}}minimum value {{minimum}}")
} }
{{/minimum}} {{/minimum}}
{{#maximum}} {{#maximum}}
// validate against {{#exclusiveMaximum}}exclusive {{/exclusiveMaximum}}maximum {{maximum}} // validate against {{#exclusiveMaximum}}exclusive {{/exclusiveMaximum}}maximum {{maximum}}
if (errors.isEmpty || !failFast) { if (errors.isEmpty || !failFast) {
@ -55,7 +65,6 @@ case class {{classname}}Data(
errors += ValidationError(path :+ {{classname}}.Fields.{{name}}, s"value '${{name}}' is not greater than the {{#exclusiveMaximum}}exclusive {{/exclusiveMaximum}}maximum value {{maximum}}") errors += ValidationError(path :+ {{classname}}.Fields.{{name}}, s"value '${{name}}' is not greater than the {{#exclusiveMaximum}}exclusive {{/exclusiveMaximum}}maximum value {{maximum}}")
} }
{{/maximum}} {{/maximum}}
{{#minLength}} {{#minLength}}
// validate min length {{minLength}} // validate min length {{minLength}}
if (errors.isEmpty || !failFast) { if (errors.isEmpty || !failFast) {
@ -65,7 +74,6 @@ case class {{classname}}Data(
} }
} }
{{/minLength}} {{/minLength}}
{{#maxLength}} {{#maxLength}}
// validate max length {{maxLength}} // validate max length {{maxLength}}
if (errors.isEmpty || !failFast) { if (errors.isEmpty || !failFast) {
@ -75,7 +83,6 @@ case class {{classname}}Data(
} }
} }
{{/maxLength}} {{/maxLength}}
{{#isEmail}} {{#isEmail}}
// validate {{name}} is a valid email address // validate {{name}} is a valid email address
if (errors.isEmpty || !failFast) { if (errors.isEmpty || !failFast) {
@ -86,7 +93,6 @@ case class {{classname}}Data(
} }
} }
{{/isEmail}} {{/isEmail}}
{{#required}}{{^isPrimitiveType}} {{#required}}{{^isPrimitiveType}}
if (errors.isEmpty || !failFast) { if (errors.isEmpty || !failFast) {
if ({{name}} == null) { if ({{name}} == null) {
@ -94,7 +100,6 @@ case class {{classname}}Data(
} }
} }
{{/isPrimitiveType}}{{/required}} {{/isPrimitiveType}}{{/required}}
{{#uniqueItems}} {{#uniqueItems}}
// validate {{name}} has unique items // validate {{name}} has unique items
if (errors.isEmpty || !failFast) { if (errors.isEmpty || !failFast) {
@ -111,7 +116,6 @@ case class {{classname}}Data(
} }
} }
{{/uniqueItems}} {{/uniqueItems}}
{{#multipleOf}} {{#multipleOf}}
if (errors.isEmpty || !failFast) { if (errors.isEmpty || !failFast) {
// validate {{name}} multiple of {{multipleOf}} // validate {{name}} multiple of {{multipleOf}}
@ -123,7 +127,6 @@ case class {{classname}}Data(
} }
} }
{{/multipleOf}} {{/multipleOf}}
{{#minItems}} {{#minItems}}
// validate min items {{minItems}} // validate min items {{minItems}}
if (errors.isEmpty || !failFast) { if (errors.isEmpty || !failFast) {
@ -133,7 +136,6 @@ case class {{classname}}Data(
} }
} }
{{/minItems}} {{/minItems}}
{{#maxItems}} {{#maxItems}}
// validate min items {{maxItems}} // validate min items {{maxItems}}
if (errors.isEmpty || !failFast) { if (errors.isEmpty || !failFast) {
@ -143,15 +145,8 @@ case class {{classname}}Data(
} }
} }
{{/maxItems}} {{/maxItems}}
{{#minProperties}} TODO - minProperties {{/minProperties}}
{{#minProperties}} {{#maxProperties}} TODO - maxProperties {{/maxProperties}}
TODO - minProperties
{{/minProperties}}
{{#maxProperties}}
TODO - maxProperties
{{/maxProperties}}
{{#items}}{{#isModel}} {{#items}}{{#isModel}}
if (errors.isEmpty || !failFast) { if (errors.isEmpty || !failFast) {
if ({{name}} != null) { if ({{name}} != null) {
@ -192,19 +187,35 @@ case class {{classname}}Data(
{{#vendorExtensions.x-wrap-in-optional}}){{/vendorExtensions.x-wrap-in-optional}} {{#vendorExtensions.x-wrap-in-optional}}){{/vendorExtensions.x-wrap-in-optional}}
{{#vendorExtensions.x-map-asModel}}.map(_.asModel){{/vendorExtensions.x-map-asModel}}{{^-last}},{{/-last}} {{#vendorExtensions.x-map-asModel}}.map(_.asModel){{/vendorExtensions.x-map-asModel}}{{^-last}},{{/-last}}
{{/vars}} {{/vars}}
{{#isAdditionalPropertiesTrue}}, additionalProperties{{/isAdditionalPropertiesTrue}}
) )
} }
} }
object {{classname}}Data { object {{classname}}Data {
given readWriter : RW[{{classname}}Data] = macroRW 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 = try { def fromJsonString(jason : String) : {{classname}}Data = {
read[{{classname}}Data](jason) val parsed = try {
read[ujson.Value](jason)
} catch { } catch {
case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e") case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e")
} }
fromJson(parsed)
}
def manyFromJsonString(jason : String) : Seq[{{classname}}Data] = try { def manyFromJsonString(jason : String) : Seq[{{classname}}Data] = try {
read[List[{{classname}}Data]](jason) read[List[{{classname}}Data]](jason)

View File

@ -51,3 +51,20 @@ given ReadWriter[OffsetDateTime] = readwriter[String].bimap[OffsetDateTime](
OffsetDateTime.parse(str, DateTimeFormatter.ISO_INSTANT) OffsetDateTime.parse(str, DateTimeFormatter.ISO_INSTANT)
) )
) )
extension (json: ujson.Value) {
def mergeWith(other: ujson.Value): ujson.Value = (json, other) match {
case (ujson.Obj(aMap), ujson.Obj(bMap)) =>
val mergedMap: scala.collection.mutable.Map[String, ujson.Value] = aMap ++ bMap.map {
case (k, v) => k -> (aMap.get(k) match {
case Some(aValue) => aValue.mergeWith(v)
case None => v
})
}
ujson.Obj.from(mergedMap)
case (ujson.Arr(aArray), ujson.Arr(bArray)) => ujson.Arr(aArray ++ bArray)
case (aValue, ujson.Null) => aValue
case (_, bValue) => bValue
}
}

View File

@ -63,7 +63,7 @@ class OpenApiRoutes(localPort: Int, swaggerUrl: Option[String]) extends cask.Rou
val urlConn = new URL(url).openConnection() val urlConn = new URL(url).openConnection()
urlConn.setRequestProperty("User-Agent", "Mozilla/5.0") urlConn.setRequestProperty("User-Agent", "Mozilla/5.0")
Using(urlConn.getInputStream) { inputStream => val extracted = Using(urlConn.getInputStream) { inputStream =>
val zipIn = new ZipInputStream(new BufferedInputStream(inputStream)) val zipIn = new ZipInputStream(new BufferedInputStream(inputStream))
LazyList.continually(zipIn.getNextEntry).takeWhile(_ != null).foreach { entry => LazyList.continually(zipIn.getNextEntry).takeWhile(_ != null).foreach { entry =>
@ -77,6 +77,12 @@ class OpenApiRoutes(localPort: Int, swaggerUrl: Option[String]) extends cask.Rou
zipIn.closeEntry() zipIn.closeEntry()
} }
} }
if (extracted.isFailure) {
println(s"Error extracting swagger: ${extracted}")
} else {
println(s"Extracting swagger: ${extracted}")
}
} }
def extractFile(name: String, zipIn: ZipInputStream, filePath: String): Unit = { def extractFile(name: String, zipIn: ZipInputStream, filePath: String): Unit = {

View File

@ -38,7 +38,8 @@
{{/vendorExtensions.x-deserialize-asModelMap}} {{/vendorExtensions.x-deserialize-asModelMap}}
{{/isMap}} {{/isMap}}
{{^isMap}} {{^isMap}}
{{paramName}}Data <- Parsed.eval({{vendorExtensions.x-container-type}}Data.fromJsonString(request.bodyAsString)).mapError(e => s"Error parsing json as {{vendorExtensions.x-container-type}} from >${request.bodyAsString}< : ${e}") /* not array or map */ {{paramName}}Json <- Parsed.fromTry(request.bodyAsJson)
{{paramName}}Data <- Parsed.eval({{vendorExtensions.x-container-type}}Data.fromJson({{paramName}}Json)) /* not array or map */
{{paramName}} <- Parsed.fromTry({{paramName}}Data.validated(failFast)) {{paramName}} <- Parsed.fromTry({{paramName}}Data.validated(failFast))
{{/isMap}} {{/isMap}}
{{/isArray}} {{/isArray}}

View File

@ -1,6 +1,6 @@
//> using scala "3.3.1" //> using scala "3.3.1"
//> using lib "com.lihaoyi::cask:0.9.2" //> using dep "com.lihaoyi::cask:0.9.2"
//> using lib "com.lihaoyi::scalatags:0.8.2" //> using dep "com.lihaoyi::scalatags:0.8.2"
/** /**
* OpenAPI Petstore * OpenAPI Petstore
* This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
@ -21,11 +21,21 @@ package cask.groupId.server
import _root_.sample.cask.model.* import _root_.sample.cask.model.*
import _root_.sample.cask.api.* import _root_.sample.cask.api.*
/** an example of how you can add your own additional routes to your app */
object MoreRoutes extends cask.Routes {
@cask.get("/echo")
def more(request: cask.Request) = s"request was ${request.bodyAsString}"
initialize()
}
/** /**
* This is an example of how you might extends BaseApp for a runnable application. * This is an example of how you might extends BaseApp for a runnable application.
* *
* See the README.md for how to create your own app * See the README.md for how to create your own app
*/ */
object ExampleApp extends BaseApp() { object ExampleApp extends BaseApp() {
// override to include our additional route
override def allRoutes = super.allRoutes ++ Option(MoreRoutes)
start() start()
} }

View File

@ -75,7 +75,7 @@ class OpenApiRoutes(localPort: Int, swaggerUrl: Option[String]) extends cask.Rou
val urlConn = new URL(url).openConnection() val urlConn = new URL(url).openConnection()
urlConn.setRequestProperty("User-Agent", "Mozilla/5.0") urlConn.setRequestProperty("User-Agent", "Mozilla/5.0")
Using(urlConn.getInputStream) { inputStream => val extracted = Using(urlConn.getInputStream) { inputStream =>
val zipIn = new ZipInputStream(new BufferedInputStream(inputStream)) val zipIn = new ZipInputStream(new BufferedInputStream(inputStream))
LazyList.continually(zipIn.getNextEntry).takeWhile(_ != null).foreach { entry => LazyList.continually(zipIn.getNextEntry).takeWhile(_ != null).foreach { entry =>
@ -89,6 +89,12 @@ class OpenApiRoutes(localPort: Int, swaggerUrl: Option[String]) extends cask.Rou
zipIn.closeEntry() zipIn.closeEntry()
} }
} }
if (extracted.isFailure) {
println(s"Error extracting swagger: ${extracted}")
} else {
println(s"Extracting swagger: ${extracted}")
}
} }
def extractFile(name: String, zipIn: ZipInputStream, filePath: String): Unit = { def extractFile(name: String, zipIn: ZipInputStream, filePath: String): Unit = {

View File

@ -60,12 +60,13 @@ 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 {
petData <- Parsed.eval(PetData.fromJsonString(request.bodyAsString)).mapError(e => s"Error parsing json as Pet from >${request.bodyAsString}< : ${e}") /* not array or map */ petJson <- Parsed.fromTry(request.bodyAsJson)
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)) result <- Parsed.eval(service.addPet(pet))
} yield result } yield result
result match { (result : @unchecked) match {
case Left(error) => cask.Response(error, 500) case Left(error) => cask.Response(error, 500)
case Right(value : Pet) => cask.Response(data = write(value), 200, headers = Seq("Content-Type" -> "application/json")) case Right(value : Pet) => cask.Response(data = write(value), 200, headers = Seq("Content-Type" -> "application/json"))
case Right(other) => cask.Response(s"$other", 200) case Right(other) => cask.Response(s"$other", 200)
@ -86,7 +87,7 @@ class PetRoutes(service : PetService) extends cask.Routes {
result <- Parsed.eval(service.deletePet(petId, apiKey)) result <- Parsed.eval(service.deletePet(petId, apiKey))
} yield result } yield result
result match { (result : @unchecked) match {
case Left(error) => cask.Response(error, 500) case Left(error) => cask.Response(error, 500)
case Right(other) => cask.Response(s"$other", 200) case Right(other) => cask.Response(s"$other", 200)
} }
@ -104,7 +105,7 @@ class PetRoutes(service : PetService) extends cask.Routes {
result <- Parsed.eval(service.findPetsByStatus(status)) result <- Parsed.eval(service.findPetsByStatus(status))
} yield result } yield result
result match { (result : @unchecked) match {
case Left(error) => cask.Response(error, 500) case Left(error) => cask.Response(error, 500)
case Right(value : List[Pet]) => cask.Response(data = write(value), 200, headers = Seq("Content-Type" -> "application/json")) case Right(value : List[Pet]) => cask.Response(data = write(value), 200, headers = Seq("Content-Type" -> "application/json"))
case Right(other) => cask.Response(s"$other", 200) case Right(other) => cask.Response(s"$other", 200)
@ -123,7 +124,7 @@ class PetRoutes(service : PetService) extends cask.Routes {
result <- Parsed.eval(service.findPetsByTags(tags)) result <- Parsed.eval(service.findPetsByTags(tags))
} yield result } yield result
result match { (result : @unchecked) match {
case Left(error) => cask.Response(error, 500) case Left(error) => cask.Response(error, 500)
case Right(value : List[Pet]) => cask.Response(data = write(value), 200, headers = Seq("Content-Type" -> "application/json")) case Right(value : List[Pet]) => cask.Response(data = write(value), 200, headers = Seq("Content-Type" -> "application/json"))
case Right(other) => cask.Response(s"$other", 200) case Right(other) => cask.Response(s"$other", 200)
@ -143,7 +144,7 @@ class PetRoutes(service : PetService) extends cask.Routes {
result <- Parsed.eval(service.getPetById(petId)) result <- Parsed.eval(service.getPetById(petId))
} yield result } yield result
result match { (result : @unchecked) match {
case Left(error) => cask.Response(error, 500) case Left(error) => cask.Response(error, 500)
case Right(value : Pet) => cask.Response(data = write(value), 200, headers = Seq("Content-Type" -> "application/json")) case Right(value : Pet) => cask.Response(data = write(value), 200, headers = Seq("Content-Type" -> "application/json"))
case Right(other) => cask.Response(s"$other", 200) case Right(other) => cask.Response(s"$other", 200)
@ -159,12 +160,13 @@ 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 {
petData <- Parsed.eval(PetData.fromJsonString(request.bodyAsString)).mapError(e => s"Error parsing json as Pet from >${request.bodyAsString}< : ${e}") /* not array or map */ petJson <- Parsed.fromTry(request.bodyAsJson)
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)) result <- Parsed.eval(service.updatePet(pet))
} yield result } yield result
result match { (result : @unchecked) match {
case Left(error) => cask.Response(error, 500) case Left(error) => cask.Response(error, 500)
case Right(value : Pet) => cask.Response(data = write(value), 200, headers = Seq("Content-Type" -> "application/json")) case Right(value : Pet) => cask.Response(data = write(value), 200, headers = Seq("Content-Type" -> "application/json"))
case Right(other) => cask.Response(s"$other", 200) case Right(other) => cask.Response(s"$other", 200)
@ -186,7 +188,7 @@ class PetRoutes(service : PetService) extends cask.Routes {
result <- Parsed.eval(service.updatePetWithForm(petId, name, status)) result <- Parsed.eval(service.updatePetWithForm(petId, name, status))
} yield result } yield result
result match { (result : @unchecked) match {
case Left(error) => cask.Response(error, 500) case Left(error) => cask.Response(error, 500)
case Right(other) => cask.Response(s"$other", 200) case Right(other) => cask.Response(s"$other", 200)
} }
@ -207,7 +209,7 @@ class PetRoutes(service : PetService) extends cask.Routes {
result <- Parsed.eval(service.uploadFile(petId, additionalMetadata, file)) result <- Parsed.eval(service.uploadFile(petId, additionalMetadata, file))
} yield result } yield result
result match { (result : @unchecked) match {
case Left(error) => cask.Response(error, 500) case Left(error) => cask.Response(error, 500)
case Right(value : ApiResponse) => cask.Response(data = write(value), 200, headers = Seq("Content-Type" -> "application/json")) case Right(value : ApiResponse) => cask.Response(data = write(value), 200, headers = Seq("Content-Type" -> "application/json"))
case Right(other) => cask.Response(s"$other", 200) case Right(other) => cask.Response(s"$other", 200)

View File

@ -41,7 +41,7 @@ class StoreRoutes(service : StoreService) extends cask.Routes {
result <- Parsed.eval(service.deleteOrder(orderId)) result <- Parsed.eval(service.deleteOrder(orderId))
} yield result } yield result
result match { (result : @unchecked) match {
case Left(error) => cask.Response(error, 500) case Left(error) => cask.Response(error, 500)
case Right(other) => cask.Response(s"$other", 200) case Right(other) => cask.Response(s"$other", 200)
} }
@ -59,7 +59,7 @@ class StoreRoutes(service : StoreService) extends cask.Routes {
result <- Parsed.eval(service.getInventory()) result <- Parsed.eval(service.getInventory())
} yield result } yield result
result match { (result : @unchecked) match {
case Left(error) => cask.Response(error, 500) case Left(error) => cask.Response(error, 500)
case Right(value : Map[String, Int]) => cask.Response(data = write(value), 200, headers = Seq("Content-Type" -> "application/json")) case Right(value : Map[String, Int]) => cask.Response(data = write(value), 200, headers = Seq("Content-Type" -> "application/json"))
case Right(other) => cask.Response(s"$other", 200) case Right(other) => cask.Response(s"$other", 200)
@ -78,7 +78,7 @@ class StoreRoutes(service : StoreService) extends cask.Routes {
result <- Parsed.eval(service.getOrderById(orderId)) result <- Parsed.eval(service.getOrderById(orderId))
} yield result } yield result
result match { (result : @unchecked) match {
case Left(error) => cask.Response(error, 500) case Left(error) => cask.Response(error, 500)
case Right(value : Order) => cask.Response(data = write(value), 200, headers = Seq("Content-Type" -> "application/json")) case Right(value : Order) => cask.Response(data = write(value), 200, headers = Seq("Content-Type" -> "application/json"))
case Right(other) => cask.Response(s"$other", 200) case Right(other) => cask.Response(s"$other", 200)
@ -93,12 +93,13 @@ 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 {
orderData <- Parsed.eval(OrderData.fromJsonString(request.bodyAsString)).mapError(e => s"Error parsing json as Order from >${request.bodyAsString}< : ${e}") /* not array or map */ orderJson <- Parsed.fromTry(request.bodyAsJson)
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)) result <- Parsed.eval(service.placeOrder(order))
} yield result } yield result
result match { (result : @unchecked) match {
case Left(error) => cask.Response(error, 500) case Left(error) => cask.Response(error, 500)
case Right(value : Order) => cask.Response(data = write(value), 200, headers = Seq("Content-Type" -> "application/json")) case Right(value : Order) => cask.Response(data = write(value), 200, headers = Seq("Content-Type" -> "application/json"))
case Right(other) => cask.Response(s"$other", 200) case Right(other) => cask.Response(s"$other", 200)

View File

@ -49,12 +49,13 @@ 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 {
userData <- Parsed.eval(UserData.fromJsonString(request.bodyAsString)).mapError(e => s"Error parsing json as User from >${request.bodyAsString}< : ${e}") /* not array or map */ userJson <- Parsed.fromTry(request.bodyAsJson)
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)) result <- Parsed.eval(service.createUser(user))
} yield result } yield result
result match { (result : @unchecked) match {
case Left(error) => cask.Response(error, 500) case Left(error) => cask.Response(error, 500)
case Right(other) => cask.Response(s"$other", 200) case Right(other) => cask.Response(s"$other", 200)
} }
@ -73,7 +74,7 @@ class UserRoutes(service : UserService) extends cask.Routes {
result <- Parsed.eval(service.createUsersWithArrayInput(user)) result <- Parsed.eval(service.createUsersWithArrayInput(user))
} yield result } yield result
result match { (result : @unchecked) match {
case Left(error) => cask.Response(error, 500) case Left(error) => cask.Response(error, 500)
case Right(other) => cask.Response(s"$other", 200) case Right(other) => cask.Response(s"$other", 200)
} }
@ -92,7 +93,7 @@ class UserRoutes(service : UserService) extends cask.Routes {
result <- Parsed.eval(service.createUsersWithListInput(user)) result <- Parsed.eval(service.createUsersWithListInput(user))
} yield result } yield result
result match { (result : @unchecked) match {
case Left(error) => cask.Response(error, 500) case Left(error) => cask.Response(error, 500)
case Right(other) => cask.Response(s"$other", 200) case Right(other) => cask.Response(s"$other", 200)
} }
@ -111,7 +112,7 @@ class UserRoutes(service : UserService) extends cask.Routes {
result <- Parsed.eval(service.deleteUser(username)) result <- Parsed.eval(service.deleteUser(username))
} yield result } yield result
result match { (result : @unchecked) match {
case Left(error) => cask.Response(error, 500) case Left(error) => cask.Response(error, 500)
case Right(other) => cask.Response(s"$other", 200) case Right(other) => cask.Response(s"$other", 200)
} }
@ -129,7 +130,7 @@ class UserRoutes(service : UserService) extends cask.Routes {
result <- Parsed.eval(service.getUserByName(username)) result <- Parsed.eval(service.getUserByName(username))
} yield result } yield result
result match { (result : @unchecked) match {
case Left(error) => cask.Response(error, 500) case Left(error) => cask.Response(error, 500)
case Right(value : User) => cask.Response(data = write(value), 200, headers = Seq("Content-Type" -> "application/json")) case Right(value : User) => cask.Response(data = write(value), 200, headers = Seq("Content-Type" -> "application/json"))
case Right(other) => cask.Response(s"$other", 200) case Right(other) => cask.Response(s"$other", 200)
@ -147,7 +148,7 @@ class UserRoutes(service : UserService) extends cask.Routes {
result <- Parsed.eval(service.loginUser(username, password)) result <- Parsed.eval(service.loginUser(username, password))
} yield result } yield result
result match { (result : @unchecked) match {
case Left(error) => cask.Response(error, 500) case Left(error) => cask.Response(error, 500)
case Right(value : String) => cask.Response(data = write(value), 200, headers = Seq("Content-Type" -> "application/json")) case Right(value : String) => cask.Response(data = write(value), 200, headers = Seq("Content-Type" -> "application/json"))
case Right(other) => cask.Response(s"$other", 200) case Right(other) => cask.Response(s"$other", 200)
@ -166,7 +167,7 @@ class UserRoutes(service : UserService) extends cask.Routes {
result <- Parsed.eval(service.logoutUser()) result <- Parsed.eval(service.logoutUser())
} yield result } yield result
result match { (result : @unchecked) match {
case Left(error) => cask.Response(error, 500) case Left(error) => cask.Response(error, 500)
case Right(other) => cask.Response(s"$other", 200) case Right(other) => cask.Response(s"$other", 200)
} }
@ -182,12 +183,13 @@ class UserRoutes(service : UserService) extends cask.Routes {
val result = for { val result = for {
username <- Parsed(username) username <- Parsed(username)
userData <- Parsed.eval(UserData.fromJsonString(request.bodyAsString)).mapError(e => s"Error parsing json as User from >${request.bodyAsString}< : ${e}") /* not array or map */ userJson <- Parsed.fromTry(request.bodyAsJson)
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)) result <- Parsed.eval(service.updateUser(username, user))
} yield result } yield result
result match { (result : @unchecked) match {
case Left(error) => cask.Response(error, 500) case Left(error) => cask.Response(error, 500)
case Right(other) => cask.Response(s"$other", 200) case Right(other) => cask.Response(s"$other", 200)
} }

View File

@ -23,6 +23,7 @@ import java.time.LocalDate
import java.util.UUID import java.util.UUID
import scala.reflect.ClassTag import scala.reflect.ClassTag
import scala.util.* import scala.util.*
import upickle.default.*
// needed for BigDecimal params // needed for BigDecimal params
given cask.endpoints.QueryParamReader.SimpleParam[BigDecimal](BigDecimal.apply) given cask.endpoints.QueryParamReader.SimpleParam[BigDecimal](BigDecimal.apply)
@ -155,6 +156,15 @@ extension (request: cask.Request) {
def bodyAsString = new String(request.readAllBytes(), "UTF-8") def bodyAsString = new String(request.readAllBytes(), "UTF-8")
def bodyAsJson : Try[ujson.Value] = {
val jason = bodyAsString
try {
Success(read[ujson.Value](jason))
} catch {
case scala.util.control.NonFatal(e) => sys.error(s"Error parsing json '$jason': $e")
}
}
def pathValue(index: Int, paramName: String, required : Boolean): Parsed[String] = { def pathValue(index: Int, paramName: String, required : Boolean): Parsed[String] = {
request request
.remainingPathSegments .remainingPathSegments

View File

@ -21,30 +21,29 @@ 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 asJson: String = asData.asJson def asJsonString: String = asData.asJsonString
def asJson: ujson.Value = asData.asJson
def asData : ApiResponseData = { def asData : ApiResponseData = {
ApiResponseData( ApiResponseData(
code = code.getOrElse(0), code = code.getOrElse(0),
`type` = `type`.getOrElse(""), `type` = `type`.getOrElse(""),
message = message.getOrElse("") message = message.getOrElse("")
) )
} }
} }
object ApiResponse { object ApiResponse {
given RW[ApiResponse] = summon[RW[ujson.Value]].bimap[ApiResponse](_.asJson, json => read[ApiResponseData](json).asModel)
given RW[ApiResponse] = ApiResponseData.readWriter.bimap[ApiResponse](_.asData, _.asModel) enum Fields(val fieldName : String) extends Field(fieldName) {
enum Fields(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

@ -25,66 +25,34 @@ import upickle.default.*
*/ */
case class ApiResponseData( case class ApiResponseData(
code: Int = 0 , code: Int = 0 ,
`type`: String = "" , `type`: String = "" ,
message: String = "" message: String = ""
) {
def asJson: String = write(this) ) derives RW {
def asJsonString: String = asJson.toString()
def asJson : ujson.Value = {
val jason = writeJs(this)
jason
}
def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = { def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = {
val errors = scala.collection.mutable.ListBuffer[ValidationError]() val errors = scala.collection.mutable.ListBuffer[ValidationError]()
// ================== // ================== code validation ==================
// code
// ==================
// `type`
// ==================
// message
// ================== `type` validation ==================
// ================== message validation ==================
@ -115,19 +83,28 @@ case class ApiResponseData(
message message
) )
) )
} }
} }
object ApiResponseData { object ApiResponseData {
given readWriter : RW[ApiResponseData] = macroRW def fromJson(jason : ujson.Value) : ApiResponseData = try {
val data = read[ApiResponseData](jason)
data
} catch {
case NonFatal(e) => sys.error(s"Error creating ApiResponseData from json '$jason': $e")
}
def fromJsonString(jason : String) : ApiResponseData = try { def fromJsonString(jason : String) : ApiResponseData = {
read[ApiResponseData](jason) val parsed = try {
read[ujson.Value](jason)
} catch { } catch {
case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e") case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e")
} }
fromJson(parsed)
}
def manyFromJsonString(jason : String) : Seq[ApiResponseData] = try { def manyFromJsonString(jason : String) : Seq[ApiResponseData] = try {
read[List[ApiResponseData]](jason) read[List[ApiResponseData]](jason)

View File

@ -21,27 +21,27 @@ 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 asJson: String = asData.asJson def asJsonString: String = asData.asJsonString
def asJson: ujson.Value = asData.asJson
def asData : CategoryData = { def asData : CategoryData = {
CategoryData( CategoryData(
id = id.getOrElse(0), id = id.getOrElse(0),
name = name.getOrElse("") name = name.getOrElse("")
) )
} }
} }
object Category { object Category {
given RW[Category] = summon[RW[ujson.Value]].bimap[Category](_.asJson, json => read[CategoryData](json).asModel)
given RW[Category] = CategoryData.readWriter.bimap[Category](_.asData, _.asModel) enum Fields(val fieldName : String) extends Field(fieldName) {
enum Fields(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

@ -25,35 +25,27 @@ import upickle.default.*
*/ */
case class CategoryData( case class CategoryData(
id: Long = 0 , id: Long = 0 ,
name: String = "" name: String = ""
) {
def asJson: String = write(this) ) derives RW {
def asJsonString: String = asJson.toString()
def asJson : ujson.Value = {
val jason = writeJs(this)
jason
}
def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = { def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = {
val errors = scala.collection.mutable.ListBuffer[ValidationError]() val errors = scala.collection.mutable.ListBuffer[ValidationError]()
// ================== // ================== id validation ==================
// id
// ================== name validation ==================
// ==================
// name
// validate against pattern '^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$' // validate against pattern '^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$'
if (errors.isEmpty || !failFast) { if (errors.isEmpty || !failFast) {
val regex = """^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$""" val regex = """^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$"""
@ -65,17 +57,6 @@ case class CategoryData(
errors.toSeq errors.toSeq
} }
@ -97,19 +78,28 @@ case class CategoryData(
name name
) )
) )
} }
} }
object CategoryData { object CategoryData {
given readWriter : RW[CategoryData] = macroRW def fromJson(jason : ujson.Value) : CategoryData = try {
val data = read[CategoryData](jason)
data
} catch {
case NonFatal(e) => sys.error(s"Error creating CategoryData from json '$jason': $e")
}
def fromJsonString(jason : String) : CategoryData = try { def fromJsonString(jason : String) : CategoryData = {
read[CategoryData](jason) val parsed = try {
read[ujson.Value](jason)
} catch { } catch {
case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e") case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e")
} }
fromJson(parsed)
}
def manyFromJsonString(jason : String) : Seq[CategoryData] = try { def manyFromJsonString(jason : String) : Seq[CategoryData] = try {
read[List[CategoryData]](jason) read[List[CategoryData]](jason)

View File

@ -22,21 +22,18 @@ 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 asJson: String = asData.asJson def asJsonString: String = asData.asJsonString
def asJson: ujson.Value = asData.asJson
def asData : OrderData = { def asData : OrderData = {
OrderData( OrderData(
@ -46,16 +43,15 @@ case class Order(
shipDate = shipDate.getOrElse(null), shipDate = shipDate.getOrElse(null),
status = status.getOrElse(null), status = status.getOrElse(null),
complete = complete.getOrElse(false) complete = complete.getOrElse(false)
) )
} }
} }
object Order { object Order {
given RW[Order] = summon[RW[ujson.Value]].bimap[Order](_.asJson, json => read[OrderData](json).asModel)
given RW[Order] = OrderData.readWriter.bimap[Order](_.asData, _.asModel) enum Fields(val fieldName : String) extends Field(fieldName) {
enum Fields(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")

View File

@ -26,127 +26,56 @@ import upickle.default.*
*/ */
case class OrderData( case class OrderData(
id: Long = 0 , id: Long = 0 ,
petId: Long = 0 , petId: Long = 0 ,
quantity: Int = 0 , quantity: Int = 0 ,
shipDate: OffsetDateTime = null , shipDate: OffsetDateTime = null ,
/* Order Status */ /* Order Status */
status: Order.StatusEnum = null , status: Order.StatusEnum = null ,
complete: Boolean = false complete: Boolean = false
) {
def asJson: String = write(this) ) derives RW {
def asJsonString: String = asJson.toString()
def asJson : ujson.Value = {
val jason = writeJs(this)
jason
}
def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = { def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = {
val errors = scala.collection.mutable.ListBuffer[ValidationError]() val errors = scala.collection.mutable.ListBuffer[ValidationError]()
// ================== // ================== id validation ==================
// id
// ================== petId validation ==================
// ================== quantity validation ==================
// ================== shipDate validation ==================
// ==================
// petId
// ==================
// quantity
// ==================
// shipDate
// ==================
// status
// ==================
// complete
// ================== status validation ==================
// ================== complete validation ==================
@ -189,19 +118,28 @@ case class OrderData(
complete complete
) )
) )
} }
} }
object OrderData { object OrderData {
given readWriter : RW[OrderData] = macroRW def fromJson(jason : ujson.Value) : OrderData = try {
val data = read[OrderData](jason)
data
} catch {
case NonFatal(e) => sys.error(s"Error creating OrderData from json '$jason': $e")
}
def fromJsonString(jason : String) : OrderData = try { def fromJsonString(jason : String) : OrderData = {
read[OrderData](jason) val parsed = try {
read[ujson.Value](jason)
} catch { } catch {
case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e") case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e")
} }
fromJson(parsed)
}
def manyFromJsonString(jason : String) : Seq[OrderData] = try { def manyFromJsonString(jason : String) : Seq[OrderData] = try {
read[List[OrderData]](jason) read[List[OrderData]](jason)

View File

@ -23,21 +23,18 @@ 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 asJson: String = asData.asJson def asJsonString: String = asData.asJsonString
def asJson: ujson.Value = asData.asJson
def asData : PetData = { def asData : PetData = {
PetData( PetData(
@ -47,16 +44,15 @@ case class Pet(
photoUrls = photoUrls, photoUrls = photoUrls,
tags = tags.map(_.asData), tags = tags.map(_.asData),
status = status.getOrElse(null) status = status.getOrElse(null)
) )
} }
} }
object Pet { object Pet {
given RW[Pet] = summon[RW[ujson.Value]].bimap[Pet](_.asJson, json => read[PetData](json).asModel)
given RW[Pet] = PetData.readWriter.bimap[Pet](_.asData, _.asModel) enum Fields(val fieldName : String) extends Field(fieldName) {
enum Fields(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")

View File

@ -27,55 +27,32 @@ import upickle.default.*
*/ */
case class PetData( case class PetData(
id: Long = 0 , id: Long = 0 ,
category: CategoryData = null , category: CategoryData = null ,
name: String, name: String,
photoUrls: Seq[String], photoUrls: Seq[String],
tags: Seq[TagData] = Nil , tags: Seq[TagData] = Nil ,
/* pet status in the store */ /* pet status in the store */
status: Pet.StatusEnum = null status: Pet.StatusEnum = null
) {
def asJson: String = write(this) ) derives RW {
def asJsonString: String = asJson.toString()
def asJson : ujson.Value = {
val jason = writeJs(this)
jason
}
def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = { def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = {
val errors = scala.collection.mutable.ListBuffer[ValidationError]() val errors = scala.collection.mutable.ListBuffer[ValidationError]()
// ================== // ================== id validation ==================
// id
// ==================
// category
// ================== category validation ==================
@ -85,55 +62,19 @@ case class PetData(
if category != null then errors ++= category.validationErrors(path :+ Pet.Fields.category, failFast) if category != null then errors ++= category.validationErrors(path :+ Pet.Fields.category, failFast)
} }
// ================== // ================== name validation ==================
// name
// ==================
// photoUrls
// ==================
// tags
// ================== photoUrls validation ==================
// ================== tags validation ==================
@ -151,19 +92,7 @@ case class PetData(
} }
// ================== // ================== status validation ==================
// status
@ -206,19 +135,28 @@ case class PetData(
status status
) )
) )
} }
} }
object PetData { object PetData {
given readWriter : RW[PetData] = macroRW def fromJson(jason : ujson.Value) : PetData = try {
val data = read[PetData](jason)
data
} catch {
case NonFatal(e) => sys.error(s"Error creating PetData from json '$jason': $e")
}
def fromJsonString(jason : String) : PetData = try { def fromJsonString(jason : String) : PetData = {
read[PetData](jason) val parsed = try {
read[ujson.Value](jason)
} catch { } catch {
case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e") case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e")
} }
fromJson(parsed)
}
def manyFromJsonString(jason : String) : Seq[PetData] = try { def manyFromJsonString(jason : String) : Seq[PetData] = try {
read[List[PetData]](jason) read[List[PetData]](jason)

View File

@ -21,27 +21,27 @@ 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 asJson: String = asData.asJson def asJsonString: String = asData.asJsonString
def asJson: ujson.Value = asData.asJson
def asData : TagData = { def asData : TagData = {
TagData( TagData(
id = id.getOrElse(0), id = id.getOrElse(0),
name = name.getOrElse("") name = name.getOrElse("")
) )
} }
} }
object Tag { object Tag {
given RW[Tag] = summon[RW[ujson.Value]].bimap[Tag](_.asJson, json => read[TagData](json).asModel)
given RW[Tag] = TagData.readWriter.bimap[Tag](_.asData, _.asModel) enum Fields(val fieldName : String) extends Field(fieldName) {
enum Fields(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

@ -25,46 +25,27 @@ import upickle.default.*
*/ */
case class TagData( case class TagData(
id: Long = 0 , id: Long = 0 ,
name: String = "" name: String = ""
) {
def asJson: String = write(this) ) derives RW {
def asJsonString: String = asJson.toString()
def asJson : ujson.Value = {
val jason = writeJs(this)
jason
}
def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = { def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = {
val errors = scala.collection.mutable.ListBuffer[ValidationError]() val errors = scala.collection.mutable.ListBuffer[ValidationError]()
// ================== // ================== id validation ==================
// id
// ==================
// name
// ================== name validation ==================
@ -91,19 +72,28 @@ case class TagData(
name name
) )
) )
} }
} }
object TagData { object TagData {
given readWriter : RW[TagData] = macroRW def fromJson(jason : ujson.Value) : TagData = try {
val data = read[TagData](jason)
data
} catch {
case NonFatal(e) => sys.error(s"Error creating TagData from json '$jason': $e")
}
def fromJsonString(jason : String) : TagData = try { def fromJsonString(jason : String) : TagData = {
read[TagData](jason) val parsed = try {
read[ujson.Value](jason)
} catch { } catch {
case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e") case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e")
} }
fromJson(parsed)
}
def manyFromJsonString(jason : String) : Seq[TagData] = try { def manyFromJsonString(jason : String) : Seq[TagData] = try {
read[List[TagData]](jason) read[List[TagData]](jason)

View File

@ -21,25 +21,20 @@ 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 asJson: String = asData.asJson def asJsonString: String = asData.asJsonString
def asJson: ujson.Value = asData.asJson
def asData : UserData = { def asData : UserData = {
UserData( UserData(
@ -51,16 +46,15 @@ case class User(
password = password.getOrElse(""), password = password.getOrElse(""),
phone = phone.getOrElse(""), phone = phone.getOrElse(""),
userStatus = userStatus.getOrElse(0) userStatus = userStatus.getOrElse(0)
) )
} }
} }
object User { object User {
given RW[User] = summon[RW[ujson.Value]].bimap[User](_.asJson, json => read[UserData](json).asModel)
given RW[User] = UserData.readWriter.bimap[User](_.asData, _.asModel) enum Fields(val fieldName : String) extends Field(fieldName) {
enum Fields(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")

View File

@ -25,167 +25,70 @@ import upickle.default.*
*/ */
case class UserData( case class UserData(
id: Long = 0 , id: Long = 0 ,
username: String = "" , username: String = "" ,
firstName: String = "" , firstName: String = "" ,
lastName: String = "" , lastName: String = "" ,
email: String = "" , email: String = "" ,
password: String = "" , password: String = "" ,
phone: String = "" , phone: String = "" ,
/* User Status */ /* User Status */
userStatus: Int = 0 userStatus: Int = 0
) {
def asJson: String = write(this) ) derives RW {
def asJsonString: String = asJson.toString()
def asJson : ujson.Value = {
val jason = writeJs(this)
jason
}
def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = { def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = {
val errors = scala.collection.mutable.ListBuffer[ValidationError]() val errors = scala.collection.mutable.ListBuffer[ValidationError]()
// ================== // ================== id validation ==================
// id
// ================== username validation ==================
// ================== firstName validation ==================
// ================== lastName validation ==================
// ==================
// username
// ================== email validation ==================
// ================== password validation ==================
// ================== phone validation ==================
// ==================
// firstName
// ==================
// lastName
// ==================
// email
// ==================
// password
// ==================
// phone
// ==================
// userStatus
// ================== userStatus validation ==================
@ -236,19 +139,28 @@ case class UserData(
userStatus userStatus
) )
) )
} }
} }
object UserData { object UserData {
given readWriter : RW[UserData] = macroRW def fromJson(jason : ujson.Value) : UserData = try {
val data = read[UserData](jason)
data
} catch {
case NonFatal(e) => sys.error(s"Error creating UserData from json '$jason': $e")
}
def fromJsonString(jason : String) : UserData = try { def fromJsonString(jason : String) : UserData = {
read[UserData](jason) val parsed = try {
read[ujson.Value](jason)
} catch { } catch {
case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e") case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e")
} }
fromJson(parsed)
}
def manyFromJsonString(jason : String) : Seq[UserData] = try { def manyFromJsonString(jason : String) : Seq[UserData] = try {
read[List[UserData]](jason) read[List[UserData]](jason)

View File

@ -63,3 +63,20 @@ given ReadWriter[OffsetDateTime] = readwriter[String].bimap[OffsetDateTime](
OffsetDateTime.parse(str, DateTimeFormatter.ISO_INSTANT) OffsetDateTime.parse(str, DateTimeFormatter.ISO_INSTANT)
) )
) )
extension (json: ujson.Value) {
def mergeWith(other: ujson.Value): ujson.Value = (json, other) match {
case (ujson.Obj(aMap), ujson.Obj(bMap)) =>
val mergedMap: scala.collection.mutable.Map[String, ujson.Value] = aMap ++ bMap.map {
case (k, v) => k -> (aMap.get(k) match {
case Some(aValue) => aValue.mergeWith(v)
case None => v
})
}
ujson.Obj.from(mergedMap)
case (ujson.Arr(aArray), ujson.Arr(bArray)) => ujson.Arr(aArray ++ bArray)
case (aValue, ujson.Null) => aValue
case (_, bValue) => bValue
}
}