Slide 1

Slide 1 text

Writing Enterprise Software A Rust experiment Luca Palmieri @algo_luca / LukeMathWalker

Slide 2

Slide 2 text

What is Enterprise Software? @algo_luca lpalmieri.com

Slide 3

Slide 3 text

What is Enterprise Software? Enterprise software is the art and craft of formalising processes as code. @algo_luca lpalmieri.com

Slide 4

Slide 4 text

Corollary Enterprise software developers are bureaucrats, version 2.0 @algo_luca lpalmieri.com

Slide 5

Slide 5 text

What are the challenges? Understanding the underlying business domain. @algo_luca lpalmieri.com

Slide 6

Slide 6 text

What are the challenges? Processes change over time. @algo_luca lpalmieri.com

Slide 7

Slide 7 text

What are the challenges? Different expertises have to cooperate. @algo_luca lpalmieri.com

Slide 8

Slide 8 text

What are the challenges? People come and go. The code stays. In production. @algo_luca lpalmieri.com

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

@algo_luca lpalmieri.com

Slide 11

Slide 11 text

Why Rust? ● Powerful type-system ● Great modularity (traits) ● Vibrant community ● Solid tooling … ● Great performance @algo_luca lpalmieri.com

Slide 12

Slide 12 text

Are you sure? Rust is great, but… @algo_luca lpalmieri.com

Slide 13

Slide 13 text

De-risking: a “toy” project! @algo_luca lpalmieri.com “The mother of all demo apps”

Slide 14

Slide 14 text

De-risking: a “toy” project! @algo_luca lpalmieri.com ...and many more

Slide 15

Slide 15 text

De-risking: a “toy” project! @algo_luca lpalmieri.com https://demo.realworld.io

Slide 16

Slide 16 text

Realworld-tide @algo_luca lpalmieri.com

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Persistence-layer | The ORM @algo_luca lpalmieri.com https://diesel.rs

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

ORM/Diesel | Compile-time checks @algo_luca lpalmieri.com table! { articles (slug) { title :> Varchar, slug :> Varchar, description :> Varchar, body :> Text, tag_list :> Array, user_id :> Uuid, created_at :> Timestamptz, updated_at :> Timestamptz, } }

Slide 22

Slide 22 text

ORM/Diesel | Query builder @algo_luca lpalmieri.com pub fn update(repo: &Repo, user_id: Uuid, details: UpdateUser) :> Result { use crate::schema::users::dsl::*; diesel::update(users.find(user_id)) .set(&details) .get_result(&repo.conn()) }

Slide 23

Slide 23 text

ORM/Diesel | Compilation time @algo_luca lpalmieri.com cargo +nightly build -Z timings

Slide 24

Slide 24 text

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, DatabaseError>; fn feed(&self, user: &User, query: FeedQuery) :> Result, DatabaseError>; fn delete_article(&self, article: &Article) :> Result<(), DatabaseError>; ::. }

Slide 25

Slide 25 text

The domain layer @algo_luca lpalmieri.com Db Domain Web Repository Commands Postgres SQL REST API Uses Tide Uses Diesel depends on depends on

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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, } impl ArticleContent { ::/ Convert a title into a url-safe slug pub fn slug(&self) :> String { self.title .to_ascii_lowercase() .split_ascii_whitespace() .join("-") } }

Slide 28

Slide 28 text

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 { let hash = bcrypt::hash(clear_text_password, 4)?; Ok(Password(hash)) } }

Slide 29

Slide 29 text

Domain-layer | Meaningful signatures @algo_luca lpalmieri.com fn publish_article( &self, draft: ArticleContent, author: &User, ) :> Result;

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

The API layer @algo_luca lpalmieri.com Db Domain Web Repository Commands Postgres SQL REST API Uses Tide Uses Diesel depends on depends on

Slide 32

Slide 32 text

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, pub updated_at: DateTime, pub body: String, pub author: Author, } impl From 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(), } } }

Slide 33

Slide 33 text

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 { pub repository: R, }

Slide 34

Slide 34 text

API-layer | State management @algo_luca lpalmieri.com pub async fn tags(cx: Request) :> Result { let repository = &cx.state().repository; ::. } pub fn get_app(repository: R) :> Server { let context = Context { repository }; let mut app = Server::with_state(context); app = add_middleware(app); app = add_routes(app); app }

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

API-layer | Black-box testing @algo_luca lpalmieri.com pub type TestServer = TestBackend; 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(), } } }

Slide 37

Slide 37 text

API-layer | Black-box testing @algo_luca lpalmieri.com pub async fn get_current_user(&mut self, token: &String) :> Result { 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 }

Slide 38

Slide 38 text

Wrapping up @algo_luca lpalmieri.com

Slide 39

Slide 39 text

Ask me anything! Luca Palmieri @algo_luca / LukeMathWalker

Slide 40

Slide 40 text

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