Who's Awesome?

You are.

Someone recently posted this on reddit, and I think that it's awesome. I challenged myself to make this into a web service in under five minutes, because let's face it -- who has time to pick a thing four times?

Want to skip all the code and just get to the result? Here's some feel good messaging.

The Setup

I'm setting up shuttle.rs because it's super fast to set up, and thruster as the web framework because I wrote it, so I better know how to use it.

mkdir pep-generator
cargo shuttle init --thruster

I'm adding a few quick deps, basically the shuttle stuff, thruster stuff, tokio, and askama for templating.

askama = "0.11.1" # For templating
chrono = "0.4.22" # For time stuff
log = "0.4.17" # For teh logz
rand = "0.8.5" # Random!
shuttle-aws-rds = { version = "0.7.2", features = ["postgres"] } # For storing data
shuttle-service = { version = "0.7.2", features = ["web-thruster"] } # For deploying easily
sqlx = { version = "0.6.2", features = ["runtime-tokio-native-tls", "postgres", "chrono", "uuid"] } # For dealing with postgres
thruster = { version = "1.3.0", features = ["hyper_server"] } # For http framework stuff
tokio = { version = "1.20.1", features = ["macros"] } # For an async runtime

The Schema

Schemas are simple, the ideas behind twitter are simple, we'll probably never need to make a change to this, so here's the schema; throw it in schema.sql.

