📜 ⬆️ ⬇️

How to write a JS library in ScalaJS

Scala.js opens up a huge world of front-end technologies for Scala developers. Usually, projects using Scala.js are web or nodejs applications, but there are times when you just need to create a JavaScript library.

There are some subtleties in writing such a Scala.js library, but they will seem familiar to JS developers. In this article, we will create a simple Scala.js library ( code ) to work with the Github API and focus on the idiomatic JS API.

But first, you probably want to ask why you might even need to do such a library? For example, if you already have a client application written in JavaScript and it communicates with the Scala backend.
')
It is unlikely that you will be able to write it from scratch using Scala.js, but you can write a library for interaction between you and the front-end developers, which will allow you to:

It is also a great choice for developing the Javascript API SDK, thanks to all these benefits.

Recently, I was faced with the fact that our REST JSON API has two different browser clients, so developing an isomorphic library was a good choice.

Let's start the library creation

Requirements: as Scala developers, we want to write in a functional style and use all Scala chips. In turn, the library API should be easy to understand for JS developers.

Let's start with the directory structure , it is no different from the usual structure for a Scala application:
+-- 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

API

The goal is to simplify interaction with the Github API. To begin, we will make only one feature - the download of users and their repositories. So this is a public method and a pair of models with response results. Let's start with the model.

Model

Define our classes like this:
 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 


Nothing complicated, User has several repositories, and the repository can be an original or fork, how can we export it for JS developers?

For a complete description of the functionality, see Export Scala.js APIs to Javascript .

API for creating objects.
Let's see how it works, a simple solution to export a constructor.
 @JSExport case class Fork(name: String, /*...*/)] 

But it won't work, you don't have an exported 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) } 

Here, using 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"); 

A note regarding caching Scala objects:

Making a 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() 


If we now try to get the name of the fork, we get undefined . Everything is correct, it was not exported, let's export the model properties.

There are no problems with native types such as String , Boolean or Int , you can export them like this:
 sealed trait Repo { @JSExport def name: String // ... } 


The case field of a class can be exported using the @(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 


Option

But you guessed it there is a problem with 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.

On the other hand, we would like to save 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 } 


Let's try:
 console.log("fork.name: " + fork.name); console.log("fork.homepage: " + fork.homepage); 


We left our favorite Option and made a clean beautiful JS API. Hooray!

List

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; }); 


Subtypes

There is still one problem with the 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.

there is no pattern matching in Javascript (pattern matching) and the use of inheritance is not so popular (and sometimes controversial), so we have several options:



I choose the 2 way, it is easy to abstract and use it in the whole project, let's declare mixin which exports the 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 { // ... } 


... and use it:
 // JS fork.type // "Fork" 


You can make a little safer by storing constants (here the compiler will help us):
 class TypeNameConstant[T: ClassTag] { @JSExport("type") def typ: String = classTag[T].runtimeClass.getSimpleName } 


With this helper, we can declare the necessary constants in the GitHub object:
 @JSExportAll object Github { //... val Fork = new TypeNameConstant[model.Fork] val Origin = new TypeNameConstant[model.Origin] } 


This will allow us to avoid javascript strings, an example
 // JS function isFork(repo) { return repo.type == Github.Fork.type } 


This is how we work with subtypes.

What if I can't change the object I want to export?

In this case, you may be exporting your cross-compiled model classes or objects from imported libraries. The methods are the same for Option and for List , with one difference — you need to implement the JS acceptable wrapper and conversion classes yourself.

Here it is important to use js replacements for export only ( Scala => JS ) and for creating instances ( JS => Scala ) All business logic should be implemented only by pure Scala classes.

Suppose we have a class Commit , which we can not change.
 case class Commit(hash: String) 


Here's how to export it:
 object CommitJS { def fromCommit(c: Commit): CommitJS = CommitJS(c.hash) } case class CommitJS(@(JSExport@field) hash: String) { def toCommit: Commit = Commit(hash) } 


Then, for example, the Branch class from the code we manage will look like this:
 case class Branch(initial: Commit) { @JSExport("initial") def initialJS: CommitJS = CommitJS.fromCommit(initial) } 


Since in the JS environment, commits are represented as CommitJS objects, the factory method for Branch will be:
 @JSExport def createBranch(initial: CommitJS) = Branch(initial.toCommit) 


Of course, this is not a super method, but it is checked by the compiler. That is why I prefer to look at such a library not only as a proxy for value-classes, but as a facade that hides unnecessary details and simplifies the API.

AJAX

Implementation

For simplicity, we will use the scalajs-dom Ajax library extension for network requests. Let's digress from the export and just implement the API.

In order not to complicate things, we will put everything related to AJAX into the API object, it will have two methods: to load the user and load the repository.

We will also make a DTO layer to separate the API from the model. The result of the method will be 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]] = //... } 


Code deserialization is hidden, it is not interesting to us, the load method returns an error string if the code is not 200, otherwise it converts the answer into JSON, and then into DTO

Now we can convert the API response to the model.
 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 = //.. } 


Here we use monad transformer to work with Future[\/[..]] , and then convert the DTO into a model.

Great, it looks like a functional Scala code, nice to look at. Now let's move on to exporting the loadUser method for users of our library.

Share the Future

Now we have a question, what is the standard way to work with asynchronous calls in Javascript? I can already hear the laughter of the js developers, because it does not exist. Callbacks, event emitters, promises, fibers, generators, async / await, this is all used, what should we choose? I think promises are the closest implementation to Scala Future. Promises are very popular and are already supported out of the box by many modern browsers, we will take them. First you need to tell our code about promises. this is called “Typed Facade”. we can easily do it ourselves, but there is already an implementation in scalajs-dom. Here is an example for those who want to do the implementation themselves:
 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 } 


Well, companion object with methods like 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] 


So, now we just need to convert 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)) } }) } } 


The recovery function turns the “fallen” Future into the “fallen” Promise . The left side of the disjunction also “drops” the promise.

So now let's share our promise with front-end friends, as usual we add it to the 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) 

Here, in case of an error, we drop the promise with an error from the exception. Everything, now we can test API.

 // JS Github.loadUser("vpavkin") .then(function (result) { console.log("Name: ", result.name); }, function (error) { console.log("Error occured:", error) }); // Name: Vladimir Pavkin 


Great, now we can use Future and everything we are used to - and still export it as an idiomatic JS API.

Conclusion Here are some tips on writing a Javascript library using Scala.js

Now you know that all this can be exported.

Sample code can be found on GitHub: https://github.com/vpavkin/scalajs-library-tips


Vladimir Pavkin
Scala – Developer

Source: https://habr.com/ru/post/272625/


All Articles