Skip to content

Commit

Permalink
Scala cask api effects (#19936)
Browse files Browse the repository at this point in the history
* Scala-cask improvements:

 * fixe for grouped methods which have routes containing dashes.

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

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

 * Fix for grouped operations which contain duplicate query parameters

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

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

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

 * Introduced a generic effect F[_] for services

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

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

* scala-cask: regenerated samples

* Fix for reserved-word properties in the API

* Fix for null imports and reserved-word enum types

* Fixes for api methods with backticked params

* Fix for duplicate (by name) grouped params

* small syntax fix

* logging response type

* Regenerated samples

* String.format fix
  • Loading branch information
aaronp authored Nov 6, 2024
1 parent cded99c commit b51b18e
Show file tree
Hide file tree
Showing 41 changed files with 1,725 additions and 821 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ import scala.reflect.ClassTag
import scala.util.*
import upickle.default.*


extension (f: java.io.File) {
def bytes: Array[Byte] = java.nio.file.Files.readAllBytes(f.toPath)
def toBase64: String = java.util.Base64.getEncoder.encodeToString(bytes)
}

given Writer[java.io.File] = new Writer[java.io.File] {
def write0[V](out: upickle.core.Visitor[?, V], v: java.io.File) = out.visitString(v.toBase64, -1)
}

// needed for BigDecimal params
given cask.endpoints.QueryParamReader.SimpleParam[BigDecimal](BigDecimal.apply)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import {{modelPackage}}.*

import upickle.default.{ReadWriter => RW, macroRW}
import upickle.default.*
import scala.util.Try

{{#imports}}import {{import}}
{{/imports}}

class {{classname}}Routes(service : {{classname}}Service) extends cask.Routes {
class {{classname}}Routes(service : {{classname}}Service[Try]) extends cask.Routes {
{{#route-groups}}
// route group for {{methodName}}
Expand All @@ -35,7 +36,7 @@ class {{classname}}Routes(service : {{classname}}Service) extends cask.Routes {
* {{description}}
*/
{{vendorExtensions.x-annotation}}("{{vendorExtensions.x-cask-path}}")
def {{operationId}}({{vendorExtensions.x-cask-path-typed}}) = {
def {{operationId}}({{{vendorExtensions.x-cask-path-typed}}}) = {
{{#authMethods}}
// auth method {{name}} : {{type}}, keyParamName: {{keyParamName}}
{{/authMethods}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,123 @@ package {{apiPackage}}

{{#imports}}import _root_.{{import}}
{{/imports}}

import scala.util.Failure
import scala.util.Try
import _root_.{{modelPackage}}.*

/**
* The {{classname}}Service companion object.
*
* Use the {{classname}}Service() companion object to create an instance which returns a 'not implemented' error
* for each operation.
*
*/
object {{classname}}Service {
def apply() : {{classname}}Service = new {{classname}}Service {
/**
* The 'Handler' is an implementation of {{classname}}Service convenient for delegating or overriding individual functions
*/
case class Handler[F[_]](
{{#operations}}
{{#operation}}
override def {{operationId}}({{vendorExtensions.x-param-list-typed}}) : {{vendorExtensions.x-response-type}} = ???
{{operationId}}Handler : ({{{vendorExtensions.x-param-list-typed}}}) => F[{{{vendorExtensions.x-response-type}}}]{{^-last}}, {{/-last}}
{{/operation}}
{{/operations}}
) extends {{classname}}Service[F] {
{{#operations}}
{{#operation}}

override def {{operationId}}({{{vendorExtensions.x-param-list-typed}}}) : F[{{{vendorExtensions.x-response-type}}}] = {
{{operationId}}Handler({{{vendorExtensions.x-param-list}}})
}
{{/operation}}
{{/operations}}
}

def apply() : {{classname}}Service[Try] = {{classname}}Service.Handler[Try](
{{#operations}}
{{#operation}}
({{#allParams}}_{{^-last}}, {{/-last}}{{/allParams}}) => notImplemented("{{operationId}}"){{^-last}}, {{/-last}}
{{/operation}}
{{/operations}}
)

private def notImplemented(name : String) = Failure(new Exception(s"TODO: $name not implemented"))
}

/**
* The {{classname}} business-logic
*
*
* The 'asHandler' will return an implementation which allows for easily overriding individual operations.
*
* equally there are "on<Function>" helper methods for easily overriding individual functions
*
* @tparam F the effect type (Future, Try, IO, ID, etc) of the operations
*/
trait {{classname}}Service {
trait {{classname}}Service[F[_]] {
{{#operations}}
{{#operation}}
/** {{{summary}}}
* {{{description}}}
* @return {{returnType}}
*/
def {{operationId}}({{vendorExtensions.x-param-list-typed}}) : {{vendorExtensions.x-response-type}}
def {{operationId}}({{{vendorExtensions.x-param-list-typed}}}) : F[{{{vendorExtensions.x-response-type}}}]

/**
* override {{operationId}} with the given handler
* @return a new implementation of {{classname}}Service[F] with {{operationId}} overridden using the given handler
*/
final def {{vendorExtensions.x-handlerName}}(handler : ({{{vendorExtensions.x-param-list-typed}}}) => F[{{{vendorExtensions.x-response-type}}}]) : {{classname}}Service[F] = {
asHandler.copy({{operationId}}Handler = handler)
}
{{/operation}}
{{/operations}}

/**
* @return a Handler implementation of this service
*/
final def asHandler : {{classname}}Service.Handler[F] = this match {
case h : {{classname}}Service.Handler[F] => h
case _ =>
{{classname}}Service.Handler[F](
{{#operations}}
{{#operation}}
({{{vendorExtensions.x-param-list}}}) => {{operationId}}({{{vendorExtensions.x-param-list}}}){{^-last}}, {{/-last}}
{{/operation}}
{{/operations}}
)
}

/**
* This function will change the effect type of this service.
*
* It's not unlike a typical map operation from A => B, except we're not mapping
* a type from A to B, but rather from F[A] => G[A] using the 'changeEffect' function.
*
* For, this could turn an asynchronous service (one which returns Future[_] types) into
* a synchronous one (one which returns Try[_] types) by awaiting on the Future.
*
* It could change an IO type (like cats effect or ZIO) into an ID[A] which is just:
* ```
* type ID[A] => A
* ```
*
* @tparam G the new "polymorphic" effect type
* @param changeEffect the "natural transformation" which can change one effect type into another
* @return a new {{classname}}Service service implementation with effect type [G]
*/
final def mapEffect[G[_]](changeEffect : [A] => F[A] => G[A]) : {{classname}}Service[G] = {
val self = this
new {{classname}}Service[G] {
{{#operations}}
{{#operation}}
override def {{operationId}}({{{vendorExtensions.x-param-list-typed}}}) : G[{{{vendorExtensions.x-response-type}}}] = changeEffect {
self.{{operationId}}({{{vendorExtensions.x-param-list}}})
}
{{/operation}}
{{/operations}}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
//> using lib "com.lihaoyi::cask:0.9.2"
//> using lib "com.lihaoyi::scalatags:0.8.2"
{{>licenseInfo}}

// this file was generated from app.mustache
package {{packageName}}

import scala.util.Try
{{#imports}}import {{import}}
{{/imports}}
import _root_.{{modelPackage}}.*
Expand All @@ -20,16 +20,17 @@ import _root_.{{apiPackage}}.*
* If you wanted fine-grained control over the routes and services, you could
* extend the cask.MainRoutes and mix in this trait by using this:
*
* \{\{\{
* ```
* override def allRoutes = appRoutes
* \}\}\}
* ```
*
* More typically, however, you would extend the 'BaseApp' class
*/
trait AppRoutes {
{{#operations}}
def app{{classname}}Service : {{classname}}Service = {{classname}}Service()
def app{{classname}}Service : {{classname}}Service[Try] = {{classname}}Service()
def routeFor{{classname}} : {{classname}}Routes = {{classname}}Routes(app{{classname}}Service)

{{/operations}}

def appRoutes = Seq(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// this file was generated from app.mustache
package {{packageName}}

import scala.util.Try
{{#imports}}import {{import}}
{{/imports}}
import _root_.{{modelPackage}}.*
Expand All @@ -16,7 +17,7 @@ import _root_.{{apiPackage}}.*
* passing in the custom business logic services
*/
class BaseApp({{#operations}}
override val app{{classname}}Service : {{classname}}Service = {{classname}}Service(),
override val app{{classname}}Service : {{classname}}Service[Try] = {{classname}}Service(),
{{/operations}}
override val port : Int = sys.env.get("PORT").map(_.toInt).getOrElse(8080)) extends cask.MainRoutes with AppRoutes {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package {{modelPackage}}
{{#imports}}import {{import}}
{{/imports}}

import scala.util.control.NonFatal

// see https://com-lihaoyi.github.io/upickle/
Expand All @@ -11,52 +12,13 @@ import upickle.default.*

{{#models}}
{{#model}}
case class {{classname}}(
{{#vars}}
{{#description}}
/* {{{description}}} */
{{/description}}
{{name}}: {{#isEnum}}{{^required}}Option[{{/required}}{{classname}}.{{datatypeWithEnum}}{{^required}}]{{/required}}{{/isEnum}}{{^isEnum}}{{{vendorExtensions.x-datatype-model}}}{{/isEnum}}{{^required}} = {{{vendorExtensions.x-defaultValue-model}}} {{/required}}{{^-last}},{{/-last}}
{{/vars}}

{{#isAdditionalPropertiesTrue}}, additionalProperties : ujson.Value = ujson.Null{{/isAdditionalPropertiesTrue}}
) {
def asJsonString: String = asData.asJsonString
def asJson: ujson.Value = asData.asJson
def asData : {{classname}}Data = {
{{classname}}Data(
{{#vars}}
{{name}} = {{name}}{{#vendorExtensions.x-map-asModel}}.map(_.asData){{/vendorExtensions.x-map-asModel}}{{#vendorExtensions.x-wrap-in-optional}}.getOrElse({{{defaultValue}}}){{/vendorExtensions.x-wrap-in-optional}}{{^-last}},{{/-last}}
{{/vars}}
{{#isAdditionalPropertiesTrue}}, additionalProperties{{/isAdditionalPropertiesTrue}}
)
}
}

object {{classname}} {
given RW[{{classname}}] = summon[RW[ujson.Value]].bimap[{{classname}}](_.asJson, json => read[{{classname}}Data](json).asModel)

enum Fields(val fieldName : String) extends Field(fieldName) {
{{#vars}}
case {{name}} extends Fields("{{name}}")
{{/vars}}
}

{{#vars}}
{{#isEnum}}
// baseName={{{baseName}}}
// nameInCamelCase = {{{nameInCamelCase}}}
enum {{datatypeWithEnum}} derives ReadWriter {
{{#_enum}}
case {{.}}
{{/_enum}}
}
{{/isEnum}}
{{/vars}}

}
{{#isEnum}}
{{>modelEnum}}
{{/isEnum}}
{{^isEnum}}
{{>modelClass}}
{{/isEnum}}

{{/model}}
{{/models}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@

case class {{classname}}(
{{#vars}}
{{#description}}
/* {{{description}}} */
{{/description}}
{{name}}: {{#isEnum}}{{^required}}Option[{{/required}}{{classname}}.{{datatypeWithEnum}}{{^required}}]{{/required}}{{/isEnum}}{{^isEnum}}{{{vendorExtensions.x-datatype-model}}}{{/isEnum}}{{^required}} = {{{vendorExtensions.x-defaultValue-model}}} {{/required}}{{^-last}},{{/-last}}
{{/vars}}

{{#isAdditionalPropertiesTrue}}, additionalProperties : ujson.Value = ujson.Null{{/isAdditionalPropertiesTrue}}
) {
def asJsonString: String = asData.asJsonString
def asJson: ujson.Value = asData.asJson
def asData : {{classname}}Data = {
{{classname}}Data(
{{#vars}}
{{name}} = {{{vendorExtensions.x-asData}}}{{^-last}},{{/-last}}
{{/vars}}
{{#isAdditionalPropertiesTrue}}, additionalProperties{{/isAdditionalPropertiesTrue}}
)
}
}

object {{classname}} {
given RW[{{classname}}] = summon[RW[ujson.Value]].bimap[{{classname}}](_.asJson, json => read[{{classname}}Data](json).asModel)

enum Fields(val fieldName : String) extends Field(fieldName) {
{{#vars}}
case {{name}} extends Fields("{{name}}")
{{/vars}}
}

{{#vars}}
{{#isEnum}}
// baseName={{{baseName}}}
// nameInCamelCase = {{{nameInCamelCase}}}
enum {{datatypeWithEnum}} derives ReadWriter {
{{#_enum}}
case {{.}}
{{/_enum}}
}
{{/isEnum}}
{{/vars}}

}
Loading

0 comments on commit b51b18e

Please sign in to comment.