Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Writing enterprise software - A Rust experiment

Writing enterprise software - A Rust experiment

Rust has more to bring to the table than performance and tight control over resources.
Luca Palmieri will walk us through the implementation of a Realworld backend service using Rust (Tide + Diesel), with a focus on the opportunities opened up by the Rust type system for domain modelling, API design and communicating intent when writing enterprise software.

www.lpalmieri.com/

Luca Palmieri

February 25, 2020
Tweet

Transcript

  1. What is Enterprise Software? Enterprise software is the art and

    craft of formalising processes as code. @algo_luca lpalmieri.com
  2. What are the challenges? People come and go. The code

    stays. In production. @algo_luca lpalmieri.com
  3. What do we need? We’d like correct code which is

    expressive enough to model the domain and supple enough to support its evolution over time. (Yes, it should also be executable by a machine) @algo_luca lpalmieri.com
  4. Why Rust? • Powerful type-system • Great modularity (traits) •

    Vibrant community • Solid tooling … • Great performance @algo_luca lpalmieri.com
  5. The architecture @algo_luca lpalmieri.com Db Domain Web Repository Commands Postgres

    SQL REST API Uses Tide Uses Diesel depends on depends on The application
  6. Domain The persistence layer @algo_luca lpalmieri.com Db Repository Postgres SQL

    Uses Diesel Web Commands REST API Uses Tide depends on depends on
  7. ORM/Diesel | SQL-first migrations @algo_luca lpalmieri.com CREATE TABLE articles (

    title VARCHAR(255) NOT NULL, slug VARCHAR(255) PRIMARY KEY, description VARCHAR(1024) NOT NULL, body TEXT NOT NULL, tag_list TEXT[] NOT NULL, user_id UUID NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); SELECT diesel_manage_updated_at('articles');
  8. ORM/Diesel | Compile-time checks @algo_luca lpalmieri.com table! { articles (slug)

    { title :> Varchar, slug :> Varchar, description :> Varchar, body :> Text, tag_list :> Array<Text>, user_id :> Uuid, created_at :> Timestamptz, updated_at :> Timestamptz, } }
  9. ORM/Diesel | Query builder @algo_luca lpalmieri.com pub fn update(repo: &Repo,

    user_id: Uuid, details: UpdateUser) :> Result<User, Error> { use crate::schema::users::dsl::*; diesel::update(users.find(user_id)) .set(&details) .get_result(&repo.conn()) }
  10. Persistence-layer | Inversion of control @algo_luca lpalmieri.com Repository trait, defined

    in the domain layer. pub trait Repository { fn find_articles(&self, query: ArticleQuery) :> Result<Vec<Article>, DatabaseError>; fn feed(&self, user: &User, query: FeedQuery) :> Result<Vec<ArticleView>, DatabaseError>; fn delete_article(&self, article: &Article) :> Result<(), DatabaseError>; ::. }
  11. The domain layer @algo_luca lpalmieri.com Db Domain Web Repository Commands

    Postgres SQL REST API Uses Tide Uses Diesel depends on depends on
  12. We can leverage the type system to represent the constraints

    of our domain, making incorrect state difficult or impossible to represent. Designing with types (F# for fun and profit) Domain Modeling Made Functional (Scott Wlaschin) Parse, don’t validate (Alexis King) Domain-layer | Type-driven development @algo_luca lpalmieri.com
  13. Domain-layer | Control over mutability @algo_luca lpalmieri.com :[derive(Clone, Debug, PartialEq)]

    pub struct ArticleContent { pub title: String, pub description: String, pub body: String, pub tag_list: Vec<String>, } impl ArticleContent { ::/ Convert a title into a url-safe slug pub fn slug(&self) :> String { self.title .to_ascii_lowercase() .split_ascii_whitespace() .join("-") } }
  14. Domain-layer | Semantic usage of ownership @algo_luca lpalmieri.com :[derive(Clone, Debug,

    PartialEq)] pub struct Password(String); impl Password { ::/ Given a clear-text password, it returns a `Password` instance ::/ containing the password's hash. ::/ The clear text password is consumed. pub fn from_clear_text(clear_text_password: String) :> Result<Password, PasswordError> { let hash = bcrypt::hash(clear_text_password, 4)?; Ok(Password(hash)) } }
  15. Domain-layer | Meaningful signatures @algo_luca lpalmieri.com fn publish_article( &self, draft:

    ArticleContent, author: &User, ) :> Result<Article, PublishArticleError>;
  16. Domain-layer | Error as values @algo_luca lpalmieri.com :[derive(thiserror::Error, Debug)] pub

    enum PublishArticleError { :[error("There is no author with user id {author_id:?}.")] AuthorNotFound { author_id: Uuid, :[source] source: GetUserError, }, :[error("There is already an article using {slug:?} as slug. Change title!")] DuplicatedSlug { slug: String, :[source] source: DatabaseError, }, :[error("Something went wrong.")] DatabaseError(:[from] DatabaseError), }
  17. The API layer @algo_luca lpalmieri.com Db Domain Web Repository Commands

    Postgres SQL REST API Uses Tide Uses Diesel depends on depends on
  18. API-layer | De/Serialization @algo_luca lpalmieri.com :[derive(Serialize, Deserialize, Clone, Debug)] :[serde(rename_all

    = "camelCase")] pub struct Comment { pub id: u64, pub created_at: DateTime<Utc>, pub updated_at: DateTime<Utc>, pub body: String, pub author: Author, } impl From<domain::Comment> for Comment { fn from(c: domain::Comment) :> Self { Self { id: c.id, body: c.body, created_at: c.created_at, updated_at: c.updated_at, author: c.author.into(), } } }
  19. API-layer | State management @algo_luca lpalmieri.com ::/ The shared state

    of our application. ::/ It's generic with respect to the actual implementation of the repository: ::/ this enables swapping different implementations, both for production usage ::/ or ease of testing (mocks and stubs). pub struct Context<R: 'static + Repository + Sync + Send> { pub repository: R, }
  20. API-layer | State management @algo_luca lpalmieri.com pub async fn tags<R:

    ::.>(cx: Request<Context<R:>) :> Result<Response, ErrorResponse> { let repository = &cx.state().repository; ::. } pub fn get_app<R: Repository + Send + Sync>(repository: R) :> Server<Context<R:> { let context = Context { repository }; let mut app = Server::with_state(context); app = add_middleware(app); app = add_routes(app); app }
  21. API-layer | Routing @algo_luca lpalmieri.com api.at("/api/user") .get(|req| async move {

    result_to_response(crate::users::get_current_user(req).await) }) .put(|req| async move { result_to_response(crate::users::update_user(req).await) });
  22. API-layer | Black-box testing @algo_luca lpalmieri.com pub type TestServer =

    TestBackend<Service<Context<Repository::>; pub struct TestApp { pub server: TestServer, pub repository: Repository, } impl TestApp { pub fn new() :> Self { let app = get_app(get_repo()); let server = make_server(app.into_http_service()).unwrap(); Self { server, repository: get_repo(), } } }
  23. API-layer | Black-box testing @algo_luca lpalmieri.com pub async fn get_current_user(&mut

    self, token: &String) :> Result<UserResponse, Response> { let auth_header = format!("token: {}", token); let response = self .server .simulate( http::Request::get("/api/user") .header("Authorization", auth_header) .body(http_service::Body::empty()) .unwrap(), ) .unwrap(); response_json_if_success(response).await }
  24. Designing with types (F# for fun and profit) Domain Modeling

    Made Functional (Scott Wlaschin) Parse, don’t validate (Alexis King) Ferris’ doodles (Esther Arzola) References @algo_luca lpalmieri.com