Overview

I started writing lta-rs on 13 March 2019 as a learning project to learn the Rust programming language. Over time I learnt quite a lot about the language and initially the design was not as good and it contained many bad programming practices. The goal of the library at that time was only 2, simplicity and predictable usage but over time as the async ecosystem in Rust matures, I realised that the implementation rely too strongly on it's base library reqwest which relies on hyper which relies on tokio. Don't get me wrong, tokio is the best in breed across multiple programming languages and while tokio is a fantastic runtime for async programming, there is another one called async-std. I wondered for quite awhile regarding the implication of using a library is tied strongly to one runtime and realised that if anyone wants to use my project for async-std or any other runtime, they will not be able to and hence they would end up rewriting everything that I did except that it will be tailor made to run on async-std. With this problem in mind, I am currently thinking of solutions that allows lta-rs to be runtime agnostic in order to solve this async fragmentation.

Problems

In order for lta-rs to be runtime agnostic, I have to refactor lta-rs quite a lot and there will be a lot of breaking changes. There are also other things that such as not being able to use async in traits without using a workaround.

Current experiment

Currently, I feel that this can only be solved mainly using traits and while this will introduce a lot of boilerplate, this is the only way without abusing macros or any other tricks. For the example below, I will use dtolnay's async-trait library. This library certainly meet my goals to make lta-rs runtime agnostic however there is a small cost and there will be a performance penalty because all the Futures will get boxed (note that other languages always box their Future 🤣). While it is not nice to have a performance degradation, I personally feel that ergonomics and extensibility should be the primary goal.

This demo below is currently in lta-async lib.rs and should be moved to the utils module in the future. Note: This does not run. Do not try to copy and paste.

/// Example trait that would be provided by lta_utils_commons

/// RB -> Request Builder
pub trait Client<RB> {
    fn get_req_builder(&self, url: &str) -> RB;
}

/// Currently you will have to use dtolnay's async_trait library
#[async_trait]
pub trait BusRequests<RB> {
    type ClientType: Client<RB>;

    async fn get_arrival(
        c: &Self::ClientType,
        bus_stop_code: u32,
        service_no: Option<&str>,
    ) -> LTAResult<BusArrivalResp>;

    async fn get_bus_services(
        c: &Self::ClientType,
        skip: Option<u32>,
    ) -> LTAResult<Vec<BusService>>;

    async fn get_bus_routes(
        c: &Self::ClientType,
        skip: Option<u32>,
    ) -> LTAResult<Vec<BusRoute>>;

    async fn get_bus_stops(c: &Self::ClientType, skip: Option<u32>) -> LTAResult<Vec<BusStop>>;
}

Example implementation of trait

/// Example of impl

pub struct Bus;

#[async_trait]
impl BusRequests<reqwest::RequestBuilder> for Bus {
    type ClientType = AsyncLTAClient;

    async fn get_arrival(
        c: &Self::ClientType,
        bus_stop_code: u32,
        service_no: Option<&str>,
    ) -> LTAResult<BusArrivalResp> {
        build_req_async_with_query::<RawBusArrivalResp, _, _, _>(
            c,
            api_url!("/BusArrivalv2"),
            move |rb| match service_no {
                Some(srv_no) => rb.query(&[
                    ("BusStopCode", bus_stop_code.to_string()),
                    ("ServiceNo", srv_no.to_string()),
                ]),
                None => rb.query(&[("BusStopCode", bus_stop_code.to_string())]),
            },
        )
        .await
    }

    async fn get_bus_services(
        c: &Self::ClientType,
        skip: Option<u32>,
    ) -> LTAResult<Vec<BusService>> {
        build_req_async_with_skip::<BusServiceResp, _, _>(c, api_url!("/BusServices"), skip)
            .await
    }

    async fn get_bus_routes(c: &Self::ClientType, skip: Option<u32>) -> LTAResult<Vec<BusRoute>> {
        build_req_async_with_skip::<BusRouteResp, _, _>(c, api_url!("/BusRoutes"), skip).await
    }

    async fn get_bus_stops(c: &Self::ClientType, skip: Option<u32>) -> LTAResult<Vec<BusStop>> {
        build_req_async_with_skip::<BusStopsResp, _, _>(c, api_url!("/BusStops"), skip).await
    }
}
use std::env::var;

/// Example of usage
#[tokio::test]
async fn get_bus_service_new() -> LTAResult<()> {
    let api_key = var("API_KEY").expect("No API_KEY found!");

    // A reqwest Client that impl `Client` trait
    // this can be any other client, eg actix-web client, surf, isahc etc
    let client = AsyncLTAClient::new(api_key); 
    let bus = Bus::get_arrival(&client, 83139, None).await?;
    dbg!(bus);

    Ok(())
}

As you can see from the examples above, my goal is to maintain the way you call the library API across different type of client while still getting the same kind of result.

Other goals

One of my other goals for lta-rs is for it to be stable and eventually go into maintenance mode. This is currently not going to happen any time soon because I am still experimenting with a lot of different things such as async traits and there are still no performance benchmarks for the one implemented using boxed Futures (ie currenty implementation for async traits) and therefore I am not able to judge if I should go on or wait for the feature to be stabilised. Do note that once lta-rs goes 1.0, there still might be changes because the data returned by LTA might change

Conclusion

In conclusion, I would like to refactor the whole library to be runtime agnostic and while the some of you might feel that it is unnecessary, I want like my library to be usable on as many places as possible. There will be certainly some edge cases where I will not be able to solve and to think that the library will be usable with all types of client libraries is unrealistic, but it will (hopefully) be usable with enough client libraries that support most runtimes.