-- Creating the users table
CREATE TABLE IF NOT EXISTS first_list (
    id SERIAL PRIMARY KEY,
    item TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS second_list (
    id SERIAL PRIMARY KEY,
    item TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS third_list (
    id SERIAL PRIMARY KEY,
    item TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS fourth_list (
    id SERIAL PRIMARY KEY,
    item TEXT NOT NULL
);

-- Okay... yes, this took more than five minutes for me to copy...
INSERT INTO first_list (id, item)
VALUES
    (1, 'Champ,'),
    (2, 'Fact:'),
    (3, 'Everybody says'),
    (4, 'Dang...'),
    (5, 'Check it:'),
    (6, 'Just saying...'),
    (7, 'Superstar,'),
    (8, 'Tiger,'),
    (9, 'Self,'),
    (10, 'Know this:'),
    (11, 'News alert:'),
    (12, 'Girl,'),
    (13, 'Ace,'),
    (14, 'Excuse me but'),
    (15, 'Experts agree:'),
    (16, 'In my opinion,'),
    (17, 'Hear ye, hear ye:'),
    (18, 'Okay, listen up:');
    
INSERT INTO second_list (id, item)
VALUES
    (1, 'the mere idea of you'),
    (2, 'your soul'),
    (3, 'your hair today'),
    (4, 'everything you do'),
    (5, 'your personal style'),
    (6, 'every thought you have'),
    (7, 'that sparkle in your eye'),
    (8, 'your presence here'),
    (9, 'what you got going on'),
    (10, 'the essential you'),
    (11, 'your life\'s journey'),
    (12, 'that saucy personality'),
    (13, 'your DNA'),
    (14, 'that brain of yours'),
    (15, 'your choice of attire'),
    (16, 'the way you roll'),
    (17, 'whatever your secret is'),
    (18, 'all of y\'all');

INSERT INTO third_list (id, item)
VALUES
    (1, 'has serious game,'),
    (2, 'rains magic,'),
    (3, 'deserves the Nobel Prize,'),
    (4, 'raises the roof,'),
    (5, 'breeds miracles,'),
    (6, 'is paying off big time,'),
    (7, 'shows mad skills,'),
    (8, 'just shimmers,'),
    (9, 'is a national treasure,'),
    (10, 'gets the party hopping,'),
    (11, 'is the next big thing,'),
    (12, 'roars like a lion,'),
    (13, 'is a rainbow factory,'),
    (14, 'is made of diamonds,'),
    (15, 'makes birds sing,'),
    (16, 'should be taught in school,'),
    (17, 'makes my world go \'round,'),
    (18, 'is 100% legit,');

INSERT INTO fourth_list (id, item)
VALUES
    (1, '24/7.'),
    (2, 'can I get an amen?'),
    (3, 'and that\'s a fact.'),
    (4, 'so treat yourself.'),
    (5, 'you feel me?'),
    (6, 'that\'s just science.'),
    (7, 'would I lie?'),
    (8, 'for reals.'),
    (9, 'mic drop.'),
    (10, 'you hidden gem.'),
    (11, 'snuggle bear.'),
    (12, 'period.'),
    (13, 'can I get an amen?'),
    (14, 'now let\s dance.'),
    (15, 'high five.'),
    (16, 'say it again!'),
    (17, 'according to CNN.'),
    (18, 'so get used to it.');

The Code

We have the basic code in lib.rs, first let's populate the DB. If you aren't using an IDE that lets you auto import, then you should get one at this point.

#[shuttle_service::main]
async fn thruster(
    #[shuttle_aws_rds::Postgres] pool: PgPool,
) -> shuttle_service::ShuttleThruster<HyperServer<Ctx, ()>> {
    info!("Starting server...");

    pool.execute(include_str!("../schema.sql"))
        .await
        .map_err(|e| CustomError::new(e))?;

    info!("Server started...");

    Ok(HyperServer::new(
        // I changed the route here
        App::<HyperRequest, Ctx, ()>::create(generate_context, ()).get("/", m![hello]),
    ))
}

I also need to add a pool for the middleware to make calls to, so I need to have a singleton on the server. So I used TypedHyperContext along with some structs to hold it. I had to delete some of the default imports that these override.

pub type Ctx = TypedHyperContext<RequestConfig>;

pub struct ServerConfig {
    pub pool: Pool<Postgres>,
}

#[derive(Clone)]
pub struct RequestConfig {
    pub pool: Pool<Postgres>,
}

fn generate_context(request: HyperRequest, state: &ServerConfig, _path: &str) -> Ctx {
    Ctx::new(
        request,
        RequestConfig {
            pool: state.pool.clone(),
        },
    )
}

Oops -- we have to update the thruster function type signature now to include the server config too;

#[shuttle_service::main]
async fn thruster(
    #[shuttle_aws_rds::Postgres] pool: PgPool,
) -> shuttle_service::ShuttleThruster<HyperServer<Ctx, ServerConfig>> {
    info!("Starting server...");

    pool.execute(include_str!("../schema.sql"))
        .await
        .map_err(|e| CustomError::new(e))?;

    info!("Server started...");

    Ok(HyperServer::new(
        App::<HyperRequest, Ctx, ServerConfig>::create(generate_context, ServerConfig { pool })
            .get("/hello", m![hello]),
    ))
}

Okay, so we're making that DB, but we have no page except stupid "Hello, world!" I'm going to take that function and reuse it for our / route.

#[derive(Template)]
#[template(path = "home.html")]
pub struct Home<'a> {
    first: &'a str,
    second: &'a str,
    third: &'a str,
    fourth: &'a str,
}

#[middleware_fn]
pub async fn hello(mut context: Ctx, _next: MiddlewareNext<Ctx>) -> MiddlewareResult<Ctx> {
    context.set("Content-Type", "text/html");
    context.body(
        &Home {
            first: todo!(),
            second: todo!(),
            third: todo!(),
            fourth: todo!(),
        }
        .render()
        .unwrap(),
    );

    Ok(context)
}

Okay, let's randomly grab them phrases from the database now!

#[derive(Debug, FromRow)]
pub struct Phrase {
    pub id: i32,
    pub item: String,
}

async fn fetch_phrase(pool: &Pool<Postgres>, table: &str) -> Result<Phrase, sqlx::Error> {
    sqlx::query_as(&format!("SELECT id, item FROM {} WHERE id = $1", table))
        .bind(rand::rngs::OsRng::default().gen_range(1..=18))
        .fetch_one(pool)
        .await
}

#[middleware_fn]
pub async fn hello(mut context: Ctx, _next: MiddlewareNext<Ctx>) -> MiddlewareResult<Ctx> {
    let phrase_1 = fetch_phrase(&context.extra.pool, "first_list")
        .await
        .map_err(|_e| {
            error!("_e: {:#?}", _e);
            ThrusterError::generic_error(Ctx::new_without_request(context.extra.clone()))
        })?;

    let phrase_2 = fetch_phrase(&context.extra.pool, "second_list")
        .await
        .map_err(|_e| {
            error!("_e: {:#?}", _e);
            ThrusterError::generic_error(Ctx::new_without_request(context.extra.clone()))
        })?;

    let phrase_3 = fetch_phrase(&context.extra.pool, "third_list")
        .await
        .map_err(|_e| {
            error!("_e: {:#?}", _e);
            ThrusterError::generic_error(Ctx::new_without_request(context.extra.clone()))
        })?;

    let phrase_4 = fetch_phrase(&context.extra.pool, "fourth_list")
        .await
        .map_err(|_e| {
            error!("_e: {:#?}", _e);
            ThrusterError::generic_error(Ctx::new_without_request(context.extra.clone()))
        })?;

    context.set("Content-Type", "text/html");
    context.body(
        &Home {
            first: &phrase_1.item,
            second: &phrase_2.item,
            third: &phrase_3.item,
            fourth: &phrase_4.item,
        }
        .render()
        .unwrap(),
    );

    Ok(context)
}

Last step -- let's fill in templates/home.html.

<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="title" content="You're Awesome" />
    <meta name="description" content="and someone needs to tell you." />
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta name="language" content="en-us" />
    <style>
      h1,
      h2 {
        text-align: center;
      }
    </style>
  </head>
  <body>
    <h2>{{ first }}</h2>
    <h1>{{ second }} {{ third }}</h1>
    <h2>{{ fourth }}</h2>
  </body>
</html>

Time to run this sucka!

cargo shuttle run

This runs it locally, so let's see how it looks:

Now I'll ship it to prod, easy as setting up the shuttle project remotely:

cargo shuttle project new

And then deploying the code

cargo shuttle deploy --allow-dirty

Now, follow the link in the resulting command and get a nice lil' message to brighten your day.

All the code can be found here.

Last updated