Now Reading
zerocal – A Serverless Calendar App in Rust Operating on shuttle.rs

zerocal – A Serverless Calendar App in Rust Operating on shuttle.rs

2022-10-06 10:28:42

— Tips on how to create a calendar occasion from the comfort of your command line. ????

Each every so often my buddies and I meet for dinner. I worth these evenings, however the worst half is scheduling these occasions!

We ship out a message to the group.
We await a response.
We resolve on a date.
Somebody sends out a calendar invite.
Issues lastly occur.

None of that’s enjoyable besides for the dinner.

Being the cheap individual you’re, you’d assume: “Why do not you simply use a scheduling app?”.

I’ve tried lots of them. None of them are any good. They’re all… an excessive amount of!

Simply let me ship out an invitation and whoever desires can present up.

  • I don’t wish to need to create an account to your calendar/scheduling/no matter app.
  • I don’t wish to have so as to add my buddies.
  • I don’t wish to have so as to add my buddies’ buddies.
  • I don’t wish to have so as to add my buddies’ buddies’ buddies.
  • You get the thought: I simply wish to ship out an invitation and get no response from you.

The nerdy, introvert engineer’s resolution

???? What we undoubtedly want is one more calendar app which permits us to create occasions and ship out an invitation with a hyperlink to that occasion! You in all probability did not see that coming now, did you?

Oh, and I do not wish to use Google Calendar to create the occasion as a result of I don’t trust them.

Like several cheap individual, I wished a technique to create calendar entries from my terminal.

That is how I pitched the thought to my buddies final time. The reply was: “I don’t know, seems like an answer in the hunt for an issue.” However you realize what they are saying: By no means ask a starfish for instructions.

Present, don’t inform

That night time I went house and constructed an internet site that might create a calendar entry from GET parameters.

It permits you to create a calendar occasion from the comfort of your command line:

> curl https://zerocal.shuttleapp.rs?begin=2022-11-04+20:00&length=3h&title=Birthday&description=paaarty
BEGIN:VCALENDAR
VERSION:2.0
PRODID:ICALENDAR-RS
CALSCALE:GREGORIAN
BEGIN:VEVENT
DTSTAMP:20221002T123149Z
CLASS:CONFIDENTIAL
DESCRIPTION:paaarty
DTEND:20221002T133149Z
DTSTART:20221002T123149Z
SUMMARY:Birthday
UID:c99dd4bb-5c35-4d61-9c46-7a471de0e7f4
END:VEVENT
END:VCALENDAR

You’ll be able to then save that to a file and open it along with your calendar app.

> curl https://zerocal.shuttleapp.rs?begin=2022-11-04+20:00&length=3h&title=Birthday&description=paaarty > birthday.ics
> open birthday.ics

In a way, it is a “serverless calendar app”, haha. There is no such thing as a state on the server, it simply generates a calendar occasion on the fly and returns it.

How I constructed it

You in all probability seen that the URL comprises “shuttleapp.rs”. That is as a result of I am utilizing shuttle.rs to host the web site.

Shuttle is a internet hosting service for Rust tasks and I wished to attempt it out for a very long time.

To initialize the mission utilizing the superior axum internet framework, I’ve used

cargo set up cargo-shuttle
cargo shuttle init --axum --name zerocal zerocal

and I used to be greeted with every thing I wanted to get began:

use axum::{routing::get, Router};
use sync_wrapper::SyncWrapper;

async fn hello_world() -> &'static str {
  "Hey, world!"
}

#[shuttle_service::main]
async fn axum() -> shuttle_service::ShuttleAxum {
  let router = Router::new().route("/howdy", get(hello_world));
  let sync_wrapper = SyncWrapper::new(router);

  Okay(sync_wrapper)
}

Let’s shortly commit the adjustments:

git add .gitignore Cargo.toml src/
git commit -m "Hey World"

To deploy the code, I wanted to join a shuttle account. This may be completed over at https://www.shuttle.rs/login.

