Announcing Obsidian 0.2

Gan Jun Kai

Gan Jun Kai

Co-author of Obsidian

After months of hard work from the team, we are proud to announce the release of obsidian version 0.2. This release included a lot of features to improve obsidian's developer experience. This blog post covers the highlights of the release.

obsidian is a Rust async http framework for reliable and efficient web.

New Logo!

Screenshot of Obsidian logo

We now have a logo!

Async/Await Support

Rust 1.39 introduced async/.await syntax, which can be used to write asynchronous funtion in synchronous way. It also makes the codes more straight forward. In obsidian, we treat the developer experience as on of the top priority goal. Thus, migration to async/await enabled structure is definitely needed.

async fn get_user(mut ctx: Context) -> ContextResult {
#[derive(Serialize, Deserialize)]
struct User {
name: String,
age: i8,
}
let user: User = ctx.json().await?;
ctx.build_json(user).ok()
}

Application State

Application state is shared with all the routes. It can be accessed by handlers and middlewares. You can inject your database connection in to application state.

use obsidian::{context::Context, App, ObsidianError};
#[derive(Clone)]
pub struct AppState {
pub db_connection_string: String,
}
#[tokio::main]
async fn main() {
let mut app: App<AppState> = App::new();
let addr = ([127, 0, 0, 1], 3000).into();
app.set_app_state(AppState {
db_connection_string: "localhost:1433".to_string()
});
app.get("/", |ctx: Context| async {
let app_state = ctx.get::<AppState>().ok_or(ObsidianError::NoneError)?;
let res = Some(format!("connection string: {}", &app_state.db_connection_string));
ctx.build(res).ok()
});
app.listen(&addr, || {
println!("server is listening to {}", &addr);
})
.await;
}

Dynamic Context data

Dynamic context data is just like application state, but within context scope. For example, it can be used to pass data between middlewares and handlers.

async fn get_profile(ctx: Context) -> ContextResult {
let current_user = ctx
.extensions()
.get::<User>()
.ok_or(ObsidianError::NoneError)?;
ctx.build_json(current_user).ok()
}

Response Header

You now can return a response with headers. And did I mention custom headers is supported as well?

async fn get_point(ctx: Context) -> ContextResult {
let point = Point { x: 1, y: 2 };
ctx.build_json(point)
.with_header(header::AUTHORIZATION, "token")
.with_header_str("X-Custom-Header", "Custom header value")
.ok()
}

Other improvements

  • New Response Syntax: To solve the lifetime issue with context
  • Bugs fixed on the Responder
  • Apply clippy to the source code

What is next?

So far, we have most of the basic functions ready as a POC. Thus, it's time to evolve the framework itself.

As we mentioned, developer experience and productivity are always our top priority in obsidian. The framework is still new and needed a lot of improvements. We always have the convesation on improving those confusing API and syntax noise. We also want to explore more the possibility of the type system, hopefully most of the errors will be captured on compile time.

If you managed to appease the compiler, there’s a good chance your code will work–barring any logic flaws.

We are also experimenting a tool to generate new obsidian project with convention over configuration. The generated project should include some default configuration such as database connection config by default because we found that setting up database connection everytime for a new project is quite annoying. We want our users focus on the important stuffs without sweating the small things.

According to Rust 2019 survey, productivity is still an important goal for their work. Most of the developers in the survey, with Rust or without Rust, are working on backend application. Therefore we think we are on the right track. We want to build a framework focus on developer experience and productivity and more importantly, we will enjoy using it everyday.

The documentation is a WIP