+-- build.sbt +-- project ¦ +-- build.properties ¦ L-- plugins.sbt +-- src ¦ L-- main ¦ +-- resources ¦ ¦ +-- demo.js ¦ ¦ L-- index-fastopt.html ¦ L-- scala L-- version.sbt
resources/index-fastopt.html
- the page will only load our library and the resources/demo.js
file to check the API case class User(name: String, avatarUrl: String, repos: List[Repo]) sealed trait Repo { def name: String def description: String def stargazersCount: Int def homepage: Option[String] } case class Fork(name: String, description: String, stargazersCount: Int, homepage: Option[String]) extends Repo case class Origin(name: String, description: String, stargazersCount: Int, homepage: Option[String], forksCount: Int) extends Repo
User
has several repositories, and the repository can be an original or fork, how can we export it for JS developers? @JSExport case class Fork(name: String, /*...*/)]
Option
constructor, so you can't create a homepage
parameter. There are other restrictions for case classes, you cannot export constructors with inheritance, such code will not even compile @JSExport case class A(a: Int) @JSExport case class B(b: Int) extends A(12) @JSExport object Github { @JSExport def createFork(name: String, description: String, stargazersCount: Int, homepage: UndefOr[String]): Fork = Fork(name, description, stargazersCount, homepage.toOption) }
js.UndefOr
we process an optional parameter in the JS style: you can pass a String
or you can do without it at all: // JS var homelessFork = Github().createFork("bar-fork", "Bar", 1); var fork = Github().createFork("bar-fork", "Bar", 1, "http://foo.bar");
Github()
call every time is not the best idea, if you don’t need laziness, you can cache them at startup: <!--index-fastopt.html--> <script> var Github = Github()
undefined
. Everything is correct, it was not exported, let's export the model properties.String
, Boolean
or Int
, you can export them like this: sealed trait Repo { @JSExport def name: String // ... }
@(JSExport@field)
annotation. Example for forks
property: case class Origin(name: String, description: String, stargazersCount: Int, homepage: Option[String], @(JSExport@field) forks: Int) extends Repo
homepage: Option[String]
. We can export it too, but it is useless to get the value from Option
, js the developer will have to call some method, but for Option
nothing is exported.Option
so that our Scala code remains simple and straightforward. A simple solution is to export a special js getter: import scala.scalajs.js.JSConverters._ sealed trait Repo { //... // , JS def homepage: Option[String] @JSExport("homepage") def homepageJS: js.UndefOr[String] = homepage.orUndefined }
console.log("fork.name: " + fork.name); console.log("fork.homepage: " + fork.homepage);
Option
and made a clean beautiful JS API. Hooray!User.repos
is a List
and has difficulty exporting it. The solution is the same, just export it as a JS array: @JSExport("repos") def reposJS: js.Array[Repo] = repos.toJSArray // JS user.repos.map(function (repo) { return repo.name; });
Repo
trait. Since we do not export constructors, the JS developer will not be able to figure out which subtype of Repo
he is dealing with.isFork: Boolean
or hasForks: Boolean
methods. This is normal, but not generalized enough.type: String
property for all subtypes.type
property: trait Typed { self => @JSExport("type") def typ: String = self.getClass.getSimpleName } </code> , <code>type</code> Scala. <source lang="scala"> sealed trait Repo extends Typed { // ... }
// JS fork.type // "Fork"
class TypeNameConstant[T: ClassTag] { @JSExport("type") def typ: String = classTag[T].runtimeClass.getSimpleName }
GitHub
object: @JSExportAll object Github { //... val Fork = new TypeNameConstant[model.Fork] val Origin = new TypeNameConstant[model.Origin] }
// JS function isFork(repo) { return repo.type == Github.Fork.type }
Option
and for List
, with one difference — you need to implement the JS acceptable wrapper and conversion classes yourself.Scala => JS
) and for creating instances ( JS => Scala
) All business logic should be implemented only by pure Scala classes.Commit
, which we can not change. case class Commit(hash: String)
object CommitJS { def fromCommit(c: Commit): CommitJS = CommitJS(c.hash) } case class CommitJS(@(JSExport@field) hash: String) { def toCommit: Commit = Commit(hash) }
Branch
class from the code we manage will look like this: case class Branch(initial: Commit) { @JSExport("initial") def initialJS: CommitJS = CommitJS.fromCommit(initial) }
CommitJS
objects, the factory method for Branch
will be: @JSExport def createBranch(initial: CommitJS) = Branch(initial.toCommit)
Ajax
library extension for network requests. Let's digress from the export and just implement the API.API
object, it will have two methods: to load the user and load the repository.Future[String \/ DTO]
, where DTO
is the type of data requested, and String
will represent an error. Here is the code itself: object API { case class UserDTO(name: String, avatar_url: String) case class RepoDTO(name: String, description: String, stargazers_count: Int, homepage: Option[String], forks: Int, fork: Boolean) def user(login: String) (implicit ec: ExecutionContext): Future[String \/ UserDTO] = load(login, s"$BASE_URL/users/$login", jsonToUserDTO) def repos(login: String) (implicit ec: ExecutionContext): Future[String \/ List[RepoDTO]] = load(login, s"$BASE_URL/users/$login/repos", arrayToRepos) private def load[T](login: String, url: String, parser: js.Any => Option[T]) (implicit ec: ExecutionContext): Future[String \/ T] = if (login.isEmpty) Future.successful("Error: login can't be empty".left) else Ajax.get(url).map(xhr => if (xhr.status == 200) { parser(js.JSON.parse(xhr.responseText)) .map(_.right) .getOrElse("Request failed: can't deserialize result".left) } else { s"Request failed with response code ${xhr.status}".left } ) private val BASE_URL: String = "https://api.github.com" private def jsonToUserDTO(json: js.Any): Option[UserDTO] = //... private def arrayToRepos(json: js.Any): Option[List[RepoDTO]] = //... }
load
method returns an error string if the code is not 200, otherwise it converts the answer into JSON, and then into DTO import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue object Github { // ... def loadUser(login: String): Future[String \/ User] = { for { userDTO <- EitherT(API.user(login)) repoDTO <- EitherT(API.repos(login)) } yield userFromDTO(userDTO, repoDTO) }.run private def userFromDTO(dto: API.UserDTO, repos: List[API.RepoDTO]): User = //.. }
Future[\/[..]]
, and then convert the DTO into a model.loadUser
method for users of our library. trait Promise[+A] extends js.Object { @JSName("catch") def recover[B >: A]( onRejected: js.Function1[Any, B]): Promise[Any] = js.native @JSName("then") def andThen[B]( onFulfilled: js.Function1[A, B]): Promise[Any] = js.native @JSName("then") def andThen[B]( onFulfilled: js.Function1[A, B], onRejected: js.Function1[Any, B]): Promise[Any] = js.native }
Promise.all
. Now we just need to expand this trait: @JSName("Promise") class Promise[+R]( executor: js.Function2[js.Function1[R, Any], js.Function1[Any, Any], Any] ) extends org.scalajs.dom.raw.Promise[R]
Future
to Promise
. We do this with the implicit class: object promise { implicit class JSFutureOps[R: ClassTag, E: ClassTag](f: Future[\/[E, R]]) { def toPromise(recovery: Throwable => js.Any) (implicit ectx: ExecutionContext): Promise[R] = new Promise[R]((resolve: js.Function1[R, Unit], reject: js.Function1[js.Any, Unit]) => { f.onSuccess({ case \/-(f: R) => resolve(f) case -\/(e: E) => reject(e.asInstanceOf[js.Any]) }) f.onFailure { case e: Throwable => reject(recovery(e)) } }) } }
Future
into the “fallen” Promise
. The left side of the disjunction also “drops” the promise.Github
object next to the original method: def loadUser(login: String): Future[String \/ User] = //... @JSExport("loadUser") def loadUserJS(login: String): Promise[User] = loadUser(login).toPromise(_.getMessage)
// JS Github.loadUser("vpavkin") .then(function (result) { console.log("Name: ", result.name); }, function (error) { console.log("Error occured:", error) }); // Name: Vladimir Pavkin
Option
, List
and other Scala items. Use a getter that converts to js.UndefOr
and js.Array
;js.*
Types and convert them to standard Scala types;type
string field into a sums type;Future
as JS Promise
;Source: https://habr.com/ru/post/272625/
All Articles