It is going to ask you to authorize it to entry your Github account.

Then:

cargo shuttle login

and at last:

cargo shuttle deploy

Now let’s head over to zerocal.shuttleapp.rs:

Hey World!

Deploying the primary model took lower than 5 minutes. Neat! We’re all set for our customized calendar app.

Writing the app

To create the calendar occasion, I used the icalendar crate (shout out to hoodie for creating this good library!). iCalendar is a typical for creating calendar occasions that’s supported by most calendar apps.

cargo add icalendar
cargo add chrono # For date and time parsing

Let’s create a demo calendar occasion:

let occasion = Occasion::new()
  .abstract("take a look at occasion")
  .description("right here I've one thing actually essential to do")
  .begins(Utc::now())
  .ends(Utc::now() + Length::days(1))
  .completed();

Easy sufficient.

Tips on how to return a file!?

Now that we have now a calendar occasion, we have to return it to the consumer. However how will we return it as a file?

There’s an instance of the right way to return a file dynamically in axum here.

async fn calendar() -> impl IntoResponse {
  let ical = Calendar::new()
    .push(
      // add an occasion
      Occasion::new()
        .abstract("It really works! ????")
        .description("Assembly with the Rust group")
        .begins(Utc::now() + Length::hours(1))
        .ends(Utc::now() + Length::hours(2))
        .completed(),
    )
    .completed();

  CalendarResponse(ical)
}

Some fascinating issues to notice right here:

  • Each calendar file is a group of occasions so we wrap the occasion in a Calendar object, which represents the gathering.
  • impl IntoResponse is a trait that permits us to return any kind that implements it.
  • CalendarResponse is a newtype wrapper round Calendar that implements IntoResponse.

Right here is the CalendarResponse implementation:

/// Newtype wrapper round Calendar for `IntoResponse` impl
#[derive(Debug)]
pub struct CalendarResponse(pub Calendar);

impl IntoResponse for CalendarResponse {
  fn into_response(self) -> Response {
    let mut res = Response::new(boxed(Full::from(self.0.to_string())));
    res.headers_mut().insert(
      header::CONTENT_TYPE,
      HeaderValue::from_static("textual content/calendar"),
    );
    res
  }
}

We simply create a brand new Response object and set the Content material-Sort header to the proper MIME kind for iCalendar information: textual content/calendar. Then we return the response.

Add date parsing

This half is a bit hacky, so be happy to look over it. We have to parse the date and length from the question string. I used dateparser, as a result of it helps sooo many different date formats.

async fn calendar(Question(params): Question&LTHashMapString, String>>) -> impl IntoResponse {
  let mut occasion = Occasion::new();
  occasion.class(Class::Confidential);

  if let Some(title) = params.get("title") {
    occasion.abstract(title);
  } else {
    occasion.abstract(DEFAULT_EVENT_TITLE);
  }
  if let Some(description) = params.get("description") {
    occasion.description(description);
  } else {
    occasion.description("Powered by zerocal.shuttleapp.rs");
  }

  if let Some(begin) = params.get("begin") {
    let begin = dateparser::parse(begin).unwrap();
    occasion.begins(begin);
    if let Some(length) = params.get("length") {
      let length = humantime::parse_duration(length).unwrap();
      let length = chrono::Length::from_std(length).unwrap();
      occasion.ends(begin + length);
    }
  }

  if let Some(finish) = params.get("finish") {
    let finish = dateparser::parse(finish).unwrap();
    occasion.ends(finish);
    if let Some(length) = params.get("length") {
      if params.get("begin").is_none() {
        let length = humantime::parse_duration(length).unwrap();
        let length = chrono::Length::from_std(length).unwrap();
        occasion.begins(finish - length);
      }
    }
  }

  let ical = Calendar::new().push(occasion.completed()).completed();

  CalendarResponse(ical)
}

Could be good to help extra date codecs like now and tomorrow, however I will go away that for one more time.

