Crates.io | lavish-compiler |
lib.rs | lavish-compiler |
version | 0.4.0 |
source | src |
created_at | 2019-06-06 13:04:26.326533 |
updated_at | 2019-06-25 00:32:12.065769 |
description | Compiler for the Lavish IDL |
homepage | |
repository | https://github.com/lavish-lang/lavish-compiler |
max_upload_size | |
id | 139402 |
size | 161,879 |
lavish
lets you declare services, and implement/consume them
easily from a variety of languages.
It is opinionated:
lavish is still under development, it is not usable yet:
Schemas can define "functions", that take arguments and return results.
server fn log(message: string)
server fn get_version() -> (major: i64, minor: i64, patch: i64)
server fn shutdown()
All input parameters and output parameters (results) are named.
Functions can be namespaced:
namespace utils {
server fn log()
server fn get_version()
}
namespace system {
server fn shutdown()
}
Functions can be implemented by the server or the client:
namespace session {
// try to log in. if password authentication is
// enabled, @get_password is called.
server fn login(username: string)
client fn get_password(username: string) -> (password: string)
server fn logout()
}
Built-in types (lowercase) are:
i8
, u16
, u32
, u64
: Unsigned integeri8
, i16
, i32
, i64
: Signed integer
f32
, f64
: Floating-point numberbool
: Booleanstring
: UTF-8 stringdata
: Raw byte arraytimestamp
: UTC date + timeCustom types can be declared, those should be CamelCase
:
enum LoginType {
Anonymous = "anonymous",
Password = "password",
}
struct Session {
login_type: LoginType,
connected_at: timestamp,
}
namespace session {
server fn login() -> (session: Session)
// etc.
}
By default, all fields must be specified - there are no default
values. However, fields can be made optional with option<T>
:
// password can be None in Rust, nil in Go, undefined in TypeScript
server fn login(password: option<string>)
Arrays are declared with array<T>
:
server fn login(ciphers: array<Cipher>)
Maps are declared with map<K, V>
:
server fn login(options: map<string, string>)
option
, map
, and array
can be nested:
server fn login(options: option<map<string, string>>)
Third-party schemas can be imported:
import itchio from "github.com/itchio/go-itchio"
namespace fetch {
server fn game(id: i64) -> (game: option<itchio.Game>)
}
(More on the import mechanism later.)
A workspace is a directory that contains a lavish-rules
file.
A lavish-rules
file is like a Makefile
for lavish - it tells
it what to compile, and for which language.
The lavish
command-line tool compiles schema files to Rust, Go,
TypeScript code.
Each workspace:
Let's say we're writing a simple Go service that returns the current time.
Before running the lavish compiler, our repo looks like:
- go.mod
- main.go
- services/
- clock.lavish
- lavish-rules
clock.lavish
contains:
server fn current_time() -> (time: timestamp)
And lavish-rules
contains:
target go
build clock from "./clock.lavish"
The lavish compiler accepts the path to the workspace:
lavish build ./services
After running the lavish compiler, our repo will look like:
- go.mod
- main.go
- services/
- clock.lavish
- lavish-rules
- clock/ <-- generated
- clock.go <-- generated
We can now implement the clock server, with something like:
package main
import (
"github.com/fasterthanlime/clock/services/clock"
"time"
)
func Handler() clock.ServerHandler {
var h clock.ServerHandler
h.OnCurrentTime(func () (clock.CurrentTimeResults, error) {
res := clock.CurrentTimeResults{
time: time.Now(),
}
return res, nil
})
return h
}
Finally, we can add a lavish-rules
file to the top-level, so that
we can later seemlessly import it from other projects:
export "./services/clock.lavish" as clock
Let's say we want to call our clock service from rust.
Our initial Rust repo will look like:
- Cargo.toml
- src
- main.rs
- services/
- lavish-rules
Our lavish-rules
file will look like:
target rust
build clock from "github.com/fasterthanlime/clock"
Running the compiler with:
lavish build ./src/services
...will complain that clock
is missing.
Running:
lavish fetch ./src/services
Will populate the lavish-vendor
folder:
- Cargo.toml
- src
- main.rs
- lavish-rules
- lavish-vendor/ <-- new
- clock.lavish <-- new
Running compile again will generate rust code:
- Cargo.toml
- src
- main.rs
- lavish-rules
- lavish-vendor/
- clock.lavish
- clock/ <-- new
- mod.rs <-- new
Now, the clock
module can be imported from Rust and used
to consume the service, with something like:
mod clock;
type Error = Box<dyn std::error::Error + 'static>;
async fn example() -> Result<(), Error> {
// Create router - don't implement any functions from our side.
let r = clock::client::Router::new();
// Connect to server over TCP, with default timeout.
let client = lavish::connect(r, "localhost:5959")?.client();
{
let time = client.call(clock::current_time::Params {})?.time;
println!("Server time: {:#?}", time);
}
// when all handles go out of scope, the connection is closed
}
Initial repo:
- src/
- main.ts
- services/
- lavish-rules
Contents of lavish-rules
:
target ts
build clock from "github.com/itchio"
lavish fetch src/services
- src/
- main.ts
- services/
- lavish-rules
- lavish-vendor/ <-- new
- clock.lavish <-- new
lavish compile src/services
- src/
- main.ts
- services/
- lavish-rules
- lavish-vendor/
- clock.lavish
- clock <-- new
- index.ts <-- new
We can then use it, from index.ts
:
import clock from "./services/clock"
async function main() {
let socket = new net.Socket();
await new Promise((resolve, reject) => {
socket.on("error", reject);
socket.connect({ host: "localhost", port: 5959 }, resolve);
});
let client = new clock.Client(socket);
console.log(`Server time: `, await client.getTime());
socket.close();
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
Say you use two services, A
and B
, and they both use types from schema C
.
You want to be able to pass a result from a call to A
, as a parameter into
a call to B
.
If you build
both A
and B
in the same workspace, you'll end up with three directories: A
, B
, and C
. Both A
and B
will use the types
from C
.
Also:
lavish-rules
) is, uh, not that badThen you can't use A
and B
in the same workspace. You can make two
workspaces though!
It does, very much so.
Again, simpler implementation. If you want to generate bindings for multiple languages in a single repo, you can have:
- foobar/
- lavish-rules
- foobar-js/
- lavish-rules
- foobar-go/
- lavish-rules
- foobar-rs/
- lavish-rules
import from
paths?My idea for the import syntax is, for local files:
import foo from "./foo.lavish"
import bar from "../bar.lavish"
And for repos:
import foo from "github.com/user/foo"
import foo from "gitlab.com/user/bar"
Given host/user/project
, it tries:
https://host/user/project.git
git@host:user/project.git
lavish build
need internet connectivity?No, it does not. lavish fetch
does.
lavish fetch
a mini package manager, sorta?Sorta, yes. You caught me. The alternative seems to involve copying lots of files around or manually cloning repos which sucks for a variety of reasons.
TL;DR: lavish fetch
vendors, lavish build
works offline.
I like JSON-RPC a lot, because of its simplicity. That's what I used before. Msgpack-RPC is very similar, except with faster serialization, a proper timestamp type, and the ability to pass raw bytes around.
Cap'n Proto RPC is awe-inspiring. Not only is it fast, it also brings unique features - capabilities, and promise pipelining. I got really really excited about it.
However, after spending some time implementing capnp-rpc on top of an existing TypeScript serialization library, I finally conceded that:
tarpc looks great, but Rust-only.
grpc is definitely trying to be everything to everyone. I would like to consume services from a variety of applications written with a variety of languages - a MsgPack serialization lib + TCP sockets is a reasonable ask for that. ProtoBufs + HTTP/2 is not.