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;
}
} 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);
additionalPropertiesIsAnyType = true;
}

View File

@ -39,6 +39,10 @@ import static org.openapitools.codegen.utils.StringUtils.camelize;
public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements CodegenConfig {
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);
@Override
@ -115,6 +119,8 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
typeMapping.put("integer", "Int");
typeMapping.put("long", "Long");
typeMapping.put("AnyType", AdditionalPropertiesType);
//TODO binary should be mapped to byte array
// mapped to String as a workaround
typeMapping.put("binary", "String");
@ -241,6 +247,7 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
importMapping.put("OffsetDateTime", "java.time.OffsetDateTime");
importMapping.put("LocalTime", "java.time.LocalTime");
importMapping.put("Value", "ujson.Value");
importMapping.put(AdditionalPropertiesType, "ujson.Value");
}
static boolean consumesMimetype(CodegenOperation op, String mimetype) {
@ -614,7 +621,7 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
if (p.getIsEnumOrRef()) {
p.defaultValue = "null";
} else {
p.defaultValue = defaultValueNonOption(p);
p.defaultValue = defaultValueNonOption(p, "null");
}
} else if (p.defaultValue.contains("Seq.empty")) {
p.defaultValue = "Nil";
@ -767,6 +774,23 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
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) {
if (p.getIsArray()) {
if (p.getUniqueItems()) {
@ -777,7 +801,7 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
if (p.getIsMap()) {
return "Map.empty";
}
if (p.getIsNumber()) {
if (isNumeric(p)) {
return "0";
}
if (p.getIsEnum()) {
@ -792,38 +816,13 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
if (p.getIsString()) {
return "\"\"";
}
return fallbackDefaultValue;
}
if (fallbackDefaultValue != null && !fallbackDefaultValue.trim().isEmpty()) {
return fallbackDefaultValue;
}
private static String defaultValueNonOption(CodegenProperty p) {
if (p.getIsArray()) {
return "Nil";
}
if (p.getIsMap()) {
return "Map.empty";
}
if (p.isNumber || p.isNumeric) {
return "0";
}
if (p.isBoolean) {
return "false";
}
if (p.isUuid) {
return "java.util.UUID.randomUUID()";
}
if (p.isModel) {
return "null";
}
if (p.isDate || p.isDateTime) {
return "null";
}
if (p.isString) {
return "\"\"";
}
return p.defaultValue;
return "null";
}
@Override
public CodegenProperty fromProperty(String name, Schema schema) {
CodegenProperty property = super.fromProperty(name, schema);
@ -847,9 +846,9 @@ public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements Code
@Override
public String toModelImport(String name) {
final String result = super.toModelImport(name);
String result = super.toModelImport(name);
if (importMapping.containsKey(name)) {
return importMapping.get(name);
result = importMapping.get(name);
}
return result;
}

View File

@ -11,6 +11,7 @@ import java.time.LocalDate
import java.util.UUID
import scala.reflect.ClassTag
import scala.util.*
import upickle.default.*
// needed for BigDecimal params
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 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] = {
request
.remainingPathSegments

View File

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

View File

@ -1,6 +1,6 @@
//> using scala "3.3.1"
//> using lib "com.lihaoyi::cask:0.9.2"
//> using lib "com.lihaoyi::scalatags:0.8.2"
//> using dep "com.lihaoyi::cask:0.9.2"
//> using dep "com.lihaoyi::scalatags:0.8.2"
{{>licenseInfo}}
// this file was generated from app.mustache
@ -11,11 +11,21 @@ package {{packageName}}
import _root_.{{modelPackage}}.*
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.
*
* See the README.md for how to create your own app
*/
object ExampleApp extends BaseApp() {
// override to include our additional route
override def allRoutes = super.allRoutes ++ Option(MoreRoutes)
start()
}

View File

@ -17,26 +17,28 @@ case class {{classname}}(
/* {{{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}}
{{/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 = {
{{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}}{
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(fieldName : String) extends Field(fieldName) {
enum Fields(val fieldName : String) extends Field(fieldName) {
{{#vars}}
case {{name}} extends Fields("{{name}}")
{{/vars}}

View File

@ -21,16 +21,28 @@ case class {{classname}}Data(
/* {{{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}}
{{/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] = {
val errors = scala.collection.mutable.ListBuffer[ValidationError]()
{{#vars}}
// ==================
// {{name}}
// ================== {{name}} validation ==================
{{#pattern}}
// validate against pattern '{{{pattern}}}'
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")
}
{{/pattern}}
{{#minimum}}
// validate against {{#exclusiveMinimum}}exclusive {{/exclusiveMinimum}}minimum {{minimum}}
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}}")
}
{{/minimum}}
{{#maximum}}
// validate against {{#exclusiveMaximum}}exclusive {{/exclusiveMaximum}}maximum {{maximum}}
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}}")
}
{{/maximum}}
{{#minLength}}
// validate min length {{minLength}}
if (errors.isEmpty || !failFast) {
@ -65,7 +74,6 @@ case class {{classname}}Data(
}
}
{{/minLength}}
{{#maxLength}}
// validate max length {{maxLength}}
if (errors.isEmpty || !failFast) {
@ -75,7 +83,6 @@ case class {{classname}}Data(
}
}
{{/maxLength}}
{{#isEmail}}
// validate {{name}} is a valid email address
if (errors.isEmpty || !failFast) {
@ -86,7 +93,6 @@ case class {{classname}}Data(
}
}
{{/isEmail}}
{{#required}}{{^isPrimitiveType}}
if (errors.isEmpty || !failFast) {
if ({{name}} == null) {
@ -94,7 +100,6 @@ case class {{classname}}Data(
}
}
{{/isPrimitiveType}}{{/required}}
{{#uniqueItems}}
// validate {{name}} has unique items
if (errors.isEmpty || !failFast) {
@ -111,7 +116,6 @@ case class {{classname}}Data(
}
}
{{/uniqueItems}}
{{#multipleOf}}
if (errors.isEmpty || !failFast) {
// validate {{name}} multiple of {{multipleOf}}
@ -123,7 +127,6 @@ case class {{classname}}Data(
}
}
{{/multipleOf}}
{{#minItems}}
// validate min items {{minItems}}
if (errors.isEmpty || !failFast) {
@ -133,7 +136,6 @@ case class {{classname}}Data(
}
}
{{/minItems}}
{{#maxItems}}
// validate min items {{maxItems}}
if (errors.isEmpty || !failFast) {
@ -143,15 +145,8 @@ case class {{classname}}Data(
}
}
{{/maxItems}}
{{#minProperties}}
TODO - minProperties
{{/minProperties}}
{{#maxProperties}}
TODO - maxProperties
{{/maxProperties}}
{{#minProperties}} TODO - minProperties {{/minProperties}}
{{#maxProperties}} TODO - maxProperties {{/maxProperties}}
{{#items}}{{#isModel}}
if (errors.isEmpty || !failFast) {
if ({{name}} != null) {
@ -192,19 +187,35 @@ case class {{classname}}Data(
{{#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 {
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 {
read[{{classname}}Data](jason)
} catch {
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)

View File

@ -51,3 +51,20 @@ given ReadWriter[OffsetDateTime] = readwriter[String].bimap[OffsetDateTime](
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()
urlConn.setRequestProperty("User-Agent", "Mozilla/5.0")
Using(urlConn.getInputStream) { inputStream =>
val extracted = Using(urlConn.getInputStream) { inputStream =>
val zipIn = new ZipInputStream(new BufferedInputStream(inputStream))
LazyList.continually(zipIn.getNextEntry).takeWhile(_ != null).foreach { entry =>
@ -77,6 +77,12 @@ class OpenApiRoutes(localPort: Int, swaggerUrl: Option[String]) extends cask.Rou
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 = {

View File

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

View File

@ -1,6 +1,6 @@
//> using scala "3.3.1"
//> using lib "com.lihaoyi::cask:0.9.2"
//> using lib "com.lihaoyi::scalatags:0.8.2"
//> using dep "com.lihaoyi::cask:0.9.2"
//> using dep "com.lihaoyi::scalatags:0.8.2"
/**
* OpenAPI Petstore
* This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
@ -21,11 +21,21 @@ package cask.groupId.server
import _root_.sample.cask.model.*
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.
*
* See the README.md for how to create your own app
*/
object ExampleApp extends BaseApp() {
// override to include our additional route
override def allRoutes = super.allRoutes ++ Option(MoreRoutes)
start()
}

View File

@ -75,7 +75,7 @@ class OpenApiRoutes(localPort: Int, swaggerUrl: Option[String]) extends cask.Rou
val urlConn = new URL(url).openConnection()
urlConn.setRequestProperty("User-Agent", "Mozilla/5.0")
Using(urlConn.getInputStream) { inputStream =>
val extracted = Using(urlConn.getInputStream) { inputStream =>
val zipIn = new ZipInputStream(new BufferedInputStream(inputStream))
LazyList.continually(zipIn.getNextEntry).takeWhile(_ != null).foreach { entry =>
@ -89,6 +89,12 @@ class OpenApiRoutes(localPort: Int, swaggerUrl: Option[String]) extends cask.Rou
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 = {

View File

@ -60,12 +60,13 @@ class PetRoutes(service : PetService) extends cask.Routes {
def failFast = request.queryParams.keySet.contains("failFast")
val result = for {
petData <- Parsed.eval(PetData.fromJsonString(request.bodyAsString)).mapError(e => s"Error parsing json as Pet from >${request.bodyAsString}< : ${e}") /* not array or map */
petJson <- Parsed.fromTry(request.bodyAsJson)
petData <- Parsed.eval(PetData.fromJson(petJson)) /* not array or map */
pet <- Parsed.fromTry(petData.validated(failFast))
result <- Parsed.eval(service.addPet(pet))
} yield result
result match {
(result : @unchecked) match {
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(other) => cask.Response(s"$other", 200)
@ -86,7 +87,7 @@ class PetRoutes(service : PetService) extends cask.Routes {
result <- Parsed.eval(service.deletePet(petId, apiKey))
} yield result
result match {
(result : @unchecked) match {
case Left(error) => cask.Response(error, 500)
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))
} yield result
result match {
(result : @unchecked) match {
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(other) => cask.Response(s"$other", 200)
@ -123,7 +124,7 @@ class PetRoutes(service : PetService) extends cask.Routes {
result <- Parsed.eval(service.findPetsByTags(tags))
} yield result
result match {
(result : @unchecked) match {
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(other) => cask.Response(s"$other", 200)
@ -143,7 +144,7 @@ class PetRoutes(service : PetService) extends cask.Routes {
result <- Parsed.eval(service.getPetById(petId))
} yield result
result match {
(result : @unchecked) match {
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(other) => cask.Response(s"$other", 200)
@ -159,12 +160,13 @@ class PetRoutes(service : PetService) extends cask.Routes {
def failFast = request.queryParams.keySet.contains("failFast")
val result = for {
petData <- Parsed.eval(PetData.fromJsonString(request.bodyAsString)).mapError(e => s"Error parsing json as Pet from >${request.bodyAsString}< : ${e}") /* not array or map */
petJson <- Parsed.fromTry(request.bodyAsJson)
petData <- Parsed.eval(PetData.fromJson(petJson)) /* not array or map */
pet <- Parsed.fromTry(petData.validated(failFast))
result <- Parsed.eval(service.updatePet(pet))
} yield result
result match {
(result : @unchecked) match {
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(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))
} yield result
result match {
(result : @unchecked) match {
case Left(error) => cask.Response(error, 500)
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))
} yield result
result match {
(result : @unchecked) match {
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(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))
} yield result
result match {
(result : @unchecked) match {
case Left(error) => cask.Response(error, 500)
case Right(other) => cask.Response(s"$other", 200)
}
@ -59,7 +59,7 @@ class StoreRoutes(service : StoreService) extends cask.Routes {
result <- Parsed.eval(service.getInventory())
} yield result
result match {
(result : @unchecked) match {
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(other) => cask.Response(s"$other", 200)
@ -78,7 +78,7 @@ class StoreRoutes(service : StoreService) extends cask.Routes {
result <- Parsed.eval(service.getOrderById(orderId))
} yield result
result match {
(result : @unchecked) match {
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(other) => cask.Response(s"$other", 200)
@ -93,12 +93,13 @@ class StoreRoutes(service : StoreService) extends cask.Routes {
def failFast = request.queryParams.keySet.contains("failFast")
val result = for {
orderData <- Parsed.eval(OrderData.fromJsonString(request.bodyAsString)).mapError(e => s"Error parsing json as Order from >${request.bodyAsString}< : ${e}") /* not array or map */
orderJson <- Parsed.fromTry(request.bodyAsJson)
orderData <- Parsed.eval(OrderData.fromJson(orderJson)) /* not array or map */
order <- Parsed.fromTry(orderData.validated(failFast))
result <- Parsed.eval(service.placeOrder(order))
} yield result
result match {
(result : @unchecked) match {
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(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")
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))
result <- Parsed.eval(service.createUser(user))
} yield result
result match {
(result : @unchecked) match {
case Left(error) => cask.Response(error, 500)
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))
} yield result
result match {
(result : @unchecked) match {
case Left(error) => cask.Response(error, 500)
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))
} yield result
result match {
(result : @unchecked) match {
case Left(error) => cask.Response(error, 500)
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))
} yield result
result match {
(result : @unchecked) match {
case Left(error) => cask.Response(error, 500)
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))
} yield result
result match {
(result : @unchecked) match {
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(other) => cask.Response(s"$other", 200)
@ -147,7 +148,7 @@ class UserRoutes(service : UserService) extends cask.Routes {
result <- Parsed.eval(service.loginUser(username, password))
} yield result
result match {
(result : @unchecked) match {
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(other) => cask.Response(s"$other", 200)
@ -166,7 +167,7 @@ class UserRoutes(service : UserService) extends cask.Routes {
result <- Parsed.eval(service.logoutUser())
} yield result
result match {
(result : @unchecked) match {
case Left(error) => cask.Response(error, 500)
case Right(other) => cask.Response(s"$other", 200)
}
@ -182,12 +183,13 @@ class UserRoutes(service : UserService) extends cask.Routes {
val result = for {
username <- Parsed(username)
userData <- Parsed.eval(UserData.fromJsonString(request.bodyAsString)).mapError(e => s"Error parsing json as User from >${request.bodyAsString}< : ${e}") /* not array or map */
userJson <- Parsed.fromTry(request.bodyAsJson)
userData <- Parsed.eval(UserData.fromJson(userJson)) /* not array or map */
user <- Parsed.fromTry(userData.validated(failFast))
result <- Parsed.eval(service.updateUser(username, user))
} yield result
result match {
(result : @unchecked) match {
case Left(error) => cask.Response(error, 500)
case Right(other) => cask.Response(s"$other", 200)
}

View File

@ -23,6 +23,7 @@ import java.time.LocalDate
import java.util.UUID
import scala.reflect.ClassTag
import scala.util.*
import upickle.default.*
// needed for BigDecimal params
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 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] = {
request
.remainingPathSegments

View File

@ -21,30 +21,29 @@ import upickle.default.*
case class ApiResponse(
code: Option[Int] = None ,
`type`: Option[String] = None ,
message: Option[String] = None
`type`: 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 = {
ApiResponseData(
code = code.getOrElse(0),
`type` = `type`.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(fieldName : String) extends Field(fieldName) {
enum Fields(val fieldName : String) extends Field(fieldName) {
case code extends Fields("code")
case `type` extends Fields("`type`")
case message extends Fields("message")

View File

@ -25,66 +25,34 @@ import upickle.default.*
*/
case class ApiResponseData(
code: Int = 0 ,
`type`: String = "" ,
message: String = ""
`type`: String = "" ,
message: String = ""
) derives RW {
) {
def asJsonString: String = asJson.toString()
def asJson: String = write(this)
def asJson : ujson.Value = {
val jason = writeJs(this)
jason
}
def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = {
val errors = scala.collection.mutable.ListBuffer[ValidationError]()
// ==================
// code
// ==================
// `type`
// ==================
// message
// ================== code validation ==================
// ================== `type` validation ==================
// ================== message validation ==================
@ -115,19 +83,28 @@ case class ApiResponseData(
message
)
)
}
}
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 {
read[ApiResponseData](jason)
} catch {
def fromJsonString(jason : String) : ApiResponseData = {
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[ApiResponseData] = try {
read[List[ApiResponseData]](jason)

View File

@ -21,27 +21,27 @@ import upickle.default.*
case class Category(
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 = {
CategoryData(
id = id.getOrElse(0),
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(fieldName : String) extends Field(fieldName) {
enum Fields(val fieldName : String) extends Field(fieldName) {
case id extends Fields("id")
case name extends Fields("name")
}

View File

@ -25,35 +25,27 @@ import upickle.default.*
*/
case class CategoryData(
id: Long = 0 ,
name: String = ""
name: String = ""
) {
) derives RW {
def asJson: String = write(this)
def asJsonString: String = asJson.toString()
def asJson : ujson.Value = {
val jason = writeJs(this)
jason
}
def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = {
val errors = scala.collection.mutable.ListBuffer[ValidationError]()
// ==================
// id
// ================== id validation ==================
// ==================
// name
// ================== name validation ==================
// validate against pattern '^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$'
if (errors.isEmpty || !failFast) {
val regex = """^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$"""
@ -65,17 +57,6 @@ case class CategoryData(
errors.toSeq
}
@ -97,19 +78,28 @@ case class CategoryData(
name
)
)
}
}
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 {
read[CategoryData](jason)
} catch {
def fromJsonString(jason : String) : CategoryData = {
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[CategoryData] = try {
read[List[CategoryData]](jason)

View File

@ -22,21 +22,18 @@ import upickle.default.*
case class Order(
id: Option[Long] = None ,
petId: Option[Long] = None ,
quantity: Option[Int] = None ,
shipDate: Option[OffsetDateTime] = None ,
/* Order Status */
petId: Option[Long] = None ,
quantity: Option[Int] = None ,
shipDate: Option[OffsetDateTime] = None ,
/* Order Status */
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 = {
OrderData(
@ -46,16 +43,15 @@ case class Order(
shipDate = shipDate.getOrElse(null),
status = status.getOrElse(null),
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(fieldName : String) extends Field(fieldName) {
enum Fields(val fieldName : String) extends Field(fieldName) {
case id extends Fields("id")
case petId extends Fields("petId")
case quantity extends Fields("quantity")

View File

@ -26,127 +26,56 @@ import upickle.default.*
*/
case class OrderData(
id: Long = 0 ,
petId: Long = 0 ,
quantity: Int = 0 ,
shipDate: OffsetDateTime = null ,
/* Order Status */
petId: Long = 0 ,
quantity: Int = 0 ,
shipDate: OffsetDateTime = null ,
/* Order Status */
status: Order.StatusEnum = null ,
complete: Boolean = false
complete: Boolean = false
) {
) derives RW {
def asJson: String = write(this)
def asJsonString: String = asJson.toString()
def asJson : ujson.Value = {
val jason = writeJs(this)
jason
}
def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = {
val errors = scala.collection.mutable.ListBuffer[ValidationError]()
// ==================
// id
// ================== id validation ==================
// ================== petId validation ==================
// ================== quantity validation ==================
// ==================
// petId
// ==================
// quantity
// ==================
// shipDate
// ==================
// status
// ==================
// complete
// ================== shipDate validation ==================
// ================== status validation ==================
// ================== complete validation ==================
@ -189,19 +118,28 @@ case class OrderData(
complete
)
)
}
}
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 {
read[OrderData](jason)
} catch {
def fromJsonString(jason : String) : OrderData = {
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[OrderData] = try {
read[List[OrderData]](jason)

View File

@ -23,21 +23,18 @@ import upickle.default.*
case class Pet(
id: Option[Long] = None ,
category: Option[Category] = None ,
name: String,
photoUrls: Seq[String],
tags: Seq[Tag] = Nil ,
/* pet status in the store */
category: Option[Category] = None ,
name: String,
photoUrls: Seq[String],
tags: Seq[Tag] = Nil ,
/* pet status in the store */
status: Option[Pet.StatusEnum] = None
) {
def asJson: String = asData.asJson
) {
def asJsonString: String = asData.asJsonString
def asJson: ujson.Value = asData.asJson
def asData : PetData = {
PetData(
@ -47,16 +44,15 @@ case class Pet(
photoUrls = photoUrls,
tags = tags.map(_.asData),
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(fieldName : String) extends Field(fieldName) {
enum Fields(val fieldName : String) extends Field(fieldName) {
case id extends Fields("id")
case category extends Fields("category")
case name extends Fields("name")

View File

@ -27,55 +27,32 @@ import upickle.default.*
*/
case class PetData(
id: Long = 0 ,
category: CategoryData = null ,
name: String,
photoUrls: Seq[String],
tags: Seq[TagData] = Nil ,
/* pet status in the store */
category: CategoryData = null ,
name: String,
photoUrls: Seq[String],
tags: Seq[TagData] = Nil ,
/* pet status in the store */
status: Pet.StatusEnum = null
) {
def asJson: String = write(this)
) 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] = {
val errors = scala.collection.mutable.ListBuffer[ValidationError]()
// ==================
// id
// ==================
// category
// ================== id validation ==================
// ================== category validation ==================
@ -85,55 +62,19 @@ case class PetData(
if category != null then errors ++= category.validationErrors(path :+ Pet.Fields.category, failFast)
}
// ==================
// name
// ==================
// photoUrls
// ==================
// tags
// ================== name validation ==================
// ================== photoUrls validation ==================
// ================== tags validation ==================
@ -151,19 +92,7 @@ case class PetData(
}
// ==================
// status
// ================== status validation ==================
@ -206,19 +135,28 @@ case class PetData(
status
)
)
}
}
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 {
read[PetData](jason)
} catch {
def fromJsonString(jason : String) : PetData = {
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[PetData] = try {
read[List[PetData]](jason)

View File

@ -21,27 +21,27 @@ import upickle.default.*
case class Tag(
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 = {
TagData(
id = id.getOrElse(0),
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(fieldName : String) extends Field(fieldName) {
enum Fields(val fieldName : String) extends Field(fieldName) {
case id extends Fields("id")
case name extends Fields("name")
}

View File

@ -25,46 +25,27 @@ import upickle.default.*
*/
case class TagData(
id: Long = 0 ,
name: String = ""
name: String = ""
) {
) derives RW {
def asJson: String = write(this)
def asJsonString: String = asJson.toString()
def asJson : ujson.Value = {
val jason = writeJs(this)
jason
}
def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = {
val errors = scala.collection.mutable.ListBuffer[ValidationError]()
// ==================
// id
// ==================
// name
// ================== id validation ==================
// ================== name validation ==================
@ -91,19 +72,28 @@ case class TagData(
name
)
)
}
}
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 {
read[TagData](jason)
} catch {
def fromJsonString(jason : String) : TagData = {
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[TagData] = try {
read[List[TagData]](jason)

View File

@ -21,25 +21,20 @@ import upickle.default.*
case class User(
id: Option[Long] = None ,
username: Option[String] = None ,
firstName: Option[String] = None ,
lastName: Option[String] = None ,
email: Option[String] = None ,
password: Option[String] = None ,
phone: Option[String] = None ,
/* User Status */
username: Option[String] = None ,
firstName: Option[String] = None ,
lastName: Option[String] = None ,
email: Option[String] = None ,
password: Option[String] = None ,
phone: Option[String] = None ,
/* User Status */
userStatus: Option[Int] = None
) {
def asJson: String = asData.asJson
) {
def asJsonString: String = asData.asJsonString
def asJson: ujson.Value = asData.asJson
def asData : UserData = {
UserData(
@ -51,16 +46,15 @@ case class User(
password = password.getOrElse(""),
phone = phone.getOrElse(""),
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(fieldName : String) extends Field(fieldName) {
enum Fields(val fieldName : String) extends Field(fieldName) {
case id extends Fields("id")
case username extends Fields("username")
case firstName extends Fields("firstName")

View File

@ -25,167 +25,70 @@ import upickle.default.*
*/
case class UserData(
id: Long = 0 ,
username: String = "" ,
firstName: String = "" ,
lastName: String = "" ,
email: String = "" ,
password: String = "" ,
phone: String = "" ,
/* User Status */
username: String = "" ,
firstName: String = "" ,
lastName: String = "" ,
email: String = "" ,
password: String = "" ,
phone: String = "" ,
/* User Status */
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] = {
val errors = scala.collection.mutable.ListBuffer[ValidationError]()
// ==================
// id
// ================== id validation ==================
// ================== username validation ==================
// ================== firstName validation ==================
// ================== lastName validation ==================
// ==================
// username
// ================== email validation ==================
// ================== password validation ==================
// ==================
// firstName
// ==================
// lastName
// ==================
// email
// ==================
// password
// ==================
// phone
// ==================
// userStatus
// ================== phone validation ==================
// ================== userStatus validation ==================
@ -236,19 +139,28 @@ case class UserData(
userStatus
)
)
}
}
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 {
read[UserData](jason)
} catch {
def fromJsonString(jason : String) : UserData = {
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[UserData] = try {
read[List[UserData]](jason)

View File

@ -63,3 +63,20 @@ given ReadWriter[OffsetDateTime] = readwriter[String].bimap[OffsetDateTime](
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
}
}