Let’s take a look at it:

See Also

> cargo shuttle run # This begins an area dev server
> curl 127.0.0.1:8000?begin=2022-11-04+20:00&length=3h&title=Birthday&description=Occasion
*???? bleep bloop, calendar file created*

Good, it really works!

Opening it within the browser creates a brand new occasion within the calendar:

Of course, it also works on Chrome, but you do [support the open web](https://contrachrome.com/), right?
After all, it additionally works on Chrome, however you do support the open web, proper?

And for all of the odd individuals who do not use a terminal to create a calendar occasion, let’s additionally add a type to the web site.

Add a type

type>
  desk>
    tr>
      td>
        label for="title">Occasion Title&LT/label>
      &LT/td>
      td>
        enter kind="textual content" id="title" title="title" worth="Birthday" />
      &LT/td>
    &LT/tr>
    tr>
      td>
        label for="desc">Description&LT/label>
      &LT/td>
      td>
        enter kind="textual content" id="desc" title="desc" worth="Occasion" />
      &LT/td>
    &LT/tr>
    tr>
      td>label for="begin">Begin&LT/label>&LT/td>
      td>
        enter kind="datetime-local" id="begin" title="begin" />
      &LT/td>
    &LT/tr>
    tr>
      td>label for="finish">Finish&LT/label>&LT/td>
      td>
        enter kind="datetime-local" id="finish" title="finish" />
      &LT/td>
    &LT/tr>
  &LT/desk>
&LT/type>

I modified the calendar perform a bit to return the shape if the question string is empty:

async fn calendar(Question(params): Question&LTHashMapString, String>>) -> impl IntoResponse {
  // if question is empty, present type
  if params.is_empty() {
    return Response::builder()
      .standing(200)
      .physique(boxed(Full::from(include_str!("../static/index.html"))))
      .unwrap();
  }

  // ...
}

After some extra tweaking, we bought ourselves a pleasant little type in all of its internet 1.0 glory:

The form
The shape

And that is it! We now have a little bit internet app that may create calendar occasions. Properly, nearly. We nonetheless must deploy it.

Deploying

cargo shuttle deploy

Proper, that is all. It is that straightforward. Due to the oldsters over at shuttle.rs for making this attainable.

The calendar app is now obtainable at zerocal.shuttleapp.rs.

Now I can lastly ship my buddies a hyperlink to a calendar occasion for our subsequent pub crawl. They will certainly recognize it.yeahyeah

From zero to calendar in 100 traces of Rust

Boy it feels good to be writing some plain HTML once more.
Constructing little apps by no means will get outdated.

Take a look at the supply code on GitHub and assist me make it higher! ????

Listed below are some concepts:

  • ✅ Add location help (e.g. location=Berlin or location=https://zoom.us/take a look at). Due to sigaloid.
  • Add help for extra human-readable date codecs (e.g. now, tomorrow).
  • Add help for recurring occasions.
  • Add help for timezones.
  • Add Google calendar short-links (https://calendar.google.com/calendar/render?motion=TEMPLATE&dates=20221003T224500Zpercent2F20221003T224500Z&particulars=&location=&textual content=).
  • Add instance bash command to create a calendar occasion from the command line.
  • Shorten the URL (e.g. zerocal.shuttleapp.rs/2022-11-04T20:00/3h/Birthday/Occasion)?

Take a look at the issue tracker and be happy to open a PR!

Thanks for studying! I largely write about Rust and my (open-source) tasks. If you need to obtain future posts robotically, you may subscribe through RSS or e-mail:

Sponsor me on Github My Amazon wish list

Due to Simon Brüggen for reviewing drafts of this text.

Source Link

What's Your Reaction?
Excited
0
Happy
0
In Love
0
Not Sure
0
Silly
0
View Comments (0)

Leave a Reply

Your email address will not be published.

2022 Blinking Robots.
WordPress by Doejo

Scroll To Top