-
Notifications
You must be signed in to change notification settings - Fork 0
Builtin Serialization
Firefly comes with serialization bultin in the language, this means that serialization occurs naturally.
The data type describes the structure of the data which you want to share, store or manipulate.
data User {
name: String,
email: String,
password: String,
birthDate: LocalDate
}
Once defined, this data could be serialized and deserialized naturally:
fun registerUser(data user: User): Try<Status> =
req.post("http://data-service:80/register", user)
And on the other side:
@Post("/register")
fun registerUserInDatabase(data user: User): Try<Status> =
databaseService.save(user)
However, this introduces the need of having User data structure defined in both sides, as an overcome to aliviate this problem, Firefly allows you to just pass or receive what you need, so you can have a common lib only with data types.
Let's say we only want to receive User name and email:
fun getUser(name: String): Try<data User[name, email]> =
req.get("http://data-service:80/query").param("name", name).shaped(param = "userShape")
And in the service:
@Get("/query")
fun queryUserByName(name: String, userShape: DataShape): Try<data User[_]> =
databaseService.getByName(name).withShape(userShape)
In this case, we assume that the DataService is able of querying only the requested fields, but in cases that the class does not support that, Firefly will implicitly remove unwanted fields before returning the object.
As you could select the fields you want, Firefly won't let you pass User[name, email] as User, because the first one does not "fits" the shape of the second one, so the following code won't compile:
fun getUser(name: String): Try<data User[name, email]> =
req.get("http://data-service:80/query").param("name", name).shaped(param = "userShape")
fun updateUser(data user: User) =
req.put("http://data-service:80/update")
fun changeUsername(oldName: String, newName: String) {
val user = this.getUser(name)
val newUser = user.update(name = oldName)
// Fails to compile: User { name, email } does not have enough fields to fit as
// User { name, email, password, birthDate }
this.updateUser(newUser);
}
A workaround to this is to use a different approach to update user, using the Shape information, and selectively update user fields.
All data types are polymorphic, this means that they assigns to any data which has the same shape, or is similar to, this means that:
data User {
name: String,
email: String,
password: String,
birthDate: LocalDate
}
Could be used as
data RootUser {
name: String,
email: String,
password: String,
birthDate: LocalDate
}
See the example below:
fun registerRegularUserAsRoot(regularUserName: String) {
val user: User[name, email] = this.getUser(regularUserName)
this.registerRootUser(user)
}
fun registerRootUser(rootUser: RootUser[name, email]) {
// ...
}
Also, a data with more fields could fit a data with less fields:
data Person {
name: String,
birthDate: LocalDate
}
fun getPerson(userName: String): Person {
val user: User = this.getFullUser(userName)
return user
}
Firefly could also serialize function calls
fun registerUser(user: User) =
req.post("http://data-service:80/do", call(dataService::save))
And in the service
expose dataService {
save(User): Try<Status>
}
@Post("/do")
fun do(functionCall: Call<_>): Try<Status> = functionCall()
This works by serializing the field or type name and resolving it on the other side using static invocation (in other words, almost no runtime overhead other than function resolution through switch-case), also, to be possible to invoke a function, the receiver of function must to explicitly expose the function.
Firefly could serialize the entire function body, as long as the function body only uses exposed functions and objects, and provides shapes, for example:
val dataService = shape {
fun save(user: User) // Return type not required
}
val passwordService = shape {
fun <I> hash(passwordProperty: Property<I, String>): I
}
fun registerUser(user: User) =
req.post("http://data-service:80/do", fun(dataService, passwordService) {
val newUser = passwordService.hash(user::password)
dataService.save(newUser)
})
And in the other side:
expose passwordService {
fun <I> hash(passwordProperty: Property<I, String>): I
}
expose dataService {
save(User): Try<Status>
}
@Post("/do")
fun do(functionCall: Call<_>): Try<Status> = functionCall()
Liked that? This serialization mechanism is available for any JVM language through Super project, however, only Firefly provides the library mechanism as a natural language feature.