Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Building capabilities

The final piece of the puzzle we should look at in our exploration of the Weather app before we move to the Shell is Capabilities.

We looked at effects a fair bit and explored the Commands and CommandBuilders, but in practice, it's quite rare that you'd interact with those directly from your app.

Typically, you'll be working with effects using Capabilities - more developer-friendly APIs which implement a specific kind of side-effect in a generic fashion. They define the core-shell message protocol for the side-effect and provide an ergonomic API to create the right CommandBuilders. Examples include: HTTP client, Timer operations, Key-Value storage, Secrets provider, Geolocation, etc.

In practice, we find there is a limited number of these effect packages, the should be very reusable, and an individual app will typically need around seven of them, almost certainly less than ten.

Included capabilities

The weather app uses two out of the three capabilities provided with Crux: HTTP client (crux_http), Key-Value store (crux_kv) (the third is the time capability – crux_time).

These are the most common things we think people will want to use in their apps. There are more, and we will probably build those over time as well, we just haven't worked on a motivating use-case ourselves yet. If you have and built a capability which you'd like to donate, definitely get in touch!

Let's look at the use of crux_http quickly, as it's the most extensive of the three. The Weather app makes a pretty typical move and centralises the weather API use in a client:

#![allow(unused)]
fn main() {
pub struct WeatherApi;

impl WeatherApi {
    /// Build an `HttpRequest` for testing purposes
    #[cfg(test)]
    pub fn build(location: Location) -> HttpRequest {
        use crate::weather::model::current_response::WEATHER_URL;

        HttpRequest::get(WEATHER_URL)
            .query(&CurrentWeatherQuery {
                lat: location.lat.to_string(),
                lon: location.lon.to_string(),
                units: "metric",
                appid: API_KEY.clone(),
            })
            .expect("could not serialize query string")
            .build()
    }

    /// Fetch current weather for a specific location
    pub fn fetch<Effect, Event>(
        location: Location,
    ) -> RequestBuilder<
        Effect,
        Event,
        impl std::future::Future<Output = Result<CurrentWeatherResponse, WeatherError>>,
    >
    where
        Effect: From<Request<HttpRequest>> + Send + 'static,
        Event: Send + 'static,
    {
        Http::get(WEATHER_URL)
            .expect_json::<CurrentWeatherResponse>()
            .query(&CurrentWeatherQuery {
                lat: location.lat.to_string(),
                lon: location.lon.to_string(),
                units: "metric",
                appid: API_KEY.clone(),
            })
            .expect("could not serialize query string")
            .build()
            .map(|result| match result {
                Ok(mut response) => match response.take_body() {
                    Some(weather_data) => Ok(weather_data),
                    None => Err(WeatherError::ParseError),
                },
                Err(_) => Err(WeatherError::NetworkError),
            })
    }
}
}

The main method there is fetch, which uses Http::get from crux_http to create a GET request expecting a json response which deserialises into a specific type, and provides a URL query to specify the search. At the end of that chained call is a .map unpicking the response and turning it into a more convenient Result type for the app code.

The interesting thing here is that the fetch method returns a RequestBuilder. In a way, this makes it a half-way step to a custom capability, but it also just means the fetch call is convenient to use from both normal and async context.

This is one of the things capabilities do - they map the lower-level FFI protocols into a more convenient API for the app developer.

Let's look at the other thing they do.

Custom capabilities

The Weather app has one specialty - it works with location services. This is an example of a capability which we'd probably struggle to find a cross-platform crate for. It's also not so common and complex, that we feel we should develop and maintain an official one. So a custom capability in the app is the way to go.

The capability defines two things:

  1. The protocol for communicating to the Shell
  2. The APIs used by the programmer of the Core

Here is Weather app's Location capability in full:

#![allow(unused)]
fn main() {
// This module defines the effect for accessing location information in a cross-platform way using Crux.
// The structure here is designed to be serializable, portable, and to fit into Crux's command/request architecture.

use std::future::Future;

use crux_core::{Command, Request, capability::Operation, command::RequestBuilder};
use facet::Facet;
use serde::{Deserialize, Serialize};

use super::Location;

// The operations that can be performed related to location.
// Using an enum allows us to easily add more operations in the future and ensures type safety.
#[derive(Facet, Clone, Serialize, Deserialize, Debug, PartialEq)]
#[repr(C)]
pub enum LocationOperation {
    IsLocationEnabled,
    GetLocation,
}

// The response structure for a location request.
// This is serializable so it can be sent across the FFI boundary.

// The possible results from performing a location operation.
// This enum allows us to handle different response types in a type-safe way.
#[derive(Facet, Clone, Serialize, Deserialize, Debug, PartialEq)]
#[repr(C)]
pub enum LocationResult {
    Enabled(bool),
    Location(Option<Location>),
}

#[must_use]
pub fn is_location_enabled<Effect, Event>()
-> RequestBuilder<Effect, Event, impl Future<Output = bool>>
where
    Effect: Send + From<Request<LocationOperation>> + 'static,
    Event: Send + 'static,
{
    Command::request_from_shell(LocationOperation::IsLocationEnabled).map(|result| match result {
        LocationResult::Enabled(val) => val,
        LocationResult::Location(_) => false,
    })
}

#[must_use]
pub fn get_location<Effect, Event>()
-> RequestBuilder<Effect, Event, impl Future<Output = Option<Location>>>
where
    Effect: Send + From<Request<LocationOperation>> + 'static,
    Event: Send + 'static,
{
    Command::request_from_shell(LocationOperation::GetLocation).map(|result| match result {
        LocationResult::Location(loc) => loc,
        LocationResult::Enabled(_) => None,
    })
}

// Implement the Operation trait so that Crux knows how to handle this effect.
// This ties the operation type to its output/result type.
impl Operation for LocationOperation {
    type Output = LocationResult;
}
}

There are two interesting types: LocationOperation and LocationResult - they are the request and response pair for the capability. The capability tells Crux that LocationResult is the expected output for the LocationOperation with the trait implementation at the very bottom. It marks the LocationOperation as an Operation as defined by Crux and associates the output type.

That's number 1 done - protocol defined. This is what the Shell will need to understand and return back in order to implement the location capability.

The rest of the code are the two APIs used by the Core developer - is_location_enabled and get_location. Their type signatures are fairly complex, so lets pick them apart.

First, they are both generic over Effect and Event. This isn't strictly necessary for local capabilities, but it makes the capability reusable for any Effect and Event, not just the ones from the Weather app.

The other interesting thing is the trait bound Effect: From<Request<LocationOperation>>, which says that the Effect type needs to be able to convert from a location Request, or in other words - we need to be able to wrap a Request<LocationOperation> into the app's Effect type. All Effect types generated with the #[effect] macro already do this.

Other than that, the APIs just create command builds and return them. Those types are also somewhat gnarly, but it's mostly the impl Future<Output = [value]>, that's interesting. Notice that the Output types are not LocationResult, they are the specific convenient type the Core developer wants.

And that's all Capabilities do - they provide a convenient API for creating CommandBuilders, and converting between convenient Rust types and an FFI "wire protocol" used to communicate with the Shell.

In the ports and adapters architecture, Capabilities are the ports, and the shell-side implementations are the adapters.

In fact, let's go build one in the next chapter.