Pathikrit Bhowmick
Scala for 10+ years
// build.sbt def module(name: String) = Project(id = name, base = file(name)) .settings(scalaVersion := "2.13.7") lazy val framework = module("framework") lazy val shared = module("shared") .dependsOn(framework) lazy val web = module("web") .dependsOn(shared) lazy val server = module("server") .dependsOn(shared) lazy val root = project.in(file(".")) .aggregate(server, framework, shared, web) def cmd(name: String, commands: String*) = Command.command(name){ s => s.copy(remainingCommands = commands.toList.map(cmd => Exec(cmd, None)) ++ s.remainingCommands) } commands ++= List( cmd("dev", "scalafmtAll", "~;web/fastOptJS;server/reStart;", "server/reStop") )
// build.sbt def module(name: String) = Project(id = name, base = file(name)) .settings(scalaVersion := "2.13.7") lazy val framework = module("framework") lazy val shared = module("shared") .dependsOn(framework) lazy val web = module("web") .dependsOn(shared) lazy val server = module("server") .dependsOn(shared) lazy val root = project.in(file(".")) .aggregate(server, framework, shared, web) def cmd(name: String, commands: String*) = Command.command(name){ s => s.copy(remainingCommands = commands.toList.map(cmd => Exec(cmd, None)) ++ s.remainingCommands) } commands ++= List( cmd("dev", "scalafmtAll", "~;web/fastOptJS;server/reStart;", "server/reStop") )
// project/Dependencies.scala object Dependencies { val cask = "com.lihaoyi" %% "cask" % "0.8.0" val requests = "com.lihaoyi" %% "requests" % "0.7.1" val scalaJs = Def.setting(Seq( // Tests "org.scalameta" %%% "munit" % "0.7.29" % Test, // Scala JS "org.scala-js" %%% "scalajs-dom" % "1.1.0", "io.github.cquiroz" %%% "scala-java-time" % "2.2.2", "com.lihaoyi" %%% "scalatags" % "0.9.4", // Scala CSS "com.github.japgolly.scalacss" %%% "core" % "0.8.0-RC1", "com.github.japgolly.scalacss" %%% "ext-scalatags" % "0.8.0-RC1", // JS Wrappers "io.udash" %%% "udash-jquery" % "3.0.4", "org.openmole.scaladget" %%% "bootstrapslider" % "1.3.7", // API Layer "com.lihaoyi" %%% "upickle" % "1.3.8", "com.softwaremill.sttp.model" %%% "core" % "1.3.4", // Shared Layer "org.typelevel" %%% "squants" % "1.6.0" )) }
//build.sbt def module(name: String) = Project(id = name, base = file(name)) .settings(scalaVersion := "2.13.7") lazy val framework = module("framework") .enablePlugins(ScalaJSPlugin) .settings(libraryDependencies ++= Seq(Dependencies.cask) ++ Dependencies.scalaJs.value) lazy val shared = module("shared") .enablePlugins(ScalaJSPlugin) .dependsOn(framework) lazy val web = module("web") .enablePlugins(ScalaJSPlugin) .dependsOn(shared) lazy val server = module("server") .dependsOn(shared) .settings(libraryDependencies ++= Seq( Dependencies.requests, Dependencies.logback, Dependencies.scalaLogging, )) lazy val root = project.in(file(".")).aggregate(server, framework, shared, web)
import org.scalajs.dom.raw._ object Webpage { def html = doctype("html")( html( head( meta(charset := "utf-8"), meta(attr("name") := "viewport", content := "width=device-width, initial-scale=1, shrink-to-fit=no"), ), body( div(`class` := "container card w-25 mt-5 p-3")( h3("Mortgage Calculator"), form(id := "calculator")( label("APR", `for` := "apr", `class` := "col-form-label"), input(value := "apr", id := "apr", `type` := "number", `class` := "form-control"), button("Calculate", id := "calc_payments", `type` := "button", `class` := "btn btn-primary m-2") ), ), ) ) ) }
// Webpage.html.render <!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/> </head> <body> <div class="container card w-25 mt-5 p-3"> <h3>Mortgage Calculator</h3> <form id="calculator"> <label for="apr" class="col-form-label">APR</label> <input value="apr" id="apr" type="number" class="form-control"/> <button id="calc_payments" type="button" class="btn btn-primary m-2">Calculate</button> </form> </div> </body> </html>
// framework/Page.scala abstract class Page(name: String) { def renderDoc: doctype = doctype("html")(html(renderHead, renderBody, scripts)) def styles: List[StyleSheet.Standalone] = Nil def cssLibs: List[Tag] = List(JsLibs.bootstrap.css) def jsLibs: List[Tag] = List( JsLibs.jquery, JsLibs.bootstrap.js, ) def scripts: List[Modifier] = jsLibs ++ List( script(src := "/js/main.js"), script(s"$name.init()"), ) def renderHead: Tag = head( meta(charset := "utf-8"), meta(attr("name") := "viewport", content := kv("width" -> "device-width", "initial-scale" -> "1")), cssLibs, ) def renderBody: Tag @JSExport def init(): Any = {} }
import scalatags.Text.all._ import org.scalajs.dom.raw._ import scalajs.js.annotation.JSExportTopLevel import io.udash.wrappers.jquery.{jQ => $, _} @JSExportTopLevel("hello_world") object HelloWorld extends framework.Page("hello_world") { override def renderBody = body( div(`class` := "container")( button("Push Me", id := "btn", `type` := "button"), div(`id` := "output") ) ) override def init() = { $("#btn").on("click", (elmt, evt) => { $("#output").text(Math.random()) }) } }
// framework/JsLibs.scala import scalatags.Text.all._ object JsLibs { def css(lib: String, version: String): Tag = link(href := s"https://cdn.jsdelivr.net/npm/$lib@$version/dist/css/$lib.min.css", rel := "stylesheet") def js(lib: String, version: String): Tag = js(lib = lib, version = version, file = lib) def js(lib: String, version: String, file: String): Tag = script(src := s"https://cdn.jsdelivr.net/npm/$lib@$version/dist/$file.min.js") // libraries val jquery = js("jquery", version = "3.2.1") object bootstrap { val version = "5.1.3" val css = JsLibs.css("bootstrap", version = version) val js = JsLibs.js("bootstrap", version = version, file = "js/bootstrap.bundle") } }
// framework/JsLibs.scala import scalatags.Text.all._ object JsLibs { def css(lib: String, version: String): Tag = link(href := s"https://cdn.jsdelivr.net/npm/$lib@$version/dist/css/$lib.min.css", rel := "stylesheet") def js(lib: String, version: String): Tag = js(lib = lib, version = version, file = lib) def js(lib: String, version: String, file: String): Tag = script(src := s"https://cdn.jsdelivr.net/npm/$lib@$version/dist/$file.min.js") // libraries val jquery = js("jquery", version = "3.2.1") object bootstrap { val version = "5.1.3" val css = JsLibs.css("bootstrap", version = version) val js = JsLibs.js("bootstrap", version = version, file = "js/bootstrap.bundle") } }
// framework/JsLibs.scala import scalatags.Text.all._ object JsLibs { def css(lib: String, version: String): Tag = link(href := s"https://cdn.jsdelivr.net/npm/$lib@$version/dist/css/$lib.min.css", rel := "stylesheet") def js(lib: String, version: String): Tag = js(lib = lib, version = version, file = lib) def js(lib: String, version: String, file: String): Tag = script(src := s"https://cdn.jsdelivr.net/npm/$lib@$version/dist/$file.min.js") // libraries val jquery = js("jquery", version = "3.2.1") object bootstrap { val version = "5.1.3" val css = JsLibs.css("bootstrap", version = version) val js = JsLibs.js("bootstrap", version = version, file = "js/bootstrap.bundle") } }
import scalajs.js.annotation.JSExportTopLevel import org.scalajs.dom.raw._ import scalatags.Text.{all => t} import scalatags.Text.all._ @JSExportTopLevel("mortgage_calculator") object MortgageCalculator extends framework.Page("mortgage_calculator") { override def renderBody = body( div(`class` := "container card w-25 mt-5 p-3")( h3("Mortgage Calculator"), form(t.id := "calculator")( input(label = "Loan Amount ($)", id = "loan", default = 1e6.toInt), input(label = "APR (%)", id = "apr", default = 5), input(label = "Mortgage Period (years)", id = "years", default = 30), input(label = "New APR", id = "new_apr", default = 3), button("Calculate", id := "calc_payments", `type` := "button", `class` := "btn btn-primary m-2"), button("Refinance?", id := "refinance", `type` := "button", `class` := "btn btn-secondary m-2"), ), ), div(id := "output", `class` := "container"), ) def input(label: String, id: String, default: Int): Tag = div(`class` := "mb-3")( t.label(label, `for` := id, `class` := "col-form-label"), t.input(value := default, t.id := id, `type` := "number", `class` := "form-control"), ) }
override def init() = { $("#calc_payments").on("click", calc) } def calc(element: Element, event: JQueryEvent) = { val format = new DecimalFormat("$ #.00"); import api.Mortgage $("#output").html( table(`class` := "table table-striped font-monospace")( tr(th("#"), th("Balance"), th("Payment"), th("Principal"), th("Interest")), ).render, ) for { amount <- $("#loan").value().asInstanceOf[String].toIntOption apr <- $("#apr").value().asInstanceOf[String].toFloatOption years <- $("#years").value().asInstanceOf[String].toIntOption mortgage = Mortgage(amount = amount, apr = apr, years = years) payments <- Mortgage.API.payments(mortgage) (payment, row) <- payments.zipWithIndex } $("#output tr:last").after( tr( td(row + 1), td(format.format(payment.balance)), td(format.format(payment.payment)), td(format.format(payment.principal)), td(format.format(payment.interest)), ).render, ) }
override def init() = { $("#calc_payments").on("click", calc) } def calc(element: Element, event: JQueryEvent) = { val format = new DecimalFormat("$ #.00"); import api.Mortgage $("#output").html( table(`class` := "table table-striped font-monospace")( tr(th("#"), th("Balance"), th("Payment"), th("Principal"), th("Interest")), ).render, ) for { amount <- $("#loan").value().asInstanceOf[String].toIntOption apr <- $("#apr").value().asInstanceOf[String].toFloatOption years <- $("#years").value().asInstanceOf[String].toIntOption mortgage = Mortgage(amount = amount, apr = apr, years = years) payments <- Mortgage.API.payments(mortgage) (payment, row) <- payments.zipWithIndex } $("#output tr:last").after( tr( td(row + 1), td(format.format(payment.balance)), td(format.format(payment.payment)), td(format.format(payment.principal)), td(format.format(payment.interest)), ).render, ) }
// shared/src/main/scala/api/Mortgage.scala package api case class Mortgage(principal: Double, apr: Double, years: Int) case class Payment(principal: Double, interest: Double, balance: Double) { val payment = principal + interest } object Mortgage { trait API { def payments(m: Mortgage): Seq[Payment] def refinance(mortgage: Mortgage, newRate: Double): Double } }
// shared/src/main/scala/api/Mortgage.scala package api case class Mortgage(principal: Double, apr: Double, years: Int) case class Payment(principal: Double, interest: Double, balance: Double) { val payment = principal + interest } object Mortgage { import framework.RPC object API { val payments = new RPC[Mortgage, Seq[Payment]]("/api/mortgage/payments") val refinance = new RPC[(Mortgage, Double), Double]("/api/mortgage/refinance") } }
//framework/src/main/scala/framework/RPC.scala import upickle.default._ import scala.concurrent.{ExecutionContext, Future} class RPC[I: ReadWriter, O: ReadWriter](path: String) { def apply(input: I): Future[O] = ??? }
//framework/src/main/scala/framework/RPC.scala import org.scalajs.dom.ext.Ajax import upickle.default._ import scala.concurrent.{ExecutionContext, Future} class RPC[I: ReadWriter, O: ReadWriter](path: String) { def apply(input: I)(implicit ec: ExecutionContext): Future[O] = Ajax.post( url = path, data = write(input), headers = Map("Content-Type" -> "application/json"), ).map(res => read[O](res.responseText)) }
//framework/src/main/scala/framework/RPC.scala import org.scalajs.dom.HttpMethod import org.scalajs.dom.ext.Ajax import upickle.default._ import scala.concurrent.{ExecutionContext, Future} class RPC[I: ReadWriter, O: ReadWriter](method: HttpMethod, path: String) { def apply(input: I)(implicit ec: ExecutionContext): Future[O] = { method match { case HttpMethod.GET => Ajax.get( url = path, data = ??? // I -> url params ).map(res => read[O](res.responseText)) case HttpMethod.POST => Ajax.post( url = path, data = write(input), headers = Map("Content-Type" -> "application/json"), ) } } }
//framework/src/main/scala/framework/RPC.scala import org.scalajs.dom.HttpMethod import org.scalajs.dom.ext.Ajax import upickle.default._ import scala.concurrent.{ExecutionContext, Future} class RPC[I: ReadWriter, O: ReadWriter](method: HttpMethod, path: String) { def apply(input: I)(implicit ec: ExecutionContext): Future[O] = { method match { case HttpMethod.GET => Ajax.get( url = path, data = ??? // I -> url params ).map(res => read[O](res.responseText)) case HttpMethod.POST => Ajax.post( url = path, data = write(input), headers = Map("Content-Type" -> "application/json"), ) } } }
//shared/src/main/scala/api/Mortgage.scala package api import framework.RPC import upickle.default._ case class Mortgage(amount: Double, apr: Double, years: Int) object Mortgage { implicit val rw: ReadWriter[Mortgage] = macroRW object API { val payments = new RPC[Mortgage, Seq[Payment]]("/api/mortgage/payments") val refinance = new RPC[(Mortgage, Double), Double]("/api/mortgage/refinance") } } case class Payment(principal: Double, interest: Double, balance: Double) { val payment = principal + interest } object Payment { implicit val rw: ReadWriter[Payment] = macroRW }
//shared/src/main/scala/api/Mortgage.scala package api import framework.RPC import upickle.default._ case class Mortgage(amount: Double, apr: Double, years: Int) object Mortgage { implicit val rw: ReadWriter[Mortgage] = macroRW object API { val payments = new RPC[Mortgage, Seq[Payment]]("/api/mortgage/payments") val refinance = new RPC[(Mortgage, Double), Double]("/api/mortgage/refinance") } } case class Payment(principal: Double, interest: Double, balance: Double) { val payment = principal + interest } object Payment { implicit val rw: ReadWriter[Payment] = macroRW }
//server/src/main/scala/services/MortgageApiImpl.scala package services import api.{Mortgage, Payment} object MortgageApiImpl { def payments(mortgage: Mortgage): Seq[Payment] = { val r = mortgage.apr / 100 / 12 val n = 12 * mortgage.years val c = (x: Int) => 1 - Math.pow(1 + r, -x.toDouble) val monthlyPayment = mortgage.amount * r / c(n) val balance = (i: Int) => mortgage.amount * c(n - i) / c(n) val principal = (i: Int) => balance(i - 1) - balance(i) val interest = (i: Int) => monthlyPayment - principal(i) (1 to n).map(i => Payment( principal = principal(i), interest = interest(i), balance = balance(i)) ) } def refinancePenalty(mortgage: Mortgage, newApr: Double): Double = { def totalInterest(m: Mortgage) = payments(m).map(_.interest).sum totalInterest(mortgage) - totalInterest(mortgage.copy(apr = newApr)) } }
//server/src/main/scala/server/Router.scala package server import framework.RPC import services.MortgageApiImpl object Router extends cask.MainRoutes { val isProd = sys.env.get("IS_PROD").flatMap(_.toBooleanOption).exists(identity) @cask.get("/") def index() = views.MortgageCalculator.renderDoc @cask.staticFiles("/js/") def scalaJs() = s"web/target/scala-2.13/${if (isProd) "web-opt" else "web-fastopt"}/" @cask.post("/api", subpath = true) def routeApi(req: cask.Request) = RPC.wire( api.Mortgage.API.payments ~~> MortgageApiImpl.payments, api.Mortgage.API.refinance ~~> Function.tupled(MortgageApiImpl.refinance), )(req) initialize() }
//server/src/main/scala/server/Router.scala package server import framework.RPC import services.MortgageApiImpl object Router extends cask.MainRoutes { val isProd = sys.env.get("IS_PROD").flatMap(_.toBooleanOption).exists(identity) @cask.get("/") def index() = views.MortgageCalculator.renderDoc @cask.staticFiles("/js/") def scalaJs() = s"web/target/scala-2.13/${if (isProd) "web-opt" else "web-fastopt"}/" @cask.post("/api", subpath = true) def routeApi(req: cask.Request) = RPC.wire( api.Mortgage.API.payments ~~> MortgageApiImpl.payments, api.Mortgage.API.refinance ~~> Function.tupled(MortgageApiImpl.refinance), )(req) initialize() }
class RPC[I: ReadWriter, O: ReadWriter](path: String) { def apply(input: I)(implicit ec: ExecutionContext): Future[O] = Ajax.post( url = path, data = write(input), headers = Map("Content-Type" -> "application/json"), ).map(res => read[O](res.responseText)) def ~~>(f: Function[I, O]): RPC.RequestHandler = { req => req.remainingPathSegments.mkString("/") match { case `path` => Try(read[I](req.data)) match { case Failure(exception) => RPC.error(StatusCode.BadRequest, errors = List(exception.getMessage)) case Success(input) => Try(f(input)) match { case Failure(err) => RPC.error(StatusCode.InternalServerError, errors = List(err.getMessage)) case Success(result) => cask.Response(data = writeJs(result)) } } } } } object RPC { import cask.{Request, Response} type RequestHandler = PartialFunction[Request, Response[ujson.Value]] def error(statusCode: StatusCode, errors: List[String] = Nil) = cask.Response(data = ujson.Obj("errors" -> errors), statusCode = statusCode.code) def wire(handlers: RequestHandler*): Request => Response[ujson.Value] = handlers.reduce(_ orElse _).orElse(_ => error(StatusCode.NotFound)) }
class RPC[I: ReadWriter, O: ReadWriter](path: String) { def apply(input: I)(implicit ec: ExecutionContext): Future[O] = Ajax.post( url = path, data = write(input), headers = Map("Content-Type" -> "application/json"), ).map(res => read[O](res.responseText)) def ~~>(f: Function[I, O]): RPC.RequestHandler = { req => req.remainingPathSegments.mkString("/") match { case `path` => Try(read[I](req.data)) match { case Failure(exception) => RPC.error(StatusCode.BadRequest, errors = List(exception.getMessage)) case Success(input) => Try(f(input)) match { case Failure(err) => RPC.error(StatusCode.InternalServerError, errors = List(err.getMessage)) case Success(result) => cask.Response(data = writeJs(result)) } } } } } object RPC { import cask.{Request, Response} type RequestHandler = PartialFunction[Request, Response[ujson.Value]] def error(statusCode: StatusCode, errors: List[String] = Nil) = cask.Response(data = ujson.Obj("errors" -> errors), statusCode = statusCode.code) def wire(handlers: RequestHandler*): Request => Response[ujson.Value] = handlers.reduce(_ orElse _).orElse(_ => error(StatusCode.NotFound)) }
class RPC[I: ReadWriter, O: ReadWriter](path: String) { def apply(input: I)(implicit ec: ExecutionContext): Future[O] = Ajax.post( url = path, data = write(input), headers = Map("Content-Type" -> "application/json"), ).map(res => read[O](res.responseText)) def ~~>(f: Function[I, O]): RPC.RequestHandler = { req => req.remainingPathSegments.mkString("/") match { case `path` => Try(read[I](req.data)) match { case Failure(exception) => RPC.error(StatusCode.BadRequest, errors = List(exception.getMessage)) case Success(input) => Try(f(input)) match { case Failure(err) => RPC.error(StatusCode.InternalServerError, errors = List(err.getMessage)) case Success(result) => cask.Response(data = writeJs(result)) } } } } } object RPC { import cask.{Request, Response} type RequestHandler = PartialFunction[Request, Response[ujson.Value]] def error(statusCode: StatusCode, errors: List[String] = Nil) = cask.Response(data = ujson.Obj("errors" -> errors), statusCode = statusCode.code) def wire(handlers: RequestHandler*): Request => Response[ujson.Value] = handlers.reduce(_ orElse _).orElse(_ => error(StatusCode.NotFound)) }
class RPC[I: ReadWriter, O: ReadWriter](path: String) { def apply(input: I)(implicit ec: ExecutionContext): Future[O] = Ajax.post( url = path, data = write(input), headers = Map("Content-Type" -> "application/json"), ).map(res => read[O](res.responseText)) def ~~>(f: Function[I, O]): RPC.RequestHandler = { req => req.remainingPathSegments.mkString("/") match { case `path` => Try(read[I](req.data)) match { case Failure(exception) => RPC.error(StatusCode.BadRequest, errors = List(exception.getMessage)) case Success(input) => Try(f(input)) match { case Failure(err) => RPC.error(StatusCode.InternalServerError, errors = List(err.getMessage)) case Success(result) => cask.Response(data = writeJs(result)) } } } } } object RPC { import cask.{Request, Response} type RequestHandler = PartialFunction[Request, Response[ujson.Value]] def error(statusCode: StatusCode, errors: List[String] = Nil) = cask.Response(data = ujson.Obj("errors" -> errors), statusCode = statusCode.code) def wire(handlers: RequestHandler*): Request => Response[ujson.Value] = handlers.reduce(_ orElse _).orElse(_ => error(StatusCode.NotFound)) }
class RPC[I: ReadWriter, O: ReadWriter](path: String) { def apply(input: I)(implicit ec: ExecutionContext): Future[O] = Ajax.post( url = path, data = write(input), headers = Map("Content-Type" -> "application/json"), ).map(res => read[O](res.responseText)) def ~~>(f: Function[I, O]): RPC.RequestHandler = { req => req.remainingPathSegments.mkString("/") match { case `path` => Try(read[I](req.data)) match { case Failure(exception) => RPC.error(StatusCode.BadRequest, errors = List(exception.getMessage)) case Success(input) => Try(f(input)) match { case Failure(err) => RPC.error(StatusCode.InternalServerError, errors = List(err.getMessage)) case Success(result) => cask.Response(data = writeJs(result)) } } } } } object RPC { import cask.{Request, Response} type RequestHandler = PartialFunction[Request, Response[ujson.Value]] def error(statusCode: StatusCode, errors: List[String] = Nil) = cask.Response(data = ujson.Obj("errors" -> errors), statusCode = statusCode.code) def wire(handlers: RequestHandler*): Request => Response[ujson.Value] = handlers.reduce(_ orElse _).orElse(_ => error(StatusCode.NotFound)) }
case class Validation[A](rules: Map[String, A => Boolean]) { def rule(msg: String)(check: A => Boolean): Validation[A] = copy(rules = rules + (msg -> check)) def apply(a: A): Validation.Result[A] = rules.collect({ case (msg, f) if !f(a) => msg }).toList match { case Nil => Right(a) case violations => Left(violations) } } object Validation { type Result[A] = Either[List[String], A] def empty[A]: Validation[A] = Validation(Map.empty[String, A => Boolean]) def apply[A]: Validation[A] = empty def apply[A](a: A)(implicit f: Validation[A]): Result[A] = f(a) implicit class Dsl[A: Validation](a: A) { def validate: Result[A] = Validation(a) def isValid: Boolean = validate.isRight } }
case class Address(street: String, city: String, zip: String, country: String) object Address { implicit val validator: Validation[Address] = Validation[Address] .rule("street must be valid")(_.street.length > 5) .rule("zip must be of length 5")(_.zip.length == 5) .rule("country must be valid")(_.country.length == 2) } case class Person(name: String, address: Address) object Person { implicit val validator: Validation[Person] = Validation[Person] .rule("name must exist")(_.name.nonEmpty) .rule("name must start with capital letter")(_.name(0).isUpper) .rule("address is valid")(_.address.isValid) }
class RPC[I: ReadWriter, O: ReadWriter](path: String) { def inputValidator: Validation[I] = Validation.empty def ~~>(f: Function[I, O]): RPC.RequestHandler = { req => req.remainingPathSegments.mkString("/") match { case `path` => Try(read[I](req.data)) match { case Failure(exception) => RPC.error(StatusCode.BadRequest, errors = List(exception.getMessage)) case Success(input) => inputValidator(input) match { case Left(violations) => RPC.error(StatusCode.BadRequest, errors = violations) case _ => Try(f(input)) match { case Failure(exception) => RPC.error(StatusCode.InternalServerError, errors = List(exception.getMessage)) case Success(result) => cask.Response(data = writeJs(result)) } } } } } }
class RPC[I: ReadWriter, O: ReadWriter](path: String) { def inputValidator: Validation[I] = Validation.empty def ~~>(f: Function[I, O]): RPC.RequestHandler = { req => req.remainingPathSegments.mkString("/") match { case `path` => Try(read[I](req.data)) match { case Failure(exception) => RPC.error(StatusCode.BadRequest, errors = List(exception.getMessage)) case Success(input) => inputValidator(input) match { case Left(violations) => RPC.error(StatusCode.BadRequest, errors = violations) case _ => Try(f(input)) match { case Failure(exception) => RPC.error(StatusCode.InternalServerError, errors = List(exception.getMessage)) case Success(result) => cask.Response(data = writeJs(result)) } } } } } }
val payments = new RPC[Mortgage, Seq[Payment]]("/mortgage/payments") { override def inputValidator = Validation[Mortgage] }
import scalatags.Text.TypedTag import scalacss.DevDefaults._ import scalacss.ScalatagsCss._ abstract class Page(name: String) { final lazy val renderDoc: doctype = doctype("html")( html(renderHead, renderBody, scripts) ) def styles: List[StyleSheet.Standalone] = { import scala.language.postfixOps List( new StyleSheet.Standalone { import dsl._ List(".slider", ".slider-horizontal").mkString - ( width(100 %%), ) }, ) } def cssLibs: List[Modifier] = List( JsLibs.bootstrap.css, styles.head.render[TypedTag[String]] ) }
import scalatags.Text.TypedTag import scalacss.DevDefaults._ import scalacss.ScalatagsCss._ abstract class Page(name: String) { final lazy val renderDoc: doctype = doctype("html")( html(renderHead, renderBody, scripts) ) def styles: List[StyleSheet.Standalone] = { import scala.language.postfixOps List( new StyleSheet.Standalone { import dsl._ List(".slider", ".slider-horizontal").mkString - ( width(100 %%), ) }, ) } def cssLibs: List[Modifier] = List( JsLibs.bootstrap.css, styles.head.render[TypedTag[String]] ) }
<!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"/> <style type="text/css"> .slider .slider-horizontal { width: 100%; } </style> </head>
override def init() = { $("#calc_payments").on("click", calc) } def calc(element: Element, event: JQueryEvent) = { val format = new DecimalFormat("$ #.00"); import api.Mortgage $("#output").html( table(`class` := "table table-striped font-monospace")( tr(th("#"), th("Balance"), th("Payment"), th("Principal"), th("Interest")), ).render, ) for { amount <- $("#loan").value().asInstanceOf[String].toIntOption apr <- $("#apr").value().asInstanceOf[String].toFloatOption years <- $("#years").value().asInstanceOf[String].toIntOption mortgage = Mortgage(amount = amount, apr = apr, years = years) payments <- Mortgage.API.payments(mortgage) (payment, row) <- payments.zipWithIndex } $("#output tr:last").after( tr( td(row + 1), td(format.format(payment.balance)), td(format.format(payment.payment)), td(format.format(payment.principal)), td(format.format(payment.interest)), ).render, ) }
override def init() = { $("#calc_payments").on("click", calc) } def calc(element: Element, event: JQueryEvent) = { val format = new DecimalFormat("$ #.00"); import api.Mortgage $("#output").html( table(`class` := "table table-striped font-monospace")( tr(th("#"), th("Balance"), th("Payment"), th("Principal"), th("Interest")), ).render, ) for { amount <- $("#loan").value().asInstanceOf[String].toIntOption apr <- $("#apr").value().asInstanceOf[String].toFloatOption years <- $("#years").value().asInstanceOf[String].toIntOption mortgage = Mortgage(amount = amount, apr = apr, years = years) payments <- Mortgage.API.payments(mortgage) (payment, row) <- payments.zipWithIndex } $("#output tr:last").after( tr( td(row + 1), td(format.format(payment.balance)), td(format.format(payment.payment)), td(format.format(payment.principal)), td(format.format(payment.interest)), ).render, ) }
override def init() = { $("#calc_payments").on("click", calc) } def calc(element: Element, event: JQueryEvent) = { val format = new DecimalFormat("$ #.00"); import api.Mortgage $("#output").html( table(`class` := "table table-striped font-monospace")( tr(th("#"), th("Balance"), th("Payment"), th("Principal"), th("Interest")), ).render, ) for { amount <- $("#loan").value().as[Int] apr <- $("#apr").value().as[Double] years <- $("#years").value().as[Int] mortgage = Mortgage(amount = amount, apr = apr, years = years) payments <- Mortgage.API.payments(mortgage) (payment, row) <- payments.zipWithIndex } $("#output tr:last").after( tr( td(row + 1), td(format.format(payment.balance)), td(format.format(payment.payment)), td(format.format(payment.principal)), td(format.format(payment.interest)), ).render, ) }
override def init() = { $("#calc_payments").on("click", calc) } def calc(element: Element, event: JQueryEvent) = { val format = new DecimalFormat("$ #.00"); import api.Mortgage $("#output").html( table(`class` := "table table-striped font-monospace")( tr(th("#"), th("Balance"), th("Payment"), th("Principal"), th("Interest")), ).render, ) for { amount <- $("#loan").value().as[Int] apr <- $("#apr").value().as[Double] years <- $("#years").value().as[Int] mortgage = Mortgage(amount = amount, apr = apr, years = years) payments <- Mortgage.API.payments(mortgage) (payment, row) <- payments.zipWithIndex } $("#output tr:last").after( tr( td(row + 1), td(format.format(payment.balance)), td(format.format(payment.payment)), td(format.format(payment.principal)), td(format.format(payment.interest)), ).render, ) }
// framework/src/main/scala/framework/JsRead.scala package framework import scala.scalajs.js.| import scala.util.Try trait JsRead[A] { self => def apply(value: JsRead.Or): Option[A] def map[B](f: A => Option[B]): JsRead[B] = self(_).flatMap(f) def tryMap[B](f: A => B): JsRead[B] = map(a => Try(f(a)).toOption) } object JsRead { type Or = _ | _ def apply[A](f: Or => Option[A]): JsRead[A] = f(_) implicit class Dsl(value: Or) { def as[A](implicit reader: JsRead[A]): Option[A] = reader(value) } implicit val string: JsRead[String] = JsRead(x => Try(x.asInstanceOf[String]).toOption) implicit val int: JsRead[Int] = string.map(_.toIntOption) implicit val double: JsRead[Double] = string.map(_.toDoubleOption) }