diff --git a/bin/configs/scala-cask-petstore-new.yaml b/bin/configs/scala-cask-petstore.yaml similarity index 100% rename from bin/configs/scala-cask-petstore-new.yaml rename to bin/configs/scala-cask-petstore.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index 5ee43cb4261..e48f46c981f 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -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; } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaCaskServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaCaskServerCodegen.java index c6eae192b9d..e5a56b02fed 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaCaskServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaCaskServerCodegen.java @@ -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; } diff --git a/modules/openapi-generator/src/main/resources/scala-cask/apiPackage.mustache b/modules/openapi-generator/src/main/resources/scala-cask/apiPackage.mustache index 6acc47b3100..b315826e170 100644 --- a/modules/openapi-generator/src/main/resources/scala-cask/apiPackage.mustache +++ b/modules/openapi-generator/src/main/resources/scala-cask/apiPackage.mustache @@ -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) @@ -142,6 +143,15 @@ extension (request: cask.Request) { def headerManyValues(paramName: String, required: Boolean): Parsed[List[String]] = Parsed.manyValues(request.headers, paramName, required) 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 diff --git a/modules/openapi-generator/src/main/resources/scala-cask/apiRoutes.mustache b/modules/openapi-generator/src/main/resources/scala-cask/apiRoutes.mustache index db3cdd65cb4..293d74466f1 100644 --- a/modules/openapi-generator/src/main/resources/scala-cask/apiRoutes.mustache +++ b/modules/openapi-generator/src/main/resources/scala-cask/apiRoutes.mustache @@ -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}} diff --git a/modules/openapi-generator/src/main/resources/scala-cask/exampleApp.mustache b/modules/openapi-generator/src/main/resources/scala-cask/exampleApp.mustache index f1247210dac..827206a040c 100644 --- a/modules/openapi-generator/src/main/resources/scala-cask/exampleApp.mustache +++ b/modules/openapi-generator/src/main/resources/scala-cask/exampleApp.mustache @@ -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() } diff --git a/modules/openapi-generator/src/main/resources/scala-cask/model.mustache b/modules/openapi-generator/src/main/resources/scala-cask/model.mustache index c4b430b7ced..a6f60a6e9dc 100644 --- a/modules/openapi-generator/src/main/resources/scala-cask/model.mustache +++ b/modules/openapi-generator/src/main/resources/scala-cask/model.mustache @@ -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}} diff --git a/modules/openapi-generator/src/main/resources/scala-cask/modelData.mustache b/modules/openapi-generator/src/main/resources/scala-cask/modelData.mustache index 8dfdef358f6..ec24792b452 100644 --- a/modules/openapi-generator/src/main/resources/scala-cask/modelData.mustache +++ b/modules/openapi-generator/src/main/resources/scala-cask/modelData.mustache @@ -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) diff --git a/modules/openapi-generator/src/main/resources/scala-cask/modelPackage.mustache b/modules/openapi-generator/src/main/resources/scala-cask/modelPackage.mustache index f582c3e9fb1..d4928454a90 100644 --- a/modules/openapi-generator/src/main/resources/scala-cask/modelPackage.mustache +++ b/modules/openapi-generator/src/main/resources/scala-cask/modelPackage.mustache @@ -50,4 +50,21 @@ given ReadWriter[OffsetDateTime] = readwriter[String].bimap[OffsetDateTime]( str => scala.util.Try(OffsetDateTime.parse(str, DateTimeFormatter.ISO_DATE_TIME)).getOrElse( OffsetDateTime.parse(str, DateTimeFormatter.ISO_INSTANT) ) -) \ No newline at end of file +) + + +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 + } +} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/scala-cask/openapiRoute.mustache b/modules/openapi-generator/src/main/resources/scala-cask/openapiRoute.mustache index 58c35063555..0656d1e4600 100644 --- a/modules/openapi-generator/src/main/resources/scala-cask/openapiRoute.mustache +++ b/modules/openapi-generator/src/main/resources/scala-cask/openapiRoute.mustache @@ -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 = { diff --git a/modules/openapi-generator/src/main/resources/scala-cask/parseHttpParams.mustache b/modules/openapi-generator/src/main/resources/scala-cask/parseHttpParams.mustache index 18193e0606c..6da75c69786 100644 --- a/modules/openapi-generator/src/main/resources/scala-cask/parseHttpParams.mustache +++ b/modules/openapi-generator/src/main/resources/scala-cask/parseHttpParams.mustache @@ -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}} diff --git a/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/ExampleApp.scala b/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/ExampleApp.scala index 9c5e733b73e..b49381daf7e 100644 --- a/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/ExampleApp.scala +++ b/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/ExampleApp.scala @@ -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() } diff --git a/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/OpenApiRoutes.scala b/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/OpenApiRoutes.scala index a991ce2aaf4..403a519f2a3 100644 --- a/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/OpenApiRoutes.scala +++ b/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/OpenApiRoutes.scala @@ -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 = { diff --git a/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/PetRoutes.scala b/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/PetRoutes.scala index 75d4799d6e3..e69ef16de01 100644 --- a/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/PetRoutes.scala +++ b/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/PetRoutes.scala @@ -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) diff --git a/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/StoreRoutes.scala b/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/StoreRoutes.scala index ede98a18d8b..b4a550889d7 100644 --- a/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/StoreRoutes.scala +++ b/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/StoreRoutes.scala @@ -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) diff --git a/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/UserRoutes.scala b/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/UserRoutes.scala index 3bd483d6b80..64b82a45d88 100644 --- a/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/UserRoutes.scala +++ b/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/UserRoutes.scala @@ -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) } diff --git a/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/package.scala b/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/package.scala index ab25a84875a..10b838f37aa 100644 --- a/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/package.scala +++ b/samples/server/petstore/scala-cask/jvm/src/main/scala/sample/cask/api/package.scala @@ -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) @@ -154,6 +155,15 @@ extension (request: cask.Request) { def headerManyValues(paramName: String, required: Boolean): Parsed[List[String]] = Parsed.manyValues(request.headers, paramName, required) 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 diff --git a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/ApiResponse.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/ApiResponse.scala index ff5d8abaa14..4fb7f930c58 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/ApiResponse.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/ApiResponse.scala @@ -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") diff --git a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/ApiResponseData.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/ApiResponseData.scala index b93b7eb44ef..9212af8a3df 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/ApiResponseData.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/ApiResponseData.scala @@ -25,69 +25,37 @@ import upickle.default.* */ case class ApiResponseData( code: Int = 0 , + `type`: String = "" , + message: String = "" + - `type`: String = "" , +) derives RW { - message: String = "" + 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 - - - - - - + // ================== code validation ================== + + + - - - - - - + // ================== `type` validation ================== + + + - // ================== - // `type` - - - - - - + // ================== message validation ================== - - - - - - - - - // ================== - // message - - - - - - - - - - - - - errors.toSeq @@ -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) diff --git a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Category.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Category.scala index d0bf01a2861..b143171f9d2 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Category.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Category.scala @@ -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") } diff --git a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/CategoryData.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/CategoryData.scala index 77a834683a8..5c0dcdfc4cc 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/CategoryData.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/CategoryData.scala @@ -25,55 +25,36 @@ import upickle.default.* */ case class CategoryData( id: Long = 0 , + name: String = "" + - name: 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]() - // ================== - // 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]+$""" if name == null || !regex.r.matches(name) then errors += ValidationError(path :+ Category.Fields.name, s"value '$name' doesn't match pattern $regex") } - - - - - - - - - - - - - + + 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) diff --git a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Order.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Order.scala index 85bda97b815..354e8a5e752 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Order.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Order.scala @@ -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") diff --git a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/OrderData.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/OrderData.scala index 0de58a7f0b3..16d81bce47a 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/OrderData.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/OrderData.scala @@ -26,130 +26,59 @@ 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 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]() - // ================== - // id - - - - - - + // ================== id validation ================== + + + - - - - - - + // ================== petId validation ================== + + + - // ================== - // petId - - - - - - + // ================== quantity validation ================== + + + - - - - - - + // ================== shipDate validation ================== + + + - // ================== - // quantity - - - - - - + // ================== status validation ================== + + + - - - - - - + // ================== complete validation ================== - - // ================== - // shipDate - - - - - - - - - - - - - - - // ================== - // status - - - - - - - - - - - - - - - - - // ================== - // complete - - - - - - - - - - - - - - errors.toSeq @@ -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) diff --git a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Pet.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Pet.scala index 068f593303c..44c16221842 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Pet.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Pet.scala @@ -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") diff --git a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/PetData.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/PetData.scala index db6a3e1a512..ea7a34c6193 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/PetData.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/PetData.scala @@ -27,116 +27,57 @@ 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 + - ) { +) 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 ================== + + + - - - - - - + // ================== category validation ================== + - - // ================== - // category - - - - - - - - - - - - - // validating category if (errors.isEmpty || !failFast) { if category != null then errors ++= category.validationErrors(path :+ Pet.Fields.category, failFast) } - // ================== - // name - - - - - - + // ================== name validation ================== + + + - - - - - - + // ================== photoUrls validation ================== + + + - // ================== - // photoUrls - - - - - - + // ================== tags validation ================== - - - - - - - - - // ================== - // tags - - - - - - - - - - - - - if (errors.isEmpty || !failFast) { if (tags != null) { @@ -151,22 +92,10 @@ case class PetData( } - // ================== - // status - - - - - - + // ================== status validation ================== + + - - - - - - - errors.toSeq @@ -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) diff --git a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Tag.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Tag.scala index a8bd2a35866..0406c84c56c 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Tag.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/Tag.scala @@ -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") } diff --git a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/TagData.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/TagData.scala index e8c66334bcb..e6aa8c81642 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/TagData.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/TagData.scala @@ -25,49 +25,30 @@ import upickle.default.* */ case class TagData( id: Long = 0 , + name: String = "" + - name: 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]() - // ================== - // id - - - - - - + // ================== id validation ================== + + + - - - - - - + // ================== name validation ================== + - - // ================== - // name - - - - - - - - - - - - - errors.toSeq @@ -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) diff --git a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/User.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/User.scala index 286cdb3b652..5dfb0761b78 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/User.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/User.scala @@ -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") diff --git a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/UserData.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/UserData.scala index 8b8ca7908ab..42cd2a14f83 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/UserData.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/UserData.scala @@ -25,170 +25,73 @@ 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 + - ) { +) 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 ================== + + + - - - - - - + // ================== username validation ================== + + + - // ================== - // username - - - - - - + // ================== firstName validation ================== + + + - - - - - - + // ================== lastName validation ================== + + + - // ================== - // firstName - - - - - - + // ================== email validation ================== + + + - - - - - - + // ================== password validation ================== + + + - // ================== - // lastName - - - - - - + // ================== phone validation ================== + + + - - - - - - + // ================== userStatus validation ================== - - // ================== - // email - - - - - - - - - - - - - - - // ================== - // password - - - - - - - - - - - - - - - - - // ================== - // phone - - - - - - - - - - - - - - - - - // ================== - // userStatus - - - - - - - - - - - - - - errors.toSeq @@ -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) diff --git a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/package.scala b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/package.scala index b0c893da671..45a650c9fb6 100644 --- a/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/package.scala +++ b/samples/server/petstore/scala-cask/shared/src/main/scala/sample/cask/model/package.scala @@ -62,4 +62,21 @@ given ReadWriter[OffsetDateTime] = readwriter[String].bimap[OffsetDateTime]( str => scala.util.Try(OffsetDateTime.parse(str, DateTimeFormatter.ISO_DATE_TIME)).getOrElse( OffsetDateTime.parse(str, DateTimeFormatter.ISO_INSTANT) ) -) \ No newline at end of file +) + + +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 + } +} \ No newline at end of file