Document not found (404)
+This URL is invalid, sorry. Please use the navigation bar or search to continue.
+ +diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 000000000..e69de29bb diff --git a/404.html b/404.html new file mode 100644 index 000000000..8fb7bd53f --- /dev/null +++ b/404.html @@ -0,0 +1,223 @@ + + +
+ + +This URL is invalid, sorry. Please use the navigation bar or search to continue.
+ +These are the steps to set up Android Studio to build and run a simple Android +app that calls into a shared core.
+This walk-through assumes you have already added the shared
and shared_types
libraries to your repo, as described in Shared core and types.
We want to make setting up Android Studio to work with Crux really easy. As time progresses we will try to simplify and automate as much as possible, but at the moment there is some manual configuration to do. This only needs doing once, so we hope it's not too much trouble. If you know of any better ways than those we describe below, please either raise an issue (or a PR) at https://github.com/redbadger/crux.
+The first thing we need to do is create a new Android app in Android Studio.
+Open Android Studio and create a new project, for "Phone and Tablet", of type
+"Empty Compose Activity (Material3)". In this walk-through, we'll call it
+"Counter", use a minimum SDK of API 34, and save it in a directory called
+Android
.
Your repo's directory structure might now look something like this (some files +elided):
+.
+├── Android
+│ ├── app
+│ │ ├── build.gradle
+│ │ ├── libs
+│ │ └── src
+│ │ └── main
+│ │ ├── AndroidManifest.xml
+│ │ └── java
+│ │ └── com
+│ │ └── example
+│ │ └── counter
+│ │ └── MainActivity.kt
+│ ├── build.gradle
+│ ├── gradle.properties
+│ ├── local.properties
+│ └── settings.gradle
+├── Cargo.lock
+├── Cargo.toml
+├── shared
+│ ├── build.rs
+│ ├── Cargo.toml
+│ ├── src
+│ │ ├── counter.rs
+│ │ ├── lib.rs
+│ │ └── shared.udl
+│ └── uniffi.toml
+├── shared_types
+│ ├── build.rs
+│ ├── Cargo.toml
+│ └── src
+│ └── lib.rs
+└── target
+
+This shared Android library (aar
) is going to wrap our shared Rust library.
Under File -> New -> New Module
, choose "Android Library" and call it
+something like shared
. Set the "Package name" to match the one from your
+/shared/uniffi.toml
, e.g. com.example.counter.shared
.
For more information on how to add an Android library see +https://developer.android.com/studio/projects/android-library.
+We can now add this library as a dependency of our app.
+Edit the app's build.gradle
(/Android/app/build.gradle
) to look like
+this:
plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+}
+
+android {
+ namespace 'com.example.simple_counter'
+ compileSdk 34
+
+ defaultConfig {
+ applicationId "com.example.simple_counter"
+ minSdk 33
+ targetSdk 34
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary true
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles {
+ getDefaultProguardFile('proguard-android-optimize.txt')
+ 'proguard-rules.pro'
+ }
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+ buildFeatures {
+ compose true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion '1.5.3'
+ }
+ packagingOptions {
+ resources {
+ excludes += '/META-INF/*'
+ }
+ }
+}
+
+dependencies {
+ // our shared library
+ implementation project(path: ':shared')
+
+ def composeBom = platform('androidx.compose:compose-bom:2022.10.00')
+ implementation composeBom
+ androidTestImplementation composeBom
+
+ implementation 'androidx.compose.material3:material3:1.2.0-alpha10'
+
+ // Optional - Integration with ViewModels
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
+ // Optional - Integration with LiveData
+ implementation("androidx.compose.runtime:runtime-livedata")
+
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
+
+ implementation 'androidx.core:core-ktx:1.12.0'
+ implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
+ implementation 'androidx.activity:activity-compose:1.8.1'
+ implementation "androidx.compose.ui:ui:1.5.4"
+ implementation "androidx.compose.ui:ui-tooling-preview:1.5.4"
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+ androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.5.4"
+ debugImplementation "androidx.compose.ui:ui-tooling:1.5.4"
+ debugImplementation "androidx.compose.ui:ui-test-manifest:1.5.4"
+}
+
+We'll use the following tools to incorporate our Rust shared library into the +Android library added above. This includes compiling and linking the Rust +dynamic library and generating the runtime bindings and the shared types.
+Let's get started.
+Add the four rust android toolchains to your system:
+$ rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android
+
+Edit the project's build.gradle
(/Android/build.gradle
) to look like
+this:
plugins {
+ id 'com.android.application' version '8.1.2' apply false
+ id 'com.android.library' version '8.1.2' apply false
+ id 'org.jetbrains.kotlin.android' version '1.9.10' apply false
+ id "org.mozilla.rust-android-gradle.rust-android" version "0.9.3" apply false
+}
+
+Edit the library's build.gradle
(/Android/shared/build.gradle
) to look
+like this:
plugins {
+ id 'com.android.library'
+ id 'org.jetbrains.kotlin.android'
+ id 'org.mozilla.rust-android-gradle.rust-android'
+}
+
+android {
+ namespace 'com.example.simple_counter.shared'
+ compileSdk 34
+
+ ndkVersion "25.2.9519653"
+
+
+ defaultConfig {
+ minSdk 33
+ targetSdk 34
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles {
+ getDefaultProguardFile('proguard-android-optimize.txt')
+ 'proguard-rules.pro'
+ }
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+
+ sourceSets {
+ main.java.srcDirs += "${projectDir}/../../shared_types/generated/java"
+ }
+}
+
+dependencies {
+ implementation "net.java.dev.jna:jna:5.13.0@aar"
+
+ implementation 'androidx.core:core-ktx:1.12.0'
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'com.google.android.material:material:1.10.0'
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+}
+
+apply plugin: 'org.mozilla.rust-android-gradle.rust-android'
+
+cargo {
+ module = "../.."
+ libname = "shared"
+ // these are the four recommended targets for Android that will ensure your library works on all mainline android devices
+ // make sure you have included the rust toolchain for each of these targets: \
+ // `rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android`
+ targets = ["arm", "arm64", "x86", "x86_64"]
+ extraCargoBuildArguments = ['--package', 'shared']
+}
+
+afterEvaluate {
+ // The `cargoBuild` task isn't available until after evaluation.
+ android.libraryVariants.configureEach { variant ->
+ def productFlavor = ""
+ variant.productFlavors.each {
+ productFlavor += "${it.name.capitalize()}"
+ }
+ def buildType = "${variant.buildType.name.capitalize()}"
+
+ tasks.named("compileDebugKotlin") {
+ it.dependsOn(tasks.named("typesGen"), tasks.named("bindGen"))
+ }
+
+ tasks.named("generate${productFlavor}${buildType}Assets") {
+ it.dependsOn(tasks.named("cargoBuild"))
+ }
+ }
+}
+
+tasks.register('bindGen', Exec) {
+ def outDir = "${projectDir}/../../shared_types/generated/java"
+ workingDir "../../"
+ if (System.getProperty('os.name').toLowerCase().contains('windows')) {
+ commandLine("cmd", "/c",
+ "cargo build -p shared && " + "target\\debug\\uniffi-bindgen generate shared\\src\\shared.udl " + "--language kotlin " + "--out-dir " + outDir.replace('/', '\\'))
+ } else {
+ commandLine("sh", "-c",
+ """\
+ cargo build -p shared && \
+ target/debug/uniffi-bindgen generate shared/src/shared.udl \
+ --language kotlin \
+ --out-dir $outDir
+ """)
+ }
+}
+
+tasks.register('typesGen', Exec) {
+ workingDir "../../"
+ if (System.getProperty('os.name').toLowerCase().contains('windows')) {
+ commandLine("cmd", "/c", "cargo build -p shared_types")
+ } else {
+ commandLine("sh", "-c", "cargo build -p shared_types")
+ }
+}
+
+
+
+If you now build your project you should see the newly built shared library +object file.
+$ ls --tree Android/shared/build/rustJniLibs
+Android/shared/build/rustJniLibs
+└── android
+ └── arm64-v8a
+ └── libshared.so
+ └── armeabi-v7a
+ └── libshared.so
+ └── x86
+ └── libshared.so
+ └── x86_64
+ └── libshared.so
+
+You should also see the generated types — note that the sourceSets
directive
+in the shared library gradle file (above) allows us to build our shared library
+against the generated types in the shared_types/generated
folder.
$ ls --tree shared_types/generated/java
+shared_types/generated/java
+└── com
+ ├── example
+ │ └── counter
+ │ ├── shared
+ │ │ └── shared.kt
+ │ └── shared_types
+ │ ├── Effect.java
+ │ ├── Event.java
+ │ ├── RenderOperation.java
+ │ ├── Request.java
+ │ ├── Requests.java
+ │ ├── TraitHelpers.java
+ │ └── ViewModel.java
+ └── novi
+ ├── bincode
+ │ ├── BincodeDeserializer.java
+ │ └── BincodeSerializer.java
+ └── serde
+ ├── ArrayLen.java
+ ├── BinaryDeserializer.java
+ ├── BinarySerializer.java
+ ├── Bytes.java
+ ├── DeserializationError.java
+ ├── Deserializer.java
+ ├── Int128.java
+ ├── SerializationError.java
+ ├── Serializer.java
+ ├── Slice.java
+ ├── Tuple2.java
+ ├── Tuple3.java
+ ├── Tuple4.java
+ ├── Tuple5.java
+ ├── Tuple6.java
+ ├── Unit.java
+ └── Unsigned.java
+
+There is a slightly more advanced +example of an +Android app in the Crux repository.
+However, we will use the
+simple counter example,
+which has shared
and shared_types
libraries that will work with the
+following example code.
A simple app that increments, decrements and resets a counter.
+First, let's add some boilerplate code to wrap our core and handle the
+capabilities that we are using. For this example, we only need to support the
+Render
capability, which triggers a render of the UI.
This code that wraps the core only needs to be written once — it only grows when +we need to support additional capabilities.
+Edit Android/app/src/main/java/com/example/simple_counter/Core.kt
to look like
+the following. This code sends our (UI-generated) events to the core, and
+handles any effects that the core asks for. In this simple example, we aren't
+calling any HTTP APIs or handling any side effects other than rendering the UI,
+so we just handle this render effect by updating the published view model from
+the core.
package com.example.simple_counter
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.mutableStateOf
+import com.example.simple_counter.shared.processEvent
+import com.example.simple_counter.shared.view
+import com.example.simple_counter.shared_types.Effect
+import com.example.simple_counter.shared_types.Event
+import com.example.simple_counter.shared_types.Request
+import com.example.simple_counter.shared_types.Requests
+import com.example.simple_counter.shared_types.ViewModel
+
+class Core : androidx.lifecycle.ViewModel() {
+ var view: ViewModel by mutableStateOf(ViewModel.bincodeDeserialize(view()))
+ private set
+
+ fun update(event: Event) {
+ val effects = processEvent(event.bincodeSerialize())
+
+ val requests = Requests.bincodeDeserialize(effects)
+ for (request in requests) {
+ processEffect(request)
+ }
+ }
+
+ private fun processEffect(request: Request) {
+ when (request.effect) {
+ is Effect.Render -> {
+ this.view = ViewModel.bincodeDeserialize(view())
+ }
+ }
+ }
+}
+
+That when
statement, above, is where you would handle any other effects that
+your core might ask for. For example, if your core needs to make an HTTP
+request, you would handle that here. To see an example of this, take a look at
+the
+counter example
+in the Crux repository.
Edit /Android/app/src/main/java/com/example/simple_counter/MainActivity.kt
to
+look like the following:
@file:OptIn(ExperimentalUnsignedTypes::class)
+
+package com.example.simple_counter
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.example.simple_counter.shared_types.Event
+import com.example.simple_counter.ui.theme.CounterTheme
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ CounterTheme {
+ Surface(
+ modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background
+ ) {
+ View()
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun View(core: Core = viewModel()) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(10.dp),
+ ) {
+ Text(text = core.view.count.toString(), modifier = Modifier.padding(10.dp))
+ Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+ Button(
+ onClick = { core.update(Event.Reset()) }, colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.error
+ )
+ ) { Text(text = "Reset", color = Color.White) }
+ Button(
+ onClick = { core.update(Event.Increment()) }, colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ )
+ ) { Text(text = "Increment", color = Color.White) }
+ Button(
+ onClick = { core.update(Event.Decrement()) }, colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.secondary
+ )
+ ) { Text(text = "Decrement", color = Color.White) }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun DefaultPreview() {
+ CounterTheme {
+ View()
+ }
+}
+
+
+
+ When we use Crux to build Android apps, the Core API bindings are generated in +Java using Mozilla's Uniffi.
+The shared core (that contains our app's behavior) is compiled to a dynamic +library, using Mozilla's +Rust gradle plugin for Android +and the Android NDK. The library is loaded +at runtime using +Java Native Access.
+The shared types are generated by Crux as Java packages, which we can add to our
+Android project using sourceSets
. The Java code to serialize and deserialize
+these types across the boundary is also generated by Crux as Java packages.
This section has a guide for building Android apps with Crux:
+ + +Redirecting to... ./Android/android.html.
+ + diff --git a/getting_started/core.html b/getting_started/core.html new file mode 100644 index 000000000..e7d303c49 --- /dev/null +++ b/getting_started/core.html @@ -0,0 +1,573 @@ + + + + + +These are the steps to set up the two crates forming the shared core – the core +itself, and the shared types crate which does type generation for the foreign +languages.
+Most of these steps are going to be automated in future tooling, and published as crates. For now the set up is effectively a copy & paste from one of the example projects.
+This is an example of a
+rust-toolchain.toml
+file, which you can add at the root of your repo. It should ensure that the
+correct rust channel and compile targets are installed automatically for you
+when you use any rust tooling within the repo.
[toolchain]
+channel = "stable"
+components = ["rustfmt", "rustc-dev"]
+targets = [
+ "aarch64-apple-darwin",
+ "aarch64-apple-ios",
+ "aarch64-apple-ios-sim",
+ "aarch64-linux-android",
+ "wasm32-unknown-unknown",
+ "x86_64-apple-ios"
+]
+profile = "minimal"
+
+The first library to create is the one that will be shared across all platforms,
+containing the behavior of the app. You can call it whatever you like, but we
+have chosen the name shared
here. You can create the shared rust library, like
+this:
cargo new --lib shared
+
+We'll be adding a bunch of other folders into the monorepo, so we are choosing
+to use Cargo Workspaces. Edit the workspace /Cargo.toml
file, at the monorepo
+root, to add the new library to our workspace. It should look something like
+this (the package
and dependencies
fields are just examples):
[workspace]
+members = ["shared", "shared_types", "web-dioxus", "web-leptos", "web-yew"]
+resolver = "1"
+
+[workspace.package]
+authors = ["Red Badger Consulting Limited"]
+edition = "2021"
+repository = "https://github.com/redbadger/crux/"
+license = "Apache-2.0"
+keywords = ["crux", "crux_core", "cross-platform-ui", "ffi", "wasm"]
+rust-version = "1.66"
+
+[workspace.dependencies]
+anyhow = "1.0.75"
+serde = "1.0.193"
+
+The library's manifest, at /shared/Cargo.toml
, should look something like the
+following, but there are a few things to note:
crate-type
+lib
is the default rust library when linking into a rust binary, e.g. in
+the web-yew
, or cli
, variantstaticlib
is a static library (libshared.a
) for including in the Swift
+iOS app variantcdylib
is a C-ABI dynamic library (libshared.so
) for use with JNA when
+included in the Kotlin Android app varianttypegen
that depends on the feature with
+the same name in the crux_core
crate. This is used by its sister library
+(often called shared_types
) that will generate types for use across the FFI
+boundary (see the section below on generating shared types).path
fields on the crux dependencies are for the
+examples in the Crux repo
+and so you will probably not need themuniffi-bindgen
target should make sense after
+you read the next section[package]
+name = "shared"
+version = "0.1.0"
+edition = "2021"
+rust-version = "1.66"
+
+[lib]
+crate-type = ["lib", "staticlib", "cdylib"]
+name = "shared"
+
+[features]
+typegen = ["crux_core/typegen"]
+
+[dependencies]
+crux_core = "0.6"
+crux_macros = "0.3"
+serde = { workspace = true, features = ["derive"] }
+lazy_static = "1.4.0"
+uniffi = "0.25.2"
+wasm-bindgen = "0.2.89"
+
+[target.uniffi-bindgen.dependencies]
+uniffi = { version = "0.25.2", features = ["cli"] }
+
+[build-dependencies]
+uniffi = { version = "0.25.2", features = ["build"] }
+
+Crux uses Mozilla's Uniffi to generate +the FFI bindings for iOS and Android.
+uniffi-bindgen
CLI toolSince Mozilla released version 0.23.0
of Uniffi, we need to also generate the
+binary that generates these bindings. This avoids the possibility of getting a
+version mismatch between a separately installed binary and the crate's Uniffi
+version. You can read more about it
+here.
Generating the binary is simple, we just add the following to our crate, in a
+file called /shared/src/bin/uniffi-bindgen.rs
.
fn main() {
+ uniffi::uniffi_bindgen_main()
+}
+And then we can build it with cargo.
+cargo run -p shared --bin uniffi-bindgen
+
+# or
+
+cargo build
+./target/debug/uniffi-bindgen
+
+The uniffi-bindgen
executable will be used during the build in XCode and in
+Android Studio (see the following pages).
We will need an interface definition file for the FFI bindings. Uniffi has its
+own file format (similar to WebIDL) that has a .udl
extension. You can create
+one at /shared/src/shared.udl
, like this:
namespace shared {
+ bytes process_event([ByRef] bytes msg);
+ bytes handle_response([ByRef] bytes uuid, [ByRef] bytes res);
+ bytes view();
+};
+
+There are also a few additional parameters to tell Uniffi how to create bindings
+for Kotlin and Swift. They live in the file /shared/uniffi.toml
, like this
+(feel free to adjust accordingly):
[bindings.kotlin]
+package_name = "com.example.counter.shared"
+cdylib_name = "shared"
+
+[bindings.swift]
+cdylib_name = "shared_ffi"
+omit_argument_labels = true
+
+Finally, we need a build.rs
file in the root of the crate
+(/shared/build.rs
), to generate the bindings:
fn main() {
+ uniffi::generate_scaffolding("./src/shared.udl").unwrap();
+}
+Soon we will have macros and/or code-gen to help with this, but for now, we need
+some scaffolding in /shared/src/lib.rs
. You'll notice that we are re-exporting
+the Request
type and the capabilities we want to use in our native Shells, as
+well as our public types from the shared library.
pub mod app;
+
+use lazy_static::lazy_static;
+use wasm_bindgen::prelude::wasm_bindgen;
+
+pub use crux_core::{bridge::Bridge, Core, Request};
+
+pub use app::*;
+
+// TODO hide this plumbing
+
+uniffi::include_scaffolding!("shared");
+
+lazy_static! {
+ static ref CORE: Bridge<Effect, Counter> = Bridge::new(Core::new::<Capabilities>());
+}
+
+#[wasm_bindgen]
+pub fn process_event(data: &[u8]) -> Vec<u8> {
+ CORE.process_event(data)
+}
+
+#[wasm_bindgen]
+pub fn handle_response(uuid: &[u8], data: &[u8]) -> Vec<u8> {
+ CORE.handle_response(uuid, data)
+}
+
+#[wasm_bindgen]
+pub fn view() -> Vec<u8> {
+ CORE.view()
+}
+Now we are in a position to create a basic app in /shared/src/app.rs
. This is
+from the
+simple Counter example
+(which also has tests, although we're not showing them here):
use crux_core::{render::Render, App};
+use crux_macros::Effect;
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+pub enum Event {
+ Increment,
+ Decrement,
+ Reset,
+}
+
+#[derive(Default)]
+pub struct Model {
+ count: isize,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub struct ViewModel {
+ pub count: String,
+}
+
+#[cfg_attr(feature = "typegen", derive(crux_macros::Export))]
+#[derive(Effect)]
+#[effect(app = "Counter")]
+pub struct Capabilities {
+ render: Render<Event>,
+}
+
+#[derive(Default)]
+pub struct Counter;
+
+impl App for Counter {
+ type Event = Event;
+ type Model = Model;
+ type ViewModel = ViewModel;
+ type Capabilities = Capabilities;
+
+ fn update(&self, event: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities) {
+ match event {
+ Event::Increment => model.count += 1,
+ Event::Decrement => model.count -= 1,
+ Event::Reset => model.count = 0,
+ };
+
+ caps.render.render();
+ }
+
+ fn view(&self, model: &Self::Model) -> Self::ViewModel {
+ ViewModel {
+ count: format!("Count is: {}", model.count),
+ }
+ }
+}
+Make sure everything builds OK
+cargo build
+
+This crate serves as the container for type generation for the foreign +languages.
+Copy over the +shared_types +folder from the counter example.
+Edit the build.rs
file and make sure that your app type is registered. You
+may also need to register any nested enum types (due to a current limitation
+with the reflection library, see
+https://github.com/zefchain/serde-reflection/tree/main/serde-reflection#supported-features).
+Here is an example of this from the
+build.rs
+file in the shared_types
crate of the
+notes example:
use crux_core::typegen::TypeGen;
+use shared::{NoteEditor, TextCursor};
+use std::path::PathBuf;
+
+fn main() {
+ println!("cargo:rerun-if-changed=../shared");
+
+ let mut gen = TypeGen::new();
+
+ gen.register_app::<NoteEditor>().expect("register");
+
+ // Note: currently required as we can't find enums inside enums, see:
+ // https://github.com/zefchain/serde-reflection/tree/main/serde-reflection#supported-features
+ gen.register_type::<TextCursor>().expect("register");
+
+ let output_root = PathBuf::from("./generated");
+
+ gen.swift("SharedTypes", output_root.join("swift"))
+ .expect("swift type gen failed");
+
+ // TODO these are for later
+ //
+ // gen.java("com.example.counter.shared_types", output_root.join("java"))
+ // .expect("java type gen failed");
+
+ gen.typescript("shared_types", output_root.join("typescript"))
+ .expect("typescript type gen failed");
+}
+For the above to compile, your Capabilities
struct must implement the Export
trait. There is a derive macro that can do this for you, e.g.:
#[cfg_attr(feature = "typegen", derive(crux_macros::Export))]
+pub struct Capabilities {
+ pub render: Render<Event>,
+ pub http: Http<Event>,
+}
+Make sure everything builds and foreign types get generated into the
+generated
folder.
cargo build
+
+When we use Crux to build iOS apps, the Core API bindings are generated in Swift +(with C headers) using Mozilla's Uniffi.
+The shared core (that contains our app's behavior) is compiled to a static
+library and linked into the iOS binary. To do this we use
+cargo-xcode
to generate an Xcode
+project for the shared core, which we can include as a sub-project in our iOS
+app.
The shared types are generated by Crux as a Swift package, which we can add to +our iOS project as a dependency. The Swift code to serialize and deserialize +these types across the boundary is also generated by Crux as Swift packages.
+ +This section has two guides for building iOS apps with Crux:
+ +We recommend the first option, as it's definitely the simplest way to get +started.
+ +These are the steps to set up Xcode to build and run a simple iOS app that calls +into a shared core.
+We recommend setting up Xcode with XcodeGen as described in the +previous section. It is the simplest way to create an Xcode +project to build and run a simple iOS app that calls into a shared core. However, +if you want to set up Xcode manually then read on.
+This walk-through assumes you have already added the shared
and shared_types
+libraries to your repo — as described in Shared core and types
+— and that you have built them using cargo build
.
The first thing we need to do is create a new iOS app in Xcode.
+Let's call the app "SimpleCounter" and select "SwiftUI" for the interface and +"Swift" for the language. If you choose to create the app in the root folder of +your monorepo, then you might want to rename the folder it creates to "iOS". +Your repo's directory structure might now look something like this (some files +elided):
+.
+├── Cargo.lock
+├── Cargo.toml
+├── iOS
+│ ├── SimpleCounter
+│ │ ├── ContentView.swift
+│ │ └── SimpleCounterApp.swift
+│ └── SimpleCounter.xcodeproj
+│ └── project.pbxproj
+├── shared
+│ ├── build.rs
+│ ├── Cargo.toml
+│ ├── src
+│ │ ├── counter.rs
+│ │ ├── lib.rs
+│ │ └── shared.udl
+│ └── uniffi.toml
+├── shared_types
+│ ├── build.rs
+│ ├── Cargo.toml
+│ └── src
+│ └── lib.rs
+└── target
+
+We want UniFFI to create the Swift bindings and the C headers for our shared
+library, and store them in a directory called generated
.
To achieve this, we'll associate a script with files that match the pattern
+*.udl
(this will catch the interface definition file we created earlier), and
+then add our shared.udl
file to the project.
Note that our shared library generates the uniffi-bindgen
binary (as explained
+on the page "Shared core and types") that the script relies on, so
+make sure you have built it already, using cargo build
.
In "Build Rules", add a rule to process files that match the pattern *.udl
+with the following script (and also uncheck "Run once per architecture").
#!/bin/bash
+set -e
+
+# Skip during indexing phase in XCode 13+
+if [ "$ACTION" == "indexbuild" ]; then
+ echo "Not building *.udl files during indexing."
+ exit 0
+fi
+
+# Skip for preview builds
+if [ "$ENABLE_PREVIEWS" = "YES" ]; then
+ echo "Not building *.udl files during preview builds."
+ exit 0
+fi
+
+cd "${INPUT_FILE_DIR}/.."
+"${BUILD_DIR}/debug/uniffi-bindgen" generate "src/${INPUT_FILE_NAME}" --language swift --out-dir "${PROJECT_DIR}/generated"
+
+
+We'll need to add the following as output files:
+$(PROJECT_DIR)/generated/$(INPUT_FILE_BASE).swift
+
+$(PROJECT_DIR)/generated/$(INPUT_FILE_BASE)FFI.h
+
+Now go to "Build Phases, Compile Sources", and add /shared/src/shared.udl
+using the "add other" button, selecting "Create folder references".
Build the project (cmd-B), which will fail, but the above script should run +successfully and the "generated" folder should contain the generated Swift types +and C header files:
+$ ls iOS/generated
+shared.swift sharedFFI.h sharedFFI.modulemap
+
+In "Build Settings", search for "bridging header", and add
+generated/sharedFFI.h
, for any architecture/SDK, i.e. in both Debug and
+Release. If there isn't already a setting for "bridging header" you can add one
+(and then delete it) as per
+this StackOverflow question
When we build our iOS app, we also want to build the Rust core as a static +library so that it can be linked into the binary that we're going to ship.
+We will use cargo-xcode
to generate an Xcode project for our shared library, which we can add as a sub-project in Xcode.
Recent changes to cargo-xcode
mean that we need to use version <=1.7.0 for now.
If you don't have this already, you can install it with cargo install --force cargo-xcode --version 1.7.0
.
Let's generate the sub-project:
+cargo xcode
+
+This generates an Xcode project for each crate in the workspace, but we're only
+interested in the one it creates in the shared
directory. Don't open this
+generated project yet.
Using Finder, drag the shared/shared.xcodeproj
folder under the Xcode project
+root.
Then, in the "Build Phases, Link Binary with Libraries" section, add the
+libshared_static.a
library (you should be able to navigate to it as
+Workspace -> shared -> libshared_static.a
)
Using Finder, drag the shared_types/generated/swift/SharedTypes
folder under
+the Xcode project root.
Then, in the "Build Phases, Link Binary with Libraries" section, add the
+SharedTypes
library (you should be able to navigate to it as
+Workspace -> SharedTypes -> SharedTypes
)
There is slightly more advanced +example of an +iOS app in the Crux repository.
+However, we will use the
+simple counter example,
+which has shared
and shared_types
libraries that will work with the
+following example code.
A simple app that increments, decrements and resets a counter.
+First, let's add some boilerplate code to wrap our core and handle the
+capabilities that we are using. For this example, we only need to support the
+Render
capability, which triggers a render of the UI.
This code that wraps the core only needs to be written once — it only grows when +we need to support additional capabilities.
+Edit iOS/SimpleCounter/core.swift
to look like the following. This code sends
+our (UI-generated) events to the core, and handles any effects that the core
+asks for. In this simple example, we aren't calling any HTTP APIs or handling
+any side effects other than rendering the UI, so we just handle this render
+effect by updating the published view model from the core.
import Foundation
+import SharedTypes
+
+@MainActor
+class Core: ObservableObject {
+ @Published var view: ViewModel
+
+ init() {
+ self.view = try! .bincodeDeserialize(input: [UInt8](SimpleCounter.view()))
+ }
+
+ func update(_ event: Event) {
+ let effects = [UInt8](processEvent(Data(try! event.bincodeSerialize())))
+
+ let requests: [Request] = try! .bincodeDeserialize(input: effects)
+ for request in requests {
+ processEffect(request)
+ }
+ }
+
+ func processEffect(_ request: Request) {
+ switch request.effect {
+ case .render:
+ view = try! .bincodeDeserialize(input: [UInt8](SimpleCounter.view()))
+ }
+ }
+}
+
+That switch
statement, above, is where you would handle any other effects that
+your core might ask for. For example, if your core needs to make an HTTP
+request, you would handle that here. To see an example of this, take a look at
+the
+counter example
+in the Crux repository.
Edit iOS/SimpleCounter/ContentView.swift
to look like the following:
import SharedTypes
+import SwiftUI
+
+struct ContentView: View {
+ @ObservedObject var core: Core
+
+ var body: some View {
+ VStack {
+ Image(systemName: "globe")
+ .imageScale(.large)
+ .foregroundColor(.accentColor)
+ Text(core.view.count)
+ HStack {
+ ActionButton(label: "Reset", color: .red) {
+ core.update(.reset)
+ }
+ ActionButton(label: "Inc", color: .green) {
+ core.update(.increment)
+ }
+ ActionButton(label: "Dec", color: .yellow) {
+ core.update(.decrement)
+ }
+ }
+ }
+ }
+}
+
+struct ActionButton: View {
+ var label: String
+ var color: Color
+ var action: () -> Void
+
+ init(label: String, color: Color, action: @escaping () -> Void) {
+ self.label = label
+ self.color = color
+ self.action = action
+ }
+
+ var body: some View {
+ Button(action: action) {
+ Text(label)
+ .fontWeight(.bold)
+ .font(.body)
+ .padding(EdgeInsets(top: 10, leading: 15, bottom: 10, trailing: 15))
+ .background(color)
+ .cornerRadius(10)
+ .foregroundColor(.white)
+ .padding()
+ }
+ }
+}
+
+struct ContentView_Previews: PreviewProvider {
+ static var previews: some View {
+ ContentView(core: Core())
+ }
+}
+
+And create iOS/SimpleCounter/SimpleCounterApp.swift
to look like this:
import SwiftUI
+
+@main
+struct SimpleCounterApp: App {
+ var body: some Scene {
+ WindowGroup {
+ ContentView(core: Core())
+ }
+ }
+}
+
+
+
+ These are the steps to set up Xcode to build and run a simple iOS app that calls +into a shared core.
+We think that using XcodeGen may be the simplest way to create an Xcode project to build and run a simple iOS app that calls into a shared core. If you'd rather set up Xcode manually, you can jump to iOS — Swift and SwiftUI — manual setup, otherwise read on.
+This walk-through assumes you have already added the shared
and shared_types
libraries to your repo — as described in Shared core and types.
When we build our iOS app, we also want to build the Rust core as a static +library so that it can be linked into the binary that we're going to ship.
+We will use cargo-xcode
to generate an Xcode project for our shared library, which we can add as a sub-project in Xcode.
Recent changes to cargo-xcode
mean that we need to use version <=1.7.0 for now.
If you don't have this already, you can install it with cargo install --force cargo-xcode --version 1.7.0
.
Let's generate the sub-project:
+cargo xcode
+
+This generates an Xcode project for each crate in the workspace, but we're only
+interested in the one it creates in the shared
directory. Don't open this
+generated project yet, it'll be included when we generate the Xcode project for
+our iOS app.
We will use XcodeGen
to generate an Xcode project for our iOS app.
If you don't have this already, you can install it with brew install xcodegen
.
Before we generate the Xcode project, we need to create some directories and a
+project.yml
file:
mkdir -p iOS/SimpleCounter
+cd iOS
+touch project.yml
+
+The project.yml
file describes the Xcode project we want to generate. Here's
+one for the SimpleCounter example — you may want to adapt this for your own
+project:
name: SimpleCounter
+projectReferences:
+ Shared:
+ path: ../shared/shared.xcodeproj
+packages:
+ SharedTypes:
+ path: ../shared_types/generated/swift/SharedTypes
+options:
+ bundleIdPrefix: com.example.simple_counter
+attributes:
+ BuildIndependentTargetsInParallel: true
+targets:
+ SimpleCounter:
+ type: application
+ platform: iOS
+ deploymentTarget: "15.0"
+ sources:
+ - SimpleCounter
+ - path: ../shared/src/shared.udl
+ buildPhase: sources
+ dependencies:
+ - target: Shared/uniffi-bindgen-bin
+ - target: Shared/shared-staticlib
+ - package: SharedTypes
+ info:
+ path: SimpleCounter/Info.plist
+ properties:
+ UISupportedInterfaceOrientations:
+ - UIInterfaceOrientationPortrait
+ - UIInterfaceOrientationLandscapeLeft
+ - UIInterfaceOrientationLandscapeRight
+ UILaunchScreen: {}
+ settings:
+ OTHER_LDFLAGS: [-w]
+ SWIFT_OBJC_BRIDGING_HEADER: generated/sharedFFI.h
+ ENABLE_USER_SCRIPT_SANDBOXING: NO
+ buildRules:
+ - name: Generate FFI
+ filePattern: "*.udl"
+ script: |
+ #!/bin/bash
+ set -e
+
+ # Skip during indexing phase in XCode 13+
+ if [ "$ACTION" == "indexbuild" ]; then
+ echo "Not building *.udl files during indexing."
+ exit 0
+ fi
+
+ # Skip for preview builds
+ if [ "$ENABLE_PREVIEWS" = "YES" ]; then
+ echo "Not building *.udl files during preview builds."
+ exit 0
+ fi
+
+ cd "${INPUT_FILE_DIR}/.."
+ "${BUILD_DIR}/debug/uniffi-bindgen" generate "src/${INPUT_FILE_NAME}" --language swift --out-dir "${PROJECT_DIR}/generated"
+ outputFiles:
+ - $(PROJECT_DIR)/generated/$(INPUT_FILE_BASE).swift
+ - $(PROJECT_DIR)/generated/$(INPUT_FILE_BASE)FFI.h
+ runOncePerArchitecture: false
+
+Then we can generate the Xcode project:
+xcodegen
+
+This should create an iOS/SimpleCounter/SimpleCounter.xcodeproj
project file,
+which we can open in Xcode. It should build OK, but we will need to add some
+code!
There is slightly more advanced +example of an +iOS app in the Crux repository.
+However, we will use the
+simple counter example,
+which has shared
and shared_types
libraries that will work with the
+following example code.
A simple app that increments, decrements and resets a counter.
+First, let's add some boilerplate code to wrap our core and handle the
+capabilities that we are using. For this example, we only need to support the
+Render
capability, which triggers a render of the UI.
This code that wraps the core only needs to be written once — it only grows when +we need to support additional capabilities.
+Edit iOS/SimpleCounter/core.swift
to look like the following. This code sends
+our (UI-generated) events to the core, and handles any effects that the core
+asks for. In this simple example, we aren't calling any HTTP APIs or handling
+any side effects other than rendering the UI, so we just handle this render
+effect by updating the published view model from the core.
import Foundation
+import SharedTypes
+
+@MainActor
+class Core: ObservableObject {
+ @Published var view: ViewModel
+
+ init() {
+ self.view = try! .bincodeDeserialize(input: [UInt8](SimpleCounter.view()))
+ }
+
+ func update(_ event: Event) {
+ let effects = [UInt8](processEvent(Data(try! event.bincodeSerialize())))
+
+ let requests: [Request] = try! .bincodeDeserialize(input: effects)
+ for request in requests {
+ processEffect(request)
+ }
+ }
+
+ func processEffect(_ request: Request) {
+ switch request.effect {
+ case .render:
+ view = try! .bincodeDeserialize(input: [UInt8](SimpleCounter.view()))
+ }
+ }
+}
+
+That switch
statement, above, is where you would handle any other effects that
+your core might ask for. For example, if your core needs to make an HTTP
+request, you would handle that here. To see an example of this, take a look at
+the
+counter example
+in the Crux repository.
Edit iOS/SimpleCounter/ContentView.swift
to look like the following:
import SharedTypes
+import SwiftUI
+
+struct ContentView: View {
+ @ObservedObject var core: Core
+
+ var body: some View {
+ VStack {
+ Image(systemName: "globe")
+ .imageScale(.large)
+ .foregroundColor(.accentColor)
+ Text(core.view.count)
+ HStack {
+ ActionButton(label: "Reset", color: .red) {
+ core.update(.reset)
+ }
+ ActionButton(label: "Inc", color: .green) {
+ core.update(.increment)
+ }
+ ActionButton(label: "Dec", color: .yellow) {
+ core.update(.decrement)
+ }
+ }
+ }
+ }
+}
+
+struct ActionButton: View {
+ var label: String
+ var color: Color
+ var action: () -> Void
+
+ init(label: String, color: Color, action: @escaping () -> Void) {
+ self.label = label
+ self.color = color
+ self.action = action
+ }
+
+ var body: some View {
+ Button(action: action) {
+ Text(label)
+ .fontWeight(.bold)
+ .font(.body)
+ .padding(EdgeInsets(top: 10, leading: 15, bottom: 10, trailing: 15))
+ .background(color)
+ .cornerRadius(10)
+ .foregroundColor(.white)
+ .padding()
+ }
+ }
+}
+
+struct ContentView_Previews: PreviewProvider {
+ static var previews: some View {
+ ContentView(core: Core())
+ }
+}
+
+And create iOS/SimpleCounter/SimpleCounterApp.swift
to look like this:
import SwiftUI
+
+@main
+struct SimpleCounterApp: App {
+ var body: some Scene {
+ WindowGroup {
+ ContentView(core: Core())
+ }
+ }
+}
+
+Run xcodegen
again to update the Xcode project with these newly created source
+files (or add them manually in Xcode to the SimpleCounter
group), and then
+open iOS/SimpleCounter/SimpleCounter.xcodeproj
in Xcode. You might need to
+select the SimpleCounter
scheme, and an appropriate simulator, in the
+drop-down at the top, before you build.
Redirecting to... ./iOS/manual.html.
+ + diff --git a/getting_started/ios_with_xcodegen.html b/getting_started/ios_with_xcodegen.html new file mode 100644 index 000000000..00416f1b4 --- /dev/null +++ b/getting_started/ios_with_xcodegen.html @@ -0,0 +1,12 @@ + + + + +Redirecting to... ./iOS/with_xcodegen.html.
+ + diff --git a/getting_started/web/dioxus.html b/getting_started/web/dioxus.html new file mode 100644 index 000000000..7e30c3c12 --- /dev/null +++ b/getting_started/web/dioxus.html @@ -0,0 +1,481 @@ + + + + + +These are the steps to set up and run a simple Rust Web app that calls into a +shared core.
+This walk-through assumes you have already added the shared
and shared_types
libraries to your repo, as described in Shared core and types.
There are many frameworks available for writing Web applications in Rust. We've chosen Dioxus for this walk-through. However, a similar setup would work for other frameworks that compile to WebAssembly.
+Dioxus has a CLI tool called dx
, which can initialize, build and serve our app.
cargo install dioxus-cli
+
+Test that the executable is available.
+dx --help
+
+Before we create a new app, let's add it to our Cargo workspace (so that the
+dx
tool won't complain), by editing the root Cargo.toml
file.
For this example, we'll call the app web-dioxus
.
[workspace]
+members = ["shared", "web-dioxus"]
+
+Now we can create a new Dioxus app. The tool asks for a project name, which
+we'll provide as web-dioxus
.
dx create
+
+cd web-dioxus
+
+Now we can start fleshing out our project. Let's add some dependencies to the
+project's Cargo.toml
.
[package]
+name = "web-dioxus"
+version = "0.1.0"
+authors = ["Stuart Harris <stuart.harris@red-badger.com>"]
+edition = "2021"
+
+[dependencies]
+console_error_panic_hook = "0.1.7"
+dioxus = "0.4"
+dioxus-logger = "0.4.1"
+dioxus-web = "0.4"
+futures-util = "0.3.29"
+log = "0.4.20"
+shared = { path = "../shared" }
+
+There is slightly more advanced example of a Dioxus app in the Crux repository.
+However, we will use the simple counter example, which has shared
and shared_types
libraries that will work with the following example code.
A simple app that increments, decrements and resets a counter.
+First, let's add some boilerplate code to wrap our core and handle the
+capabilities that we are using. For this example, we only need to support the
+Render
capability, which triggers a render of the UI.
This code that wraps the core only needs to be written once — it only grows when +we need to support additional capabilities.
+Edit src/core.rs
to look like the following. This code sends our
+(UI-generated) events to the core, and handles any effects that the core asks
+for. In this simple example, we aren't calling any HTTP APIs or handling any
+side effects other than rendering the UI, so we just handle this render effect
+by updating the component's view
hook with the core's ViewModel.
Also note that because both our core and our shell are written in Rust (and run +in the same memory space), we do not need to serialize and deserialize the data +that we pass between them. We can just pass the data directly.
+use dioxus::prelude::{UnboundedReceiver, UseState};
+use futures_util::StreamExt;
+use std::rc::Rc;
+
+use shared::{Capabilities, Counter, Effect, Event, ViewModel};
+
+pub type Core = Rc<shared::Core<Effect, Counter>>;
+
+pub fn new() -> Core {
+ Rc::new(shared::Core::new::<Capabilities>())
+}
+
+pub async fn core_service(
+ core: &Core,
+ mut rx: UnboundedReceiver<Event>,
+ view: UseState<ViewModel>,
+) {
+ while let Some(event) = rx.next().await {
+ update(core, event, &view);
+ }
+}
+
+pub fn update(core: &Core, event: Event, view: &UseState<ViewModel>) {
+ log::debug!("event: {:?}", event);
+
+ for effect in core.process_event(event) {
+ process_effect(core, effect, view);
+ }
+}
+
+pub fn process_effect(core: &Core, effect: Effect, view: &UseState<ViewModel>) {
+ log::debug!("effect: {:?}", effect);
+
+ match effect {
+ Effect::Render(_) => {
+ view.set(core.view());
+ }
+ };
+}
+That match
statement, above, is where you would handle any other effects that
+your core might ask for. For example, if your core needs to make an HTTP
+request, you would handle that here. To see an example of this, take a look at
+the
+counter example
+in the Crux repository.
Edit src/main.rs
to look like the following. This code sets up the Dioxus app,
+and connects the core to the UI. Not only do we create a hook for the view state
+but we also create a coroutine that plugs in the Dioxus "service" we defined
+above to constantly send any events from the UI to the core.
mod core;
+
+use dioxus::prelude::*;
+use dioxus_web::Config;
+use log::LevelFilter;
+
+use shared::Event;
+
+use crate::core::Core;
+
+fn app(cx: Scope<Core>) -> Element {
+ let core = cx.props;
+ let view = use_state(cx, || core.view());
+ let dispatcher = use_coroutine(cx, |rx| {
+ to_owned![core, view];
+ async move { core::core_service(&core, rx, view).await }
+ });
+
+ render! {
+ main {
+ section { class: "section has-text-centered",
+ p { class: "is-size-5", "{view.count}" }
+ div { class: "buttons section is-centered",
+ button { class:"button is-primary is-danger",
+ onclick: move |_| {
+ dispatcher.send(Event::Reset);
+ },
+ "Reset"
+ }
+ button { class:"button is-primary is-success",
+ onclick: move |_| {
+ dispatcher.send(Event::Increment);
+ },
+ "Increment"
+ }
+ button { class:"button is-primary is-warning",
+ onclick: move |_| {
+ dispatcher.send(Event::Decrement);
+ },
+ "Decrement"
+ }
+ }
+ }
+ }
+ }
+}
+
+fn main() {
+ dioxus_logger::init(LevelFilter::Debug).expect("failed to init logger");
+ console_error_panic_hook::set_once();
+
+ dioxus_web::launch_with_props(app, core::new(), Config::new());
+}
+We can add a title and a stylesheet by editing
+examples/simple_counter/web-dioxus/Dioxus.toml
.
[application]
+name = "web-dioxus"
+default_platform = "web"
+out_dir = "dist"
+asset_dir = "public"
+
+[web.app]
+title = "Crux Simple Counter example"
+
+[web.watcher]
+reload_html = true
+watch_path = ["src", "public"]
+
+[web.resource]
+style = ["https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css"]
+script = []
+
+[web.resource.dev]
+script = []
+
+Now we can build our app and serve it in one simple step.
+dx serve
+
+
+
+ When we use Crux to build Web apps, the shared core is compiled to WebAssembly. +This has the advantage of sandboxing the core, physically preventing it from +performing any side-effects (which is conveniently one of the main goals of Crux +anyway!). The invariants of Crux are actually enforced by the WebAssembly +runtime.
+We do have to decide how much of our app we want to include in the WebAssembly +binary, though. Typically, if we are writing our UI in TypeScript (or +JavaScript) we would just compile our shared behavior and the Crux Core to +WebAssembly. However, if we are writing our UI in Rust we can compile the entire +app to WebAssembly.
+When building UI with React, or any other JS/TS framework, the Core API bindings +are generated in TypeScript using Mozilla's +Uniffi, and, just like with Android and +iOS we must serialize and deserialize the messages into and out of the +WebAssembly binary.
+The shared core (that contains our app's behavior) is compiled to a WebAssembly
+binary, using wasm-pack
, which
+creates an npm package for us that we can add to our project just like any other
+npm package.
The shared types are also generated by Crux as a TypeScript npm package, which
+we can add in the same way (e.g. with pnpm add
).
This section has two guides for building TypeScript UI with Crux:
+ +When building UI with Rust, we can compile the entire app to WebAssembly, and
+reference the core and the shared
crate directly. We do not have to serialize
+and deserialize messages, because the messages stay in the same memory space.
The shared core (that contains our app's behavior) and the UI code are
+compiled to a WebAssembly binary, using the relevant toolchain for the language
+and framework we are using. We use trunk
for the Yew
+and Leptos guides and dx
+for the Dioxus guide.
When using Rust throughout, we can simply use Cargo to add the shared
crate
+directly to our app.
This section has three guides for building Rust UI with Crux:
+ + +These are the steps to set up and run a simple Rust Web app that calls into a +shared core.
+This walk-through assumes you have already added the shared
and shared_types
libraries to your repo, as described in Shared core and types.
There are many frameworks available for writing Web applications in Rust. Here we're choosing Leptos for this walk-through as a way to demonstrate how Crux can work with web frameworks that use fine-grained reactivity rather than the conceptual full re-rendering of React. However, a similar setup would work for other frameworks that compile to WebAssembly.
+Our Leptos app is just a new Rust project, which we can create with Cargo. For
+this example we'll call it web-leptos
.
cargo new web-leptos
+
+We'll also want to add this new project to our Cargo workspace, by editing the
+root Cargo.toml
file.
[workspace]
+members = ["shared", "web-leptos"]
+
+Now we can cd
into the web-leptos
directory and start fleshing out our
+project. Let's add some dependencies to shared/Cargo.toml
.
[package]
+name = "web-leptos"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+leptos = { version = "0.5.3", features = ["csr"] }
+shared = { path = "../shared" }
+
+If using nightly Rust, you can enable the "nightly" feature for Leptos. +When you do this, the signals become functions that can be called directly.
+However in our examples we are using the stable channel and so have to use
+the get()
and update()
functions explicitly.
We'll also need a file called index.html
, to serve our app.
<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Leptos Counter</title>
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
+ </head>
+ </head>
+ <body></body>
+</html>
+
+There is slightly more advanced +example of a +Leptos app in the Crux repository.
+However, we will use the
+simple counter example,
+which has shared
and shared_types
libraries that will work with the
+following example code.
A simple app that increments, decrements and resets a counter.
+First, let's add some boilerplate code to wrap our core and handle the
+capabilities that we are using. For this example, we only need to support the
+Render
capability, which triggers a render of the UI.
This code that wraps the core only needs to be written once — it only grows when +we need to support additional capabilities.
+Edit src/core.rs
to look like the following. This code sends our
+(UI-generated) events to the core, and handles any effects that the core asks
+for. In this simple example, we aren't calling any HTTP APIs or handling any
+side effects other than rendering the UI, so we just handle this render effect
+by sending the new ViewModel to the relevant Leptos signal.
Also note that because both our core and our shell are written in Rust (and run +in the same memory space), we do not need to serialize and deserialize the data +that we pass between them. We can just pass the data directly.
+use std::rc::Rc;
+
+use leptos::{SignalUpdate, WriteSignal};
+use shared::{Capabilities, Counter, Effect, Event, ViewModel};
+
+pub type Core = Rc<shared::Core<Effect, Counter>>;
+
+pub fn new() -> Core {
+ Rc::new(shared::Core::new::<Capabilities>())
+}
+
+pub fn update(core: &Core, event: Event, render: WriteSignal<ViewModel>) {
+ for effect in core.process_event(event) {
+ process_effect(core, effect, render);
+ }
+}
+
+pub fn process_effect(core: &Core, effect: Effect, render: WriteSignal<ViewModel>) {
+ match effect {
+ Effect::Render(_) => {
+ render.update(|view| *view = core.view());
+ }
+ };
+}
+That match
statement, above, is where you would handle any other effects that
+your core might ask for. For example, if your core needs to make an HTTP
+request, you would handle that here. To see an example of this, take a look at
+the
+counter example
+in the Crux repository.
Edit src/main.rs
to look like the following. This code creates two signals
+— one to update the view (which starts off with the core's current view), and
+the other to capture events from the UI (which starts of by sending the reset
+event). We also create an effect that sends these events into the core whenever
+they are raised.
mod core;
+
+use leptos::{component, create_effect, create_signal, view, IntoView, SignalGet, SignalUpdate};
+use shared::Event;
+
+#[component]
+fn RootComponent() -> impl IntoView {
+ let core = core::new();
+ let (view, render) = create_signal(core.view());
+ let (event, set_event) = create_signal(Event::Reset);
+
+ create_effect(move |_| {
+ core::update(&core, event.get(), render);
+ });
+
+ view! {
+ <section class="box container has-text-centered m-5">
+ <p class="is-size-5">{move || view.get().count}</p>
+ <div class="buttons section is-centered">
+ <button class="button is-primary is-danger"
+ on:click=move |_| set_event.update(|value| *value = Event::Reset)
+ >
+ {"Reset"}
+ </button>
+ <button class="button is-primary is-success"
+ on:click=move |_| set_event.update(|value| *value = Event::Increment)
+ >
+ {"Increment"}
+ </button>
+ <button class="button is-primary is-warning"
+ on:click=move |_| set_event.update(|value| *value = Event::Decrement)
+ >
+ {"Decrement"}
+ </button>
+ </div>
+ </section>
+ }
+}
+
+fn main() {
+ leptos::mount_to_body(|| {
+ view! { <RootComponent /> }
+ });
+}
+The easiest way to compile the app to WebAssembly and serve it in our web page
+is to use trunk
, which we can install with
+Homebrew (brew install trunk
) or Cargo
+(cargo install trunk
).
We can build our app, serve it and open it in our browser, in one simple step.
+trunk serve --open
+
+
+
+ These are the steps to set up and run a simple TypeScript Web app that calls +into a shared core.
+This walk-through assumes you have already added the shared
and shared_types
libraries to your repo, as described in Shared core and types.
For this walk-through, we'll use the pnpm
package manager
+for no reason other than we like it the most!
Let's create a simple Next.js app for TypeScript, using pnpx
(from pnpm
).
+You can probably accept the defaults.
pnpx create-next-app@latest
+
+When we build our app, we also want to compile the Rust core to WebAssembly so +that it can be referenced from our code.
+To do this, we'll use
+wasm-pack
, which you can
+install like this:
# with homebrew
+brew install wasm-pack
+
+# or directly
+curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
+
+Now that we have wasm-pack
installed, we can build our shared
library to
+WebAssembly for the browser.
(cd shared && wasm-pack build --target web)
+
+You might want to add a wasm:build
script to your package.json
+file, and call it when you build your nextjs project.
{
+ "scripts": {
+ "build": "pnpm run wasm:build && next build",
+ "dev": "pnpm run wasm:build && next dev",
+ "wasm:build": "cd ../shared && wasm-pack build --target web"
+ }
+}
+
+Add the shared
library as a Wasm package to your web-nextjs
project
cd web-nextjs
+pnpm add ../shared/pkg
+
+To generate the shared types for TypeScript, we can just run cargo build
from
+the root of our repository. You can check that they have been generated
+correctly:
ls --tree shared_types/generated/typescript
+shared_types/generated/typescript
+├── bincode
+│ ├── bincodeDeserializer.d.ts
+│ ├── bincodeDeserializer.js
+│ ├── bincodeDeserializer.ts
+│ ├── bincodeSerializer.d.ts
+│ ├── bincodeSerializer.js
+│ ├── bincodeSerializer.ts
+│ ├── mod.d.ts
+│ ├── mod.js
+│ └── mod.ts
+├── node_modules
+│ └── typescript -> .pnpm/typescript@4.8.4/node_modules/typescript
+├── package.json
+├── pnpm-lock.yaml
+├── serde
+│ ├── binaryDeserializer.d.ts
+│ ├── binaryDeserializer.js
+│ ├── binaryDeserializer.ts
+│ ├── binarySerializer.d.ts
+│ ├── binarySerializer.js
+│ ├── binarySerializer.ts
+│ ├── deserializer.d.ts
+│ ├── deserializer.js
+│ ├── deserializer.ts
+│ ├── mod.d.ts
+│ ├── mod.js
+│ ├── mod.ts
+│ ├── serializer.d.ts
+│ ├── serializer.js
+│ ├── serializer.ts
+│ ├── types.d.ts
+│ ├── types.js
+│ └── types.ts
+├── tsconfig.json
+└── types
+ ├── shared_types.d.ts
+ ├── shared_types.js
+ └── shared_types.ts
+
+You can see that it also generates an npm
package that we can add directly to
+our project.
pnpm add ../shared_types/generated/typescript
+
+There are other, more advanced, examples of Next.js apps in the Crux repository.
+However, we will use the simple counter example, which has shared
and shared_types
libraries that will work with the following example code.
A simple app that increments, decrements and resets a counter.
+First, let's add some boilerplate code to wrap our core and handle the
+capabilities that we are using. For this example, we only need to support the
+Render
capability, which triggers a render of the UI.
This code that wraps the core only needs to be written once — it only grows when +we need to support additional capabilities.
+Edit src/app/core.ts
to look like the following. This code sends our
+(UI-generated) events to the core, and handles any effects that the core asks
+for. In this simple example, we aren't calling any HTTP APIs or handling any
+side effects other than rendering the UI, so we just handle this render effect
+by updating the component's view
hook with the core's ViewModel.
Notice that we have to serialize and deserialize the data that we pass between +the core and the shell. This is because the core is running in a separate +WebAssembly instance, and so we can't just pass the data directly.
+import type { Dispatch, SetStateAction } from "react";
+
+import { process_event, view } from "shared/shared";
+import type { Effect, Event } from "shared_types/types/shared_types";
+import {
+ EffectVariantRender,
+ ViewModel,
+ Request,
+} from "shared_types/types/shared_types";
+import {
+ BincodeSerializer,
+ BincodeDeserializer,
+} from "shared_types/bincode/mod";
+
+export function update(
+ event: Event,
+ callback: Dispatch<SetStateAction<ViewModel>>
+) {
+ console.log("event", event);
+
+ const serializer = new BincodeSerializer();
+ event.serialize(serializer);
+
+ const effects = process_event(serializer.getBytes());
+
+ const requests = deserializeRequests(effects);
+ for (const { uuid, effect } of requests) {
+ processEffect(uuid, effect, callback);
+ }
+}
+
+function processEffect(
+ _uuid: number[],
+ effect: Effect,
+ callback: Dispatch<SetStateAction<ViewModel>>
+) {
+ console.log("effect", effect);
+
+ switch (effect.constructor) {
+ case EffectVariantRender: {
+ callback(deserializeView(view()));
+ break;
+ }
+ }
+}
+
+function deserializeRequests(bytes: Uint8Array): Request[] {
+ const deserializer = new BincodeDeserializer(bytes);
+ const len = deserializer.deserializeLen();
+ const requests: Request[] = [];
+ for (let i = 0; i < len; i++) {
+ const request = Request.deserialize(deserializer);
+ requests.push(request);
+ }
+ return requests;
+}
+
+function deserializeView(bytes: Uint8Array): ViewModel {
+ return ViewModel.deserialize(new BincodeDeserializer(bytes));
+}
+
+That switch
statement, above, is where you would handle any other effects that
+your core might ask for. For example, if your core needs to make an HTTP
+request, you would handle that here. To see an example of this, take a look at
+the
+counter example
+in the Crux repository.
Edit src/app/page.tsx
to look like the following. This code loads the
+WebAssembly core and sends it an initial event. Notice that we pass the
+setState
hook to the update function so that we can update the state in
+response to a render effect from the core.
"use client";
+
+import type { NextPage } from "next";
+import Head from "next/head";
+import { useEffect, useRef, useState } from "react";
+
+import init_core from "shared/shared";
+import {
+ ViewModel,
+ EventVariantReset,
+ EventVariantIncrement,
+ EventVariantDecrement,
+} from "shared_types/types/shared_types";
+
+import { update } from "./core";
+
+const Home: NextPage = () => {
+ const [view, setView] = useState(new ViewModel("0"));
+
+ const initialized = useRef(false);
+ useEffect(
+ () => {
+ if (!initialized.current) {
+ initialized.current = true;
+
+ init_core().then(() => {
+ // Initial event
+ update(new EventVariantReset(), setView);
+ });
+ }
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ /*once*/ []
+ );
+
+ return (
+ <>
+ <Head>
+ <title>Next.js Counter</title>
+ </Head>
+
+ <main>
+ <section className="box container has-text-centered m-5">
+ <p className="is-size-5">{view.count}</p>
+ <div className="buttons section is-centered">
+ <button
+ className="button is-primary is-danger"
+ onClick={() => update(new EventVariantReset(), setView)}
+ >
+ {"Reset"}
+ </button>
+ <button
+ className="button is-primary is-success"
+ onClick={() => update(new EventVariantIncrement(), setView)}
+ >
+ {"Increment"}
+ </button>
+ <button
+ className="button is-primary is-warning"
+ onClick={() => update(new EventVariantDecrement(), setView)}
+ >
+ {"Decrement"}
+ </button>
+ </div>
+ </section>
+ </main>
+ </>
+ );
+};
+
+export default Home;
+
+Now all we need is some CSS. First add the Bulma
package, and then import it
+in layout.tsx
.
pnpm add bulma
+
+import "bulma/css/bulma.css";
+import type { Metadata } from "next";
+import { Inter } from "next/font/google";
+
+const inter = Inter({ subsets: ["latin"] });
+
+export const metadata: Metadata = {
+ title: "Crux Simple Counter Example",
+ description: "Rust Core, TypeScript Shell (NextJS)",
+};
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+ <html lang="en">
+ <body className={inter.className}>{children}</body>
+ </html>
+ );
+}
+
+We can build our app, and serve it for the browser, in one simple step.
+pnpm dev
+
+
+
+ These are the steps to set up and run a simple TypeScript Web app that calls +into a shared core.
+This walk-through assumes you have already added the shared
and shared_types
libraries to your repo, as described in Shared core and types.
For this walk-through, we'll use the pnpm
package manager
+for no reason other than we like it the most! You can use npm
exactly the same
+way, though.
Let's create a simple Remix app for TypeScript, using pnpx
(from pnpm
). You
+can give it a name and then probably accept the defaults.
pnpx create-remix@latest
+
+When we build our app, we also want to compile the Rust core to WebAssembly so +that it can be referenced from our code.
+To do this, we'll use
+wasm-pack
, which you can
+install like this:
# with homebrew
+brew install wasm-pack
+
+# or directly
+curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
+
+Now that we have wasm-pack
installed, we can build our shared
library to
+WebAssembly for the browser.
(cd shared && wasm-pack build --target web)
+
+You might want to add a wasm:build
script to your package.json
+file, and call it when you build your Remix project.
{
+ "scripts": {
+ "build": "pnpm run wasm:build && remix build",
+ "dev": "pnpm run wasm:build && remix dev",
+ "wasm:build": "cd ../shared && wasm-pack build --target web"
+ }
+}
+
+Add the shared
library as a Wasm package to your web-remix
project
cd web-remix
+pnpm add ../shared/pkg
+
+We want to tell the Remix server to bundle our shared
Wasm package, so we need
+to add a serverDependenciesToBundle
key to the object exported in
+remix.config.js
:
/** @type {import('@remix-run/dev').AppConfig} */
+module.exports = {
+ ignoredRouteFiles: ["**/.*"],
+
+ // make sure the server bundles our shared library
+ serverDependenciesToBundle: [/^shared.*/],
+
+ serverModuleFormat: "cjs",
+};
+
+To generate the shared types for TypeScript, we can just run cargo build
from
+the root of our repository. You can check that they have been generated
+correctly:
ls --tree shared_types/generated/typescript
+shared_types/generated/typescript
+├── bincode
+│ ├── bincodeDeserializer.d.ts
+│ ├── bincodeDeserializer.js
+│ ├── bincodeDeserializer.ts
+│ ├── bincodeSerializer.d.ts
+│ ├── bincodeSerializer.js
+│ ├── bincodeSerializer.ts
+│ ├── mod.d.ts
+│ ├── mod.js
+│ └── mod.ts
+├── node_modules
+│ └── typescript -> .pnpm/typescript@4.8.4/node_modules/typescript
+├── package.json
+├── pnpm-lock.yaml
+├── serde
+│ ├── binaryDeserializer.d.ts
+│ ├── binaryDeserializer.js
+│ ├── binaryDeserializer.ts
+│ ├── binarySerializer.d.ts
+│ ├── binarySerializer.js
+│ ├── binarySerializer.ts
+│ ├── deserializer.d.ts
+│ ├── deserializer.js
+│ ├── deserializer.ts
+│ ├── mod.d.ts
+│ ├── mod.js
+│ ├── mod.ts
+│ ├── serializer.d.ts
+│ ├── serializer.js
+│ ├── serializer.ts
+│ ├── types.d.ts
+│ ├── types.js
+│ └── types.ts
+├── tsconfig.json
+└── types
+ ├── shared_types.d.ts
+ ├── shared_types.js
+ └── shared_types.ts
+
+You can see that it also generates an npm
package that we can add directly to
+our project.
pnpm add ../shared_types/generated/typescript
+
+The app/entry.client.tsx
file is where we can load our Wasm binary. We can
+import the shared
package and then call the init
function to load the Wasm
+binary.
Note that we import
the wasm binary as well — Remix will automatically bundle
+it for us, giving it a cache-friendly hash-based name.
/**
+ * By default, Remix will handle hydrating your app on the client for you.
+ * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
+ * For more information, see https://remix.run/file-conventions/entry.client
+ */
+
+import { RemixBrowser } from "@remix-run/react";
+import { startTransition, StrictMode } from "react";
+import { hydrateRoot } from "react-dom/client";
+import init from "shared/shared";
+import wasm from "shared/shared_bg.wasm";
+
+init(wasm).then(() => {
+ startTransition(() => {
+ hydrateRoot(
+ document,
+ <StrictMode>
+ <RemixBrowser />
+ </StrictMode>
+ );
+ });
+});
+
+We will use the simple counter example, which has shared
and shared_types
libraries that will work with the following example code.
A simple app that increments, decrements and resets a counter.
+First, let's add some boilerplate code to wrap our core and handle the
+capabilities that we are using. For this example, we only need to support the
+Render
capability, which triggers a render of the UI.
This code that wraps the core only needs to be written once — it only grows when +we need to support additional capabilities.
+Edit app/core.ts
to look like the following. This code sends our
+(UI-generated) events to the core, and handles any effects that the core asks
+for. In this simple example, we aren't calling any HTTP APIs or handling any
+side effects other than rendering the UI, so we just handle this render effect
+by updating the component's view
hook with the core's ViewModel.
Notice that we have to serialize and deserialize the data that we pass between +the core and the shell. This is because the core is running in a separate +WebAssembly instance, and so we can't just pass the data directly.
+import type { Dispatch, SetStateAction } from "react";
+
+import { process_event, view } from "shared/shared";
+import type { Effect, Event } from "shared_types/types/shared_types";
+import {
+ EffectVariantRender,
+ ViewModel,
+ Request,
+} from "shared_types/types/shared_types";
+import {
+ BincodeSerializer,
+ BincodeDeserializer,
+} from "shared_types/bincode/mod";
+
+export function update(
+ event: Event,
+ callback: Dispatch<SetStateAction<ViewModel>>
+) {
+ console.log("event", event);
+
+ const serializer = new BincodeSerializer();
+ event.serialize(serializer);
+
+ const effects = process_event(serializer.getBytes());
+
+ const requests = deserializeRequests(effects);
+ for (const { uuid, effect } of requests) {
+ processEffect(uuid, effect, callback);
+ }
+}
+
+function processEffect(
+ _uuid: number[],
+ effect: Effect,
+ callback: Dispatch<SetStateAction<ViewModel>>
+) {
+ console.log("effect", effect);
+
+ switch (effect.constructor) {
+ case EffectVariantRender: {
+ callback(deserializeView(view()));
+ break;
+ }
+ }
+}
+
+function deserializeRequests(bytes: Uint8Array): Request[] {
+ const deserializer = new BincodeDeserializer(bytes);
+ const len = deserializer.deserializeLen();
+ const requests: Request[] = [];
+ for (let i = 0; i < len; i++) {
+ const request = Request.deserialize(deserializer);
+ requests.push(request);
+ }
+ return requests;
+}
+
+function deserializeView(bytes: Uint8Array): ViewModel {
+ return ViewModel.deserialize(new BincodeDeserializer(bytes));
+}
+
+That switch
statement, above, is where you would handle any other effects that
+your core might ask for. For example, if your core needs to make an HTTP
+request, you would handle that here. To see an example of this, take a look at
+the
+counter example
+in the Crux repository.
Edit app/routes/_index.tsx
to look like the following. Notice that we pass the
+setState
hook to the update function so that we can update the state in
+response to a render effect from the core (as seen above).
import { useEffect, useRef, useState } from "react";
+
+import {
+ ViewModel,
+ EventVariantReset,
+ EventVariantIncrement,
+ EventVariantDecrement,
+} from "shared_types/types/shared_types";
+import { update } from "../core";
+
+export const meta = () => {
+ return [
+ { title: "New Remix App" },
+ { name: "description", content: "Welcome to Remix!" },
+ ];
+};
+
+export default function Index() {
+ const [view, setView] = useState(new ViewModel("0"));
+
+ const initialized = useRef(false);
+
+ useEffect(
+ () => {
+ if (!initialized.current) {
+ initialized.current = true;
+
+ // Initial event
+ update(new EventVariantReset(), setView);
+ }
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ /*once*/ []
+ );
+
+ return (
+ <main>
+ <section className="box container has-text-centered m-5">
+ <p className="is-size-5">{view.count}</p>
+ <div className="buttons section is-centered">
+ <button
+ className="button is-primary is-danger"
+ onClick={() => update(new EventVariantReset(), setView)}
+ >
+ {"Reset"}
+ </button>
+ <button
+ className="button is-primary is-success"
+ onClick={() => update(new EventVariantIncrement(), setView)}
+ >
+ {"Increment"}
+ </button>
+ <button
+ className="button is-primary is-warning"
+ onClick={() => update(new EventVariantDecrement(), setView)}
+ >
+ {"Decrement"}
+ </button>
+ </div>
+ </section>
+ </main>
+ );
+}
+
+Now all we need is some CSS.
+To add a CSS stylesheet, we can add it to the Links
export in the
+app/root.tsx
file.
export const links: LinksFunction = () => [
+ ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
+ {
+ rel: "stylesheet",
+ href: "https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css",
+ },
+];
+
+We can build our app, and serve it for the browser, in one simple step.
+pnpm dev
+
+
+
+ These are the steps to set up and run a simple TypeScript Web app that calls +into a shared core.
+This walk-through assumes you have already added the shared
and shared_types
libraries to your repo, as described in Shared core and types.
Let's create a new project which we'll call web-svelte
:
mkdir web-svelte
+cd web-svelte
+mkdir src/
+
+When we build our app, we also want to compile the Rust core to WebAssembly so +that it can be referenced from our code.
+To do this, we'll use
+wasm-pack
, which you can
+install like this:
# with homebrew
+brew install wasm-pack
+
+# or directly
+curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
+
+Now that we have wasm-pack
installed, we can build our shared
library to
+WebAssembly for the browser.
(cd shared && wasm-pack build --target web)
+
+Create a package.json
file and add the wasm:build
script:
"scripts": {
+ "wasm:build": "cd ../shared && wasm-pack build --target web",
+ "start": "npm run build && concurrently -k \"parcel serve src/index.html --port 8080 --hmr-port 1174\" ",
+ "build": "pnpm run wasm:build && parcel build src/index.html",
+ "dev": "pnpm run wasm:build && parcel build src/index.html"
+ },
+
+Also make sure to add the shared
and shared_types
as local dependencies to the package.json
:
"dependencies": {
+ // ...
+ "shared": "file:../shared/pkg",
+ "shared_types": "file:../shared_types/generated/typescript"
+ // ...
+ }
+
+Create a main.ts
file in src/
:
import "reflect-metadata";
+
+import App from "./App.svelte";
+
+document.body.setAttribute("data-app-container", "");
+
+export default new App({ target: document.body });
+
+This file is the main entry point which instantiates a new App
object.
+The App
object is defined in the App.svelte
file:
<script lang="ts">
+ import "bulma/css/bulma.css";
+ import { onMount } from "svelte";
+ import { update } from "./core";
+ import view from "./core";
+ import {
+ EventVariantReset,
+ EventVariantIncrement,
+ EventVariantDecrement,
+ } from "shared_types/types/shared_types";
+
+ onMount(async () => {
+ console.log("mount");
+ });
+</script>
+
+<section class="box container has-text-centered m-5">
+ <p class="is-size-5">{$view.count}</p>
+ <div class="buttons section is-centered">
+ <button
+ class="button is-primary is-danger"
+ on:click={() => update(new EventVariantReset())}
+ >
+ {"Reset"}
+ </button>
+ <button
+ class="button is-primary is-success"
+ on:click={() => update(new EventVariantIncrement())}
+ >
+ {"Increment"}
+ </button>
+ <button
+ class="button is-primary is-warning"
+ on:click={() => update(new EventVariantDecrement())}
+ >
+ {"Decrement"}
+ </button>
+ </div>
+</section>
+
+This file implements the UI and the behaviour for various user actions.
+In order to serve the Svelte app, create a index.html
in src/
:
<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5" />
+ <title>Simple Counter</title>
+ <meta name="apple-mobile-web-app-title" content="Simple Counter" />
+ <meta name="application-name" content="Simple Counter" />
+</head>
+<body>
+ <script type="module" src="main.ts"></script>
+</body>
+</html>
+
+This file ensures that the main entry point gets called.
+Let's add a file src/core.ts
which will wrap our core and handle the
+capabilities that we are using.
+import { process_event, view } from "shared";
+import initCore from "shared";
+import { writable } from 'svelte/store';
+import { EffectVariantRender,
+ ViewModel,
+ Request, } from "shared_types/types/shared_types";
+import type { Effect, Event } from "shared_types/types/shared_types";
+import {
+ BincodeSerializer,
+ BincodeDeserializer,
+} from "shared_types/bincode/mod";
+
+const { subscribe, set } = writable(new ViewModel("0"));
+
+export async function update(
+ event: Event
+) {
+ console.log("event", event);
+ await initCore();
+
+ const serializer = new BincodeSerializer();
+ event.serialize(serializer);
+
+ const effects = process_event(serializer.getBytes());
+ const requests = deserializeRequests(effects);
+ for (const { uuid, effect } of requests) {
+ processEffect(uuid, effect);
+ }
+}
+
+function processEffect(
+ _uuid: number[],
+ effect: Effect
+) {
+ console.log("effect", effect);
+ switch (effect.constructor) {
+ case EffectVariantRender: {
+ set(deserializeView(view()));
+ break;
+ }
+ }
+}
+
+function deserializeRequests(bytes: Uint8Array): Request[] {
+ const deserializer = new BincodeDeserializer(bytes);
+ const len = deserializer.deserializeLen();
+ const requests: Request[] = [];
+ for (let i = 0; i < len; i++) {
+ const request = Request.deserialize(deserializer);
+ requests.push(request);
+ }
+ return requests;
+}
+
+function deserializeView(bytes: Uint8Array): ViewModel {
+ return ViewModel.deserialize(new BincodeDeserializer(bytes));
+}
+
+export default {
+ subscribe
+}
+
+This code sends our (UI-generated) events to the core, and handles any effects that the core asks
+for via the update()
function. Notice that we are creating a store
+to update and manage the view model. Whenever update()
gets called to send an event to the core, we are
+fetching the updated view model via view()
and are udpating the value in the store. Svelte components can
+import and use the store values.
Notice that we have to serialize and deserialize the data that we pass between +the core and the shell. This is because the core is running in a separate +WebAssembly instance, and so we can't just pass the data directly.
+We can build our app, and serve it for the browser, in one simple step.
+npm start
+
+
+
+ These are the steps to set up and run a simple Rust Web app that calls into a +shared core.
+This walk-through assumes you have already added the shared
and shared_types
libraries to your repo, as described in Shared core and types.
There are many frameworks available for writing Web applications in Rust. We've chosen Yew for this walk-through because it is arguably the most mature. However, a similar setup would work for any framework that compiles to WebAssembly.
+Our Yew app is just a new Rust project, which we can create with Cargo. For this
+example we'll call it web-yew
.
cargo new web-yew
+
+We'll also want to add this new project to our Cargo workspace, by editing the
+root Cargo.toml
file.
[workspace]
+members = ["shared", "web-yew"]
+
+Now we can start fleshing out our project. Let's add some dependencies to
+web-yew/Cargo.toml
.
[package]
+name = "web-yew"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+shared = { path = "../shared" }
+yew = { version = "0.21.0", features = ["csr"] }
+
+We'll also need a file called index.html
, to serve our app.
<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Yew Counter</title>
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
+ </head>
+</html>
+
+There are several, more advanced, +examples of Yew apps +in the Crux repository.
+However, we will use the
+simple counter example,
+which has shared
and shared_types
libraries that will work with the
+following example code.
A simple app that increments, decrements and resets a counter.
+First, let's add some boilerplate code to wrap our core and handle the
+capabilities that we are using. For this example, we only need to support the
+Render
capability, which triggers a render of the UI.
This code that wraps the core only needs to be written once — it only grows when +we need to support additional capabilities.
+Edit src/core.rs
to look like the following. This code sends our
+(UI-generated) events to the core, and handles any effects that the core asks
+for. In this simple example, we aren't calling any HTTP APIs or handling any
+side effects other than rendering the UI, so we just handle this render effect
+by sending it directly back to the Yew component. Note that we wrap the effect
+in a Message enum because Yew components have a single associated type for
+messages and we need that to include both the events that the UI raises (to send
+to the core) and the effects that the core uses to request side effects from the
+shell.
Also note that because both our core and our shell are written in Rust (and run +in the same memory space), we do not need to serialize and deserialize the data +that we pass between them. We can just pass the data directly.
+use shared::{Capabilities, Counter, Effect, Event};
+use std::rc::Rc;
+use yew::Callback;
+
+pub type Core = Rc<shared::Core<Effect, Counter>>;
+
+pub enum Message {
+ Event(Event),
+ Effect(Effect),
+}
+
+pub fn new() -> Core {
+ Rc::new(shared::Core::new::<Capabilities>())
+}
+
+pub fn update(core: &Core, event: Event, callback: &Callback<Message>) {
+ for effect in core.process_event(event) {
+ process_effect(core, effect, callback);
+ }
+}
+
+pub fn process_effect(_core: &Core, effect: Effect, callback: &Callback<Message>) {
+ match effect {
+ render @ Effect::Render(_) => callback.emit(Message::Effect(render)),
+ }
+}
+That match
statement, above, is where you would handle any other effects that
+your core might ask for. For example, if your core needs to make an HTTP
+request, you would handle that here. To see an example of this, take a look at
+the
+counter example
+in the Crux repository.
Edit src/main.rs
to look like the following. The update
function is
+interesting here. We set up a Callback
to receive messages from the core and
+feed them back into Yew's event loop. Then we test to see if the incoming
+message is an Event
(raised by UI interaction) and if so we use it to update
+the core, returning false to indicate that the re-render will happen later. In
+this app, we can assume that any other message is a render Effect
and so we
+return true indicating to Yew that we do want to re-render.
mod core;
+
+use crate::core::{Core, Message};
+use shared::Event;
+use yew::prelude::*;
+
+#[derive(Default)]
+struct RootComponent {
+ core: Core,
+}
+
+impl Component for RootComponent {
+ type Message = Message;
+ type Properties = ();
+
+ fn create(_ctx: &Context<Self>) -> Self {
+ Self { core: core::new() }
+ }
+
+ fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
+ let link = ctx.link().clone();
+ let callback = Callback::from(move |msg| {
+ link.send_message(msg);
+ });
+ if let Message::Event(event) = msg {
+ core::update(&self.core, event, &callback);
+ false
+ } else {
+ true
+ }
+ }
+
+ fn view(&self, ctx: &Context<Self>) -> Html {
+ let link = ctx.link();
+ let view = self.core.view();
+
+ html! {
+ <section class="box container has-text-centered m-5">
+ <p class="is-size-5">{&view.count}</p>
+ <div class="buttons section is-centered">
+ <button class="button is-primary is-danger"
+ onclick={link.callback(|_| Message::Event(Event::Reset))}>
+ {"Reset"}
+ </button>
+ <button class="button is-primary is-success"
+ onclick={link.callback(|_| Message::Event(Event::Increment))}>
+ {"Increment"}
+ </button>
+ <button class="button is-primary is-warning"
+ onclick={link.callback(|_| Message::Event(Event::Decrement))}>
+ {"Decrement"}
+ </button>
+ </div>
+ </section>
+ }
+ }
+}
+
+fn main() {
+ yew::Renderer::<RootComponent>::new().render();
+}
+The easiest way to compile the app to WebAssembly and serve it in our web page
+is to use trunk
, which we can install with
+Homebrew (brew install trunk
) or Cargo
+(cargo install trunk
).
We can build our app, serve it and open it in our browser, in one simple step.
+trunk serve --open
+
+
+
+ Redirecting to... ./Web/leptos.html.
+ + diff --git a/getting_started/web_react.html b/getting_started/web_react.html new file mode 100644 index 000000000..95f0f46a5 --- /dev/null +++ b/getting_started/web_react.html @@ -0,0 +1,12 @@ + + + + +Redirecting to... ./Web/nextjs.html.
+ + diff --git a/getting_started/web_remix.html b/getting_started/web_remix.html new file mode 100644 index 000000000..64e3e24df --- /dev/null +++ b/getting_started/web_remix.html @@ -0,0 +1,12 @@ + + + + +Redirecting to... ./Web/remix.html.
+ + diff --git a/getting_started/web_svelte.html b/getting_started/web_svelte.html new file mode 100644 index 000000000..bfc70469a --- /dev/null +++ b/getting_started/web_svelte.html @@ -0,0 +1,12 @@ + + + + +Redirecting to... ./Web/svelte.html.
+ + diff --git a/getting_started/web_yew.html b/getting_started/web_yew.html new file mode 100644 index 000000000..ec76b1a79 --- /dev/null +++ b/getting_started/web_yew.html @@ -0,0 +1,12 @@ + + + + +Redirecting to... ./Web/yew.html.
+ + diff --git a/guide/capabilities.html b/guide/capabilities.html new file mode 100644 index 000000000..14900b611 --- /dev/null +++ b/guide/capabilities.html @@ -0,0 +1,293 @@ + + + + + +In the last chapter, we spoke about Effects. In this one we'll look at the APIs your app will actually use to request them – the capabilities.
+Capabilities are reusable, platform agnostic APIs for a particular type of effect. They have two key jobs:
+From the perspective of the app, you can think of capabilities as an equivalent to SDKs. And a lot of them will provide an interface to the actual platform specific SDKs.
+The Capabilities are the key to Crux being portable across as many platforms as is sensible. Crux apps are, in a sense, built in the abstract, they describe what should happen in response to events, but not how it should happen. We think this is important both for portability, and for testing and general separation of concerns. What should happen is inherent to the product, and should behave the same way on any platform – it's part of what your app is. How it should be executed (and exactly what it looks like) often depends on the platform.
+Different platforms may support different ways, for example a biometric authentication may work very differently on various devices and some may not even support it at all, but it may also be a matter of convention. Different platforms may also have different practical restrictions: while it may be perfectly appropriate to write things to disk on one platform, but internet access can't be guaranteed (e.g. on a smart watch), on another, writing to disk may not be possible, but internet connection is virtually guaranteed (e.g. in an API service, or on an embedded device in a factory). A persistent caching capability would implement the specific storage solution differently on different platforms, but would potentially share the key format and eviction strategy across them. The hard part of designing a capability is working out exactly where to draw the line between what is the intent and what is the implementation detail, what's common across platforms and what may be different on each, and implementing the former in Rust in the capability and the latter on the native side in the Shell, however is appropriate.
+Because Capabilities can own the "language" used to express intent, and the interface to request the execution of the effect, your Crux application code can be portable onto any platform capable of executing the effect in some way. Clearly, the number of different effects we can think of, and platforms we can target is enormous, and Crux doesn't want to force you to implement the entire portfolio of them on every platform. That's why Capabilities are delivered as separate modules, typically in crates, and apps can declare which ones they need. The Shell implementations need to know how to handle all requests from those capabilities, but can choose to provide only stub implementations where appropriate. For example the Cat Facts example, uses a key-value store capability for persisting the model after every interaction, which is crucial to make the CLI shell work statefully, but the other shells generally ignore the key-value requests, because state persistence across app launches is not crucial for them. The app itself (the Core) has no idea which is the case.
+In some cases, it may also make sense to implement an app-specific capability, for effects specific to your domain, which don't have a common implementation across platforms (e.g. registering a local user). Crux does not stop you from bundling a number of capabilities alongside your apps (i.e. they don't have to come from a crate). On the other hand, it might make sense to build a capability on top of an existing lower-level capability, for example a CRDT capability may use a general pub/sub capability as transport, or a specific protocol to speak to your synchronization server (e.g. over HTTP).
+There are clearly numerous scenarios, and the best rule of thumb we can think of is "focus on the intent". Provide an API to describe the intent of side-effects and then either pass the intent straight to the shell, or translate it to a sequence of more concrete intents for the Shell to execute. And keep in mind that the more complex the intent sent to the shell, the more complex the implementation on each platform. The translation between high-level intent and low level building blocks is why Capabilities exist.
+As we've already covered, the capabilities effectively straddle the FFI boundary between the Core and the Shell. On the Core side they mediate between the FFI boundary and the application code. On the shell-side the requests produced by the capability need to be actually executed and fulfilled. Each capability therefore extends the Core/Shell interface with a set of defined (and type checked) messages, in a way that allows Crux to leverage exhaustive pattern matching on the native side to ensure all necessary capabilities required by the Core are implemented.
+At the moment the Shell implementation is up to you, but we think in the future it's likely that capability crates will come with platform native code as well, making building both the Core and the Shells easier, and allow you to focus on application behaviour in the Core and look and feel in the Shell.
+Okay, time to get practical. We'll look at what it takes (and why) to use a capability, and in the next couple of chapters, we'll continue to build one and implement the Shell side of it.
+Firstly, we need to have access to an instance of the capability in our update
function. Recall that the function signature is:
fn update(&self, msg: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities)
+We get the capabilities in the caps
argument. You may be wondering why that's necessary. At first glance, we could be able to just create a capability instance ourselves, or not need one at all, after all they just provide API to make effects. There are a few reasons.
Firstly, capabilities need to be able to send a message to the shell, more precisely, they need to be able to add to the set of effects which result from the run of the update function. Sounds like a return value to you? It kind of is, and we tried that, and the type signatures involved quickly become quite unsightly. It's not the only reason though. They also need to be able to return information back to your app by queuing up events to be dispatched to the next run of the update
function. But to be really useful, they need to be able to do a series of these things and suspend their execution in the meantime.
In order to enable all that, Crux needs to be in charge of creating the instance of the capabilities to provide context to them, which they use to do the things we just listed. We'll see the details of this in the next chapter.
+Notice that the type of the argument is Self::Capabilities
— you own the type. This is to allow you to declare which capabilities you want to use in your app. That type will most likely be a struct looking like the following:
#[derive(Effect)]
+pub struct Capabilities {
+ pub http: Http<Event>,
+ pub render: Render<Event>,
+}
+Those two types come from crux_core
and crux_http
. Two things are suspicious about the above — the Event
type, which describes your app's events and the #[derive(Effect)]
derive macro.
The latter generates an Effect
enum for you, used as the payload of the messages to the Shell. It is one of the things you will need to expose via the FFI boundary. It's the type the Shell will use to understand what is being requested from it, and it mirrors the Capabilities
struct: for each field, there is a tuple variant in the Effect enum, with the respective capability's request as payload, i.e. the data describing what's being asked of the Shell.
The Event
type argument enables the "shell side" of these capabilities to send you your specific events back as the outcome of their work. Typically, you'd probably set up an Event
variant specifically for the individual uses of each capability, like this:
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
+pub enum Event {
+ Hello,
+ #[serde(skip)]
+ Set(crux_http::Result<crux_http::Response<Counter>>), // <- this
+}
+In a real app, you'd likely have more than one interaction with a HTTP server, and would most likely need one variant for each. (#[serde(skip)]
in the above code hides the variant from the type exposed to the Shell for direct calls – this event should not be dispatched directly. The other reason for it also has to do with serialization difficulties, which we'll eventually iron out).
That's it for linking the capability into our app, now we can use it in the update
function:
fn update(&self, msg: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities) {
+ match msg {
+ Event::Get => {
+ caps.http
+ .get(API_URL)
+ .expect_json::<Counter>()
+ .send(Event::Set);
+
+ caps.render.render();
+ }
+ // ...
+You can see the use of the Event::Set
variant we just discussed. Event::Set
is technically a function with this signature:
fn Event::Set(crux_http::Result<crux_http::Response<Counter>) -> Event
+Looks a lot like a callback, doesn't it. Yep. With the difference that the result is an Event
. Generally, you should be able to completely ignore this detail and just use your variant names and the code should read pretty clearly: "When done, send me Event::Set
".
The other nuance to be aware of is that the capability calls return immediately. This should hopefully be relatively obvious by now, but all that's happening is effects are getting queued up to be requested from the Shell. In a way, capability calls are implicitly asynchronous (but you can't await them).
+That's generally all there is to it. What you'll notice is that most capabilities have essentially request/response semantics — you use their APIs, and provide an event you want back, and eventually your update function will get called with that event. Most capabilities take inputs for their effect, and return output in their outcomes, but some capabilities don't do one or either of those things. Render is an example of a capability which doesn't take payload and never calls back. You'll likely see all the different variations in Crux apps.
+Now that we know how to use capabilities, we're ready to look at building our own ones. You may never need to do that, or it might be one of the first hurdles you'll come across (and if we're honest, given how young Crux is, it's more likely the latter). Either way, it's what we'll do in the next chapter.
+ +In the previous chapter, we looked at the purpose of Capabilities and using them +in Crux apps. In this one, we'll go through building our own. It will be a +simple one, but real enough to show the key parts.
+We'll extend the Counter example we've built in the +Hello World chapter and make it worse. Intentionally. We'll +add a random delay before we actually update the counter, just to annoy the user +(please don't do that in your real apps). It is a silly example, but it will +allow us to demonstrate a few things:
+In fact, let's start with that.
+The first job of our capability will be to pause for a given number of +milliseconds and then send an event to the app.
+There's a number of types and traits we will need to implement to make the +capability work with the rest of Crux, so let's quickly go over them before we +start. We will need
+Capability
traitLet's start with the payload:
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+pub struct DelayOperation {
+ millis: usize
+}
+The request is just a named type holding onto a number. It will need to cross +the FFI boundary, which is why it needs to be serializable, cloneable, etc.
+We will need our request to implement the Operation
trait, which links it with
+the type of the response we expect back. In our case we expect a response, but
+there is no data, so we'll use the unit type.
use crux_core::capability::Operation;
+
+impl Operation for DelayOperation {
+ type Output = ();
+}
+Now we can implement the capability:
+use crux_core::capability::CapabilityContext;
+use crux_macros::Capability;
+
+#[derive(Capability)]
+struct Delay<Ev> {
+ context: CapabilityContext<DelayOperation, Ev>,
+}
+
+impl<Ev> Delay<Ev>
+where
+ Ev: 'static,
+{
+ pub fn new(context: CapabilityContext<DelayOperation, Ev>) -> Self {
+ Self { context }
+ }
+
+ pub fn milliseconds(&self, millis: usize, event: Ev)
+ where
+ Ev: Send,
+ {
+ let ctx = self.context.clone();
+ self.context.spawn(async move {
+ ctx.request_from_shell(DelayOperation { millis }).await;
+
+ ctx.update_app(event);
+ });
+ }
+}
+There's a fair bit going on. The capability is generic over an event type Ev
+and holds on to a CapabilityContext
. The constructor will be called by Crux
+when starting an application that uses this capability.
The milliseconds
method is our capability's public API. It takes the delay in
+milliseconds and the event to send back. In this case, we don't expect any
+payload to return, so we take the Ev
type directly. We'll shortly see what an
+event with data looks like as well.
The implementation of the method has a little bit of boilerplate to enable us to
+use async
code. First we clone the context to be able to use it in the async
+block. Then we use the context to spawn an async move
code block in which
+we'll be able to use async
/await
. This bit of code will be the same in every
+part of your capability that needs to interact with the Shell.
You can see we use two APIs to orchestrate the interaction. First
+request_from_shell
sends the delay operation we made earlier to the Shell.
+This call returns a future, which we can .await
. Once done, we use the other
+API update_app
to dispatch the event we were given. At the .await
, the task
+will be suspended, Crux will pass the operation to the Shell wrapped in the
+Effect
type we talked about in the last chapter and the Shell will use it's
+native APIs to wait for the given duration, and eventually respond. This will
+wake our task up again and we can continue working.
Finally, we need to implement the Capability
trait. This is done for us by the
+#[derive(Capability)]
macro, but it is worth looking at the generated code:
impl<Ev> Capability<Ev> for Delay<Ev> {
+ type Operation = DelayOperation;
+ type MappedSelf<MappedEv> = Delay<MappedEv>;
+
+ fn map_event<F, NewEv>(&self, f: F) -> Self::MappedSelf<NewEv>
+ where
+ F: Fn(NewEv) -> Ev + Send + Sync + Copy + 'static,
+ Ev: 'static,
+ NewEv: 'static,
+ {
+ Delay::new(self.context.map_event(f))
+ }
+}
+What on earth is that for, you ask? This allows you to derive an instance of the
+Delay
capability from an existing one and adapt it to a different Event
+type. Yes, we know, don't read that sentence again. This will be useful to allow
+composing Crux apps from smaller Crux apps to automatically wrap the child
+events in the parent events.
We will cover this in depth in the chapter about +Composable applications.
+To make the example more contrived, but also more educational, we'll add the +random delay ability. This will
+First off, we need to add the new operation in. Here we have a choice, we can +add a random delay operation, or we can add a random number generation operation +and compose the two building blocks ourselves. We'll go for the second option +because... well because this is an example.
+Since we have multiple operations now, let's make our operation an enum
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+pub enum DelayOperation {
+ GetRandom(usize, usize),
+ Delay(usize),
+}
+We now also need an output type:
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+pub enum DelayOutput {
+ Random(usize),
+ TimeUp
+}
+And that changes the Operation
trait implementation:
impl Operation for DelayOperation {
+ type Output = DelayOutput;
+}
+The updated implementation looks like the following:
+impl<Ev> Delay<Ev>
+where
+ Ev: 'static,
+{
+ pub fn new(context: CapabilityContext<DelayOperation, Ev>) -> Self {
+ Self { context }
+ }
+
+ pub fn milliseconds(&self, millis: usize, event: Ev) {
+ let ctx = self.context.clone();
+ self.context.spawn(async move {
+ ctx.request_from_shell(DelayOperation::Delay(millis)).await; // Changed
+
+ ctx.update_app(event);
+ });
+ }
+
+ pub fn random<F>(&self, min: usize, max: usize, event: F)
+ where F: Fn(usize) -> Ev
+ {
+ let ctx = self.context.clone();
+ self.context.spawn(async move {
+ let response = ctx.request_from_shell(DelayOperation::GetRandom(min, max)).await;
+
+ let DelayOutput::Random(millis) = response else {
+ panic!("Expected a random number")
+ };
+ ctx.request_from_shell(DelayOperation::Delay(millis)).await;
+
+ ctx.update_app(event(millis));
+ });
+ }
+}
+In the new API, the event handling is a little different from the original.
+Because the event has a payload, we don't simply take an Ev
, we need a
+function that returns Ev
, if given the random number. Seems cumbersome but
+you'll see using it in the update
function of our app is quite natural:
fn update(&self, event: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities) {
+ match event {
+ //
+ // ... Some events omitted
+ //
+ Event::Increment => {
+ caps.delay.random(200, 800, Event::DoIncrement);
+ }
+ Event::DoIncrement(_millis) => {
+ // optimistic update
+ model.count.value += 1;
+ model.confirmed = Some(false);
+ caps.render.render();
+
+ // real update
+ let base = Url::parse(API_URL).unwrap();
+ let url = base.join("/inc").unwrap();
+ caps.http.post(url.as_str()).expect_json().send(Event::Set);
+ }
+ Event::Decrement => {
+ caps.delay.milliseconds(500, Event::DoIncrement);
+ }
+ Event::DoDecrement => {
+ // optimistic update
+ model.count.value -= 1;
+ model.confirmed = Some(false);
+ caps.render.render();
+
+ // real update
+ let base = Url::parse(API_URL).unwrap();
+ let url = base.join("/dec").unwrap();
+ caps.http.post(url.as_str()).expect_json().send(Event::Set);
+ }
+ }
+}
+That is essentially it for the capabilities. You can check out the complete +context API +in the docs.
+ +Now we've had a bit of a feel for what writing Crux apps is like, we'll add more context to the different components and the overall architecture of Crux apps. The architecture is heavily inspired by Elm, and if you'd like to compare, the Architecture page of their guide is an excellent starting point.
+User Interface is fundamentally event-driven. Unlike batch or stream processing, all changes in apps with UI are driven by events happening in the outside world, most commonly the user interface itself – the user touching the screen, typing on a keyboard, executing a CLI command, etc. In response, the app changes what's shown on the screen, starts an interaction with the outside world, or both.
+The Elm architecture is the simplest way of modeling this pattern in code. User interactions (along with other changes in the outside world, such as time passing) are represented by events, and in response to them, the app updates its internal state represented by a model. The link between them is a simple, pure function which takes the model and the event, and updates the model based on the events. The actual UI on screen is a direct projection of the model. Because there is virtually no other state in the app, the model must contain enough information to decide what should be on screen.
+What we're missing is for the app to be able to respond to events from the outside world by changing the outside world. While the app can run computations and keep state, in this simplistic model, it can't read or write files, draw on screen, connect to APIs over the network, etc. It can't perform side-effects. Conceptually, we need to extend the update function to not only mutate the model, but also to emit some side-effects (or just "effects" for short).
+ +TODO a better picture focusing on the update function
+This more complete model is a function which takes an event and a model, and produces a new model and optionally some effects. This is still quite a simple and pure function, and is completely predictable, for the same inputs, it will always yield the same outputs, and that is a very important design choice.
+User interface and effects are normally where testing gets very difficult. If the application logic can directly cause changes in the outside world (or input/output — I/O, in computer parlance), the only way to verify the logic completely is to look at the result of those changes. The results, however, are pixels on screen, elements in the DOM, packets going over the network and other complex, difficult to inspect and often short-lived things. The only viable strategy (in this direct scenario) to test them is to take the role of the particular device the app is working with, and pretending to be that device – a practice known as mocking (or stubbing, or faking, depending who you talk to). The APIs used to interact with these things are really complicated though, and even if you emulate them well, tests based on this approach won't be stable against changes in that API. When the API changes, your code and your tests will both have to change, taking any confidence they gave you in the first place with them. What's more, they also differ across platforms. Now we have that problem twice or more times.
+The problem is in how apps are normally written (when written in a direct, imperative style). When it comes time to perform an effect, the most straightforward code just performs it straight away. The solution, as usual, is to add indirection. What Crux does (inspired by Elm, Haskell and others) is separate the intent from the execution. Crux's effect approach focuses on capturing the intent of the effect, not the specific implementation of executing it. The intent is captured as data to benefit from type checking and from all the tools the language already provides for working with data. The business logic can stay pure, but express all the behaviour: state changes and effects. The intent is also the thing that needs to be tested. We can reasonably afford to trust that the authors of a HTTP client library, for example, have tested it and it does what it promises to do — all we need to check is that we're sending the right requests1.
+In Elm, the responsibility to execute the requested effects falls on the Elm runtime. Crux is very similar, except both the app and (some of) the runtime is your responsibility. This means some more work, but it also means you only bring what you need and nothing more, both in terms of supported platforms and the necessary APIs.
+In Crux, business logic written in Rust is captured in the update function mentioned above and the other pieces that the function needs: events, model and effects, each represented by a type. This code forms a Core, which is portable, and really easily testable.
+The execution of effects, including drawing the user interface, is done in a native Shell. Its job is to draw the appropriate UI on screen, translate user interactions into events to send to the Core, and when requested, perform effects and return their outcomes back to the Core.
+ +The Shell thus has two sides: the driving side – the interactions causing events which push the Core to action, and the driven side, which services the Core's requests for side effects. Without being prompted by the Shell, the Core does nothing, it can't – with no other I/O, there are no other triggers which could cause the Core code to run. To the Shell, the Core is a simple library, providing some computation. From the perspective of the Core, the Shell is a platform the Core runs on.
+Effects encode potentially quite complex, but common interactions, so they are the perfect candidate for some improved ergonomics in the APIs. This is where Crux capabilities come in. They provide a nicer API for creating effects, and in the future, they will likely provide implementations of the effect execution for the various supported platforms. Capabilities can also implement more complex interactions with the outside world, such as chained network API calls or processing results of effects, like parsing JSON API responses.
+We will look at how capabilities work, and will build our own in the next chapter.
+In reality, we do need to check that at least one of our HTTP requests executes successfully, but once one does, it is very likely that so long as they are described correctly, all of them will.
+As the first step, we will build a simple application, starting with a classic +Hello World, adding some state, and finally a remote API call. We will focus on +the core, rely on tests to tell us things work, and return to the shell a little +later, so unfortunately there won't be much to see until then.
+If you want to follow along, you should start by following the +Shared core and types, guide to set up the +project.
+You can find the full code for this part of the guide here
+To start with, we need a struct
to be the root of our app.
#[derive(Default)]
+pub struct Hello;
+We need to implement Default
so that Crux can construct the app for us.
To turn it into an app, we need to implement the App
trait from the
+crux_core
crate.
use crux_core::App;
+
+#[derive(Default)]
+pub struct Model;
+
+impl App for Hello {}
+If you're following along, the compiler is now screaming at you that you're
+missing four associated types for the trait: Event
, Model
, ViewModel
and
+Capabilities
.
Capabilities is the more complicated of them, and to understand what it does, we +need to talk about what makes Crux different from most UI frameworks.
+One of the key design choices in Crux is that the Core is free of side-effects +(besides its internal state). Your application can never perform anything that +directly interacts with the environment around it - no network calls, no +reading/writing files, and (somewhat obviously) not even updating the screen. +Actually doing all those things is the job of the Shell, the core can only +ask for them to be done.
+This makes the core portable between platforms, and, importantly, really easy to +test. It also separates the intent, the "functional" requirements, from the +implementation of the side-effects and the "non-functional" requirements (NFRs). +For example, your application knows it wants to store data in a SQL database, +but it doesn't need to know or care whether that database is local or remote. +That decision can even change as the application evolves, and be different on +each platform. If you want to understand this better before we carry on, you can +read a lot more about how side-effects work in Crux in the chapter on +capabilities.
+To ask the Shell for side effects, it will need to know what side effects it +needs to handle, so we will need to declare them (as an enum). Effects are +simply messages describing what should happen, and for more complex side-effects +(e.g. HTTP), they would be too unwieldy to create by hand, so to help us create +them, Crux provides capabilities - reusable libraries which give us a nice API +for requesting side-effects. We'll look at them in a lot more detail later.
+Let's start with the basics:
+use crux_core::render::Render;
+
+pub struct Capabilities {
+ render: Render<Event>,
+}
+As you can see, for now, we will use a single capability, Render
, which is
+built into Crux and available from the crux_core
crate. It simply tells the
+shell to update the screen using the latest information.
That means the core can produce a single Effect
. It will soon be more than
+one, so we'll wrap it in an enum to give ourselves space. The Effect
enum
+corresponds one to one to the Capabilities
we're using, and rather than typing
+it (and its associated trait implementations) by hand and open ourselves to
+unnecessary mistakes, we can use the Effect
derive macro from the
+crux_macros
crate.
use crux_core::render::Render;
+use crux_macros::Effect;
+
+#[derive(Effect)]
+#[effect(app = "Hello")]
+pub struct Capabilities {
+ render: Render<Event>,
+}
+Other than the derive
itself, we also need to link the effect to our app.
+We'll go into the detail of why that is in the Capabilities
+section, but the basic reason is that capabilities need to be able to send the
+app the outcomes of their work.
You probably also noticed the Event
type which capabilities are generic over,
+because they need to know the type which defines messages they can send back to
+the app. The same type is also used by the Shell to forward any user
+interactions to the Core, and in order to pass across the FFI boundary, it needs
+to be serializable. The resulting code will end up looking like this:
use crux_core::{render::Render, App};
+use crux_macros::Effect;
+use serde::{Deserialize, Serialize};
+
+#[cfg_attr(feature = "typegen", derive(crux_macros::Export))]
+#[derive(Effect)]
+#[effect(app = "Hello")]
+pub struct Capabilities {
+ render: Render<Event>,
+}
+
+#[derive(Serialize, Deserialize)]
+pub enum Event {
+ None, // we can't instantiate an empty enum, so let's have a dummy variant for now
+}
+
+#[derive(Default)]
+pub struct Hello;
+
+impl App for Hello { ... }
+In this example, we also invoke the Export
derive macro, but only when the
+typegen
feature is enabled — this is true in your shared_types
library to
+generate the foreign types for the shell. For more detail see the
+Shared core and types
+guide.
Okay, that took a little bit of effort, but with this short detour out of the +way and foundations in place, we can finally create an app and start +implementing some behavior.
+App
traitWe now have almost all the building blocks to implement the App
trait. We're
+just missing two simple types. First, a Model
to keep our app's state, it
+makes sense to make that a struct. It needs to implement Default
, which gives
+us an opportunity to set up any initial state the app might need. Second, we
+need a ViewModel
, which is a representation of what the user should see on
+screen. It might be tempting to represent the state and the view with the same
+type, but in more complicated cases it will be too constraining, and probably
+non-obvious what data are for internal bookkeeping and what should end up on
+screen, so Crux separates the concepts. Nothing stops you using the same type
+for both Model
and ViewModel
if your app is simple enough.
We'll start with a few simple types for events, model and view model.
+Now we can finally implement the trait with its two methods, update
and
+view
.
+use crux_core::{render::Render, App};
+use crux_macros::Effect;
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize)]
+pub enum Event {
+ None,
+}
+
+#[derive(Default)]
+pub struct Model;
+
+#[derive(Serialize, Deserialize)]
+pub struct ViewModel {
+ data: String,
+}
+
+#[derive(Effect)]
+#[effect(app = "Hello")]
+pub struct Capabilities {
+ render: Render<Event>,
+}
+
+#[derive(Default)]
+pub struct Hello;
+
+impl App for Hello {
+ type Event = Event;
+ type Model = Model;
+ type ViewModel = ViewModel;
+ type Capabilities = Capabilities;
+
+ fn update(&self, _event: Self::Event, _model: &mut Self::Model, caps: &Self::Capabilities) {
+ caps.render.render();
+ }
+
+ fn view(&self, _model: &Self::Model) -> Self::ViewModel {
+ ViewModel {
+ data: "Hello World".to_string(),
+ }
+ }
+}
+
+The update
function is the heart of the app. It responds to events by
+(optionally) updating the state and requesting some effects by using the
+capability's APIs.
All our update
function does is ignore all its arguments and ask the Shell to
+render the screen. It's a hello world after all.
The view
function returns the representation of what we want the Shell to show
+on screen. And true to form, it returns an instance of the ViewModel
struct
+containing Hello World!
.
That's a working hello world done, lets try it. As we said at the beginning, for +now we'll do it from tests. It may sound like a concession, but in fact, this is +the intended way for apps to be developed with Crux - from inside out, with unit +tests, focusing on behavior first and presentation later, roughly corresponding +to doing the user experience first, then the visual design.
+Here's our test:
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crux_core::{assert_effect, testing::AppTester};
+
+ #[test]
+ fn hello_says_hello_world() {
+ let hello = AppTester::<Hello, _>::default();
+ let mut model = Model::default();
+
+ // Call 'update' and request effects
+ let update = hello.update(Event::None, &mut model);
+
+ // Check update asked us to `Render`
+ assert_effect!(update, Effect::Render(_));
+
+ // Make sure the view matches our expectations
+ let actual_view = &hello.view(&model).data;
+ let expected_view = "Hello World";
+ assert_eq!(actual_view, expected_view);
+ }
+}
+
+It is a fairly underwhelming test, but it should pass (check with cargo test
).
+The test uses a testing helper from crux_core::testing
that lets us easily
+interact with the app, inspect the effects it requests and its state, without
+having to set up the machinery every time. It's not exactly complicated, but
+it's a fair amount of boiler plate code.
You can find the full code for this part of the guide +here
+Let's make things more interesting and add some behaviour. We'll teach the app +to count up and down. First, we'll need a model, which represents the state. We +could just make our model a number, but we'll go with a struct instead, so that +we can easily add more state later.
+#[derive(Default)]
+pub struct Model {
+ count: isize,
+}
+We need Default
implemented to define the initial state. For now we derive it,
+as our state is quite simple. We also update the app to show the current count:
impl App for Hello {
+// ...
+
+ type Model = Model;
+
+// ...
+
+ fn view(&self, model: &Self::Model) -> Self::ViewModel {
+ ViewModel {
+ count: format!("Count is: {}", model.count),
+ }
+ }
+}
+We'll also need a simple ViewModel
struct to hold the data that the Shell will
+render.
#[derive(Serialize, Deserialize)]
+pub struct ViewModel {
+ count: String,
+}
+Great. All that's left is adding the behaviour. That's where Event
comes in:
#[derive(Serialize, Deserialize)]
+pub enum Event {
+ Increment,
+ Decrement,
+ Reset,
+}
+The event type covers all the possible events the app can respond to. "Will that +not get massive really quickly??" I hear you ask. Don't worry about that, there +is a nice way to make this scale and get reuse as well. Let's +carry on. We need to actually handle those messages.
+impl App for Counter {
+ type Event = Event;
+ type Model = Model;
+ type ViewModel = ViewModel;
+ type Capabilities = Capabilities;
+
+ fn update(&self, event: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities) {
+ match event {
+ Event::Increment => model.count += 1,
+ Event::Decrement => model.count -= 1,
+ Event::Reset => model.count = 0,
+ };
+
+ caps.render.render();
+ }
+
+ fn view(&self, model: &Self::Model) -> Self::ViewModel {
+ ViewModel {
+ count: format!("Count is: {}", model.count),
+ }
+ }
+}
+// ...
+Pretty straightforward, we just do what we're told, update the state, and then +tell the Shell to render. Lets update the tests to check everything works as +expected.
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crux_core::{assert_effect, testing::AppTester};
+
+ #[test]
+ fn renders() {
+ let app = AppTester::<Counter, _>::default();
+ let mut model = Model::default();
+
+ let update = app.update(Event::Reset, &mut model);
+
+ // Check update asked us to `Render`
+ assert_effect!(update, Effect::Render(_));
+ }
+
+ #[test]
+ fn shows_initial_count() {
+ let app = AppTester::<Counter, _>::default();
+ let model = Model::default();
+
+ let actual_view = app.view(&model).count;
+ let expected_view = "Count is: 0";
+ assert_eq!(actual_view, expected_view);
+ }
+
+ #[test]
+ fn increments_count() {
+ let app = AppTester::<Counter, _>::default();
+ let mut model = Model::default();
+
+ let update = app.update(Event::Increment, &mut model);
+
+ let actual_view = app.view(&model).count;
+ let expected_view = "Count is: 1";
+ assert_eq!(actual_view, expected_view);
+
+ // Check update asked us to `Render`
+ assert_effect!(update, Effect::Render(_));
+ }
+
+ #[test]
+ fn decrements_count() {
+ let app = AppTester::<Counter, _>::default();
+ let mut model = Model::default();
+
+ let update = app.update(Event::Decrement, &mut model);
+
+ let actual_view = app.view(&model).count;
+ let expected_view = "Count is: -1";
+ assert_eq!(actual_view, expected_view);
+
+ // Check update asked us to `Render`
+ assert_effect!(update, Effect::Render(_));
+ }
+
+ #[test]
+ fn resets_count() {
+ let app = AppTester::<Counter, _>::default();
+ let mut model = Model::default();
+
+ app.update(Event::Increment, &mut model);
+ app.update(Event::Reset, &mut model);
+
+ let actual_view = app.view(&model).count;
+ let expected_view = "Count is: 0";
+ assert_eq!(actual_view, expected_view);
+ }
+
+ #[test]
+ fn counts_up_and_down() {
+ let app = AppTester::<Counter, _>::default();
+ let mut model = Model::default();
+
+ app.update(Event::Increment, &mut model);
+ app.update(Event::Reset, &mut model);
+ app.update(Event::Decrement, &mut model);
+ app.update(Event::Increment, &mut model);
+ app.update(Event::Increment, &mut model);
+
+ let actual_view = app.view(&model).count;
+ let expected_view = "Count is: 1";
+ assert_eq!(actual_view, expected_view);
+ }
+}
+Hopefully those all pass. We are now sure that when we build an actual UI for +this, it will work, and we'll be able to focus on making it looking +delightful.
+In more complicated cases, it might be helpful to inspect the model
directly.
+It's up to you to make the call of which one is more appropriate, in some sense
+it's the difference between black-box and white-box testing, so you should
+probably be doing both to get the confidence you need that your app is working.
Before we dive into the thinking behind the architecture, let's add one more +feature - a remote API call - to get a better feel for how side-effects and +capabilities work.
+You can find the full code for this part of the guide here
+We'll add a simple integration with a counter API we've prepared at +https://crux-counter.fly.dev. All it does is count up an down like our local +counter. It supports three requests
+GET /
returns the current countPOST /inc
increments the counterPOST /dec
decrements the counterAll three API calls return the state of the counter in JSON, which looks +something like this
+{
+ "value": 34,
+ "updated_at": 1673265904973
+}
+
+We can represent that with a struct, and we'll need to update the model as well.
+We can use Serde for the serialization (deserializing updated_at
from
+timestamp milliseconds to an option of DateTime
using chrono
).
We'll also update the count optimistically by keeping track of if/when the +server confirmed it (there are other ways to model these semantics, but let's +keep it straightforward for now).
+use chrono::{DateTime, Utc};
+use chrono::serde::ts_milliseconds_option::deserialize as ts_milliseconds_option;
+
+#[derive(Default, Serialize)]
+pub struct Model {
+ count: Count,
+}
+
+#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)]
+pub struct Count {
+ value: isize,
+ #[serde(deserialize_with = "ts_milliseconds_option")]
+ updated_at: Option<DateTime<Utc>>,
+}
+We also need to update the ViewModel
and the view()
function to display the
+new data.
#[derive(Serialize, Deserialize, Debug, Clone)]
+pub struct ViewModel {
+ pub text: String,
+ pub confirmed: bool,
+}
+
+...
+
+fn view(&self, model: &Self::Model) -> Self::ViewModel {
+ let suffix = match model.count.updated_at {
+ None => " (pending)".to_string(),
+ Some(d) => format!(" ({d})"),
+ };
+
+ Self::ViewModel {
+ text: model.count.value.to_string() + &suffix,
+ confirmed: model.count.updated_at.is_some(),
+ }
+}
+You can see that the view function caters to two states - the count has not yet
+been confirmed (updated_at
is None
), and having the count confirmed by the
+server.
In a real-world app, it's likely that this information would be captured in a +struct rather than converted to string inside the core, so that the UI can +decide how to present it. The date formatting, however, is an example of +something you may want to do consistently across all platforms and keep inside +the Core. When making these choices, think about who's decisions they are, and +do they need to be consistent across platforms or flexible. You will no doubt +get a number of those calls wrong, but that's ok, the type system is here to +help you refactor later and update the shells to work with the changes.
+We now have everything in place to update the update
function. Let's start
+with thinking about the events. The API does not support resetting the counter,
+so that variant goes, but we need a new one to kick off fetching the current
+state of the counter. The Core itself can't autonomously start anything, it is
+always driven by the Shell, either by the user via the UI, or as a result of a
+side-effect.
That gives us the following update function, with some placeholders:
+fn update(&self, event: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities) {
+ match event {
+ Event::Get => {
+ // TODO "GET /"
+ }
+ Event::Set(_response) => {
+ // TODO Get the data and update the model
+ caps.render.render();
+ }
+ Event::Increment => {
+ // optimistic update
+ model.count.value += 1;
+ model.count.updated_at = None;
+ caps.render.render();
+
+ // real update
+ // TODO "POST /inc"
+ }
+ Event::Decrement => {
+ // optimistic update
+ model.count.value -= 1;
+ model.count.updated_at = None;
+ caps.render.render();
+
+ // real update
+ // TODO "POST /dec"
+ }
+ }
+}
+To request the respective HTTP calls, we'll use
+crux_http
the
+built-in HTTP client. Since this is the first capability we're using, some
+things won't be immediately clear, but we should get there by the end of this
+chapter.
The first thing to know is that the HTTP responses will be sent back to the
+update function as an event. That's what the Event::Set
is for. The Event
+type looks as follows:
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
+pub enum Event {
+ // these variants are used by the Shell
+ Get,
+ Increment,
+ Decrement,
+
+ // this variant is private to the Core
+ #[serde(skip)]
+ Set(crux_http::Result<crux_http::Response<Count>>),
+}
+We decorate the Set
variant with #[serde(skip)]
for two reasons: one,
+there's currently a technical limitation stopping us easily serializing
+crux_http::Response
, and two, there's no reason that variant should ever be
+sent by the Shell across the FFI boundary, which is the reason for the need to
+serialize in the first place — in a way, it is private to the Core.
Finally, let's get rid of those TODOs. We'll need to add crux_http in the
+Capabilities
type, so that the update
function has access to it:
use crux_http::Http;
+
+#[derive(Effect)]
+pub struct Capabilities {
+ pub http: Http<Event>,
+ pub render: Render<Event>,
+}
+This may seem like needless boilerplate, but it allows us to only use the
+capabilities we need and, more importantly, allow capabilities to be built by
+anyone. Later on, we'll also see that Crux apps compose, relying
+on each app's Capabilities
type to declare its needs, and making sure the
+necessary capabilities exist in the parent app.
We can now implement those TODOs, so lets do it.
+const API_URL: &str = "https://crux-counter.fly.dev";
+
+//...
+
+fn update(&self, event: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities) {
+ match event {
+ Event::Get => {
+ caps.http.get(API_URL).expect_json().send(Event::Set);
+ }
+ Event::Set(Ok(mut response)) => {
+ let count = response.take_body().unwrap();
+ model.count = count;
+ caps.render.render();
+ }
+ Event::Set(Err(_)) => {
+ panic!("Oh no something went wrong");
+ }
+ Event::Increment => {
+ // optimistic update
+ model.count = Count {
+ value: model.count.value + 1,
+ updated_at: None,
+ };
+ caps.render.render();
+
+ // real update
+ let base = Url::parse(API_URL).unwrap();
+ let url = base.join("/inc").unwrap();
+ caps.http.post(url).expect_json().send(Event::Set);
+ }
+ Event::Decrement => {
+ // optimistic update
+ model.count = Count {
+ value: model.count.value - 1,
+ updated_at: None,
+ };
+ caps.render.render();
+
+ // real update
+ let base = Url::parse(API_URL).unwrap();
+ let url = base.join("/dec").unwrap();
+ caps.http.post(url).expect_json().send(Event::Set);
+ }
+ }
+ }
+
+There's a few things of note. The first one is that the .send
API at the end
+of each chain of calls to crux_http
expects a function that wraps its argument
+(a Result
of a http response) in a variant of Event
. Fortunately, enum tuple
+variants create just such a function, and we can use it. The way to read the
+call is "Send a get request, parse the response as JSON, which should be
+deserialized as a Count
, and then call me again with Event::Set
carrying the
+result". Interestingly, we didn't need to specifically mention the Count
type,
+as the type inference from the Event::Set
variant is enough, making it really
+easy to read.
The other thing of note is that the capability calls don't block. They queue up +requests to send to the shell and execution continues immediately. The requests +will be sent in the order they were queued and the asynchronous execution is the +job of the shell.
+You can find the the complete example, including the shell implementations +in the Crux repo. +It's interesting to take a closer look at the unit tests
+ /// Test that a `Get` event causes the app to fetch the current
+ /// counter value from the web API
+ #[test]
+ fn get_counter() {
+ // instantiate our app via the test harness, which gives us access to the model
+ let app = AppTester::<App, _>::default();
+
+ // set up our initial model
+ let mut model = Model::default();
+
+ // send a `Get` event to the app
+ let mut update = app.update(Event::Get, &mut model);
+
+ // check that the app emitted an HTTP request,
+ // capturing the request in the process
+ assert_let!(Effect::Http(request), &mut update.effects[0]);
+
+ // check that the request is a GET to the correct URL
+ let actual = &request.operation;
+ let expected = &HttpRequest::get("https://crux-counter.fly.dev/").build();
+ assert_eq!(actual, expected);
+
+ // resolve the request with a simulated response from the web API
+ let response = HttpResponse::ok()
+ .body(r#"{ "value": 1, "updated_at": 1672531200000 }"#)
+ .build();
+ let update = app.resolve(request, response).expect("an update");
+
+ // check that the app emitted an (internal) event to update the model
+ let actual = update.events;
+ let expected = vec![Event::Set(Ok(ResponseBuilder::ok()
+ .body(Count {
+ value: 1,
+ updated_at: Some(Utc.with_ymd_and_hms(2023, 1, 1, 0, 0, 0).unwrap()),
+ })
+ .build()))];
+ assert_eq!(actual, expected);
+ }
+
+ /// Test that a `Set` event causes the app to update the model
+ #[test]
+ fn set_counter() {
+ // instantiate our app via the test harness, which gives us access to the model
+ let app = AppTester::<App, _>::default();
+
+ // set up our initial model
+ let mut model = Model::default();
+
+ // send a `Set` event (containing the HTTP response) to the app
+ let update = app.update(
+ Event::Set(Ok(ResponseBuilder::ok()
+ .body(Count {
+ value: 1,
+ updated_at: Some(Utc.with_ymd_and_hms(2023, 1, 1, 0, 0, 0).unwrap()),
+ })
+ .build())),
+ &mut model,
+ );
+
+ // check that the app asked the shell to render
+ assert_effect!(update, Effect::Render(_));
+
+ // check that the view has been updated correctly
+ insta::assert_yaml_snapshot!(app.view(&mut model), @r###"
+ ---
+ text: "1 (2023-01-01 00:00:00 UTC)"
+ confirmed: true
+ "###);
+ }
+Incidentally, we're using insta
in that last
+test to assert that the view model is correct. If you don't know it already,
+check it out. The really cool thing is that if the test fails, it shows you a
+diff of the actual and expected output, and if you're happy with the new output,
+you can accept the change (or not) by running cargo insta review
— it will
+update the code for you to reflect the change. It's a really nice way to do
+snapshot testing, especially for the model and view model.
You can see how easy it is to check that the app is requesting the right side
+effects, with the right arguments, and even test a chain of interactions and
+make sure the behavior is correct, all without mocking or stubbing anything or
+worrying about async
code.
In the next chapter, we can put the example into perspective and discuss the +architecture it follows, inspired by Elm.
+ +So far in this book, we've been taking the perspective of being inside the core +looking out. It feels like it's now time to be in the shell, looking in.
+Interestingly, we think this is also the way to approach building apps with Crux. For any one feature, start in the middle and get your behaviour established first. Write the tests without the UI and the other side-effects in the way. Give yourself maximum confidence that the feature works exactly as you expect before you muddy the water with UI components, and their look and feel.
+OK, let's talk about the shell.
+The shell only has two responsibilities:
+We'll look at these separately. But first let's remind ourselves of how we +interact with the core (now would be a good time to read +Shared core and types if you haven't already).
+The interface is message based, and uses serialization to pass data back and +forth. The core exports the types for all the data so that it can be used and +created on the shell side with safety.
+An Event
can be passed in directly, as-is. Processing of Effect
s is a little
+more complicated, because the core needs to be able to pair the outcomes of the
+effects with the original capability call, so it can return them to the right
+caller. To do that, effects are wrapped in a Request
, which tags them with a
+UUID. To respond, the same UUID needs to be passed back in.
Requests from the core are emitted serialized, and need to be deserialized +first. Both events and effect outputs need to be serialized before being passed +back to the core.
+It is likely that this will become an implementation detail and instead, Crux will provide a more ergonomic shell-side API for the interaction, hiding both the UUID pairing and the serialization (and allowing us to iterate on the FFI implementation which, we think, could work better).
+There are only three touch-points with the core.
++#![allow(unused)] +fn main() { +pub fn process_event(data: &[u8]) -> Vec<u8> { todo!() } +pub fn handle_response(uuid: &[u8], data: &[u8]) -> Vec<u8> { todo!() } +pub fn view() -> Vec<u8> { todo!() } +}
The process_event
function takes a serialized Event
(from a UI interaction)
+and returns a serialized vector of Request
s that the shell can dispatch to the
+relevant capability's shell-side code (see the section below on how the shell
+handles capabilities).
The handle_response
function, used to return capability output back into the
+core, is similar to process_event
except that it also takes a uuid
, which
+ties the output (for example a HTTP response) being submitted with it's original
+Effect
which started it (and the corresponding request which the core wrapped
+it in).
The view
function simply retrieves the serialized view model (to which the UI
+is bound) and is called by the shell after it receives a Render
request. The
+view model is a projection of the app's state – it reflects what information the
+Core wants displayed on screen.
You're probably thinking, "Whoa! I just see slices and vectors of bytes, where's
+the type safety?". Well, the answer is that we also generate all the types that
+pass through the bridge, for each language, along with serialization and
+deserialization helpers. This is done by the serde-generate
crate (see the
+section on
+Create the shared types crate).
For now we have to manually invoke the serialization code in the shell. At some point this may be abstracted away.
+In this code snippet from the
+Counter example,
+notice that we call processEvent
and handleResponse
on the core depending on
+whether we received an Event
from the UI or from a capability, respectively.
+Regardless of which core function we call, we get back a bunch of requests,
+which we can iterate through and do the relevant thing (the following snippet
+triggers a render of the UI, or makes an HTTP call, or launches a task to wait
+for Server Sent Events, depending on what the core requested):
sealed class Outcome {
+ data class Http(val res: HttpResponse) : Outcome()
+ data class Sse(val res: SseResponse) : Outcome()
+}
+
+sealed class CoreMessage {
+ data class Event(val event: Evt) : CoreMessage()
+ data class Response(val uuid: List<UByte>, val outcome: Outcome) : CoreMessage()
+}
+
+class Model : ViewModel() {
+ var view: MyViewModel by mutableStateOf(MyViewModel("", false))
+ private set
+
+ suspend fun update(msg: CoreMessage) {
+ val requests: List<Req> =
+ when (msg) {
+ is CoreMessage.Event ->
+ Requests.bincodeDeserialize(
+ processEvent(msg.event.bincodeSerialize().toUByteArray().toList())
+ .toUByteArray()
+ .toByteArray()
+ )
+ is CoreMessage.Response ->
+ Requests.bincodeDeserialize(
+ handleResponse(
+ msg.uuid.toList(),
+ when (msg.outcome) {
+ is Outcome.Http -> msg.outcome.res.bincodeSerialize()
+ is Outcome.Sse -> msg.outcome.res.bincodeSerialize()
+ }.toUByteArray().toList()
+ ).toUByteArray().toByteArray()
+ )
+ }
+
+ for (req in requests) when (val effect = req.effect) {
+ is Effect.Render -> {
+ this.view = MyViewModel.bincodeDeserialize(view().toUByteArray().toByteArray())
+ }
+ is Effect.Http -> {
+ val response = http(httpClient, HttpMethod(effect.value.method), effect.value.url)
+ update(
+ CoreMessage.Response(
+ req.uuid.toByteArray().toUByteArray().toList(),
+ Outcome.Http(response)
+ )
+ )
+ }
+ is Effect.ServerSentEvents -> {
+ viewModelScope.launch {
+ sse(sseClient, effect.value.url) { event ->
+ update(
+ CoreMessage.Response(
+ req.uuid.toByteArray().toUByteArray().toList(),
+ Outcome.Sse(event)
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+
+Crux can work with any platform-specific UI library. We think it works best with +modern declarative UI frameworks such as +SwiftUI on iOS, +Jetpack Compose on Android, and +React/Vue or a Wasm based +framework (like Yew) on the web.
+These frameworks are all pretty much identical. If you're familiar with one, you +can work out the others easily. In the examples on this page, we'll work in an +Android shell with Kotlin.
+The components are bound to the view model, and they send events to the core.
+We've already seen a "hello world" example when we were
+setting up an Android project.
+Rather than print that out again here, we'll just look at how we need to enhance
+it to work with Kotlin coroutines. We'll probably need to do this with any real
+shell, because the update function that dispatches side effect requests from the
+core will likely need to be suspend
.
This is the View
from the
+Counter example
+in the Crux repository.
@Composable
+fun View(model: Model = viewModel()) {
+ val coroutineScope = rememberCoroutineScope()
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(10.dp),
+ ) {
+ Text(text = "Crux Counter Example", fontSize = 30.sp, modifier = Modifier.padding(10.dp))
+ Text(text = "Rust Core, Kotlin Shell (Jetpack Compose)", modifier = Modifier.padding(10.dp))
+ Text(text = model.view.text, color = if(model.view.confirmed) { Color.Black } else { Color.Gray }, modifier = Modifier.padding(10.dp))
+ Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+ Button(
+ onClick = { coroutineScope.launch { model.update(CoreMessage.Event(Evt.Decrement())) } },
+ colors = ButtonDefaults.buttonColors(containerColor = Color.hsl(44F, 1F, 0.77F))
+ ) { Text(text = "Decrement", color = Color.DarkGray) }
+ Button(
+ onClick = { coroutineScope.launch { model.update(CoreMessage.Event(Evt.Increment())) } },
+ colors =
+ ButtonDefaults.buttonColors(
+ containerColor = Color.hsl(348F, 0.86F, 0.61F)
+ )
+ ) { Text(text = "Increment", color = Color.White) }
+ }
+ }
+}
+
+Notice that the first thing we do is create a CoroutineScope that is scoped to
+the lifetime of the View (i.e. will be destroyed when the View
component is
+unmounted). Then we use this scope to launch asynchronous tasks to call the
+update
method with the specific event.
+Button(onClick = { coroutineScope.launch { model.update(CoreMessage.Event(Evt.Increment())) } })
.
+We can't call update
directly, because it is suspend
so we need to be in an
+asynchronous context to do so.
We want the shell to be as thin as possible, so we need to write as little +platform-specific code as we can because this work has to be duplicated for each +platform.
+In general, the more domain-aligned our capabilities are, the more code we'll
+write. When our capabilities are generic, and closer to the technical end of the
+spectrum, we get to write the least amount of shell code to support them.
+Getting the balance right can be tricky, and the right answer might be different
+depending on context. Obviously the Http
capability is very generic, but a CMS
+capability, for instance, might well be much more specific.
The shell-side code for the Http
capability can be very small. A (very) naive
+implementation for Android might look like this:
package com.example.counter
+
+import com.example.counter.shared_types.HttpHeader
+import com.example.counter.shared_types.HttpRequest
+import com.example.counter.shared_types.HttpResponse
+import io.ktor.client.HttpClient
+import io.ktor.client.call.body
+import io.ktor.client.request.headers
+import io.ktor.client.request.request
+import io.ktor.http.HttpMethod
+import io.ktor.util.flattenEntries
+
+suspend fun requestHttp(
+ client: HttpClient,
+ request: HttpRequest,
+): HttpResponse {
+ val response = client.request(request.url) {
+ this.method = HttpMethod(request.method)
+ this.headers {
+ for (header in request.headers) {
+ append(header.name, header.value)
+ }
+ }
+ }
+ val bytes: ByteArray = response.body()
+ val headers = response.headers.flattenEntries().map { HttpHeader(it.first, it.second) }
+ return HttpResponse(response.status.value.toShort(), headers, bytes.toList())
+}
+
+
+The shell-side code to support a capability (or "Port" in "Ports and Adapters"),
+is effectively just an "Adapter" (in the same terminology) to the native APIs.
+Note that it's the shell's responsibility to cater for threading and/or async
+coroutine requirements (so the above Kotlin function is suspend
for this
+reason).
The above function can then be called by the shell when an effect is emitted
+requesting an HTTP call. It can then post the response back to the core (along
+with the uuid
that is used by the core to tie the response up to its original
+request):
for (req in requests) when (val effect = req.effect) {
+ is Effect.Http -> {
+ val response = http(
+ httpClient,
+ HttpMethod(effect.value.method),
+ effect.value.url
+ )
+ update(
+ CoreMessage.Response(
+ req.uuid.toByteArray().toUByteArray().toList(),
+ Outcome.Http(response)
+ )
+ )
+ }
+ // ...
+}
+
+
+ One of the most compelling consequences of the Crux architecture is that it +becomes trivial to comprehensively test your application. This is because the +core is pure and therefore completely deterministic — all the side effects are +pushed to the shell.
+It's straightforward to write an exhaustive set of unit tests that give you +complete confidence in the correctness of your application code — you can test +the behavior of your application independently of platform-specific UI and API +calls.
+There is no need to mock/stub anything, and there is no need to write +integration tests.
+Not only are the unit tests easy to write, but they run extremely quickly, and +can be run in parallel.
+For example, the +Notes example app +contains complex logic related to collaborative text-editing using Conflict-free +Replicated Data Types (CRDTs). The test suite consists of 25 tests that give us +high coverage and high confidence of correctness. Many of the tests include +instantiating two instances (alice and bob) and checking that, even during +complex edits, the synchronization between them works correctly.
+This test, for example, ensures that when Alice and Bob both insert text at the +same time, they both end up with the same result. It runs in 4 milliseconds.
+#[test]
+fn two_way_sync() {
+ let (mut alice, mut bob) = make_alice_and_bob();
+
+ alice.update(Event::Insert("world".to_string()));
+ let edits = alice.edits.drain(0..).collect::<Vec<_>>();
+
+ bob.send_edits(edits.as_ref());
+
+ // Alice's inserts should go in front of Bob's cursor
+ // so we break the ambiguity of same cursor position
+ // as quickly as possible
+ bob.update(Event::Insert("Hello ".to_string()));
+ let edits = bob.edits.drain(0..).collect::<Vec<_>>();
+
+ alice.send_edits(edits.as_ref());
+
+ let alice_view = alice.view();
+ let bob_view = bob.view();
+
+ assert_eq!(alice_view.text, "Hello world".to_string());
+ assert_eq!(alice_view.text, bob_view.text);
+}
+And the full suite of 25 tests runs in 16 milliseconds.
+cargo nextest run --release -p shared
+ Finished release [optimized] target(s) in 0.07s
+ Starting 25 tests across 2 binaries
+ PASS [ 0.005s] shared app::editing_tests::handles_emoji
+ PASS [ 0.005s] shared app::editing_tests::removes_character_before_cursor
+ PASS [ 0.005s] shared app::editing_tests::moves_cursor
+ PASS [ 0.006s] shared app::editing_tests::inserts_text_at_cursor_and_renders
+ PASS [ 0.005s] shared app::editing_tests::removes_selection_on_backspace
+ PASS [ 0.005s] shared app::editing_tests::removes_character_after_cursor
+ PASS [ 0.005s] shared app::editing_tests::removes_selection_on_delete
+ PASS [ 0.007s] shared app::editing_tests::changes_selection
+ PASS [ 0.006s] shared app::editing_tests::renders_text_and_cursor
+ PASS [ 0.006s] shared app::editing_tests::replaces_empty_range_and_renders
+ PASS [ 0.005s] shared app::editing_tests::replaces_range_and_renders
+ PASS [ 0.005s] shared app::note::test::splices_text
+ PASS [ 0.005s] shared app::editing_tests::replaces_selection_and_renders
+ PASS [ 0.004s] shared app::save_load_tests::opens_a_document
+ PASS [ 0.005s] shared app::note::test::inserts_text
+ PASS [ 0.005s] shared app::save_load_tests::saves_document_when_typing_stops
+ PASS [ 0.005s] shared app::save_load_tests::starts_a_timer_after_an_edit
+ PASS [ 0.006s] shared app::save_load_tests::creates_a_document_if_it_cant_open_one
+ PASS [ 0.005s] shared app::sync_tests::concurrent_clean_edits
+ PASS [ 0.005s] shared app::sync_tests::concurrent_conflicting_edits
+ PASS [ 0.005s] shared app::sync_tests::one_way_sync
+ PASS [ 0.005s] shared app::sync_tests::remote_delete_moves_cursor
+ PASS [ 0.005s] shared app::sync_tests::remote_insert_behind_cursor
+ PASS [ 0.004s] shared app::sync_tests::two_way_sync
+ PASS [ 0.005s] shared app::sync_tests::receiving_own_edits
+------------
+ Summary [ 0.016s] 25 tests run: 25 passed, 0 skipped
+
+Crux provides a simple test harness that we can use to write unit tests for our +application code. Strictly speaking it's not needed, but it makes it easier to +avoid boilerplate and to write tests that are easy to read and understand.
+Let's take a +really simple test +from the +Notes example app +and walk through it step by step — the test replaces some selected text in a +document and checks that the correct text is rendered.
+The first thing to do is create an instance of the AppTester
test harness,
+which runs our app (NoteEditor
) and makes it easy to analyze the Event
s and
+Effect
s that are generated.
let app = AppTester::<NoteEditor, _>::default();
+The Model
is normally private to the app (NoteEditor
), but AppTester
+allows us to set it up for our test. In this case the document contains the
+string "hello"
with the last three characters selected.
let mut model = Model {
+ note: Note::with_text("hello"),
+ cursor: TextCursor::Selection(3..5),
+ ..Default::default()
+};
+Let's insert the text under the selection range. We simply create an Event
+that captures the user's action and pass it into the app's update()
method,
+along with the Model we just created (which we will be able to inspect
+afterwards).
let event = Event::Insert("ter skelter".to_string());
+let update = app.update(event, &mut model);
+We can check that the shell was asked to render by using the
+assert_effect!
+macro, which panics if none of the effects generated by the update matches the
+specified pattern.
assert_effect!(update, Effect::Render(_));
+Finally we can ask the app for its ViewModel
and use it to check that the text
+was inserted correctly and that the cursor position was updated.
let view = app.view(&model);
+
+assert_eq!(view.text, "helter skelter".to_string());
+assert_eq!(view.cursor, TextCursor::Position(14));
+Now let's take a +more complicated test +and walk through that. This test checks that a "save" timer is restarted each +time the user edits the document (after a second of no activity the document is +stored). Note that the actual timer is run by the shell (because it is a side +effect, which would make it really tricky to test) — but all we need to do is +check that the behavior of the timer is correct (i.e. started, finished and +cancelled correctly).
+Again, the first thing we need to do is create an instance of the AppTester
+test harness, which runs our app (NoteEditor
) and makes it easy to analyze the
+Event
s and Effect
s that are generated.
let app = AppTester::<NoteEditor, _>::default();
+We again need to set up a Model
that we can pass to the update()
method.
let mut model = Model {
+ note: Note::with_text("hello"),
+ cursor: TextCursor::Selection(2..4),
+ ..Default::default()
+};
+We send an Event
(e.g. raised in response to a user action) into our app in
+order to check that it does the right thing.
Here we send an Insert event, which should start a timer. We filter out just the
+Effect
s that were created by the Timer
Capability, mapping them to their
+inner Request<TimerOperation>
type.
let requests = &mut app
+ .update(Event::Insert("something".to_string()), &mut model)
+ .into_effects()
+ .filter_map(Effect::into_timer);
+There are a few things to discuss here. Firstly, the update()
method returns
+an Update
struct, which contains vectors of Event
s and Effect
s. We are
+only interested in the Effect
s, so we call into_effects()
to consume them as
+an Iterator
(there are also effects()
and effects_mut()
methods that allow
+us to borrow the Effect
s instead of consuming them, but we don't need that
+here). Secondly, we use the filter_map()
method to filter out just the
+Effect
s that were created by the Timer
Capability, using
+Effect::into_timer
to map the Effect
s to their inner
+Request<TimerOperation>
.
The Effect
derive
+macro generates filters and maps for each capability that we are using. So if
+our Capabilities
struct looked like this...
+#[cfg_attr(feature = "typegen", derive(crux_macros::Export))]
+#[derive(Effect)]
+#[effect(app = "NoteEditor")]
+pub struct Capabilities {
+ timer: Timer<Event>,
+ render: Render<Event>,
+ pub_sub: PubSub<Event>,
+ key_value: KeyValue<Event>,
+}
+... we would get the following filters and filter_maps:
+// filters
+Effect::is_timer(&self) -> bool
+Effect::is_render(&self) -> bool
+Effect::is_pub_sub(&self) -> bool
+Effect::is_key_value(&self) -> bool
+// filter_maps
+Effect::into_timer(self) -> Option<Request<TimerOperation>>
+Effect::into_render(self) -> Option<Request<RenderOperation>>
+Effect::into_pub_sub(self) -> Option<Request<PubSubOperation>>
+Effect::into_key_value(self) -> Option<Request<KeyValueOperation>>
+We want to check that the first request is a Start
operation, and that the
+timer is set to fire in 1000 milliseconds. The macro
+assert_let!()
does a pattern
+match for us and assigns the id
to a local variable called first_id
, which
+we'll use later. Finally, we don't expect any more timer requests to have been
+generated.
let mut request = requests.next().unwrap(); // this is mutable so we can resolve it later
+assert_let!(
+ TimerOperation::Start {
+ id: first_id,
+ millis: 1000
+ },
+ request.operation.clone()
+);
+assert!(requests.next().is_none());
+At this point the shell would start the timer (this is something the core can't +do as it is a side effect) and so we need to tell the app that it was created. +We do this by "resolving" the request.
+Remember that Request
s either resolve zero times (fire-and-forget, e.g. for
+Render
), once (request/response, e.g. for Http
), or many times (for streams,
+e.g. Sse
— Server-Sent Events). The Timer
capability falls into the
+"request/response" category, so we need to resolve the Start
request with a
+Created
response. This tells the app that the timer has been started, and
+allows it to cancel the timer if necessary.
Note that resolving a request could call the app's update()
method resulting
+in more Event
s being generated, which we need to feed back into the app.
let update = app
+ .resolve(&mut request, TimerOutput::Created { id: first_id }).unwrap();
+for event in update.events {
+ app.update(event, &mut model);
+}
+Before the timer fires, we'll insert another character, which should cancel the +existing timer and start a new one.
+let mut requests = app
+ .update(Event::Replace(1, 2, "a".to_string()), &mut model)
+ .into_effects()
+ .filter_map(Effect::into_timer);
+
+let cancel_request = requests.next().unwrap();
+assert_let!(
+ TimerOperation::Cancel { id: cancel_id },
+ cancel_request.operation
+);
+assert_eq!(cancel_id, first_id);
+
+let start_request = &mut requests.next().unwrap(); // this is mutable so we can resolve it later
+assert_let!(
+ TimerOperation::Start {
+ id: second_id,
+ millis: 1000
+ },
+ start_request.operation.clone()
+);
+assert_ne!(first_id, second_id);
+
+assert!(requests.next().is_none());
+Now we need to tell the app that the second timer was created.
+let update = app
+ .resolve(start_request, TimerOutput::Created { id: second_id })
+ .unwrap();
+for event in update.events {
+ app.update(event, &mut model);
+}
+In the real world, time passes and the timer fires, but all we have to do is to
+resolve our start request again, but this time with a Finished
response.
let update = app
+ .resolve(start_request, TimerOutput::Finished { id: second_id })
+ .unwrap();
+for event in update.events {
+ app.update(event, &mut model);
+}
+Another edit should result in another timer, but not in a cancellation:
+let update = app.update(Event::Backspace, &mut model);
+let mut timer_requests = update.into_effects().filter_map(Effect::into_timer);
+
+assert_let!(
+ TimerOperation::Start {
+ id: third_id,
+ millis: 1000
+ },
+ timer_requests.next().unwrap().operation
+);
+assert!(timer_requests.next().is_none()); // no cancellation
+
+assert_ne!(third_id, second_id);
+Note that this test was not about testing whether the model was updated
+correctly (that is covered in other tests) so we don't call the app's view()
+method — it's just about checking that the timer is started, cancelled and
+restarted correctly.
Crux is an experimental approach to building cross-platform applications +with better testability, higher code and behavior reuse, better safety, +security, and more joy from better tools.
+It splits the application into two distinct parts, a Core built in Rust, which +drives as much of the business logic as possible, and a Shell, built in the +platform native language (Swift, Kotlin, TypeScript), which provides all +interfaces with the external world, including the human user, and acts as a +platform on which the core runs.
+ +The interface between the two is a native FFI (Foreign Function Interface) with +cross-language type checking and message passing semantics, where simple data +structures are passed across the boundary.
+To get playing with Crux quickly, follow the Getting Started steps. If you prefer to read more about how apps are built in Crux first, read the Development Guide. And if you'd like to know what possessed us to try this in the first place, read about our Motivation.
+There are two places to find API documentation: the latest published version on docs.rs, and we also have the very latest master docs if you too like to live dangerously.
+Crux is open source on Github. A good way to learn Crux is to explore the code, play with the examples, and raise issues or pull requests. We'd love you to get involved.
+You can also join the friendly conversation on our Zulip channel.
+The architecture is event-driven, based on +event sourcing. The Core +holds the majority of state, which is updated in response to events happening in +the Shell. The interface between the Core and the Shell is messaged based.
+The user interface layer is built natively, with modern declarative UI +frameworks such as Swift UI, Jetpack Compose and React/Vue or a WASM based +framework on the web. The UI layer is as thin as it can be, and all other +application logic is performed by the shared Core. The one restriction is that +the Core is side–effect free. This is both a technical requirement (to be able +to target WebAssembly), and an intentional design goal, to separate logic from +effects and make them both easier to test in isolation.
+The core requests side-effects from the Shell through common +capabilities. The basic concept is that instead of +doing the asynchronous work, the core describes the intent for the work with +data, and passes this to the Shell to be performed. The Shell performs the work, +and returns the outcomes back to the Core. This approach is inspired by +Elm, and similar to how other purely functional +languages deal with effects and I/O (e.g. the IO monad in Haskell). It is also +similar to how iterators work in Rust.
+The Core exports types for the messages it can understand. The Shell can call +the Core and pass one of the messages. In return, it receives a set of +side-effect requests to perform. When the work is completed, the Shell sends the +result back into the Core, which responds with further requests if necessary.
+Updating the user interface is considered one of the side-effects the Core can +request. The entire interface is strongly typed and breaking changes in the core +will result in build failures in the Shell.
+We set out to prove this architecture to find a better way of building apps +across platforms. You can read more about our motivation. The +overall goals of Crux are to:
+pub(crate) enum Commands {
+ Doctor(DoctorArgs),
+}
ArgMatches
to self
.ArgMatches
to self
.Self
can parse a specific subcommandpub(crate) struct Cli {
+ pub command: Option<Commands>,
+ pub verbose: u8,
+ pub include_source_code: bool,
+ pub template_dir: PathBuf,
+ pub path: Option<PathBuf>,
+}
command: Option<Commands>
§verbose: u8
§include_source_code: bool
§template_dir: PathBuf
temporary
+path: Option<PathBuf>
ArgGroup::id
][crate::ArgGroup::id] for this set of argumentsArgMatches
to self
.ArgMatches
to self
.std::env::args_os()
, return Err on error.pub(crate) struct DoctorArgs {
+ pub(crate) fix: Option<PathBuf>,
+}
fix: Option<PathBuf>
ArgGroup::id
][crate::ArgGroup::id] for this set of argumentsArgMatches
to self
.ArgMatches
to self
.pub struct Core {
+ pub name: String,
+ pub source: PathBuf,
+ pub type_gen: Option<PathBuf>,
+ pub crux_version: String,
+}
name: String
§source: PathBuf
§type_gen: Option<PathBuf>
§crux_version: String
pub struct Shell {
+ pub name: String,
+ pub template: Option<PathBuf>,
+ pub source: PathBuf,
+ pub cores: Vec<String>,
+}
name: String
§template: Option<PathBuf>
§source: PathBuf
§cores: Vec<String>
pub struct Workspace {
+ pub name: String,
+ pub description: Option<String>,
+ pub authors: Vec<String>,
+ pub repository: Option<String>,
+ pub cores: BTreeMap<String, Core>,
+ pub shells: Option<BTreeMap<String, Shell>>,
+}
name: String
§description: Option<String>
§repository: Option<String>
§cores: BTreeMap<String, Core>
§shells: Option<BTreeMap<String, Shell>>
struct Line(Option<usize>);
0: Option<usize>
type FileMap = BTreeMap<PathBuf, String>;
struct FileMap {
+ root: Option<NodeRef<Owned, PathBuf, String, LeafOrInternal>>,
+ length: usize,
+ pub(super) alloc: ManuallyDrop<Global>,
+ _marker: PhantomData<Box<(PathBuf, String), Global>>,
+}
root: Option<NodeRef<Owned, PathBuf, String, LeafOrInternal>>
§length: usize
§alloc: ManuallyDrop<Global>
§_marker: PhantomData<Box<(PathBuf, String), Global>>
pub enum Context {
+ Core(CoreContext),
+ Shell(ShellContext),
+}
pub struct CoreContext {
+ pub workspace: String,
+ pub core_name: String,
+ pub core_name_dashes: String,
+}
workspace: String
§core_name: String
§core_name_dashes: String
Content
+for a given Template
.true
if the field exists in this content, otherwise false
.true
if the field exists in this content, otherwise false
.pub struct ShellContext {
+ pub workspace: String,
+ pub core_dir: String,
+ pub core_name: String,
+ pub type_gen: String,
+ pub shell_dir: String,
+ pub shell_name: String,
+ pub shell_name_dashes: String,
+}
workspace: String
§core_dir: String
§core_name: String
§type_gen: String
§shell_dir: String
§shell_name: String
§shell_name_dashes: String
Content
+for a given Template
.true
if the field exists in this content, otherwise false
.true
if the field exists in this content, otherwise false
.const CONFIG_FILE: &str = "Crux.toml";
Core
but in a
+serialized formuuid
links
+the Request
with the corresponding call to Core::resolve
to pass the data back
+to the App::update
function (wrapped in the event provided to the capability originating the effect).pub struct Bridge<Eff, A>where
+ Eff: Effect,
+ A: App,{ /* private fields */ }
Bridge is a core wrapper presenting the same interface as the Core
but in a
+serialized form
Receive an event from the shell.
+The event
is serialized and will be deserialized by the core before it’s passed
+to your app.
Receive a response to a capability request from the shell.
+The output
is serialized capability output. It will be deserialized by the core.
+The uuid
MUST match the uuid
of the effect that triggered it, else the core will panic.
pub struct Request<Eff>where
+ Eff: Serialize,{
+ pub uuid: Vec<u8>,
+ pub effect: Eff,
+}
Request for a side-effect passed from the Core to the Shell. The uuid
links
+the Request
with the corresponding call to Core::resolve
to pass the data back
+to the App::update
function (wrapped in the event provided to the capability originating the effect).
uuid: Vec<u8>
§effect: Eff
Redirecting to ../../../crux_core/render/index.html...
+ + + \ No newline at end of file diff --git a/master_api_docs/crux_core/capabilities/render/struct.Render.html b/master_api_docs/crux_core/capabilities/render/struct.Render.html new file mode 100644 index 000000000..478d2fe4a --- /dev/null +++ b/master_api_docs/crux_core/capabilities/render/struct.Render.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../crux_core/render/struct.Render.html...
+ + + \ No newline at end of file diff --git a/master_api_docs/crux_core/capabilities/render/struct.RenderOperation.html b/master_api_docs/crux_core/capabilities/render/struct.RenderOperation.html new file mode 100644 index 000000000..401c2a5ae --- /dev/null +++ b/master_api_docs/crux_core/capabilities/render/struct.RenderOperation.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../crux_core/render/struct.RenderOperation.html...
+ + + \ No newline at end of file diff --git a/master_api_docs/crux_core/capability/index.html b/master_api_docs/crux_core/capability/index.html new file mode 100644 index 000000000..3b4b7cbd2 --- /dev/null +++ b/master_api_docs/crux_core/capability/index.html @@ -0,0 +1,149 @@ +Capabilities provide a user-friendly API to request side-effects from the shell.
+Typically, capabilities provide I/O and host API access. Capabilities are external to the +core Crux library. Some are part of the Crux core distribution, others are expected to be built by the +community. Apps can also build single-use capabilities where necessary.
+A typical use of a capability would look like the following:
+ +fn update(&self, event: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities) {
+ match event {
+ //...
+ Event::Increment => {
+ model.count += 1;
+ caps.render.render(); // Render capability
+
+ let base = Url::parse(API_URL).unwrap();
+ let url = base.join("/inc").unwrap();
+ caps.http.post(url).expect_json().send(Event::Set); // HTTP client capability
+ }
+ Event::Set(_) => todo!(),
+ }
+}
Capabilities don’t perform side-effects themselves, they request them from the Shell. As a consequence
+the capability calls within the update
function only queue up the requests. The side-effects themselves
+are performed concurrently and don’t block the update function.
In order to use a capability, the app needs to include it in its Capabilities
associated type and WithContext
+trait implementation (which can be provided by the Effect
macro from the crux_macros
crate). For example:
mod root {
+
+// An app module which can be reused in different apps
+mod my_app {
+ use crux_core::{capability::CapabilityContext, App, render::Render};
+ use crux_macros::Effect;
+ use serde::{Serialize, Deserialize};
+
+ #[derive(Default)]
+ pub struct MyApp;
+ #[derive(Serialize, Deserialize)]
+ pub struct Event;
+
+ // The `Effect` derive macro generates an `Effect` type that is used by the
+ // Shell to dispatch side-effect requests to the right capability implementation
+ // (and, in some languages, checking that all necessary capabilities are implemented)
+ #[derive(Effect)]
+ #[effect(app = "MyApp")]
+ pub struct Capabilities {
+ pub render: Render<Event>
+ }
+
+ impl App for MyApp {
+ type Model = ();
+ type Event = Event;
+ type ViewModel = ();
+ type Capabilities = Capabilities;
+
+ fn update(&self, event: Event, model: &mut (), caps: &Capabilities) {
+ caps.render.render();
+ }
+
+ fn view(&self, model: &()) {
+ ()
+ }
+ }
+}
+}
Capabilities provide an interface to request side-effects. The interface has asynchronous semantics +with a form of callback. A typical capability call can look like this:
+ +caps.ducks.get_in_a_row(10, Event::RowOfDucks)
The call above translates into “Get 10 ducks in a row and return them to me using the RowOfDucks
event”.
+The capability’s job is to translate this request into a serializable message and instruct the Shell to
+do the duck herding and when it receives the ducks back, wrap them in the requested event and return it
+to the app.
We will refer to get_in_row
in the above call as an operation, the 10
is an input, and the
+Event::RowOfDucks
is an event constructor - a function, which eventually receives the row of ducks
+and returns a variant of the Event
enum. Conveniently, enum tuple variants can be used as functions,
+and so that will be the typical use.
This is what the capability implementation could look like:
+ +use crux_core::{
+ capability::{CapabilityContext, Operation},
+};
+use crux_macros::Capability;
+use serde::{Serialize, Deserialize};
+
+// A duck
+#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
+struct Duck;
+
+// Operations that can be requested from the Shell
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+enum DuckOperation {
+ GetInARow(usize)
+}
+
+// Respective outputs for those operations
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+enum DuckOutput {
+ GetInRow(Vec<Duck>)
+}
+
+// Link the input and output type
+impl Operation for DuckOperation {
+ type Output = DuckOutput;
+}
+
+// The capability. Context will provide the interface to the rest of the system.
+#[derive(Capability)]
+struct Ducks<Event> {
+ context: CapabilityContext<DuckOperation, Event>
+};
+
+impl<Event> Ducks<Event> {
+ pub fn new(context: CapabilityContext<DuckOperation, Event>) -> Self {
+ Self { context }
+ }
+
+ pub fn get_in_a_row<F>(&self, number_of_ducks: usize, event: F)
+ where
+ Event: 'static,
+ F: Fn(Vec<Duck>) -> Event + Send + 'static,
+ {
+ let ctx = self.context.clone();
+ // Start a shell interaction
+ self.context.spawn(async move {
+ // Instruct Shell to get ducks in a row and await the ducks
+ let ducks = ctx.request_from_shell(DuckOperation::GetInARow(number_of_ducks)).await;
+
+ // Unwrap the ducks and wrap them in the requested event
+ // This will always succeed, as long as the Shell implementation is correct
+ // and doesn't send the wrong output type back
+ if let DuckOutput::GetInRow(ducks) = ducks {
+ // Queue an app update with the ducks event
+ ctx.update_app(event(ducks));
+ }
+ })
+ }
+}
The self.context.spawn
API allows a multi-step transaction with the Shell to be performed by a capability
+without involving the app, until the exchange has completed. During the exchange, one or more events can
+be emitted (allowing a subscription or streaming like capability to be built).
For Shell requests that have no output, you can use CapabilityContext::notify_shell
.
DuckOperation
and DuckOutput
show how the set of operations can be extended. In simple capabilities,
+with a single operation, these can be structs, or simpler types. For example, the HTTP capability works directly with
+HttpRequest
and HttpResponse
.
Capability
trait for your capability. This will allow
+mapping events when composing apps from submodules.pub struct CapabilityContext<Op, Event>where
+ Op: Operation,{ /* private fields */ }
An interface for capabilities to interact with the app and the shell.
+To use update_app
, notify_shell
+or request_from_shell
, spawn a task first.
For example (from crux_time
)
+pub fn get<F>(&self, callback: F)
+where
+ F: Fn(TimeResponse) -> Ev + Send + Sync + 'static,
+{
+ let ctx = self.context.clone();
+ self.context.spawn(async move {
+ let response = ctx.request_from_shell(TimeRequest).await;
+
+ ctx.update_app(callback(response));
+ });
+}
Send an effect request to the shell, expecting an output. The
+provided operation
describes the effect input in a serialisable fashion,
+and must implement the Operation
trait to declare the expected
+output type.
request_from_shell
is returns a future of the output, which can be
+await
ed. You should only call this method inside an async task
+created with CapabilityContext::spawn
.
Send an effect request to the shell, expecting a stream of responses
+Spawn a task to do the asynchronous work. Within the task, async code +can be used to interact with the Shell and the App.
+Send an effect request to the shell in a fire and forget fashion. The
+provided operation
does not expect anything to be returned back.
Send an event to the app. The event will be processed on the next
+run of the update loop. You can call update_app
several times,
+the events will be queued up and processed sequentially after your
+async task either await
s or finishes.
Transform the CapabilityContext into one which uses the provided function to
+map each event dispatched with update_app
to a different event type.
This is useful when composing apps from modules to wrap a submodule’s +event type with a specific variant of the parent module’s event, so it can +be forwarded to the submodule when received.
+In a typical case you would implement From
on the submodule’s Capabilities
type
impl From<&Capabilities> for child::Capabilities {
+ fn from(incoming: &Capabilities) -> Self {
+ child::Capabilities {
+ some_capability: incoming.some_capability.map_event(Event::Submodule),
+ render: incoming.render.map_event(Event::Submodule),
+ }
+ }
+}
in the parent module’s update
function, you can then call .into()
on the
+capabilities, before passing them down to the submodule.
pub struct ProtoContext<Eff, Event> { /* private fields */ }
Initial version of capability Context which has not yet been specialized to a chosen capability
+Specialize the CapabilityContext to a specific capability, wrapping its operations into
+an Effect Ef
. The func
argument will typically be an Effect variant constructor, but
+can be any function taking the capability’s operation type and returning
+the effect type.
This will likely only be called from the implementation of WithContext
+for the app’s Capabilities
type. You should not need to call this function directly.
pub trait Capability<Ev> {
+ type Operation: Operation;
+ type MappedSelf<MappedEv>;
+
+ // Required method
+ fn map_event<F, NewEv>(&self, f: F) -> Self::MappedSelf<NewEv>
+ where F: Fn(NewEv) -> Ev + Send + Sync + Copy + 'static,
+ Ev: 'static,
+ NewEv: 'static + Send;
+}
Implement the Capability
trait for your capability. This will allow
+mapping events when composing apps from submodules.
Note that this implementation can be generated by the Capability
derive macro (in the crux_macros
crate).
Example:
+ +impl<Ev> Capability<Ev> for Http<Ev> {
+ type Operation = HttpOperation;
+ type MappedSelf<MappedEv> = Http<MappedEv>;
+
+ fn map_event<F, NewEvent>(&self, f: F) -> Self::MappedSelf<NewEvent>
+ where
+ F: Fn(NewEvent) -> Ev + Send + Sync + Copy + 'static,
+ Ev: 'static,
+ NewEvent: 'static,
+ {
+ Http::new(self.context.map_event(f))
+ }
+}
pub trait Operation: Serialize + PartialEq + Send + 'static {
+ type Output: DeserializeOwned + Send + 'static;
+}
Operation trait links together input and output of a side-effect.
+You implement Operation
on the payload sent by the capability to the shell using CapabilityContext::request_from_shell
.
For example (from crux_http
):
impl Operation for HttpRequest {
+ type Output = HttpResponse;
+}
Output
assigns the type this request results in.
pub trait WithContext<App, Ef>where
+ App: App,{
+ // Required method
+ fn new_with_context(
+ context: ProtoContext<Ef, App::Event>
+ ) -> App::Capabilities;
+}
Allows Crux to construct app’s set of required capabilities, providing context +they can then use to request effects and dispatch events.
+new_with_context
is called by Crux and should return an instance of the app’s Capabilities
type with
+all capabilities constructed with context passed in. Use Context::specialize
to
+create an appropriate context instance with the effect constructor which should
+wrap the requested operations.
Note that this implementation can be generated by the derive macro Effect
(in the crux_macros
crate).
impl crux_core::WithContext<App, Effect> for Capabilities {
+ fn new_with_context(
+ context: crux_core::capability::ProtoContext<Effect, Event>,
+ ) -> Capabilities {
+ Capabilities {
+ http: crux_http::Http::new(context.specialize(Effect::Http)),
+ render: crux_core::render::Render::new(context.specialize(Effect::Render)),
+ }
+ }
+}
Redirecting to ../../../crux_core/trait.Effect.html...
+ + + \ No newline at end of file diff --git a/master_api_docs/crux_core/core/request/struct.Request.html b/master_api_docs/crux_core/core/request/struct.Request.html new file mode 100644 index 000000000..03f8a3e47 --- /dev/null +++ b/master_api_docs/crux_core/core/request/struct.Request.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../crux_core/struct.Request.html...
+ + + \ No newline at end of file diff --git a/master_api_docs/crux_core/core/struct.Core.html b/master_api_docs/crux_core/core/struct.Core.html new file mode 100644 index 000000000..f3b582858 --- /dev/null +++ b/master_api_docs/crux_core/core/struct.Core.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../crux_core/struct.Core.html...
+ + + \ No newline at end of file diff --git a/master_api_docs/crux_core/index.html b/master_api_docs/crux_core/index.html new file mode 100644 index 000000000..23dbf7541 --- /dev/null +++ b/master_api_docs/crux_core/index.html @@ -0,0 +1,148 @@ +Cross-platform app development in Rust
+Crux helps you share your app’s business logic and behavior across mobile (iOS and Android) and web, +as a single, reusable core built with Rust.
+Unlike React Native, the user interface layer is built natively, with modern declarative UI frameworks +such as Swift UI, Jetpack Compose and React/Vue or a WASM based framework on the web.
+The UI layer is as thin as it can be, and all other work is done by the shared core. +The interface with the core has static type checking across languages.
+Crux applications are split into two parts: a Core written in Rust and a Shell written in the platform +native language (e.g. Swift or Kotlin). It is also possible to use Crux from Rust shells. +The Core architecture is based on Elm architecture.
+Quick glossary of terms to help you follow the example:
+Core - the shared core written in Rust
+Shell - the native side of the app on each platform handling UI and executing side effects
+App - the main module of the core containing the application logic, especially model changes +and side-effects triggered by events. App can be composed from modules, each resembling a smaller, simpler app.
+Event - main input for the core, typically triggered by user interaction in the UI
+Model - data structure (typically tree-like) holding the entire application state
+View model - data structure describing the current state of the user interface
+Effect - A side-effect the core can request from the shell. This is typically a form of I/O or similar +interaction with the host platform. Updating the UI is considered an effect.
+Capability - A user-friendly API used to request effects and provide events that should be dispatched +when the effect is completed. For example, a HTTP client is a capability.
+Below is a minimal example of a Crux-based application Core:
+ +// src/app.rs
+use crux_core::{render::Render, App};
+use crux_macros::Effect;
+use serde::{Deserialize, Serialize};
+
+// Model describing the application state
+#[derive(Default)]
+struct Model {
+ count: isize,
+}
+
+// Event describing the actions that can be taken
+#[derive(Serialize, Deserialize)]
+pub enum Event {
+ Increment,
+ Decrement,
+ Reset,
+}
+
+// Capabilities listing the side effects the Core
+// will use to request side effects from the Shell
+#[cfg_attr(feature = "typegen", derive(crux_macros::Export))]
+#[derive(Effect)]
+#[effect(app = "Hello")]
+pub struct Capabilities {
+ pub render: Render<Event>,
+}
+
+#[derive(Default)]
+struct Hello;
+
+impl App for Hello {
+ // Use the above Event
+ type Event = Event;
+ // Use the above Model
+ type Model = Model;
+ type ViewModel = String;
+ // Use the above Capabilities
+ type Capabilities = Capabilities;
+
+ fn update(&self, event: Event, model: &mut Model, caps: &Capabilities) {
+ match event {
+ Event::Increment => model.count += 1,
+ Event::Decrement => model.count -= 1,
+ Event::Reset => model.count = 0,
+ };
+
+ // Request a UI update
+ caps.render.render()
+ }
+
+ fn view(&self, model: &Model) -> Self::ViewModel {
+ format!("Count is: {}", model.count)
+ }
+}
To use the application in a user interface shell, you need to expose the core interface for FFI. +This “plumbing” will likely be simplified with macros in the future versions of Crux.
+ +// src/lib.rs
+pub mod app;
+
+use lazy_static::lazy_static;
+use wasm_bindgen::prelude::wasm_bindgen;
+
+pub use crux_core::bridge::{Bridge, Request};
+pub use crux_core::Core;
+pub use crux_http as http;
+
+pub use app::*;
+
+uniffi_macros::include_scaffolding!("hello");
+
+lazy_static! {
+ static ref CORE: Bridge<Effect, App> = Bridge::new(Core::new::<Capabilities>());
+}
+
+#[wasm_bindgen]
+pub fn process_event(data: &[u8]) -> Vec<u8> {
+ CORE.process_event(data)
+}
+
+#[wasm_bindgen]
+pub fn handle_response(uuid: &[u8], data: &[u8]) -> Vec<u8> {
+ CORE.handle_response(uuid, data)
+}
+
+#[wasm_bindgen]
+pub fn view() -> Vec<u8> {
+ CORE.view()
+}
You will also need a hello.udl
file describing the foreign function interface:
// src/hello.udl
+namespace hello {
+ sequence<u8> process_event([ByRef] sequence<u8> msg);
+ sequence<u8> handle_response([ByRef] sequence<u8> res);
+ sequence<u8> view();
+};
Finally, you will need to set up the type generation for the Model
, Message
and ViewModel
types.
+See typegen for details.
pub use self::capability::Capability;
pub use self::capability::WithContext;
Effect
from the specified Update
crux_macros
.
+This is used by the Bridge
to serialize effects going across the
+FFI boundary.Redirecting to macro.assert_effect.html...
+ + + \ No newline at end of file diff --git a/master_api_docs/crux_core/macro.assert_effect.html b/master_api_docs/crux_core/macro.assert_effect.html new file mode 100644 index 000000000..e03d07c03 --- /dev/null +++ b/master_api_docs/crux_core/macro.assert_effect.html @@ -0,0 +1,9 @@ +macro_rules! assert_effect { + ($expression:expr, $(|)? $( $pattern:pat_param )|+ $( if $guard: expr )? $(,)?) => { ... }; +}
Panics if the pattern doesn’t match an Effect
from the specified Update
Like in a match
expression, the pattern can be optionally followed by if
+and a guard expression that has access to names bound by the pattern.
use crux_core::assert_effect;
+assert_effect!(update, Effect::Render(_));
Built-in capability used to notify the Shell that a UI update is necessary.
+Render
to notify the Shell that it should update the user
+interface. This assumes a declarative UI framework is used in the Shell, which will
+take the ViewModel provided by Core::view
and reconcile the new UI state based
+on the view model with the previous one.Render
implements.pub struct Render<Ev> { /* private fields */ }
Use an instance of Render
to notify the Shell that it should update the user
+interface. This assumes a declarative UI framework is used in the Shell, which will
+take the ViewModel provided by Core::view
and reconcile the new UI state based
+on the view model with the previous one.
For imperative UIs, the Shell will need to understand the difference between the two +view models and update the user interface accordingly.
+Public API of the capability, called by App::update.
+Call render
from App::update
to signal to the Shell that
+UI should be re-drawn.
pub struct RenderOperation;
The single operation Render
implements.
source
. Read moreself
and other
values to be equal, and is used
+by ==
.pub struct Core<Ef, A>where
+ A: App,{ /* private fields */ }
The Crux core. Create an instance of this type with your effect type, and your app type as type parameters
+The core interface allows passing in events of type A::Event
using Core::process_event
.
+It will return back an effect of type Ef
, containing an effect request, with the input needed for processing
+the effect. the Effect
type can be used by shells to dispatch to the right capability implementation.
The result of the capability’s work can then be sent back to the core using Core::resolve
, passing
+in the request and the corresponding capability output type.
Create an instance of the Crux core to start a Crux application, e.g.
+ +let core: Core<HelloEffect, Hello> = Core::new::<HelloCapabilities>();
Run the app’s update
function with a given event
, returning a vector of
+effect requests.
Resolve an effect request
for operation Op
with the corresponding result.
Note that the request
is borrowed mutably. When a request that is expected to
+only be resolved once is passed in, it will be consumed and changed to a request
+which can no longer be resolved.
pub struct Request<Op>where
+ Op: Operation,{
+ pub operation: Op,
+ /* private fields */
+}
Request represents an effect request from the core to the shell.
+The operation
is the input needed to process the effect, and will be one
+of the capabilities’ Operation
types.
The request can be resolved by passing it to Core::resolve
along with the
+corresponding result of type Operation::Output
.
operation: Op
Testing support for unit testing Crux apps.
+AppTester::update
+or resolving a request with AppTester::resolve
.pub struct AppTester<App, Ef>where
+ App: App,{ /* private fields */ }
AppTester is a simplified execution environment for Crux apps for use in +tests.
+Create an instance of AppTester
with your App
and an Effect
type
+using AppTester::default
.
for example:
+ +let app = AppTester::<ExampleApp, ExampleEffect>::default();
Run the app’s update
function with an event and a model state
You can use the resulting Update
to inspect the effects which were requested
+and potential further events dispatched by capabilities.
Resolve an effect request
from previous update with an operation output.
This potentially runs the app’s update
function if the effect is completed, and
+produce another Update
.
pub struct Update<Ef, Ev> {
+ pub effects: Vec<Ef>,
+ pub events: Vec<Ev>,
+}
Update test helper holds the result of running an app update using AppTester::update
+or resolving a request with AppTester::resolve
.
effects: Vec<Ef>
Effects requested from the update run
+events: Vec<Ev>
Events dispatched from the update run
+pub trait App: Default {
+ type Event: Send + 'static;
+ type Model: Default;
+ type ViewModel: Serialize;
+ type Capabilities;
+
+ // Required methods
+ fn update(
+ &self,
+ event: Self::Event,
+ model: &mut Self::Model,
+ caps: &Self::Capabilities
+ );
+ fn view(&self, model: &Self::Model) -> Self::ViewModel;
+}
Event, typically an enum
, defines the actions that can be taken to update the application state.
ViewModel, typically a struct
describes the user interface that should be
+displayed to the user
Capabilities, typically a struct
, lists the capabilities used by this application
+Typically, Capabilities should contain at least an instance of the built-in Render
capability.
Update method defines the transition from one model
state to another in response to an event
.
Update function can mutate the model
and use the capabilities provided by the caps
argument
+to instruct the shell to perform side-effects. The side-effects will run concurrently (capability
+calls behave the same as go routines in Go or Promises in JavaScript). Capability calls
+don’t return anything, but may take a callback
event which should be dispatched when the
+effect completes.
Typically, update
should call at least Render::render
.
pub trait Effect: Send + 'static {
+ type Ffi: Serialize;
+
+ // Required method
+ fn serialize(self) -> (Self::Ffi, ResolveBytes);
+}
Implemented automatically with the Effect macro from crux_macros
.
+This is used by the Bridge
to serialize effects going across the
+FFI boundary.
pub enum State {
+ Registering(Tracer, Samples),
+ Generating(Registry),
+}
pub enum TypeGenError {
+ TypeTracing(String),
+ ValueTracing(String),
+ Deserialization(String),
+ LateRegistration,
+ Generation(String),
+ Io(Error),
+}
Generation of foreign language types (currently Swift, Java, TypeScript) for Crux
+In order to use this module, you’ll need a separate crate from your shared library, possibly
+called shared_types
. This is necessary because we need to reference types from your shared library
+during the build process (build.rs
).
This module is behind the feature called typegen
, and is not compiled into the default crate.
Ensure that you have the following line in the Cargo.toml
of your shared_types
library.
[build-dependencies]
+crux_core = { version = "0.6", features = ["typegen"] }
shared_types
library, will have an empty lib.rs
, since we only use it for generating foreign language type declarations.build.rs
in your shared_types
library, that looks something like this:use shared::{App, EffectFfi, Event};
+use crux_core::{bridge::Request, typegen::TypeGen};
+use uuid::Uuid;
+
+#[test]
+fn generate_types() {
+ let mut gen = TypeGen::new();
+
+ let sample_events = vec![Event::SendUuid(Uuid::new_v4())];
+ gen.register_type_with_samples(sample_events).unwrap();
+
+ gen.register_app::<App>().unwrap();
+
+ let temp = assert_fs::TempDir::new().unwrap();
+ let output_root = temp.join("crux_core_typegen_test");
+
+ gen.swift("SharedTypes", output_root.join("swift"))
+ .expect("swift type gen failed");
+
+ gen.java("com.example.counter.shared_types", output_root.join("java"))
+ .expect("java type gen failed");
+
+ gen.typescript("shared_types", output_root.join("typescript"))
+ .expect("typescript type gen failed");
+}
TypeGen
struct stores the registered types so that they can be generated for foreign languages
+use TypeGen::new()
to create an instancepub struct TypeGen {
+ pub state: State,
+}
The TypeGen
struct stores the registered types so that they can be generated for foreign languages
+use TypeGen::new()
to create an instance
state: State
Register all the types used in app A
to be shared with the Shell.
Do this before calling TypeGen::swift, TypeGen::java or TypeGen::typescript. +This method would normally be called in a build.rs file of a sister crate responsible for +creating “foreign language” type definitions for the FFI boundary. +See the section on +creating the shared types crate +in the Crux book for more information.
+Register sample values for types with custom serialization. This is necessary +because the type registration relies on Serde to understand the structure of the types, +and as part of the process runs a faux deserialization on each of them, with a best +guess of a default value. If that default value does not deserialize, the type registration +will fail. +You can prevent this problem by registering a valid sample value (or values), +which the deserialization will use instead.
+For each of the types that you want to share with the Shell, call this method: +e.g.
+ +#[derive(Serialize, Deserialize)]
+enum MyNestedEnum { None }
+#[derive(Serialize, Deserialize)]
+enum MyEnum { None, Nested(MyNestedEnum) }
+fn register() -> Result<(), Error> {
+ let mut gen = TypeGen::new();
+ gen.register_type::<MyEnum>()?;
+ gen.register_type::<MyNestedEnum>()?;
+ Ok(())
+}
Usually, the simple register_type()
method can generate the types you need.
+Sometimes, though, you need to provide samples of your type. The Uuid
type,
+for example, requires a sample struct to help the typegen system understand
+what it looks like. Use this method to provide samples when you register a
+type.
For each of the types that you want to share with the Shell, call this method, +providing samples of the type: +e.g.
+ + let sample_data = vec![MyUuid(Uuid::new_v4())];
+ gen.register_type_with_samples::<MyUuid>(sample_data)?;
Note: Because of the way that enums are handled by serde_reflection
,
+you may need to ensure that enums provided as samples have a first variant
+that does not use custom deserialization.
Generates types for Swift +e.g.
+ +gen.swift("SharedTypes", output_root.join("swift"))
+ .expect("swift type gen failed");
pub type Result = Result<(), TypeGenError>;
enum Result {
+ Ok(()),
+ Err(TypeGenError),
+}
pub struct Client { /* private fields */ }
An HTTP client, capable of sending Request
s
Users should only interact with this type from middlewares - normal crux code should
+make use of the Http
capability type instead.
use futures_util::future::BoxFuture;
+use crux_http::middleware::{Next, Middleware};
+use crux_http::{client::Client, Request, RequestBuilder, ResponseAsync, Result};
+use std::time;
+use std::sync::Arc;
+
+// Fetches an authorization token prior to making a request
+fn fetch_auth<'a>(mut req: Request, client: Client, next: Next<'a>) -> BoxFuture<'a, Result<ResponseAsync>> {
+ Box::pin(async move {
+ let auth_token = client.get("https://httpbin.org/get")
+ .await?
+ .body_string()
+ .await?;
+ req.append_header("Authorization", format!("Bearer {auth_token}"));
+ next.run(req, client).await
+ })
+}
Send a Request
using this client.
Submit a Request
and get the response body as bytes.
Submit a Request
and get the response body as a string.
Submit a Request
and decode the response body from json into a struct.
Submit a Request
and decode the response body from form encoding into a struct.
Any I/O error encountered while reading the body is immediately returned
+as an Err
.
If the body cannot be interpreted as valid json for the target type T
,
+an Err
is returned.
Clones the Client.
+This copies the middleware stack from the original, but shares
+the HttpClient
and http client config of the original.
+Note that individual middleware in the middleware stack are
+still shared by reference.
source
. Read moreSubscriber
to this type, returning a
+[WithDispatch
] wrapper. Read moreRedirecting to ../../crux_http/struct.Config.html...
+ + + \ No newline at end of file diff --git a/master_api_docs/crux_http/error/struct.Error.html b/master_api_docs/crux_http/error/struct.Error.html new file mode 100644 index 000000000..42ce3c017 --- /dev/null +++ b/master_api_docs/crux_http/error/struct.Error.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../crux_http/struct.Error.html...
+ + + \ No newline at end of file diff --git a/master_api_docs/crux_http/index.html b/master_api_docs/crux_http/index.html new file mode 100644 index 000000000..1e916c695 --- /dev/null +++ b/master_api_docs/crux_http/index.html @@ -0,0 +1,4 @@ +A HTTP client for use with Crux
+crux_http
allows Crux apps to make HTTP requests by asking the Shell to perform them.
This is still work in progress and large parts of HTTP are not yet supported.
+pub use http_types as http;
crux_http::Http
s and their underlying HTTP client.Response
.Middleware types
+use crux_http::middleware::{Next, Middleware};
+use crux_http::{client::Client, Request, ResponseAsync, Result};
+use std::time;
+use std::sync::Arc;
+
+/// Log each request's duration
+#[derive(Debug)]
+pub struct Logger;
+
+#[async_trait::async_trait]
+impl Middleware for Logger {
+ async fn handle(
+ &self,
+ req: Request,
+ client: Client,
+ next: Next<'_>,
+ ) -> Result<ResponseAsync> {
+ println!("sending request to {}", req.url());
+ let now = time::Instant::now();
+ let res = next.run(req, client).await?;
+ println!("request completed ({:?})", now.elapsed());
+ Ok(res)
+ }
+}
Middleware
can also be instantiated using a free function thanks to some convenient trait
+implementations.
use futures_util::future::BoxFuture;
+use crux_http::middleware::{Next, Middleware};
+use crux_http::{client::Client, Request, ResponseAsync, Result};
+use std::time;
+use std::sync::Arc;
+
+fn logger<'a>(req: Request, client: Client, next: Next<'a>) -> BoxFuture<'a, Result<ResponseAsync>> {
+ Box::pin(async move {
+ println!("sending request to {}", req.url());
+ let now = time::Instant::now();
+ let res = next.run(req, client).await?;
+ println!("request completed ({:?})", now.elapsed());
+ Ok(res)
+ })
+}
Redirecting to ../../../crux_http/middleware/struct.Redirect.html...
+ + + \ No newline at end of file diff --git a/master_api_docs/crux_http/middleware/sidebar-items.js b/master_api_docs/crux_http/middleware/sidebar-items.js new file mode 100644 index 000000000..58151e0b0 --- /dev/null +++ b/master_api_docs/crux_http/middleware/sidebar-items.js @@ -0,0 +1 @@ +window.SIDEBAR_ITEMS = {"struct":["Next","Redirect"],"trait":["Middleware"]}; \ No newline at end of file diff --git a/master_api_docs/crux_http/middleware/struct.Next.html b/master_api_docs/crux_http/middleware/struct.Next.html new file mode 100644 index 000000000..1fa6c2ada --- /dev/null +++ b/master_api_docs/crux_http/middleware/struct.Next.html @@ -0,0 +1,28 @@ +pub struct Next<'a> { /* private fields */ }
The remainder of a middleware chain, including the endpoint.
+Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read morepub struct Redirect { /* private fields */ }
A middleware which attempts to follow HTTP redirects.
+Create a new instance of the Redirect middleware, which attempts to follow redirects +up to as many times as specified.
+Consider using Redirect::default()
for the default number of redirect attempts.
This middleware will follow redirects from the Location
header if the server returns
+any of the following http response codes:
An error will be passed through the middleware stack if the value of the Location
+header is not a validly parsing url.
This will presently make at least one additional HTTP request before the actual request to +determine if there is a redirect that should be followed, so as to preserve any request body.
+
+caps.http
+ .get("https://httpbin.org/redirect/2")
+ .middleware(crux_http::middleware::Redirect::default())
+ .send(Event::ReceiveResponse)
Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read morepub trait Middleware: 'static + Send + Sync {
+ // Required method
+ fn handle<'life0, 'life1, 'async_trait>(
+ &'life0 self,
+ req: Request,
+ client: Client,
+ next: Next<'life1>
+ ) -> Pin<Box<dyn Future<Output = Result<ResponseAsync>> + Send + 'async_trait>>
+ where Self: 'async_trait,
+ 'life0: 'async_trait,
+ 'life1: 'async_trait;
+}
Middleware that wraps around remaining middleware chain.
+Asynchronously handle the request, and return a response.
+#[non_exhaustive]pub enum HttpRequestBuilderError {
+ UninitializedField(&'static str),
+ ValidationError(String),
+}
Error type for HttpRequestBuilder
+Uninitialized field
+Custom validation error
+Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read more#[non_exhaustive]pub enum HttpResponseBuilderError {
+ UninitializedField(&'static str),
+ ValidationError(String),
+}
Error type for HttpResponseBuilder
+Uninitialized field
+Custom validation error
+Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read moreThe protocol for communicating with the shell
+Crux capabilities don’t interface with the outside world themselves, they carry +out all their operations by exchanging messages with the platform specific shell. +This module defines the protocol for crux_http to communicate with the shell.
+HttpRequest
.HttpResponse
.pub struct HttpHeader {
+ pub name: String,
+ pub value: String,
+}
name: String
§value: String
source
. Read moreself
and other
values to be equal, and is used
+by ==
.Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read morepub struct HttpRequest {
+ pub method: String,
+ pub url: String,
+ pub headers: Vec<HttpHeader>,
+ pub body: Vec<u8>,
+}
method: String
§url: String
§headers: Vec<HttpHeader>
§body: Vec<u8>
source
. Read moreOutput
assigns the type this request results in.self
and other
values to be equal, and is used
+by ==
.Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read morepub struct HttpRequestBuilder { /* private fields */ }
Builder for HttpRequest
.
source
. Read moreSubscriber
to this type, returning a
+[WithDispatch
] wrapper. Read morepub struct HttpResponse {
+ pub status: u16,
+ pub headers: Vec<HttpHeader>,
+ pub body: Vec<u8>,
+}
status: u16
§headers: Vec<HttpHeader>
§body: Vec<u8>
source
. Read moreself
and other
values to be equal, and is used
+by ==
.Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read morepub struct HttpResponseBuilder { /* private fields */ }
Builder for HttpResponse
.
source
. Read moreSubscriber
to this type, returning a
+[WithDispatch
] wrapper. Read moreRedirecting to ../../crux_http/struct.Request.html...
+ + + \ No newline at end of file diff --git a/master_api_docs/crux_http/request_builder/struct.RequestBuilder.html b/master_api_docs/crux_http/request_builder/struct.RequestBuilder.html new file mode 100644 index 000000000..1d31f171b --- /dev/null +++ b/master_api_docs/crux_http/request_builder/struct.RequestBuilder.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../crux_http/struct.RequestBuilder.html...
+ + + \ No newline at end of file diff --git a/master_api_docs/crux_http/response/response/struct.Response.html b/master_api_docs/crux_http/response/response/struct.Response.html new file mode 100644 index 000000000..d518b0ee0 --- /dev/null +++ b/master_api_docs/crux_http/response/response/struct.Response.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../crux_http/struct.Response.html...
+ + + \ No newline at end of file diff --git a/master_api_docs/crux_http/response/response_async/struct.ResponseAsync.html b/master_api_docs/crux_http/response/response_async/struct.ResponseAsync.html new file mode 100644 index 000000000..787eacd3c --- /dev/null +++ b/master_api_docs/crux_http/response/response_async/struct.ResponseAsync.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../../crux_http/struct.ResponseAsync.html...
+ + + \ No newline at end of file diff --git a/master_api_docs/crux_http/sidebar-items.js b/master_api_docs/crux_http/sidebar-items.js new file mode 100644 index 000000000..b0f066145 --- /dev/null +++ b/master_api_docs/crux_http/sidebar-items.js @@ -0,0 +1 @@ +window.SIDEBAR_ITEMS = {"mod":["client","middleware","protocol","testing"],"struct":["Config","Error","Http","Request","RequestBuilder","Response","ResponseAsync"],"type":["Result"]}; \ No newline at end of file diff --git a/master_api_docs/crux_http/struct.Config.html b/master_api_docs/crux_http/struct.Config.html new file mode 100644 index 000000000..2fb225b78 --- /dev/null +++ b/master_api_docs/crux_http/struct.Config.html @@ -0,0 +1,39 @@ +#[non_exhaustive]pub struct Config {
+ pub base_url: Option<Url>,
+ pub headers: HashMap<HeaderName, HeaderValues>,
+}
Configuration for crux_http::Http
s and their underlying HTTP client.
Struct { .. }
syntax; cannot be matched against without a wildcard ..
; and struct update syntax will not work.base_url: Option<Url>
The base URL for a client. All request URLs will be relative to this URL.
+Note: a trailing slash is significant. +Without it, the last path component is considered to be a “file” name +to be removed to get at the “directory” that is used as the base.
+headers: HashMap<HeaderName, HeaderValues>
Headers to be applied to every request made by this client.
+Adds a header to be added to every request by this config.
+Default: No extra headers.
+Sets the base URL for this config. All request URLs will be relative to this URL.
+Note: a trailing slash is significant. +Without it, the last path component is considered to be a “file” name +to be removed to get at the “directory” that is used as the base.
+Default: None
(internally).
Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read morepub struct Error { /* private fields */ }
Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read morepub struct Http<Ev> { /* private fields */ }
The Http capability API.
+Instruct the Shell to perform a HTTP GET request to the provided url
.
The request can be configured via associated functions on RequestBuilder
+and then sent with RequestBuilder::send
When finished, the response will be wrapped in an event and dispatched to +the app’s `update function.
+This will panic if a malformed URL is passed.
+caps.http.get("https://httpbin.org/get").send(Event::ReceiveResponse)
Instruct the Shell to perform a HTTP HEAD request to the provided url
.
The request can be configured via associated functions on RequestBuilder
+and then sent with RequestBuilder::send
When finished, the response will be wrapped in an event and dispatched to +the app’s `update function.
+This will panic if a malformed URL is passed.
+caps.http.head("https://httpbin.org/get").send(Event::ReceiveResponse)
Instruct the Shell to perform a HTTP POST request to the provided url
.
The request can be configured via associated functions on RequestBuilder
+and then sent with RequestBuilder::send
When finished, the response will be wrapped in an event and dispatched to +the app’s `update function.
+This will panic if a malformed URL is passed.
+caps.http.post("https://httpbin.org/post").send(Event::ReceiveResponse)
Instruct the Shell to perform a HTTP PUT request to the provided url
.
The request can be configured via associated functions on RequestBuilder
+and then sent with RequestBuilder::send
When finished, the response will be wrapped in an event and dispatched to +the app’s `update function.
+This will panic if a malformed URL is passed.
+caps.http.put("https://httpbin.org/post").send(Event::ReceiveResponse)
Instruct the Shell to perform a HTTP DELETE request to the provided url
.
The request can be configured via associated functions on RequestBuilder
+and then sent with RequestBuilder::send
When finished, the response will be wrapped in an event and dispatched to +the app’s `update function.
+This will panic if a malformed URL is passed.
+caps.http.delete("https://httpbin.org/post").send(Event::ReceiveResponse)
Instruct the Shell to perform a HTTP CONNECT request to the provided url
.
The request can be configured via associated functions on RequestBuilder
+and then sent with RequestBuilder::send
When finished, the response will be wrapped in an event and dispatched to +the app’s `update function.
+This will panic if a malformed URL is passed.
+caps.http.connect("https://httpbin.org/get").send(Event::ReceiveResponse)
Instruct the Shell to perform a HTTP OPTIONS request to the provided url
.
The request can be configured via associated functions on RequestBuilder
+and then sent with RequestBuilder::send
When finished, the response will be wrapped in an event and dispatched to +the app’s `update function.
+This will panic if a malformed URL is passed.
+caps.http.options("https://httpbin.org/get").send(Event::ReceiveResponse)
Instruct the Shell to perform a HTTP TRACE request to the provided url
.
The request can be configured via associated functions on RequestBuilder
+and then sent with RequestBuilder::send
When finished, the response will be wrapped in an event and dispatched to +the app’s `update function.
+This will panic if a malformed URL is passed.
+caps.http.trace("https://httpbin.org/get").send(Event::ReceiveResponse)
Instruct the Shell to perform a HTTP PATCH request to the provided url
.
The request can be configured via associated functions on RequestBuilder
+and then sent with RequestBuilder::send
When finished, the response will be wrapped in an event and dispatched to +the app’s `update function.
+This will panic if a malformed URL is passed.
+Instruct the Shell to perform an HTTP request with the provided method
and url
.
The request can be configured via associated functions on RequestBuilder
+and then sent with RequestBuilder::send
When finished, the response will be wrapped in an event and dispatched to +the app’s `update function.
+Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read morepub struct Request { /* private fields */ }
An HTTP request, returns a Response
.
Create a new instance.
+This method is particularly useful when input URLs might be passed by third parties, and +you don’t want to panic if they’re malformed. If URLs are statically encoded, it might be +easier to use one of the shorthand methods instead.
+fn main() -> crux_http::Result<()> {
+use crux_http::http::{Url, Method};
+
+let url = Url::parse("https://httpbin.org/get")?;
+let req = crux_http::Request::new(Method::Get, url);
Get the URL querystring.
+#[derive(Serialize, Deserialize)]
+struct Index {
+ page: u32
+}
+
+let req = caps.http.get("https://httpbin.org/get?page=2").build();
+let Index { page } = req.query()?;
+assert_eq!(page, 2);
Set the URL querystring.
+#[derive(Serialize, Deserialize)]
+struct Index {
+ page: u32
+}
+
+let query = Index { page: 2 };
+let mut req = caps.http.get("https://httpbin.org/get").build();
+req.set_query(&query)?;
+assert_eq!(req.url().query(), Some("page=2"));
+assert_eq!(req.url().as_str(), "https://httpbin.org/get?page=2");
Get an HTTP header.
+let mut req = caps.http.get("https://httpbin.org/get").build();
+req.set_header("X-Requested-With", "surf");
+assert_eq!(req.header("X-Requested-With").unwrap(), "surf");
Get a mutable reference to a header.
+Set an HTTP header.
+Append a header to the headers.
+Unlike insert
this function will not override the contents of a header, but insert a
+header if there aren’t any. Or else append to the existing list of headers.
Remove a header.
+An iterator visiting all header pairs in arbitrary order, with mutable references to the +values.
+An iterator visiting all header names in arbitrary order.
+An iterator visiting all header values in arbitrary order.
+Set an HTTP header.
+let mut req = caps.http.get("https://httpbin.org/get").build();
+req.set_header("X-Requested-With", "surf");
+assert_eq!(req.header("X-Requested-With").unwrap(), "surf");
Set a request extension value.
+Get the request HTTP method.
+let req = caps.http.get("https://httpbin.org/get").build();
+assert_eq!(req.method(), crux_http::http::Method::Get);
Get the request url.
+use crux_http::http::Url;
+let req = caps.http.get("https://httpbin.org/get").build();
+assert_eq!(req.url(), &Url::parse("https://httpbin.org/get")?);
Get the request content type as a Mime
.
Gets the Content-Type
header and parses it to a Mime
type.
This method will panic if an invalid MIME type was set as a header. Use the set_header
+method to bypass any checks.
Set the request content type from a Mime
.
Get the length of the body stream, if it has been set.
+This value is set when passing a fixed-size object into as the body.
+E.g. a string, or a buffer. Consumers of this API should check this
+value to decide whether to use Chunked
encoding, or set the
+response length.
Returns true
if the set length of the body stream is zero, false
+otherwise.
Pass an AsyncRead
stream as the request body.
The encoding is set to application/octet-stream
.
Take the request body as a Body
.
This method can be called after the body has already been taken or read,
+but will return an empty Body
.
This is useful for consuming the body via an AsyncReader or AsyncBufReader.
+Pass a file as the request body.
+The content-type
is set based on the file extension using mime_guess
if the operation was
+successful. If path
has no extension, or its extension has no known MIME type mapping,
+then None
is returned.
This method will return an error if the file couldn’t be read.
+Push middleware onto a per-request middleware stack.
+Important: Setting per-request middleware incurs extra allocations.
+Creating a Client
with middleware is recommended.
Client middleware is run before per-request middleware.
+See the middleware submodule for more information on middleware.
+let mut req = caps.http.get("https://httpbin.org/get").build();
+req.middleware(crux_http::middleware::Redirect::default());
Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read morepub struct RequestBuilder<Event, ExpectBody = Vec<u8>> { /* private fields */ }
Request Builder
+Provides an ergonomic way to chain the creation of a request.
+This is generally accessed as the return value from Http::{method}()
.
use crux_http::http::{mime::HTML};
+caps.http
+ .post("https://httpbin.org/post")
+ .body("<html>hi</html>")
+ .header("custom-header", "value")
+ .content_type(HTML)
+ .send(Event::ReceiveResponse)
Sets a header on the request.
+caps.http
+ .get("https://httpbin.org/get")
+ .body("<html>hi</html>")
+ .header("header-name", "header-value")
+ .send(Event::ReceiveResponse)
Sets the Content-Type header on the request.
+caps.http
+ .get("https://httpbin.org/get")
+ .content_type(mime::HTML)
+ .send(Event::ReceiveResponse)
Sets the body of the request from any type with implements Into<Body>
, for example, any type with is AsyncRead
.
The encoding is set to application/octet-stream
.
use serde_json::json;
+use crux_http::http::mime;
+caps.http
+ .post("https://httpbin.org/post")
+ .body(json!({"any": "Into<Body>"}))
+ .content_type(mime::HTML)
+ .send(Event::ReceiveResponse)
Pass JSON as the request body.
+The encoding is set to application/json
.
This method will return an error if the provided data could not be serialized to JSON.
+#[derive(Deserialize, Serialize)]
+struct Ip {
+ ip: String
+}
+
+let data = &Ip { ip: "129.0.0.1".into() };
+caps.http
+ .post("https://httpbin.org/post")
+ .body_json(data)
+ .expect("could not serialize body")
+ .send(Event::ReceiveResponse)
Set the URL querystring.
+#[derive(Serialize, Deserialize)]
+struct Index {
+ page: u32
+}
+
+let query = Index { page: 2 };
+caps.http
+ .post("https://httpbin.org/post")
+ .query(&query)
+ .expect("could not serialize query string")
+ .send(Event::ReceiveResponse)
Push middleware onto a per-request middleware stack.
+Important: Setting per-request middleware incurs extra allocations.
+Creating a Client
with middleware is recommended.
Client middleware is run before per-request middleware.
+See the middleware submodule for more information on middleware.
+
+caps.http
+ .get("https://httpbin.org/redirect/2")
+ .middleware(crux_http::middleware::Redirect::default())
+ .send(Event::ReceiveResponse)
Decode a String from the response body prior to dispatching it to the apps update
+function
enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<String>>) }
+
+caps.http
+ .post("https://httpbin.org/json")
+ .expect_string()
+ .send(Event::ReceiveResponse)
Decode a T
from a JSON response body prior to dispatching it to the apps update
+function
#[derive(Deserialize)]
+struct Response {
+ slideshow: Slideshow
+}
+
+#[derive(Deserialize)]
+struct Slideshow {
+ author: String
+}
+
+enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Slideshow>>) }
+
+caps.http
+ .post("https://httpbin.org/json")
+ .expect_json::<Slideshow>()
+ .send(Event::ReceiveResponse)
Sends the constructed Request
and returns its result as an update Event
When finished, the response will wrapped in an event using make_event
and
+dispatched to the app’s `update function.
Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read morepub struct Response<Body> { /* private fields */ }
An HTTP Response that will be passed to in a message to an apps update function
+Get the HTTP protocol version.
+use crux_http::http::Version;
+assert_eq!(res.version(), Some(Version::Http1_1));
Get an HTTP header mutably.
+Remove a header.
+Insert an HTTP header.
+Append an HTTP header.
+An iterator visiting all header pairs in arbitrary order, with mutable references to the +values.
+An iterator visiting all header names in arbitrary order.
+An iterator visiting all header values in arbitrary order.
+Reads the entire response body into a string.
+This method can be called after the body has already been read, but will +produce an empty buffer.
+If the “encoding” feature is enabled, this method tries to decode the body +with the encoding that is specified in the Content-Type header. If the header +does not specify an encoding, UTF-8 is assumed. If the “encoding” feature is +disabled, Surf only supports reading UTF-8 response bodies. The “encoding” +feature is enabled by default.
+Any I/O error encountered while reading the body is immediately returned
+as an Err
.
If the body cannot be interpreted because the encoding is unsupported or
+incorrect, an Err
is returned.
let string: String = res.body_string()?;
+assert_eq!(string, "hello");
Reads and deserialized the entire request body from json.
+Any I/O error encountered while reading the body is immediately returned
+as an Err
.
If the body cannot be interpreted as valid json for the target type T
,
+an Err
is returned.
#[derive(Deserialize, Serialize)]
+struct Ip {
+ ip: String
+}
+
+let Ip { ip } = res.body_json()?;
+assert_eq!(ip, "127.0.0.1");
Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read morepub struct ResponseAsync { /* private fields */ }
An HTTP response that exposes async methods, for use inside middleware.
+If you’re not writing middleware you’ll never need to interact with +this type and can probably ignore it.
+Get the HTTP status code.
+let res = client.get("https://httpbin.org/get").await?;
+assert_eq!(res.status(), 200);
Get the HTTP protocol version.
+use crux_http::http::Version;
+
+let res = client.get("https://httpbin.org/get").await?;
+assert_eq!(res.version(), Some(Version::Http1_1));
Get a header.
+let res = client.get("https://httpbin.org/get").await?;
+assert!(res.header("Content-Length").is_some());
Get an HTTP header mutably.
+Remove a header.
+Insert an HTTP header.
+Append an HTTP header.
+An iterator visiting all header pairs in arbitrary order, with mutable references to the +values.
+An iterator visiting all header names in arbitrary order.
+An iterator visiting all header values in arbitrary order.
+Get a response scoped extension value.
+Set a response scoped extension value.
+Get the response content type as a Mime
.
Gets the Content-Type
header and parses it to a Mime
type.
This method will panic if an invalid MIME type was set as a header.
+use crux_http::http::mime;
+let res = client.get("https://httpbin.org/json").await?;
+assert_eq!(res.content_type(), Some(mime::JSON));
Get the length of the body stream, if it has been set.
+This value is set when passing a fixed-size object into as the body.
+E.g. a string, or a buffer. Consumers of this API should check this
+value to decide whether to use Chunked
encoding, or set the
+response length.
Returns true
if the set length of the body stream is zero, false
+otherwise.
Take the response body as a Body
.
This method can be called after the body has already been taken or read,
+but will return an empty Body
.
Useful for adjusting the whole body, such as in middleware.
+Swaps the value of the body with another body, without deinitializing +either one.
+Reads the entire request body into a byte buffer.
+This method can be called after the body has already been read, but will +produce an empty buffer.
+Any I/O error encountered while reading the body is immediately returned
+as an Err
.
let mut res = client.get("https://httpbin.org/get").await?;
+let bytes: Vec<u8> = res.body_bytes().await?;
Reads the entire response body into a string.
+This method can be called after the body has already been read, but will +produce an empty buffer.
+If the “encoding” feature is enabled, this method tries to decode the body +with the encoding that is specified in the Content-Type header. If the header +does not specify an encoding, UTF-8 is assumed. If the “encoding” feature is +disabled, Surf only supports reading UTF-8 response bodies. The “encoding” +feature is enabled by default.
+Any I/O error encountered while reading the body is immediately returned
+as an Err
.
If the body cannot be interpreted because the encoding is unsupported or
+incorrect, an Err
is returned.
let mut res = client.get("https://httpbin.org/get").await?;
+let string: String = res.body_string().await?;
Reads and deserialized the entire request body from json.
+Any I/O error encountered while reading the body is immediately returned
+as an Err
.
If the body cannot be interpreted as valid json for the target type T
,
+an Err
is returned.
#[derive(Deserialize, Serialize)]
+struct Ip {
+ ip: String
+}
+
+let mut res = client.get("https://api.ipify.org?format=json").await?;
+let Ip { ip } = res.body_json().await?;
Reads and deserialized the entire request body from form encoding.
+Any I/O error encountered while reading the body is immediately returned
+as an Err
.
If the body cannot be interpreted as valid json for the target type T
,
+an Err
is returned.
#[derive(Deserialize, Serialize)]
+struct Body {
+ apples: u32
+}
+
+let mut res = client.get("https://api.example.com/v1/response").await?;
+let Body { apples } = res.body_form().await?;
buf
in asynchronous
+manner, returning a future type. Read moreAsyncRead
into bufs
using vectored
+IO operations. Read morebuf
,
+returning an error if end of file (EOF) is hit sooner. Read moreAsyncRead
. Read moreAsyncRead
. Read morebuf
. Read morelimit
bytes from it. Read morebuf
. Read morelimit
bytes from it. Read morebuf
. Read morelimit
bytes from it. Read moreRead
. Read moreSubscriber
to this type, returning a
+[WithDispatch
] wrapper. Read moreRedirecting to ../../../crux_http/testing/struct.ResponseBuilder.html...
+ + + \ No newline at end of file diff --git a/master_api_docs/crux_http/testing/sidebar-items.js b/master_api_docs/crux_http/testing/sidebar-items.js new file mode 100644 index 000000000..37556f23b --- /dev/null +++ b/master_api_docs/crux_http/testing/sidebar-items.js @@ -0,0 +1 @@ +window.SIDEBAR_ITEMS = {"struct":["ResponseBuilder"]}; \ No newline at end of file diff --git a/master_api_docs/crux_http/testing/struct.ResponseBuilder.html b/master_api_docs/crux_http/testing/struct.ResponseBuilder.html new file mode 100644 index 000000000..cf658d788 --- /dev/null +++ b/master_api_docs/crux_http/testing/struct.ResponseBuilder.html @@ -0,0 +1,33 @@ +pub struct ResponseBuilder<Body> { /* private fields */ }
Allows users to build an http response.
+This is mostly expected to be useful in tests rather than application code.
+Constructs a new ResponseBuilder with the 200 OK status code.
+Constructs a new ResponseBuilder with the specified status code.
+Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read morepub enum KeyValueOperation {
+ Read(String),
+ Write(String, Vec<u8>),
+}
Supported operations
+source
. Read moreOutput
assigns the type this request results in.self
and other
values to be equal, and is used
+by ==
.pub enum KeyValueOutput {
+ Read(Option<Vec<u8>>),
+ Write(bool),
+}
source
. Read moreself
and other
values to be equal, and is used
+by ==
.A basic Key-Value store for use with Crux
+crux_kv
allows Crux apps to store and retrieve arbitrary data by asking the Shell to
+persist the data using platform native capabilities (e.g. disk or web localStorage)
This is still work in progress and extremely basic.
+pub struct KeyValue<Ev> { /* private fields */ }
Read a value under key
, will dispatch the event with a
+KeyValueOutput::Read(Option<Vec<u8>>)
as payload
Set key
to be the provided value
. Typically the bytes would be
+a value serialized/deserialized by the app.
Will dispatch the event with a KeyValueOutput::Write(bool)
as payload
#[derive(Capability)]
#[derive(Effect)]
+{
+ // Attributes available to this derive:
+ #[effect]
+}
+
#[derive(Export)]
TODO mod docs
+pub struct Platform<Ev> { /* private fields */ }
pub struct PlatformRequest;
source
. Read moreOutput
assigns the type this request results in.self
and other
values to be equal, and is used
+by ==
.pub struct PlatformResponse(pub String);
0: String
self
and other
values to be equal, and is used
+by ==
.Current time access for Crux apps
+Current time (on a wall clock) is considered a side-effect (although if we were to get pedantic, it’s +more of a side-cause) by Crux, and has to be obtained externally. This capability provides a simple +interface to do so.
+This is still work in progress and as such very basic. It returns time as an IS08601 string.
+pub struct Time<Ev> { /* private fields */ }
The Time capability API.
+pub struct TimeRequest;
source
. Read moreOutput
assigns the type this request results in.self
and other
values to be equal, and is used
+by ==
.pub struct TimeResponse(pub String);
0: String
source
. Read moreself
and other
values to be equal, and is used
+by ==
.U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","","temporary","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","","","","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","","","","","","","","","","","","","","","","","","","","","","","","","","","","","Returns the argument unchanged.","","Calls U::from(self)
.","","","","","","","","","","","","files in second but not in first","","Trim whitespace from end of line and ensure trailing …","files in both first and second","test if file is source code","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","","","","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","","","","","","","","","","","","","","","","","","","","","","","","","","","","","",""],"i":[0,0,0,0,0,0,0,0,0,9,0,6,10,6,10,9,9,6,9,10,6,9,10,6,6,6,6,9,10,6,9,10,6,9,10,10,6,9,10,6,9,10,6,9,10,6,10,9,6,6,9,10,6,9,10,6,6,6,9,10,6,9,10,6,9,10,6,9,10,6,9,10,6,0,0,0,16,16,18,19,16,18,19,16,19,18,16,16,18,19,16,18,19,16,16,18,19,16,18,19,16,18,19,16,18,19,16,18,19,16,18,19,16,18,19,16,16,18,19,16,18,19,19,16,18,19,16,18,19,18,16,18,19,0,23,23,23,23,23,23,23,23,23,0,23,23,23,23,0,0,39,39,0,0,0,0,0,0,0,39,0,0,39,0,0,28,0,28,0,28,32,34,28,32,34,32,34,34,32,34,32,28,32,34,28,32,34,28,32,34,28,32,34,28,32,34,28,32,34,32,34,32,34,32,34,32,34,32,34,32,34,34,34,34,28,32,34,28,32,34,34,28,32,34,32,34,0,0,0],"f":[0,0,0,0,[[],[[2,[1]]]],0,0,0,0,0,0,[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[],3],0,[[],3],[4,-1,[]],[4,-1,[]],[4,-1,[]],[4,-1,[]],[4,-1,[]],[4,-1,[]],[4,1],[4,1],[4,1],0,[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[5,[[8,[6,7]]]],[5,[[8,[9,7]]]],[5,[[8,[10,7]]]],[5,[[8,[6,7]]]],[5,[[8,[9,7]]]],[5,[[8,[10,7]]]],[[],[[12,[11]]]],[[],[[12,[11]]]],[13,14],0,[[],4],[[],4],[[],4],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,0,[-1,[[8,[-2]]],[],[]],[-1,[[8,[-2]]],[],[]],[-1,[[8,[-2]]],[],[]],[-1,[[8,[-2]]],[],[]],[-1,[[8,[-2]]],[],[]],[-1,[[8,[-2]]],[],[]],[-1,15,[]],[-1,15,[]],[-1,15,[]],[[6,5],[[8,[1,7]]]],[[9,5],[[8,[1,7]]]],[[10,5],[[8,[1,7]]]],[[6,5],[[8,[1,7]]]],[[9,5],[[8,[1,7]]]],[[10,5],[[8,[1,7]]]],0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,0,0,[[],16],[4,-1,[]],[4,-1,[]],[4,-1,[]],[4,-1,[]],[4,-1,[]],[4,-1,[]],0,[-1,[[8,[16]]],17],[-1,[[8,[18]]],17],[-1,[[8,[19]]],17],[4,1],[4,1],[4,1],[[16,20],21],[[18,20],21],[[19,20],21],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[[],4],[[],4],[[],4],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,0,0,0,[[16,-1],8,22],[[18,-1],8,22],[[19,-1],8,22],0,0,0,0,[-1,[[8,[-2]]],[],[]],[-1,[[8,[-2]]],[],[]],[-1,[[8,[-2]]],[],[]],[-1,[[8,[-2]]],[],[]],[-1,[[8,[-2]]],[],[]],[-1,[[8,[-2]]],[],[]],0,[-1,15,[]],[-1,15,[]],[-1,15,[]],0,[-1,-2,[],[]],[-1,-2,[],[]],[4,-1,[]],[4,-1,[]],[4,1],[[23,20],21],[-1,-1,[]],[[],4],[-1,-2,[],[]],[[24,13,13],1],[-1,25,[]],[-1,[[8,[-2]]],[],[]],[-1,[[8,[-2]]],[],[]],[-1,15,[]],0,0,0,0,[[[27,[26,25]],[27,[26,25]]],1],[[24,24,28,29,14],[[2,[1,30]]]],[[[27,[26,25]],[27,[26,25]]],[[31,[26]]]],[[24,[12,[24]],29,14],[[2,[1]]]],[13,25],[[[27,[26,25]],[27,[26,25]]],[[31,[26]]]],[24,14],0,[[[27,[26,25]],[27,[26,25]]],1],[[24,24,28,29,14],[[2,[[1,[[27,[26,25]],[27,[26,25]]]]]]]],0,[24,[[2,[1]]]],0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[32,33],4],[[34,33],4],0,0,0,0,[4,-1,[]],[4,-1,[]],[4,-1,[]],[4,-1,[]],[4,-1,[]],[4,-1,[]],[4,1],[4,1],[4,1],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[[],4],[[],4],[[],4],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[16,18],28],[[16,18,19],28],[[32,35,13,-1],[[8,[14]]],36],[[34,35,13,-1],[[8,[14]]],36],[[32,35,13,[37,[-1]],-2],[[8,[14]]],38,36],[[34,35,13,[37,[-1]],-2],[[8,[14]]],38,36],[[32,35,13,[37,[-1]],-2],[[8,[14]]],38,36],[[34,35,13,[37,[-1]],-2],[[8,[14]]],38,36],[[32,35,13,-1],[[8,[14]]],36],[[34,35,13,-1],[[8,[14]]],36],[[32,[37,[-1]],-2],[[8,[1]]],38,36],[[34,[37,[-1]],-2],[[8,[1]]],38,36],0,0,0,[-1,[[8,[-2]]],[],[]],[-1,[[8,[-2]]],[],[]],[-1,[[8,[-2]]],[],[]],[-1,[[8,[-2]]],[],[]],[-1,[[8,[-2]]],[],[]],[-1,[[8,[-2]]],[],[]],0,[-1,15,[]],[-1,15,[]],[-1,15,[]],0,0,0,[[],[[2,[16]]]],[16,[[2,[1]]]]],"c":[],"p":[[15,"tuple"],[6,"Result",229],[3,"Command",230],[15,"usize"],[3,"ArgMatches",231],[3,"Cli",7],[6,"Error",232],[4,"Result",233],[4,"Commands",7],[3,"DoctorArgs",7],[3,"Id",234],[4,"Option",235],[15,"str"],[15,"bool"],[3,"TypeId",236],[3,"Workspace",73],[8,"Deserializer",237],[3,"Core",73],[3,"Shell",73],[3,"Formatter",238],[6,"Result",238],[8,"Serializer",239],[3,"Line",133],[3,"Path",240],[3,"String",241],[3,"PathBuf",240],[3,"BTreeMap",242],[4,"Context",164],[15,"u8"],[3,"Error",229],[3,"Vec",243],[3,"CoreContext",164],[3,"Template",244],[3,"ShellContext",164],[15,"u64"],[8,"Encoder",245],[3,"Section",246],[8,"ContentSequence",247],[6,"FileMap",148]]},\
+"crux_core":{"doc":"Cross-platform app development in Rust","t":"IQCDIQQQDQCOLLLLAALLLLLLLMLALKLALLLLLLAKKLDDLLLLLMLLLLLLLLLLLLLLLMLIDQIQQDILLLLLLLLLLKLKLLLLLLLLLLLLLDDLLLLLLLLLLLLLLLLLLLLLLLLLDDLLLLLLLMLMLLLLLLLLLLLLLLLNNINNNNNNGEDENNLLLLLLLLLLLLLLLLLLLLLLLKLMLLLLLLLLLLLL","n":["App","Capabilities","Capability","Core","Effect","Event","Ffi","Model","Request","ViewModel","WithContext","assert_effect","borrow","borrow","borrow_mut","borrow_mut","bridge","capability","default","fmt","from","from","into","into","new","operation","process_event","render","resolve","serialize","serialize","testing","try_from","try_from","try_into","try_into","type_id","type_id","typegen","update","view","view","Bridge","Request","borrow","borrow","borrow_mut","borrow_mut","deserialize","effect","fmt","from","from","handle_response","into","into","new","process_event","serialize","try_from","try_from","try_into","try_into","type_id","type_id","uuid","view","Capability","CapabilityContext","MappedSelf","Operation","Operation","Output","ProtoContext","WithContext","borrow","borrow","borrow_mut","borrow_mut","clone","clone_into","from","from","into","into","map_event","map_event","new_with_context","notify_shell","request_from_shell","spawn","specialize","stream_from_shell","to_owned","try_from","try_from","try_into","try_into","type_id","type_id","update_app","Render","RenderOperation","borrow","borrow","borrow_mut","borrow_mut","clone","clone_into","deserialize","eq","equivalent","fmt","from","from","into","into","map_event","new","render","serialize","to_owned","try_from","try_from","try_into","try_into","type_id","type_id","AppTester","Update","as_ref","borrow","borrow","borrow_mut","borrow_mut","default","effects","effects","effects_mut","events","fmt","from","from","into","into","into_effects","resolve","try_from","try_from","try_into","try_into","type_id","type_id","update","view","Deserialization","Err","Export","Generating","Generation","Io","LateRegistration","Ok","Registering","Result","State","TypeGen","TypeGenError","TypeTracing","ValueTracing","borrow","borrow","borrow","borrow_mut","borrow_mut","borrow_mut","default","fmt","fmt","fmt","from","from","from","from","into","into","into","java","new","register_app","register_samples","register_type","register_type_with_samples","register_types","source","state","swift","to_string","try_from","try_from","try_from","try_into","try_into","try_into","type_id","type_id","type_id","typescript"],"q":[[0,"crux_core"],[42,"crux_core::bridge"],[67,"crux_core::capability"],[101,"crux_core::render"],[128,"crux_core::testing"],[155,"crux_core::typegen"],[208,"core::fmt"],[209,"core::fmt"],[210,"core::ops::function"],[211,"core::result"],[212,"core::any"],[213,"serde::de"],[214,"serde::ser"],[215,"serde::de"],[216,"serde::ser"],[217,"core::iter::traits::iterator"],[218,"anyhow"],[219,"std::io::error"],[220,"std::path"],[221,"core::convert"],[222,"core::error"],[223,"core::option"],[224,"alloc::string"]],"d":["Implement App
on your type to make it into a Crux app. Use …","Capabilities, typically a struct
, lists the capabilities …","","The Crux core. Create an instance of this type with your …","Implemented automatically with the Effect macro from …","Event, typically an enum
, defines the actions that can be …","Ffi is an enum with variants corresponding to the Effect …","Model, typically a struct
defines the internal state of …","Request represents an effect request from the core to the …","ViewModel, typically a struct
describes the user interface …","","Panics if the pattern doesn’t match an Effect
from the …","","","","","","Capabilities provide a user-friendly API to request …","","","Returns the argument unchanged.","Returns the argument unchanged.","Calls U::from(self)
.","Calls U::from(self)
.","Create an instance of the Crux core to start a Crux …","","Run the app’s update
function with a given event
, …","Built-in capability used to notify the Shell that a UI …","Resolve an effect request
for operation Op
with the …","Converts the Effect
into its FFI counterpart and returns …","Serialize this effect request using effect
as a constructor","Testing support for unit testing Crux apps.","","","","","","","Generation of foreign language types (currently Swift, …","Update method defines the transition from one model
state …","View method is used by the Shell to request the current …","Get the current state of the app’s view model.","Bridge is a core wrapper presenting the same interface as …","Request for a side-effect passed from the Core to the …","","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Receive a response to a capability request from the shell.","Calls U::from(self)
.","Calls U::from(self)
.","Create a new Bridge using the provided core
.","Receive an event from the shell.","","","","","","","","","Get the current state of the app’s view model …","Implement the Capability
trait for your capability. This …","An interface for capabilities to interact with the app and …","","Operation trait links together input and output of a …","","Output
assigns the type this request results in.","Initial version of capability Context which has not yet …","Allows Crux to construct app’s set of required …","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Calls U::from(self)
.","Calls U::from(self)
.","","Transform the CapabilityContext into one which uses the …","","Send an effect request to the shell in a fire and forget …","Send an effect request to the shell, expecting an output. …","Spawn a task to do the asynchronous work. Within the task, …","Specialize the CapabilityContext to a specific capability, …","Send an effect request to the shell, expecting a stream of …","","","","","","","","Send an event to the app. The event will be processed on …","Use an instance of Render
to notify the Shell that it …","The single operation Render
implements.","","","","","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Calls U::from(self)
.","Calls U::from(self)
.","","","Call render
from App::update
to signal to the Shell that …","","","","","","","","","AppTester is a simplified execution environment for Crux …","Update test helper holds the result of running an app …","","","","","","","","Effects requested from the update run","","Events dispatched from the update run","","Returns the argument unchanged.","Returns the argument unchanged.","Calls U::from(self)
.","Calls U::from(self)
.","","Resolve an effect request
from previous update with an …","","","","","","","Run the app’s update
function with an event and a model …","Run the app’s view
function with a model state","","Contains the error value","","","","","","Contains the success value","","","","The TypeGen
struct stores the registered types so that …","","","","","","","","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","","Returns the argument unchanged.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Generates types for Java (for use with Kotlin) e.g.","Creates an instance of the TypeGen
struct","Register all the types used in app A
to be shared with the …","Register sample values for types with custom …","For each of the types that you want to share with the …","Usually, the simple register_type()
method can generate …","","","","Generates types for Swift e.g.","","","","","","","","","","","Generates types for TypeScript e.g."],"i":[0,3,0,0,0,3,2,3,0,3,0,0,4,1,4,1,0,0,1,4,4,1,4,1,1,4,1,0,1,2,4,0,4,1,4,1,4,1,0,3,3,1,0,0,18,14,18,14,14,14,14,18,14,18,18,14,18,18,14,18,14,18,14,18,14,14,18,0,0,46,0,46,7,0,0,23,26,23,26,23,23,23,26,23,26,46,23,47,23,23,23,26,23,23,23,26,23,26,23,26,23,0,0,30,28,30,28,28,28,28,28,28,28,30,28,30,28,30,30,30,28,28,30,28,30,28,30,28,0,0,31,31,32,31,32,31,32,32,32,32,32,31,32,31,32,32,31,31,32,31,32,31,32,31,31,36,40,0,37,36,36,36,40,37,0,0,0,0,36,36,35,36,37,35,36,37,35,36,36,37,35,36,36,37,35,36,37,35,35,35,35,35,35,48,36,35,35,36,35,36,37,35,36,37,35,36,37,35],"f":[0,0,0,0,0,0,0,0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,0,[[],[[1,[-1,-2]]],2,3],[[[4,[-1]],5],6,[7,8]],[-1,-1,[]],[-1,-1,[]],[-1,-2,[],[]],[-1,-2,[],[]],[[],[[1,[-1,-2]]],2,3],0,[[[1,[-1,-2]]],[[9,[-1]]],2,3],0,[[[1,[-1,-2]],[4,[-3]]],[[9,[-1]]],2,3,7],[-1,[[10,[0]]],[]],[[[4,[-1]],-2],[[10,[-3,0]]],7,11,[]],0,[-1,[[12,[-2]]],[],[]],[-1,[[12,[-2]]],[],[]],[-1,[[12,[-2]]],[],[]],[-1,[[12,[-2]]],[],[]],[-1,13,[]],[-1,13,[]],0,[-1,10,[]],[-1,[],[]],[[[1,[-1,-2]]],[],2,3],0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,[[12,[[14,[-2]]]]],15,[16,17]],0,[[[14,[-1]],5],6,[16,8]],[-1,-1,[]],[-1,-1,[]],[[[18,[-1,-2]],[20,[19]],[20,[19]]],[[9,[19]]],[2,21],3],[-1,-2,[],[]],[-1,-2,[],[]],[[[1,[-1,-2]]],[[18,[-1,-2]]],[2,21],3],[[[18,[-1,-2]],[20,[19]]],[[9,[19]]],[2,21],3],[[[14,[-1]],-2],12,[16,16],22],[-1,[[12,[-2]]],[],[]],[-1,[[12,[-2]]],[],[]],[-1,[[12,[-2]]],[],[]],[-1,[[12,[-2]]],[],[]],[-1,13,[]],[-1,13,[]],0,[[[18,[-1,-2]]],[[9,[19]]],[2,21],3],0,0,0,0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[[23,[-1,-2]]],[[23,[-1,-2]]],7,[]],[[-1,-2],10,[],[]],[-1,-1,[]],[-1,-1,[]],[-1,-2,[],[]],[-1,-2,[],[]],[[-1,-2],[],[],[11,21,24,25]],[[[23,[-1,-2]],-3],[[23,[-1,-4]]],7,[],[11,24,21],[]],[[[26,[-1]]],[],[]],[[[23,[-1,-2]],-1],10,7,[]],0,[[[23,[-1,-2]],-3],10,7,[],[27,21]],[[[26,[-1,-2]],-3],[[23,[-4,-2]]],[],[],[11,24,21,25],7],0,[-1,-2,[],[]],[-1,[[12,[-2]]],[],[]],[-1,[[12,[-2]]],[],[]],[-1,[[12,[-2]]],[],[]],[-1,[[12,[-2]]],[],[]],[-1,13,[]],[-1,13,[]],[[[23,[-1,-2]],-2],10,7,[]],0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[28,28],[[-1,-2],10,[],[]],[-1,[[12,[28]]],15],[[28,28],29],[[-1,-2],29,[],[]],[[28,5],6],[-1,-1,[]],[-1,-1,[]],[-1,-2,[],[]],[-1,-2,[],[]],[[[30,[-1]],-2],[],[],[11,21,24,25]],[[[23,[28,-1]]],[[30,[-1]]],[]],[[[30,[-1]]],10,[]],[[28,-1],12,22],[-1,-2,[],[]],[-1,[[12,[-2]]],[],[]],[-1,[[12,[-2]]],[],[]],[-1,[[12,[-2]]],[],[]],[-1,[[12,[-2]]],[],[]],[-1,13,[]],[-1,13,[]],0,0,[[[31,[-1,-2]]],[],3,[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[],[[31,[-1,-2]]],3,21],[[[32,[-1,-2]]],[[0,[33]]],[],[]],0,[[[32,[-1,-2]]],[[0,[33]]],[],[]],0,[[[32,[-1,-2]],5],6,8,8],[-1,-1,[]],[-1,-1,[]],[-1,-2,[],[]],[-1,-2,[],[]],[[[32,[-1,-2]]],[[0,[33]]],[],[]],[[[31,[-1,-2]],[4,[-3]]],[[34,[[32,[-2]]]]],3,[],7],[-1,[[12,[-2]]],[],[]],[-1,[[12,[-2]]],[],[]],[-1,[[12,[-2]]],[],[]],[-1,[[12,[-2]]],[],[]],[-1,13,[]],[-1,13,[]],[[[31,[-1,-2]]],[[32,[-2]]],3,[]],[[[31,[-1,-2]]],[],3,[]],0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[],35],[[36,5],6],[[36,5],6],[[37,5],6],[-1,-1,[]],[-1,-1,[]],[38,36],[-1,-1,[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[35,39,-1],40,[[42,[41]]]],[[],35],[35,40],[[35,[9,[-1]]],40,[17,16]],[35,40],[[35,[9,[-1]]],40,[17,16]],[35,40],[36,[[44,[43]]]],0,[[35,39,-1],40,[[42,[41]]]],[-1,45,[]],[-1,[[12,[-2]]],[],[]],[-1,[[12,[-2]]],[],[]],[-1,[[12,[-2]]],[],[]],[-1,[[12,[-2]]],[],[]],[-1,[[12,[-2]]],[],[]],[-1,[[12,[-2]]],[],[]],[-1,13,[]],[-1,13,[]],[-1,13,[]],[[35,39,-1],40,[[42,[41]]]]],"c":[],"p":[[3,"Core",0],[8,"Effect",0],[8,"App",0],[3,"Request",0],[3,"Formatter",208],[6,"Result",208],[8,"Operation",67],[8,"Debug",208],[3,"Vec",209],[15,"tuple"],[8,"Fn",210],[4,"Result",211],[3,"TypeId",212],[3,"Request",42],[8,"Deserializer",213],[8,"Serialize",214],[8,"Deserialize",213],[3,"Bridge",42],[15,"u8"],[15,"slice"],[8,"Send",215],[8,"Serializer",214],[3,"CapabilityContext",67],[8,"Sync",215],[8,"Copy",215],[3,"ProtoContext",67],[8,"Future",216],[3,"RenderOperation",101],[15,"bool"],[3,"Render",101],[3,"AppTester",128],[3,"Update",128],[8,"Iterator",217],[6,"Result",218],[3,"TypeGen",155],[4,"TypeGenError",155],[4,"State",155],[3,"Error",219],[15,"str"],[6,"Result",155],[3,"Path",220],[8,"AsRef",221],[8,"Error",222],[4,"Option",223],[3,"String",224],[8,"Capability",67],[8,"WithContext",67],[8,"Export",155]]},\
+"crux_http":{"doc":"A HTTP client for use with Crux","t":"DNDDNDDDDGLLLLLLLLLLLLLLMLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLALLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLMCLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLALLLLLLLLLLALLLLLLLLLLLLLLLLLLLLLLLALLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLDLLLLLLLLLLLLLLLLLLLLLLLLLLLLIDDLLLLLLLLLLKLLLLLLLLLLLLLLLDDDEDDENNNNLLMMLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLMMLLLLLLLLLLMMLLLLLLLLLLMLLLLLLLLLLLLLLLLLLLLLLLLLLLLLMMLLLLLLLDLLLLLLLLLLLLL","n":["Config","Err","Error","Http","Ok","Request","RequestBuilder","Response","ResponseAsync","Result","add_header","append_header","append_header","append_header","as_mut","as_mut","as_mut","as_mut","as_mut","as_ref","as_ref","as_ref","as_ref","as_ref","base_url","body","body","body_bytes","body_bytes","body_bytes","body_bytes","body_file","body_form","body_form","body_json","body_json","body_json","body_json","body_string","body_string","body_string","body_string","borrow","borrow","borrow","borrow","borrow","borrow","borrow","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","build","client","clone","clone","clone","clone","clone","clone_into","clone_into","clone_into","clone_into","clone_into","connect","content_type","content_type","content_type","content_type","default","delete","deserialize","deserialize","eq","eq","equivalent","equivalent","expect_json","expect_string","ext","ext","fmt","fmt","fmt","fmt","fmt","fmt","from","from","from","from","from","from","from","from","from","from","from","from","from","get","head","header","header","header","header","header_mut","header_mut","header_mut","header_names","header_names","header_names","header_values","header_values","header_values","headers","http","index","index","index","index","index","index","insert_ext","insert_header","insert_header","insert_header","into","into","into","into","into","into","into","into","into","into_future","into_iter","into_iter","into_iter","is_empty","is_empty","iter","iter","iter","iter_mut","iter_mut","iter_mut","len","len","map_event","method","middleware","middleware","middleware","new","new","new","new","options","patch","poll_read","post","protocol","put","query","query","remove_header","remove_header","remove_header","request","send","serialize","serialize","set_base_url","set_body","set_body","set_content_type","set_ext","set_header","set_query","status","status","swap_body","take_body","take_body","take_body","testing","to_owned","to_owned","to_owned","to_owned","to_owned","trace","try_from","try_from","try_from","try_from","try_from","try_from","try_from","try_into","try_into","try_into","try_into","try_into","try_into","try_into","type_id","type_id","type_id","type_id","type_id","type_id","type_id","url","version","version","vzip","vzip","vzip","vzip","vzip","vzip","vzip","with_body","Client","borrow","borrow_mut","clone","clone_into","config","connect","delete","fmt","from","get","head","into","options","patch","post","put","recv_bytes","recv_form","recv_json","recv_string","request","send","to_owned","trace","try_from","try_into","type_id","vzip","Middleware","Next","Redirect","borrow","borrow","borrow_mut","borrow_mut","clone","clone_into","default","fmt","from","from","handle","handle","into","into","new","new","run","to_owned","try_from","try_from","try_into","try_into","type_id","type_id","vzip","vzip","HttpHeader","HttpRequest","HttpRequestBuilder","HttpRequestBuilderError","HttpResponse","HttpResponseBuilder","HttpResponseBuilderError","UninitializedField","UninitializedField","ValidationError","ValidationError","body","body","body","body","borrow","borrow","borrow","borrow","borrow","borrow","borrow","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","borrow_mut","build","build","clone","clone","clone","clone","clone","clone_into","clone_into","clone_into","clone_into","clone_into","default","default","delete","deserialize","deserialize","deserialize","eq","eq","eq","equivalent","equivalent","equivalent","fmt","fmt","fmt","fmt","fmt","fmt","fmt","from","from","from","from","from","from","from","from","from","from","from","get","head","header","header","headers","headers","into","into","into","into","into","into","into","json","json","method","method","name","ok","options","patch","post","put","serialize","serialize","serialize","status","status","status","to_owned","to_owned","to_owned","to_owned","to_owned","to_string","to_string","try_from","try_from","try_from","try_from","try_from","try_from","try_from","try_into","try_into","try_into","try_into","try_into","try_into","try_into","type_id","type_id","type_id","type_id","type_id","type_id","type_id","url","url","value","vzip","vzip","vzip","vzip","vzip","vzip","vzip","ResponseBuilder","body","borrow","borrow_mut","build","from","header","into","ok","try_from","try_into","type_id","vzip","with_status"],"q":[[0,"crux_http"],[230,"crux_http::client"],[259,"crux_http::middleware"],[288,"crux_http::protocol"],[426,"crux_http::testing"],[440,"http_types::headers::header_name"],[441,"core::convert"],[442,"http_types::headers::to_header_values"],[443,"http_types::headers::headers"],[444,"http_types::request"],[445,"http_types::response"],[446,"http_types::body"],[447,"core::option"],[448,"core::convert"],[449,"std::io::error"],[450,"std::path"],[451,"serde::ser"],[452,"serde::de"],[453,"alloc::string"],[454,"core::clone"],[455,"http_types::mime"],[456,"core::result"],[457,"serde::de"],[458,"core::marker"],[459,"core::marker"],[460,"core::fmt"],[461,"url::parser"],[462,"http_types::error"],[463,"http_types::headers::header_values"],[464,"http_types::headers::names"],[465,"http_types::headers::values"],[466,"http_types::headers::iter"],[467,"http_types::headers::iter_mut"],[468,"core::ops::function"],[469,"core::marker"],[470,"http_types::status_code"],[471,"url"],[472,"crux_core::capability"],[473,"core::pin"],[474,"core::task::wake"],[475,"std::io::error"],[476,"core::ops::function"],[477,"http_types::version"],[478,"core::future::future"],[479,"alloc::boxed"],[480,"alloc::sync"],[481,"futures_core::future"],[482,"derive_builder::error"]],"d":["Configuration for crux_http::Http
s and their underlying …","Contains the error value","","The Http capability API.","Contains the success value","An HTTP request, returns a Response
.","Request Builder","An HTTP Response that will be passed to in a message to an …","An HTTP response that exposes async methods, for use …","","Adds a header to be added to every request by this config.","Append a header to the headers.","Append an HTTP header.","Append an HTTP header.","","","","","","","","","","","The base URL for a client. All request URLs will be …","Sets the body of the request from any type with implements …","","Pass bytes as the request body.","Pass bytes as the request body.","Reads the entire request body into a byte buffer.","Reads the entire request body into a byte buffer.","Pass a file as the request body.","Pass a form as the request body.","Reads and deserialized the entire request body from form …","Pass JSON as the request body.","Pass JSON as the request body.","Reads and deserialized the entire request body from json.","Reads and deserialized the entire request body from json.","Pass a string as the request body.","Pass a string as the request body.","Reads the entire response body into a string.","Reads the entire response body into a string.","","","","","","","","","","","","","","","Return the constructed Request
.","","","","","","","","","","","","Instruct the Shell to perform a HTTP CONNECT request to …","Sets the Content-Type header on the request.","Get the request content type as a Mime
.","Get the response content type as a Mime
.","Get the response content type as a Mime
.","","Instruct the Shell to perform a HTTP DELETE request to the …","","","","","","","Decode a T
from a JSON response body prior to dispatching …","Decode a String from the response body prior to …","Get a request extension value.","Get a response scoped extension value.","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","","","Returns the argument unchanged.","","Converts an http::Request
to a crux_http::Request
.","Returns the argument unchanged.","Returns the argument unchanged.","","Returns the argument unchanged.","","Returns the argument unchanged.","Instruct the Shell to perform a HTTP GET request to the …","Instruct the Shell to perform a HTTP HEAD request to the …","Sets a header on the request.","Get an HTTP header.","Get a header.","Get a header.","Get a mutable reference to a header.","Get an HTTP header mutably.","Get an HTTP header mutably.","An iterator visiting all header names in arbitrary order.","An iterator visiting all header names in arbitrary order.","An iterator visiting all header names in arbitrary order.","An iterator visiting all header values in arbitrary order.","An iterator visiting all header values in arbitrary order.","An iterator visiting all header values in arbitrary order.","Headers to be applied to every request made by this client.","","Returns a reference to the value corresponding to the …","Returns a reference to the value corresponding to the …","Returns a reference to the value corresponding to the …","Returns a reference to the value corresponding to the …","Returns a reference to the value corresponding to the …","Returns a reference to the value corresponding to the …","Set a response scoped extension value.","Set an HTTP header.","Insert an HTTP header.","Insert an HTTP header.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Converts a crux_http::Request
to an http::Request
.","Calls U::from(self)
.","Calls U::from(self)
.","","Calls U::from(self)
.","","Returns a iterator of references over the remaining items.","","","Returns true
if the set length of the body stream is zero, …","Returns true
if the set length of the body stream is zero, …","An iterator visiting all header pairs in arbitrary order.","An iterator visiting all header pairs in arbitrary order.","An iterator visiting all header pairs in arbitrary order.","An iterator visiting all header pairs in arbitrary order, …","An iterator visiting all header pairs in arbitrary order, …","An iterator visiting all header pairs in arbitrary order, …","Get the length of the body stream, if it has been set.","Get the length of the body stream, if it has been set.","","Get the request HTTP method.","Middleware types","Push middleware onto a per-request middleware stack.","Push middleware onto a per-request middleware stack.","Construct new empty config.","","Create a new instance.","","Instruct the Shell to perform a HTTP OPTIONS request to …","Instruct the Shell to perform a HTTP PATCH request to the …","","Instruct the Shell to perform a HTTP POST request to the …","The protocol for communicating with the shell","Instruct the Shell to perform a HTTP PUT request to the …","Set the URL querystring.","Get the URL querystring.","Remove a header.","Remove a header.","Remove a header.","Instruct the Shell to perform an HTTP request with the …","Sends the constructed Request
and returns its result as an …","","","Sets the base URL for this config. All request URLs will …","Pass an AsyncRead
stream as the request body.","Set the body reader.","Set the request content type from a Mime
.","Set a request extension value.","Set an HTTP header.","Set the URL querystring.","Get the HTTP status code.","Get the HTTP status code.","Swaps the value of the body with another body, without …","Take the request body as a Body
.","","Take the response body as a Body
.","","","","","","","Instruct the Shell to perform a HTTP TRACE request to the …","","","","","","","","","","","","","","","","","","","","","","Get the request url.","Get the HTTP protocol version.","Get the HTTP protocol version.","","","","","","","","","An HTTP client, capable of sending Request
s","","","Clones the Client.","","Get the current configuration.","Perform an HTTP CONNECT
request using the Client
…","Perform an HTTP DELETE
request using the Client
connection.","","Returns the argument unchanged.","Perform an HTTP GET
request using the Client
connection.","Perform an HTTP HEAD
request using the Client
connection.","Calls U::from(self)
.","Perform an HTTP OPTIONS
request using the Client
…","Perform an HTTP PATCH
request using the Client
connection.","Perform an HTTP POST
request using the Client
connection.","Perform an HTTP PUT
request using the Client
connection.","Submit a Request
and get the response body as bytes.","Submit a Request
and decode the response body from form …","Submit a Request
and decode the response body from json …","Submit a Request
and get the response body as a string.","Perform a HTTP request with the given verb using the Client
…","Send a Request
using this client.","","Perform an HTTP TRACE
request using the Client
connection.","","","","","Middleware that wraps around remaining middleware chain.","The remainder of a middleware chain, including the …","A middleware which attempts to follow HTTP redirects.","","","","","","","Create a new instance of the Redirect middleware, which …","","Returns the argument unchanged.","Returns the argument unchanged.","Asynchronously handle the request, and return a response.","","Calls U::from(self)
.","Calls U::from(self)
.","Create a new instance","Create a new instance of the Redirect middleware, which …","Asynchronously execute the remaining middleware chain.","","","","","","","","","","","","Builder for HttpRequest
.","Error type for HttpRequestBuilder","","Builder for HttpResponse
.","Error type for HttpResponseBuilder","Uninitialized field","Uninitialized field","Custom validation error","Custom validation error","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","","Returns the argument unchanged.","","Returns the argument unchanged.","Returns the argument unchanged.","","Returns the argument unchanged.","","Returns the argument unchanged.","","","","","","","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","Allows users to build an http response.","Sets the body of the Response","","","Builds the response","Returns the argument unchanged.","Sets a header on the response.","Calls U::from(self)
.","Constructs a new ResponseBuilder with the 200 OK status …","","","","","Constructs a new ResponseBuilder with the specified status …"],"i":[0,2,0,0,2,0,0,0,0,0,1,6,8,9,6,6,8,9,9,6,6,8,9,9,1,13,8,13,6,8,9,6,6,9,13,6,8,9,13,6,8,9,13,1,25,6,8,9,27,13,1,25,6,8,9,27,13,0,1,25,6,8,27,1,25,6,8,27,27,13,6,8,9,1,27,25,8,25,8,25,8,13,13,6,9,13,1,25,6,8,9,13,1,25,25,25,25,6,6,8,9,9,9,27,27,27,13,6,8,9,6,8,9,6,8,9,6,8,9,1,0,6,6,8,8,9,9,9,6,8,9,13,1,25,6,6,8,9,9,27,13,6,6,6,6,9,6,8,9,6,8,9,6,9,27,6,0,13,6,1,25,6,27,27,27,9,27,0,27,13,6,6,8,9,27,13,25,8,1,6,9,6,6,6,6,8,9,9,6,8,9,0,1,25,6,8,27,27,13,1,25,6,8,9,27,13,1,25,6,8,9,27,13,1,25,6,8,9,27,6,8,9,13,1,25,6,8,9,27,8,0,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,0,0,0,66,67,66,67,66,66,67,67,66,67,52,67,66,67,66,67,66,66,66,67,66,67,66,67,66,67,0,0,0,0,0,0,0,75,76,75,76,72,73,55,42,74,72,75,55,73,76,42,74,72,75,55,73,76,42,72,73,74,72,55,73,42,74,72,55,73,42,55,42,55,74,55,42,74,55,42,74,55,42,74,75,75,55,76,76,42,74,72,75,75,75,55,73,76,76,76,42,55,55,72,73,55,42,74,72,75,55,73,76,42,72,73,72,55,74,42,55,55,55,55,74,55,42,73,42,42,74,72,55,73,42,75,76,74,72,75,55,73,76,42,74,72,75,55,73,76,42,74,72,75,55,73,76,42,72,55,74,74,72,75,55,73,76,42,0,79,79,79,79,79,79,79,79,79,79,79,79,79],"f":[0,0,0,0,0,0,0,0,0,0,[[1,-1,-2],[[2,[1]]],[[4,[3]]],5],[[6,-1,-2],7,[[4,[3]]],5],[[[8,[-1]],-2,-3],7,[],[[4,[3]]],5],[[9,-1,-2],7,[[4,[3]]],5],[6,10],[6,11],[[[8,[-1]]],10,[]],[9,10],[9,12],[6,10],[6,11],[[[8,[-1]]],10,[]],[9,10],[9,12],0,[[[13,[-1,-2]],-3],[[13,[-1,-2]]],[],[],[[4,[14]]]],[[[8,[-1]]],[[15,[-1]]],[]],[[[13,[-1,-2]],-3],[[13,[-1,-2]]],[],[],[[18,[[17,[16]]]]]],[[6,-1],7,[[18,[[17,[16]]]]]],[[[8,[[19,[16]]]]],[[2,[[19,[16]]]]]],[9,[[2,[[19,[16]]]]]],[[6,-1],[[20,[7]]],[[18,[21]]]],[[6,-1],[[2,[7]]],22],[9,[[2,[-1]]],23],[[[13,[-1,-2]],-3],[[2,[[13,[-1,-2]]]]],[],[],22],[[6,-1],[[2,[7]]],22],[[[8,[[19,[16]]]]],[[2,[-1]]],23],[9,[[2,[-1]]],23],[[[13,[-1,-2]],24],[[13,[-1,-2]]],[],[]],[[6,24],7],[[[8,[[19,[16]]]]],[[2,[24]]]],[9,[[2,[24]]]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[[13,[-1,-2]]],6,[],[]],0,[1,1],[25,25],[6,6],[[[8,[-1]]],[[8,[-1]]],26],[[[27,[-1]]],[[27,[-1]]],[]],[[-1,-2],7,[],[]],[[-1,-2],7,[],[]],[[-1,-2],7,[],[]],[[-1,-2],7,[],[]],[[-1,-2],7,[],[]],[[[27,[-1]],-2],[[13,[-1]]],[],[[18,[28]]]],[[[13,[-1,-2]],-3],[[13,[-1,-2]]],[],[],[[4,[29]]]],[6,[[15,[29]]]],[[[8,[-1]]],[[15,[29]]],[]],[9,[[15,[29]]]],[[],1],[[[27,[-1]],-2],[[13,[-1]]],[],[[18,[28]]]],[-1,[[30,[25]]],31],[-1,[[30,[[8,[-2]]]]],31,32],[[25,25],33],[[[8,[-1]],[8,[-1]]],33,34],[[-1,-2],33,[],[]],[[-1,-2],33,[],[]],[[[13,[-1,-2]]],[[13,[-1,-3]]],[],[],23],[[[13,[-1,-2]]],[[13,[-1,24]]],[],[]],[6,[[15,[-1]]],[35,36]],[9,[[15,[-1]]],[35,36]],[[[13,[-1]],37],38,[]],[[1,37],38],[[25,37],38],[[6,37],38],[[[8,[-1]],37],38,[]],[[9,37],38],[-1,-1,[]],[-1,-1,[]],[39,25],[40,25],[-1,-1,[]],[41,25],[11,6],[-1,-1,[]],[-1,-1,[]],[12,9],[-1,-1,[]],[42,9],[-1,-1,[]],[[[27,[-1]],-2],[[13,[-1]]],[],[[18,[28]]]],[[[27,[-1]],-2],[[13,[-1]]],[],[[18,[28]]]],[[[13,[-1,-2]],-3,-4],[[13,[-1,-2]]],[],[],[[4,[3]]],5],[[6,-1],[[15,[43]]],[[4,[3]]]],[[[8,[-1]],-2],[[15,[43]]],[],[[4,[3]]]],[[9,-1],[[15,[43]]],[[4,[3]]]],[[6,-1],[[15,[43]]],[[4,[3]]]],[[[8,[-1]],-2],[[15,[43]]],[],[[4,[3]]]],[[9,-1],[[15,[43]]],[[4,[3]]]],[6,44],[[[8,[-1]]],44,[]],[9,44],[6,45],[[[8,[-1]]],45,[]],[9,45],0,0,[[6,3],43],[[6,28],43],[[[8,[-1]],28],43,[]],[[[8,[-1]],3],43,[]],[[9,3],43],[[9,28],43],[[9,-1],7,[35,36]],[[6,-1,-2],[[15,[43]]],[[4,[3]]],5],[[[8,[-1]],-2,-3],7,[],[[4,[3]]],5],[[9,-1,-2],7,[[4,[3]]],5],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[6,11],[-1,-2,[],[]],[-1,-2,[],[]],[9,12],[-1,-2,[],[]],[[[13,[7]]]],[6],[6],[6],[6,[[15,[33]]]],[9,[[15,[33]]]],[6,46],[[[8,[-1]]],46,[]],[9,46],[6,47],[[[8,[-1]]],47,[]],[9,47],[6,[[15,[48]]]],[9,[[15,[48]]]],[[[27,[-1]],-2],[],[],[49,35,36,50]],[6,51],0,[[[13,[-1,-2]],-3],[[13,[-1,-2]]],[],[],52],[[6,-1],7,52],[[],1],[[[15,[53]],-1],25,[[4,[24]]]],[[51,54],6],[[[56,[55,-1]]],[[27,[-1]]],[]],[[[27,[-1]],-2],[[13,[-1]]],[],[[18,[28]]]],[[[27,[-1]],-2],[[13,[-1]]],[],[[18,[28]]]],[[[57,[9]],58,[17,[16]]],[[60,[[30,[48,59]]]]]],[[[27,[-1]],-2],[[13,[-1]]],[],[[18,[28]]]],0,[[[27,[-1]],-2],[[13,[-1]]],[],[[18,[28]]]],[[[13,[-1,-2]],-3],[[30,[[13,[-1,-2]],25]]],[],[],22],[6,[[2,[-1]]],23],[[6,-1],[[15,[43]]],[[4,[3]]]],[[[8,[-1]],-2],[[15,[43]]],[],[[4,[3]]]],[[9,-1],[[15,[43]]],[[4,[3]]]],[[[27,[-1]],51,54],[[13,[-1]]],[]],[[[13,[-1,-2]],-3],7,[],[],[61,35]],[[25,-1],30,62],[[[8,[-1]],-2],30,22,62],[[1,54],1],[[6,-1],7,[[4,[14]]]],[[9,-1],7,[[4,[14]]]],[[6,29],7],[[6,-1],[[15,[-1]]],[35,36]],[[6,-1,-2],7,[[4,[3]]],5],[[6,-1],[[2,[7]]],22],[[[8,[-1]]],53,[]],[9,53],[[9,14],7],[6,14],[[[8,[-1]]],[[15,[-1]]],[]],[9,14],0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[[27,[-1]],-2],[[13,[-1]]],[],[[18,[28]]]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,63,[]],[-1,63,[]],[-1,63,[]],[-1,63,[]],[-1,63,[]],[-1,63,[]],[-1,63,[]],[6,54],[[[8,[-1]]],[[15,[64]]],[]],[9,[[15,[64]]]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[[8,[-1]],-2],[[8,[-2]]],[],[]],0,[-1,-2,[],[]],[-1,-2,[],[]],[65,65],[[-1,-2],7,[],[]],[65,1],[[65,-1],[[13,[7]]],[[18,[28]]]],[[65,-1],[[13,[7]]],[[18,[28]]]],[[65,37],38],[-1,-1,[]],[[65,-1],[[13,[7]]],[[18,[28]]]],[[65,-1],[[13,[7]]],[[18,[28]]]],[-1,-2,[],[]],[[65,-1],[[13,[7]]],[[18,[28]]]],[[65,-1],[[13,[7]]],[[18,[28]]]],[[65,-1],[[13,[7]]],[[18,[28]]]],[[65,-1],[[13,[7]]],[[18,[28]]]],[[65,-1],[[2,[[19,[16]]]]],[[4,[6]]]],[[65,-1],[[2,[-2]]],[[4,[6]]],23],[[65,-1],[[2,[-2]]],[[4,[6]]],23],[[65,-1],[[2,[24]]],[[4,[6]]]],[[65,51,-1],[[13,[7]]],[[18,[28]]]],[[65,-1],[[2,[9]]],[[4,[6]]]],[-1,-2,[],[]],[[65,-1],[[13,[7]]],[[18,[28]]]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,63,[]],[-1,-2,[],[]],0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[66,66],[[-1,-2],7,[],[]],[[],67],[[67,37],38],[-1,-1,[]],[-1,-1,[]],[[-1,6,65,66],[[57,[[69,[68]]]]],[]],[[67,6,65,66],[[57,[[69,[68]]]]]],[-1,-2,[],[]],[-1,-2,[],[]],[[[17,[[70,[52]]]],49],66],[16,67],[[66,6,65],[[71,[[2,[9]]]]]],[-1,-2,[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,63,[]],[-1,63,[]],[-1,-2,[],[]],[-1,-2,[],[]],0,0,0,0,0,0,0,0,0,0,0,[[72,-1],72,[[4,[[19,[16]]]]]],[[73,-1],73,[[4,[[19,[16]]]]]],0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[72,55],[73,42],[74,74],[72,72],[55,55],[73,73],[42,42],[[-1,-2],7,[],[]],[[-1,-2],7,[],[]],[[-1,-2],7,[],[]],[[-1,-2],7,[],[]],[[-1,-2],7,[],[]],[[],55],[[],42],[-1,72,[[4,[24]]]],[-1,[[30,[74]]],31],[-1,[[30,[55]]],31],[-1,[[30,[42]]],31],[[74,74],33],[[55,55],33],[[42,42],33],[[-1,-2],33,[],[]],[[-1,-2],33,[],[]],[[-1,-2],33,[],[]],[[74,37],38],[[75,37],38],[[75,37],38],[[55,37],38],[[76,37],38],[[76,37],38],[[42,37],38],[-1,-1,[]],[-1,-1,[]],[77,75],[-1,-1,[]],[24,75],[-1,-1,[]],[-1,-1,[]],[77,76],[-1,-1,[]],[24,76],[-1,-1,[]],[-1,72,[[4,[24]]]],[-1,72,[[4,[24]]]],[[72,-1,-2],72,[[4,[24]]],[[4,[24]]]],[[73,-1,-2],73,[[4,[24]]],[[4,[24]]]],0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[72,-1],72,22],[[73,-1],73,22],[[72,-1],72,[[4,[24]]]],0,0,[[],73],[-1,72,[[4,[24]]]],[-1,72,[[4,[24]]]],[-1,72,[[4,[24]]]],[-1,72,[[4,[24]]]],[[74,-1],30,62],[[55,-1],30,62],[[42,-1],30,62],[[73,-1],73,[[4,[78]]]],[78,73],0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,24,[]],[-1,24,[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,63,[]],[-1,63,[]],[-1,63,[]],[-1,63,[]],[-1,63,[]],[-1,63,[]],[-1,63,[]],[[72,-1],72,[[4,[24]]]],0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],0,[[[79,[-1]],-2],[[79,[-2]]],[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[[79,[-1]]],[[8,[-1]]],[]],[-1,-1,[]],[[[79,[-1]],-2,-3],[[79,[-1]]],[],[[4,[3]]],5],[-1,-2,[],[]],[[],[[79,[[19,[16]]]]]],[-1,[[30,[-2]]],[],[]],[-1,[[30,[-2]]],[],[]],[-1,63,[]],[-1,-2,[],[]],[53,[[79,[[19,[16]]]]]]],"c":[],"p":[[3,"Config",0],[6,"Result",0],[3,"HeaderName",440],[8,"Into",441],[8,"ToHeaderValues",442],[3,"Request",0],[15,"tuple"],[3,"Response",0],[3,"ResponseAsync",0],[3,"Headers",443],[3,"Request",444],[3,"Response",445],[3,"RequestBuilder",0],[3,"Body",446],[4,"Option",447],[15,"u8"],[15,"slice"],[8,"AsRef",441],[3,"Vec",448],[6,"Result",449],[3,"Path",450],[8,"Serialize",451],[8,"DeserializeOwned",452],[3,"String",453],[3,"Error",0],[8,"Clone",454],[3,"Http",0],[15,"str"],[3,"Mime",455],[4,"Result",456],[8,"Deserializer",452],[8,"Deserialize",452],[15,"bool"],[8,"PartialEq",457],[8,"Send",458],[8,"Sync",458],[3,"Formatter",459],[6,"Result",459],[3,"Error",460],[4,"ParseError",461],[3,"Error",462],[3,"HttpResponse",288],[3,"HeaderValues",463],[3,"Names",464],[3,"Values",465],[3,"Iter",466],[3,"IterMut",467],[15,"usize"],[8,"Fn",468],[8,"Copy",458],[4,"Method",469],[8,"Middleware",259],[4,"StatusCode",470],[3,"Url",471],[3,"HttpRequest",288],[3,"CapabilityContext",472],[3,"Pin",473],[3,"Context",474],[3,"Error",449],[4,"Poll",475],[8,"FnOnce",468],[8,"Serializer",451],[3,"TypeId",476],[4,"Version",477],[3,"Client",230],[3,"Next",259],[3,"Redirect",259],[8,"Future",478],[3,"Box",479],[3,"Arc",480],[6,"BoxFuture",481],[3,"HttpRequestBuilder",288],[3,"HttpResponseBuilder",288],[3,"HttpHeader",288],[4,"HttpRequestBuilderError",288],[4,"HttpResponseBuilderError",288],[3,"UninitializedFieldError",482],[15,"u16"],[3,"ResponseBuilder",426]]},\
+"crux_kv":{"doc":"A basic Key-Value store for use with Crux","t":"DEENNNNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL","n":["KeyValue","KeyValueOperation","KeyValueOutput","Read","Read","Write","Write","borrow","borrow","borrow","borrow_mut","borrow_mut","borrow_mut","clone","clone","clone_into","clone_into","deserialize","deserialize","eq","eq","equivalent","equivalent","fmt","fmt","from","from","from","into","into","into","map_event","new","read","serialize","serialize","to_owned","to_owned","try_from","try_from","try_from","try_into","try_into","try_into","type_id","type_id","type_id","write"],"q":[[0,"crux_kv"],[48,"core::result"],[49,"serde::de"],[50,"core::fmt"],[51,"core::fmt"],[52,"core::marker"],[53,"core::marker"],[54,"serde::ser"],[55,"core::any"],[56,"alloc::vec"]],"d":["","Supported operations","","Read bytes stored under a key","","Write bytes under a key","","","","","","","","","","","","","","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","","","Read a value under key
, will dispatch the event with a …","","","","","","","","","","","","","","Set key
to be the provided value
. Typically the bytes …"],"i":[0,0,0,1,2,1,2,1,2,9,1,2,9,1,2,1,2,1,2,1,2,1,2,1,2,1,2,9,1,2,9,9,9,9,1,2,1,2,1,2,9,1,2,9,1,2,9,9],"f":[0,0,0,0,0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[1,1],[2,2],[[-1,-2],3,[],[]],[[-1,-2],3,[],[]],[-1,[[4,[1]]],5],[-1,[[4,[2]]],5],[[1,1],6],[[2,2],6],[[-1,-2],6,[],[]],[[-1,-2],6,[],[]],[[1,7],8],[[2,7],8],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[[9,[-1]],-2],[],[],[10,11,12,13]],[[[14,[1,-1]]],[[9,[-1]]],[]],[[[9,[-1]],15,-2],3,[],[10,11,12]],[[1,-1],4,16],[[2,-1],4,16],[-1,-2,[],[]],[-1,-2,[],[]],[-1,[[4,[-2]]],[],[]],[-1,[[4,[-2]]],[],[]],[-1,[[4,[-2]]],[],[]],[-1,[[4,[-2]]],[],[]],[-1,[[4,[-2]]],[],[]],[-1,[[4,[-2]]],[],[]],[-1,17,[]],[-1,17,[]],[-1,17,[]],[[[9,[-1]],15,[19,[18]],-2],3,[],[10,11,12]]],"c":[],"p":[[4,"KeyValueOperation",0],[4,"KeyValueOutput",0],[15,"tuple"],[4,"Result",48],[8,"Deserializer",49],[15,"bool"],[3,"Formatter",50],[6,"Result",50],[3,"KeyValue",0],[8,"Fn",51],[8,"Send",52],[8,"Sync",52],[8,"Copy",52],[3,"CapabilityContext",53],[15,"str"],[8,"Serializer",54],[3,"TypeId",55],[15,"u8"],[3,"Vec",56]]},\
+"crux_macros":{"doc":"","t":"YYY","n":["Capability","Effect","Export"],"q":[[0,"crux_macros"]],"d":["","",""],"i":[0,0,0],"f":[0,0,0],"c":[],"p":[]},\
+"crux_platform":{"doc":"TODO mod docs","t":"DDDLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL","n":["Platform","PlatformRequest","PlatformResponse","borrow","borrow","borrow","borrow_mut","borrow_mut","borrow_mut","clone","clone_into","deserialize","deserialize","eq","eq","equivalent","equivalent","fmt","fmt","from","from","from","get","into","into","into","map_event","new","serialize","serialize","to_owned","try_from","try_from","try_from","try_into","try_into","try_into","type_id","type_id","type_id"],"q":[[0,"crux_platform"],[40,"core::result"],[41,"serde::de"],[42,"core::fmt"],[43,"core::fmt"],[44,"core::marker"],[45,"core::marker"],[46,"serde::ser"],[47,"core::any"]],"d":["","","","","","","","","","","","","","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","","","","","","","","","","","","","",""],"i":[0,0,0,1,5,9,1,5,9,1,1,1,5,1,5,1,5,1,5,1,5,9,9,1,5,9,9,9,1,5,1,1,5,9,1,5,9,1,5,9],"f":[0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[1,1],[[-1,-2],2,[],[]],[-1,[[3,[1]]],4],[-1,[[3,[5]]],4],[[1,1],6],[[5,5],6],[[-1,-2],6,[],[]],[[-1,-2],6,[],[]],[[1,7],8],[[5,7],8],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[[[9,[-1]],-2],2,[],[10,11,12]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[[9,[-1]],-2],[],[],[10,11,12,13]],[[[14,[1,-1]]],[[9,[-1]]],[]],[[1,-1],3,15],[[5,-1],3,15],[-1,-2,[],[]],[-1,[[3,[-2]]],[],[]],[-1,[[3,[-2]]],[],[]],[-1,[[3,[-2]]],[],[]],[-1,[[3,[-2]]],[],[]],[-1,[[3,[-2]]],[],[]],[-1,[[3,[-2]]],[],[]],[-1,16,[]],[-1,16,[]],[-1,16,[]]],"c":[],"p":[[3,"PlatformRequest",0],[15,"tuple"],[4,"Result",40],[8,"Deserializer",41],[3,"PlatformResponse",0],[15,"bool"],[3,"Formatter",42],[6,"Result",42],[3,"Platform",0],[8,"Fn",43],[8,"Send",44],[8,"Sync",44],[8,"Copy",44],[3,"CapabilityContext",45],[8,"Serializer",46],[3,"TypeId",47]]},\
+"crux_time":{"doc":"Current time access for Crux apps","t":"DDDLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL","n":["Time","TimeRequest","TimeResponse","borrow","borrow","borrow","borrow_mut","borrow_mut","borrow_mut","clone","clone","clone_into","clone_into","deserialize","deserialize","eq","eq","equivalent","equivalent","fmt","fmt","from","from","from","get","into","into","into","map_event","new","serialize","serialize","to_owned","to_owned","try_from","try_from","try_from","try_into","try_into","try_into","type_id","type_id","type_id"],"q":[[0,"crux_time"],[43,"core::result"],[44,"serde::de"],[45,"core::fmt"],[46,"core::fmt"],[47,"core::marker"],[48,"core::marker"],[49,"serde::ser"],[50,"core::any"]],"d":["The Time capability API.","","","","","","","","","","","","","","","","","","","","","Returns the argument unchanged.","Returns the argument unchanged.","Returns the argument unchanged.","Request current time, which will be passed to the app as …","Calls U::from(self)
.","Calls U::from(self)
.","Calls U::from(self)
.","","","","","","","","","","","","","","",""],"i":[0,0,0,1,2,9,1,2,9,1,2,1,2,1,2,1,2,1,2,1,2,1,2,9,9,1,2,9,9,9,1,2,1,2,1,2,9,1,2,9,1,2,9],"f":[0,0,0,[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[1,1],[2,2],[[-1,-2],3,[],[]],[[-1,-2],3,[],[]],[-1,[[4,[1]]],5],[-1,[[4,[2]]],5],[[1,1],6],[[2,2],6],[[-1,-2],6,[],[]],[[-1,-2],6,[],[]],[[1,7],8],[[2,7],8],[-1,-1,[]],[-1,-1,[]],[-1,-1,[]],[[[9,[-1]],-2],3,[],[10,11,12]],[-1,-2,[],[]],[-1,-2,[],[]],[-1,-2,[],[]],[[[9,[-1]],-2],[],[],[10,11,12,13]],[[[14,[1,-1]]],[[9,[-1]]],[]],[[1,-1],4,15],[[2,-1],4,15],[-1,-2,[],[]],[-1,-2,[],[]],[-1,[[4,[-2]]],[],[]],[-1,[[4,[-2]]],[],[]],[-1,[[4,[-2]]],[],[]],[-1,[[4,[-2]]],[],[]],[-1,[[4,[-2]]],[],[]],[-1,[[4,[-2]]],[],[]],[-1,16,[]],[-1,16,[]],[-1,16,[]]],"c":[],"p":[[3,"TimeRequest",0],[3,"TimeResponse",0],[15,"tuple"],[4,"Result",43],[8,"Deserializer",44],[15,"bool"],[3,"Formatter",45],[6,"Result",45],[3,"Time",0],[8,"Fn",46],[8,"Send",47],[8,"Sync",47],[8,"Copy",47],[3,"CapabilityContext",48],[8,"Serializer",49],[3,"TypeId",50]]}\
+}');
+if (typeof window !== 'undefined' && window.initSearch) {window.initSearch(searchIndex)};
+if (typeof exports !== 'undefined') {exports.searchIndex = searchIndex};
diff --git a/master_api_docs/settings.html b/master_api_docs/settings.html
new file mode 100644
index 000000000..a51a53554
--- /dev/null
+++ b/master_api_docs/settings.html
@@ -0,0 +1 @@
+1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +
use std::path::PathBuf;
+
+use clap::{ArgAction, Args, Parser, Subcommand};
+
+#[derive(Parser)]
+#[command(
+ name = "crux",
+ bin_name = "crux",
+ author,
+ version,
+ about,
+ long_about = None,
+ arg_required_else_help(true),
+ propagate_version = true
+)]
+pub(crate) struct Cli {
+ #[command(subcommand)]
+ pub command: Option<Commands>,
+
+ #[arg(long, short, action = ArgAction::Count)]
+ pub verbose: u8,
+
+ #[arg(long, short, default_value = "false")]
+ pub include_source_code: bool,
+
+ /// temporary
+ #[arg(long, short)]
+ pub template_dir: PathBuf,
+
+ #[arg(long, short)]
+ pub path: Option<PathBuf>,
+}
+
+#[derive(Subcommand)]
+pub(crate) enum Commands {
+ #[command(visible_alias = "doc")]
+ Doctor(DoctorArgs),
+}
+
+#[derive(Args)]
+pub(crate) struct DoctorArgs {
+ #[arg(long, short)]
+ pub(crate) fix: Option<PathBuf>,
+}
+
+#[cfg(test)]
+mod cli_tests {
+ use super::*;
+
+ #[test]
+ fn test_cli() {
+ use clap::CommandFactory;
+ Cli::command().debug_assert()
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +
use std::{collections::BTreeMap, path::PathBuf};
+
+use serde::{Deserialize, Serialize};
+
+#[derive(Default, Debug, Serialize, Deserialize)]
+pub struct Workspace {
+ pub name: String,
+ pub description: Option<String>,
+ pub authors: Vec<String>,
+ pub repository: Option<String>,
+ pub cores: BTreeMap<String, Core>,
+ pub shells: Option<BTreeMap<String, Shell>>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct Core {
+ #[serde(skip)]
+ pub name: String,
+ pub source: PathBuf,
+ pub type_gen: Option<PathBuf>,
+ pub crux_version: String,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct Shell {
+ #[serde(skip)]
+ pub name: String,
+ pub template: Option<PathBuf>,
+ pub source: PathBuf,
+ pub cores: Vec<String>,
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +
use std::{fmt, path::Path};
+
+use console::{style, Style};
+use similar::{ChangeTag, TextDiff};
+
+pub(crate) fn show(file_name: &Path, desired: &str, actual: &str) {
+ let diff = TextDiff::from_lines(actual, desired);
+ for (idx, group) in diff.grouped_ops(3).iter().enumerate() {
+ if idx == 0 {
+ println!("{:-<80}", file_name.to_string_lossy());
+ }
+ for op in group {
+ for change in diff.iter_inline_changes(op) {
+ let (sign, s) = match change.tag() {
+ ChangeTag::Delete => ("-", Style::new().red()),
+ ChangeTag::Insert => ("+", Style::new().green()),
+ ChangeTag::Equal => (" ", Style::new().dim()),
+ };
+ print!(
+ "{}{} |{}",
+ style(Line(change.old_index())).dim(),
+ style(Line(change.new_index())).dim(),
+ s.apply_to(sign).bold(),
+ );
+ for (emphasized, value) in change.iter_strings_lossy() {
+ if emphasized {
+ print!("{}", s.apply_to(value).underlined());
+ } else {
+ print!("{}", s.apply_to(value));
+ }
+ }
+ if change.missing_newline() {
+ println!();
+ }
+ }
+ }
+ println!(""); // empty line between diffs
+ }
+}
+struct Line(Option<usize>);
+
+impl fmt::Display for Line {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self.0 {
+ None => write!(f, " "),
+ Some(idx) => write!(f, "{:>4}", idx + 1),
+ }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +
use std::{
+ collections::BTreeMap,
+ env, fs,
+ path::{Path, PathBuf},
+};
+
+use anyhow::{bail, Result};
+use ignore::Walk;
+use ramhorns::Template;
+
+use crate::{
+ diff,
+ template::{Context, CoreContext, ShellContext},
+ workspace,
+};
+
+const SOURCE_CODE_EXTENSIONS: [&str; 9] =
+ ["rs", "kt", "swift", "ts", "js", "tsx", "jsx", "html", "css"];
+
+type FileMap = BTreeMap<PathBuf, String>;
+
+pub(crate) fn doctor(
+ template_dir: &Path,
+ path: Option<&Path>,
+ verbosity: u8,
+ include_source_code: bool,
+) -> Result<()> {
+ let workspace = workspace::read_config()?;
+ let current_dir = &env::current_dir()?;
+ let template_root = current_dir.join(template_dir).canonicalize()?;
+
+ for (_, core) in &workspace.cores {
+ let (do_core, do_typegen) = match path {
+ Some(path) => (path == &core.source, Some(path) == core.type_gen.as_deref()),
+ None => (true, true),
+ };
+
+ if do_core {
+ compare(
+ ¤t_dir.join(&core.source),
+ &template_root.join("shared"),
+ &CoreContext::new(&workspace, core),
+ verbosity,
+ include_source_code,
+ )?;
+ }
+
+ if do_typegen {
+ if let Some(type_gen) = &core.type_gen {
+ let templates_typegen = template_root.join("shared_types");
+ if templates_typegen.exists() {
+ compare(
+ ¤t_dir.join(type_gen),
+ &templates_typegen,
+ &CoreContext::new(&workspace, core),
+ verbosity,
+ include_source_code,
+ )?;
+ }
+ }
+ }
+ }
+
+ if let Some(shells) = &workspace.shells {
+ for (name, shell) in shells {
+ let do_shell = match path {
+ Some(path) => path == &shell.source,
+ None => true,
+ };
+
+ if do_shell {
+ // TODO support shell having multiple cores
+ if shell.cores.len() > 1 {
+ eprintln!(
+ "Warning: shell {} has multiple cores, only checking first",
+ name
+ );
+ }
+ let core = workspace
+ .cores
+ .get(&shell.cores[0])
+ .expect("core not in workspace");
+ let template_root =
+ template_root.join(shell.template.as_deref().unwrap_or(Path::new(&name)));
+ if template_root.exists() {
+ compare(
+ ¤t_dir.join(&shell.source),
+ &template_root,
+ &ShellContext::new(&workspace, core, shell),
+ verbosity,
+ include_source_code,
+ )?;
+ }
+ }
+ }
+ }
+
+ workspace::write_config(&workspace)
+}
+
+fn compare(
+ root: &Path,
+ template_root: &Path,
+ context: &Context,
+ verbosity: u8,
+ include_source_code: bool,
+) -> Result<(), anyhow::Error> {
+ println!(
+ "{:-<80}\nActual: {}\nDesired: {}",
+ "",
+ root.display(),
+ template_root.display()
+ );
+ let (actual, desired) = &read_files(
+ &root,
+ &template_root,
+ context,
+ verbosity,
+ include_source_code,
+ )?;
+ missing(actual, desired);
+ common(actual, desired);
+ Ok(())
+}
+
+fn read_files(
+ root: &Path,
+ template_root: &Path,
+ context: &Context,
+ verbosity: u8,
+ include_source_code: bool,
+) -> Result<(FileMap, FileMap)> {
+ validate_path(root)?;
+ validate_path(template_root)?;
+
+ let mut actual = FileMap::new();
+ for entry in Walk::new(root).into_iter().filter_map(|e| e.ok()) {
+ if entry.file_type().expect("should have a file type").is_dir() {
+ continue;
+ }
+ let path = entry.path();
+ if !include_source_code && is_source_code(path) {
+ continue;
+ }
+ let path_display = path.display();
+ if verbosity > 0 {
+ println!("Reading: {path_display}");
+ }
+
+ match fs::read_to_string(path) {
+ Ok(contents) => {
+ let relative = path.strip_prefix(root)?.to_path_buf();
+ actual.insert(relative, ensure_trailing_newline(&contents));
+ }
+ Err(e) => match e.kind() {
+ std::io::ErrorKind::InvalidData => {
+ if verbosity > 0 {
+ println!("Warning, cannot read: {path_display}, {e}");
+ }
+ }
+ _ => bail!("Error reading: {path_display}, {e}"),
+ },
+ };
+ }
+
+ let mut desired = FileMap::new();
+ for entry in Walk::new(template_root).into_iter().filter_map(|e| e.ok()) {
+ if entry.file_type().expect("should have a file type").is_dir() {
+ continue;
+ }
+ let path = entry.path();
+ if !include_source_code && is_source_code(path) {
+ continue;
+ }
+ let path_display = path.display();
+ if verbosity > 0 {
+ println!("Reading: {path_display}");
+ }
+
+ let template = fs::read_to_string(path)?;
+ let template = Template::new(template).unwrap();
+
+ let rendered = match context {
+ Context::Core(context) => template.render(context),
+ Context::Shell(context) => template.render(context),
+ };
+ let rendered = ensure_trailing_newline(&rendered);
+
+ let relative = path.strip_prefix(template_root)?.to_path_buf();
+ desired.insert(relative, rendered);
+ }
+
+ Ok((actual, desired))
+}
+
+fn validate_path(path: &Path) -> Result<()> {
+ if !path.exists() {
+ bail!("{} does not exist", path.display());
+ }
+ if !path.is_absolute() {
+ bail!("{} is not an absolute path", path.display());
+ }
+ Ok(())
+}
+
+fn missing(actual: &FileMap, desired: &FileMap) {
+ let missing = difference(actual, desired);
+ if missing.len() == 0 {
+ println!("No missing files");
+ } else {
+ println!("Missing files:");
+ for file_name in missing {
+ println!(" {}", file_name.to_string_lossy());
+ }
+ println!("");
+ }
+}
+
+fn common(actual: &FileMap, desired: &FileMap) {
+ for file_name in &intersection(actual, desired) {
+ let desired = desired.get(file_name).expect("file not in map");
+ let actual = actual.get(file_name).expect("file not in map");
+ diff::show(file_name, desired, actual);
+ }
+}
+
+/// Trim whitespace from end of line and ensure trailing newline
+fn ensure_trailing_newline(s: &str) -> String {
+ let mut s = s.trim_end().to_string();
+ s.push('\n');
+ s
+}
+
+/// files in second but not in first
+fn difference(first: &FileMap, second: &FileMap) -> Vec<PathBuf> {
+ let mut missing = Vec::new();
+ for (k, _) in second {
+ if !first.contains_key(k) {
+ missing.push(k.clone());
+ }
+ }
+ missing
+}
+
+/// files in both first and second
+fn intersection(first: &FileMap, second: &FileMap) -> Vec<PathBuf> {
+ let mut common = Vec::new();
+ for (k, _) in first {
+ if second.contains_key(k) {
+ common.push(k.clone());
+ }
+ }
+ common
+}
+
+/// test if file is source code
+fn is_source_code(path: &Path) -> bool {
+ if let Some(ext) = path.extension() {
+ if let Some(ext) = ext.to_str() {
+ return SOURCE_CODE_EXTENSIONS.contains(&ext);
+ }
+ }
+ false
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn test_ensure_trailing_newline() {
+ assert_eq!(ensure_trailing_newline("hello\n"), "hello\n");
+ assert_eq!(ensure_trailing_newline("hello\n \t"), "hello\n");
+ assert_eq!(ensure_trailing_newline("hello\n\n "), "hello\n");
+ }
+
+ #[test]
+ fn test_find_missing_files() {
+ let mut actual_map = FileMap::new();
+ actual_map.insert(PathBuf::from("foo"), "foo".to_string());
+
+ let mut desired_map = FileMap::new();
+ desired_map.insert(PathBuf::from("foo"), "foo".to_string());
+ desired_map.insert(PathBuf::from("bar"), "bar".to_string());
+
+ let expected = vec![PathBuf::from("bar")];
+ let actual = difference(&actual_map, &desired_map);
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn test_find_common_files() {
+ let mut actual_map = FileMap::new();
+ actual_map.insert(PathBuf::from("foo"), "foo".to_string());
+
+ let mut desired_map = FileMap::new();
+ desired_map.insert(PathBuf::from("foo"), "foo".to_string());
+ desired_map.insert(PathBuf::from("bar"), "bar".to_string());
+
+ let expected = vec![PathBuf::from("foo")];
+ let actual = intersection(&actual_map, &desired_map);
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn test_is_source_code() {
+ assert!(is_source_code(Path::new("foo.rs")));
+ assert!(is_source_code(Path::new("foo.kt")));
+ assert!(is_source_code(Path::new("foo.swift")));
+ assert!(is_source_code(Path::new("foo.ts")));
+ assert!(is_source_code(Path::new("foo.js")));
+ assert!(is_source_code(Path::new("foo.tsx")));
+ assert!(is_source_code(Path::new("foo.jsx")));
+ assert!(is_source_code(Path::new("foo.html")));
+ assert!(is_source_code(Path::new("foo.css")));
+
+ assert!(!is_source_code(Path::new("foo.txt")));
+ assert!(!is_source_code(Path::new("foo")));
+ }
+}
+
use anyhow::Result;
+use args::{Commands, DoctorArgs};
+use clap::Parser;
+
+use args::Cli;
+
+mod args;
+mod config;
+mod diff;
+mod doctor;
+mod template;
+mod workspace;
+
+fn main() -> Result<()> {
+ let cli = Cli::parse();
+ match &cli.command {
+ Some(Commands::Doctor(DoctorArgs { .. })) => doctor::doctor(
+ &cli.template_dir,
+ cli.path.as_deref(),
+ cli.verbose,
+ cli.include_source_code,
+ ),
+ None => Ok(()),
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +
use ramhorns::Content;
+
+use crate::config::{Core, Shell, Workspace};
+
+pub enum Context {
+ Core(CoreContext),
+ Shell(ShellContext),
+}
+
+#[derive(Content)]
+pub struct CoreContext {
+ pub workspace: String,
+ pub core_name: String,
+ pub core_name_dashes: String,
+}
+
+impl CoreContext {
+ pub fn new(workspace: &Workspace, core: &Core) -> Context {
+ Context::Core(Self {
+ workspace: workspace.name.to_ascii_lowercase().replace(" ", "_"),
+ core_name: core.name.clone(),
+ core_name_dashes: core.name.replace("_", "-"),
+ })
+ }
+}
+
+#[derive(Content)]
+pub struct ShellContext {
+ pub workspace: String,
+ pub core_dir: String,
+ pub core_name: String,
+ pub type_gen: String,
+ pub shell_dir: String,
+ pub shell_name: String,
+ pub shell_name_dashes: String,
+}
+
+impl ShellContext {
+ pub fn new(workspace: &Workspace, core: &Core, shell: &Shell) -> Context {
+ Context::Shell(Self {
+ workspace: workspace.name.to_ascii_lowercase().replace(" ", "_"),
+ core_dir: core.source.to_string_lossy().to_string(),
+ core_name: core.name.replace("-", "_"),
+ type_gen: core
+ .type_gen
+ .as_ref()
+ .map(|x| x.to_string_lossy().to_string())
+ .or(Some("".into()))
+ .unwrap(),
+ shell_dir: shell.source.to_string_lossy().to_string(),
+ shell_name: shell.name.replace("-", "_"),
+ shell_name_dashes: shell.name.replace("_", "-"),
+ })
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +
use std::{fs, path::PathBuf};
+
+use crate::config::Workspace;
+use anyhow::{bail, Result};
+
+const CONFIG_FILE: &str = "Crux.toml";
+
+pub fn read_config() -> Result<Workspace> {
+ let path = PathBuf::from(CONFIG_FILE);
+ if let Ok(file) = &fs::read_to_string(path) {
+ let mut workspace: Workspace = toml::from_str(file)?;
+
+ let all_cores = workspace.cores.keys().cloned().collect::<Vec<_>>();
+ if all_cores.len() == 0 {
+ bail!("{CONFIG_FILE}: no cores defined");
+ }
+
+ for (name, core) in &mut workspace.cores {
+ core.name = name.to_string();
+ if !core.source.exists() {
+ bail!(
+ "{CONFIG_FILE}: core ({name}) source directory ({path}) does not exist",
+ path = core.source.display()
+ );
+ }
+ }
+
+ if let Some(shells) = &mut workspace.shells {
+ for (name, shell) in shells {
+ shell.name = name.to_string();
+ if !shell.source.exists() {
+ bail!(
+ "{CONFIG_FILE}: shell ({name}) source directory ({path}) does not exist",
+ path = shell.source.display()
+ );
+ }
+ if !shell.cores.iter().all(|core| all_cores.contains(core)) {
+ bail!("{CONFIG_FILE}: shell ({name}) references a core that does not exist");
+ }
+ }
+ }
+
+ Ok(workspace)
+ } else {
+ Ok(Workspace::default())
+ }
+}
+
+pub fn write_config(workspace: &Workspace) -> Result<()> {
+ let path = PathBuf::from(CONFIG_FILE);
+ let toml = toml::to_string(workspace)?;
+ fs::write(path, toml)?;
+ Ok(())
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +
mod registry;
+mod request_serde;
+
+use serde::{Deserialize, Serialize};
+
+use crate::Effect;
+use crate::{App, Core};
+use registry::ResolveRegistry;
+// ResolveByte is public to be accessible from crux_macros
+#[doc(hidden)]
+pub use request_serde::ResolveBytes;
+
+/// Request for a side-effect passed from the Core to the Shell. The `uuid` links
+/// the `Request` with the corresponding call to [`Core::resolve`] to pass the data back
+/// to the [`App::update`] function (wrapped in the event provided to the capability originating the effect).
+#[derive(Debug, Serialize, Deserialize)]
+pub struct Request<Eff>
+where
+ Eff: Serialize,
+{
+ pub uuid: Vec<u8>,
+ pub effect: Eff,
+}
+
+/// Bridge is a core wrapper presenting the same interface as the [`Core`] but in a
+/// serialized form
+pub struct Bridge<Eff, A>
+where
+ Eff: Effect,
+ A: App,
+{
+ core: Core<Eff, A>,
+ registry: ResolveRegistry,
+}
+
+impl<Eff, A> Bridge<Eff, A>
+where
+ Eff: Effect + Send + 'static,
+ A: App,
+{
+ /// Create a new Bridge using the provided `core`.
+ pub fn new(core: Core<Eff, A>) -> Self {
+ Self {
+ core,
+ registry: Default::default(),
+ }
+ }
+
+ /// Receive an event from the shell.
+ ///
+ /// The `event` is serialized and will be deserialized by the core before it's passed
+ /// to your app.
+ pub fn process_event<'de>(&self, event: &'de [u8]) -> Vec<u8>
+ where
+ A::Event: Deserialize<'de>,
+ {
+ self.process(None, event)
+ }
+
+ /// Receive a response to a capability request from the shell.
+ ///
+ /// The `output` is serialized capability output. It will be deserialized by the core.
+ /// The `uuid` MUST match the `uuid` of the effect that triggered it, else the core will panic.
+ pub fn handle_response<'de>(&self, uuid: &[u8], output: &'de [u8]) -> Vec<u8>
+ where
+ A::Event: Deserialize<'de>,
+ {
+ self.process(Some(uuid), output)
+ }
+
+ fn process<'de>(&self, uuid: Option<&[u8]>, data: &'de [u8]) -> Vec<u8>
+ where
+ A::Event: Deserialize<'de>,
+ {
+ let effects = match uuid {
+ None => {
+ let shell_event =
+ bincode::deserialize(data).expect("Message deserialization failed.");
+
+ self.core.process_event(shell_event)
+ }
+ Some(uuid) => {
+ self.registry.resume(uuid, data).expect(
+ "Response could not be handled. The request did not expect a response.",
+ );
+
+ self.core.process()
+ }
+ };
+
+ let requests: Vec<_> = effects
+ .into_iter()
+ .map(|eff| self.registry.register(eff))
+ .collect();
+
+ bincode::serialize(&requests).expect("Request serialization failed.")
+ }
+
+ /// Get the current state of the app's view model (serialized).
+ pub fn view(&self) -> Vec<u8> {
+ bincode::serialize(&self.core.view()).expect("View should serialize")
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +
use std::{
+ collections::{hash_map::Entry, HashMap},
+ sync::Mutex,
+};
+
+use uuid::Uuid;
+
+use super::Request;
+use crate::bridge::request_serde::ResolveBytes;
+use crate::core::ResolveError;
+use crate::Effect;
+
+type Store<T> = HashMap<[u8; 16], T>;
+
+pub struct ResolveRegistry(Mutex<Store<ResolveBytes>>);
+
+impl Default for ResolveRegistry {
+ fn default() -> Self {
+ Self(Mutex::new(Store::new()))
+ }
+}
+
+impl ResolveRegistry {
+ /// Register an effect for future continuation, when it has been processed
+ /// and output given back to the core.
+ ///
+ /// The `effect` will be serialized into its FFI counterpart before being stored
+ /// and wrapped in a [`Request`].
+ pub fn register<Eff>(&self, effect: Eff) -> Request<Eff::Ffi>
+ where
+ Eff: Effect,
+ {
+ let uuid = *Uuid::new_v4().as_bytes();
+ let (effect, resolve) = effect.serialize();
+
+ self.0
+ .lock()
+ .expect("Registry Mutex poisoned.")
+ .insert(uuid, resolve);
+
+ Request {
+ uuid: uuid.to_vec(),
+ effect,
+ }
+ }
+
+ /// Resume a previously registered effect. This may fail, either because UUID wasn't
+ /// found or because this effect was not expected to be resumed again.
+ pub fn resume(&self, uuid: &[u8], body: &[u8]) -> Result<(), ResolveError> {
+ let mut registry_lock = self.0.lock().expect("Registry Mutex poisoned");
+
+ let entry = {
+ let mut uuid_buf = [0; 16];
+ uuid_buf.copy_from_slice(uuid);
+
+ registry_lock.entry(uuid_buf)
+ };
+
+ let Entry::Occupied(mut entry) = entry else {
+ // FIXME return an Err instead of panicking here.
+ panic!("Request with UUID {uuid:?} not found.");
+ };
+
+ let resolve = entry.get_mut();
+
+ resolve.resolve(body)
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +
use crate::{
+ capability::Operation,
+ core::{Resolve, ResolveError},
+ Request,
+};
+
+type ResolveOnceBytes = Box<dyn FnOnce(&[u8]) + Send>;
+type ResolveManyBytes = Box<dyn Fn(&[u8]) -> Result<(), ()> + Send>;
+
+/// A deserializing version of Resolve
+///
+/// ResolveBytes is a separate type because lifetime elision doesn't work
+/// through generic type arguments. We can't create a ResolveRegistry of
+/// Resolve<&[u8]> without specifying an explicit lifetime.
+/// If you see a better way around this, please open a PR.
+pub enum ResolveBytes {
+ Never,
+ Once(ResolveOnceBytes),
+ Many(ResolveManyBytes),
+}
+
+impl ResolveBytes {
+ pub(crate) fn resolve(&mut self, bytes: &[u8]) -> Result<(), ResolveError> {
+ match self {
+ ResolveBytes::Never => Err(ResolveError::Never),
+ ResolveBytes::Many(f) => f(bytes).map_err(|_| ResolveError::FinishedMany),
+ ResolveBytes::Once(_) => {
+ // The resolve has been used, turn it into a Never
+ if let ResolveBytes::Once(f) = std::mem::replace(self, ResolveBytes::Never) {
+ f(bytes);
+ }
+
+ Ok(())
+ }
+ }
+ }
+}
+
+impl<Op> Request<Op>
+where
+ Op: Operation,
+{
+ /// Serialize this effect request using `effect` as a constructor
+ /// for a serializable Effect `Eff`
+ pub fn serialize<F, Eff>(self, effect: F) -> (Eff, ResolveBytes)
+ where
+ F: Fn(Op) -> Eff,
+ {
+ // FIXME should Eff be bound as `Serializable`?
+ let (operation, resolve) = (self.operation, self.resolve);
+
+ let resolve = resolve
+ .deserializing(|bytes| bincode::deserialize(bytes).expect("Deserialization failed"));
+
+ (effect(operation), resolve)
+ }
+}
+
+impl<Out> Resolve<Out> {
+ /// Convert this Resolve into a version which deserializes from bytes, consuming it.
+ /// The `func` argument is a 'deserializer' converting from bytes into the `Out` type.
+ fn deserializing<F>(self, func: F) -> ResolveBytes
+ where
+ F: (Fn(&[u8]) -> Out) + Send + Sync + 'static,
+ Out: 'static,
+ {
+ match self {
+ Resolve::Never => ResolveBytes::Never,
+ Resolve::Once(resolve) => ResolveBytes::Once(Box::new(move |bytes| {
+ let out = func(bytes);
+ resolve(out)
+ })),
+ Resolve::Many(resolve) => ResolveBytes::Many(Box::new(move |bytes| {
+ let out = func(bytes);
+ resolve(out)
+ })),
+ }
+ }
+}
+
1 +
pub mod render;
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +
//! Built-in capability used to notify the Shell that a UI update is necessary.
+
+use serde::{Deserialize, Serialize};
+
+use crate::{
+ capability::{CapabilityContext, Operation},
+ Capability,
+};
+
+/// Use an instance of `Render` to notify the Shell that it should update the user
+/// interface. This assumes a declarative UI framework is used in the Shell, which will
+/// take the ViewModel provided by [`Core::view`](crate::Core::view) and reconcile the new UI state based
+/// on the view model with the previous one.
+///
+/// For imperative UIs, the Shell will need to understand the difference between the two
+/// view models and update the user interface accordingly.
+pub struct Render<Ev> {
+ context: CapabilityContext<RenderOperation, Ev>,
+}
+
+/// The single operation `Render` implements.
+#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
+pub struct RenderOperation;
+
+impl Operation for RenderOperation {
+ type Output = ();
+}
+
+/// Public API of the capability, called by App::update.
+impl<Ev> Render<Ev>
+where
+ Ev: 'static,
+{
+ pub fn new(context: CapabilityContext<RenderOperation, Ev>) -> Self {
+ Self { context }
+ }
+
+ /// Call `render` from [`App::update`](crate::App::update) to signal to the Shell that
+ /// UI should be re-drawn.
+ pub fn render(&self) {
+ let ctx = self.context.clone();
+ self.context.spawn(async move {
+ ctx.notify_shell(RenderOperation).await;
+ });
+ }
+}
+
+impl<Ev> Capability<Ev> for Render<Ev> {
+ type Operation = RenderOperation;
+ type MappedSelf<MappedEv> = Render<MappedEv>;
+
+ fn map_event<F, NewEv>(&self, f: F) -> Self::MappedSelf<NewEv>
+ where
+ F: Fn(NewEv) -> Ev + Send + Sync + Copy + 'static,
+ Ev: 'static,
+ NewEv: 'static,
+ {
+ Render::new(self.context.map_event(f))
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +
// Wrappers around crossbeam_channel that only expose the functionality we need (and is safe on wasm)
+
+use std::sync::Arc;
+
+pub(crate) fn channel<T>() -> (Sender<T>, Receiver<T>)
+where
+ T: Send + 'static,
+{
+ let (sender, receiver) = crossbeam_channel::unbounded();
+ let sender = Sender {
+ inner: Arc::new(sender),
+ };
+ let receiver = Receiver { inner: receiver };
+
+ (sender, receiver)
+}
+
+pub struct Receiver<T> {
+ inner: crossbeam_channel::Receiver<T>,
+}
+
+impl<T> Receiver<T> {
+ /// Receives a message if any are waiting.
+ ///
+ /// Panics if the receiver has disconnected, so shouldn't be used if
+ /// that's possible.
+ pub fn receive(&self) -> Option<T> {
+ match self.inner.try_recv() {
+ Ok(inner) => Some(inner),
+ Err(crossbeam_channel::TryRecvError::Empty) => None,
+ Err(crossbeam_channel::TryRecvError::Disconnected) => {
+ // Users _generally_ shouldn't be messing with channels themselves, so
+ // this probably shouldn't happen. Might happen in tests, but lets
+ // fix that if we get complaints
+ panic!("Receiver was disconnected.")
+ }
+ }
+ }
+
+ /// Receives a message if any are waiting.
+ /// Returns the error branch if the sender has disconnected.
+ ///
+ /// This API isn't that nice, but isn't intended for public consumption
+ /// so whatevs.
+ pub fn try_receive(&self) -> Result<Option<T>, ()> {
+ match self.inner.try_recv() {
+ Ok(inner) => Ok(Some(inner)),
+ Err(crossbeam_channel::TryRecvError::Empty) => Ok(None),
+ Err(crossbeam_channel::TryRecvError::Disconnected) => Err(()),
+ }
+ }
+
+ pub fn drain(&self) -> Drain<T> {
+ Drain { receiver: self }
+ }
+}
+
+pub struct Drain<'a, T> {
+ receiver: &'a Receiver<T>,
+}
+
+impl<'a, T> Iterator for Drain<'a, T> {
+ type Item = T;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ self.receiver.receive()
+ }
+}
+
+pub struct Sender<T> {
+ inner: Arc<dyn SenderInner<T> + Send + Sync>,
+}
+
+impl<T> Clone for Sender<T> {
+ fn clone(&self) -> Self {
+ Self {
+ inner: Arc::clone(&self.inner),
+ }
+ }
+}
+
+impl<T> Sender<T>
+where
+ T: 'static,
+{
+ pub fn send(&self, t: T) {
+ self.inner.send(t)
+ }
+
+ pub fn map_input<NewT, F>(&self, func: F) -> Sender<NewT>
+ where
+ F: Fn(NewT) -> T + Send + Sync + 'static,
+ {
+ Sender {
+ inner: Arc::new(MappedInner {
+ sender: Arc::clone(&self.inner),
+ func,
+ }),
+ }
+ }
+}
+
+trait SenderInner<T> {
+ fn send(&self, t: T);
+}
+
+impl<T> SenderInner<T> for crossbeam_channel::Sender<T> {
+ fn send(&self, t: T) {
+ crossbeam_channel::Sender::send(self, t).unwrap()
+ }
+}
+
+pub struct MappedInner<T, F> {
+ sender: Arc<dyn SenderInner<T> + Send + Sync>,
+ func: F,
+}
+
+impl<F, T, U> SenderInner<U> for MappedInner<T, F>
+where
+ F: Fn(U) -> T,
+{
+ fn send(&self, value: U) {
+ self.sender.send((self.func)(value))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use static_assertions::assert_impl_all;
+
+ use super::*;
+
+ assert_impl_all!(Sender<i32>: Send);
+
+ #[test]
+ fn test_channels() {
+ let (send, recv) = channel();
+
+ send.send(Some(1));
+ assert_eq!(recv.receive(), Some(Some(1)));
+
+ let wrapped_send = send.map_input(Some);
+ wrapped_send.send(1);
+ assert_eq!(recv.receive(), Some(Some(1)));
+
+ assert_eq!(recv.receive(), None);
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +
use std::{
+ sync::{Arc, Mutex},
+ task::Context,
+};
+
+use crossbeam_channel::{Receiver, Sender};
+use futures::{
+ future,
+ task::{waker_ref, ArcWake},
+ Future, FutureExt,
+};
+
+pub(crate) struct QueuingExecutor {
+ ready_queue: Receiver<Arc<Task>>,
+}
+
+#[derive(Clone)]
+pub struct Spawner {
+ task_sender: Sender<Arc<Task>>,
+}
+
+struct Task {
+ future: Mutex<Option<future::BoxFuture<'static, ()>>>,
+
+ task_sender: Sender<Arc<Task>>,
+}
+
+pub(crate) fn executor_and_spawner() -> (QueuingExecutor, Spawner) {
+ let (task_sender, ready_queue) = crossbeam_channel::unbounded();
+
+ (QueuingExecutor { ready_queue }, Spawner { task_sender })
+}
+
+impl Spawner {
+ pub fn spawn(&self, future: impl Future<Output = ()> + 'static + Send) {
+ let future = future.boxed();
+ let task = Arc::new(Task {
+ future: Mutex::new(Some(future)),
+ task_sender: self.task_sender.clone(),
+ });
+
+ self.task_sender
+ .send(task)
+ .expect("to be able to send tasks on an unbounded queue")
+ }
+}
+
+impl ArcWake for Task {
+ fn wake_by_ref(arc_self: &Arc<Self>) {
+ let cloned = arc_self.clone();
+ arc_self
+ .task_sender
+ .send(cloned)
+ .expect("to be able to send tasks on an unbounded queue")
+ }
+}
+
+impl QueuingExecutor {
+ pub fn run_all(&self) {
+ // While there are tasks to be processed
+ while let Ok(task) = self.ready_queue.try_recv() {
+ // Unlock the future in the Task
+ let mut future_slot = task.future.lock().unwrap();
+
+ // Take it, replace with None, ...
+ if let Some(mut future) = future_slot.take() {
+ let waker = waker_ref(&task);
+ let context = &mut Context::from_waker(&waker);
+
+ // ...and poll it
+ if future.as_mut().poll(context).is_pending() {
+ // If it's still pending, put it back
+ *future_slot = Some(future)
+ }
+ }
+ }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +
//! Capabilities provide a user-friendly API to request side-effects from the shell.
+//!
+//! Typically, capabilities provide I/O and host API access. Capabilities are external to the
+//! core Crux library. Some are part of the Crux core distribution, others are expected to be built by the
+//! community. Apps can also build single-use capabilities where necessary.
+//!
+//! # Example use
+//!
+//! A typical use of a capability would look like the following:
+//!
+//! ```rust
+//!# use url::Url;
+//!# const API_URL: &str = "";
+//!# pub enum Event { Increment, Set(crux_http::Result<crux_http::Response<usize>>) }
+//!# #[derive(crux_macros::Effect)]
+//!# pub struct Capabilities {
+//!# pub render: crux_core::render::Render<Event>,
+//!# pub http: crux_http::Http<Event>,
+//!# }
+//!# #[derive(Default)] pub struct Model { count: usize }
+//!# #[derive(Default)] pub struct App;
+//!#
+//!# impl crux_core::App for App {
+//!# type Event = Event;
+//!# type Model = Model;
+//!# type ViewModel = ();
+//!# type Capabilities = Capabilities;
+//! fn update(&self, event: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities) {
+//! match event {
+//! //...
+//! Event::Increment => {
+//! model.count += 1;
+//! caps.render.render(); // Render capability
+//!
+//! let base = Url::parse(API_URL).unwrap();
+//! let url = base.join("/inc").unwrap();
+//! caps.http.post(url).expect_json().send(Event::Set); // HTTP client capability
+//! }
+//! Event::Set(_) => todo!(),
+//! }
+//! }
+//!# fn view(&self, model: &Self::Model) {
+//!# unimplemented!()
+//!# }
+//!# }
+
+//! ```
+//!
+//! Capabilities don't _perform_ side-effects themselves, they request them from the Shell. As a consequence
+//! the capability calls within the `update` function **only queue up the requests**. The side-effects themselves
+//! are performed concurrently and don't block the update function.
+//!
+//! In order to use a capability, the app needs to include it in its `Capabilities` associated type and `WithContext`
+//! trait implementation (which can be provided by the `Effect` macro from the `crux_macros` crate). For example:
+//!
+//! ```rust
+//! mod root {
+//!
+//! // An app module which can be reused in different apps
+//! mod my_app {
+//! use crux_core::{capability::CapabilityContext, App, render::Render};
+//! use crux_macros::Effect;
+//! use serde::{Serialize, Deserialize};
+//!
+//! #[derive(Default)]
+//! pub struct MyApp;
+//! #[derive(Serialize, Deserialize)]
+//! pub struct Event;
+//!
+//! // The `Effect` derive macro generates an `Effect` type that is used by the
+//! // Shell to dispatch side-effect requests to the right capability implementation
+//! // (and, in some languages, checking that all necessary capabilities are implemented)
+//! #[derive(Effect)]
+//! #[effect(app = "MyApp")]
+//! pub struct Capabilities {
+//! pub render: Render<Event>
+//! }
+//!
+//! impl App for MyApp {
+//! type Model = ();
+//! type Event = Event;
+//! type ViewModel = ();
+//! type Capabilities = Capabilities;
+//!
+//! fn update(&self, event: Event, model: &mut (), caps: &Capabilities) {
+//! caps.render.render();
+//! }
+//!
+//! fn view(&self, model: &()) {
+//! ()
+//! }
+//! }
+//! }
+//! }
+//! ```
+//!
+//! # Implementing a capability
+//!
+//! Capabilities provide an interface to request side-effects. The interface has asynchronous semantics
+//! with a form of callback. A typical capability call can look like this:
+//!
+//! ```rust,ignore
+//! caps.ducks.get_in_a_row(10, Event::RowOfDucks)
+//! ```
+//!
+//! The call above translates into "Get 10 ducks in a row and return them to me using the `RowOfDucks` event".
+//! The capability's job is to translate this request into a serializable message and instruct the Shell to
+//! do the duck herding and when it receives the ducks back, wrap them in the requested event and return it
+//! to the app.
+//!
+//! We will refer to `get_in_row` in the above call as an _operation_, the `10` is an _input_, and the
+//! `Event::RowOfDucks` is an event constructor - a function, which eventually receives the row of ducks
+//! and returns a variant of the `Event` enum. Conveniently, enum tuple variants can be used as functions,
+//! and so that will be the typical use.
+//!
+//! This is what the capability implementation could look like:
+//!
+//! ```rust
+//! use crux_core::{
+//! capability::{CapabilityContext, Operation},
+//! };
+//! use crux_macros::Capability;
+//! use serde::{Serialize, Deserialize};
+//!
+//! // A duck
+//! #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
+//! struct Duck;
+//!
+//! // Operations that can be requested from the Shell
+//! #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+//! enum DuckOperation {
+//! GetInARow(usize)
+//! }
+//!
+//! // Respective outputs for those operations
+//! #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+//! enum DuckOutput {
+//! GetInRow(Vec<Duck>)
+//! }
+//!
+//! // Link the input and output type
+//! impl Operation for DuckOperation {
+//! type Output = DuckOutput;
+//! }
+//!
+//! // The capability. Context will provide the interface to the rest of the system.
+//! #[derive(Capability)]
+//! struct Ducks<Event> {
+//! context: CapabilityContext<DuckOperation, Event>
+//! };
+//!
+//! impl<Event> Ducks<Event> {
+//! pub fn new(context: CapabilityContext<DuckOperation, Event>) -> Self {
+//! Self { context }
+//! }
+//!
+//! pub fn get_in_a_row<F>(&self, number_of_ducks: usize, event: F)
+//! where
+//! Event: 'static,
+//! F: Fn(Vec<Duck>) -> Event + Send + 'static,
+//! {
+//! let ctx = self.context.clone();
+//! // Start a shell interaction
+//! self.context.spawn(async move {
+//! // Instruct Shell to get ducks in a row and await the ducks
+//! let ducks = ctx.request_from_shell(DuckOperation::GetInARow(number_of_ducks)).await;
+//!
+//! // Unwrap the ducks and wrap them in the requested event
+//! // This will always succeed, as long as the Shell implementation is correct
+//! // and doesn't send the wrong output type back
+//! if let DuckOutput::GetInRow(ducks) = ducks {
+//! // Queue an app update with the ducks event
+//! ctx.update_app(event(ducks));
+//! }
+//! })
+//! }
+//! }
+//! ```
+//!
+//! The `self.context.spawn` API allows a multi-step transaction with the Shell to be performed by a capability
+//! without involving the app, until the exchange has completed. During the exchange, one or more events can
+//! be emitted (allowing a subscription or streaming like capability to be built).
+//!
+//! For Shell requests that have no output, you can use [`CapabilityContext::notify_shell`].
+//!
+//! `DuckOperation` and `DuckOutput` show how the set of operations can be extended. In simple capabilities,
+//! with a single operation, these can be structs, or simpler types. For example, the HTTP capability works directly with
+//! `HttpRequest` and `HttpResponse`.
+
+pub(crate) mod channel;
+
+mod executor;
+mod shell_request;
+mod shell_stream;
+
+use futures::Future;
+use std::sync::Arc;
+
+pub(crate) use channel::channel;
+pub(crate) use executor::{executor_and_spawner, QueuingExecutor};
+
+use crate::Request;
+use channel::Sender;
+
+/// Operation trait links together input and output of a side-effect.
+///
+/// You implement `Operation` on the payload sent by the capability to the shell using [`CapabilityContext::request_from_shell`].
+///
+/// For example (from `crux_http`):
+///
+/// ```rust,ignore
+/// impl Operation for HttpRequest {
+/// type Output = HttpResponse;
+/// }
+/// ```
+pub trait Operation: serde::Serialize + PartialEq + Send + 'static {
+ /// `Output` assigns the type this request results in.
+ type Output: serde::de::DeserializeOwned + Send + 'static;
+}
+
+/// Implement the `Capability` trait for your capability. This will allow
+/// mapping events when composing apps from submodules.
+///
+/// Note that this implementation can be generated by the `Capability` derive macro (in the `crux_macros` crate).
+///
+/// Example:
+///
+/// ```rust
+///# use crux_core::{Capability, capability::{CapabilityContext, Operation}};
+///# pub struct Http<Ev> {
+///# context: CapabilityContext<HttpOperation, Ev>,
+///# }
+///# #[derive(serde::Serialize, PartialEq, Eq)] pub struct HttpOperation;
+///# impl Operation for HttpOperation {
+///# type Output = ();
+///# }
+///# impl<Ev> Http<Ev> where Ev: 'static, {
+///# pub fn new(context: CapabilityContext<HttpOperation, Ev>) -> Self {
+///# Self { context }
+///# }
+///# }
+/// impl<Ev> Capability<Ev> for Http<Ev> {
+/// type Operation = HttpOperation;
+/// type MappedSelf<MappedEv> = Http<MappedEv>;
+///
+/// fn map_event<F, NewEvent>(&self, f: F) -> Self::MappedSelf<NewEvent>
+/// where
+/// F: Fn(NewEvent) -> Ev + Send + Sync + Copy + 'static,
+/// Ev: 'static,
+/// NewEvent: 'static,
+/// {
+/// Http::new(self.context.map_event(f))
+/// }
+/// }
+/// ```
+pub trait Capability<Ev> {
+ type Operation: Operation;
+
+ type MappedSelf<MappedEv>;
+
+ fn map_event<F, NewEv>(&self, f: F) -> Self::MappedSelf<NewEv>
+ where
+ F: Fn(NewEv) -> Ev + Send + Sync + Copy + 'static,
+ Ev: 'static,
+ NewEv: 'static + Send;
+}
+
+/// Allows Crux to construct app's set of required capabilities, providing context
+/// they can then use to request effects and dispatch events.
+///
+/// `new_with_context` is called by Crux and should return an instance of the app's `Capabilities` type with
+/// all capabilities constructed with context passed in. Use `Context::specialize` to
+/// create an appropriate context instance with the effect constructor which should
+/// wrap the requested operations.
+///
+/// Note that this implementation can be generated by the derive macro `Effect` (in the `crux_macros` crate).
+///
+/// ```rust
+/// # #[derive(Default)]
+/// # struct App;
+/// # pub enum Event {}
+/// # #[allow(dead_code)]
+/// # pub struct Capabilities {
+/// # http: crux_http::Http<Event>,
+/// # render: crux_core::render::Render<Event>,
+/// # }
+/// # pub enum Effect {
+/// # Http(crux_core::Request<<crux_http::Http<Event> as crux_core::capability::Capability<Event>>::Operation>),
+/// # Render(crux_core::Request<<crux_core::render::Render<Event> as crux_core::capability::Capability<Event>>::Operation>),
+/// # }
+/// # #[derive(serde::Serialize)]
+/// # pub enum EffectFfi {
+/// # Http(<crux_http::Http<Event> as crux_core::capability::Capability<Event>>::Operation),
+/// # Render(<crux_core::render::Render<Event> as crux_core::capability::Capability<Event>>::Operation),
+/// # }
+/// # impl crux_core::App for App {
+/// # type Event = Event;
+/// # type Model = ();
+/// # type ViewModel = ();
+/// # type Capabilities = Capabilities;
+/// # fn update(&self, _event: Self::Event, _model: &mut Self::Model, _caps: &Self::Capabilities) {
+/// # todo!()
+/// # }
+/// # fn view(&self, _model: &Self::Model) -> Self::ViewModel {
+/// # todo!()
+/// # }
+/// # }
+/// # impl crux_core::Effect for Effect {
+/// # type Ffi = EffectFfi;
+/// # fn serialize<'out>(self) -> (Self::Ffi, crux_core::bridge::ResolveBytes) {
+/// # match self {
+/// # Effect::Http(request) => request.serialize(EffectFfi::Http),
+/// # Effect::Render(request) => request.serialize(EffectFfi::Render),
+/// # }
+/// # }
+/// # }
+/// impl crux_core::WithContext<App, Effect> for Capabilities {
+/// fn new_with_context(
+/// context: crux_core::capability::ProtoContext<Effect, Event>,
+/// ) -> Capabilities {
+/// Capabilities {
+/// http: crux_http::Http::new(context.specialize(Effect::Http)),
+/// render: crux_core::render::Render::new(context.specialize(Effect::Render)),
+/// }
+/// }
+/// }
+/// ```
+pub trait WithContext<App, Ef>
+where
+ App: crate::App,
+{
+ fn new_with_context(context: ProtoContext<Ef, App::Event>) -> App::Capabilities;
+}
+
+/// An interface for capabilities to interact with the app and the shell.
+///
+/// To use [`update_app`](CapabilityContext::update_app), [`notify_shell`](CapabilityContext::notify_shell)
+/// or [`request_from_shell`](CapabilityContext::request_from_shell), spawn a task first.
+///
+/// For example (from `crux_time`)
+///
+/// ```rust
+/// # #[derive(PartialEq,serde::Serialize)]pub struct TimeRequest;
+/// # #[derive(serde::Deserialize)]pub struct TimeResponse(pub String);
+/// # impl crux_core::capability::Operation for TimeRequest {
+/// # type Output = TimeResponse;
+/// # }
+/// # pub struct Time<Ev> {
+/// # context: crux_core::capability::CapabilityContext<TimeRequest, Ev>,
+/// # }
+/// # impl<Ev> Time<Ev> where Ev: 'static, {
+/// # pub fn new(context: crux_core::capability::CapabilityContext<TimeRequest, Ev>) -> Self {
+/// # Self { context }
+/// # }
+///
+/// pub fn get<F>(&self, callback: F)
+/// where
+/// F: Fn(TimeResponse) -> Ev + Send + Sync + 'static,
+/// {
+/// let ctx = self.context.clone();
+/// self.context.spawn(async move {
+/// let response = ctx.request_from_shell(TimeRequest).await;
+///
+/// ctx.update_app(callback(response));
+/// });
+/// }
+/// # }
+/// ```
+///
+pub struct CapabilityContext<Op, Event>
+where
+ Op: Operation,
+{
+ inner: std::sync::Arc<ContextInner<Op, Event>>,
+}
+
+struct ContextInner<Op, Event>
+where
+ Op: Operation,
+{
+ shell_channel: Sender<Request<Op>>,
+ app_channel: Sender<Event>,
+ spawner: executor::Spawner,
+}
+
+/// Initial version of capability Context which has not yet been specialized to a chosen capability
+pub struct ProtoContext<Eff, Event> {
+ shell_channel: Sender<Eff>,
+ app_channel: Sender<Event>,
+ spawner: executor::Spawner,
+}
+
+impl<Op, Ev> Clone for CapabilityContext<Op, Ev>
+where
+ Op: Operation,
+{
+ fn clone(&self) -> Self {
+ Self {
+ inner: Arc::clone(&self.inner),
+ }
+ }
+}
+
+impl<Eff, Ev> ProtoContext<Eff, Ev>
+where
+ Ev: 'static,
+ Eff: 'static,
+{
+ pub(crate) fn new(
+ shell_channel: Sender<Eff>,
+ app_channel: Sender<Ev>,
+ spawner: executor::Spawner,
+ ) -> Self {
+ Self {
+ shell_channel,
+ app_channel,
+ spawner,
+ }
+ }
+
+ /// Specialize the CapabilityContext to a specific capability, wrapping its operations into
+ /// an Effect `Ef`. The `func` argument will typically be an Effect variant constructor, but
+ /// can be any function taking the capability's operation type and returning
+ /// the effect type.
+ ///
+ /// This will likely only be called from the implementation of [`WithContext`]
+ /// for the app's `Capabilities` type. You should not need to call this function directly.
+ pub fn specialize<Op, F>(&self, func: F) -> CapabilityContext<Op, Ev>
+ where
+ F: Fn(Request<Op>) -> Eff + Sync + Send + Copy + 'static,
+ Op: Operation,
+ {
+ CapabilityContext::new(
+ self.shell_channel.map_input(func),
+ self.app_channel.clone(),
+ self.spawner.clone(),
+ )
+ }
+}
+
+impl<Op, Ev> CapabilityContext<Op, Ev>
+where
+ Op: Operation,
+ Ev: 'static,
+{
+ pub(crate) fn new(
+ shell_channel: Sender<Request<Op>>,
+ app_channel: Sender<Ev>,
+ spawner: executor::Spawner,
+ ) -> Self {
+ let inner = Arc::new(ContextInner {
+ shell_channel,
+ app_channel,
+ spawner,
+ });
+
+ CapabilityContext { inner }
+ }
+
+ /// Spawn a task to do the asynchronous work. Within the task, async code
+ /// can be used to interact with the Shell and the App.
+ pub fn spawn(&self, f: impl Future<Output = ()> + 'static + Send) {
+ self.inner.spawner.spawn(f);
+ }
+
+ /// Send an effect request to the shell in a fire and forget fashion. The
+ /// provided `operation` does not expect anything to be returned back.
+ pub async fn notify_shell(&self, operation: Op) {
+ // This function might look like it doesn't need to be async but
+ // it's important that it is. It forces all capabilities to
+ // spawn onto the executor which keeps the ordering of effects
+ // consistent with their function calls.
+ self.inner
+ .shell_channel
+ .send(Request::resolves_never(operation));
+ }
+
+ /// Send an event to the app. The event will be processed on the next
+ /// run of the update loop. You can call `update_app` several times,
+ /// the events will be queued up and processed sequentially after your
+ /// async task either `await`s or finishes.
+ pub fn update_app(&self, event: Ev) {
+ self.inner.app_channel.send(event);
+ }
+
+ /// Transform the CapabilityContext into one which uses the provided function to
+ /// map each event dispatched with `update_app` to a different event type.
+ ///
+ /// This is useful when composing apps from modules to wrap a submodule's
+ /// event type with a specific variant of the parent module's event, so it can
+ /// be forwarded to the submodule when received.
+ ///
+ /// In a typical case you would implement `From` on the submodule's `Capabilities` type
+ ///
+ /// ```rust
+ /// # use crux_core::Capability;
+ /// # #[derive(Default)]
+ /// # struct App;
+ /// # pub enum Event {
+ /// # Submodule(child::Event),
+ /// # }
+ /// # #[derive(crux_macros::Effect)]
+ /// # pub struct Capabilities {
+ /// # some_capability: crux_time::Time<Event>,
+ /// # render: crux_core::render::Render<Event>,
+ /// # }
+ /// # impl crux_core::App for App {
+ /// # type Event = Event;
+ /// # type Model = ();
+ /// # type ViewModel = ();
+ /// # type Capabilities = Capabilities;
+ /// # fn update(
+ /// # &self,
+ /// # _event: Self::Event,
+ /// # _model: &mut Self::Model,
+ /// # _caps: &Self::Capabilities,
+ /// # ) {
+ /// # todo!()
+ /// # }
+ /// # fn view(&self, _model: &Self::Model) -> Self::ViewModel {
+ /// # todo!()
+ /// # }
+ /// # }
+ ///impl From<&Capabilities> for child::Capabilities {
+ /// fn from(incoming: &Capabilities) -> Self {
+ /// child::Capabilities {
+ /// some_capability: incoming.some_capability.map_event(Event::Submodule),
+ /// render: incoming.render.map_event(Event::Submodule),
+ /// }
+ /// }
+ ///}
+ /// # mod child {
+ /// # #[derive(Default)]
+ /// # struct App;
+ /// # pub struct Event;
+ /// # #[derive(crux_macros::Effect)]
+ /// # pub struct Capabilities {
+ /// # pub some_capability: crux_time::Time<Event>,
+ /// # pub render: crux_core::render::Render<Event>,
+ /// # }
+ /// # impl crux_core::App for App {
+ /// # type Event = Event;
+ /// # type Model = ();
+ /// # type ViewModel = ();
+ /// # type Capabilities = Capabilities;
+ /// # fn update(
+ /// # &self,
+ /// # _event: Self::Event,
+ /// # _model: &mut Self::Model,
+ /// # _caps: &Self::Capabilities,
+ /// # ) {
+ /// # todo!()
+ /// # }
+ /// # fn view(&self, _model: &Self::Model) -> Self::ViewModel {
+ /// # todo!()
+ /// # }
+ /// # }
+ /// # }
+ /// ```
+ ///
+ /// in the parent module's `update` function, you can then call `.into()` on the
+ /// capabilities, before passing them down to the submodule.
+ pub fn map_event<NewEv, F>(&self, func: F) -> CapabilityContext<Op, NewEv>
+ where
+ F: Fn(NewEv) -> Ev + Sync + Send + 'static,
+ NewEv: 'static,
+ {
+ CapabilityContext::new(
+ self.inner.shell_channel.clone(),
+ self.inner.app_channel.map_input(func),
+ self.inner.spawner.clone(),
+ )
+ }
+
+ pub(crate) fn send_request(&self, request: Request<Op>) {
+ self.inner.shell_channel.send(request);
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use serde::Serialize;
+ use static_assertions::assert_impl_all;
+
+ use super::*;
+
+ #[allow(dead_code)]
+ enum Effect {}
+
+ #[allow(dead_code)]
+ enum Event {}
+
+ #[derive(PartialEq, Serialize)]
+ struct Op {}
+
+ impl Operation for Op {
+ type Output = ();
+ }
+
+ assert_impl_all!(ProtoContext<Effect, Event>: Send, Sync);
+ assert_impl_all!(CapabilityContext<Op, Event>: Send, Sync);
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +
//! Async support for implementing capabilities
+//!
+use std::{
+ sync::{Arc, Mutex},
+ task::{Poll, Waker},
+};
+
+use futures::Future;
+
+use crate::Request;
+
+pub struct ShellRequest<T> {
+ shared_state: Arc<Mutex<SharedState<T>>>,
+}
+
+struct SharedState<T> {
+ result: Option<T>,
+ waker: Option<Waker>,
+ send_request: Option<Box<dyn FnOnce() + Send + 'static>>,
+}
+
+impl<T> Future for ShellRequest<T> {
+ type Output = T;
+
+ fn poll(
+ self: std::pin::Pin<&mut Self>,
+ cx: &mut std::task::Context<'_>,
+ ) -> std::task::Poll<Self::Output> {
+ let mut shared_state = self.shared_state.lock().unwrap();
+
+ // If there's still a request to send, take it and send it
+ if let Some(send_request) = shared_state.send_request.take() {
+ send_request();
+ }
+
+ // If a result has been delivered, we're ready to continue
+ // Else we're pending with the waker from context
+ match shared_state.result.take() {
+ Some(result) => Poll::Ready(result),
+ None => {
+ shared_state.waker = Some(cx.waker().clone());
+ Poll::Pending
+ }
+ }
+ }
+}
+
+impl<Op, Ev> crate::capability::CapabilityContext<Op, Ev>
+where
+ Op: crate::capability::Operation,
+ Ev: 'static,
+{
+ /// Send an effect request to the shell, expecting an output. The
+ /// provided `operation` describes the effect input in a serialisable fashion,
+ /// and must implement the [`Operation`](crate::capability::Operation) trait to declare the expected
+ /// output type.
+ ///
+ /// `request_from_shell` is returns a future of the output, which can be
+ /// `await`ed. You should only call this method inside an async task
+ /// created with [`CapabilityContext::spawn`](crate::capability::CapabilityContext::spawn).
+ pub fn request_from_shell(&self, operation: Op) -> ShellRequest<Op::Output> {
+ let shared_state = Arc::new(Mutex::new(SharedState {
+ result: None,
+ waker: None,
+ send_request: None,
+ }));
+
+ // Our callback holds a weak pointer to avoid circular references
+ // from shared_state -> send_request -> request -> shared_state
+ let callback_shared_state = Arc::downgrade(&shared_state);
+
+ let request = Request::resolves_once(operation, move |result| {
+ let Some(shared_state) = callback_shared_state.upgrade() else {
+ // The ShellRequest was dropped before we were called, so just
+ // do nothing.
+ return;
+ };
+
+ let mut shared_state = shared_state.lock().unwrap();
+
+ // Attach the result to the shared state of the future
+ shared_state.result = Some(result);
+ // Signal the executor to wake the task holding this future
+ if let Some(waker) = shared_state.waker.take() {
+ waker.wake()
+ }
+ });
+
+ // Send the request on the next poll of the ShellRequest future
+ let send_req_context = self.clone();
+ let send_request = move || send_req_context.send_request(request);
+
+ shared_state.lock().unwrap().send_request = Some(Box::new(send_request));
+
+ ShellRequest { shared_state }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use assert_matches::assert_matches;
+
+ use crate::capability::{channel, executor_and_spawner, CapabilityContext, Operation};
+
+ #[derive(serde::Serialize, PartialEq, Eq, Debug)]
+ struct TestOperation;
+
+ impl Operation for TestOperation {
+ type Output = ();
+ }
+
+ #[test]
+ fn test_effect_future() {
+ let (request_sender, requests) = channel();
+ let (event_sender, events) = channel::<()>();
+ let (executor, spawner) = executor_and_spawner();
+ let capability_context =
+ CapabilityContext::new(request_sender, event_sender.clone(), spawner.clone());
+
+ let future = capability_context.request_from_shell(TestOperation);
+
+ // The future hasn't been awaited so we shouldn't have any requests.
+ assert_matches!(requests.receive(), None);
+ assert_matches!(events.receive(), None);
+
+ // It also shouldn't have spawned anything so check that
+ executor.run_all();
+ assert_matches!(requests.receive(), None);
+ assert_matches!(events.receive(), None);
+
+ spawner.spawn(async move {
+ future.await;
+ event_sender.send(());
+ });
+
+ // We still shouldn't have any requests
+ assert_matches!(requests.receive(), None);
+ assert_matches!(events.receive(), None);
+
+ executor.run_all();
+ let mut request = requests.receive().expect("we should have a request here");
+ assert_matches!(requests.receive(), None);
+ assert_matches!(events.receive(), None);
+
+ request.resolve(()).expect("request should resolve");
+
+ assert_matches!(requests.receive(), None);
+ assert_matches!(events.receive(), None);
+
+ executor.run_all();
+ assert_matches!(requests.receive(), None);
+ assert_matches!(events.receive(), Some(()));
+ assert_matches!(events.receive(), None);
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +
use std::{
+ sync::{Arc, Mutex},
+ task::{Poll, Waker},
+};
+
+use futures::Stream;
+
+use super::{channel, channel::Receiver};
+use crate::core::Request;
+
+pub struct ShellStream<T> {
+ shared_state: Arc<Mutex<SharedState<T>>>,
+}
+
+struct SharedState<T> {
+ receiver: Receiver<T>,
+ waker: Option<Waker>,
+ send_request: Option<Box<dyn FnOnce() + Send + 'static>>,
+}
+
+impl<T> Stream for ShellStream<T> {
+ type Item = T;
+
+ fn poll_next(
+ self: std::pin::Pin<&mut Self>,
+ cx: &mut std::task::Context<'_>,
+ ) -> Poll<Option<Self::Item>> {
+ let mut shared_state = self.shared_state.lock().unwrap();
+
+ if let Some(send_request) = shared_state.send_request.take() {
+ send_request();
+ }
+
+ match shared_state.receiver.try_receive() {
+ Ok(Some(next)) => Poll::Ready(Some(next)),
+ Ok(None) => {
+ shared_state.waker = Some(cx.waker().clone());
+ Poll::Pending
+ }
+ Err(_) => Poll::Ready(None),
+ }
+ }
+}
+
+impl<Op, Ev> crate::capability::CapabilityContext<Op, Ev>
+where
+ Op: crate::capability::Operation,
+ Ev: 'static,
+{
+ /// Send an effect request to the shell, expecting a stream of responses
+ pub fn stream_from_shell(&self, operation: Op) -> ShellStream<Op::Output> {
+ let (sender, receiver) = channel();
+ let shared_state = Arc::new(Mutex::new(SharedState {
+ receiver,
+ waker: None,
+ send_request: None,
+ }));
+
+ // Our callback holds a weak pointer so the channel can be freed
+ // whenever the associated task ends.
+ let callback_shared_state = Arc::downgrade(&shared_state);
+
+ let request = Request::resolves_many_times(operation, move |result| {
+ let Some(shared_state) = callback_shared_state.upgrade() else {
+ // Let the caller know that the associated task has finished.
+ return Err(());
+ };
+
+ let mut shared_state = shared_state.lock().unwrap();
+
+ sender.send(result);
+ if let Some(waker) = shared_state.waker.take() {
+ waker.wake();
+ }
+
+ Ok(())
+ });
+
+ // Put a callback into our shared_state so that we only send
+ // our request to the shell when the stream is first polled.
+ let send_req_context = self.clone();
+ let send_request = move || send_req_context.send_request(request);
+ shared_state.lock().unwrap().send_request = Some(Box::new(send_request));
+
+ ShellStream { shared_state }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use assert_matches::assert_matches;
+
+ use crate::capability::{channel, executor_and_spawner, CapabilityContext, Operation};
+
+ #[derive(serde::Serialize, PartialEq, Eq, Debug)]
+ struct TestOperation;
+
+ impl Operation for TestOperation {
+ type Output = Option<Done>;
+ }
+
+ #[derive(serde::Deserialize, PartialEq, Eq, Debug)]
+ struct Done;
+
+ #[test]
+ fn test_shell_stream() {
+ let (request_sender, requests) = channel();
+ let (event_sender, events) = channel::<()>();
+ let (executor, spawner) = executor_and_spawner();
+ let capability_context =
+ CapabilityContext::new(request_sender, event_sender.clone(), spawner.clone());
+
+ let mut stream = capability_context.stream_from_shell(TestOperation);
+
+ // The stream hasn't been polled so we shouldn't have any requests.
+ assert_matches!(requests.receive(), None);
+ assert_matches!(events.receive(), None);
+
+ // It also shouldn't have spawned anything so check that
+ executor.run_all();
+ assert_matches!(requests.receive(), None);
+ assert_matches!(events.receive(), None);
+
+ spawner.spawn(async move {
+ use futures::StreamExt;
+ while let Some(maybe_done) = stream.next().await {
+ event_sender.send(());
+ if maybe_done.is_some() {
+ break;
+ }
+ }
+ });
+
+ // We still shouldn't have any requests
+ assert_matches!(requests.receive(), None);
+ assert_matches!(events.receive(), None);
+
+ executor.run_all();
+ let mut request = requests.receive().expect("we should have a request here");
+
+ assert_matches!(requests.receive(), None);
+ assert_matches!(events.receive(), None);
+
+ request.resolve(None).unwrap();
+
+ executor.run_all();
+
+ // We should have one event
+ assert_matches!(requests.receive(), None);
+ assert_matches!(events.receive(), Some(()));
+ assert_matches!(events.receive(), None);
+
+ // Resolve it a few more times and then finish.
+ request.resolve(None).unwrap();
+ request.resolve(None).unwrap();
+ request.resolve(Some(Done)).unwrap();
+ executor.run_all();
+
+ // We should have three events
+ assert_matches!(requests.receive(), None);
+ assert_matches!(events.receive(), Some(()));
+ assert_matches!(events.receive(), Some(()));
+ assert_matches!(events.receive(), Some(()));
+ assert_matches!(events.receive(), None);
+
+ // The next resolve should error as we've terminated the task
+ request
+ .resolve(None)
+ .expect_err("resolving a finished task should error");
+ }
+}
+
use serde::Serialize;
+
+use crate::bridge::ResolveBytes;
+
+/// Implemented automatically with the Effect macro from `crux_macros`.
+/// This is used by the [`Bridge`](crate::bridge::Bridge) to serialize effects going across the
+/// FFI boundary.
+pub trait Effect: Send + 'static {
+ /// Ffi is an enum with variants corresponding to the Effect variants
+ /// but instead of carrying a `Request<Op>` they carry the `Op` directly
+ type Ffi: Serialize;
+
+ /// Converts the `Effect` into its FFI counterpart and returns it alongside
+ /// a deserializing version of the resolve callback for the request that the
+ /// original `Effect` was carrying.
+ fn serialize(self) -> (Self::Ffi, ResolveBytes);
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +
mod effect;
+mod request;
+mod resolve;
+
+use std::sync::RwLock;
+
+pub use effect::Effect;
+pub use request::Request;
+pub use resolve::ResolveError;
+
+pub(crate) use resolve::Resolve;
+
+use crate::capability::{self, channel::Receiver, Operation, ProtoContext, QueuingExecutor};
+use crate::{App, WithContext};
+
+/// The Crux core. Create an instance of this type with your effect type, and your app type as type parameters
+///
+/// The core interface allows passing in events of type `A::Event` using [`Core::process_event`].
+/// It will return back an effect of type `Ef`, containing an effect request, with the input needed for processing
+/// the effect. the `Effect` type can be used by shells to dispatch to the right capability implementation.
+///
+/// The result of the capability's work can then be sent back to the core using [`Core::resolve`], passing
+/// in the request and the corresponding capability output type.
+pub struct Core<Ef, A>
+where
+ A: App,
+{
+ model: RwLock<A::Model>,
+ executor: QueuingExecutor,
+ capabilities: A::Capabilities,
+ requests: Receiver<Ef>,
+ capability_events: Receiver<A::Event>,
+ app: A,
+}
+
+impl<Ef, A> Core<Ef, A>
+where
+ Ef: Effect,
+ A: App,
+{
+ /// Create an instance of the Crux core to start a Crux application, e.g.
+ ///
+ /// ```rust,ignore
+ /// let core: Core<HelloEffect, Hello> = Core::new::<HelloCapabilities>();
+ /// ```
+ ///
+ pub fn new<Capabilities>() -> Self
+ where
+ Capabilities: WithContext<A, Ef>,
+ {
+ let (request_sender, request_receiver) = capability::channel();
+ let (event_sender, event_receiver) = capability::channel();
+ let (executor, spawner) = capability::executor_and_spawner();
+ let capability_context = ProtoContext::new(request_sender, event_sender, spawner);
+
+ Self {
+ model: Default::default(),
+ executor,
+ app: Default::default(),
+ capabilities: Capabilities::new_with_context(capability_context),
+ requests: request_receiver,
+ capability_events: event_receiver,
+ }
+ }
+
+ /// Run the app's `update` function with a given `event`, returning a vector of
+ /// effect requests.
+ pub fn process_event(&self, event: A::Event) -> Vec<Ef> {
+ let mut model = self.model.write().expect("Model RwLock was poisoned.");
+
+ self.app.update(event, &mut model, &self.capabilities);
+
+ self.process()
+ }
+
+ /// Resolve an effect `request` for operation `Op` with the corresponding result.
+ ///
+ /// Note that the `request` is borrowed mutably. When a request that is expected to
+ /// only be resolved once is passed in, it will be consumed and changed to a request
+ /// which can no longer be resolved.
+ pub fn resolve<Op>(&self, request: &mut Request<Op>, result: Op::Output) -> Vec<Ef>
+ where
+ Op: Operation,
+ {
+ let resolve_result = request.resolve(result);
+ debug_assert!(resolve_result.is_ok());
+
+ self.process()
+ }
+
+ pub(crate) fn process(&self) -> Vec<Ef> {
+ self.executor.run_all();
+
+ while let Some(capability_event) = self.capability_events.receive() {
+ let mut model = self.model.write().expect("Model RwLock was poisoned.");
+ self.app
+ .update(capability_event, &mut model, &self.capabilities);
+ drop(model);
+ self.executor.run_all();
+ }
+
+ self.requests.drain().collect()
+ }
+
+ /// Get the current state of the app's view model.
+ pub fn view(&self) -> A::ViewModel {
+ let model = self.model.read().expect("Model RwLock was poisoned.");
+
+ self.app.view(&model)
+ }
+}
+
+impl<Ef, A> Default for Core<Ef, A>
+where
+ Ef: Effect,
+ A: App,
+ A::Capabilities: WithContext<A, Ef>,
+{
+ fn default() -> Self {
+ Self::new::<A::Capabilities>()
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +
use std::fmt::{self, Debug};
+
+use crate::{
+ capability::Operation,
+ core::resolve::{Resolve, ResolveError},
+};
+
+/// Request represents an effect request from the core to the shell.
+///
+/// The `operation` is the input needed to process the effect, and will be one
+/// of the capabilities' [`Operation`] types.
+///
+/// The request can be resolved by passing it to `Core::resolve` along with the
+/// corresponding result of type `Operation::Output`.
+pub struct Request<Op>
+where
+ Op: Operation,
+{
+ pub operation: Op,
+ pub(crate) resolve: Resolve<Op::Output>,
+}
+
+impl<Op> Request<Op>
+where
+ Op: Operation,
+{
+ pub(crate) fn resolves_never(operation: Op) -> Self {
+ Self {
+ operation,
+ resolve: Resolve::Never,
+ }
+ }
+
+ pub(crate) fn resolves_once<F>(operation: Op, resolve: F) -> Self
+ where
+ F: FnOnce(Op::Output) + Send + 'static,
+ {
+ Self {
+ operation,
+ resolve: Resolve::Once(Box::new(resolve)),
+ }
+ }
+
+ pub(crate) fn resolves_many_times<F>(operation: Op, resolve: F) -> Self
+ where
+ F: Fn(Op::Output) -> Result<(), ()> + Send + 'static,
+ {
+ Self {
+ operation,
+ resolve: Resolve::Many(Box::new(resolve)),
+ }
+ }
+
+ pub(crate) fn resolve(&mut self, output: Op::Output) -> Result<(), ResolveError> {
+ self.resolve.resolve(output)
+ }
+}
+
+impl<Op> fmt::Debug for Request<Op>
+where
+ Op: Operation + Debug,
+{
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_tuple("Request").field(&self.operation).finish()
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +
use thiserror::Error;
+
+type ResolveOnce<Out> = Box<dyn FnOnce(Out) + Send>;
+type ResolveMany<Out> = Box<dyn Fn(Out) -> Result<(), ()> + Send>;
+
+/// Resolve is a callback used to resolve an effect request and continue
+/// one of the capability Tasks running on the executor.
+pub(crate) enum Resolve<Out> {
+ Never,
+ Once(ResolveOnce<Out>),
+ Many(ResolveMany<Out>),
+}
+
+impl<Out> Resolve<Out> {
+ pub fn resolve(&mut self, output: Out) -> Result<(), ResolveError> {
+ match self {
+ Resolve::Never => Err(ResolveError::Never),
+ Resolve::Many(f) => f(output).map_err(|_| ResolveError::FinishedMany),
+ Resolve::Once(_) => {
+ // The resolve has been used, turn it into a Never
+ if let Resolve::Once(f) = std::mem::replace(self, Resolve::Never) {
+ f(output);
+ }
+
+ Ok(())
+ }
+ }
+ }
+}
+
+#[derive(Error, Debug)]
+pub enum ResolveError {
+ #[error("Attempted to resolve a request that is not expected to be resolved.")]
+ Never,
+ #[error("Attempted to resolve a request that has concluded.")]
+ FinishedMany,
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +
//! Cross-platform app development in Rust
+//!
+//! Crux helps you share your app's business logic and behavior across mobile (iOS and Android) and web,
+//! as a single, reusable core built with Rust.
+//!
+//! Unlike React Native, the user interface layer is built natively, with modern declarative UI frameworks
+//! such as Swift UI, Jetpack Compose and React/Vue or a WASM based framework on the web.
+//!
+//! The UI layer is as thin as it can be, and all other work is done by the shared core.
+//! The interface with the core has static type checking across languages.
+//!
+//! ## Getting Started
+//!
+//! Crux applications are split into two parts: a Core written in Rust and a Shell written in the platform
+//! native language (e.g. Swift or Kotlin). It is also possible to use Crux from Rust shells.
+//! The Core architecture is based on [Elm architecture](https://guide.elm-lang.org/architecture/).
+//!
+//! Quick glossary of terms to help you follow the example:
+//!
+//! * Core - the shared core written in Rust
+//!
+//! * Shell - the native side of the app on each platform handling UI and executing side effects
+//!
+//! * App - the main module of the core containing the application logic, especially model changes
+//! and side-effects triggered by events. App can be composed from modules, each resembling a smaller, simpler app.
+//!
+//! * Event - main input for the core, typically triggered by user interaction in the UI
+//!
+//! * Model - data structure (typically tree-like) holding the entire application state
+//!
+//! * View model - data structure describing the current state of the user interface
+//!
+//! * Effect - A side-effect the core can request from the shell. This is typically a form of I/O or similar
+//! interaction with the host platform. Updating the UI is considered an effect.
+//!
+//! * Capability - A user-friendly API used to request effects and provide events that should be dispatched
+//! when the effect is completed. For example, a HTTP client is a capability.
+//!
+//! Below is a minimal example of a Crux-based application Core:
+//!
+//! ```rust
+//!// src/app.rs
+//!use crux_core::{render::Render, App};
+//!use crux_macros::Effect;
+//!use serde::{Deserialize, Serialize};
+//!
+//!// Model describing the application state
+//!#[derive(Default)]
+//!struct Model {
+//! count: isize,
+//!}
+//!
+//!// Event describing the actions that can be taken
+//!#[derive(Serialize, Deserialize)]
+//!pub enum Event {
+//! Increment,
+//! Decrement,
+//! Reset,
+//!}
+//!
+//!// Capabilities listing the side effects the Core
+//!// will use to request side effects from the Shell
+//!#[cfg_attr(feature = "typegen", derive(crux_macros::Export))]
+//!#[derive(Effect)]
+//!#[effect(app = "Hello")]
+//!pub struct Capabilities {
+//! pub render: Render<Event>,
+//!}
+//!
+//!#[derive(Default)]
+//!struct Hello;
+//!
+//!impl App for Hello {
+//! // Use the above Event
+//! type Event = Event;
+//! // Use the above Model
+//! type Model = Model;
+//! type ViewModel = String;
+//! // Use the above Capabilities
+//! type Capabilities = Capabilities;
+//!
+//! fn update(&self, event: Event, model: &mut Model, caps: &Capabilities) {
+//! match event {
+//! Event::Increment => model.count += 1,
+//! Event::Decrement => model.count -= 1,
+//! Event::Reset => model.count = 0,
+//! };
+//!
+//! // Request a UI update
+//! caps.render.render()
+//! }
+//!
+//! fn view(&self, model: &Model) -> Self::ViewModel {
+//! format!("Count is: {}", model.count)
+//! }
+//!}
+//! ```
+//!
+//! ## Integrating with a Shell
+//!
+//! To use the application in a user interface shell, you need to expose the core interface for FFI.
+//! This "plumbing" will likely be simplified with macros in the future versions of Crux.
+//!
+//! ```rust,ignore
+//! // src/lib.rs
+//! pub mod app;
+//!
+//! use lazy_static::lazy_static;
+//! use wasm_bindgen::prelude::wasm_bindgen;
+//!
+//! pub use crux_core::bridge::{Bridge, Request};
+//! pub use crux_core::Core;
+//! pub use crux_http as http;
+//!
+//! pub use app::*;
+//!
+//! uniffi_macros::include_scaffolding!("hello");
+//!
+//! lazy_static! {
+//! static ref CORE: Bridge<Effect, App> = Bridge::new(Core::new::<Capabilities>());
+//! }
+//!
+//! #[wasm_bindgen]
+//! pub fn process_event(data: &[u8]) -> Vec<u8> {
+//! CORE.process_event(data)
+//! }
+//!
+//! #[wasm_bindgen]
+//! pub fn handle_response(uuid: &[u8], data: &[u8]) -> Vec<u8> {
+//! CORE.handle_response(uuid, data)
+//! }
+//!
+//! #[wasm_bindgen]
+//! pub fn view() -> Vec<u8> {
+//! CORE.view()
+//! }
+//! ```
+//!
+//! You will also need a `hello.udl` file describing the foreign function interface:
+//!
+//! ```ignore
+//! // src/hello.udl
+//! namespace hello {
+//! sequence<u8> process_event([ByRef] sequence<u8> msg);
+//! sequence<u8> handle_response([ByRef] sequence<u8> res);
+//! sequence<u8> view();
+//! };
+//! ```
+//!
+//! Finally, you will need to set up the type generation for the `Model`, `Message` and `ViewModel` types.
+//! See [typegen] for details.
+//!
+
+pub mod bridge;
+pub mod capability;
+pub mod testing;
+#[cfg(feature = "typegen")]
+pub mod typegen;
+
+mod capabilities;
+mod core;
+
+use serde::Serialize;
+
+pub use self::{
+ capabilities::*,
+ capability::{Capability, WithContext},
+ core::{Core, Effect, Request},
+};
+
+/// Implement [`App`] on your type to make it into a Crux app. Use your type implementing [`App`]
+/// as the type argument to [`Core`] or [`Bridge`](bridge::Bridge).
+pub trait App: Default {
+ /// Event, typically an `enum`, defines the actions that can be taken to update the application state.
+ type Event: Send + 'static;
+ /// Model, typically a `struct` defines the internal state of the application
+ type Model: Default;
+ /// ViewModel, typically a `struct` describes the user interface that should be
+ /// displayed to the user
+ type ViewModel: Serialize;
+ /// Capabilities, typically a `struct`, lists the capabilities used by this application
+ /// Typically, Capabilities should contain at least an instance of the built-in [`Render`](crate::render::Render) capability.
+ type Capabilities;
+
+ /// Update method defines the transition from one `model` state to another in response to an `event`.
+ ///
+ /// Update function can mutate the `model` and use the capabilities provided by the `caps` argument
+ /// to instruct the shell to perform side-effects. The side-effects will run concurrently (capability
+ /// calls behave the same as go routines in Go or Promises in JavaScript). Capability calls
+ /// don't return anything, but may take a `callback` event which should be dispatched when the
+ /// effect completes.
+ ///
+ /// Typically, `update` should call at least [`Render::render`](crate::render::Render::render).
+ fn update(&self, event: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities);
+
+ /// View method is used by the Shell to request the current state of the user interface
+ fn view(&self, model: &Self::Model) -> Self::ViewModel;
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +
//! Testing support for unit testing Crux apps.
+use std::rc::Rc;
+
+use anyhow::Result;
+
+use crate::{
+ capability::{
+ channel::Receiver, executor_and_spawner, Operation, ProtoContext, QueuingExecutor,
+ },
+ Request, WithContext,
+};
+
+/// AppTester is a simplified execution environment for Crux apps for use in
+/// tests.
+///
+/// Create an instance of `AppTester` with your `App` and an `Effect` type
+/// using [`AppTester::default`].
+///
+/// for example:
+///
+/// ```rust,ignore
+/// let app = AppTester::<ExampleApp, ExampleEffect>::default();
+/// ```
+pub struct AppTester<App, Ef>
+where
+ App: crate::App,
+{
+ app: App,
+ capabilities: App::Capabilities,
+ context: Rc<AppContext<Ef, App::Event>>,
+}
+
+struct AppContext<Ef, Ev> {
+ commands: Receiver<Ef>,
+ events: Receiver<Ev>,
+ executor: QueuingExecutor,
+}
+
+impl<App, Ef> AppTester<App, Ef>
+where
+ App: crate::App,
+{
+ /// Run the app's `update` function with an event and a model state
+ ///
+ /// You can use the resulting [`Update`] to inspect the effects which were requested
+ /// and potential further events dispatched by capabilities.
+ pub fn update(&self, event: App::Event, model: &mut App::Model) -> Update<Ef, App::Event> {
+ self.app.update(event, model, &self.capabilities);
+ self.context.updates()
+ }
+
+ /// Resolve an effect `request` from previous update with an operation output.
+ ///
+ /// This potentially runs the app's `update` function if the effect is completed, and
+ /// produce another `Update`.
+ pub fn resolve<Op: Operation>(
+ &self,
+ request: &mut Request<Op>,
+ value: Op::Output,
+ ) -> Result<Update<Ef, App::Event>> {
+ request.resolve(value)?;
+
+ Ok(self.context.updates())
+ }
+
+ /// Run the app's `view` function with a model state
+ pub fn view(&self, model: &App::Model) -> App::ViewModel {
+ self.app.view(model)
+ }
+}
+
+impl<App, Ef> Default for AppTester<App, Ef>
+where
+ App: crate::App,
+ App::Capabilities: WithContext<App, Ef>,
+ App::Event: Send,
+ Ef: Send + 'static,
+{
+ fn default() -> Self {
+ let (command_sender, commands) = crate::capability::channel();
+ let (event_sender, events) = crate::capability::channel();
+ let (executor, spawner) = executor_and_spawner();
+ let capability_context = ProtoContext::new(command_sender, event_sender, spawner);
+
+ Self {
+ app: App::default(),
+ capabilities: App::Capabilities::new_with_context(capability_context),
+ context: Rc::new(AppContext {
+ commands,
+ events,
+ executor,
+ }),
+ }
+ }
+}
+
+impl<App, Ef> AsRef<App::Capabilities> for AppTester<App, Ef>
+where
+ App: crate::App,
+{
+ fn as_ref(&self) -> &App::Capabilities {
+ &self.capabilities
+ }
+}
+
+impl<Ef, Ev> AppContext<Ef, Ev> {
+ pub fn updates(self: &Rc<Self>) -> Update<Ef, Ev> {
+ self.executor.run_all();
+ let effects = self.commands.drain().collect();
+ let events = self.events.drain().collect();
+
+ Update { effects, events }
+ }
+}
+
+/// Update test helper holds the result of running an app update using [`AppTester::update`]
+/// or resolving a request with [`AppTester::resolve`].
+#[derive(Debug)]
+pub struct Update<Ef, Ev> {
+ /// Effects requested from the update run
+ pub effects: Vec<Ef>,
+ /// Events dispatched from the update run
+ pub events: Vec<Ev>,
+}
+
+impl<Ef, Ev> Update<Ef, Ev> {
+ pub fn into_effects(self) -> impl Iterator<Item = Ef> {
+ self.effects.into_iter()
+ }
+
+ pub fn effects(&self) -> impl Iterator<Item = &Ef> {
+ self.effects.iter()
+ }
+
+ pub fn effects_mut(&mut self) -> impl Iterator<Item = &mut Ef> {
+ self.effects.iter_mut()
+ }
+}
+
+/// Panics if the pattern doesn't match an `Effect` from the specified `Update`
+///
+/// Like in a `match` expression, the pattern can be optionally followed by `if`
+/// and a guard expression that has access to names bound by the pattern.
+///
+/// # Example
+///
+/// ```
+/// # use crux_core::testing::Update;
+/// # enum Effect { Render(String) };
+/// # enum Event { None };
+/// # let effects = vec![Effect::Render("test".to_string())].into_iter().collect();
+/// # let mut update = Update { effects, events: vec!(Event::None) };
+/// use crux_core::assert_effect;
+/// assert_effect!(update, Effect::Render(_));
+/// ```
+#[macro_export]
+macro_rules! assert_effect {
+ ($expression:expr, $(|)? $( $pattern:pat_param )|+ $( if $guard: expr )? $(,)?) => {
+ assert!($expression.effects().any(|e| matches!(e, $( $pattern )|+ $( if $guard )?)));
+ };
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +
//! Generation of foreign language types (currently Swift, Java, TypeScript) for Crux
+//!
+//! In order to use this module, you'll need a separate crate from your shared library, possibly
+//! called `shared_types`. This is necessary because we need to reference types from your shared library
+//! during the build process (`build.rs`).
+//!
+//! This module is behind the feature called `typegen`, and is not compiled into the default crate.
+//!
+//! Ensure that you have the following line in the `Cargo.toml` of your `shared_types` library.
+//!
+//! ```rust,ignore
+//! [build-dependencies]
+//! crux_core = { version = "0.6", features = ["typegen"] }
+//! ```
+//!
+//! * Your `shared_types` library, will have an empty `lib.rs`, since we only use it for generating foreign language type declarations.
+//! * Create a `build.rs` in your `shared_types` library, that looks something like this:
+//!
+//! ```rust
+//! # mod shared {
+//! # use crux_core::render::Render;
+//! # use crux_macros::Effect;
+//! # use serde::{Deserialize, Serialize};
+//! # #[derive(Default)]
+//! # pub struct App;
+//! # #[derive(Serialize, Deserialize)]
+//! # pub enum Event {
+//! # None,
+//! # SendUuid(uuid::Uuid),
+//! # }
+//! # #[derive(Serialize, Deserialize)]
+//! # pub struct ViewModel;
+//! # impl crux_core::App for App {
+//! # type Event = Event;
+//! # type Model = ();
+//! # type ViewModel = ViewModel;
+//! # type Capabilities = Capabilities;
+//! # fn update(&self, _event: Event, _model: &mut Self::Model, _caps: &Capabilities) {}
+//! # fn view(&self, _model: &Self::Model) -> Self::ViewModel {
+//! # todo!();
+//! # }
+//! # }
+//! # #[derive(Effect)]
+//! # pub struct Capabilities {
+//! # pub render: Render<Event>,
+//! # }
+//! # }
+//!use shared::{App, EffectFfi, Event};
+//!use crux_core::{bridge::Request, typegen::TypeGen};
+//!use uuid::Uuid;
+//!
+//!#[test]
+//!fn generate_types() {
+//! let mut gen = TypeGen::new();
+//!
+//! let sample_events = vec![Event::SendUuid(Uuid::new_v4())];
+//! gen.register_type_with_samples(sample_events).unwrap();
+//!
+//! gen.register_app::<App>().unwrap();
+//!
+//! let temp = assert_fs::TempDir::new().unwrap();
+//! let output_root = temp.join("crux_core_typegen_test");
+//!
+//! gen.swift("SharedTypes", output_root.join("swift"))
+//! .expect("swift type gen failed");
+//!
+//! gen.java("com.example.counter.shared_types", output_root.join("java"))
+//! .expect("java type gen failed");
+//!
+//! gen.typescript("shared_types", output_root.join("typescript"))
+//! .expect("typescript type gen failed");
+//!}
+//! ```
+
+use serde::Deserialize;
+use serde_generate::{java, swift, typescript, Encoding, SourceInstaller};
+use serde_reflection::{Registry, Tracer, TracerConfig};
+use std::{
+ fs::{self, File},
+ io::Write,
+ mem,
+ path::{Path, PathBuf},
+};
+use thiserror::Error;
+
+// Expose from `serde_reflection` for `register_type_with_samples()`
+use serde_reflection::Samples;
+
+use crate::App;
+
+pub type Result = std::result::Result<(), TypeGenError>;
+
+static DESERIALIZATION_ERROR_HINT: &str = r#"
+This might be because you attempted to pass types with custom serialization across the FFI boundary. Make sure that:
+1. Types you use in Event, ViewModel and Capabilities serialize as a container, otherwise wrap them in a new type struct,
+ e.g. MyUuid(uuid::Uuid)
+2. Sample values of such types have been provided to the type generator using TypeGen::register_samples, before any type registration."#;
+
+#[derive(Error, Debug)]
+pub enum TypeGenError {
+ #[error("type tracing failed {0}")]
+ TypeTracing(String),
+ #[error("value tracing failed {0}")]
+ ValueTracing(String),
+ #[error("type tracing failed: {0} {}", DESERIALIZATION_ERROR_HINT)]
+ Deserialization(String),
+ #[error("code has been generated, too late to register types")]
+ LateRegistration,
+ #[error("type generation failed: {0}")]
+ Generation(String),
+ #[error("error writing generated types")]
+ Io(#[from] std::io::Error),
+}
+
+#[derive(Debug)]
+pub enum State {
+ Registering(Tracer, Samples),
+ Generating(Registry),
+}
+
+pub trait Export {
+ fn register_types(generator: &mut TypeGen) -> Result;
+}
+
+/// The `TypeGen` struct stores the registered types so that they can be generated for foreign languages
+/// use `TypeGen::new()` to create an instance
+pub struct TypeGen {
+ pub state: State,
+}
+
+impl Default for TypeGen {
+ fn default() -> Self {
+ TypeGen {
+ state: State::Registering(Tracer::new(TracerConfig::default()), Samples::new()),
+ }
+ }
+}
+
+impl TypeGen {
+ /// Creates an instance of the `TypeGen` struct
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ /// Register all the types used in app `A` to be shared with the Shell.
+ ///
+ /// Do this before calling TypeGen::swift, TypeGen::java or TypeGen::typescript.
+ /// This method would normally be called in a build.rs file of a sister crate responsible for
+ /// creating "foreign language" type definitions for the FFI boundary.
+ /// See the section on
+ /// [creating the shared types crate](https://redbadger.github.io/crux/getting_started/core.html#create-the-shared-types-crate)
+ /// in the Crux book for more information.
+ pub fn register_app<A: App>(&mut self) -> Result
+ where
+ A::Capabilities: Export,
+ A::Event: Deserialize<'static>,
+ A::ViewModel: Deserialize<'static> + 'static,
+ {
+ self.register_type::<A::Event>()?;
+ self.register_type::<A::ViewModel>()?;
+
+ A::Capabilities::register_types(self)?;
+
+ Ok(())
+ }
+
+ /// Register sample values for types with custom serialization. This is necessary
+ /// because the type registration relies on Serde to understand the structure of the types,
+ /// and as part of the process runs a faux deserialization on each of them, with a best
+ /// guess of a default value. If that default value does not deserialize, the type registration
+ /// will fail.
+ /// You can prevent this problem by registering a valid sample value (or values),
+ /// which the deserialization will use instead.
+ pub fn register_samples<'de, T>(&mut self, sample_data: Vec<T>) -> Result
+ where
+ T: serde::Deserialize<'de> + serde::Serialize,
+ {
+ match &mut self.state {
+ State::Registering(tracer, samples) => {
+ for sample in &sample_data {
+ match tracer.trace_value::<T>(samples, sample) {
+ Ok(_) => {}
+ Err(e) => return Err(TypeGenError::ValueTracing(e.explanation())),
+ }
+ }
+ Ok(())
+ }
+ _ => Err(TypeGenError::LateRegistration),
+ }
+ }
+ /// For each of the types that you want to share with the Shell, call this method:
+ /// e.g.
+ /// ```rust
+ /// # use crux_core::typegen::TypeGen;
+ /// # use serde::{Serialize, Deserialize};
+ /// # use anyhow::Error;
+ /// #[derive(Serialize, Deserialize)]
+ /// enum MyNestedEnum { None }
+ /// #[derive(Serialize, Deserialize)]
+ /// enum MyEnum { None, Nested(MyNestedEnum) }
+ /// fn register() -> Result<(), Error> {
+ /// let mut gen = TypeGen::new();
+ /// gen.register_type::<MyEnum>()?;
+ /// gen.register_type::<MyNestedEnum>()?;
+ /// Ok(())
+ /// }
+ /// ```
+ pub fn register_type<'de, T>(&mut self) -> Result
+ where
+ T: serde::Deserialize<'de>,
+ {
+ match &mut self.state {
+ State::Registering(tracer, _) => match tracer.trace_simple_type::<T>() {
+ Ok(_) => Ok(()),
+ Err(e @ serde_reflection::Error::DeserializationError(_)) => {
+ Err(TypeGenError::Deserialization(format!(
+ "{}: {}",
+ e.to_string(),
+ e.explanation()
+ )))
+ }
+ Err(e) => Err(TypeGenError::TypeTracing(format!(
+ "{}: {}",
+ e.to_string(),
+ e.explanation()
+ ))),
+ },
+ _ => Err(TypeGenError::LateRegistration),
+ }
+ }
+
+ /// Usually, the simple `register_type()` method can generate the types you need.
+ /// Sometimes, though, you need to provide samples of your type. The `Uuid` type,
+ /// for example, requires a sample struct to help the typegen system understand
+ /// what it looks like. Use this method to provide samples when you register a
+ /// type.
+ ///
+ /// For each of the types that you want to share with the Shell, call this method,
+ /// providing samples of the type:
+ /// e.g.
+ /// ```rust
+ /// # use crux_core::typegen::TypeGen;
+ /// # use uuid::Uuid;
+ /// # use serde::{Serialize, Deserialize};
+ /// # use anyhow::Error;
+ /// # #[derive(Serialize, Deserialize, Debug)]
+ /// # struct MyUuid(Uuid);
+ /// # fn register() -> Result<(), Error> {
+ /// # let mut gen = TypeGen::new();
+ /// let sample_data = vec![MyUuid(Uuid::new_v4())];
+ /// gen.register_type_with_samples::<MyUuid>(sample_data)?;
+ /// # Ok(())
+ /// # }
+ /// ```
+ ///
+ /// Note: Because of the way that enums are handled by `serde_reflection`,
+ /// you may need to ensure that enums provided as samples have a first variant
+ /// that does not use custom deserialization.
+ pub fn register_type_with_samples<'de, T>(&'de mut self, sample_data: Vec<T>) -> Result
+ where
+ T: serde::Deserialize<'de> + serde::Serialize,
+ {
+ match &mut self.state {
+ State::Registering(tracer, samples) => {
+ for sample in &sample_data {
+ match tracer.trace_value::<T>(samples, sample) {
+ Ok(_) => {}
+ Err(e @ serde_reflection::Error::DeserializationError(_)) => {
+ return Err(TypeGenError::ValueTracing(format!(
+ "{}: {}",
+ e.to_string(),
+ e.explanation()
+ )))
+ }
+ Err(e) => {
+ return Err(TypeGenError::ValueTracing(format!(
+ "{}: {}",
+ e.to_string(),
+ e.explanation()
+ )))
+ }
+ }
+ }
+
+ match tracer.trace_type::<T>(samples) {
+ Ok(_) => Ok(()),
+ Err(e @ serde_reflection::Error::DeserializationError(_)) => {
+ Err(TypeGenError::Deserialization(format!(
+ "{}: {}",
+ e.to_string(),
+ e.explanation()
+ )))
+ }
+ Err(e) => Err(TypeGenError::TypeTracing(format!(
+ "{}: {}",
+ e.to_string(),
+ e.explanation()
+ ))),
+ }
+ }
+ _ => Err(TypeGenError::LateRegistration),
+ }
+ }
+
+ /// Generates types for Swift
+ /// e.g.
+ /// ```rust
+ /// # use crux_core::typegen::TypeGen;
+ /// # use std::env::temp_dir;
+ /// # let mut gen = TypeGen::new();
+ /// # let output_root = temp_dir().join("crux_core_typegen_doctest");
+ /// gen.swift("SharedTypes", output_root.join("swift"))
+ /// .expect("swift type gen failed");
+ /// ```
+ pub fn swift(&mut self, module_name: &str, path: impl AsRef<Path>) -> Result {
+ self.ensure_registry()?;
+
+ let path = path.as_ref().join(module_name);
+
+ fs::create_dir_all(&path)?;
+
+ let installer = swift::Installer::new(path.clone());
+ installer
+ .install_serde_runtime()
+ .map_err(|e| TypeGenError::Generation(e.to_string()))?;
+ installer
+ .install_bincode_runtime()
+ .map_err(|e| TypeGenError::Generation(e.to_string()))?;
+
+ let registry = match &self.state {
+ State::Generating(registry) => registry,
+ _ => panic!("registry creation failed"),
+ };
+
+ let config = serde_generate::CodeGeneratorConfig::new(module_name.to_string())
+ .with_encodings(vec![Encoding::Bincode]);
+
+ installer
+ .install_module(&config, registry)
+ .map_err(|e| TypeGenError::Generation(e.to_string()))?;
+
+ // add bincode deserialization for Vec<Request>
+ let mut output = File::create(
+ path.join("Sources")
+ .join(module_name)
+ .join("Requests.swift"),
+ )?;
+ write!(
+ output,
+ "{}",
+ include_str!("../typegen_extensions/swift/requests.swift")
+ )?;
+
+ // wrap it all up in a swift package
+ let mut output = File::create(path.join("Package.swift"))?;
+ write!(
+ output,
+ "{}",
+ include_str!("../typegen_extensions/swift/Package.swift")
+ .replace("SharedTypes", module_name)
+ )?;
+
+ Ok(())
+ }
+
+ /// Generates types for Java (for use with Kotlin)
+ /// e.g.
+ /// ```rust
+ /// # use crux_core::typegen::TypeGen;
+ /// # use std::env::temp_dir;
+ /// # let mut gen = TypeGen::new();
+ /// # let output_root = temp_dir().join("crux_core_typegen_doctest");
+ /// gen.java(
+ /// "com.redbadger.crux_core.shared_types",
+ /// output_root.join("java"),
+ /// )
+ /// .expect("java type gen failed");
+ /// ```
+ pub fn java(&mut self, package_name: &str, path: impl AsRef<Path>) -> Result {
+ self.ensure_registry()?;
+
+ fs::create_dir_all(&path)?;
+
+ let package_path = package_name.replace('.', "/");
+
+ // remove any existing generated shared types, this ensures that we remove no longer used types
+ fs::remove_dir_all(path.as_ref().join(&package_path)).unwrap_or(());
+
+ let config = serde_generate::CodeGeneratorConfig::new(package_name.to_string())
+ .with_encodings(vec![Encoding::Bincode]);
+
+ let installer = java::Installer::new(path.as_ref().to_path_buf());
+ installer
+ .install_serde_runtime()
+ .map_err(|e| TypeGenError::Generation(e.to_string()))?;
+ installer
+ .install_bincode_runtime()
+ .map_err(|e| TypeGenError::Generation(e.to_string()))?;
+
+ let registry = match &self.state {
+ State::Generating(registry) => registry,
+ _ => panic!("registry creation failed"),
+ };
+
+ installer
+ .install_module(&config, registry)
+ .map_err(|e| TypeGenError::Generation(e.to_string()))?;
+
+ let requests = format!(
+ "package {package_name};\n\n{}",
+ include_str!("../typegen_extensions/java/Requests.java")
+ );
+
+ fs::write(
+ path.as_ref()
+ .to_path_buf()
+ .join(package_path)
+ .join("Requests.java"),
+ requests,
+ )?;
+
+ Ok(())
+ }
+
+ /// Generates types for TypeScript
+ /// e.g.
+ /// ```rust
+ /// # use crux_core::typegen::TypeGen;
+ /// # use std::env::temp_dir;
+ /// # let mut gen = TypeGen::new();
+ /// # let output_root = temp_dir().join("crux_core_typegen_doctest");
+ /// gen.typescript("shared_types", output_root.join("typescript"))
+ /// .expect("typescript type gen failed");
+ /// ```
+ pub fn typescript(&mut self, module_name: &str, path: impl AsRef<Path>) -> Result {
+ self.ensure_registry()?;
+
+ fs::create_dir_all(&path)?;
+ let output_dir = path.as_ref().to_path_buf();
+
+ let installer = typescript::Installer::new(output_dir.clone());
+ installer
+ .install_serde_runtime()
+ .map_err(|e| TypeGenError::Generation(e.to_string()))?;
+ installer
+ .install_bincode_runtime()
+ .map_err(|e| TypeGenError::Generation(e.to_string()))?;
+
+ let extensions_dir =
+ PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("typegen_extensions/typescript");
+ copy(extensions_dir, path).expect("Could not copy TS runtime");
+
+ let registry = match &self.state {
+ State::Generating(registry) => registry,
+ _ => panic!("registry creation failed"),
+ };
+
+ let config = serde_generate::CodeGeneratorConfig::new(module_name.to_string())
+ .with_encodings(vec![Encoding::Bincode]);
+
+ let generator = serde_generate::typescript::CodeGenerator::new(&config);
+ let mut source = Vec::new();
+ generator.output(&mut source, registry)?;
+
+ // FIXME fix import paths in generated code which assume running on Deno
+ let out = String::from_utf8_lossy(&source)
+ .replace(
+ "import { BcsSerializer, BcsDeserializer } from '../bcs/mod.ts';",
+ "",
+ )
+ .replace(".ts'", "'");
+
+ let types_dir = output_dir.join("types");
+ fs::create_dir_all(&types_dir)?;
+
+ let mut output = File::create(types_dir.join(format!("{module_name}.ts")))?;
+ write!(output, "{out}")?;
+
+ // Install dependencies
+ std::process::Command::new("pnpm")
+ .current_dir(output_dir.clone())
+ .arg("install")
+ .status()
+ .expect("Could not pnpm install");
+
+ // Build TS code and emit declarations
+ std::process::Command::new("pnpm")
+ .current_dir(output_dir)
+ .arg("exec")
+ .arg("tsc")
+ .arg("--build")
+ .status()
+ .expect("Could tsc --build");
+
+ Ok(())
+ }
+
+ fn ensure_registry(&mut self) -> Result {
+ if let State::Registering(_, _) = self.state {
+ // replace the current state with a dummy tracer
+ let old_state = mem::replace(
+ &mut self.state,
+ State::Registering(Tracer::new(TracerConfig::default()), Samples::new()),
+ );
+
+ // convert tracer to registry
+ if let State::Registering(tracer, _) = old_state {
+ // replace dummy with registry
+ self.state = State::Generating(
+ tracer
+ .registry()
+ .map_err(|e| TypeGenError::Generation(e.explanation()))?,
+ );
+ }
+ }
+ Ok(())
+ }
+}
+
+fn copy(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result {
+ fs::create_dir_all(to.as_ref())?;
+
+ let entries = fs::read_dir(from)?;
+ for entry in entries {
+ let entry = entry?;
+
+ let to = to.as_ref().to_path_buf().join(entry.file_name());
+ if entry.file_type()?.is_dir() {
+ copy(entry.path(), to)?;
+ } else {
+ fs::copy(entry.path(), to)?;
+ };
+ }
+
+ Ok(())
+}
+
+#[cfg(feature = "typegen")]
+#[cfg(test)]
+mod tests {
+ use crate::typegen::TypeGen;
+ use serde::{Deserialize, Serialize};
+ use uuid::Uuid;
+
+ #[derive(Serialize, Deserialize, Debug)]
+ struct MyUuid(Uuid);
+
+ #[test]
+ fn test_typegen_for_uuid_without_samples() {
+ let mut gen = TypeGen::new();
+ let result = gen.register_type::<MyUuid>();
+
+ assert!(
+ result.is_err(),
+ "typegen unexpectedly succeeded for Uuid, without samples"
+ )
+ }
+
+ #[test]
+ fn test_typegen_for_uuid_with_samples() {
+ let sample_data = vec![MyUuid(Uuid::new_v4())];
+ let mut gen = TypeGen::new();
+ let result = gen.register_type_with_samples(sample_data);
+ dbg!(&result);
+ assert!(result.is_ok(), "typegen failed for Uuid, with samples");
+
+ let sample_data = vec!["a".to_string(), "b".to_string()];
+ let result = gen.register_type_with_samples(sample_data);
+ assert!(result.is_ok(), "typegen failed with second sample data set");
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +
use std::fmt;
+use std::sync::Arc;
+
+use crate::http::{Method, Url};
+use crate::middleware::{Middleware, Next};
+use crate::protocol::{EffectSender, ProtocolRequestBuilder};
+use crate::{Config, Request, RequestBuilder, ResponseAsync, Result};
+
+/// An HTTP client, capable of sending `Request`s
+///
+/// Users should only interact with this type from middlewares - normal crux code should
+/// make use of the `Http` capability type instead.
+///
+/// # Examples
+///
+/// ```no_run
+/// use futures_util::future::BoxFuture;
+/// use crux_http::middleware::{Next, Middleware};
+/// use crux_http::{client::Client, Request, RequestBuilder, ResponseAsync, Result};
+/// use std::time;
+/// use std::sync::Arc;
+///
+/// // Fetches an authorization token prior to making a request
+/// fn fetch_auth<'a>(mut req: Request, client: Client, next: Next<'a>) -> BoxFuture<'a, Result<ResponseAsync>> {
+/// Box::pin(async move {
+/// let auth_token = client.get("https://httpbin.org/get")
+/// .await?
+/// .body_string()
+/// .await?;
+/// req.append_header("Authorization", format!("Bearer {auth_token}"));
+/// next.run(req, client).await
+/// })
+/// }
+/// ```
+pub struct Client {
+ config: Config,
+ effect_sender: Arc<dyn EffectSender + Send + Sync>,
+ /// Holds the middleware stack.
+ ///
+ /// Note(Fishrock123): We do actually want this structure.
+ /// The outer Arc allows us to clone in .send() without cloning the array.
+ /// The Vec allows us to add middleware at runtime.
+ /// The inner Arc-s allow us to implement Clone without sharing the vector with the parent.
+ /// We don't use a Mutex around the Vec here because adding a middleware during execution should be an error.
+ #[allow(clippy::rc_buffer)]
+ middleware: Arc<Vec<Arc<dyn Middleware>>>,
+}
+
+impl Clone for Client {
+ /// Clones the Client.
+ ///
+ /// This copies the middleware stack from the original, but shares
+ /// the `HttpClient` and http client config of the original.
+ /// Note that individual middleware in the middleware stack are
+ /// still shared by reference.
+ fn clone(&self) -> Self {
+ Self {
+ config: self.config.clone(),
+ effect_sender: Arc::clone(&self.effect_sender),
+ middleware: Arc::new(self.middleware.iter().cloned().collect()),
+ }
+ }
+}
+
+impl fmt::Debug for Client {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "Client {{}}")
+ }
+}
+
+impl Client {
+ pub(crate) fn new<Sender>(sender: Sender) -> Self
+ where
+ Sender: EffectSender + Send + Sync + 'static,
+ {
+ Self {
+ config: Config::default(),
+ effect_sender: Arc::new(sender),
+ middleware: Arc::new(vec![]),
+ }
+ }
+
+ // This is currently dead code because there's no easy way to configure a client.
+ // TODO: fix that in some future PR
+ #[allow(dead_code)]
+ /// Push middleware onto the middleware stack.
+ ///
+ /// See the [middleware] submodule for more information on middleware.
+ ///
+ /// [middleware]: ../middleware/index.html
+ pub(crate) fn with(mut self, middleware: impl Middleware) -> Self {
+ let m = Arc::get_mut(&mut self.middleware)
+ .expect("Registering middleware is not possible after the Client has been used");
+ m.push(Arc::new(middleware));
+ self
+ }
+
+ /// Send a `Request` using this client.
+ pub async fn send(&self, req: impl Into<Request>) -> Result<ResponseAsync> {
+ let mut req: Request = req.into();
+ let middleware = self.middleware.clone();
+
+ let mw_stack = match req.take_middleware() {
+ Some(req_mw) => {
+ let mut mw = Vec::with_capacity(middleware.len() + req_mw.len());
+ mw.extend(middleware.iter().cloned());
+ mw.extend(req_mw);
+ Arc::new(mw)
+ }
+ None => middleware,
+ };
+
+ let next = Next::new(&mw_stack, &|req, client| {
+ Box::pin(async move {
+ let req = req.into_protocol_request().await.unwrap();
+ Ok(client.effect_sender.send(req).await.into())
+ })
+ });
+
+ let client = Self {
+ config: self.config.clone(),
+ effect_sender: Arc::clone(&self.effect_sender),
+ // Erase the middleware stack for the Client accessible from within middleware.
+ // This avoids gratuitous circular borrow & logic issues.
+ middleware: Arc::new(vec![]),
+ };
+
+ let res = next.run(req, client).await?;
+ Ok(ResponseAsync::new(res.into()))
+ }
+
+ /// Submit a `Request` and get the response body as bytes.
+ pub async fn recv_bytes(&self, req: impl Into<Request>) -> Result<Vec<u8>> {
+ let mut res = self.send(req.into()).await?;
+ res.body_bytes().await
+ }
+
+ /// Submit a `Request` and get the response body as a string.
+ pub async fn recv_string(&self, req: impl Into<Request>) -> Result<String> {
+ let mut res = self.send(req.into()).await?;
+ res.body_string().await
+ }
+
+ /// Submit a `Request` and decode the response body from json into a struct.
+ pub async fn recv_json<T: serde::de::DeserializeOwned>(
+ &self,
+ req: impl Into<Request>,
+ ) -> Result<T> {
+ let mut res = self.send(req.into()).await?;
+ res.body_json::<T>().await
+ }
+
+ /// Submit a `Request` and decode the response body from form encoding into a struct.
+ ///
+ /// # Errors
+ ///
+ /// Any I/O error encountered while reading the body is immediately returned
+ /// as an `Err`.
+ ///
+ /// If the body cannot be interpreted as valid json for the target type `T`,
+ /// an `Err` is returned.
+ pub async fn recv_form<T: serde::de::DeserializeOwned>(
+ &self,
+ req: impl Into<Request>,
+ ) -> Result<T> {
+ let mut res = self.send(req.into()).await?;
+ res.body_form::<T>().await
+ }
+
+ /// Perform an HTTP `GET` request using the `Client` connection.
+ ///
+ /// # Panics
+ ///
+ /// This will panic if a malformed URL is passed.
+ ///
+ /// # Errors
+ ///
+ /// Returns errors from the middleware, http backend, and network sockets.
+ pub fn get(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
+ RequestBuilder::new_for_middleware(Method::Get, self.url(uri), self.clone())
+ }
+
+ /// Perform an HTTP `HEAD` request using the `Client` connection.
+ ///
+ /// # Panics
+ ///
+ /// This will panic if a malformed URL is passed.
+ ///
+ /// # Errors
+ ///
+ /// Returns errors from the middleware, http backend, and network sockets.
+ pub fn head(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
+ RequestBuilder::new_for_middleware(Method::Head, self.url(uri), self.clone())
+ }
+
+ /// Perform an HTTP `POST` request using the `Client` connection.
+ ///
+ /// # Panics
+ ///
+ /// This will panic if a malformed URL is passed.
+ ///
+ /// # Errors
+ ///
+ /// Returns errors from the middleware, http backend, and network sockets.
+ pub fn post(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
+ RequestBuilder::new_for_middleware(Method::Post, self.url(uri), self.clone())
+ }
+
+ /// Perform an HTTP `PUT` request using the `Client` connection.
+ ///
+ /// # Panics
+ ///
+ /// This will panic if a malformed URL is passed.
+ ///
+ /// # Errors
+ ///
+ /// Returns errors from the middleware, http backend, and network sockets.
+ pub fn put(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
+ RequestBuilder::new_for_middleware(Method::Put, self.url(uri), self.clone())
+ }
+
+ /// Perform an HTTP `DELETE` request using the `Client` connection.
+ ///
+ /// # Panics
+ ///
+ /// This will panic if a malformed URL is passed.
+ ///
+ /// # Errors
+ ///
+ /// Returns errors from the middleware, http backend, and network sockets.
+ pub fn delete(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
+ RequestBuilder::new_for_middleware(Method::Delete, self.url(uri), self.clone())
+ }
+
+ /// Perform an HTTP `CONNECT` request using the `Client` connection.
+ ///
+ /// # Panics
+ ///
+ /// This will panic if a malformed URL is passed.
+ ///
+ /// # Errors
+ ///
+ /// Returns errors from the middleware, http backend, and network sockets.
+ pub fn connect(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
+ RequestBuilder::new_for_middleware(Method::Connect, self.url(uri), self.clone())
+ }
+
+ /// Perform an HTTP `OPTIONS` request using the `Client` connection.
+ ///
+ /// # Panics
+ ///
+ /// This will panic if a malformed URL is passed.
+ ///
+ /// # Errors
+ ///
+ /// Returns errors from the middleware, http backend, and network sockets.
+ pub fn options(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
+ RequestBuilder::new_for_middleware(Method::Options, self.url(uri), self.clone())
+ }
+
+ /// Perform an HTTP `TRACE` request using the `Client` connection.
+ ///
+ /// # Panics
+ ///
+ /// This will panic if a malformed URL is passed.
+ ///
+ /// # Errors
+ ///
+ /// Returns errors from the middleware, http backend, and network sockets.
+ pub fn trace(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
+ RequestBuilder::new_for_middleware(Method::Trace, self.url(uri), self.clone())
+ }
+
+ /// Perform an HTTP `PATCH` request using the `Client` connection.
+ ///
+ /// # Panics
+ ///
+ /// This will panic if a malformed URL is passed.
+ ///
+ /// # Errors
+ ///
+ /// Returns errors from the middleware, http backend, and network sockets.
+ pub fn patch(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
+ RequestBuilder::new_for_middleware(Method::Patch, self.url(uri), self.clone())
+ }
+
+ /// Perform a HTTP request with the given verb using the `Client` connection.
+ ///
+ /// # Panics
+ ///
+ /// This will panic if a malformed URL is passed.
+ ///
+ /// # Errors
+ ///
+ /// Returns errors from the middleware, http backend, and network sockets.
+ pub fn request(&self, verb: Method, uri: impl AsRef<str>) -> RequestBuilder<()> {
+ RequestBuilder::new_for_middleware(verb, self.url(uri), self.clone())
+ }
+
+ /// Get the current configuration.
+ pub fn config(&self) -> &Config {
+ &self.config
+ }
+
+ // private function to generate a url based on the base_path
+ fn url(&self, uri: impl AsRef<str>) -> Url {
+ match &self.config.base_url {
+ None => uri.as_ref().parse().unwrap(),
+ Some(base) => base.join(uri.as_ref()).unwrap(),
+ }
+ }
+}
+
+#[cfg(test)]
+mod client_tests {
+ use super::Client;
+ use crate::protocol::{HttpRequest, HttpResponse};
+ use crate::testing::FakeShell;
+
+ #[futures_test::test]
+ async fn an_http_get() {
+ let mut shell = FakeShell::default();
+ shell.provide_response(HttpResponse::ok().body("Hello World!").build());
+
+ let client = Client::new(shell.clone());
+
+ let mut response = client.get("https://example.com").await.unwrap();
+ assert_eq!(response.body_string().await.unwrap(), "Hello World!");
+
+ assert_eq!(
+ shell.take_requests_received(),
+ vec![HttpRequest::get("https://example.com/").build()]
+ )
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +
//! Configuration for `HttpClient`s.
+
+use std::{collections::HashMap, fmt::Debug};
+
+use http_types::headers::{HeaderName, HeaderValues, ToHeaderValues};
+
+use crate::http::Url;
+use crate::Result;
+
+/// Configuration for `crux_http::Http`s and their underlying HTTP client.
+#[non_exhaustive]
+#[derive(Clone, Debug, Default)]
+pub struct Config {
+ /// The base URL for a client. All request URLs will be relative to this URL.
+ ///
+ /// Note: a trailing slash is significant.
+ /// Without it, the last path component is considered to be a “file” name
+ /// to be removed to get at the “directory” that is used as the base.
+ pub base_url: Option<Url>,
+ /// Headers to be applied to every request made by this client.
+ pub headers: HashMap<HeaderName, HeaderValues>,
+}
+
+impl Config {
+ /// Construct new empty config.
+ pub fn new() -> Self {
+ Self::default()
+ }
+}
+
+impl Config {
+ /// Adds a header to be added to every request by this config.
+ ///
+ /// Default: No extra headers.
+ pub fn add_header(
+ mut self,
+ name: impl Into<HeaderName>,
+ values: impl ToHeaderValues,
+ ) -> Result<Self> {
+ self.headers
+ .insert(name.into(), values.to_header_values()?.collect());
+ Ok(self)
+ }
+
+ /// Sets the base URL for this config. All request URLs will be relative to this URL.
+ ///
+ /// Note: a trailing slash is significant.
+ /// Without it, the last path component is considered to be a “file” name
+ /// to be removed to get at the “directory” that is used as the base.
+ ///
+ /// Default: `None` (internally).
+ pub fn set_base_url(mut self, base: Url) -> Self {
+ self.base_url = Some(base);
+ self
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
+pub struct Error {
+ message: String,
+ code: Option<crate::http::StatusCode>,
+}
+
+impl Error {
+ pub fn new(code: Option<crate::http::StatusCode>, message: impl Into<String>) -> Self {
+ Error {
+ message: message.into(),
+ code,
+ }
+ }
+}
+
+impl From<crate::http::Error> for Error {
+ fn from(e: crate::http::Error) -> Self {
+ Error {
+ message: e.to_string(),
+ code: Some(e.status()),
+ }
+ }
+}
+
+impl From<serde_json::Error> for Error {
+ fn from(e: serde_json::Error) -> Self {
+ Error {
+ message: e.to_string(),
+ code: None,
+ }
+ }
+}
+
+impl From<url::ParseError> for Error {
+ fn from(e: url::ParseError) -> Self {
+ Error {
+ message: e.to_string(),
+ code: None,
+ }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +
use std::marker::PhantomData;
+
+use http_types::convert::DeserializeOwned;
+
+use crate::{Response, Result};
+
+pub trait ResponseExpectation {
+ type Body;
+
+ fn decode(&self, resp: crate::Response<Vec<u8>>) -> Result<Response<Self::Body>>;
+}
+
+pub struct ExpectBytes;
+
+impl ResponseExpectation for ExpectBytes {
+ type Body = Vec<u8>;
+
+ fn decode(&self, resp: crate::Response<Vec<u8>>) -> Result<Response<Vec<u8>>> {
+ Ok(resp)
+ }
+}
+
+#[derive(Default)]
+pub struct ExpectString;
+
+impl ResponseExpectation for ExpectString {
+ type Body = String;
+
+ fn decode(&self, mut resp: crate::Response<Vec<u8>>) -> Result<Response<String>> {
+ let body = resp.body_string()?;
+ Ok(resp.with_body(body))
+ }
+}
+
+pub struct ExpectJson<T> {
+ phantom: PhantomData<fn() -> T>,
+}
+
+impl<T> Default for ExpectJson<T> {
+ fn default() -> Self {
+ Self {
+ phantom: Default::default(),
+ }
+ }
+}
+
+impl<T> ResponseExpectation for ExpectJson<T>
+where
+ T: DeserializeOwned,
+{
+ type Body = T;
+
+ fn decode(&self, mut resp: crate::Response<Vec<u8>>) -> Result<Response<T>> {
+ let body = resp.body_json::<T>()?;
+ Ok(resp.with_body(body))
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +
//! A HTTP client for use with Crux
+//!
+//! `crux_http` allows Crux apps to make HTTP requests by asking the Shell to perform them.
+//!
+//! This is still work in progress and large parts of HTTP are not yet supported.
+// #![warn(missing_docs)]
+
+use crux_core::capability::CapabilityContext;
+use crux_macros::Capability;
+use http::Method;
+use url::Url;
+
+mod config;
+mod error;
+mod expect;
+mod request;
+mod request_builder;
+mod response;
+
+pub mod client;
+pub mod middleware;
+pub mod protocol;
+pub mod testing;
+
+pub use http_types::{self as http};
+
+pub use self::{
+ config::Config,
+ error::Error,
+ request::Request,
+ request_builder::RequestBuilder,
+ response::{Response, ResponseAsync},
+};
+
+use client::Client;
+
+pub type Result<T> = std::result::Result<T, Error>;
+
+/// The Http capability API.
+#[derive(Capability)]
+pub struct Http<Ev> {
+ context: CapabilityContext<protocol::HttpRequest, Ev>,
+ client: Client,
+}
+
+impl<Ev> Clone for Http<Ev> {
+ fn clone(&self) -> Self {
+ Self {
+ context: self.context.clone(),
+ client: self.client.clone(),
+ }
+ }
+}
+
+impl<Ev> Http<Ev>
+where
+ Ev: 'static,
+{
+ pub fn new(context: CapabilityContext<protocol::HttpRequest, Ev>) -> Self {
+ Self {
+ client: Client::new(context.clone()),
+ context,
+ }
+ }
+
+ /// Instruct the Shell to perform a HTTP GET request to the provided `url`.
+ ///
+ /// The request can be configured via associated functions on `RequestBuilder`
+ /// and then sent with `RequestBuilder::send`
+ ///
+ /// When finished, the response will be wrapped in an event and dispatched to
+ /// the app's `update function.
+ ///
+ /// # Panics
+ ///
+ /// This will panic if a malformed URL is passed.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
+ /// # struct Capabilities { http: crux_http::Http<Event> }
+ /// # fn update(caps: &Capabilities) {
+ /// caps.http.get("https://httpbin.org/get").send(Event::ReceiveResponse)
+ /// # }
+ /// ```
+ pub fn get(&self, url: impl AsRef<str>) -> RequestBuilder<Ev> {
+ RequestBuilder::new(Method::Get, url.as_ref().parse().unwrap(), self.clone())
+ }
+
+ /// Instruct the Shell to perform a HTTP HEAD request to the provided `url`.
+ ///
+ /// The request can be configured via associated functions on `RequestBuilder`
+ /// and then sent with `RequestBuilder::send`
+ ///
+ /// When finished, the response will be wrapped in an event and dispatched to
+ /// the app's `update function.
+ ///
+ /// # Panics
+ ///
+ /// This will panic if a malformed URL is passed.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
+ /// # struct Capabilities { http: crux_http::Http<Event> }
+ /// # fn update(caps: &Capabilities) {
+ /// caps.http.head("https://httpbin.org/get").send(Event::ReceiveResponse)
+ /// # }
+ /// ```
+ pub fn head(&self, url: impl AsRef<str>) -> RequestBuilder<Ev> {
+ RequestBuilder::new(Method::Head, url.as_ref().parse().unwrap(), self.clone())
+ }
+
+ /// Instruct the Shell to perform a HTTP POST request to the provided `url`.
+ ///
+ /// The request can be configured via associated functions on `RequestBuilder`
+ /// and then sent with `RequestBuilder::send`
+ ///
+ /// When finished, the response will be wrapped in an event and dispatched to
+ /// the app's `update function.
+ ///
+ /// # Panics
+ ///
+ /// This will panic if a malformed URL is passed.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
+ /// # struct Capabilities { http: crux_http::Http<Event> }
+ /// # fn update(caps: &Capabilities) {
+ /// caps.http.post("https://httpbin.org/post").send(Event::ReceiveResponse)
+ /// # }
+ /// ```
+ pub fn post(&self, url: impl AsRef<str>) -> RequestBuilder<Ev> {
+ RequestBuilder::new(Method::Post, url.as_ref().parse().unwrap(), self.clone())
+ }
+
+ /// Instruct the Shell to perform a HTTP PUT request to the provided `url`.
+ ///
+ /// The request can be configured via associated functions on `RequestBuilder`
+ /// and then sent with `RequestBuilder::send`
+ ///
+ /// When finished, the response will be wrapped in an event and dispatched to
+ /// the app's `update function.
+ ///
+ /// # Panics
+ ///
+ /// This will panic if a malformed URL is passed.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
+ /// # struct Capabilities { http: crux_http::Http<Event> }
+ /// # fn update(caps: &Capabilities) {
+ /// caps.http.put("https://httpbin.org/post").send(Event::ReceiveResponse)
+ /// # }
+ /// ```
+ pub fn put(&self, url: impl AsRef<str>) -> RequestBuilder<Ev> {
+ RequestBuilder::new(Method::Put, url.as_ref().parse().unwrap(), self.clone())
+ }
+
+ /// Instruct the Shell to perform a HTTP DELETE request to the provided `url`.
+ ///
+ /// The request can be configured via associated functions on `RequestBuilder`
+ /// and then sent with `RequestBuilder::send`
+ ///
+ /// When finished, the response will be wrapped in an event and dispatched to
+ /// the app's `update function.
+ ///
+ /// # Panics
+ ///
+ /// This will panic if a malformed URL is passed.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
+ /// # struct Capabilities { http: crux_http::Http<Event> }
+ /// # fn update(caps: &Capabilities) {
+ /// caps.http.delete("https://httpbin.org/post").send(Event::ReceiveResponse)
+ /// # }
+ /// ```
+ pub fn delete(&self, url: impl AsRef<str>) -> RequestBuilder<Ev> {
+ RequestBuilder::new(Method::Delete, url.as_ref().parse().unwrap(), self.clone())
+ }
+
+ /// Instruct the Shell to perform a HTTP CONNECT request to the provided `url`.
+ ///
+ /// The request can be configured via associated functions on `RequestBuilder`
+ /// and then sent with `RequestBuilder::send`
+ ///
+ /// When finished, the response will be wrapped in an event and dispatched to
+ /// the app's `update function.
+ ///
+ /// # Panics
+ ///
+ /// This will panic if a malformed URL is passed.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
+ /// # struct Capabilities { http: crux_http::Http<Event> }
+ /// # fn update(caps: &Capabilities) {
+ /// caps.http.connect("https://httpbin.org/get").send(Event::ReceiveResponse)
+ /// # }
+ /// ```
+ pub fn connect(&self, url: impl AsRef<str>) -> RequestBuilder<Ev> {
+ RequestBuilder::new(Method::Connect, url.as_ref().parse().unwrap(), self.clone())
+ }
+
+ /// Instruct the Shell to perform a HTTP OPTIONS request to the provided `url`.
+ ///
+ /// The request can be configured via associated functions on `RequestBuilder`
+ /// and then sent with `RequestBuilder::send`
+ ///
+ /// When finished, the response will be wrapped in an event and dispatched to
+ /// the app's `update function.
+ ///
+ /// # Panics
+ ///
+ /// This will panic if a malformed URL is passed.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
+ /// # struct Capabilities { http: crux_http::Http<Event> }
+ /// # fn update(caps: &Capabilities) {
+ /// caps.http.options("https://httpbin.org/get").send(Event::ReceiveResponse)
+ /// # }
+ /// ```
+ pub fn options(&self, url: impl AsRef<str>) -> RequestBuilder<Ev> {
+ RequestBuilder::new(Method::Options, url.as_ref().parse().unwrap(), self.clone())
+ }
+
+ /// Instruct the Shell to perform a HTTP TRACE request to the provided `url`.
+ ///
+ /// The request can be configured via associated functions on `RequestBuilder`
+ /// and then sent with `RequestBuilder::send`
+ ///
+ /// When finished, the response will be wrapped in an event and dispatched to
+ /// the app's `update function.
+ ///
+ /// # Panics
+ ///
+ /// This will panic if a malformed URL is passed.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
+ /// # struct Capabilities { http: crux_http::Http<Event> }
+ /// # fn update(caps: &Capabilities) {
+ /// caps.http.trace("https://httpbin.org/get").send(Event::ReceiveResponse)
+ /// # }
+ /// ```
+ pub fn trace(&self, url: impl AsRef<str>) -> RequestBuilder<Ev> {
+ RequestBuilder::new(Method::Trace, url.as_ref().parse().unwrap(), self.clone())
+ }
+
+ /// Instruct the Shell to perform a HTTP PATCH request to the provided `url`.
+ ///
+ /// The request can be configured via associated functions on `RequestBuilder`
+ /// and then sent with `RequestBuilder::send`
+ ///
+ /// When finished, the response will be wrapped in an event and dispatched to
+ /// the app's `update function.
+ ///
+ /// # Panics
+ ///
+ /// This will panic if a malformed URL is passed.
+ pub fn patch(&self, url: impl AsRef<str>) -> RequestBuilder<Ev> {
+ RequestBuilder::new(Method::Patch, url.as_ref().parse().unwrap(), self.clone())
+ }
+
+ /// Instruct the Shell to perform an HTTP request with the provided `method` and `url`.
+ ///
+ /// The request can be configured via associated functions on `RequestBuilder`
+ /// and then sent with `RequestBuilder::send`
+ ///
+ /// When finished, the response will be wrapped in an event and dispatched to
+ /// the app's `update function.
+ pub fn request(&self, method: http::Method, url: Url) -> RequestBuilder<Ev> {
+ RequestBuilder::new(method, url, self.clone())
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +
//! Middleware types
+//!
+//! # Examples
+//! ```no_run
+//! use crux_http::middleware::{Next, Middleware};
+//! use crux_http::{client::Client, Request, ResponseAsync, Result};
+//! use std::time;
+//! use std::sync::Arc;
+//!
+//! /// Log each request's duration
+//! #[derive(Debug)]
+//! pub struct Logger;
+//!
+//! #[async_trait::async_trait]
+//! impl Middleware for Logger {
+//! async fn handle(
+//! &self,
+//! req: Request,
+//! client: Client,
+//! next: Next<'_>,
+//! ) -> Result<ResponseAsync> {
+//! println!("sending request to {}", req.url());
+//! let now = time::Instant::now();
+//! let res = next.run(req, client).await?;
+//! println!("request completed ({:?})", now.elapsed());
+//! Ok(res)
+//! }
+//! }
+//! ```
+//! `Middleware` can also be instantiated using a free function thanks to some convenient trait
+//! implementations.
+//!
+//! ```no_run
+//! use futures_util::future::BoxFuture;
+//! use crux_http::middleware::{Next, Middleware};
+//! use crux_http::{client::Client, Request, ResponseAsync, Result};
+//! use std::time;
+//! use std::sync::Arc;
+//!
+//! fn logger<'a>(req: Request, client: Client, next: Next<'a>) -> BoxFuture<'a, Result<ResponseAsync>> {
+//! Box::pin(async move {
+//! println!("sending request to {}", req.url());
+//! let now = time::Instant::now();
+//! let res = next.run(req, client).await?;
+//! println!("request completed ({:?})", now.elapsed());
+//! Ok(res)
+//! })
+//! }
+//! ```
+
+use std::sync::Arc;
+
+use crate::{Client, Request, ResponseAsync, Result};
+
+mod redirect;
+
+pub use redirect::Redirect;
+
+use async_trait::async_trait;
+use futures_util::future::BoxFuture;
+
+/// Middleware that wraps around remaining middleware chain.
+#[async_trait]
+pub trait Middleware: 'static + Send + Sync {
+ /// Asynchronously handle the request, and return a response.
+ async fn handle(&self, req: Request, client: Client, next: Next<'_>) -> Result<ResponseAsync>;
+}
+
+// This allows functions to work as middleware too.
+#[async_trait]
+impl<F> Middleware for F
+where
+ F: Send
+ + Sync
+ + 'static
+ + for<'a> Fn(Request, Client, Next<'a>) -> BoxFuture<'a, Result<ResponseAsync>>,
+{
+ async fn handle(&self, req: Request, client: Client, next: Next<'_>) -> Result<ResponseAsync> {
+ (self)(req, client, next).await
+ }
+}
+
+/// The remainder of a middleware chain, including the endpoint.
+#[allow(missing_debug_implementations)]
+pub struct Next<'a> {
+ next_middleware: &'a [Arc<dyn Middleware>],
+ endpoint: &'a (dyn (Fn(Request, Client) -> BoxFuture<'static, Result<ResponseAsync>>)
+ + Send
+ + Sync
+ + 'static),
+}
+
+impl Clone for Next<'_> {
+ fn clone(&self) -> Self {
+ Self {
+ next_middleware: self.next_middleware,
+ endpoint: self.endpoint,
+ }
+ }
+}
+
+impl Copy for Next<'_> {}
+
+impl<'a> Next<'a> {
+ /// Create a new instance
+ pub fn new(
+ next: &'a [Arc<dyn Middleware>],
+ endpoint: &'a (dyn (Fn(Request, Client) -> BoxFuture<'static, Result<ResponseAsync>>)
+ + Send
+ + Sync
+ + 'static),
+ ) -> Self {
+ Self {
+ endpoint,
+ next_middleware: next,
+ }
+ }
+
+ /// Asynchronously execute the remaining middleware chain.
+ pub fn run(mut self, req: Request, client: Client) -> BoxFuture<'a, Result<ResponseAsync>> {
+ if let Some((current, next)) = self.next_middleware.split_first() {
+ self.next_middleware = next;
+ current.handle(req, client, self)
+ } else {
+ (self.endpoint)(req, client)
+ }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +
//! HTTP Redirect middleware.
+//!
+//! # Examples
+//!
+//! ```no_run
+//! # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
+//! # struct Capabilities { http: crux_http::Http<Event> }
+//! # fn update(caps: &Capabilities) {
+//!
+//! caps.http
+//! .get("https://httpbin.org/redirect/2")
+//! .middleware(crux_http::middleware::Redirect::default())
+//! .send(Event::ReceiveResponse)
+//! # }
+//! ```
+
+use crate::http::{self, headers, StatusCode, Url};
+use crate::middleware::{Middleware, Next, Request};
+use crate::{Client, ResponseAsync, Result};
+
+// List of acceptable 300-series redirect codes.
+const REDIRECT_CODES: &[StatusCode] = &[
+ StatusCode::MovedPermanently,
+ StatusCode::Found,
+ StatusCode::SeeOther,
+ StatusCode::TemporaryRedirect,
+ StatusCode::PermanentRedirect,
+];
+
+/// A middleware which attempts to follow HTTP redirects.
+#[derive(Debug)]
+pub struct Redirect {
+ attempts: u8,
+}
+
+impl Redirect {
+ /// Create a new instance of the Redirect middleware, which attempts to follow redirects
+ /// up to as many times as specified.
+ ///
+ /// Consider using `Redirect::default()` for the default number of redirect attempts.
+ ///
+ /// This middleware will follow redirects from the `Location` header if the server returns
+ /// any of the following http response codes:
+ /// - 301 Moved Permanently
+ /// - 302 Found
+ /// - 303 See other
+ /// - 307 Temporary Redirect
+ /// - 308 Permanent Redirect
+ ///
+ /// # Errors
+ ///
+ /// An error will be passed through the middleware stack if the value of the `Location`
+ /// header is not a validly parsing url.
+ ///
+ /// # Caveats
+ ///
+ /// This will presently make at least one additional HTTP request before the actual request to
+ /// determine if there is a redirect that should be followed, so as to preserve any request body.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
+ /// # struct Capabilities { http: crux_http::Http<Event> }
+ /// # fn update(caps: &Capabilities) {
+ ///
+ /// caps.http
+ /// .get("https://httpbin.org/redirect/2")
+ /// .middleware(crux_http::middleware::Redirect::default())
+ /// .send(Event::ReceiveResponse)
+ /// # }
+ /// ```
+ pub fn new(attempts: u8) -> Self {
+ Redirect { attempts }
+ }
+}
+
+#[async_trait::async_trait]
+impl Middleware for Redirect {
+ async fn handle(
+ &self,
+ mut req: Request,
+ client: Client,
+ next: Next<'_>,
+ ) -> Result<ResponseAsync> {
+ let mut redirect_count: u8 = 0;
+
+ // Note(Jeremiah): This is not ideal.
+ //
+ // HttpClient is currently too limiting for efficient redirects.
+ // We do not want to make unnecessary full requests, but it is
+ // presently required due to how Body streams work.
+ //
+ // Ideally we'd have methods to send a partial request stream,
+ // without sending the body, that would potnetially be able to
+ // get a server status before we attempt to send the body.
+ //
+ // As a work around we clone the request first (without the body),
+ // and try sending it until we get some status back that is not a
+ // redirect.
+
+ let mut base_url = req.url().clone();
+
+ while redirect_count < self.attempts {
+ redirect_count += 1;
+ let r: Request = req.clone();
+ let res: ResponseAsync = client.send(r).await?;
+ if REDIRECT_CODES.contains(&res.status()) {
+ if let Some(location) = res.header(headers::LOCATION) {
+ let http_req: &mut http::Request = req.as_mut();
+ *http_req.url_mut() = match Url::parse(location.last().as_str()) {
+ Ok(valid_url) => {
+ base_url = valid_url;
+ base_url.clone()
+ }
+ Err(e) => match e {
+ http::url::ParseError::RelativeUrlWithoutBase => {
+ base_url.join(location.last().as_str())?
+ }
+ e => return Err(e.into()),
+ },
+ };
+ }
+ } else {
+ break;
+ }
+ }
+
+ Ok(next.run(req, client).await?)
+ }
+}
+
+impl Default for Redirect {
+ /// Create a new instance of the Redirect middleware, which attempts to follow up to
+ /// 3 redirects (not including the actual request).
+ fn default() -> Self {
+ Self { attempts: 3 }
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +
//! The protocol for communicating with the shell
+//!
+//! Crux capabilities don't interface with the outside world themselves, they carry
+//! out all their operations by exchanging messages with the platform specific shell.
+//! This module defines the protocol for crux_http to communicate with the shell.
+
+use async_trait::async_trait;
+use derive_builder::Builder;
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+pub struct HttpHeader {
+ pub name: String,
+ pub value: String,
+}
+
+#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq, Builder)]
+#[builder(
+ custom_constructor,
+ build_fn(private, name = "fallible_build"),
+ setter(into)
+)]
+pub struct HttpRequest {
+ pub method: String,
+ pub url: String,
+ #[builder(setter(custom))]
+ pub headers: Vec<HttpHeader>,
+ pub body: Vec<u8>,
+}
+
+macro_rules! http_method {
+ ($name:ident, $method:expr) => {
+ pub fn $name(url: impl Into<String>) -> HttpRequestBuilder {
+ HttpRequestBuilder {
+ method: Some($method.to_string()),
+ url: Some(url.into()),
+ headers: Some(vec![]),
+ body: Some(vec![]),
+ }
+ }
+ };
+}
+
+impl HttpRequest {
+ http_method!(get, "GET");
+ http_method!(put, "PUT");
+ http_method!(delete, "DELETE");
+ http_method!(post, "POST");
+ http_method!(patch, "PATCH");
+ http_method!(head, "HEAD");
+ http_method!(options, "OPTIONS");
+}
+
+impl HttpRequestBuilder {
+ pub fn header(&mut self, name: impl Into<String>, value: impl Into<String>) -> &mut Self {
+ self.headers.get_or_insert_with(Vec::new).push(HttpHeader {
+ name: name.into(),
+ value: value.into(),
+ });
+ self
+ }
+
+ pub fn json(&mut self, body: impl serde::Serialize) -> &mut Self {
+ self.body = Some(serde_json::to_vec(&body).unwrap());
+ self
+ }
+
+ pub fn build(&self) -> HttpRequest {
+ self.fallible_build()
+ .expect("All required fields were initialized")
+ }
+}
+
+#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq, Builder)]
+#[builder(
+ custom_constructor,
+ build_fn(private, name = "fallible_build"),
+ setter(into)
+)]
+pub struct HttpResponse {
+ pub status: u16, // FIXME this probably should be a giant enum instead.
+ #[builder(setter(custom))]
+ pub headers: Vec<HttpHeader>,
+ pub body: Vec<u8>,
+}
+
+impl HttpResponse {
+ pub fn status(status: u16) -> HttpResponseBuilder {
+ HttpResponseBuilder {
+ status: Some(status.into()),
+ headers: Some(vec![]),
+ body: Some(vec![]),
+ }
+ }
+ pub fn ok() -> HttpResponseBuilder {
+ Self::status(200)
+ }
+}
+
+impl HttpResponseBuilder {
+ pub fn header(&mut self, name: impl Into<String>, value: impl Into<String>) -> &mut Self {
+ self.headers.get_or_insert_with(Vec::new).push(HttpHeader {
+ name: name.into(),
+ value: value.into(),
+ });
+ self
+ }
+
+ pub fn json(&mut self, body: impl serde::Serialize) -> &mut Self {
+ self.body = Some(serde_json::to_vec(&body).unwrap());
+ self
+ }
+
+ pub fn build(&self) -> HttpResponse {
+ self.fallible_build()
+ .expect("All required fields were initialized")
+ }
+}
+
+impl crux_core::capability::Operation for HttpRequest {
+ type Output = HttpResponse;
+}
+
+#[async_trait]
+pub(crate) trait EffectSender {
+ async fn send(&self, effect: HttpRequest) -> HttpResponse;
+}
+
+#[async_trait]
+impl<Ev> EffectSender for crux_core::capability::CapabilityContext<HttpRequest, Ev>
+where
+ Ev: 'static,
+{
+ async fn send(&self, effect: HttpRequest) -> HttpResponse {
+ crux_core::capability::CapabilityContext::request_from_shell(self, effect).await
+ }
+}
+
+#[async_trait]
+pub(crate) trait ProtocolRequestBuilder {
+ async fn into_protocol_request(mut self) -> crate::Result<HttpRequest>;
+}
+
+#[async_trait]
+impl ProtocolRequestBuilder for crate::Request {
+ async fn into_protocol_request(mut self) -> crate::Result<HttpRequest> {
+ let body = if self.is_empty() == Some(false) {
+ self.take_body().into_bytes().await?
+ } else {
+ vec![]
+ };
+
+ Ok(HttpRequest {
+ method: self.method().to_string(),
+ url: self.url().to_string(),
+ headers: self
+ .iter()
+ .flat_map(|(name, values)| {
+ values.iter().map(|value| HttpHeader {
+ name: name.to_string(),
+ value: value.to_string(),
+ })
+ })
+ .collect(),
+ body,
+ })
+ }
+}
+
+impl From<HttpResponse> for crate::ResponseAsync {
+ fn from(effect_response: HttpResponse) -> Self {
+ let mut res = crate::http::Response::new(effect_response.status);
+ res.set_body(effect_response.body);
+ for header in effect_response.headers {
+ res.append_header(header.name.as_str(), header.value);
+ }
+
+ crate::ResponseAsync::new(res)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_http_request_get() {
+ let req = HttpRequest::get("https://example.com").build();
+
+ assert_eq!(
+ req,
+ HttpRequest {
+ method: "GET".to_string(),
+ url: "https://example.com".to_string(),
+ ..Default::default()
+ }
+ );
+ }
+
+ #[test]
+ fn test_http_request_get_with_fields() {
+ let req = HttpRequest::get("https://example.com")
+ .header("foo", "bar")
+ .body("123")
+ .build();
+
+ assert_eq!(
+ req,
+ HttpRequest {
+ method: "GET".to_string(),
+ url: "https://example.com".to_string(),
+ headers: vec![HttpHeader {
+ name: "foo".to_string(),
+ value: "bar".to_string(),
+ }],
+ body: "123".as_bytes().to_vec(),
+ }
+ );
+ }
+
+ #[test]
+ fn test_http_response_status() {
+ let req = HttpResponse::status(302).build();
+
+ assert_eq!(
+ req,
+ HttpResponse {
+ status: 302,
+ ..Default::default()
+ }
+ );
+ }
+
+ #[test]
+ fn test_http_response_status_with_fields() {
+ let req = HttpResponse::status(302)
+ .header("foo", "bar")
+ .body("hello world")
+ .build();
+
+ assert_eq!(
+ req,
+ HttpResponse {
+ status: 302,
+ headers: vec![HttpHeader {
+ name: "foo".to_string(),
+ value: "bar".to_string(),
+ }],
+ body: "hello world".as_bytes().to_vec(),
+ }
+ );
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +
use crate::http::{
+ self,
+ headers::{self, HeaderName, HeaderValues, ToHeaderValues},
+ Body, Method, Mime, Url,
+};
+use crate::middleware::Middleware;
+
+use serde::Serialize;
+
+use std::fmt;
+use std::ops::Index;
+use std::sync::Arc;
+
+/// An HTTP request, returns a `Response`.
+#[derive(Clone)]
+pub struct Request {
+ /// Holds the state of the request.
+ req: http::Request,
+ /// Holds an optional per-request middleware stack.
+ middleware: Option<Vec<Arc<dyn Middleware>>>,
+}
+
+impl Request {
+ /// Create a new instance.
+ ///
+ /// This method is particularly useful when input URLs might be passed by third parties, and
+ /// you don't want to panic if they're malformed. If URLs are statically encoded, it might be
+ /// easier to use one of the shorthand methods instead.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// fn main() -> crux_http::Result<()> {
+ /// use crux_http::http::{Url, Method};
+ ///
+ /// let url = Url::parse("https://httpbin.org/get")?;
+ /// let req = crux_http::Request::new(Method::Get, url);
+ /// # Ok(()) }
+ /// ```
+ pub fn new(method: Method, url: Url) -> Self {
+ let req = http::Request::new(method, url);
+ Self {
+ req,
+ middleware: None,
+ }
+ }
+
+ /// Get the URL querystring.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use serde::{Deserialize, Serialize};
+ /// # enum Event {}
+ /// # struct Capabilities { http: crux_http::Http<Event> }
+ /// # fn update(caps: &Capabilities) -> crux_http::Result<()> {
+ /// #[derive(Serialize, Deserialize)]
+ /// struct Index {
+ /// page: u32
+ /// }
+ ///
+ /// let req = caps.http.get("https://httpbin.org/get?page=2").build();
+ /// let Index { page } = req.query()?;
+ /// assert_eq!(page, 2);
+ /// # Ok(()) }
+ /// ```
+ pub fn query<T: serde::de::DeserializeOwned>(&self) -> crate::Result<T> {
+ Ok(self.req.query()?)
+ }
+
+ /// Set the URL querystring.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use serde::{Deserialize, Serialize};
+ /// # enum Event {}
+ /// # struct Capabilities { http: crux_http::Http<Event> }
+ /// # fn update(caps: &Capabilities) -> crux_http::Result<()> {
+ /// #[derive(Serialize, Deserialize)]
+ /// struct Index {
+ /// page: u32
+ /// }
+ ///
+ /// let query = Index { page: 2 };
+ /// let mut req = caps.http.get("https://httpbin.org/get").build();
+ /// req.set_query(&query)?;
+ /// assert_eq!(req.url().query(), Some("page=2"));
+ /// assert_eq!(req.url().as_str(), "https://httpbin.org/get?page=2");
+ /// # Ok(()) }
+ /// ```
+ pub fn set_query(&mut self, query: &impl Serialize) -> crate::Result<()> {
+ Ok(self.req.set_query(query)?)
+ }
+
+ /// Get an HTTP header.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # enum Event {}
+ /// # struct Capabilities { http: crux_http::Http<Event> }
+ /// # fn update(caps: &Capabilities) -> crux_http::Result<()> {
+ /// let mut req = caps.http.get("https://httpbin.org/get").build();
+ /// req.set_header("X-Requested-With", "surf");
+ /// assert_eq!(req.header("X-Requested-With").unwrap(), "surf");
+ /// # Ok(()) }
+ /// ```
+ pub fn header(&self, key: impl Into<HeaderName>) -> Option<&HeaderValues> {
+ self.req.header(key)
+ }
+
+ /// Get a mutable reference to a header.
+ pub fn header_mut(&mut self, name: impl Into<HeaderName>) -> Option<&mut HeaderValues> {
+ self.req.header_mut(name)
+ }
+
+ /// Set an HTTP header.
+ pub fn insert_header(
+ &mut self,
+ name: impl Into<HeaderName>,
+ values: impl ToHeaderValues,
+ ) -> Option<HeaderValues> {
+ self.req.insert_header(name, values)
+ }
+
+ /// Append a header to the headers.
+ ///
+ /// Unlike `insert` this function will not override the contents of a header, but insert a
+ /// header if there aren't any. Or else append to the existing list of headers.
+ pub fn append_header(&mut self, name: impl Into<HeaderName>, values: impl ToHeaderValues) {
+ self.req.append_header(name, values)
+ }
+
+ /// Remove a header.
+ pub fn remove_header(&mut self, name: impl Into<HeaderName>) -> Option<HeaderValues> {
+ self.req.remove_header(name)
+ }
+
+ /// An iterator visiting all header pairs in arbitrary order.
+ #[must_use]
+ pub fn iter(&self) -> headers::Iter<'_> {
+ self.req.iter()
+ }
+
+ /// An iterator visiting all header pairs in arbitrary order, with mutable references to the
+ /// values.
+ #[must_use]
+ pub fn iter_mut(&mut self) -> headers::IterMut<'_> {
+ self.req.iter_mut()
+ }
+
+ /// An iterator visiting all header names in arbitrary order.
+ #[must_use]
+ pub fn header_names(&self) -> headers::Names<'_> {
+ self.req.header_names()
+ }
+
+ /// An iterator visiting all header values in arbitrary order.
+ #[must_use]
+ pub fn header_values(&self) -> headers::Values<'_> {
+ self.req.header_values()
+ }
+
+ /// Set an HTTP header.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # enum Event {}
+ /// # struct Capabilities { http: crux_http::Http<Event> }
+ /// # fn update(caps: &Capabilities) -> crux_http::Result<()> {
+ /// let mut req = caps.http.get("https://httpbin.org/get").build();
+ /// req.set_header("X-Requested-With", "surf");
+ /// assert_eq!(req.header("X-Requested-With").unwrap(), "surf");
+ /// # Ok(()) }
+ /// ```
+ pub fn set_header(&mut self, key: impl Into<HeaderName>, value: impl ToHeaderValues) {
+ self.insert_header(key, value);
+ }
+
+ /// Get a request extension value.
+ #[must_use]
+ pub fn ext<T: Send + Sync + 'static>(&self) -> Option<&T> {
+ self.req.ext().get()
+ }
+
+ /// Set a request extension value.
+ pub fn set_ext<T: Send + Sync + 'static>(&mut self, val: T) -> Option<T> {
+ self.req.ext_mut().insert(val)
+ }
+
+ /// Get the request HTTP method.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # enum Event {}
+ /// # struct Capabilities { http: crux_http::Http<Event> }
+ /// # fn update(caps: &Capabilities) -> crux_http::Result<()> {
+ /// let req = caps.http.get("https://httpbin.org/get").build();
+ /// assert_eq!(req.method(), crux_http::http::Method::Get);
+ /// # Ok(()) }
+ /// ```
+ pub fn method(&self) -> Method {
+ self.req.method()
+ }
+
+ /// Get the request url.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # enum Event {}
+ /// # struct Capabilities { http: crux_http::Http<Event> }
+ /// # fn update(caps: &Capabilities) -> crux_http::Result<()> {
+ /// use crux_http::http::Url;
+ /// let req = caps.http.get("https://httpbin.org/get").build();
+ /// assert_eq!(req.url(), &Url::parse("https://httpbin.org/get")?);
+ /// # Ok(()) }
+ /// ```
+ pub fn url(&self) -> &Url {
+ self.req.url()
+ }
+
+ /// Get the request content type as a `Mime`.
+ ///
+ /// Gets the `Content-Type` header and parses it to a `Mime` type.
+ ///
+ /// [Read more on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types)
+ ///
+ /// # Panics
+ ///
+ /// This method will panic if an invalid MIME type was set as a header. Use the [`set_header`]
+ /// method to bypass any checks.
+ ///
+ /// [`set_header`]: #method.set_header
+ pub fn content_type(&self) -> Option<Mime> {
+ self.req.content_type()
+ }
+
+ /// Set the request content type from a `Mime`.
+ ///
+ /// [Read more on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types)
+ pub fn set_content_type(&mut self, mime: Mime) {
+ self.req.set_content_type(mime);
+ }
+
+ /// Get the length of the body stream, if it has been set.
+ ///
+ /// This value is set when passing a fixed-size object into as the body.
+ /// E.g. a string, or a buffer. Consumers of this API should check this
+ /// value to decide whether to use `Chunked` encoding, or set the
+ /// response length.
+ #[allow(clippy::len_without_is_empty)]
+ pub fn len(&self) -> Option<usize> {
+ self.req.len()
+ }
+
+ /// Returns `true` if the set length of the body stream is zero, `false`
+ /// otherwise.
+ pub fn is_empty(&self) -> Option<bool> {
+ self.req.is_empty()
+ }
+
+ /// Pass an `AsyncRead` stream as the request body.
+ ///
+ /// # Mime
+ ///
+ /// The encoding is set to `application/octet-stream`.
+ pub fn set_body(&mut self, body: impl Into<Body>) {
+ self.req.set_body(body)
+ }
+
+ /// Take the request body as a `Body`.
+ ///
+ /// This method can be called after the body has already been taken or read,
+ /// but will return an empty `Body`.
+ ///
+ /// This is useful for consuming the body via an AsyncReader or AsyncBufReader.
+ pub fn take_body(&mut self) -> Body {
+ self.req.take_body()
+ }
+
+ /// Pass JSON as the request body.
+ ///
+ /// # Mime
+ ///
+ /// The `content-type` is set to `application/json`.
+ ///
+ /// # Errors
+ ///
+ /// This method will return an error if the provided data could not be serialized to JSON.
+ pub fn body_json(&mut self, json: &impl Serialize) -> crate::Result<()> {
+ self.set_body(Body::from_json(json)?);
+ Ok(())
+ }
+
+ /// Pass a string as the request body.
+ ///
+ /// # Mime
+ ///
+ /// The `content-type` is set to `text/plain; charset=utf-8`.
+ pub fn body_string(&mut self, string: String) {
+ self.set_body(Body::from_string(string))
+ }
+
+ /// Pass bytes as the request body.
+ ///
+ /// # Mime
+ ///
+ /// The `content-type` is set to `application/octet-stream`.
+ pub fn body_bytes(&mut self, bytes: impl AsRef<[u8]>) {
+ self.set_body(Body::from(bytes.as_ref()))
+ }
+
+ /// Pass a file as the request body.
+ ///
+ /// # Mime
+ ///
+ /// The `content-type` is set based on the file extension using [`mime_guess`] if the operation was
+ /// successful. If `path` has no extension, or its extension has no known MIME type mapping,
+ /// then `None` is returned.
+ ///
+ /// [`mime_guess`]: https://docs.rs/mime_guess
+ ///
+ /// # Errors
+ ///
+ /// This method will return an error if the file couldn't be read.
+ #[cfg(not(target_arch = "wasm32"))]
+ pub async fn body_file(&mut self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
+ self.set_body(Body::from_file(path).await?);
+ Ok(())
+ }
+
+ /// Pass a form as the request body.
+ ///
+ /// # Mime
+ ///
+ /// The `content-type` is set to `application/x-www-form-urlencoded`.
+ ///
+ /// # Errors
+ ///
+ /// An error will be returned if the encoding failed.
+ pub fn body_form(&mut self, form: &impl Serialize) -> crate::Result<()> {
+ self.set_body(Body::from_form(form)?);
+ Ok(())
+ }
+
+ /// Push middleware onto a per-request middleware stack.
+ ///
+ /// **Important**: Setting per-request middleware incurs extra allocations.
+ /// Creating a `Client` with middleware is recommended.
+ ///
+ /// Client middleware is run before per-request middleware.
+ ///
+ /// See the [middleware] submodule for more information on middleware.
+ ///
+ /// [middleware]: ../middleware/index.html
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # enum Event {}
+ /// # struct Capabilities { http: crux_http::Http<Event> }
+ /// # fn update(caps: &Capabilities) -> crux_http::Result<()> {
+ /// let mut req = caps.http.get("https://httpbin.org/get").build();
+ /// req.middleware(crux_http::middleware::Redirect::default());
+ /// # Ok(()) }
+ /// ```
+ pub fn middleware(&mut self, middleware: impl Middleware) {
+ if self.middleware.is_none() {
+ self.middleware = Some(vec![]);
+ }
+
+ self.middleware.as_mut().unwrap().push(Arc::new(middleware));
+ }
+
+ pub(crate) fn take_middleware(&mut self) -> Option<Vec<Arc<dyn Middleware>>> {
+ self.middleware.take()
+ }
+}
+
+impl AsRef<http::Headers> for Request {
+ fn as_ref(&self) -> &http::Headers {
+ self.req.as_ref()
+ }
+}
+
+impl AsMut<http::Headers> for Request {
+ fn as_mut(&mut self) -> &mut http::Headers {
+ self.req.as_mut()
+ }
+}
+
+impl AsRef<http::Request> for Request {
+ fn as_ref(&self) -> &http::Request {
+ &self.req
+ }
+}
+
+impl AsMut<http::Request> for Request {
+ fn as_mut(&mut self) -> &mut http::Request {
+ &mut self.req
+ }
+}
+
+impl From<http::Request> for Request {
+ /// Converts an `http::Request` to a `crux_http::Request`.
+ fn from(req: http::Request) -> Self {
+ Self {
+ req,
+ middleware: None,
+ }
+ }
+}
+
+#[allow(clippy::from_over_into)]
+impl Into<http::Request> for Request {
+ /// Converts a `crux_http::Request` to an `http::Request`.
+ fn into(self) -> http::Request {
+ self.req
+ }
+}
+
+impl fmt::Debug for Request {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ fmt::Debug::fmt(&self.req, f)
+ }
+}
+
+impl IntoIterator for Request {
+ type Item = (HeaderName, HeaderValues);
+ type IntoIter = headers::IntoIter;
+
+ /// Returns a iterator of references over the remaining items.
+ #[inline]
+ fn into_iter(self) -> Self::IntoIter {
+ self.req.into_iter()
+ }
+}
+
+impl<'a> IntoIterator for &'a Request {
+ type Item = (&'a HeaderName, &'a HeaderValues);
+ type IntoIter = headers::Iter<'a>;
+
+ #[inline]
+ fn into_iter(self) -> Self::IntoIter {
+ self.req.iter()
+ }
+}
+
+impl<'a> IntoIterator for &'a mut Request {
+ type Item = (&'a HeaderName, &'a mut HeaderValues);
+ type IntoIter = headers::IterMut<'a>;
+
+ #[inline]
+ fn into_iter(self) -> Self::IntoIter {
+ self.req.iter_mut()
+ }
+}
+
+impl Index<HeaderName> for Request {
+ type Output = HeaderValues;
+
+ /// Returns a reference to the value corresponding to the supplied name.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the name is not present in `Request`.
+ #[inline]
+ fn index(&self, name: HeaderName) -> &HeaderValues {
+ &self.req[name]
+ }
+}
+
+impl Index<&str> for Request {
+ type Output = HeaderValues;
+
+ /// Returns a reference to the value corresponding to the supplied name.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the name is not present in `Request`.
+ #[inline]
+ fn index(&self, name: &str) -> &HeaderValues {
+ &self.req[name]
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +
use crate::expect::{ExpectBytes, ExpectJson, ExpectString};
+use crate::middleware::Middleware;
+use crate::{
+ expect::ResponseExpectation,
+ http::{
+ headers::{HeaderName, ToHeaderValues},
+ Body, Method, Mime, Url,
+ },
+};
+use crate::{Client, Error, Request, Response, ResponseAsync, Result};
+
+use futures_util::future::BoxFuture;
+use http_types::convert::DeserializeOwned;
+use serde::Serialize;
+
+use std::{fmt, marker::PhantomData};
+
+/// Request Builder
+///
+/// Provides an ergonomic way to chain the creation of a request.
+/// This is generally accessed as the return value from `Http::{method}()`.
+///
+/// # Examples
+///
+/// ```no_run
+/// use crux_http::http::{mime::HTML};
+/// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
+/// # struct Capabilities { http: crux_http::Http<Event> }
+/// # fn update(caps: &Capabilities) {
+/// caps.http
+/// .post("https://httpbin.org/post")
+/// .body("<html>hi</html>")
+/// .header("custom-header", "value")
+/// .content_type(HTML)
+/// .send(Event::ReceiveResponse)
+/// # }
+/// ```
+#[must_use]
+pub struct RequestBuilder<Event, ExpectBody = Vec<u8>> {
+ /// Holds the state of the request.
+ req: Option<Request>,
+
+ cap_or_client: CapOrClient<Event>,
+
+ phantom: PhantomData<fn() -> Event>,
+
+ expectation: Box<dyn ResponseExpectation<Body = ExpectBody> + Send>,
+}
+
+// Middleware request builders won't have access to the capability, so they get a client
+// and therefore can't send events themselves. Normal request builders get direct access
+// to the capability itself.
+enum CapOrClient<Event> {
+ Client(Client),
+ Capability(crate::Http<Event>),
+}
+
+impl<Event> RequestBuilder<Event, Vec<u8>> {
+ pub(crate) fn new(method: Method, url: Url, capability: crate::Http<Event>) -> Self {
+ Self {
+ req: Some(Request::new(method, url)),
+ cap_or_client: CapOrClient::Capability(capability),
+ phantom: PhantomData,
+ expectation: Box::new(ExpectBytes),
+ }
+ }
+}
+
+impl RequestBuilder<(), Vec<u8>> {
+ pub(crate) fn new_for_middleware(method: Method, url: Url, client: Client) -> Self {
+ Self {
+ req: Some(Request::new(method, url)),
+ cap_or_client: CapOrClient::Client(client),
+ phantom: PhantomData,
+ expectation: Box::new(ExpectBytes),
+ }
+ }
+}
+
+impl<Event, ExpectBody> RequestBuilder<Event, ExpectBody>
+where
+ Event: 'static,
+ ExpectBody: 'static,
+{
+ /// Sets a header on the request.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
+ /// # struct Capabilities { http: crux_http::Http<Event> }
+ /// # fn update(caps: &Capabilities) {
+ /// caps.http
+ /// .get("https://httpbin.org/get")
+ /// .body("<html>hi</html>")
+ /// .header("header-name", "header-value")
+ /// .send(Event::ReceiveResponse)
+ /// # }
+ /// ```
+ pub fn header(mut self, key: impl Into<HeaderName>, value: impl ToHeaderValues) -> Self {
+ self.req.as_mut().unwrap().insert_header(key, value);
+ self
+ }
+
+ /// Sets the Content-Type header on the request.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # use crux_http::http::mime;
+ /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
+ /// # struct Capabilities { http: crux_http::Http<Event> }
+ /// # fn update(caps: &Capabilities) {
+ /// caps.http
+ /// .get("https://httpbin.org/get")
+ /// .content_type(mime::HTML)
+ /// .send(Event::ReceiveResponse)
+ /// # }
+ /// ```
+ pub fn content_type(mut self, content_type: impl Into<Mime>) -> Self {
+ self.req
+ .as_mut()
+ .unwrap()
+ .set_content_type(content_type.into());
+ self
+ }
+
+ /// Sets the body of the request from any type with implements `Into<Body>`, for example, any type with is `AsyncRead`.
+ /// # Mime
+ ///
+ /// The encoding is set to `application/octet-stream`.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
+ /// # struct Capabilities { http: crux_http::Http<Event> }
+ /// # fn update(caps: &Capabilities) {
+ /// use serde_json::json;
+ /// use crux_http::http::mime;
+ /// caps.http
+ /// .post("https://httpbin.org/post")
+ /// .body(json!({"any": "Into<Body>"}))
+ /// .content_type(mime::HTML)
+ /// .send(Event::ReceiveResponse)
+ /// # }
+ /// ```
+ pub fn body(mut self, body: impl Into<Body>) -> Self {
+ self.req.as_mut().unwrap().set_body(body);
+ self
+ }
+
+ /// Pass JSON as the request body.
+ ///
+ /// # Mime
+ ///
+ /// The encoding is set to `application/json`.
+ ///
+ /// # Errors
+ ///
+ /// This method will return an error if the provided data could not be serialized to JSON.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # use serde::{Deserialize, Serialize};
+ /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
+ /// # struct Capabilities { http: crux_http::Http<Event> }
+ /// # fn update(caps: &Capabilities) {
+ /// #[derive(Deserialize, Serialize)]
+ /// struct Ip {
+ /// ip: String
+ /// }
+ ///
+ /// let data = &Ip { ip: "129.0.0.1".into() };
+ /// caps.http
+ /// .post("https://httpbin.org/post")
+ /// .body_json(data)
+ /// .expect("could not serialize body")
+ /// .send(Event::ReceiveResponse)
+ /// # }
+ /// ```
+ pub fn body_json(self, json: &impl Serialize) -> crate::Result<Self> {
+ Ok(self.body(Body::from_json(json)?))
+ }
+
+ /// Pass a string as the request body.
+ ///
+ /// # Mime
+ ///
+ /// The encoding is set to `text/plain; charset=utf-8`.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
+ /// # struct Capabilities { http: crux_http::Http<Event> }
+ /// # fn update(caps: &Capabilities) {
+ /// caps.http
+ /// .post("https://httpbin.org/post")
+ /// .body_string("hello_world".to_string())
+ /// .send(Event::ReceiveResponse)
+ /// # }
+ /// ```
+ pub fn body_string(self, string: String) -> Self {
+ self.body(Body::from_string(string))
+ }
+
+ /// Pass bytes as the request body.
+ ///
+ /// # Mime
+ ///
+ /// The encoding is set to `application/octet-stream`.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
+ /// # struct Capabilities { http: crux_http::Http<Event> }
+ /// # fn update(caps: &Capabilities) {
+ /// caps.http
+ /// .post("https://httpbin.org/post")
+ /// .body_bytes(b"hello_world".to_owned())
+ /// .send(Event::ReceiveResponse)
+ /// # }
+ /// ```
+ pub fn body_bytes(self, bytes: impl AsRef<[u8]>) -> Self {
+ self.body(Body::from(bytes.as_ref()))
+ }
+
+ /// Set the URL querystring.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # use serde::{Deserialize, Serialize};
+ /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
+ /// # struct Capabilities { http: crux_http::Http<Event> }
+ /// # fn update(caps: &Capabilities) {
+ /// #[derive(Serialize, Deserialize)]
+ /// struct Index {
+ /// page: u32
+ /// }
+ ///
+ /// let query = Index { page: 2 };
+ /// caps.http
+ /// .post("https://httpbin.org/post")
+ /// .query(&query)
+ /// .expect("could not serialize query string")
+ /// .send(Event::ReceiveResponse)
+ /// # }
+ /// ```
+ pub fn query(mut self, query: &impl Serialize) -> std::result::Result<Self, Error> {
+ self.req.as_mut().unwrap().set_query(query)?;
+
+ Ok(self)
+ }
+
+ /// Push middleware onto a per-request middleware stack.
+ ///
+ /// **Important**: Setting per-request middleware incurs extra allocations.
+ /// Creating a `Client` with middleware is recommended.
+ ///
+ /// Client middleware is run before per-request middleware.
+ ///
+ /// See the [middleware] submodule for more information on middleware.
+ ///
+ /// [middleware]: ../middleware/index.html
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
+ /// # struct Capabilities { http: crux_http::Http<Event> }
+ /// # fn update(caps: &Capabilities) {
+ ///
+ /// caps.http
+ /// .get("https://httpbin.org/redirect/2")
+ /// .middleware(crux_http::middleware::Redirect::default())
+ /// .send(Event::ReceiveResponse)
+ /// # }
+ /// ```
+ pub fn middleware(mut self, middleware: impl Middleware) -> Self {
+ self.req.as_mut().unwrap().middleware(middleware);
+ self
+ }
+
+ /// Return the constructed `Request`.
+ pub fn build(self) -> Request {
+ self.req.unwrap()
+ }
+
+ /// Decode a String from the response body prior to dispatching it to the apps `update`
+ /// function
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # struct Capabilities { http: crux_http::Http<Event> }
+ /// enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<String>>) }
+ ///
+ /// # fn update(caps: &Capabilities) {
+ /// caps.http
+ /// .post("https://httpbin.org/json")
+ /// .expect_string()
+ /// .send(Event::ReceiveResponse)
+ /// # }
+ /// ```
+ pub fn expect_string(self) -> RequestBuilder<Event, String> {
+ let expectation = Box::<ExpectString>::default();
+ RequestBuilder {
+ req: self.req,
+ cap_or_client: self.cap_or_client,
+ phantom: PhantomData,
+ expectation,
+ }
+ }
+
+ /// Decode a `T` from a JSON response body prior to dispatching it to the apps `update`
+ /// function
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # use serde::{Deserialize, Serialize};
+ /// # struct Capabilities { http: crux_http::Http<Event> }
+ /// #[derive(Deserialize)]
+ /// struct Response {
+ /// slideshow: Slideshow
+ /// }
+ ///
+ /// #[derive(Deserialize)]
+ /// struct Slideshow {
+ /// author: String
+ /// }
+ ///
+ /// enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Slideshow>>) }
+ ///
+ /// # fn update(caps: &Capabilities) {
+ /// caps.http
+ /// .post("https://httpbin.org/json")
+ /// .expect_json::<Slideshow>()
+ /// .send(Event::ReceiveResponse)
+ /// # }
+ /// ```
+ pub fn expect_json<T>(self) -> RequestBuilder<Event, T>
+ where
+ T: DeserializeOwned + 'static,
+ {
+ let expectation = Box::<ExpectJson<T>>::default();
+ RequestBuilder {
+ req: self.req,
+ cap_or_client: self.cap_or_client,
+ phantom: PhantomData,
+ expectation,
+ }
+ }
+
+ /// Sends the constructed `Request` and returns its result as an update `Event`
+ ///
+ /// When finished, the response will wrapped in an event using `make_event` and
+ /// dispatched to the app's `update function.
+ pub fn send<F>(self, make_event: F)
+ where
+ F: FnOnce(crate::Result<Response<ExpectBody>>) -> Event + Send + 'static,
+ {
+ let CapOrClient::Capability(capability) = self.cap_or_client else {
+ panic!("Called RequestBuilder::send in a middleware context");
+ };
+ let request = self.req;
+
+ let ctx = capability.context.clone();
+ ctx.spawn(async move {
+ let result = capability.client.send(request.unwrap()).await;
+
+ let resp = match result {
+ Ok(resp) => resp,
+ Err(e) => {
+ capability.context.update_app(make_event(Err(e)));
+ return;
+ }
+ };
+
+ // Note: doing an unwrap here, but since we're reading bytes from
+ // a prepopulated buffer there should be no way for this to fail
+ // currently.
+ let resp = Response::<Vec<u8>>::new(resp).await.unwrap();
+
+ capability
+ .context
+ .update_app(make_event(self.expectation.decode(resp)));
+ });
+ }
+}
+
+impl std::future::IntoFuture for RequestBuilder<()> {
+ type Output = Result<ResponseAsync>;
+
+ type IntoFuture = BoxFuture<'static, Result<ResponseAsync>>;
+
+ fn into_future(self) -> Self::IntoFuture {
+ Box::pin(async move {
+ let CapOrClient::Client(client) = self.cap_or_client else {
+ panic!("Tried to await a RequestBuilder in a non-middleware context");
+ };
+
+ client.send(self.req.unwrap()).await
+ })
+ }
+}
+
+impl<Ev> fmt::Debug for RequestBuilder<Ev> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ fmt::Debug::fmt(&self.req, f)
+ }
+}
+
+// impl From<RequestBuilder<Ev>> for Request {
+// /// Converts a `crux_http::RequestBuilder` to a `crux_http::Request`.
+// fn from(builder: RequestBuilder) -> Request {
+// builder.build()
+// }
+// }
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +
use crate::http::Error;
+
+use std::fmt;
+use std::io;
+
+/// An error occurred while decoding a response body to a string.
+///
+/// The error carries the encoding that was used to attempt to decode the body, and the raw byte
+/// contents of the body. This can be used to treat un-decodable bodies specially or to implement a
+/// fallback parsing strategy.
+#[derive(Clone)]
+pub struct DecodeError {
+ /// The name of the encoding that was used to try to decode the input.
+ pub encoding: String,
+ /// The input data as bytes.
+ pub data: Vec<u8>,
+}
+
+// Override debug output so you don't get each individual byte in `data` printed out separately,
+// because it can be many megabytes large. The actual content is not that interesting anyways
+// and can be accessed manually if it is required.
+impl fmt::Debug for DecodeError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("DecodeError")
+ .field("encoding", &self.encoding)
+ // Perhaps we can output the first N bytes of the response in the future
+ .field("data", &format!("{} bytes", self.data.len()))
+ .finish()
+ }
+}
+
+impl fmt::Display for DecodeError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "could not decode body as {}", &self.encoding)
+ }
+}
+
+impl std::error::Error for DecodeError {}
+
+/// Check if an encoding label refers to the UTF-8 encoding.
+#[allow(dead_code)]
+fn is_utf8_encoding(encoding_label: &str) -> bool {
+ encoding_label.eq_ignore_ascii_case("utf-8")
+ || encoding_label.eq_ignore_ascii_case("utf8")
+ || encoding_label.eq_ignore_ascii_case("unicode-1-1-utf-8")
+}
+
+/// Decode a response body as utf-8.
+///
+/// # Errors
+///
+/// If the body cannot be decoded as utf-8, this function returns an `std::io::Error` of kind
+/// `std::io::ErrorKind::InvalidData`, carrying a `DecodeError` struct.
+#[cfg(not(feature = "encoding"))]
+pub fn decode_body(bytes: Vec<u8>, content_encoding: Option<&str>) -> Result<String, Error> {
+ if is_utf8_encoding(content_encoding.unwrap_or("utf-8")) {
+ Ok(String::from_utf8(bytes).map_err(|err| {
+ let err = DecodeError {
+ encoding: "utf-8".to_string(),
+ data: err.into_bytes(),
+ };
+ io::Error::new(io::ErrorKind::InvalidData, err)
+ })?)
+ } else {
+ let err = DecodeError {
+ encoding: "utf-8".to_string(),
+ data: bytes,
+ };
+ Err(io::Error::new(io::ErrorKind::InvalidData, err).into())
+ }
+}
+
+/// Decode a response body as the given content type.
+///
+/// If the input bytes are valid utf-8, this does not make a copy.
+///
+/// # Errors
+///
+/// If an unsupported encoding is requested, or the body does not conform to the requested
+/// encoding, this function returns an `std::io::Error` of kind `std::io::ErrorKind::InvalidData`,
+/// carrying a `DecodeError` struct.
+#[cfg(all(feature = "encoding", not(target_arch = "wasm32")))]
+pub fn decode_body(bytes: Vec<u8>, content_encoding: Option<&str>) -> Result<String, Error> {
+ use encoding_rs::Encoding;
+ use std::borrow::Cow;
+
+ let content_encoding = content_encoding.unwrap_or("utf-8");
+ if let Some(encoding) = Encoding::for_label(content_encoding.as_bytes()) {
+ let (decoded, encoding_used, failed) = encoding.decode(&bytes);
+ if failed {
+ let err = DecodeError {
+ encoding: encoding_used.name().into(),
+ data: bytes,
+ };
+ Err(io::Error::new(io::ErrorKind::InvalidData, err).into())
+ } else {
+ Ok(match decoded {
+ // If encoding_rs returned a `Cow::Borrowed`, the bytes are guaranteed to be valid
+ // UTF-8, by virtue of being UTF-8 or being in the subset of ASCII that is the same
+ // in UTF-8.
+ Cow::Borrowed(_) => unsafe { String::from_utf8_unchecked(bytes) },
+ Cow::Owned(string) => string,
+ })
+ }
+ } else {
+ let err = DecodeError {
+ encoding: content_encoding.to_string(),
+ data: bytes,
+ };
+ Err(io::Error::new(io::ErrorKind::InvalidData, err).into())
+ }
+}
+
+/// Decode a response body as the given content type.
+///
+/// This always makes a copy. (It could be optimized to avoid the copy if the encoding is utf-8.)
+///
+/// # Errors
+///
+/// If an unsupported encoding is requested, or the body does not conform to the requested
+/// encoding, this function returns an `std::io::Error` of kind `std::io::ErrorKind::InvalidData`,
+/// carrying a `DecodeError` struct.
+#[cfg(all(feature = "encoding", target_arch = "wasm32"))]
+pub fn decode_body(mut bytes: Vec<u8>, content_encoding: Option<&str>) -> Result<String, Error> {
+ use web_sys::TextDecoder;
+
+ // Encoding names are always valid ASCII, so we can avoid including casing mapping tables
+ let content_encoding = content_encoding.unwrap_or("utf-8").to_ascii_lowercase();
+ if is_utf8_encoding(&content_encoding) {
+ return String::from_utf8(bytes)
+ .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err).into());
+ }
+
+ let decoder = TextDecoder::new_with_label(&content_encoding).unwrap();
+
+ Ok(decoder.decode_with_u8_array(&mut bytes).map_err(|_| {
+ let err = DecodeError {
+ encoding: content_encoding.to_string(),
+ data: bytes,
+ };
+ io::Error::new(io::ErrorKind::InvalidData, err)
+ })?)
+}
+
+#[cfg(test)]
+mod decode_tests {
+ use super::decode_body;
+
+ #[test]
+ fn utf8() {
+ let input = "Rød grød med fløde";
+ assert_eq!(
+ decode_body(input.as_bytes().to_vec(), Some("utf-8")).unwrap(),
+ input,
+ "Parses utf-8"
+ );
+ }
+
+ #[test]
+ fn default_utf8() {
+ let input = "Rød grød med fløde";
+ assert_eq!(
+ decode_body(input.as_bytes().to_vec(), None).unwrap(),
+ input,
+ "Defaults to utf-8"
+ );
+ }
+
+ #[test]
+ fn euc_kr() {
+ let input = vec![
+ 0xb3, 0xbb, 0x20, 0xc7, 0xb0, 0xc0, 0xb8, 0xb7, 0xce, 0x20, 0xb5, 0xb9, 0xbe, 0xc6,
+ 0xbf, 0xc0, 0xb6, 0xf3, 0x2c, 0x20, 0xb3, 0xbb, 0x20, 0xbe, 0xc8, 0xbf, 0xa1, 0xbc,
+ 0xad, 0x20, 0xc0, 0xe1, 0xb5, 0xe9, 0xb0, 0xc5, 0xb6, 0xf3,
+ ];
+
+ let result = decode_body(input, Some("euc-kr"));
+ if cfg!(feature = "encoding") {
+ assert_eq!(result.unwrap(), "내 품으로 돌아오라, 내 안에서 잠들거라");
+ } else {
+ assert!(result.is_err(), "Only utf-8 is supported");
+ }
+ }
+}
+
mod decode;
+#[allow(clippy::module_inception)]
+mod response;
+mod response_async;
+
+pub use self::{response::Response, response_async::ResponseAsync};
+
+pub(crate) fn new_headers() -> crate::http::Headers {
+ // http-types doesn't seem to let you construct a Headers, very annoying.
+ // So here's a horrible hack to do it.
+ crate::http::Request::new(crate::http::Method::Get, "https://thisisveryannoying.com")
+ .as_ref()
+ .clone()
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +
use super::{decode::decode_body, new_headers};
+use crate::http::{
+ self,
+ headers::{self, HeaderName, HeaderValues, ToHeaderValues},
+ Mime, StatusCode, Version,
+};
+
+use http::{headers::CONTENT_TYPE, Headers};
+use serde::de::DeserializeOwned;
+
+use std::fmt;
+use std::ops::Index;
+
+/// An HTTP Response that will be passed to in a message to an apps update function
+#[derive(Clone, serde::Serialize, serde::Deserialize)]
+pub struct Response<Body> {
+ version: Option<http::Version>,
+ status: http::StatusCode,
+ #[serde(with = "header_serde")]
+ headers: Headers,
+ body: Option<Body>,
+}
+
+impl<Body> Response<Body> {
+ /// Create a new instance.
+ pub(crate) async fn new(mut res: super::ResponseAsync) -> crate::Result<Response<Vec<u8>>> {
+ let body = res.body_bytes().await?;
+
+ let headers: &Headers = res.as_ref();
+ let headers = headers.clone();
+
+ Ok(Response {
+ status: res.status(),
+ headers,
+ version: res.version(),
+ body: Some(body),
+ })
+ }
+
+ /// Get the HTTP status code.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # let res = crux_http::testing::ResponseBuilder::ok().build();
+ /// assert_eq!(res.status(), 200);
+ /// ```
+ pub fn status(&self) -> StatusCode {
+ self.status
+ }
+
+ /// Get the HTTP protocol version.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # let res = crux_http::testing::ResponseBuilder::ok().build();
+ /// use crux_http::http::Version;
+ /// assert_eq!(res.version(), Some(Version::Http1_1));
+ /// ```
+ pub fn version(&self) -> Option<Version> {
+ self.version
+ }
+
+ /// Get a header.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # let res = crux_http::testing::ResponseBuilder::ok()
+ /// # .header("Content-Length", "1")
+ /// # .build();
+ /// assert!(res.header("Content-Length").is_some());
+ /// ```
+ pub fn header(&self, name: impl Into<HeaderName>) -> Option<&HeaderValues> {
+ self.headers.get(name)
+ }
+
+ /// Get an HTTP header mutably.
+ pub fn header_mut(&mut self, name: impl Into<HeaderName>) -> Option<&mut HeaderValues> {
+ self.headers.get_mut(name)
+ }
+
+ /// Remove a header.
+ pub fn remove_header(&mut self, name: impl Into<HeaderName>) -> Option<HeaderValues> {
+ self.headers.remove(name)
+ }
+
+ /// Insert an HTTP header.
+ pub fn insert_header(&mut self, key: impl Into<HeaderName>, value: impl ToHeaderValues) {
+ self.headers.insert(key, value);
+ }
+
+ /// Append an HTTP header.
+ pub fn append_header(&mut self, key: impl Into<HeaderName>, value: impl ToHeaderValues) {
+ self.headers.append(key, value);
+ }
+
+ /// An iterator visiting all header pairs in arbitrary order.
+ #[must_use]
+ pub fn iter(&self) -> headers::Iter<'_> {
+ self.headers.iter()
+ }
+
+ /// An iterator visiting all header pairs in arbitrary order, with mutable references to the
+ /// values.
+ #[must_use]
+ pub fn iter_mut(&mut self) -> headers::IterMut<'_> {
+ self.headers.iter_mut()
+ }
+
+ /// An iterator visiting all header names in arbitrary order.
+ #[must_use]
+ pub fn header_names(&self) -> headers::Names<'_> {
+ self.headers.names()
+ }
+
+ /// An iterator visiting all header values in arbitrary order.
+ #[must_use]
+ pub fn header_values(&self) -> headers::Values<'_> {
+ self.headers.values()
+ }
+
+ /// Get the response content type as a `Mime`.
+ ///
+ /// Gets the `Content-Type` header and parses it to a `Mime` type.
+ ///
+ /// [Read more on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types)
+ ///
+ /// # Panics
+ ///
+ /// This method will panic if an invalid MIME type was set as a header.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # let res = crux_http::testing::ResponseBuilder::ok()
+ /// # .header("Content-Type", "application/json")
+ /// # .build();
+ /// use crux_http::http::mime;
+ /// assert_eq!(res.content_type(), Some(mime::JSON));
+ /// ```
+ pub fn content_type(&self) -> Option<Mime> {
+ self.header(CONTENT_TYPE)?.last().as_str().parse().ok()
+ }
+
+ pub fn body(&self) -> Option<&Body> {
+ self.body.as_ref()
+ }
+
+ pub fn take_body(&mut self) -> Option<Body> {
+ self.body.take()
+ }
+
+ pub fn with_body<NewBody>(self, body: NewBody) -> Response<NewBody> {
+ Response {
+ body: Some(body),
+ headers: self.headers,
+ status: self.status,
+ version: self.version,
+ }
+ }
+}
+
+impl Response<Vec<u8>> {
+ pub(crate) fn new_with_status(status: http::StatusCode) -> Self {
+ let headers = new_headers();
+
+ Response {
+ status,
+ headers,
+ version: None,
+ body: None,
+ }
+ }
+
+ /// Reads the entire request body into a byte buffer.
+ ///
+ /// This method can be called after the body has already been read, but will
+ /// produce an empty buffer.
+ ///
+ /// # Errors
+ ///
+ /// Any I/O error encountered while reading the body is immediately returned
+ /// as an `Err`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # fn main() -> crux_http::Result<()> {
+ /// # let mut res = crux_http::testing::ResponseBuilder::ok()
+ /// # .header("Content-Type", "application/json")
+ /// # .body(vec![0u8, 1])
+ /// # .build();
+ /// let bytes: Vec<u8> = res.body_bytes()?;
+ /// # Ok(()) }
+ /// ```
+ pub fn body_bytes(&mut self) -> crate::Result<Vec<u8>> {
+ self.body
+ .take()
+ .ok_or_else(|| crate::Error::new(Some(self.status()), "Body had no bytes"))
+ }
+
+ /// Reads the entire response body into a string.
+ ///
+ /// This method can be called after the body has already been read, but will
+ /// produce an empty buffer.
+ ///
+ /// # Encodings
+ ///
+ /// If the "encoding" feature is enabled, this method tries to decode the body
+ /// with the encoding that is specified in the Content-Type header. If the header
+ /// does not specify an encoding, UTF-8 is assumed. If the "encoding" feature is
+ /// disabled, Surf only supports reading UTF-8 response bodies. The "encoding"
+ /// feature is enabled by default.
+ ///
+ /// # Errors
+ ///
+ /// Any I/O error encountered while reading the body is immediately returned
+ /// as an `Err`.
+ ///
+ /// If the body cannot be interpreted because the encoding is unsupported or
+ /// incorrect, an `Err` is returned.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # fn main() -> crux_http::Result<()> {
+ /// # let mut res = crux_http::testing::ResponseBuilder::ok()
+ /// # .header("Content-Type", "application/json")
+ /// # .body("hello".to_string().into_bytes())
+ /// # .build();
+ /// let string: String = res.body_string()?;
+ /// assert_eq!(string, "hello");
+ /// # Ok(()) }
+ /// ```
+ pub fn body_string(&mut self) -> crate::Result<String> {
+ let bytes = self.body_bytes()?;
+
+ let mime = self.content_type();
+ let claimed_encoding = mime
+ .as_ref()
+ .and_then(|mime| mime.param("charset"))
+ .map(|name| name.to_string());
+ Ok(decode_body(bytes, claimed_encoding.as_deref())?)
+ }
+
+ /// Reads and deserialized the entire request body from json.
+ ///
+ /// # Errors
+ ///
+ /// Any I/O error encountered while reading the body is immediately returned
+ /// as an `Err`.
+ ///
+ /// If the body cannot be interpreted as valid json for the target type `T`,
+ /// an `Err` is returned.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use serde::{Deserialize, Serialize};
+ /// # fn main() -> crux_http::Result<()> {
+ /// # let mut res = crux_http::testing::ResponseBuilder::ok()
+ /// # .header("Content-Type", "application/json")
+ /// # .body("{\"ip\": \"127.0.0.1\"}".to_string().into_bytes())
+ /// # .build();
+ /// #[derive(Deserialize, Serialize)]
+ /// struct Ip {
+ /// ip: String
+ /// }
+ ///
+ /// let Ip { ip } = res.body_json()?;
+ /// assert_eq!(ip, "127.0.0.1");
+ /// # Ok(()) }
+ /// ```
+ pub fn body_json<T: DeserializeOwned>(&mut self) -> crate::Result<T> {
+ let body_bytes = self.body_bytes()?;
+ serde_json::from_slice(&body_bytes).map_err(crate::Error::from)
+ }
+}
+
+impl<Body> AsRef<http::Headers> for Response<Body> {
+ fn as_ref(&self) -> &http::Headers {
+ &self.headers
+ }
+}
+
+impl<Body> AsMut<http::Headers> for Response<Body> {
+ fn as_mut(&mut self) -> &mut http::Headers {
+ &mut self.headers
+ }
+}
+
+impl<Body> fmt::Debug for Response<Body> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("Response")
+ .field("version", &self.version)
+ .field("status", &self.status)
+ .field("headers", &self.headers)
+ .finish_non_exhaustive()
+ }
+}
+
+impl<Body> Index<HeaderName> for Response<Body> {
+ type Output = HeaderValues;
+
+ /// Returns a reference to the value corresponding to the supplied name.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the name is not present in `Response`.
+ #[inline]
+ fn index(&self, name: HeaderName) -> &HeaderValues {
+ &self.headers[name]
+ }
+}
+
+impl<Body> Index<&str> for Response<Body> {
+ type Output = HeaderValues;
+
+ /// Returns a reference to the value corresponding to the supplied name.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the name is not present in `Response`.
+ #[inline]
+ fn index(&self, name: &str) -> &HeaderValues {
+ &self.headers[name]
+ }
+}
+
+impl<Body> PartialEq for Response<Body>
+where
+ Body: PartialEq,
+{
+ fn eq(&self, other: &Self) -> bool {
+ self.version == other.version
+ && self.status == other.status
+ && self.headers.iter().zip(other.headers.iter()).all(
+ |((lhs_name, lhs_values), (rhs_name, rhs_values))| {
+ lhs_name == rhs_name
+ && lhs_values
+ .iter()
+ .zip(rhs_values.iter())
+ .all(|(lhs, rhs)| lhs == rhs)
+ },
+ )
+ && self.body == other.body
+ }
+}
+
+impl<Body> Eq for Response<Body> where Body: Eq {}
+
+mod header_serde {
+ use crate::{
+ http::{self, Headers},
+ response::new_headers,
+ };
+ use http::headers::HeaderName;
+ use serde::{de::Error, Deserializer, Serializer};
+
+ pub fn serialize<S>(headers: &Headers, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.collect_map(headers.iter().map(|(name, values)| {
+ (
+ name.as_str(),
+ values.iter().map(|v| v.as_str()).collect::<Vec<_>>(),
+ )
+ }))
+ }
+
+ pub fn deserialize<'de, D>(deserializer: D) -> Result<Headers, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let strs = <Vec<(String, Vec<String>)> as serde::Deserialize>::deserialize(deserializer)?;
+
+ let mut headers = new_headers();
+
+ for (name, values) in strs {
+ let name = HeaderName::from_string(name).map_err(D::Error::custom)?;
+ for value in values {
+ headers.append(&name, value);
+ }
+ }
+
+ Ok(headers)
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +
use crate::http::{
+ self,
+ headers::{self, HeaderName, HeaderValues, ToHeaderValues},
+ Body, Mime, StatusCode, Version,
+};
+
+use futures_util::io::AsyncRead;
+use serde::de::DeserializeOwned;
+
+use std::fmt;
+use std::io;
+use std::ops::Index;
+use std::pin::Pin;
+use std::task::{Context, Poll};
+
+use super::decode::decode_body;
+
+pin_project_lite::pin_project! {
+ /// An HTTP response that exposes async methods, for use inside middleware.
+ ///
+ /// If you're not writing middleware you'll never need to interact with
+ /// this type and can probably ignore it.
+ pub struct ResponseAsync {
+ #[pin]
+ res: crate::http::Response,
+ }
+}
+
+impl ResponseAsync {
+ /// Create a new instance.
+ pub(crate) fn new(res: http::Response) -> Self {
+ Self { res }
+ }
+
+ /// Get the HTTP status code.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # use crux_http::client::Client;
+ /// # async fn middleware(client: Client) -> crux_http::Result<()> {
+ /// let res = client.get("https://httpbin.org/get").await?;
+ /// assert_eq!(res.status(), 200);
+ /// # Ok(()) }
+ /// ```
+ pub fn status(&self) -> StatusCode {
+ self.res.status()
+ }
+
+ /// Get the HTTP protocol version.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # use crux_http::client::Client;
+ /// # async fn middleware(client: Client) -> crux_http::Result<()> {
+ /// use crux_http::http::Version;
+ ///
+ /// let res = client.get("https://httpbin.org/get").await?;
+ /// assert_eq!(res.version(), Some(Version::Http1_1));
+ /// # Ok(()) }
+ /// ```
+ pub fn version(&self) -> Option<Version> {
+ self.res.version()
+ }
+
+ /// Get a header.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # use crux_http::client::Client;
+ /// # async fn middleware(client: Client) -> crux_http::Result<()> {
+ /// let res = client.get("https://httpbin.org/get").await?;
+ /// assert!(res.header("Content-Length").is_some());
+ /// # Ok(()) }
+ /// ```
+ pub fn header(&self, name: impl Into<HeaderName>) -> Option<&HeaderValues> {
+ self.res.header(name)
+ }
+
+ /// Get an HTTP header mutably.
+ pub fn header_mut(&mut self, name: impl Into<HeaderName>) -> Option<&mut HeaderValues> {
+ self.res.header_mut(name)
+ }
+
+ /// Remove a header.
+ pub fn remove_header(&mut self, name: impl Into<HeaderName>) -> Option<HeaderValues> {
+ self.res.remove_header(name)
+ }
+
+ /// Insert an HTTP header.
+ pub fn insert_header(&mut self, key: impl Into<HeaderName>, value: impl ToHeaderValues) {
+ self.res.insert_header(key, value);
+ }
+
+ /// Append an HTTP header.
+ pub fn append_header(&mut self, key: impl Into<HeaderName>, value: impl ToHeaderValues) {
+ self.res.append_header(key, value);
+ }
+
+ /// An iterator visiting all header pairs in arbitrary order.
+ #[must_use]
+ pub fn iter(&self) -> headers::Iter<'_> {
+ self.res.iter()
+ }
+
+ /// An iterator visiting all header pairs in arbitrary order, with mutable references to the
+ /// values.
+ #[must_use]
+ pub fn iter_mut(&mut self) -> headers::IterMut<'_> {
+ self.res.iter_mut()
+ }
+
+ /// An iterator visiting all header names in arbitrary order.
+ #[must_use]
+ pub fn header_names(&self) -> headers::Names<'_> {
+ self.res.header_names()
+ }
+
+ /// An iterator visiting all header values in arbitrary order.
+ #[must_use]
+ pub fn header_values(&self) -> headers::Values<'_> {
+ self.res.header_values()
+ }
+
+ /// Get a response scoped extension value.
+ #[must_use]
+ pub fn ext<T: Send + Sync + 'static>(&self) -> Option<&T> {
+ self.res.ext().get()
+ }
+
+ /// Set a response scoped extension value.
+ pub fn insert_ext<T: Send + Sync + 'static>(&mut self, val: T) {
+ self.res.ext_mut().insert(val);
+ }
+
+ /// Get the response content type as a `Mime`.
+ ///
+ /// Gets the `Content-Type` header and parses it to a `Mime` type.
+ ///
+ /// [Read more on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types)
+ ///
+ /// # Panics
+ ///
+ /// This method will panic if an invalid MIME type was set as a header.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # use crux_http::client::Client;
+ /// # async fn middleware(client: Client) -> crux_http::Result<()> {
+ /// use crux_http::http::mime;
+ /// let res = client.get("https://httpbin.org/json").await?;
+ /// assert_eq!(res.content_type(), Some(mime::JSON));
+ /// # Ok(()) }
+ /// ```
+ pub fn content_type(&self) -> Option<Mime> {
+ self.res.content_type()
+ }
+
+ /// Get the length of the body stream, if it has been set.
+ ///
+ /// This value is set when passing a fixed-size object into as the body.
+ /// E.g. a string, or a buffer. Consumers of this API should check this
+ /// value to decide whether to use `Chunked` encoding, or set the
+ /// response length.
+ #[allow(clippy::len_without_is_empty)]
+ pub fn len(&self) -> Option<usize> {
+ self.res.len()
+ }
+
+ /// Returns `true` if the set length of the body stream is zero, `false`
+ /// otherwise.
+ pub fn is_empty(&self) -> Option<bool> {
+ self.res.is_empty()
+ }
+
+ /// Set the body reader.
+ pub fn set_body(&mut self, body: impl Into<Body>) {
+ self.res.set_body(body);
+ }
+
+ /// Take the response body as a `Body`.
+ ///
+ /// This method can be called after the body has already been taken or read,
+ /// but will return an empty `Body`.
+ ///
+ /// Useful for adjusting the whole body, such as in middleware.
+ pub fn take_body(&mut self) -> Body {
+ self.res.take_body()
+ }
+
+ /// Swaps the value of the body with another body, without deinitializing
+ /// either one.
+ pub fn swap_body(&mut self, body: &mut Body) {
+ self.res.swap_body(body)
+ }
+
+ /// Reads the entire request body into a byte buffer.
+ ///
+ /// This method can be called after the body has already been read, but will
+ /// produce an empty buffer.
+ ///
+ /// # Errors
+ ///
+ /// Any I/O error encountered while reading the body is immediately returned
+ /// as an `Err`.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # use crux_http::client::Client;
+ /// # async fn middleware(client: Client) -> crux_http::Result<()> {
+ /// let mut res = client.get("https://httpbin.org/get").await?;
+ /// let bytes: Vec<u8> = res.body_bytes().await?;
+ /// # Ok(()) }
+ /// ```
+ pub async fn body_bytes(&mut self) -> crate::Result<Vec<u8>> {
+ Ok(self.res.body_bytes().await?)
+ }
+
+ /// Reads the entire response body into a string.
+ ///
+ /// This method can be called after the body has already been read, but will
+ /// produce an empty buffer.
+ ///
+ /// # Encodings
+ ///
+ /// If the "encoding" feature is enabled, this method tries to decode the body
+ /// with the encoding that is specified in the Content-Type header. If the header
+ /// does not specify an encoding, UTF-8 is assumed. If the "encoding" feature is
+ /// disabled, Surf only supports reading UTF-8 response bodies. The "encoding"
+ /// feature is enabled by default.
+ ///
+ /// # Errors
+ ///
+ /// Any I/O error encountered while reading the body is immediately returned
+ /// as an `Err`.
+ ///
+ /// If the body cannot be interpreted because the encoding is unsupported or
+ /// incorrect, an `Err` is returned.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # use crux_http::client::Client;
+ /// # async fn middleware(client: Client) -> crux_http::Result<()> {
+ /// let mut res = client.get("https://httpbin.org/get").await?;
+ /// let string: String = res.body_string().await?;
+ /// # Ok(()) }
+ /// ```
+ pub async fn body_string(&mut self) -> crate::Result<String> {
+ let bytes = self.body_bytes().await?;
+ let mime = self.content_type();
+ let claimed_encoding = mime
+ .as_ref()
+ .and_then(|mime| mime.param("charset"))
+ .map(|name| name.to_string());
+ Ok(decode_body(bytes, claimed_encoding.as_deref())?)
+ }
+
+ /// Reads and deserialized the entire request body from json.
+ ///
+ /// # Errors
+ ///
+ /// Any I/O error encountered while reading the body is immediately returned
+ /// as an `Err`.
+ ///
+ /// If the body cannot be interpreted as valid json for the target type `T`,
+ /// an `Err` is returned.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # use serde::{Deserialize, Serialize};
+ /// # use crux_http::client::Client;
+ /// # async fn middleware(client: Client) -> crux_http::Result<()> {
+ /// #[derive(Deserialize, Serialize)]
+ /// struct Ip {
+ /// ip: String
+ /// }
+ ///
+ /// let mut res = client.get("https://api.ipify.org?format=json").await?;
+ /// let Ip { ip } = res.body_json().await?;
+ /// # Ok(()) }
+ /// ```
+ pub async fn body_json<T: DeserializeOwned>(&mut self) -> crate::Result<T> {
+ let body_bytes = self.body_bytes().await?;
+ serde_json::from_slice(&body_bytes).map_err(crate::Error::from)
+ }
+
+ /// Reads and deserialized the entire request body from form encoding.
+ ///
+ /// # Errors
+ ///
+ /// Any I/O error encountered while reading the body is immediately returned
+ /// as an `Err`.
+ ///
+ /// If the body cannot be interpreted as valid json for the target type `T`,
+ /// an `Err` is returned.
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// # use serde::{Deserialize, Serialize};
+ /// # use crux_http::client::Client;
+ /// # async fn middleware(client: Client) -> crux_http::Result<()> {
+ /// #[derive(Deserialize, Serialize)]
+ /// struct Body {
+ /// apples: u32
+ /// }
+ ///
+ /// let mut res = client.get("https://api.example.com/v1/response").await?;
+ /// let Body { apples } = res.body_form().await?;
+ /// # Ok(()) }
+ /// ```
+ pub async fn body_form<T: serde::de::DeserializeOwned>(&mut self) -> crate::Result<T> {
+ Ok(self.res.body_form().await?)
+ }
+}
+
+impl From<http::Response> for ResponseAsync {
+ fn from(response: http::Response) -> Self {
+ Self::new(response)
+ }
+}
+
+#[allow(clippy::from_over_into)]
+impl Into<http::Response> for ResponseAsync {
+ fn into(self) -> http::Response {
+ self.res
+ }
+}
+
+impl AsRef<http::Headers> for ResponseAsync {
+ fn as_ref(&self) -> &http::Headers {
+ self.res.as_ref()
+ }
+}
+
+impl AsMut<http::Headers> for ResponseAsync {
+ fn as_mut(&mut self) -> &mut http::Headers {
+ self.res.as_mut()
+ }
+}
+
+impl AsRef<http::Response> for ResponseAsync {
+ fn as_ref(&self) -> &http::Response {
+ &self.res
+ }
+}
+
+impl AsMut<http::Response> for ResponseAsync {
+ fn as_mut(&mut self) -> &mut http::Response {
+ &mut self.res
+ }
+}
+
+impl AsyncRead for ResponseAsync {
+ fn poll_read(
+ mut self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &mut [u8],
+ ) -> Poll<Result<usize, io::Error>> {
+ Pin::new(&mut self.res).poll_read(cx, buf)
+ }
+}
+
+impl fmt::Debug for ResponseAsync {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("Response")
+ .field("response", &self.res)
+ .finish()
+ }
+}
+
+impl Index<HeaderName> for ResponseAsync {
+ type Output = HeaderValues;
+
+ /// Returns a reference to the value corresponding to the supplied name.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the name is not present in `Response`.
+ #[inline]
+ fn index(&self, name: HeaderName) -> &HeaderValues {
+ &self.res[name]
+ }
+}
+
+impl Index<&str> for ResponseAsync {
+ type Output = HeaderValues;
+
+ /// Returns a reference to the value corresponding to the supplied name.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the name is not present in `Response`.
+ #[inline]
+ fn index(&self, name: &str) -> &HeaderValues {
+ &self.res[name]
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +
use http::headers::{HeaderName, ToHeaderValues};
+
+use crate::http;
+
+use crate::response::Response;
+
+/// Allows users to build an http response.
+///
+/// This is mostly expected to be useful in tests rather than application code.
+pub struct ResponseBuilder<Body> {
+ response: Response<Body>,
+}
+
+impl ResponseBuilder<Vec<u8>> {
+ /// Constructs a new ResponseBuilder with the 200 OK status code.
+ pub fn ok() -> ResponseBuilder<Vec<u8>> {
+ ResponseBuilder::with_status(http::StatusCode::Ok)
+ }
+
+ /// Constructs a new ResponseBuilder with the specified status code.
+ pub fn with_status(status: http::StatusCode) -> ResponseBuilder<Vec<u8>> {
+ let response = Response::new_with_status(status);
+
+ ResponseBuilder { response }
+ }
+}
+
+impl<Body> ResponseBuilder<Body> {
+ /// Sets the body of the Response
+ pub fn body<NewBody>(self, body: NewBody) -> ResponseBuilder<NewBody> {
+ let response = self.response.with_body(body);
+ ResponseBuilder { response }
+ }
+
+ /// Sets a header on the response.
+ pub fn header(mut self, key: impl Into<HeaderName>, value: impl ToHeaderValues) -> Self {
+ self.response.insert_header(key, value);
+ self
+ }
+
+ /// Builds the response
+ pub fn build(self) -> Response<Body> {
+ self.response
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +
//! A basic Key-Value store for use with Crux
+//!
+//! `crux_kv` allows Crux apps to store and retrieve arbitrary data by asking the Shell to
+//! persist the data using platform native capabilities (e.g. disk or web localStorage)
+//!
+//! This is still work in progress and extremely basic.
+use crux_core::capability::{CapabilityContext, Operation};
+use crux_macros::Capability;
+use serde::{Deserialize, Serialize};
+
+/// Supported operations
+#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
+pub enum KeyValueOperation {
+ /// Read bytes stored under a key
+ Read(String),
+ /// Write bytes under a key
+ Write(String, Vec<u8>),
+}
+
+#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
+pub enum KeyValueOutput {
+ // TODO: Add support for errors
+ Read(Option<Vec<u8>>),
+ // TODO: Add support for errors
+ Write(bool),
+}
+
+impl Operation for KeyValueOperation {
+ type Output = KeyValueOutput;
+}
+
+#[derive(Capability)]
+pub struct KeyValue<Ev> {
+ context: CapabilityContext<KeyValueOperation, Ev>,
+}
+
+impl<Ev> KeyValue<Ev>
+where
+ Ev: 'static,
+{
+ pub fn new(context: CapabilityContext<KeyValueOperation, Ev>) -> Self {
+ Self { context }
+ }
+
+ /// Read a value under `key`, will dispatch the event with a
+ /// `KeyValueOutput::Read(Option<Vec<u8>>)` as payload
+ pub fn read<F>(&self, key: &str, make_event: F)
+ where
+ F: Fn(KeyValueOutput) -> Ev + Send + Sync + 'static,
+ {
+ let ctx = self.context.clone();
+ let key = key.to_string();
+ self.context.spawn(async move {
+ let output = ctx.request_from_shell(KeyValueOperation::Read(key)).await;
+
+ ctx.update_app(make_event(output))
+ });
+ }
+
+ /// Set `key` to be the provided `value`. Typically the bytes would be
+ /// a value serialized/deserialized by the app.
+ ///
+ /// Will dispatch the event with a `KeyValueOutput::Write(bool)` as payload
+ pub fn write<F>(&self, key: &str, value: Vec<u8>, make_event: F)
+ where
+ F: Fn(KeyValueOutput) -> Ev + Send + Sync + 'static,
+ {
+ self.context.spawn({
+ let context = self.context.clone();
+ let key = key.to_string();
+ async move {
+ let resp = context
+ .request_from_shell(KeyValueOperation::Write(key, value))
+ .await;
+
+ context.update_app(make_event(resp))
+ }
+ });
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +
use darling::{ast, util, FromDeriveInput, FromField, ToTokens};
+use proc_macro2::TokenStream;
+use proc_macro_error::{abort, OptionExt};
+use quote::quote;
+use syn::{DeriveInput, GenericArgument, Ident, PathArguments, Type};
+
+#[derive(FromDeriveInput, Debug)]
+#[darling(supports(struct_named))]
+struct CapabilityStructReceiver {
+ ident: Ident,
+ data: ast::Data<util::Ignored, CapabilityFieldReceiver>,
+}
+
+#[derive(FromField, Debug)]
+pub struct CapabilityFieldReceiver {
+ ident: Option<Ident>,
+ ty: Type,
+}
+
+impl ToTokens for CapabilityStructReceiver {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ let name = &self.ident;
+ let operation_type = self
+ .data
+ .as_ref()
+ .take_struct()
+ .expect_or_abort("should be a struct")
+ .fields
+ .iter()
+ .find(|f| f.ident.as_ref().unwrap() == "context")
+ .map(|f| first_generic_parameter(&f.ty))
+ .expect_or_abort("could not find a field named `context`");
+
+ tokens.extend(quote! {
+ impl<Ev> crux_core::capability::Capability<Ev> for #name<Ev> {
+ type Operation = #operation_type;
+ type MappedSelf<MappedEv> = #name<MappedEv>;
+
+ fn map_event<F, NewEv>(&self, f: F) -> Self::MappedSelf<NewEv>
+ where
+ F: Fn(NewEv) -> Ev + Send + Sync + Copy + 'static,
+ Ev: 'static,
+ NewEv: 'static,
+ {
+ #name::new(self.context.map_event(f))
+ }
+ }
+ })
+ }
+}
+
+pub(crate) fn capability_impl(input: &DeriveInput) -> TokenStream {
+ let input = match CapabilityStructReceiver::from_derive_input(input) {
+ Ok(v) => v,
+ Err(e) => {
+ return e.write_errors();
+ }
+ };
+
+ quote!(#input)
+}
+
+fn first_generic_parameter(ty: &Type) -> Type {
+ let generic_param = match ty.clone() {
+ Type::Path(mut path) if path.qself.is_none() => {
+ // Get the last segment of the path where the generic parameters should be
+ let last = path.path.segments.last_mut().expect("type has no segments");
+ let type_params = std::mem::take(&mut last.arguments);
+
+ let first_type_parameter = match type_params {
+ PathArguments::AngleBracketed(params) => params.args.first().cloned(),
+ _ => None,
+ };
+
+ // This argument must be a type
+ match first_type_parameter {
+ Some(GenericArgument::Type(t2)) => Some(t2),
+ _ => None,
+ }
+ }
+ _ => None,
+ };
+ let Some(generic_param) = generic_param else {
+ abort!(ty, "context field type should have generic type parameters");
+ };
+ generic_param
+}
+
+#[cfg(test)]
+mod tests {
+ use darling::{FromDeriveInput, FromMeta};
+ use quote::quote;
+ use syn::{parse_str, Type};
+
+ use crate::capability::CapabilityStructReceiver;
+
+ use super::first_generic_parameter;
+
+ #[test]
+ fn test_derive() {
+ let input = r#"
+ #[derive(Capability)]
+ pub struct Render<Ev> {
+ context: CapabilityContext<RenderOperation, Ev>,
+ }
+ "#;
+ let input = parse_str(input).unwrap();
+ let input = CapabilityStructReceiver::from_derive_input(&input).unwrap();
+
+ let actual = quote!(#input);
+
+ insta::assert_snapshot!(pretty_print(&actual), @r###"
+ impl<Ev> crux_core::capability::Capability<Ev> for Render<Ev> {
+ type Operation = RenderOperation;
+ type MappedSelf<MappedEv> = Render<MappedEv>;
+ fn map_event<F, NewEv>(&self, f: F) -> Self::MappedSelf<NewEv>
+ where
+ F: Fn(NewEv) -> Ev + Send + Sync + Copy + 'static,
+ Ev: 'static,
+ NewEv: 'static,
+ {
+ Render::new(self.context.map_event(f))
+ }
+ }
+ "###);
+ }
+
+ #[test]
+ fn test_first_generic_parameter() {
+ let ty = Type::from_string("CapabilityContext<my_mod::MyOperation, Ev>").unwrap();
+
+ let first_param = first_generic_parameter(&ty);
+
+ assert_eq!(
+ quote!(#first_param).to_string(),
+ quote!(my_mod::MyOperation).to_string()
+ );
+ }
+
+ fn pretty_print(ts: &proc_macro2::TokenStream) -> String {
+ let file = syn::parse_file(&ts.to_string()).unwrap();
+ prettyplease::unparse(&file)
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +
use darling::{ast, util, FromDeriveInput, FromField, FromMeta, ToTokens};
+use proc_macro2::{Literal, TokenStream};
+use proc_macro_error::{abort_call_site, OptionExt};
+use quote::{format_ident, quote};
+use std::collections::BTreeMap;
+use syn::{DeriveInput, GenericArgument, Ident, PathArguments, Type};
+
+#[derive(FromDeriveInput, Debug)]
+#[darling(attributes(effect), supports(struct_named))]
+struct EffectStructReceiver {
+ ident: Ident,
+ name: Option<Ident>,
+ app: Option<Type>,
+ data: ast::Data<util::Ignored, EffectFieldReceiver>,
+}
+
+#[derive(FromField, Debug)]
+pub struct EffectFieldReceiver {
+ ident: Option<Ident>,
+ ty: Type,
+}
+
+impl ToTokens for EffectStructReceiver {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ let ident = &self.ident;
+
+ let (effect_name, ffi_effect_name, ffi_effect_rename) = match self.name {
+ Some(ref name) => {
+ let ffi_ef_name = format_ident!("{}Ffi", name);
+ let ffi_ef_rename = Literal::string(&name.to_string());
+
+ (quote!(#name), quote!(#ffi_ef_name), quote!(#ffi_ef_rename))
+ }
+ None => (quote!(Effect), quote!(EffectFfi), quote!("Effect")),
+ };
+
+ let app = match self.app {
+ Some(ref app) => quote!(#app),
+ None => {
+ let x = Type::from_string("App").unwrap();
+ quote!(#x)
+ }
+ };
+
+ let fields = self
+ .data
+ .as_ref()
+ .take_struct()
+ .expect_or_abort("should be a struct")
+ .fields;
+
+ let fields: BTreeMap<Ident, (Type, Ident, Type)> = fields
+ .iter()
+ .map(|f| (f.ident.clone().unwrap(), split_on_generic(&f.ty)))
+ .collect();
+
+ let events: Vec<_> = fields.values().map(|(_, _, t)| t).collect();
+ if !events
+ .windows(2)
+ .all(|win| win[0].to_token_stream().to_string() == win[1].to_token_stream().to_string())
+ {
+ abort_call_site!("all fields should be generic over the same event type");
+ }
+ let event = events
+ .first()
+ .expect_or_abort("Capabilities struct has no fields");
+
+ let mut variants = Vec::new();
+ let mut with_context_fields = Vec::new();
+ let mut ffi_variants = Vec::new();
+ let mut match_arms = Vec::new();
+ let mut filters = Vec::new();
+
+ for (field_name, (capability, variant, event)) in fields.iter() {
+ variants.push(quote! { #variant(::crux_core::Request<<#capability<#event> as ::crux_core::capability::Capability<#event>>::Operation>) });
+ with_context_fields.push(quote! { #field_name: #capability::new(context.specialize(#effect_name::#variant)) });
+ ffi_variants.push(quote! { #variant(<#capability<#event> as ::crux_core::capability::Capability<#event>>::Operation) });
+ match_arms.push(quote! { #effect_name::#variant(request) => request.serialize(#ffi_effect_name::#variant) });
+
+ let filter_fn = format_ident!("is_{}", field_name);
+ let map_fn = format_ident!("into_{}", field_name);
+ filters.push(quote! {
+ impl #effect_name {
+ pub fn #filter_fn(&self) -> bool {
+ if let #effect_name::#variant(_) = self {
+ true
+ } else {
+ false
+ }
+ }
+ pub fn #map_fn(self) -> Option<crux_core::Request<<#capability<#event> as ::crux_core::capability::Capability<#event>>::Operation>> {
+ if let #effect_name::#variant(request) = self {
+ Some(request)
+ } else {
+ None
+ }
+ }
+ }
+ });
+ }
+
+ tokens.extend(quote! {
+ #[derive(Debug)]
+ pub enum #effect_name {
+ #(#variants ,)*
+ }
+
+ #[derive(::serde::Serialize, ::serde::Deserialize)]
+ #[serde(rename = #ffi_effect_rename)]
+ pub enum #ffi_effect_name {
+ #(#ffi_variants ,)*
+ }
+
+ impl ::crux_core::Effect for #effect_name {
+ type Ffi = #ffi_effect_name;
+
+ fn serialize<'out>(self) -> (Self::Ffi, ::crux_core::bridge::ResolveBytes) {
+ match self {
+ #(#match_arms ,)*
+ }
+ }
+ }
+
+ impl ::crux_core::WithContext<#app, #effect_name> for #ident {
+ fn new_with_context(context: ::crux_core::capability::ProtoContext<#effect_name, #event>) -> #ident {
+ #ident {
+ #(#with_context_fields ,)*
+ }
+ }
+ }
+
+ #(#filters)*
+ })
+ }
+}
+
+pub(crate) fn effect_impl(input: &DeriveInput) -> TokenStream {
+ let input = match EffectStructReceiver::from_derive_input(input) {
+ Ok(v) => v,
+ Err(e) => {
+ return e.write_errors();
+ }
+ };
+
+ quote!(#input)
+}
+
+fn split_on_generic(ty: &Type) -> (Type, Ident, Type) {
+ let ty = ty.clone();
+ match ty {
+ Type::Path(mut path) if path.qself.is_none() => {
+ // Get the last segment of the path where the generic parameter should be
+
+ let last = path.path.segments.last_mut().expect("type has no segments");
+ let type_name = last.ident.clone();
+ let type_params = std::mem::take(&mut last.arguments);
+
+ // It should have only one angle-bracketed param
+ let generic_arg = match type_params {
+ PathArguments::AngleBracketed(params) => params.args.first().cloned(),
+ _ => None,
+ };
+
+ // This argument must be a type
+ match generic_arg {
+ Some(GenericArgument::Type(t2)) => Some((Type::Path(path), type_name, t2)),
+ _ => None,
+ }
+ }
+ _ => None,
+ }
+ .expect_or_abort("capabilities should be generic over a single event type")
+}
+
+#[cfg(test)]
+mod tests {
+ use darling::{FromDeriveInput, FromMeta, ToTokens};
+ use quote::quote;
+ use syn::{parse_str, Type};
+
+ use crate::effect::EffectStructReceiver;
+
+ use super::split_on_generic;
+
+ #[test]
+ fn defaults() {
+ let input = r#"
+ #[derive(Effect)]
+ pub struct Capabilities {
+ pub render: Render<Event>,
+ }
+ "#;
+ let input = parse_str(input).unwrap();
+ let input = EffectStructReceiver::from_derive_input(&input).unwrap();
+
+ let actual = quote!(#input);
+
+ insta::assert_snapshot!(pretty_print(&actual), @r###"
+ #[derive(Debug)]
+ pub enum Effect {
+ Render(
+ ::crux_core::Request<
+ <Render<Event> as ::crux_core::capability::Capability<Event>>::Operation,
+ >,
+ ),
+ }
+ #[derive(::serde::Serialize, ::serde::Deserialize)]
+ #[serde(rename = "Effect")]
+ pub enum EffectFfi {
+ Render(<Render<Event> as ::crux_core::capability::Capability<Event>>::Operation),
+ }
+ impl ::crux_core::Effect for Effect {
+ type Ffi = EffectFfi;
+ fn serialize<'out>(self) -> (Self::Ffi, ::crux_core::bridge::ResolveBytes) {
+ match self {
+ Effect::Render(request) => request.serialize(EffectFfi::Render),
+ }
+ }
+ }
+ impl ::crux_core::WithContext<App, Effect> for Capabilities {
+ fn new_with_context(
+ context: ::crux_core::capability::ProtoContext<Effect, Event>,
+ ) -> Capabilities {
+ Capabilities {
+ render: Render::new(context.specialize(Effect::Render)),
+ }
+ }
+ }
+ impl Effect {
+ pub fn is_render(&self) -> bool {
+ if let Effect::Render(_) = self { true } else { false }
+ }
+ pub fn into_render(
+ self,
+ ) -> Option<
+ crux_core::Request<
+ <Render<Event> as ::crux_core::capability::Capability<Event>>::Operation,
+ >,
+ > {
+ if let Effect::Render(request) = self { Some(request) } else { None }
+ }
+ }
+ "###);
+ }
+
+ #[test]
+ fn split_event_types_preserves_path() {
+ let ty = Type::from_string("crux_core::render::Render<Event>").unwrap();
+
+ let (actual_type, actual_ident, actual_event) = split_on_generic(&ty);
+
+ assert_eq!(
+ quote!(#actual_type).to_string(),
+ quote!(crux_core::render::Render).to_string()
+ );
+
+ assert_eq!(
+ quote!(#actual_ident).to_string(),
+ quote!(Render).to_string()
+ );
+
+ assert_eq!(quote!(#actual_event).to_string(), quote!(Event).to_string());
+ }
+
+ #[test]
+ fn full() {
+ let input = r#"
+ #[derive(Effect)]
+ #[effect(name = "MyEffect", app = "MyApp")]
+ pub struct MyCapabilities {
+ pub http: crux_http::Http<MyEvent>,
+ pub key_value: KeyValue<MyEvent>,
+ pub platform: Platform<MyEvent>,
+ pub render: Render<MyEvent>,
+ pub time: Time<MyEvent>,
+ }
+ "#;
+ let input = parse_str(input).unwrap();
+ let input = EffectStructReceiver::from_derive_input(&input).unwrap();
+
+ let actual = quote!(#input);
+
+ insta::assert_snapshot!(pretty_print(&actual), @r###"
+ #[derive(Debug)]
+ pub enum MyEffect {
+ Http(
+ ::crux_core::Request<
+ <crux_http::Http<
+ MyEvent,
+ > as ::crux_core::capability::Capability<MyEvent>>::Operation,
+ >,
+ ),
+ KeyValue(
+ ::crux_core::Request<
+ <KeyValue<
+ MyEvent,
+ > as ::crux_core::capability::Capability<MyEvent>>::Operation,
+ >,
+ ),
+ Platform(
+ ::crux_core::Request<
+ <Platform<
+ MyEvent,
+ > as ::crux_core::capability::Capability<MyEvent>>::Operation,
+ >,
+ ),
+ Render(
+ ::crux_core::Request<
+ <Render<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation,
+ >,
+ ),
+ Time(
+ ::crux_core::Request<
+ <Time<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation,
+ >,
+ ),
+ }
+ #[derive(::serde::Serialize, ::serde::Deserialize)]
+ #[serde(rename = "MyEffect")]
+ pub enum MyEffectFfi {
+ Http(
+ <crux_http::Http<
+ MyEvent,
+ > as ::crux_core::capability::Capability<MyEvent>>::Operation,
+ ),
+ KeyValue(
+ <KeyValue<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation,
+ ),
+ Platform(
+ <Platform<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation,
+ ),
+ Render(<Render<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation),
+ Time(<Time<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation),
+ }
+ impl ::crux_core::Effect for MyEffect {
+ type Ffi = MyEffectFfi;
+ fn serialize<'out>(self) -> (Self::Ffi, ::crux_core::bridge::ResolveBytes) {
+ match self {
+ MyEffect::Http(request) => request.serialize(MyEffectFfi::Http),
+ MyEffect::KeyValue(request) => request.serialize(MyEffectFfi::KeyValue),
+ MyEffect::Platform(request) => request.serialize(MyEffectFfi::Platform),
+ MyEffect::Render(request) => request.serialize(MyEffectFfi::Render),
+ MyEffect::Time(request) => request.serialize(MyEffectFfi::Time),
+ }
+ }
+ }
+ impl ::crux_core::WithContext<MyApp, MyEffect> for MyCapabilities {
+ fn new_with_context(
+ context: ::crux_core::capability::ProtoContext<MyEffect, MyEvent>,
+ ) -> MyCapabilities {
+ MyCapabilities {
+ http: crux_http::Http::new(context.specialize(MyEffect::Http)),
+ key_value: KeyValue::new(context.specialize(MyEffect::KeyValue)),
+ platform: Platform::new(context.specialize(MyEffect::Platform)),
+ render: Render::new(context.specialize(MyEffect::Render)),
+ time: Time::new(context.specialize(MyEffect::Time)),
+ }
+ }
+ }
+ impl MyEffect {
+ pub fn is_http(&self) -> bool {
+ if let MyEffect::Http(_) = self { true } else { false }
+ }
+ pub fn into_http(
+ self,
+ ) -> Option<
+ crux_core::Request<
+ <crux_http::Http<
+ MyEvent,
+ > as ::crux_core::capability::Capability<MyEvent>>::Operation,
+ >,
+ > {
+ if let MyEffect::Http(request) = self { Some(request) } else { None }
+ }
+ }
+ impl MyEffect {
+ pub fn is_key_value(&self) -> bool {
+ if let MyEffect::KeyValue(_) = self { true } else { false }
+ }
+ pub fn into_key_value(
+ self,
+ ) -> Option<
+ crux_core::Request<
+ <KeyValue<
+ MyEvent,
+ > as ::crux_core::capability::Capability<MyEvent>>::Operation,
+ >,
+ > {
+ if let MyEffect::KeyValue(request) = self { Some(request) } else { None }
+ }
+ }
+ impl MyEffect {
+ pub fn is_platform(&self) -> bool {
+ if let MyEffect::Platform(_) = self { true } else { false }
+ }
+ pub fn into_platform(
+ self,
+ ) -> Option<
+ crux_core::Request<
+ <Platform<
+ MyEvent,
+ > as ::crux_core::capability::Capability<MyEvent>>::Operation,
+ >,
+ > {
+ if let MyEffect::Platform(request) = self { Some(request) } else { None }
+ }
+ }
+ impl MyEffect {
+ pub fn is_render(&self) -> bool {
+ if let MyEffect::Render(_) = self { true } else { false }
+ }
+ pub fn into_render(
+ self,
+ ) -> Option<
+ crux_core::Request<
+ <Render<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation,
+ >,
+ > {
+ if let MyEffect::Render(request) = self { Some(request) } else { None }
+ }
+ }
+ impl MyEffect {
+ pub fn is_time(&self) -> bool {
+ if let MyEffect::Time(_) = self { true } else { false }
+ }
+ pub fn into_time(
+ self,
+ ) -> Option<
+ crux_core::Request<
+ <Time<MyEvent> as ::crux_core::capability::Capability<MyEvent>>::Operation,
+ >,
+ > {
+ if let MyEffect::Time(request) = self { Some(request) } else { None }
+ }
+ }
+ "###);
+ }
+
+ #[test]
+ #[should_panic]
+ fn should_panic_when_multiple_event_types() {
+ let input = r#"
+ #[derive(Effect)]
+ pub struct Capabilities {
+ pub render: Render<MyEvent>,
+ pub time: Time<YourEvent>,
+ }
+ "#;
+ let input = parse_str(input).unwrap();
+ let input = EffectStructReceiver::from_derive_input(&input).unwrap();
+
+ let mut actual = quote!();
+ input.to_tokens(&mut actual);
+ }
+
+ fn pretty_print(ts: &proc_macro2::TokenStream) -> String {
+ let file = syn::parse_file(&ts.to_string()).unwrap();
+ prettyplease::unparse(&file)
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +
use darling::{ast, util, FromDeriveInput, FromField, ToTokens};
+use proc_macro2::TokenStream;
+use proc_macro_error::OptionExt;
+use quote::{format_ident, quote};
+use syn::{DeriveInput, GenericArgument, Ident, PathArguments, Type};
+
+#[derive(FromDeriveInput, Debug)]
+#[darling(supports(struct_named))]
+struct ExportStructReceiver {
+ ident: Ident,
+ name: Option<Ident>,
+ data: ast::Data<util::Ignored, ExportFieldReceiver>,
+}
+
+#[derive(FromField, Debug)]
+pub struct ExportFieldReceiver {
+ ty: Type,
+}
+
+impl ToTokens for ExportStructReceiver {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ let ident = &self.ident;
+
+ let ffi_export_name = match self.name {
+ Some(ref name) => {
+ let ffi_ef_name = format_ident!("{}Ffi", name);
+
+ quote!(#ffi_ef_name)
+ }
+ None => quote!(EffectFfi),
+ };
+
+ let fields = self
+ .data
+ .as_ref()
+ .take_struct()
+ .expect_or_abort("should be a struct")
+ .fields;
+
+ let mut output_type_exports = Vec::new();
+
+ for (capability, event) in fields.iter().map(|f| split_on_generic(&f.ty)) {
+ output_type_exports.push(quote! {
+ generator.register_type::<<#capability<#event> as ::crux_core::capability::Capability<#event>>::Operation>()?;
+ generator
+ .register_type::<<<#capability<#event> as ::crux_core::capability::Capability<#event>>::Operation as ::crux_core::capability::Operation>::Output>()?;
+ });
+ }
+
+ tokens.extend(quote! {
+ impl ::crux_core::typegen::Export for #ident {
+ fn register_types(generator: &mut ::crux_core::typegen::TypeGen) -> ::crux_core::typegen::Result {
+ #(#output_type_exports)*
+
+ generator.register_type::<#ffi_export_name>()?;
+ generator.register_type::<::crux_core::bridge::Request<#ffi_export_name>>()?;
+
+ Ok(())
+ }
+ }
+ })
+ }
+}
+
+pub(crate) fn export_impl(input: &DeriveInput) -> TokenStream {
+ let input = match ExportStructReceiver::from_derive_input(input) {
+ Ok(v) => v,
+ Err(e) => {
+ return e.write_errors();
+ }
+ };
+
+ quote!(#input)
+}
+
+fn split_on_generic(ty: &Type) -> (Type, Type) {
+ let ty = ty.clone();
+ match ty {
+ Type::Path(mut path) if path.qself.is_none() => {
+ // Get the last segment of the path where the generic parameter should be
+
+ let last = path.path.segments.last_mut().expect("type has no segments");
+ let type_params = std::mem::take(&mut last.arguments);
+
+ // It should have only one angle-bracketed param
+ let generic_arg = match type_params {
+ PathArguments::AngleBracketed(params) => params.args.first().cloned(),
+ _ => None,
+ };
+
+ // This argument must be a type
+ match generic_arg {
+ Some(GenericArgument::Type(t2)) => Some((Type::Path(path), t2)),
+ _ => None,
+ }
+ }
+ _ => None,
+ }
+ .expect_or_abort("capabilities should be generic over a single event type")
+}
+
+#[cfg(test)]
+mod tests {
+ use darling::{FromDeriveInput, FromMeta};
+ use quote::quote;
+ use syn::{parse_str, Type};
+
+ use crate::export::ExportStructReceiver;
+
+ use super::split_on_generic;
+
+ #[test]
+ fn defaults() {
+ let input = r#"
+ #[derive(Export)]
+ pub struct Capabilities {
+ pub render: Render<Event>,
+ }
+ "#;
+ let input = parse_str(input).unwrap();
+ let input = ExportStructReceiver::from_derive_input(&input).unwrap();
+
+ let actual = quote!(#input);
+
+ insta::assert_snapshot!(pretty_print(&actual), @r###"
+ impl ::crux_core::typegen::Export for Capabilities {
+ fn register_types(
+ generator: &mut ::crux_core::typegen::TypeGen,
+ ) -> ::crux_core::typegen::Result {
+ generator
+ .register_type::<
+ <Render<Event> as ::crux_core::capability::Capability<Event>>::Operation,
+ >()?;
+ generator
+ .register_type::<
+ <<Render<
+ Event,
+ > as ::crux_core::capability::Capability<
+ Event,
+ >>::Operation as ::crux_core::capability::Operation>::Output,
+ >()?;
+ generator.register_type::<EffectFfi>()?;
+ generator.register_type::<::crux_core::bridge::Request<EffectFfi>>()?;
+ Ok(())
+ }
+ }
+ "###);
+ }
+
+ #[test]
+ fn split_event_types_preserves_path() {
+ let ty = Type::from_string("crux_core::render::Render<Event>").unwrap();
+
+ let (actual_type, actual_event) = split_on_generic(&ty);
+
+ assert_eq!(
+ quote!(#actual_type).to_string(),
+ quote!(crux_core::render::Render).to_string()
+ );
+
+ assert_eq!(quote!(#actual_event).to_string(), quote!(Event).to_string());
+ }
+
+ #[test]
+ fn full() {
+ let input = r#"
+ #[derive(Export)]
+ pub struct MyCapabilities {
+ pub http: crux_http::Http<MyEvent>,
+ pub key_value: KeyValue<MyEvent>,
+ pub platform: Platform<MyEvent>,
+ pub render: Render<MyEvent>,
+ pub time: Time<MyEvent>,
+ }
+ "#;
+ let input = parse_str(input).unwrap();
+ let input = ExportStructReceiver::from_derive_input(&input).unwrap();
+
+ let actual = quote!(#input);
+
+ insta::assert_snapshot!(pretty_print(&actual), @r###"
+ impl ::crux_core::typegen::Export for MyCapabilities {
+ fn register_types(
+ generator: &mut ::crux_core::typegen::TypeGen,
+ ) -> ::crux_core::typegen::Result {
+ generator
+ .register_type::<
+ <crux_http::Http<
+ MyEvent,
+ > as ::crux_core::capability::Capability<MyEvent>>::Operation,
+ >()?;
+ generator
+ .register_type::<
+ <<crux_http::Http<
+ MyEvent,
+ > as ::crux_core::capability::Capability<
+ MyEvent,
+ >>::Operation as ::crux_core::capability::Operation>::Output,
+ >()?;
+ generator
+ .register_type::<
+ <KeyValue<
+ MyEvent,
+ > as ::crux_core::capability::Capability<MyEvent>>::Operation,
+ >()?;
+ generator
+ .register_type::<
+ <<KeyValue<
+ MyEvent,
+ > as ::crux_core::capability::Capability<
+ MyEvent,
+ >>::Operation as ::crux_core::capability::Operation>::Output,
+ >()?;
+ generator
+ .register_type::<
+ <Platform<
+ MyEvent,
+ > as ::crux_core::capability::Capability<MyEvent>>::Operation,
+ >()?;
+ generator
+ .register_type::<
+ <<Platform<
+ MyEvent,
+ > as ::crux_core::capability::Capability<
+ MyEvent,
+ >>::Operation as ::crux_core::capability::Operation>::Output,
+ >()?;
+ generator
+ .register_type::<
+ <Render<
+ MyEvent,
+ > as ::crux_core::capability::Capability<MyEvent>>::Operation,
+ >()?;
+ generator
+ .register_type::<
+ <<Render<
+ MyEvent,
+ > as ::crux_core::capability::Capability<
+ MyEvent,
+ >>::Operation as ::crux_core::capability::Operation>::Output,
+ >()?;
+ generator
+ .register_type::<
+ <Time<
+ MyEvent,
+ > as ::crux_core::capability::Capability<MyEvent>>::Operation,
+ >()?;
+ generator
+ .register_type::<
+ <<Time<
+ MyEvent,
+ > as ::crux_core::capability::Capability<
+ MyEvent,
+ >>::Operation as ::crux_core::capability::Operation>::Output,
+ >()?;
+ generator.register_type::<EffectFfi>()?;
+ generator.register_type::<::crux_core::bridge::Request<EffectFfi>>()?;
+ Ok(())
+ }
+ }
+ "###);
+ }
+
+ fn pretty_print(ts: &proc_macro2::TokenStream) -> String {
+ let file = syn::parse_file(&ts.to_string()).unwrap();
+ prettyplease::unparse(&file)
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +
mod capability;
+mod effect;
+mod export;
+
+use capability::capability_impl;
+use effect::effect_impl;
+use export::export_impl;
+use proc_macro::TokenStream;
+use proc_macro_error::proc_macro_error;
+use syn::parse_macro_input;
+
+#[proc_macro_derive(Effect, attributes(effect))]
+#[proc_macro_error]
+pub fn effect(input: TokenStream) -> TokenStream {
+ effect_impl(&parse_macro_input!(input)).into()
+}
+
+#[proc_macro_derive(Export)]
+#[proc_macro_error]
+pub fn export(input: TokenStream) -> TokenStream {
+ export_impl(&parse_macro_input!(input)).into()
+}
+
+#[proc_macro_derive(Capability)]
+#[proc_macro_error]
+pub fn capability(input: TokenStream) -> TokenStream {
+ capability_impl(&parse_macro_input!(input)).into()
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +
//! TODO mod docs
+
+use crux_core::capability::{CapabilityContext, Operation};
+use crux_macros::Capability;
+use serde::{Deserialize, Serialize};
+
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+pub struct PlatformRequest;
+
+// TODO revisit this
+#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
+pub struct PlatformResponse(pub String);
+
+impl Operation for PlatformRequest {
+ type Output = PlatformResponse;
+}
+
+#[derive(Capability)]
+pub struct Platform<Ev> {
+ context: CapabilityContext<PlatformRequest, Ev>,
+}
+
+impl<Ev> Platform<Ev>
+where
+ Ev: 'static,
+{
+ pub fn new(context: CapabilityContext<PlatformRequest, Ev>) -> Self {
+ Self { context }
+ }
+
+ pub fn get<F>(&self, callback: F)
+ where
+ F: Fn(PlatformResponse) -> Ev + Send + Sync + 'static,
+ {
+ self.context.spawn({
+ let context = self.context.clone();
+ async move {
+ let response = context.request_from_shell(PlatformRequest).await;
+
+ context.update_app(callback(response));
+ }
+ });
+ }
+}
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +
//! Current time access for Crux apps
+//!
+//! Current time (on a wall clock) is considered a side-effect (although if we were to get pedantic, it's
+//! more of a side-cause) by Crux, and has to be obtained externally. This capability provides a simple
+//! interface to do so.
+//!
+//! This is still work in progress and as such very basic. It returns time as an IS08601 string.
+use crux_core::capability::{CapabilityContext, Operation};
+use crux_macros::Capability;
+use serde::{Deserialize, Serialize};
+
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+pub struct TimeRequest;
+
+// TODO revisit this
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+pub struct TimeResponse(pub String);
+
+impl Operation for TimeRequest {
+ type Output = TimeResponse;
+}
+
+/// The Time capability API.
+#[derive(Capability)]
+pub struct Time<Ev> {
+ context: CapabilityContext<TimeRequest, Ev>,
+}
+
+impl<Ev> Time<Ev>
+where
+ Ev: 'static,
+{
+ pub fn new(context: CapabilityContext<TimeRequest, Ev>) -> Self {
+ Self { context }
+ }
+
+ /// Request current time, which will be passed to the app as `TimeResponse`
+ /// wrapped in the event produced by the `callback`.
+ pub fn get<F>(&self, callback: F)
+ where
+ F: Fn(TimeResponse) -> Ev + Send + Sync + 'static,
+ {
+ self.context.spawn({
+ let context = self.context.clone();
+ async move {
+ let response = context.request_from_shell(TimeRequest).await;
+
+ context.update_app(callback(response));
+ }
+ });
+ }
+}
+
fn:
) to \
+ restrict the search to a given item kind.","Accepted kinds are: fn
, mod
, struct
, \
+ enum
, trait
, type
, macro
, \
+ and const
.","Search functions by type signature (e.g., vec -> usize
or \
+ -> vec
or String, enum:Cow -> bool
)","You can look for items with an exact name by putting double quotes around \
+ your request: \"string\"
","Look for functions that accept or return \
+ slices and \
+ arrays by writing \
+ square brackets (e.g., -> [u8]
or [] -> Option
)","Look for items inside another one by searching for a path: vec::Vec
",].map(x=>""+x+"
").join("");const div_infos=document.createElement("div");addClass(div_infos,"infos");div_infos.innerHTML="${value.replaceAll(" ", " ")}
`}else{error[index]=value}});output+=`We set out to prove this approach to building apps largely because we've seen the drawbacks of all the other approaches in real life, and thought "there must be a better way". The two major available approaches to building the same application for iOS and Android are:
+The drawback of the first approach is doing the work twice. In order to build every feature for iOS and Android at the same time, you need twice the number of people, either people who happily do Swift and Kotlin (and they are very rare), or more likely a set of iOS engineers and another set of Android engineers. This typically leads to forming two separate, platform-focused teams. We have witnessed situations first-hand, where those teams struggle with the same design problems, and despite one encountering and solving the problem first, the other one can learn nothing from their experience (and that's despite long design discussions).
+We think such experience with the platform native approach are common, and the reason why people look to React Native and Flutter. The issues with React Native are two fold
+React Native effectively takes over, and works hard to insulate the engineer from the native platform underneath and pretend it doesn't really exist, but of course, inevitably, it does and the user interface ends up being built in a combination of 90% JavaScript/TypeScript and 10% Kotlin/Swift. This was still a major win when React Native was first introduced, because the platform native UI toolkits were imperative, following a version of MVC architecture, and generally made it quite difficult to get UI state management right. React on the other hand is declarative, leaving much less space for errors stemming from the UI getting into an undefined state. This benefit was clearly recognised by iOS and Android, and both introduced their own declarative UI toolkit - Swift UI and Jetpack Compose. Both of them are quite good, matching that particular advantage of React Native, and leaving only building things once (in theory). But in exchange, the have to be written in JavaScript (and adjacent tools and languages).
+The main issue with the JavaScript ecosystem is that it's built on sand. The underlying language is quite loose and has a lot of inconsistencies. It came with no package manager originally, now it has three. To serve code to the browser, it gets bundled, and the list of bundlers is too long to include here. Webpack, the most popular one is famously difficult to configure. JavaScript was built as a dynamic language which leads to a lot of basic human errors, which are made while writing the code, only being discovered when running the code. Static type systems aim to solve that problem and TypeScript adds this onto JavaScript, but the types only go so far (until they hit an any
type, or dependencies with no type definitions), and they disappear at runtime.
In short, upgrading JavaScript to something modern takes a lot of tooling. Getting all this tooling set up and ready to build things is an all day job, and so more tooling, like Next.js has popped up providing this configuration in a box, batteries included. Perhaps the final admission of this problem is the recent Rome tools project, attempting to bring all the various tools under one roof (and Rome itself is built in Rust...).
+It's no wonder that even a working setup of all the tooling has sharp edges, and cannot afford to be nearly as strict as tooling designed with strictness in mind, such as Rust's. The heart of the problem is that computers are strict and precise instruments, and humans are sloppy creatures. With enough humans (more than 10, being generous) and no additional help, the resulting code will be sloppy, full of unhandled edge cases, undefined behaviour being relied on, circular dependencies preventing testing in isolation, etc. (and yes, these are not hypotheticals).
+Contrast that with Rust, which is as strict as it gets, and generally backs up the claim that if it compiles it will work (and if you struggle to get it past the compiler, it's probably a bad idea). The tooling and package management is built in with cargo
. There are fewer decisions to make when setting up a Rust project.
In short, we think the JS ecosystem has jumped the shark, the complexity toothpaste is out of the tube, and it's time to stop. But there's no real viable alternative. Crux is our attempt to provide one.
+In reality it's more like 1.4x effort build the same app for two platforms.
+Crux is an experimental approach to building cross-platform applications +with better testability, higher code and behavior reuse, better safety, +security, and more joy from better tools.
+It splits the application into two distinct parts, a Core built in Rust, which +drives as much of the business logic as possible, and a Shell, built in the +platform native language (Swift, Kotlin, TypeScript), which provides all +interfaces with the external world, including the human user, and acts as a +platform on which the core runs.
+ +The interface between the two is a native FFI (Foreign Function Interface) with +cross-language type checking and message passing semantics, where simple data +structures are passed across the boundary.
+To get playing with Crux quickly, follow the Getting Started steps. If you prefer to read more about how apps are built in Crux first, read the Development Guide. And if you'd like to know what possessed us to try this in the first place, read about our Motivation.
+There are two places to find API documentation: the latest published version on docs.rs, and we also have the very latest master docs if you too like to live dangerously.
+Crux is open source on Github. A good way to learn Crux is to explore the code, play with the examples, and raise issues or pull requests. We'd love you to get involved.
+You can also join the friendly conversation on our Zulip channel.
+The architecture is event-driven, based on +event sourcing. The Core +holds the majority of state, which is updated in response to events happening in +the Shell. The interface between the Core and the Shell is messaged based.
+The user interface layer is built natively, with modern declarative UI +frameworks such as Swift UI, Jetpack Compose and React/Vue or a WASM based +framework on the web. The UI layer is as thin as it can be, and all other +application logic is performed by the shared Core. The one restriction is that +the Core is side–effect free. This is both a technical requirement (to be able +to target WebAssembly), and an intentional design goal, to separate logic from +effects and make them both easier to test in isolation.
+The core requests side-effects from the Shell through common +capabilities. The basic concept is that instead of +doing the asynchronous work, the core describes the intent for the work with +data, and passes this to the Shell to be performed. The Shell performs the work, +and returns the outcomes back to the Core. This approach is inspired by +Elm, and similar to how other purely functional +languages deal with effects and I/O (e.g. the IO monad in Haskell). It is also +similar to how iterators work in Rust.
+The Core exports types for the messages it can understand. The Shell can call +the Core and pass one of the messages. In return, it receives a set of +side-effect requests to perform. When the work is completed, the Shell sends the +result back into the Core, which responds with further requests if necessary.
+Updating the user interface is considered one of the side-effects the Core can +request. The entire interface is strongly typed and breaking changes in the core +will result in build failures in the Shell.
+We set out to prove this architecture to find a better way of building apps +across platforms. You can read more about our motivation. The +overall goals of Crux are to:
+Crux is an experimental approach to building cross-platform applications +with better testability, higher code and behavior reuse, better safety, +security, and more joy from better tools.
+It splits the application into two distinct parts, a Core built in Rust, which +drives as much of the business logic as possible, and a Shell, built in the +platform native language (Swift, Kotlin, TypeScript), which provides all +interfaces with the external world, including the human user, and acts as a +platform on which the core runs.
+ +The interface between the two is a native FFI (Foreign Function Interface) with +cross-language type checking and message passing semantics, where simple data +structures are passed across the boundary.
+To get playing with Crux quickly, follow the Getting Started steps. If you prefer to read more about how apps are built in Crux first, read the Development Guide. And if you'd like to know what possessed us to try this in the first place, read about our Motivation.
+There are two places to find API documentation: the latest published version on docs.rs, and we also have the very latest master docs if you too like to live dangerously.
+Crux is open source on Github. A good way to learn Crux is to explore the code, play with the examples, and raise issues or pull requests. We'd love you to get involved.
+You can also join the friendly conversation on our Zulip channel.
+The architecture is event-driven, based on +event sourcing. The Core +holds the majority of state, which is updated in response to events happening in +the Shell. The interface between the Core and the Shell is messaged based.
+The user interface layer is built natively, with modern declarative UI +frameworks such as Swift UI, Jetpack Compose and React/Vue or a WASM based +framework on the web. The UI layer is as thin as it can be, and all other +application logic is performed by the shared Core. The one restriction is that +the Core is side–effect free. This is both a technical requirement (to be able +to target WebAssembly), and an intentional design goal, to separate logic from +effects and make them both easier to test in isolation.
+The core requests side-effects from the Shell through common +capabilities. The basic concept is that instead of +doing the asynchronous work, the core describes the intent for the work with +data, and passes this to the Shell to be performed. The Shell performs the work, +and returns the outcomes back to the Core. This approach is inspired by +Elm, and similar to how other purely functional +languages deal with effects and I/O (e.g. the IO monad in Haskell). It is also +similar to how iterators work in Rust.
+The Core exports types for the messages it can understand. The Shell can call +the Core and pass one of the messages. In return, it receives a set of +side-effect requests to perform. When the work is completed, the Shell sends the +result back into the Core, which responds with further requests if necessary.
+Updating the user interface is considered one of the side-effects the Core can +request. The entire interface is strongly typed and breaking changes in the core +will result in build failures in the Shell.
+We set out to prove this architecture to find a better way of building apps +across platforms. You can read more about our motivation. The +overall goals of Crux are to:
+We set out to prove this approach to building apps largely because we've seen the drawbacks of all the other approaches in real life, and thought "there must be a better way". The two major available approaches to building the same application for iOS and Android are:
+The drawback of the first approach is doing the work twice. In order to build every feature for iOS and Android at the same time, you need twice the number of people, either people who happily do Swift and Kotlin (and they are very rare), or more likely a set of iOS engineers and another set of Android engineers. This typically leads to forming two separate, platform-focused teams. We have witnessed situations first-hand, where those teams struggle with the same design problems, and despite one encountering and solving the problem first, the other one can learn nothing from their experience (and that's despite long design discussions).
+We think such experience with the platform native approach are common, and the reason why people look to React Native and Flutter. The issues with React Native are two fold
+React Native effectively takes over, and works hard to insulate the engineer from the native platform underneath and pretend it doesn't really exist, but of course, inevitably, it does and the user interface ends up being built in a combination of 90% JavaScript/TypeScript and 10% Kotlin/Swift. This was still a major win when React Native was first introduced, because the platform native UI toolkits were imperative, following a version of MVC architecture, and generally made it quite difficult to get UI state management right. React on the other hand is declarative, leaving much less space for errors stemming from the UI getting into an undefined state. This benefit was clearly recognised by iOS and Android, and both introduced their own declarative UI toolkit - Swift UI and Jetpack Compose. Both of them are quite good, matching that particular advantage of React Native, and leaving only building things once (in theory). But in exchange, the have to be written in JavaScript (and adjacent tools and languages).
+The main issue with the JavaScript ecosystem is that it's built on sand. The underlying language is quite loose and has a lot of inconsistencies. It came with no package manager originally, now it has three. To serve code to the browser, it gets bundled, and the list of bundlers is too long to include here. Webpack, the most popular one is famously difficult to configure. JavaScript was built as a dynamic language which leads to a lot of basic human errors, which are made while writing the code, only being discovered when running the code. Static type systems aim to solve that problem and TypeScript adds this onto JavaScript, but the types only go so far (until they hit an any
type, or dependencies with no type definitions), and they disappear at runtime.
In short, upgrading JavaScript to something modern takes a lot of tooling. Getting all this tooling set up and ready to build things is an all day job, and so more tooling, like Next.js has popped up providing this configuration in a box, batteries included. Perhaps the final admission of this problem is the recent Rome tools project, attempting to bring all the various tools under one roof (and Rome itself is built in Rust...).
+It's no wonder that even a working setup of all the tooling has sharp edges, and cannot afford to be nearly as strict as tooling designed with strictness in mind, such as Rust's. The heart of the problem is that computers are strict and precise instruments, and humans are sloppy creatures. With enough humans (more than 10, being generous) and no additional help, the resulting code will be sloppy, full of unhandled edge cases, undefined behaviour being relied on, circular dependencies preventing testing in isolation, etc. (and yes, these are not hypotheticals).
+Contrast that with Rust, which is as strict as it gets, and generally backs up the claim that if it compiles it will work (and if you struggle to get it past the compiler, it's probably a bad idea). The tooling and package management is built in with cargo
. There are fewer decisions to make when setting up a Rust project.
In short, we think the JS ecosystem has jumped the shark, the complexity toothpaste is out of the tube, and it's time to stop. But there's no real viable alternative. Crux is our attempt to provide one.
+In reality it's more like 1.4x effort build the same app for two platforms.
+These are the steps to set up the two crates forming the shared core – the core +itself, and the shared types crate which does type generation for the foreign +languages.
+Most of these steps are going to be automated in future tooling, and published as crates. For now the set up is effectively a copy & paste from one of the example projects.
+This is an example of a
+rust-toolchain.toml
+file, which you can add at the root of your repo. It should ensure that the
+correct rust channel and compile targets are installed automatically for you
+when you use any rust tooling within the repo.
[toolchain]
+channel = "stable"
+components = ["rustfmt", "rustc-dev"]
+targets = [
+ "aarch64-apple-darwin",
+ "aarch64-apple-ios",
+ "aarch64-apple-ios-sim",
+ "aarch64-linux-android",
+ "wasm32-unknown-unknown",
+ "x86_64-apple-ios"
+]
+profile = "minimal"
+
+The first library to create is the one that will be shared across all platforms,
+containing the behavior of the app. You can call it whatever you like, but we
+have chosen the name shared
here. You can create the shared rust library, like
+this:
cargo new --lib shared
+
+We'll be adding a bunch of other folders into the monorepo, so we are choosing
+to use Cargo Workspaces. Edit the workspace /Cargo.toml
file, at the monorepo
+root, to add the new library to our workspace. It should look something like
+this (the package
and dependencies
fields are just examples):
[workspace]
+members = ["shared", "shared_types", "web-dioxus", "web-leptos", "web-yew"]
+resolver = "1"
+
+[workspace.package]
+authors = ["Red Badger Consulting Limited"]
+edition = "2021"
+repository = "https://github.com/redbadger/crux/"
+license = "Apache-2.0"
+keywords = ["crux", "crux_core", "cross-platform-ui", "ffi", "wasm"]
+rust-version = "1.66"
+
+[workspace.dependencies]
+anyhow = "1.0.75"
+serde = "1.0.193"
+
+The library's manifest, at /shared/Cargo.toml
, should look something like the
+following, but there are a few things to note:
crate-type
+lib
is the default rust library when linking into a rust binary, e.g. in
+the web-yew
, or cli
, variantstaticlib
is a static library (libshared.a
) for including in the Swift
+iOS app variantcdylib
is a C-ABI dynamic library (libshared.so
) for use with JNA when
+included in the Kotlin Android app varianttypegen
that depends on the feature with
+the same name in the crux_core
crate. This is used by its sister library
+(often called shared_types
) that will generate types for use across the FFI
+boundary (see the section below on generating shared types).path
fields on the crux dependencies are for the
+examples in the Crux repo
+and so you will probably not need themuniffi-bindgen
target should make sense after
+you read the next section[package]
+name = "shared"
+version = "0.1.0"
+edition = "2021"
+rust-version = "1.66"
+
+[lib]
+crate-type = ["lib", "staticlib", "cdylib"]
+name = "shared"
+
+[features]
+typegen = ["crux_core/typegen"]
+
+[dependencies]
+crux_core = "0.6"
+crux_macros = "0.3"
+serde = { workspace = true, features = ["derive"] }
+lazy_static = "1.4.0"
+uniffi = "0.25.2"
+wasm-bindgen = "0.2.89"
+
+[target.uniffi-bindgen.dependencies]
+uniffi = { version = "0.25.2", features = ["cli"] }
+
+[build-dependencies]
+uniffi = { version = "0.25.2", features = ["build"] }
+
+Crux uses Mozilla's Uniffi to generate +the FFI bindings for iOS and Android.
+uniffi-bindgen
CLI toolSince Mozilla released version 0.23.0
of Uniffi, we need to also generate the
+binary that generates these bindings. This avoids the possibility of getting a
+version mismatch between a separately installed binary and the crate's Uniffi
+version. You can read more about it
+here.
Generating the binary is simple, we just add the following to our crate, in a
+file called /shared/src/bin/uniffi-bindgen.rs
.
fn main() {
+ uniffi::uniffi_bindgen_main()
+}
+And then we can build it with cargo.
+cargo run -p shared --bin uniffi-bindgen
+
+# or
+
+cargo build
+./target/debug/uniffi-bindgen
+
+The uniffi-bindgen
executable will be used during the build in XCode and in
+Android Studio (see the following pages).
We will need an interface definition file for the FFI bindings. Uniffi has its
+own file format (similar to WebIDL) that has a .udl
extension. You can create
+one at /shared/src/shared.udl
, like this:
namespace shared {
+ bytes process_event([ByRef] bytes msg);
+ bytes handle_response([ByRef] bytes uuid, [ByRef] bytes res);
+ bytes view();
+};
+
+There are also a few additional parameters to tell Uniffi how to create bindings
+for Kotlin and Swift. They live in the file /shared/uniffi.toml
, like this
+(feel free to adjust accordingly):
[bindings.kotlin]
+package_name = "com.example.counter.shared"
+cdylib_name = "shared"
+
+[bindings.swift]
+cdylib_name = "shared_ffi"
+omit_argument_labels = true
+
+Finally, we need a build.rs
file in the root of the crate
+(/shared/build.rs
), to generate the bindings:
fn main() {
+ uniffi::generate_scaffolding("./src/shared.udl").unwrap();
+}
+Soon we will have macros and/or code-gen to help with this, but for now, we need
+some scaffolding in /shared/src/lib.rs
. You'll notice that we are re-exporting
+the Request
type and the capabilities we want to use in our native Shells, as
+well as our public types from the shared library.
pub mod app;
+
+use lazy_static::lazy_static;
+use wasm_bindgen::prelude::wasm_bindgen;
+
+pub use crux_core::{bridge::Bridge, Core, Request};
+
+pub use app::*;
+
+// TODO hide this plumbing
+
+uniffi::include_scaffolding!("shared");
+
+lazy_static! {
+ static ref CORE: Bridge<Effect, Counter> = Bridge::new(Core::new::<Capabilities>());
+}
+
+#[wasm_bindgen]
+pub fn process_event(data: &[u8]) -> Vec<u8> {
+ CORE.process_event(data)
+}
+
+#[wasm_bindgen]
+pub fn handle_response(uuid: &[u8], data: &[u8]) -> Vec<u8> {
+ CORE.handle_response(uuid, data)
+}
+
+#[wasm_bindgen]
+pub fn view() -> Vec<u8> {
+ CORE.view()
+}
+Now we are in a position to create a basic app in /shared/src/app.rs
. This is
+from the
+simple Counter example
+(which also has tests, although we're not showing them here):
use crux_core::{render::Render, App};
+use crux_macros::Effect;
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+pub enum Event {
+ Increment,
+ Decrement,
+ Reset,
+}
+
+#[derive(Default)]
+pub struct Model {
+ count: isize,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub struct ViewModel {
+ pub count: String,
+}
+
+#[cfg_attr(feature = "typegen", derive(crux_macros::Export))]
+#[derive(Effect)]
+#[effect(app = "Counter")]
+pub struct Capabilities {
+ render: Render<Event>,
+}
+
+#[derive(Default)]
+pub struct Counter;
+
+impl App for Counter {
+ type Event = Event;
+ type Model = Model;
+ type ViewModel = ViewModel;
+ type Capabilities = Capabilities;
+
+ fn update(&self, event: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities) {
+ match event {
+ Event::Increment => model.count += 1,
+ Event::Decrement => model.count -= 1,
+ Event::Reset => model.count = 0,
+ };
+
+ caps.render.render();
+ }
+
+ fn view(&self, model: &Self::Model) -> Self::ViewModel {
+ ViewModel {
+ count: format!("Count is: {}", model.count),
+ }
+ }
+}
+Make sure everything builds OK
+cargo build
+
+This crate serves as the container for type generation for the foreign +languages.
+Copy over the +shared_types +folder from the counter example.
+Edit the build.rs
file and make sure that your app type is registered. You
+may also need to register any nested enum types (due to a current limitation
+with the reflection library, see
+https://github.com/zefchain/serde-reflection/tree/main/serde-reflection#supported-features).
+Here is an example of this from the
+build.rs
+file in the shared_types
crate of the
+notes example:
use crux_core::typegen::TypeGen;
+use shared::{NoteEditor, TextCursor};
+use std::path::PathBuf;
+
+fn main() {
+ println!("cargo:rerun-if-changed=../shared");
+
+ let mut gen = TypeGen::new();
+
+ gen.register_app::<NoteEditor>().expect("register");
+
+ // Note: currently required as we can't find enums inside enums, see:
+ // https://github.com/zefchain/serde-reflection/tree/main/serde-reflection#supported-features
+ gen.register_type::<TextCursor>().expect("register");
+
+ let output_root = PathBuf::from("./generated");
+
+ gen.swift("SharedTypes", output_root.join("swift"))
+ .expect("swift type gen failed");
+
+ // TODO these are for later
+ //
+ // gen.java("com.example.counter.shared_types", output_root.join("java"))
+ // .expect("java type gen failed");
+
+ gen.typescript("shared_types", output_root.join("typescript"))
+ .expect("typescript type gen failed");
+}
+For the above to compile, your Capabilities
struct must implement the Export
trait. There is a derive macro that can do this for you, e.g.:
#[cfg_attr(feature = "typegen", derive(crux_macros::Export))]
+pub struct Capabilities {
+ pub render: Render<Event>,
+ pub http: Http<Event>,
+}
+Make sure everything builds and foreign types get generated into the
+generated
folder.
cargo build
+
+When we use Crux to build iOS apps, the Core API bindings are generated in Swift +(with C headers) using Mozilla's Uniffi.
+The shared core (that contains our app's behavior) is compiled to a static
+library and linked into the iOS binary. To do this we use
+cargo-xcode
to generate an Xcode
+project for the shared core, which we can include as a sub-project in our iOS
+app.
The shared types are generated by Crux as a Swift package, which we can add to +our iOS project as a dependency. The Swift code to serialize and deserialize +these types across the boundary is also generated by Crux as Swift packages.
+ +This section has two guides for building iOS apps with Crux:
+ +We recommend the first option, as it's definitely the simplest way to get +started.
+These are the steps to set up Xcode to build and run a simple iOS app that calls +into a shared core.
+We think that using XcodeGen may be the simplest way to create an Xcode project to build and run a simple iOS app that calls into a shared core. If you'd rather set up Xcode manually, you can jump to iOS — Swift and SwiftUI — manual setup, otherwise read on.
+This walk-through assumes you have already added the shared
and shared_types
libraries to your repo — as described in Shared core and types.
When we build our iOS app, we also want to build the Rust core as a static +library so that it can be linked into the binary that we're going to ship.
+We will use cargo-xcode
to generate an Xcode project for our shared library, which we can add as a sub-project in Xcode.
Recent changes to cargo-xcode
mean that we need to use version <=1.7.0 for now.
If you don't have this already, you can install it with cargo install --force cargo-xcode --version 1.7.0
.
Let's generate the sub-project:
+cargo xcode
+
+This generates an Xcode project for each crate in the workspace, but we're only
+interested in the one it creates in the shared
directory. Don't open this
+generated project yet, it'll be included when we generate the Xcode project for
+our iOS app.
We will use XcodeGen
to generate an Xcode project for our iOS app.
If you don't have this already, you can install it with brew install xcodegen
.
Before we generate the Xcode project, we need to create some directories and a
+project.yml
file:
mkdir -p iOS/SimpleCounter
+cd iOS
+touch project.yml
+
+The project.yml
file describes the Xcode project we want to generate. Here's
+one for the SimpleCounter example — you may want to adapt this for your own
+project:
name: SimpleCounter
+projectReferences:
+ Shared:
+ path: ../shared/shared.xcodeproj
+packages:
+ SharedTypes:
+ path: ../shared_types/generated/swift/SharedTypes
+options:
+ bundleIdPrefix: com.example.simple_counter
+attributes:
+ BuildIndependentTargetsInParallel: true
+targets:
+ SimpleCounter:
+ type: application
+ platform: iOS
+ deploymentTarget: "15.0"
+ sources:
+ - SimpleCounter
+ - path: ../shared/src/shared.udl
+ buildPhase: sources
+ dependencies:
+ - target: Shared/uniffi-bindgen-bin
+ - target: Shared/shared-staticlib
+ - package: SharedTypes
+ info:
+ path: SimpleCounter/Info.plist
+ properties:
+ UISupportedInterfaceOrientations:
+ - UIInterfaceOrientationPortrait
+ - UIInterfaceOrientationLandscapeLeft
+ - UIInterfaceOrientationLandscapeRight
+ UILaunchScreen: {}
+ settings:
+ OTHER_LDFLAGS: [-w]
+ SWIFT_OBJC_BRIDGING_HEADER: generated/sharedFFI.h
+ ENABLE_USER_SCRIPT_SANDBOXING: NO
+ buildRules:
+ - name: Generate FFI
+ filePattern: "*.udl"
+ script: |
+ #!/bin/bash
+ set -e
+
+ # Skip during indexing phase in XCode 13+
+ if [ "$ACTION" == "indexbuild" ]; then
+ echo "Not building *.udl files during indexing."
+ exit 0
+ fi
+
+ # Skip for preview builds
+ if [ "$ENABLE_PREVIEWS" = "YES" ]; then
+ echo "Not building *.udl files during preview builds."
+ exit 0
+ fi
+
+ cd "${INPUT_FILE_DIR}/.."
+ "${BUILD_DIR}/debug/uniffi-bindgen" generate "src/${INPUT_FILE_NAME}" --language swift --out-dir "${PROJECT_DIR}/generated"
+ outputFiles:
+ - $(PROJECT_DIR)/generated/$(INPUT_FILE_BASE).swift
+ - $(PROJECT_DIR)/generated/$(INPUT_FILE_BASE)FFI.h
+ runOncePerArchitecture: false
+
+Then we can generate the Xcode project:
+xcodegen
+
+This should create an iOS/SimpleCounter/SimpleCounter.xcodeproj
project file,
+which we can open in Xcode. It should build OK, but we will need to add some
+code!
There is slightly more advanced +example of an +iOS app in the Crux repository.
+However, we will use the
+simple counter example,
+which has shared
and shared_types
libraries that will work with the
+following example code.
A simple app that increments, decrements and resets a counter.
+First, let's add some boilerplate code to wrap our core and handle the
+capabilities that we are using. For this example, we only need to support the
+Render
capability, which triggers a render of the UI.
This code that wraps the core only needs to be written once — it only grows when +we need to support additional capabilities.
+Edit iOS/SimpleCounter/core.swift
to look like the following. This code sends
+our (UI-generated) events to the core, and handles any effects that the core
+asks for. In this simple example, we aren't calling any HTTP APIs or handling
+any side effects other than rendering the UI, so we just handle this render
+effect by updating the published view model from the core.
import Foundation
+import SharedTypes
+
+@MainActor
+class Core: ObservableObject {
+ @Published var view: ViewModel
+
+ init() {
+ self.view = try! .bincodeDeserialize(input: [UInt8](SimpleCounter.view()))
+ }
+
+ func update(_ event: Event) {
+ let effects = [UInt8](processEvent(Data(try! event.bincodeSerialize())))
+
+ let requests: [Request] = try! .bincodeDeserialize(input: effects)
+ for request in requests {
+ processEffect(request)
+ }
+ }
+
+ func processEffect(_ request: Request) {
+ switch request.effect {
+ case .render:
+ view = try! .bincodeDeserialize(input: [UInt8](SimpleCounter.view()))
+ }
+ }
+}
+
+That switch
statement, above, is where you would handle any other effects that
+your core might ask for. For example, if your core needs to make an HTTP
+request, you would handle that here. To see an example of this, take a look at
+the
+counter example
+in the Crux repository.
Edit iOS/SimpleCounter/ContentView.swift
to look like the following:
import SharedTypes
+import SwiftUI
+
+struct ContentView: View {
+ @ObservedObject var core: Core
+
+ var body: some View {
+ VStack {
+ Image(systemName: "globe")
+ .imageScale(.large)
+ .foregroundColor(.accentColor)
+ Text(core.view.count)
+ HStack {
+ ActionButton(label: "Reset", color: .red) {
+ core.update(.reset)
+ }
+ ActionButton(label: "Inc", color: .green) {
+ core.update(.increment)
+ }
+ ActionButton(label: "Dec", color: .yellow) {
+ core.update(.decrement)
+ }
+ }
+ }
+ }
+}
+
+struct ActionButton: View {
+ var label: String
+ var color: Color
+ var action: () -> Void
+
+ init(label: String, color: Color, action: @escaping () -> Void) {
+ self.label = label
+ self.color = color
+ self.action = action
+ }
+
+ var body: some View {
+ Button(action: action) {
+ Text(label)
+ .fontWeight(.bold)
+ .font(.body)
+ .padding(EdgeInsets(top: 10, leading: 15, bottom: 10, trailing: 15))
+ .background(color)
+ .cornerRadius(10)
+ .foregroundColor(.white)
+ .padding()
+ }
+ }
+}
+
+struct ContentView_Previews: PreviewProvider {
+ static var previews: some View {
+ ContentView(core: Core())
+ }
+}
+
+And create iOS/SimpleCounter/SimpleCounterApp.swift
to look like this:
import SwiftUI
+
+@main
+struct SimpleCounterApp: App {
+ var body: some Scene {
+ WindowGroup {
+ ContentView(core: Core())
+ }
+ }
+}
+
+Run xcodegen
again to update the Xcode project with these newly created source
+files (or add them manually in Xcode to the SimpleCounter
group), and then
+open iOS/SimpleCounter/SimpleCounter.xcodeproj
in Xcode. You might need to
+select the SimpleCounter
scheme, and an appropriate simulator, in the
+drop-down at the top, before you build.
You should then be able to run the app in the simulator or on an iPhone, and it should look like this:
+ +These are the steps to set up Xcode to build and run a simple iOS app that calls +into a shared core.
+We recommend setting up Xcode with XcodeGen as described in the +previous section. It is the simplest way to create an Xcode +project to build and run a simple iOS app that calls into a shared core. However, +if you want to set up Xcode manually then read on.
+This walk-through assumes you have already added the shared
and shared_types
+libraries to your repo — as described in Shared core and types
+— and that you have built them using cargo build
.
The first thing we need to do is create a new iOS app in Xcode.
+Let's call the app "SimpleCounter" and select "SwiftUI" for the interface and +"Swift" for the language. If you choose to create the app in the root folder of +your monorepo, then you might want to rename the folder it creates to "iOS". +Your repo's directory structure might now look something like this (some files +elided):
+.
+├── Cargo.lock
+├── Cargo.toml
+├── iOS
+│ ├── SimpleCounter
+│ │ ├── ContentView.swift
+│ │ └── SimpleCounterApp.swift
+│ └── SimpleCounter.xcodeproj
+│ └── project.pbxproj
+├── shared
+│ ├── build.rs
+│ ├── Cargo.toml
+│ ├── src
+│ │ ├── counter.rs
+│ │ ├── lib.rs
+│ │ └── shared.udl
+│ └── uniffi.toml
+├── shared_types
+│ ├── build.rs
+│ ├── Cargo.toml
+│ └── src
+│ └── lib.rs
+└── target
+
+We want UniFFI to create the Swift bindings and the C headers for our shared
+library, and store them in a directory called generated
.
To achieve this, we'll associate a script with files that match the pattern
+*.udl
(this will catch the interface definition file we created earlier), and
+then add our shared.udl
file to the project.
Note that our shared library generates the uniffi-bindgen
binary (as explained
+on the page "Shared core and types") that the script relies on, so
+make sure you have built it already, using cargo build
.
In "Build Rules", add a rule to process files that match the pattern *.udl
+with the following script (and also uncheck "Run once per architecture").
#!/bin/bash
+set -e
+
+# Skip during indexing phase in XCode 13+
+if [ "$ACTION" == "indexbuild" ]; then
+ echo "Not building *.udl files during indexing."
+ exit 0
+fi
+
+# Skip for preview builds
+if [ "$ENABLE_PREVIEWS" = "YES" ]; then
+ echo "Not building *.udl files during preview builds."
+ exit 0
+fi
+
+cd "${INPUT_FILE_DIR}/.."
+"${BUILD_DIR}/debug/uniffi-bindgen" generate "src/${INPUT_FILE_NAME}" --language swift --out-dir "${PROJECT_DIR}/generated"
+
+
+We'll need to add the following as output files:
+$(PROJECT_DIR)/generated/$(INPUT_FILE_BASE).swift
+
+$(PROJECT_DIR)/generated/$(INPUT_FILE_BASE)FFI.h
+
+Now go to "Build Phases, Compile Sources", and add /shared/src/shared.udl
+using the "add other" button, selecting "Create folder references".
Build the project (cmd-B), which will fail, but the above script should run +successfully and the "generated" folder should contain the generated Swift types +and C header files:
+$ ls iOS/generated
+shared.swift sharedFFI.h sharedFFI.modulemap
+
+In "Build Settings", search for "bridging header", and add
+generated/sharedFFI.h
, for any architecture/SDK, i.e. in both Debug and
+Release. If there isn't already a setting for "bridging header" you can add one
+(and then delete it) as per
+this StackOverflow question
When we build our iOS app, we also want to build the Rust core as a static +library so that it can be linked into the binary that we're going to ship.
+We will use cargo-xcode
to generate an Xcode project for our shared library, which we can add as a sub-project in Xcode.
Recent changes to cargo-xcode
mean that we need to use version <=1.7.0 for now.
If you don't have this already, you can install it with cargo install --force cargo-xcode --version 1.7.0
.
Let's generate the sub-project:
+cargo xcode
+
+This generates an Xcode project for each crate in the workspace, but we're only
+interested in the one it creates in the shared
directory. Don't open this
+generated project yet.
Using Finder, drag the shared/shared.xcodeproj
folder under the Xcode project
+root.
Then, in the "Build Phases, Link Binary with Libraries" section, add the
+libshared_static.a
library (you should be able to navigate to it as
+Workspace -> shared -> libshared_static.a
)
Using Finder, drag the shared_types/generated/swift/SharedTypes
folder under
+the Xcode project root.
Then, in the "Build Phases, Link Binary with Libraries" section, add the
+SharedTypes
library (you should be able to navigate to it as
+Workspace -> SharedTypes -> SharedTypes
)
There is slightly more advanced +example of an +iOS app in the Crux repository.
+However, we will use the
+simple counter example,
+which has shared
and shared_types
libraries that will work with the
+following example code.
A simple app that increments, decrements and resets a counter.
+First, let's add some boilerplate code to wrap our core and handle the
+capabilities that we are using. For this example, we only need to support the
+Render
capability, which triggers a render of the UI.
This code that wraps the core only needs to be written once — it only grows when +we need to support additional capabilities.
+Edit iOS/SimpleCounter/core.swift
to look like the following. This code sends
+our (UI-generated) events to the core, and handles any effects that the core
+asks for. In this simple example, we aren't calling any HTTP APIs or handling
+any side effects other than rendering the UI, so we just handle this render
+effect by updating the published view model from the core.
import Foundation
+import SharedTypes
+
+@MainActor
+class Core: ObservableObject {
+ @Published var view: ViewModel
+
+ init() {
+ self.view = try! .bincodeDeserialize(input: [UInt8](SimpleCounter.view()))
+ }
+
+ func update(_ event: Event) {
+ let effects = [UInt8](processEvent(Data(try! event.bincodeSerialize())))
+
+ let requests: [Request] = try! .bincodeDeserialize(input: effects)
+ for request in requests {
+ processEffect(request)
+ }
+ }
+
+ func processEffect(_ request: Request) {
+ switch request.effect {
+ case .render:
+ view = try! .bincodeDeserialize(input: [UInt8](SimpleCounter.view()))
+ }
+ }
+}
+
+That switch
statement, above, is where you would handle any other effects that
+your core might ask for. For example, if your core needs to make an HTTP
+request, you would handle that here. To see an example of this, take a look at
+the
+counter example
+in the Crux repository.
Edit iOS/SimpleCounter/ContentView.swift
to look like the following:
import SharedTypes
+import SwiftUI
+
+struct ContentView: View {
+ @ObservedObject var core: Core
+
+ var body: some View {
+ VStack {
+ Image(systemName: "globe")
+ .imageScale(.large)
+ .foregroundColor(.accentColor)
+ Text(core.view.count)
+ HStack {
+ ActionButton(label: "Reset", color: .red) {
+ core.update(.reset)
+ }
+ ActionButton(label: "Inc", color: .green) {
+ core.update(.increment)
+ }
+ ActionButton(label: "Dec", color: .yellow) {
+ core.update(.decrement)
+ }
+ }
+ }
+ }
+}
+
+struct ActionButton: View {
+ var label: String
+ var color: Color
+ var action: () -> Void
+
+ init(label: String, color: Color, action: @escaping () -> Void) {
+ self.label = label
+ self.color = color
+ self.action = action
+ }
+
+ var body: some View {
+ Button(action: action) {
+ Text(label)
+ .fontWeight(.bold)
+ .font(.body)
+ .padding(EdgeInsets(top: 10, leading: 15, bottom: 10, trailing: 15))
+ .background(color)
+ .cornerRadius(10)
+ .foregroundColor(.white)
+ .padding()
+ }
+ }
+}
+
+struct ContentView_Previews: PreviewProvider {
+ static var previews: some View {
+ ContentView(core: Core())
+ }
+}
+
+And create iOS/SimpleCounter/SimpleCounterApp.swift
to look like this:
import SwiftUI
+
+@main
+struct SimpleCounterApp: App {
+ var body: some Scene {
+ WindowGroup {
+ ContentView(core: Core())
+ }
+ }
+}
+
+You should then be able to run the app in the simulator or on an iPhone, and it should look like this:
+ +When we use Crux to build Android apps, the Core API bindings are generated in +Java using Mozilla's Uniffi.
+The shared core (that contains our app's behavior) is compiled to a dynamic +library, using Mozilla's +Rust gradle plugin for Android +and the Android NDK. The library is loaded +at runtime using +Java Native Access.
+The shared types are generated by Crux as Java packages, which we can add to our
+Android project using sourceSets
. The Java code to serialize and deserialize
+these types across the boundary is also generated by Crux as Java packages.
This section has a guide for building Android apps with Crux:
+ +These are the steps to set up Android Studio to build and run a simple Android +app that calls into a shared core.
+This walk-through assumes you have already added the shared
and shared_types
libraries to your repo, as described in Shared core and types.
We want to make setting up Android Studio to work with Crux really easy. As time progresses we will try to simplify and automate as much as possible, but at the moment there is some manual configuration to do. This only needs doing once, so we hope it's not too much trouble. If you know of any better ways than those we describe below, please either raise an issue (or a PR) at https://github.com/redbadger/crux.
+The first thing we need to do is create a new Android app in Android Studio.
+Open Android Studio and create a new project, for "Phone and Tablet", of type
+"Empty Compose Activity (Material3)". In this walk-through, we'll call it
+"Counter", use a minimum SDK of API 34, and save it in a directory called
+Android
.
Your repo's directory structure might now look something like this (some files +elided):
+.
+├── Android
+│ ├── app
+│ │ ├── build.gradle
+│ │ ├── libs
+│ │ └── src
+│ │ └── main
+│ │ ├── AndroidManifest.xml
+│ │ └── java
+│ │ └── com
+│ │ └── example
+│ │ └── counter
+│ │ └── MainActivity.kt
+│ ├── build.gradle
+│ ├── gradle.properties
+│ ├── local.properties
+│ └── settings.gradle
+├── Cargo.lock
+├── Cargo.toml
+├── shared
+│ ├── build.rs
+│ ├── Cargo.toml
+│ ├── src
+│ │ ├── counter.rs
+│ │ ├── lib.rs
+│ │ └── shared.udl
+│ └── uniffi.toml
+├── shared_types
+│ ├── build.rs
+│ ├── Cargo.toml
+│ └── src
+│ └── lib.rs
+└── target
+
+This shared Android library (aar
) is going to wrap our shared Rust library.
Under File -> New -> New Module
, choose "Android Library" and call it
+something like shared
. Set the "Package name" to match the one from your
+/shared/uniffi.toml
, e.g. com.example.counter.shared
.
For more information on how to add an Android library see +https://developer.android.com/studio/projects/android-library.
+We can now add this library as a dependency of our app.
+Edit the app's build.gradle
(/Android/app/build.gradle
) to look like
+this:
plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+}
+
+android {
+ namespace 'com.example.simple_counter'
+ compileSdk 34
+
+ defaultConfig {
+ applicationId "com.example.simple_counter"
+ minSdk 33
+ targetSdk 34
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary true
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles {
+ getDefaultProguardFile('proguard-android-optimize.txt')
+ 'proguard-rules.pro'
+ }
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+ buildFeatures {
+ compose true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion '1.5.3'
+ }
+ packagingOptions {
+ resources {
+ excludes += '/META-INF/*'
+ }
+ }
+}
+
+dependencies {
+ // our shared library
+ implementation project(path: ':shared')
+
+ def composeBom = platform('androidx.compose:compose-bom:2022.10.00')
+ implementation composeBom
+ androidTestImplementation composeBom
+
+ implementation 'androidx.compose.material3:material3:1.2.0-alpha10'
+
+ // Optional - Integration with ViewModels
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
+ // Optional - Integration with LiveData
+ implementation("androidx.compose.runtime:runtime-livedata")
+
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
+
+ implementation 'androidx.core:core-ktx:1.12.0'
+ implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
+ implementation 'androidx.activity:activity-compose:1.8.1'
+ implementation "androidx.compose.ui:ui:1.5.4"
+ implementation "androidx.compose.ui:ui-tooling-preview:1.5.4"
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+ androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.5.4"
+ debugImplementation "androidx.compose.ui:ui-tooling:1.5.4"
+ debugImplementation "androidx.compose.ui:ui-test-manifest:1.5.4"
+}
+
+We'll use the following tools to incorporate our Rust shared library into the +Android library added above. This includes compiling and linking the Rust +dynamic library and generating the runtime bindings and the shared types.
+Let's get started.
+Add the four rust android toolchains to your system:
+$ rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android
+
+Edit the project's build.gradle
(/Android/build.gradle
) to look like
+this:
plugins {
+ id 'com.android.application' version '8.1.2' apply false
+ id 'com.android.library' version '8.1.2' apply false
+ id 'org.jetbrains.kotlin.android' version '1.9.10' apply false
+ id "org.mozilla.rust-android-gradle.rust-android" version "0.9.3" apply false
+}
+
+Edit the library's build.gradle
(/Android/shared/build.gradle
) to look
+like this:
plugins {
+ id 'com.android.library'
+ id 'org.jetbrains.kotlin.android'
+ id 'org.mozilla.rust-android-gradle.rust-android'
+}
+
+android {
+ namespace 'com.example.simple_counter.shared'
+ compileSdk 34
+
+ ndkVersion "25.2.9519653"
+
+
+ defaultConfig {
+ minSdk 33
+ targetSdk 34
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles {
+ getDefaultProguardFile('proguard-android-optimize.txt')
+ 'proguard-rules.pro'
+ }
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+
+ sourceSets {
+ main.java.srcDirs += "${projectDir}/../../shared_types/generated/java"
+ }
+}
+
+dependencies {
+ implementation "net.java.dev.jna:jna:5.13.0@aar"
+
+ implementation 'androidx.core:core-ktx:1.12.0'
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'com.google.android.material:material:1.10.0'
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+}
+
+apply plugin: 'org.mozilla.rust-android-gradle.rust-android'
+
+cargo {
+ module = "../.."
+ libname = "shared"
+ // these are the four recommended targets for Android that will ensure your library works on all mainline android devices
+ // make sure you have included the rust toolchain for each of these targets: \
+ // `rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android`
+ targets = ["arm", "arm64", "x86", "x86_64"]
+ extraCargoBuildArguments = ['--package', 'shared']
+}
+
+afterEvaluate {
+ // The `cargoBuild` task isn't available until after evaluation.
+ android.libraryVariants.configureEach { variant ->
+ def productFlavor = ""
+ variant.productFlavors.each {
+ productFlavor += "${it.name.capitalize()}"
+ }
+ def buildType = "${variant.buildType.name.capitalize()}"
+
+ tasks.named("compileDebugKotlin") {
+ it.dependsOn(tasks.named("typesGen"), tasks.named("bindGen"))
+ }
+
+ tasks.named("generate${productFlavor}${buildType}Assets") {
+ it.dependsOn(tasks.named("cargoBuild"))
+ }
+ }
+}
+
+tasks.register('bindGen', Exec) {
+ def outDir = "${projectDir}/../../shared_types/generated/java"
+ workingDir "../../"
+ if (System.getProperty('os.name').toLowerCase().contains('windows')) {
+ commandLine("cmd", "/c",
+ "cargo build -p shared && " + "target\\debug\\uniffi-bindgen generate shared\\src\\shared.udl " + "--language kotlin " + "--out-dir " + outDir.replace('/', '\\'))
+ } else {
+ commandLine("sh", "-c",
+ """\
+ cargo build -p shared && \
+ target/debug/uniffi-bindgen generate shared/src/shared.udl \
+ --language kotlin \
+ --out-dir $outDir
+ """)
+ }
+}
+
+tasks.register('typesGen', Exec) {
+ workingDir "../../"
+ if (System.getProperty('os.name').toLowerCase().contains('windows')) {
+ commandLine("cmd", "/c", "cargo build -p shared_types")
+ } else {
+ commandLine("sh", "-c", "cargo build -p shared_types")
+ }
+}
+
+
+
+If you now build your project you should see the newly built shared library +object file.
+$ ls --tree Android/shared/build/rustJniLibs
+Android/shared/build/rustJniLibs
+└── android
+ └── arm64-v8a
+ └── libshared.so
+ └── armeabi-v7a
+ └── libshared.so
+ └── x86
+ └── libshared.so
+ └── x86_64
+ └── libshared.so
+
+You should also see the generated types — note that the sourceSets
directive
+in the shared library gradle file (above) allows us to build our shared library
+against the generated types in the shared_types/generated
folder.
$ ls --tree shared_types/generated/java
+shared_types/generated/java
+└── com
+ ├── example
+ │ └── counter
+ │ ├── shared
+ │ │ └── shared.kt
+ │ └── shared_types
+ │ ├── Effect.java
+ │ ├── Event.java
+ │ ├── RenderOperation.java
+ │ ├── Request.java
+ │ ├── Requests.java
+ │ ├── TraitHelpers.java
+ │ └── ViewModel.java
+ └── novi
+ ├── bincode
+ │ ├── BincodeDeserializer.java
+ │ └── BincodeSerializer.java
+ └── serde
+ ├── ArrayLen.java
+ ├── BinaryDeserializer.java
+ ├── BinarySerializer.java
+ ├── Bytes.java
+ ├── DeserializationError.java
+ ├── Deserializer.java
+ ├── Int128.java
+ ├── SerializationError.java
+ ├── Serializer.java
+ ├── Slice.java
+ ├── Tuple2.java
+ ├── Tuple3.java
+ ├── Tuple4.java
+ ├── Tuple5.java
+ ├── Tuple6.java
+ ├── Unit.java
+ └── Unsigned.java
+
+There is a slightly more advanced +example of an +Android app in the Crux repository.
+However, we will use the
+simple counter example,
+which has shared
and shared_types
libraries that will work with the
+following example code.
A simple app that increments, decrements and resets a counter.
+First, let's add some boilerplate code to wrap our core and handle the
+capabilities that we are using. For this example, we only need to support the
+Render
capability, which triggers a render of the UI.
This code that wraps the core only needs to be written once — it only grows when +we need to support additional capabilities.
+Edit Android/app/src/main/java/com/example/simple_counter/Core.kt
to look like
+the following. This code sends our (UI-generated) events to the core, and
+handles any effects that the core asks for. In this simple example, we aren't
+calling any HTTP APIs or handling any side effects other than rendering the UI,
+so we just handle this render effect by updating the published view model from
+the core.
package com.example.simple_counter
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.mutableStateOf
+import com.example.simple_counter.shared.processEvent
+import com.example.simple_counter.shared.view
+import com.example.simple_counter.shared_types.Effect
+import com.example.simple_counter.shared_types.Event
+import com.example.simple_counter.shared_types.Request
+import com.example.simple_counter.shared_types.Requests
+import com.example.simple_counter.shared_types.ViewModel
+
+class Core : androidx.lifecycle.ViewModel() {
+ var view: ViewModel by mutableStateOf(ViewModel.bincodeDeserialize(view()))
+ private set
+
+ fun update(event: Event) {
+ val effects = processEvent(event.bincodeSerialize())
+
+ val requests = Requests.bincodeDeserialize(effects)
+ for (request in requests) {
+ processEffect(request)
+ }
+ }
+
+ private fun processEffect(request: Request) {
+ when (request.effect) {
+ is Effect.Render -> {
+ this.view = ViewModel.bincodeDeserialize(view())
+ }
+ }
+ }
+}
+
+That when
statement, above, is where you would handle any other effects that
+your core might ask for. For example, if your core needs to make an HTTP
+request, you would handle that here. To see an example of this, take a look at
+the
+counter example
+in the Crux repository.
Edit /Android/app/src/main/java/com/example/simple_counter/MainActivity.kt
to
+look like the following:
@file:OptIn(ExperimentalUnsignedTypes::class)
+
+package com.example.simple_counter
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.example.simple_counter.shared_types.Event
+import com.example.simple_counter.ui.theme.CounterTheme
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ CounterTheme {
+ Surface(
+ modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background
+ ) {
+ View()
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun View(core: Core = viewModel()) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(10.dp),
+ ) {
+ Text(text = core.view.count.toString(), modifier = Modifier.padding(10.dp))
+ Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+ Button(
+ onClick = { core.update(Event.Reset()) }, colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.error
+ )
+ ) { Text(text = "Reset", color = Color.White) }
+ Button(
+ onClick = { core.update(Event.Increment()) }, colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ )
+ ) { Text(text = "Increment", color = Color.White) }
+ Button(
+ onClick = { core.update(Event.Decrement()) }, colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.secondary
+ )
+ ) { Text(text = "Decrement", color = Color.White) }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun DefaultPreview() {
+ CounterTheme {
+ View()
+ }
+}
+
+You should then be able to run the app in the simulator, and it should look like this:
+ +When we use Crux to build Web apps, the shared core is compiled to WebAssembly. +This has the advantage of sandboxing the core, physically preventing it from +performing any side-effects (which is conveniently one of the main goals of Crux +anyway!). The invariants of Crux are actually enforced by the WebAssembly +runtime.
+We do have to decide how much of our app we want to include in the WebAssembly +binary, though. Typically, if we are writing our UI in TypeScript (or +JavaScript) we would just compile our shared behavior and the Crux Core to +WebAssembly. However, if we are writing our UI in Rust we can compile the entire +app to WebAssembly.
+When building UI with React, or any other JS/TS framework, the Core API bindings +are generated in TypeScript using Mozilla's +Uniffi, and, just like with Android and +iOS we must serialize and deserialize the messages into and out of the +WebAssembly binary.
+The shared core (that contains our app's behavior) is compiled to a WebAssembly
+binary, using wasm-pack
, which
+creates an npm package for us that we can add to our project just like any other
+npm package.
The shared types are also generated by Crux as a TypeScript npm package, which
+we can add in the same way (e.g. with pnpm add
).
This section has two guides for building TypeScript UI with Crux:
+ +When building UI with Rust, we can compile the entire app to WebAssembly, and
+reference the core and the shared
crate directly. We do not have to serialize
+and deserialize messages, because the messages stay in the same memory space.
The shared core (that contains our app's behavior) and the UI code are
+compiled to a WebAssembly binary, using the relevant toolchain for the language
+and framework we are using. We use trunk
for the Yew
+and Leptos guides and dx
+for the Dioxus guide.
When using Rust throughout, we can simply use Cargo to add the shared
crate
+directly to our app.
This section has three guides for building Rust UI with Crux:
+ +These are the steps to set up and run a simple TypeScript Web app that calls +into a shared core.
+This walk-through assumes you have already added the shared
and shared_types
libraries to your repo, as described in Shared core and types.
For this walk-through, we'll use the pnpm
package manager
+for no reason other than we like it the most!
Let's create a simple Next.js app for TypeScript, using pnpx
(from pnpm
).
+You can probably accept the defaults.
pnpx create-next-app@latest
+
+When we build our app, we also want to compile the Rust core to WebAssembly so +that it can be referenced from our code.
+To do this, we'll use
+wasm-pack
, which you can
+install like this:
# with homebrew
+brew install wasm-pack
+
+# or directly
+curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
+
+Now that we have wasm-pack
installed, we can build our shared
library to
+WebAssembly for the browser.
(cd shared && wasm-pack build --target web)
+
+You might want to add a wasm:build
script to your package.json
+file, and call it when you build your nextjs project.
{
+ "scripts": {
+ "build": "pnpm run wasm:build && next build",
+ "dev": "pnpm run wasm:build && next dev",
+ "wasm:build": "cd ../shared && wasm-pack build --target web"
+ }
+}
+
+Add the shared
library as a Wasm package to your web-nextjs
project
cd web-nextjs
+pnpm add ../shared/pkg
+
+To generate the shared types for TypeScript, we can just run cargo build
from
+the root of our repository. You can check that they have been generated
+correctly:
ls --tree shared_types/generated/typescript
+shared_types/generated/typescript
+├── bincode
+│ ├── bincodeDeserializer.d.ts
+│ ├── bincodeDeserializer.js
+│ ├── bincodeDeserializer.ts
+│ ├── bincodeSerializer.d.ts
+│ ├── bincodeSerializer.js
+│ ├── bincodeSerializer.ts
+│ ├── mod.d.ts
+│ ├── mod.js
+│ └── mod.ts
+├── node_modules
+│ └── typescript -> .pnpm/typescript@4.8.4/node_modules/typescript
+├── package.json
+├── pnpm-lock.yaml
+├── serde
+│ ├── binaryDeserializer.d.ts
+│ ├── binaryDeserializer.js
+│ ├── binaryDeserializer.ts
+│ ├── binarySerializer.d.ts
+│ ├── binarySerializer.js
+│ ├── binarySerializer.ts
+│ ├── deserializer.d.ts
+│ ├── deserializer.js
+│ ├── deserializer.ts
+│ ├── mod.d.ts
+│ ├── mod.js
+│ ├── mod.ts
+│ ├── serializer.d.ts
+│ ├── serializer.js
+│ ├── serializer.ts
+│ ├── types.d.ts
+│ ├── types.js
+│ └── types.ts
+├── tsconfig.json
+└── types
+ ├── shared_types.d.ts
+ ├── shared_types.js
+ └── shared_types.ts
+
+You can see that it also generates an npm
package that we can add directly to
+our project.
pnpm add ../shared_types/generated/typescript
+
+There are other, more advanced, examples of Next.js apps in the Crux repository.
+However, we will use the simple counter example, which has shared
and shared_types
libraries that will work with the following example code.
A simple app that increments, decrements and resets a counter.
+First, let's add some boilerplate code to wrap our core and handle the
+capabilities that we are using. For this example, we only need to support the
+Render
capability, which triggers a render of the UI.
This code that wraps the core only needs to be written once — it only grows when +we need to support additional capabilities.
+Edit src/app/core.ts
to look like the following. This code sends our
+(UI-generated) events to the core, and handles any effects that the core asks
+for. In this simple example, we aren't calling any HTTP APIs or handling any
+side effects other than rendering the UI, so we just handle this render effect
+by updating the component's view
hook with the core's ViewModel.
Notice that we have to serialize and deserialize the data that we pass between +the core and the shell. This is because the core is running in a separate +WebAssembly instance, and so we can't just pass the data directly.
+import type { Dispatch, SetStateAction } from "react";
+
+import { process_event, view } from "shared/shared";
+import type { Effect, Event } from "shared_types/types/shared_types";
+import {
+ EffectVariantRender,
+ ViewModel,
+ Request,
+} from "shared_types/types/shared_types";
+import {
+ BincodeSerializer,
+ BincodeDeserializer,
+} from "shared_types/bincode/mod";
+
+export function update(
+ event: Event,
+ callback: Dispatch<SetStateAction<ViewModel>>
+) {
+ console.log("event", event);
+
+ const serializer = new BincodeSerializer();
+ event.serialize(serializer);
+
+ const effects = process_event(serializer.getBytes());
+
+ const requests = deserializeRequests(effects);
+ for (const { uuid, effect } of requests) {
+ processEffect(uuid, effect, callback);
+ }
+}
+
+function processEffect(
+ _uuid: number[],
+ effect: Effect,
+ callback: Dispatch<SetStateAction<ViewModel>>
+) {
+ console.log("effect", effect);
+
+ switch (effect.constructor) {
+ case EffectVariantRender: {
+ callback(deserializeView(view()));
+ break;
+ }
+ }
+}
+
+function deserializeRequests(bytes: Uint8Array): Request[] {
+ const deserializer = new BincodeDeserializer(bytes);
+ const len = deserializer.deserializeLen();
+ const requests: Request[] = [];
+ for (let i = 0; i < len; i++) {
+ const request = Request.deserialize(deserializer);
+ requests.push(request);
+ }
+ return requests;
+}
+
+function deserializeView(bytes: Uint8Array): ViewModel {
+ return ViewModel.deserialize(new BincodeDeserializer(bytes));
+}
+
+That switch
statement, above, is where you would handle any other effects that
+your core might ask for. For example, if your core needs to make an HTTP
+request, you would handle that here. To see an example of this, take a look at
+the
+counter example
+in the Crux repository.
Edit src/app/page.tsx
to look like the following. This code loads the
+WebAssembly core and sends it an initial event. Notice that we pass the
+setState
hook to the update function so that we can update the state in
+response to a render effect from the core.
"use client";
+
+import type { NextPage } from "next";
+import Head from "next/head";
+import { useEffect, useRef, useState } from "react";
+
+import init_core from "shared/shared";
+import {
+ ViewModel,
+ EventVariantReset,
+ EventVariantIncrement,
+ EventVariantDecrement,
+} from "shared_types/types/shared_types";
+
+import { update } from "./core";
+
+const Home: NextPage = () => {
+ const [view, setView] = useState(new ViewModel("0"));
+
+ const initialized = useRef(false);
+ useEffect(
+ () => {
+ if (!initialized.current) {
+ initialized.current = true;
+
+ init_core().then(() => {
+ // Initial event
+ update(new EventVariantReset(), setView);
+ });
+ }
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ /*once*/ []
+ );
+
+ return (
+ <>
+ <Head>
+ <title>Next.js Counter</title>
+ </Head>
+
+ <main>
+ <section className="box container has-text-centered m-5">
+ <p className="is-size-5">{view.count}</p>
+ <div className="buttons section is-centered">
+ <button
+ className="button is-primary is-danger"
+ onClick={() => update(new EventVariantReset(), setView)}
+ >
+ {"Reset"}
+ </button>
+ <button
+ className="button is-primary is-success"
+ onClick={() => update(new EventVariantIncrement(), setView)}
+ >
+ {"Increment"}
+ </button>
+ <button
+ className="button is-primary is-warning"
+ onClick={() => update(new EventVariantDecrement(), setView)}
+ >
+ {"Decrement"}
+ </button>
+ </div>
+ </section>
+ </main>
+ </>
+ );
+};
+
+export default Home;
+
+Now all we need is some CSS. First add the Bulma
package, and then import it
+in layout.tsx
.
pnpm add bulma
+
+import "bulma/css/bulma.css";
+import type { Metadata } from "next";
+import { Inter } from "next/font/google";
+
+const inter = Inter({ subsets: ["latin"] });
+
+export const metadata: Metadata = {
+ title: "Crux Simple Counter Example",
+ description: "Rust Core, TypeScript Shell (NextJS)",
+};
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+ <html lang="en">
+ <body className={inter.className}>{children}</body>
+ </html>
+ );
+}
+
+We can build our app, and serve it for the browser, in one simple step.
+pnpm dev
+
+
+These are the steps to set up and run a simple TypeScript Web app that calls +into a shared core.
+This walk-through assumes you have already added the shared
and shared_types
libraries to your repo, as described in Shared core and types.
For this walk-through, we'll use the pnpm
package manager
+for no reason other than we like it the most! You can use npm
exactly the same
+way, though.
Let's create a simple Remix app for TypeScript, using pnpx
(from pnpm
). You
+can give it a name and then probably accept the defaults.
pnpx create-remix@latest
+
+When we build our app, we also want to compile the Rust core to WebAssembly so +that it can be referenced from our code.
+To do this, we'll use
+wasm-pack
, which you can
+install like this:
# with homebrew
+brew install wasm-pack
+
+# or directly
+curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
+
+Now that we have wasm-pack
installed, we can build our shared
library to
+WebAssembly for the browser.
(cd shared && wasm-pack build --target web)
+
+You might want to add a wasm:build
script to your package.json
+file, and call it when you build your Remix project.
{
+ "scripts": {
+ "build": "pnpm run wasm:build && remix build",
+ "dev": "pnpm run wasm:build && remix dev",
+ "wasm:build": "cd ../shared && wasm-pack build --target web"
+ }
+}
+
+Add the shared
library as a Wasm package to your web-remix
project
cd web-remix
+pnpm add ../shared/pkg
+
+We want to tell the Remix server to bundle our shared
Wasm package, so we need
+to add a serverDependenciesToBundle
key to the object exported in
+remix.config.js
:
/** @type {import('@remix-run/dev').AppConfig} */
+module.exports = {
+ ignoredRouteFiles: ["**/.*"],
+
+ // make sure the server bundles our shared library
+ serverDependenciesToBundle: [/^shared.*/],
+
+ serverModuleFormat: "cjs",
+};
+
+To generate the shared types for TypeScript, we can just run cargo build
from
+the root of our repository. You can check that they have been generated
+correctly:
ls --tree shared_types/generated/typescript
+shared_types/generated/typescript
+├── bincode
+│ ├── bincodeDeserializer.d.ts
+│ ├── bincodeDeserializer.js
+│ ├── bincodeDeserializer.ts
+│ ├── bincodeSerializer.d.ts
+│ ├── bincodeSerializer.js
+│ ├── bincodeSerializer.ts
+│ ├── mod.d.ts
+│ ├── mod.js
+│ └── mod.ts
+├── node_modules
+│ └── typescript -> .pnpm/typescript@4.8.4/node_modules/typescript
+├── package.json
+├── pnpm-lock.yaml
+├── serde
+│ ├── binaryDeserializer.d.ts
+│ ├── binaryDeserializer.js
+│ ├── binaryDeserializer.ts
+│ ├── binarySerializer.d.ts
+│ ├── binarySerializer.js
+│ ├── binarySerializer.ts
+│ ├── deserializer.d.ts
+│ ├── deserializer.js
+│ ├── deserializer.ts
+│ ├── mod.d.ts
+│ ├── mod.js
+│ ├── mod.ts
+│ ├── serializer.d.ts
+│ ├── serializer.js
+│ ├── serializer.ts
+│ ├── types.d.ts
+│ ├── types.js
+│ └── types.ts
+├── tsconfig.json
+└── types
+ ├── shared_types.d.ts
+ ├── shared_types.js
+ └── shared_types.ts
+
+You can see that it also generates an npm
package that we can add directly to
+our project.
pnpm add ../shared_types/generated/typescript
+
+The app/entry.client.tsx
file is where we can load our Wasm binary. We can
+import the shared
package and then call the init
function to load the Wasm
+binary.
Note that we import
the wasm binary as well — Remix will automatically bundle
+it for us, giving it a cache-friendly hash-based name.
/**
+ * By default, Remix will handle hydrating your app on the client for you.
+ * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
+ * For more information, see https://remix.run/file-conventions/entry.client
+ */
+
+import { RemixBrowser } from "@remix-run/react";
+import { startTransition, StrictMode } from "react";
+import { hydrateRoot } from "react-dom/client";
+import init from "shared/shared";
+import wasm from "shared/shared_bg.wasm";
+
+init(wasm).then(() => {
+ startTransition(() => {
+ hydrateRoot(
+ document,
+ <StrictMode>
+ <RemixBrowser />
+ </StrictMode>
+ );
+ });
+});
+
+We will use the simple counter example, which has shared
and shared_types
libraries that will work with the following example code.
A simple app that increments, decrements and resets a counter.
+First, let's add some boilerplate code to wrap our core and handle the
+capabilities that we are using. For this example, we only need to support the
+Render
capability, which triggers a render of the UI.
This code that wraps the core only needs to be written once — it only grows when +we need to support additional capabilities.
+Edit app/core.ts
to look like the following. This code sends our
+(UI-generated) events to the core, and handles any effects that the core asks
+for. In this simple example, we aren't calling any HTTP APIs or handling any
+side effects other than rendering the UI, so we just handle this render effect
+by updating the component's view
hook with the core's ViewModel.
Notice that we have to serialize and deserialize the data that we pass between +the core and the shell. This is because the core is running in a separate +WebAssembly instance, and so we can't just pass the data directly.
+import type { Dispatch, SetStateAction } from "react";
+
+import { process_event, view } from "shared/shared";
+import type { Effect, Event } from "shared_types/types/shared_types";
+import {
+ EffectVariantRender,
+ ViewModel,
+ Request,
+} from "shared_types/types/shared_types";
+import {
+ BincodeSerializer,
+ BincodeDeserializer,
+} from "shared_types/bincode/mod";
+
+export function update(
+ event: Event,
+ callback: Dispatch<SetStateAction<ViewModel>>
+) {
+ console.log("event", event);
+
+ const serializer = new BincodeSerializer();
+ event.serialize(serializer);
+
+ const effects = process_event(serializer.getBytes());
+
+ const requests = deserializeRequests(effects);
+ for (const { uuid, effect } of requests) {
+ processEffect(uuid, effect, callback);
+ }
+}
+
+function processEffect(
+ _uuid: number[],
+ effect: Effect,
+ callback: Dispatch<SetStateAction<ViewModel>>
+) {
+ console.log("effect", effect);
+
+ switch (effect.constructor) {
+ case EffectVariantRender: {
+ callback(deserializeView(view()));
+ break;
+ }
+ }
+}
+
+function deserializeRequests(bytes: Uint8Array): Request[] {
+ const deserializer = new BincodeDeserializer(bytes);
+ const len = deserializer.deserializeLen();
+ const requests: Request[] = [];
+ for (let i = 0; i < len; i++) {
+ const request = Request.deserialize(deserializer);
+ requests.push(request);
+ }
+ return requests;
+}
+
+function deserializeView(bytes: Uint8Array): ViewModel {
+ return ViewModel.deserialize(new BincodeDeserializer(bytes));
+}
+
+That switch
statement, above, is where you would handle any other effects that
+your core might ask for. For example, if your core needs to make an HTTP
+request, you would handle that here. To see an example of this, take a look at
+the
+counter example
+in the Crux repository.
Edit app/routes/_index.tsx
to look like the following. Notice that we pass the
+setState
hook to the update function so that we can update the state in
+response to a render effect from the core (as seen above).
import { useEffect, useRef, useState } from "react";
+
+import {
+ ViewModel,
+ EventVariantReset,
+ EventVariantIncrement,
+ EventVariantDecrement,
+} from "shared_types/types/shared_types";
+import { update } from "../core";
+
+export const meta = () => {
+ return [
+ { title: "New Remix App" },
+ { name: "description", content: "Welcome to Remix!" },
+ ];
+};
+
+export default function Index() {
+ const [view, setView] = useState(new ViewModel("0"));
+
+ const initialized = useRef(false);
+
+ useEffect(
+ () => {
+ if (!initialized.current) {
+ initialized.current = true;
+
+ // Initial event
+ update(new EventVariantReset(), setView);
+ }
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ /*once*/ []
+ );
+
+ return (
+ <main>
+ <section className="box container has-text-centered m-5">
+ <p className="is-size-5">{view.count}</p>
+ <div className="buttons section is-centered">
+ <button
+ className="button is-primary is-danger"
+ onClick={() => update(new EventVariantReset(), setView)}
+ >
+ {"Reset"}
+ </button>
+ <button
+ className="button is-primary is-success"
+ onClick={() => update(new EventVariantIncrement(), setView)}
+ >
+ {"Increment"}
+ </button>
+ <button
+ className="button is-primary is-warning"
+ onClick={() => update(new EventVariantDecrement(), setView)}
+ >
+ {"Decrement"}
+ </button>
+ </div>
+ </section>
+ </main>
+ );
+}
+
+Now all we need is some CSS.
+To add a CSS stylesheet, we can add it to the Links
export in the
+app/root.tsx
file.
export const links: LinksFunction = () => [
+ ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
+ {
+ rel: "stylesheet",
+ href: "https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css",
+ },
+];
+
+We can build our app, and serve it for the browser, in one simple step.
+pnpm dev
+
+
+These are the steps to set up and run a simple TypeScript Web app that calls +into a shared core.
+This walk-through assumes you have already added the shared
and shared_types
libraries to your repo, as described in Shared core and types.
Let's create a new project which we'll call web-svelte
:
mkdir web-svelte
+cd web-svelte
+mkdir src/
+
+When we build our app, we also want to compile the Rust core to WebAssembly so +that it can be referenced from our code.
+To do this, we'll use
+wasm-pack
, which you can
+install like this:
# with homebrew
+brew install wasm-pack
+
+# or directly
+curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
+
+Now that we have wasm-pack
installed, we can build our shared
library to
+WebAssembly for the browser.
(cd shared && wasm-pack build --target web)
+
+Create a package.json
file and add the wasm:build
script:
"scripts": {
+ "wasm:build": "cd ../shared && wasm-pack build --target web",
+ "start": "npm run build && concurrently -k \"parcel serve src/index.html --port 8080 --hmr-port 1174\" ",
+ "build": "pnpm run wasm:build && parcel build src/index.html",
+ "dev": "pnpm run wasm:build && parcel build src/index.html"
+ },
+
+Also make sure to add the shared
and shared_types
as local dependencies to the package.json
:
"dependencies": {
+ // ...
+ "shared": "file:../shared/pkg",
+ "shared_types": "file:../shared_types/generated/typescript"
+ // ...
+ }
+
+Create a main.ts
file in src/
:
import "reflect-metadata";
+
+import App from "./App.svelte";
+
+document.body.setAttribute("data-app-container", "");
+
+export default new App({ target: document.body });
+
+This file is the main entry point which instantiates a new App
object.
+The App
object is defined in the App.svelte
file:
<script lang="ts">
+ import "bulma/css/bulma.css";
+ import { onMount } from "svelte";
+ import { update } from "./core";
+ import view from "./core";
+ import {
+ EventVariantReset,
+ EventVariantIncrement,
+ EventVariantDecrement,
+ } from "shared_types/types/shared_types";
+
+ onMount(async () => {
+ console.log("mount");
+ });
+</script>
+
+<section class="box container has-text-centered m-5">
+ <p class="is-size-5">{$view.count}</p>
+ <div class="buttons section is-centered">
+ <button
+ class="button is-primary is-danger"
+ on:click={() => update(new EventVariantReset())}
+ >
+ {"Reset"}
+ </button>
+ <button
+ class="button is-primary is-success"
+ on:click={() => update(new EventVariantIncrement())}
+ >
+ {"Increment"}
+ </button>
+ <button
+ class="button is-primary is-warning"
+ on:click={() => update(new EventVariantDecrement())}
+ >
+ {"Decrement"}
+ </button>
+ </div>
+</section>
+
+This file implements the UI and the behaviour for various user actions.
+In order to serve the Svelte app, create a index.html
in src/
:
<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5" />
+ <title>Simple Counter</title>
+ <meta name="apple-mobile-web-app-title" content="Simple Counter" />
+ <meta name="application-name" content="Simple Counter" />
+</head>
+<body>
+ <script type="module" src="main.ts"></script>
+</body>
+</html>
+
+This file ensures that the main entry point gets called.
+Let's add a file src/core.ts
which will wrap our core and handle the
+capabilities that we are using.
+import { process_event, view } from "shared";
+import initCore from "shared";
+import { writable } from 'svelte/store';
+import { EffectVariantRender,
+ ViewModel,
+ Request, } from "shared_types/types/shared_types";
+import type { Effect, Event } from "shared_types/types/shared_types";
+import {
+ BincodeSerializer,
+ BincodeDeserializer,
+} from "shared_types/bincode/mod";
+
+const { subscribe, set } = writable(new ViewModel("0"));
+
+export async function update(
+ event: Event
+) {
+ console.log("event", event);
+ await initCore();
+
+ const serializer = new BincodeSerializer();
+ event.serialize(serializer);
+
+ const effects = process_event(serializer.getBytes());
+ const requests = deserializeRequests(effects);
+ for (const { uuid, effect } of requests) {
+ processEffect(uuid, effect);
+ }
+}
+
+function processEffect(
+ _uuid: number[],
+ effect: Effect
+) {
+ console.log("effect", effect);
+ switch (effect.constructor) {
+ case EffectVariantRender: {
+ set(deserializeView(view()));
+ break;
+ }
+ }
+}
+
+function deserializeRequests(bytes: Uint8Array): Request[] {
+ const deserializer = new BincodeDeserializer(bytes);
+ const len = deserializer.deserializeLen();
+ const requests: Request[] = [];
+ for (let i = 0; i < len; i++) {
+ const request = Request.deserialize(deserializer);
+ requests.push(request);
+ }
+ return requests;
+}
+
+function deserializeView(bytes: Uint8Array): ViewModel {
+ return ViewModel.deserialize(new BincodeDeserializer(bytes));
+}
+
+export default {
+ subscribe
+}
+
+This code sends our (UI-generated) events to the core, and handles any effects that the core asks
+for via the update()
function. Notice that we are creating a store
+to update and manage the view model. Whenever update()
gets called to send an event to the core, we are
+fetching the updated view model via view()
and are udpating the value in the store. Svelte components can
+import and use the store values.
Notice that we have to serialize and deserialize the data that we pass between +the core and the shell. This is because the core is running in a separate +WebAssembly instance, and so we can't just pass the data directly.
+We can build our app, and serve it for the browser, in one simple step.
+npm start
+
+
+These are the steps to set up and run a simple Rust Web app that calls into a +shared core.
+This walk-through assumes you have already added the shared
and shared_types
libraries to your repo, as described in Shared core and types.
There are many frameworks available for writing Web applications in Rust. We've chosen Yew for this walk-through because it is arguably the most mature. However, a similar setup would work for any framework that compiles to WebAssembly.
+Our Yew app is just a new Rust project, which we can create with Cargo. For this
+example we'll call it web-yew
.
cargo new web-yew
+
+We'll also want to add this new project to our Cargo workspace, by editing the
+root Cargo.toml
file.
[workspace]
+members = ["shared", "web-yew"]
+
+Now we can start fleshing out our project. Let's add some dependencies to
+web-yew/Cargo.toml
.
[package]
+name = "web-yew"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+shared = { path = "../shared" }
+yew = { version = "0.21.0", features = ["csr"] }
+
+We'll also need a file called index.html
, to serve our app.
<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Yew Counter</title>
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
+ </head>
+</html>
+
+There are several, more advanced, +examples of Yew apps +in the Crux repository.
+However, we will use the
+simple counter example,
+which has shared
and shared_types
libraries that will work with the
+following example code.
A simple app that increments, decrements and resets a counter.
+First, let's add some boilerplate code to wrap our core and handle the
+capabilities that we are using. For this example, we only need to support the
+Render
capability, which triggers a render of the UI.
This code that wraps the core only needs to be written once — it only grows when +we need to support additional capabilities.
+Edit src/core.rs
to look like the following. This code sends our
+(UI-generated) events to the core, and handles any effects that the core asks
+for. In this simple example, we aren't calling any HTTP APIs or handling any
+side effects other than rendering the UI, so we just handle this render effect
+by sending it directly back to the Yew component. Note that we wrap the effect
+in a Message enum because Yew components have a single associated type for
+messages and we need that to include both the events that the UI raises (to send
+to the core) and the effects that the core uses to request side effects from the
+shell.
Also note that because both our core and our shell are written in Rust (and run +in the same memory space), we do not need to serialize and deserialize the data +that we pass between them. We can just pass the data directly.
+use shared::{Capabilities, Counter, Effect, Event};
+use std::rc::Rc;
+use yew::Callback;
+
+pub type Core = Rc<shared::Core<Effect, Counter>>;
+
+pub enum Message {
+ Event(Event),
+ Effect(Effect),
+}
+
+pub fn new() -> Core {
+ Rc::new(shared::Core::new::<Capabilities>())
+}
+
+pub fn update(core: &Core, event: Event, callback: &Callback<Message>) {
+ for effect in core.process_event(event) {
+ process_effect(core, effect, callback);
+ }
+}
+
+pub fn process_effect(_core: &Core, effect: Effect, callback: &Callback<Message>) {
+ match effect {
+ render @ Effect::Render(_) => callback.emit(Message::Effect(render)),
+ }
+}
+That match
statement, above, is where you would handle any other effects that
+your core might ask for. For example, if your core needs to make an HTTP
+request, you would handle that here. To see an example of this, take a look at
+the
+counter example
+in the Crux repository.
Edit src/main.rs
to look like the following. The update
function is
+interesting here. We set up a Callback
to receive messages from the core and
+feed them back into Yew's event loop. Then we test to see if the incoming
+message is an Event
(raised by UI interaction) and if so we use it to update
+the core, returning false to indicate that the re-render will happen later. In
+this app, we can assume that any other message is a render Effect
and so we
+return true indicating to Yew that we do want to re-render.
mod core;
+
+use crate::core::{Core, Message};
+use shared::Event;
+use yew::prelude::*;
+
+#[derive(Default)]
+struct RootComponent {
+ core: Core,
+}
+
+impl Component for RootComponent {
+ type Message = Message;
+ type Properties = ();
+
+ fn create(_ctx: &Context<Self>) -> Self {
+ Self { core: core::new() }
+ }
+
+ fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
+ let link = ctx.link().clone();
+ let callback = Callback::from(move |msg| {
+ link.send_message(msg);
+ });
+ if let Message::Event(event) = msg {
+ core::update(&self.core, event, &callback);
+ false
+ } else {
+ true
+ }
+ }
+
+ fn view(&self, ctx: &Context<Self>) -> Html {
+ let link = ctx.link();
+ let view = self.core.view();
+
+ html! {
+ <section class="box container has-text-centered m-5">
+ <p class="is-size-5">{&view.count}</p>
+ <div class="buttons section is-centered">
+ <button class="button is-primary is-danger"
+ onclick={link.callback(|_| Message::Event(Event::Reset))}>
+ {"Reset"}
+ </button>
+ <button class="button is-primary is-success"
+ onclick={link.callback(|_| Message::Event(Event::Increment))}>
+ {"Increment"}
+ </button>
+ <button class="button is-primary is-warning"
+ onclick={link.callback(|_| Message::Event(Event::Decrement))}>
+ {"Decrement"}
+ </button>
+ </div>
+ </section>
+ }
+ }
+}
+
+fn main() {
+ yew::Renderer::<RootComponent>::new().render();
+}
+The easiest way to compile the app to WebAssembly and serve it in our web page
+is to use trunk
, which we can install with
+Homebrew (brew install trunk
) or Cargo
+(cargo install trunk
).
We can build our app, serve it and open it in our browser, in one simple step.
+trunk serve --open
+
+
+These are the steps to set up and run a simple Rust Web app that calls into a +shared core.
+This walk-through assumes you have already added the shared
and shared_types
libraries to your repo, as described in Shared core and types.
There are many frameworks available for writing Web applications in Rust. Here we're choosing Leptos for this walk-through as a way to demonstrate how Crux can work with web frameworks that use fine-grained reactivity rather than the conceptual full re-rendering of React. However, a similar setup would work for other frameworks that compile to WebAssembly.
+Our Leptos app is just a new Rust project, which we can create with Cargo. For
+this example we'll call it web-leptos
.
cargo new web-leptos
+
+We'll also want to add this new project to our Cargo workspace, by editing the
+root Cargo.toml
file.
[workspace]
+members = ["shared", "web-leptos"]
+
+Now we can cd
into the web-leptos
directory and start fleshing out our
+project. Let's add some dependencies to shared/Cargo.toml
.
[package]
+name = "web-leptos"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+leptos = { version = "0.5.3", features = ["csr"] }
+shared = { path = "../shared" }
+
+If using nightly Rust, you can enable the "nightly" feature for Leptos. +When you do this, the signals become functions that can be called directly.
+However in our examples we are using the stable channel and so have to use
+the get()
and update()
functions explicitly.
We'll also need a file called index.html
, to serve our app.
<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Leptos Counter</title>
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
+ </head>
+ </head>
+ <body></body>
+</html>
+
+There is slightly more advanced +example of a +Leptos app in the Crux repository.
+However, we will use the
+simple counter example,
+which has shared
and shared_types
libraries that will work with the
+following example code.
A simple app that increments, decrements and resets a counter.
+First, let's add some boilerplate code to wrap our core and handle the
+capabilities that we are using. For this example, we only need to support the
+Render
capability, which triggers a render of the UI.
This code that wraps the core only needs to be written once — it only grows when +we need to support additional capabilities.
+Edit src/core.rs
to look like the following. This code sends our
+(UI-generated) events to the core, and handles any effects that the core asks
+for. In this simple example, we aren't calling any HTTP APIs or handling any
+side effects other than rendering the UI, so we just handle this render effect
+by sending the new ViewModel to the relevant Leptos signal.
Also note that because both our core and our shell are written in Rust (and run +in the same memory space), we do not need to serialize and deserialize the data +that we pass between them. We can just pass the data directly.
+use std::rc::Rc;
+
+use leptos::{SignalUpdate, WriteSignal};
+use shared::{Capabilities, Counter, Effect, Event, ViewModel};
+
+pub type Core = Rc<shared::Core<Effect, Counter>>;
+
+pub fn new() -> Core {
+ Rc::new(shared::Core::new::<Capabilities>())
+}
+
+pub fn update(core: &Core, event: Event, render: WriteSignal<ViewModel>) {
+ for effect in core.process_event(event) {
+ process_effect(core, effect, render);
+ }
+}
+
+pub fn process_effect(core: &Core, effect: Effect, render: WriteSignal<ViewModel>) {
+ match effect {
+ Effect::Render(_) => {
+ render.update(|view| *view = core.view());
+ }
+ };
+}
+That match
statement, above, is where you would handle any other effects that
+your core might ask for. For example, if your core needs to make an HTTP
+request, you would handle that here. To see an example of this, take a look at
+the
+counter example
+in the Crux repository.
Edit src/main.rs
to look like the following. This code creates two signals
+— one to update the view (which starts off with the core's current view), and
+the other to capture events from the UI (which starts of by sending the reset
+event). We also create an effect that sends these events into the core whenever
+they are raised.
mod core;
+
+use leptos::{component, create_effect, create_signal, view, IntoView, SignalGet, SignalUpdate};
+use shared::Event;
+
+#[component]
+fn RootComponent() -> impl IntoView {
+ let core = core::new();
+ let (view, render) = create_signal(core.view());
+ let (event, set_event) = create_signal(Event::Reset);
+
+ create_effect(move |_| {
+ core::update(&core, event.get(), render);
+ });
+
+ view! {
+ <section class="box container has-text-centered m-5">
+ <p class="is-size-5">{move || view.get().count}</p>
+ <div class="buttons section is-centered">
+ <button class="button is-primary is-danger"
+ on:click=move |_| set_event.update(|value| *value = Event::Reset)
+ >
+ {"Reset"}
+ </button>
+ <button class="button is-primary is-success"
+ on:click=move |_| set_event.update(|value| *value = Event::Increment)
+ >
+ {"Increment"}
+ </button>
+ <button class="button is-primary is-warning"
+ on:click=move |_| set_event.update(|value| *value = Event::Decrement)
+ >
+ {"Decrement"}
+ </button>
+ </div>
+ </section>
+ }
+}
+
+fn main() {
+ leptos::mount_to_body(|| {
+ view! { <RootComponent /> }
+ });
+}
+The easiest way to compile the app to WebAssembly and serve it in our web page
+is to use trunk
, which we can install with
+Homebrew (brew install trunk
) or Cargo
+(cargo install trunk
).
We can build our app, serve it and open it in our browser, in one simple step.
+trunk serve --open
+
+
+These are the steps to set up and run a simple Rust Web app that calls into a +shared core.
+This walk-through assumes you have already added the shared
and shared_types
libraries to your repo, as described in Shared core and types.
There are many frameworks available for writing Web applications in Rust. We've chosen Dioxus for this walk-through. However, a similar setup would work for other frameworks that compile to WebAssembly.
+Dioxus has a CLI tool called dx
, which can initialize, build and serve our app.
cargo install dioxus-cli
+
+Test that the executable is available.
+dx --help
+
+Before we create a new app, let's add it to our Cargo workspace (so that the
+dx
tool won't complain), by editing the root Cargo.toml
file.
For this example, we'll call the app web-dioxus
.
[workspace]
+members = ["shared", "web-dioxus"]
+
+Now we can create a new Dioxus app. The tool asks for a project name, which
+we'll provide as web-dioxus
.
dx create
+
+cd web-dioxus
+
+Now we can start fleshing out our project. Let's add some dependencies to the
+project's Cargo.toml
.
[package]
+name = "web-dioxus"
+version = "0.1.0"
+authors = ["Stuart Harris <stuart.harris@red-badger.com>"]
+edition = "2021"
+
+[dependencies]
+console_error_panic_hook = "0.1.7"
+dioxus = "0.4"
+dioxus-logger = "0.4.1"
+dioxus-web = "0.4"
+futures-util = "0.3.29"
+log = "0.4.20"
+shared = { path = "../shared" }
+
+There is slightly more advanced example of a Dioxus app in the Crux repository.
+However, we will use the simple counter example, which has shared
and shared_types
libraries that will work with the following example code.
A simple app that increments, decrements and resets a counter.
+First, let's add some boilerplate code to wrap our core and handle the
+capabilities that we are using. For this example, we only need to support the
+Render
capability, which triggers a render of the UI.
This code that wraps the core only needs to be written once — it only grows when +we need to support additional capabilities.
+Edit src/core.rs
to look like the following. This code sends our
+(UI-generated) events to the core, and handles any effects that the core asks
+for. In this simple example, we aren't calling any HTTP APIs or handling any
+side effects other than rendering the UI, so we just handle this render effect
+by updating the component's view
hook with the core's ViewModel.
Also note that because both our core and our shell are written in Rust (and run +in the same memory space), we do not need to serialize and deserialize the data +that we pass between them. We can just pass the data directly.
+use dioxus::prelude::{UnboundedReceiver, UseState};
+use futures_util::StreamExt;
+use std::rc::Rc;
+
+use shared::{Capabilities, Counter, Effect, Event, ViewModel};
+
+pub type Core = Rc<shared::Core<Effect, Counter>>;
+
+pub fn new() -> Core {
+ Rc::new(shared::Core::new::<Capabilities>())
+}
+
+pub async fn core_service(
+ core: &Core,
+ mut rx: UnboundedReceiver<Event>,
+ view: UseState<ViewModel>,
+) {
+ while let Some(event) = rx.next().await {
+ update(core, event, &view);
+ }
+}
+
+pub fn update(core: &Core, event: Event, view: &UseState<ViewModel>) {
+ log::debug!("event: {:?}", event);
+
+ for effect in core.process_event(event) {
+ process_effect(core, effect, view);
+ }
+}
+
+pub fn process_effect(core: &Core, effect: Effect, view: &UseState<ViewModel>) {
+ log::debug!("effect: {:?}", effect);
+
+ match effect {
+ Effect::Render(_) => {
+ view.set(core.view());
+ }
+ };
+}
+That match
statement, above, is where you would handle any other effects that
+your core might ask for. For example, if your core needs to make an HTTP
+request, you would handle that here. To see an example of this, take a look at
+the
+counter example
+in the Crux repository.
Edit src/main.rs
to look like the following. This code sets up the Dioxus app,
+and connects the core to the UI. Not only do we create a hook for the view state
+but we also create a coroutine that plugs in the Dioxus "service" we defined
+above to constantly send any events from the UI to the core.
mod core;
+
+use dioxus::prelude::*;
+use dioxus_web::Config;
+use log::LevelFilter;
+
+use shared::Event;
+
+use crate::core::Core;
+
+fn app(cx: Scope<Core>) -> Element {
+ let core = cx.props;
+ let view = use_state(cx, || core.view());
+ let dispatcher = use_coroutine(cx, |rx| {
+ to_owned![core, view];
+ async move { core::core_service(&core, rx, view).await }
+ });
+
+ render! {
+ main {
+ section { class: "section has-text-centered",
+ p { class: "is-size-5", "{view.count}" }
+ div { class: "buttons section is-centered",
+ button { class:"button is-primary is-danger",
+ onclick: move |_| {
+ dispatcher.send(Event::Reset);
+ },
+ "Reset"
+ }
+ button { class:"button is-primary is-success",
+ onclick: move |_| {
+ dispatcher.send(Event::Increment);
+ },
+ "Increment"
+ }
+ button { class:"button is-primary is-warning",
+ onclick: move |_| {
+ dispatcher.send(Event::Decrement);
+ },
+ "Decrement"
+ }
+ }
+ }
+ }
+ }
+}
+
+fn main() {
+ dioxus_logger::init(LevelFilter::Debug).expect("failed to init logger");
+ console_error_panic_hook::set_once();
+
+ dioxus_web::launch_with_props(app, core::new(), Config::new());
+}
+We can add a title and a stylesheet by editing
+examples/simple_counter/web-dioxus/Dioxus.toml
.
[application]
+name = "web-dioxus"
+default_platform = "web"
+out_dir = "dist"
+asset_dir = "public"
+
+[web.app]
+title = "Crux Simple Counter example"
+
+[web.watcher]
+reload_html = true
+watch_path = ["src", "public"]
+
+[web.resource]
+style = ["https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css"]
+script = []
+
+[web.resource.dev]
+script = []
+
+Now we can build our app and serve it in one simple step.
+dx serve
+
+
+As the first step, we will build a simple application, starting with a classic +Hello World, adding some state, and finally a remote API call. We will focus on +the core, rely on tests to tell us things work, and return to the shell a little +later, so unfortunately there won't be much to see until then.
+If you want to follow along, you should start by following the +Shared core and types, guide to set up the +project.
+You can find the full code for this part of the guide here
+To start with, we need a struct
to be the root of our app.
#[derive(Default)]
+pub struct Hello;
+We need to implement Default
so that Crux can construct the app for us.
To turn it into an app, we need to implement the App
trait from the
+crux_core
crate.
use crux_core::App;
+
+#[derive(Default)]
+pub struct Model;
+
+impl App for Hello {}
+If you're following along, the compiler is now screaming at you that you're
+missing four associated types for the trait: Event
, Model
, ViewModel
and
+Capabilities
.
Capabilities is the more complicated of them, and to understand what it does, we +need to talk about what makes Crux different from most UI frameworks.
+One of the key design choices in Crux is that the Core is free of side-effects +(besides its internal state). Your application can never perform anything that +directly interacts with the environment around it - no network calls, no +reading/writing files, and (somewhat obviously) not even updating the screen. +Actually doing all those things is the job of the Shell, the core can only +ask for them to be done.
+This makes the core portable between platforms, and, importantly, really easy to +test. It also separates the intent, the "functional" requirements, from the +implementation of the side-effects and the "non-functional" requirements (NFRs). +For example, your application knows it wants to store data in a SQL database, +but it doesn't need to know or care whether that database is local or remote. +That decision can even change as the application evolves, and be different on +each platform. If you want to understand this better before we carry on, you can +read a lot more about how side-effects work in Crux in the chapter on +capabilities.
+To ask the Shell for side effects, it will need to know what side effects it +needs to handle, so we will need to declare them (as an enum). Effects are +simply messages describing what should happen, and for more complex side-effects +(e.g. HTTP), they would be too unwieldy to create by hand, so to help us create +them, Crux provides capabilities - reusable libraries which give us a nice API +for requesting side-effects. We'll look at them in a lot more detail later.
+Let's start with the basics:
+use crux_core::render::Render;
+
+pub struct Capabilities {
+ render: Render<Event>,
+}
+As you can see, for now, we will use a single capability, Render
, which is
+built into Crux and available from the crux_core
crate. It simply tells the
+shell to update the screen using the latest information.
That means the core can produce a single Effect
. It will soon be more than
+one, so we'll wrap it in an enum to give ourselves space. The Effect
enum
+corresponds one to one to the Capabilities
we're using, and rather than typing
+it (and its associated trait implementations) by hand and open ourselves to
+unnecessary mistakes, we can use the Effect
derive macro from the
+crux_macros
crate.
use crux_core::render::Render;
+use crux_macros::Effect;
+
+#[derive(Effect)]
+#[effect(app = "Hello")]
+pub struct Capabilities {
+ render: Render<Event>,
+}
+Other than the derive
itself, we also need to link the effect to our app.
+We'll go into the detail of why that is in the Capabilities
+section, but the basic reason is that capabilities need to be able to send the
+app the outcomes of their work.
You probably also noticed the Event
type which capabilities are generic over,
+because they need to know the type which defines messages they can send back to
+the app. The same type is also used by the Shell to forward any user
+interactions to the Core, and in order to pass across the FFI boundary, it needs
+to be serializable. The resulting code will end up looking like this:
use crux_core::{render::Render, App};
+use crux_macros::Effect;
+use serde::{Deserialize, Serialize};
+
+#[cfg_attr(feature = "typegen", derive(crux_macros::Export))]
+#[derive(Effect)]
+#[effect(app = "Hello")]
+pub struct Capabilities {
+ render: Render<Event>,
+}
+
+#[derive(Serialize, Deserialize)]
+pub enum Event {
+ None, // we can't instantiate an empty enum, so let's have a dummy variant for now
+}
+
+#[derive(Default)]
+pub struct Hello;
+
+impl App for Hello { ... }
+In this example, we also invoke the Export
derive macro, but only when the
+typegen
feature is enabled — this is true in your shared_types
library to
+generate the foreign types for the shell. For more detail see the
+Shared core and types
+guide.
Okay, that took a little bit of effort, but with this short detour out of the +way and foundations in place, we can finally create an app and start +implementing some behavior.
+App
traitWe now have almost all the building blocks to implement the App
trait. We're
+just missing two simple types. First, a Model
to keep our app's state, it
+makes sense to make that a struct. It needs to implement Default
, which gives
+us an opportunity to set up any initial state the app might need. Second, we
+need a ViewModel
, which is a representation of what the user should see on
+screen. It might be tempting to represent the state and the view with the same
+type, but in more complicated cases it will be too constraining, and probably
+non-obvious what data are for internal bookkeeping and what should end up on
+screen, so Crux separates the concepts. Nothing stops you using the same type
+for both Model
and ViewModel
if your app is simple enough.
We'll start with a few simple types for events, model and view model.
+Now we can finally implement the trait with its two methods, update
and
+view
.
+use crux_core::{render::Render, App};
+use crux_macros::Effect;
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize)]
+pub enum Event {
+ None,
+}
+
+#[derive(Default)]
+pub struct Model;
+
+#[derive(Serialize, Deserialize)]
+pub struct ViewModel {
+ data: String,
+}
+
+#[derive(Effect)]
+#[effect(app = "Hello")]
+pub struct Capabilities {
+ render: Render<Event>,
+}
+
+#[derive(Default)]
+pub struct Hello;
+
+impl App for Hello {
+ type Event = Event;
+ type Model = Model;
+ type ViewModel = ViewModel;
+ type Capabilities = Capabilities;
+
+ fn update(&self, _event: Self::Event, _model: &mut Self::Model, caps: &Self::Capabilities) {
+ caps.render.render();
+ }
+
+ fn view(&self, _model: &Self::Model) -> Self::ViewModel {
+ ViewModel {
+ data: "Hello World".to_string(),
+ }
+ }
+}
+
+The update
function is the heart of the app. It responds to events by
+(optionally) updating the state and requesting some effects by using the
+capability's APIs.
All our update
function does is ignore all its arguments and ask the Shell to
+render the screen. It's a hello world after all.
The view
function returns the representation of what we want the Shell to show
+on screen. And true to form, it returns an instance of the ViewModel
struct
+containing Hello World!
.
That's a working hello world done, lets try it. As we said at the beginning, for +now we'll do it from tests. It may sound like a concession, but in fact, this is +the intended way for apps to be developed with Crux - from inside out, with unit +tests, focusing on behavior first and presentation later, roughly corresponding +to doing the user experience first, then the visual design.
+Here's our test:
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crux_core::{assert_effect, testing::AppTester};
+
+ #[test]
+ fn hello_says_hello_world() {
+ let hello = AppTester::<Hello, _>::default();
+ let mut model = Model::default();
+
+ // Call 'update' and request effects
+ let update = hello.update(Event::None, &mut model);
+
+ // Check update asked us to `Render`
+ assert_effect!(update, Effect::Render(_));
+
+ // Make sure the view matches our expectations
+ let actual_view = &hello.view(&model).data;
+ let expected_view = "Hello World";
+ assert_eq!(actual_view, expected_view);
+ }
+}
+
+It is a fairly underwhelming test, but it should pass (check with cargo test
).
+The test uses a testing helper from crux_core::testing
that lets us easily
+interact with the app, inspect the effects it requests and its state, without
+having to set up the machinery every time. It's not exactly complicated, but
+it's a fair amount of boiler plate code.
You can find the full code for this part of the guide +here
+Let's make things more interesting and add some behaviour. We'll teach the app +to count up and down. First, we'll need a model, which represents the state. We +could just make our model a number, but we'll go with a struct instead, so that +we can easily add more state later.
+#[derive(Default)]
+pub struct Model {
+ count: isize,
+}
+We need Default
implemented to define the initial state. For now we derive it,
+as our state is quite simple. We also update the app to show the current count:
impl App for Hello {
+// ...
+
+ type Model = Model;
+
+// ...
+
+ fn view(&self, model: &Self::Model) -> Self::ViewModel {
+ ViewModel {
+ count: format!("Count is: {}", model.count),
+ }
+ }
+}
+We'll also need a simple ViewModel
struct to hold the data that the Shell will
+render.
#[derive(Serialize, Deserialize)]
+pub struct ViewModel {
+ count: String,
+}
+Great. All that's left is adding the behaviour. That's where Event
comes in:
#[derive(Serialize, Deserialize)]
+pub enum Event {
+ Increment,
+ Decrement,
+ Reset,
+}
+The event type covers all the possible events the app can respond to. "Will that +not get massive really quickly??" I hear you ask. Don't worry about that, there +is a nice way to make this scale and get reuse as well. Let's +carry on. We need to actually handle those messages.
+impl App for Counter {
+ type Event = Event;
+ type Model = Model;
+ type ViewModel = ViewModel;
+ type Capabilities = Capabilities;
+
+ fn update(&self, event: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities) {
+ match event {
+ Event::Increment => model.count += 1,
+ Event::Decrement => model.count -= 1,
+ Event::Reset => model.count = 0,
+ };
+
+ caps.render.render();
+ }
+
+ fn view(&self, model: &Self::Model) -> Self::ViewModel {
+ ViewModel {
+ count: format!("Count is: {}", model.count),
+ }
+ }
+}
+// ...
+Pretty straightforward, we just do what we're told, update the state, and then +tell the Shell to render. Lets update the tests to check everything works as +expected.
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crux_core::{assert_effect, testing::AppTester};
+
+ #[test]
+ fn renders() {
+ let app = AppTester::<Counter, _>::default();
+ let mut model = Model::default();
+
+ let update = app.update(Event::Reset, &mut model);
+
+ // Check update asked us to `Render`
+ assert_effect!(update, Effect::Render(_));
+ }
+
+ #[test]
+ fn shows_initial_count() {
+ let app = AppTester::<Counter, _>::default();
+ let model = Model::default();
+
+ let actual_view = app.view(&model).count;
+ let expected_view = "Count is: 0";
+ assert_eq!(actual_view, expected_view);
+ }
+
+ #[test]
+ fn increments_count() {
+ let app = AppTester::<Counter, _>::default();
+ let mut model = Model::default();
+
+ let update = app.update(Event::Increment, &mut model);
+
+ let actual_view = app.view(&model).count;
+ let expected_view = "Count is: 1";
+ assert_eq!(actual_view, expected_view);
+
+ // Check update asked us to `Render`
+ assert_effect!(update, Effect::Render(_));
+ }
+
+ #[test]
+ fn decrements_count() {
+ let app = AppTester::<Counter, _>::default();
+ let mut model = Model::default();
+
+ let update = app.update(Event::Decrement, &mut model);
+
+ let actual_view = app.view(&model).count;
+ let expected_view = "Count is: -1";
+ assert_eq!(actual_view, expected_view);
+
+ // Check update asked us to `Render`
+ assert_effect!(update, Effect::Render(_));
+ }
+
+ #[test]
+ fn resets_count() {
+ let app = AppTester::<Counter, _>::default();
+ let mut model = Model::default();
+
+ app.update(Event::Increment, &mut model);
+ app.update(Event::Reset, &mut model);
+
+ let actual_view = app.view(&model).count;
+ let expected_view = "Count is: 0";
+ assert_eq!(actual_view, expected_view);
+ }
+
+ #[test]
+ fn counts_up_and_down() {
+ let app = AppTester::<Counter, _>::default();
+ let mut model = Model::default();
+
+ app.update(Event::Increment, &mut model);
+ app.update(Event::Reset, &mut model);
+ app.update(Event::Decrement, &mut model);
+ app.update(Event::Increment, &mut model);
+ app.update(Event::Increment, &mut model);
+
+ let actual_view = app.view(&model).count;
+ let expected_view = "Count is: 1";
+ assert_eq!(actual_view, expected_view);
+ }
+}
+Hopefully those all pass. We are now sure that when we build an actual UI for +this, it will work, and we'll be able to focus on making it looking +delightful.
+In more complicated cases, it might be helpful to inspect the model
directly.
+It's up to you to make the call of which one is more appropriate, in some sense
+it's the difference between black-box and white-box testing, so you should
+probably be doing both to get the confidence you need that your app is working.
Before we dive into the thinking behind the architecture, let's add one more +feature - a remote API call - to get a better feel for how side-effects and +capabilities work.
+You can find the full code for this part of the guide here
+We'll add a simple integration with a counter API we've prepared at +https://crux-counter.fly.dev. All it does is count up an down like our local +counter. It supports three requests
+GET /
returns the current countPOST /inc
increments the counterPOST /dec
decrements the counterAll three API calls return the state of the counter in JSON, which looks +something like this
+{
+ "value": 34,
+ "updated_at": 1673265904973
+}
+
+We can represent that with a struct, and we'll need to update the model as well.
+We can use Serde for the serialization (deserializing updated_at
from
+timestamp milliseconds to an option of DateTime
using chrono
).
We'll also update the count optimistically by keeping track of if/when the +server confirmed it (there are other ways to model these semantics, but let's +keep it straightforward for now).
+use chrono::{DateTime, Utc};
+use chrono::serde::ts_milliseconds_option::deserialize as ts_milliseconds_option;
+
+#[derive(Default, Serialize)]
+pub struct Model {
+ count: Count,
+}
+
+#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)]
+pub struct Count {
+ value: isize,
+ #[serde(deserialize_with = "ts_milliseconds_option")]
+ updated_at: Option<DateTime<Utc>>,
+}
+We also need to update the ViewModel
and the view()
function to display the
+new data.
#[derive(Serialize, Deserialize, Debug, Clone)]
+pub struct ViewModel {
+ pub text: String,
+ pub confirmed: bool,
+}
+
+...
+
+fn view(&self, model: &Self::Model) -> Self::ViewModel {
+ let suffix = match model.count.updated_at {
+ None => " (pending)".to_string(),
+ Some(d) => format!(" ({d})"),
+ };
+
+ Self::ViewModel {
+ text: model.count.value.to_string() + &suffix,
+ confirmed: model.count.updated_at.is_some(),
+ }
+}
+You can see that the view function caters to two states - the count has not yet
+been confirmed (updated_at
is None
), and having the count confirmed by the
+server.
In a real-world app, it's likely that this information would be captured in a +struct rather than converted to string inside the core, so that the UI can +decide how to present it. The date formatting, however, is an example of +something you may want to do consistently across all platforms and keep inside +the Core. When making these choices, think about who's decisions they are, and +do they need to be consistent across platforms or flexible. You will no doubt +get a number of those calls wrong, but that's ok, the type system is here to +help you refactor later and update the shells to work with the changes.
+We now have everything in place to update the update
function. Let's start
+with thinking about the events. The API does not support resetting the counter,
+so that variant goes, but we need a new one to kick off fetching the current
+state of the counter. The Core itself can't autonomously start anything, it is
+always driven by the Shell, either by the user via the UI, or as a result of a
+side-effect.
That gives us the following update function, with some placeholders:
+fn update(&self, event: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities) {
+ match event {
+ Event::Get => {
+ // TODO "GET /"
+ }
+ Event::Set(_response) => {
+ // TODO Get the data and update the model
+ caps.render.render();
+ }
+ Event::Increment => {
+ // optimistic update
+ model.count.value += 1;
+ model.count.updated_at = None;
+ caps.render.render();
+
+ // real update
+ // TODO "POST /inc"
+ }
+ Event::Decrement => {
+ // optimistic update
+ model.count.value -= 1;
+ model.count.updated_at = None;
+ caps.render.render();
+
+ // real update
+ // TODO "POST /dec"
+ }
+ }
+}
+To request the respective HTTP calls, we'll use
+crux_http
the
+built-in HTTP client. Since this is the first capability we're using, some
+things won't be immediately clear, but we should get there by the end of this
+chapter.
The first thing to know is that the HTTP responses will be sent back to the
+update function as an event. That's what the Event::Set
is for. The Event
+type looks as follows:
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
+pub enum Event {
+ // these variants are used by the Shell
+ Get,
+ Increment,
+ Decrement,
+
+ // this variant is private to the Core
+ #[serde(skip)]
+ Set(crux_http::Result<crux_http::Response<Count>>),
+}
+We decorate the Set
variant with #[serde(skip)]
for two reasons: one,
+there's currently a technical limitation stopping us easily serializing
+crux_http::Response
, and two, there's no reason that variant should ever be
+sent by the Shell across the FFI boundary, which is the reason for the need to
+serialize in the first place — in a way, it is private to the Core.
Finally, let's get rid of those TODOs. We'll need to add crux_http in the
+Capabilities
type, so that the update
function has access to it:
use crux_http::Http;
+
+#[derive(Effect)]
+pub struct Capabilities {
+ pub http: Http<Event>,
+ pub render: Render<Event>,
+}
+This may seem like needless boilerplate, but it allows us to only use the
+capabilities we need and, more importantly, allow capabilities to be built by
+anyone. Later on, we'll also see that Crux apps compose, relying
+on each app's Capabilities
type to declare its needs, and making sure the
+necessary capabilities exist in the parent app.
We can now implement those TODOs, so lets do it.
+const API_URL: &str = "https://crux-counter.fly.dev";
+
+//...
+
+fn update(&self, event: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities) {
+ match event {
+ Event::Get => {
+ caps.http.get(API_URL).expect_json().send(Event::Set);
+ }
+ Event::Set(Ok(mut response)) => {
+ let count = response.take_body().unwrap();
+ model.count = count;
+ caps.render.render();
+ }
+ Event::Set(Err(_)) => {
+ panic!("Oh no something went wrong");
+ }
+ Event::Increment => {
+ // optimistic update
+ model.count = Count {
+ value: model.count.value + 1,
+ updated_at: None,
+ };
+ caps.render.render();
+
+ // real update
+ let base = Url::parse(API_URL).unwrap();
+ let url = base.join("/inc").unwrap();
+ caps.http.post(url).expect_json().send(Event::Set);
+ }
+ Event::Decrement => {
+ // optimistic update
+ model.count = Count {
+ value: model.count.value - 1,
+ updated_at: None,
+ };
+ caps.render.render();
+
+ // real update
+ let base = Url::parse(API_URL).unwrap();
+ let url = base.join("/dec").unwrap();
+ caps.http.post(url).expect_json().send(Event::Set);
+ }
+ }
+ }
+
+There's a few things of note. The first one is that the .send
API at the end
+of each chain of calls to crux_http
expects a function that wraps its argument
+(a Result
of a http response) in a variant of Event
. Fortunately, enum tuple
+variants create just such a function, and we can use it. The way to read the
+call is "Send a get request, parse the response as JSON, which should be
+deserialized as a Count
, and then call me again with Event::Set
carrying the
+result". Interestingly, we didn't need to specifically mention the Count
type,
+as the type inference from the Event::Set
variant is enough, making it really
+easy to read.
The other thing of note is that the capability calls don't block. They queue up +requests to send to the shell and execution continues immediately. The requests +will be sent in the order they were queued and the asynchronous execution is the +job of the shell.
+You can find the the complete example, including the shell implementations +in the Crux repo. +It's interesting to take a closer look at the unit tests
+ /// Test that a `Get` event causes the app to fetch the current
+ /// counter value from the web API
+ #[test]
+ fn get_counter() {
+ // instantiate our app via the test harness, which gives us access to the model
+ let app = AppTester::<App, _>::default();
+
+ // set up our initial model
+ let mut model = Model::default();
+
+ // send a `Get` event to the app
+ let mut update = app.update(Event::Get, &mut model);
+
+ // check that the app emitted an HTTP request,
+ // capturing the request in the process
+ assert_let!(Effect::Http(request), &mut update.effects[0]);
+
+ // check that the request is a GET to the correct URL
+ let actual = &request.operation;
+ let expected = &HttpRequest::get("https://crux-counter.fly.dev/").build();
+ assert_eq!(actual, expected);
+
+ // resolve the request with a simulated response from the web API
+ let response = HttpResponse::ok()
+ .body(r#"{ "value": 1, "updated_at": 1672531200000 }"#)
+ .build();
+ let update = app.resolve(request, response).expect("an update");
+
+ // check that the app emitted an (internal) event to update the model
+ let actual = update.events;
+ let expected = vec![Event::Set(Ok(ResponseBuilder::ok()
+ .body(Count {
+ value: 1,
+ updated_at: Some(Utc.with_ymd_and_hms(2023, 1, 1, 0, 0, 0).unwrap()),
+ })
+ .build()))];
+ assert_eq!(actual, expected);
+ }
+
+ /// Test that a `Set` event causes the app to update the model
+ #[test]
+ fn set_counter() {
+ // instantiate our app via the test harness, which gives us access to the model
+ let app = AppTester::<App, _>::default();
+
+ // set up our initial model
+ let mut model = Model::default();
+
+ // send a `Set` event (containing the HTTP response) to the app
+ let update = app.update(
+ Event::Set(Ok(ResponseBuilder::ok()
+ .body(Count {
+ value: 1,
+ updated_at: Some(Utc.with_ymd_and_hms(2023, 1, 1, 0, 0, 0).unwrap()),
+ })
+ .build())),
+ &mut model,
+ );
+
+ // check that the app asked the shell to render
+ assert_effect!(update, Effect::Render(_));
+
+ // check that the view has been updated correctly
+ insta::assert_yaml_snapshot!(app.view(&mut model), @r###"
+ ---
+ text: "1 (2023-01-01 00:00:00 UTC)"
+ confirmed: true
+ "###);
+ }
+Incidentally, we're using insta
in that last
+test to assert that the view model is correct. If you don't know it already,
+check it out. The really cool thing is that if the test fails, it shows you a
+diff of the actual and expected output, and if you're happy with the new output,
+you can accept the change (or not) by running cargo insta review
— it will
+update the code for you to reflect the change. It's a really nice way to do
+snapshot testing, especially for the model and view model.
You can see how easy it is to check that the app is requesting the right side
+effects, with the right arguments, and even test a chain of interactions and
+make sure the behavior is correct, all without mocking or stubbing anything or
+worrying about async
code.
In the next chapter, we can put the example into perspective and discuss the +architecture it follows, inspired by Elm.
+Now we've had a bit of a feel for what writing Crux apps is like, we'll add more context to the different components and the overall architecture of Crux apps. The architecture is heavily inspired by Elm, and if you'd like to compare, the Architecture page of their guide is an excellent starting point.
+User Interface is fundamentally event-driven. Unlike batch or stream processing, all changes in apps with UI are driven by events happening in the outside world, most commonly the user interface itself – the user touching the screen, typing on a keyboard, executing a CLI command, etc. In response, the app changes what's shown on the screen, starts an interaction with the outside world, or both.
+The Elm architecture is the simplest way of modeling this pattern in code. User interactions (along with other changes in the outside world, such as time passing) are represented by events, and in response to them, the app updates its internal state represented by a model. The link between them is a simple, pure function which takes the model and the event, and updates the model based on the events. The actual UI on screen is a direct projection of the model. Because there is virtually no other state in the app, the model must contain enough information to decide what should be on screen.
+What we're missing is for the app to be able to respond to events from the outside world by changing the outside world. While the app can run computations and keep state, in this simplistic model, it can't read or write files, draw on screen, connect to APIs over the network, etc. It can't perform side-effects. Conceptually, we need to extend the update function to not only mutate the model, but also to emit some side-effects (or just "effects" for short).
+ +TODO a better picture focusing on the update function
+This more complete model is a function which takes an event and a model, and produces a new model and optionally some effects. This is still quite a simple and pure function, and is completely predictable, for the same inputs, it will always yield the same outputs, and that is a very important design choice.
+User interface and effects are normally where testing gets very difficult. If the application logic can directly cause changes in the outside world (or input/output — I/O, in computer parlance), the only way to verify the logic completely is to look at the result of those changes. The results, however, are pixels on screen, elements in the DOM, packets going over the network and other complex, difficult to inspect and often short-lived things. The only viable strategy (in this direct scenario) to test them is to take the role of the particular device the app is working with, and pretending to be that device – a practice known as mocking (or stubbing, or faking, depending who you talk to). The APIs used to interact with these things are really complicated though, and even if you emulate them well, tests based on this approach won't be stable against changes in that API. When the API changes, your code and your tests will both have to change, taking any confidence they gave you in the first place with them. What's more, they also differ across platforms. Now we have that problem twice or more times.
+The problem is in how apps are normally written (when written in a direct, imperative style). When it comes time to perform an effect, the most straightforward code just performs it straight away. The solution, as usual, is to add indirection. What Crux does (inspired by Elm, Haskell and others) is separate the intent from the execution. Crux's effect approach focuses on capturing the intent of the effect, not the specific implementation of executing it. The intent is captured as data to benefit from type checking and from all the tools the language already provides for working with data. The business logic can stay pure, but express all the behaviour: state changes and effects. The intent is also the thing that needs to be tested. We can reasonably afford to trust that the authors of a HTTP client library, for example, have tested it and it does what it promises to do — all we need to check is that we're sending the right requests1.
+In Elm, the responsibility to execute the requested effects falls on the Elm runtime. Crux is very similar, except both the app and (some of) the runtime is your responsibility. This means some more work, but it also means you only bring what you need and nothing more, both in terms of supported platforms and the necessary APIs.
+In Crux, business logic written in Rust is captured in the update function mentioned above and the other pieces that the function needs: events, model and effects, each represented by a type. This code forms a Core, which is portable, and really easily testable.
+The execution of effects, including drawing the user interface, is done in a native Shell. Its job is to draw the appropriate UI on screen, translate user interactions into events to send to the Core, and when requested, perform effects and return their outcomes back to the Core.
+ +The Shell thus has two sides: the driving side – the interactions causing events which push the Core to action, and the driven side, which services the Core's requests for side effects. Without being prompted by the Shell, the Core does nothing, it can't – with no other I/O, there are no other triggers which could cause the Core code to run. To the Shell, the Core is a simple library, providing some computation. From the perspective of the Core, the Shell is a platform the Core runs on.
+Effects encode potentially quite complex, but common interactions, so they are the perfect candidate for some improved ergonomics in the APIs. This is where Crux capabilities come in. They provide a nicer API for creating effects, and in the future, they will likely provide implementations of the effect execution for the various supported platforms. Capabilities can also implement more complex interactions with the outside world, such as chained network API calls or processing results of effects, like parsing JSON API responses.
+We will look at how capabilities work, and will build our own in the next chapter.
+In reality, we do need to check that at least one of our HTTP requests executes successfully, but once one does, it is very likely that so long as they are described correctly, all of them will.
+In the last chapter, we spoke about Effects. In this one we'll look at the APIs your app will actually use to request them – the capabilities.
+Capabilities are reusable, platform agnostic APIs for a particular type of effect. They have two key jobs:
+From the perspective of the app, you can think of capabilities as an equivalent to SDKs. And a lot of them will provide an interface to the actual platform specific SDKs.
+The Capabilities are the key to Crux being portable across as many platforms as is sensible. Crux apps are, in a sense, built in the abstract, they describe what should happen in response to events, but not how it should happen. We think this is important both for portability, and for testing and general separation of concerns. What should happen is inherent to the product, and should behave the same way on any platform – it's part of what your app is. How it should be executed (and exactly what it looks like) often depends on the platform.
+Different platforms may support different ways, for example a biometric authentication may work very differently on various devices and some may not even support it at all, but it may also be a matter of convention. Different platforms may also have different practical restrictions: while it may be perfectly appropriate to write things to disk on one platform, but internet access can't be guaranteed (e.g. on a smart watch), on another, writing to disk may not be possible, but internet connection is virtually guaranteed (e.g. in an API service, or on an embedded device in a factory). A persistent caching capability would implement the specific storage solution differently on different platforms, but would potentially share the key format and eviction strategy across them. The hard part of designing a capability is working out exactly where to draw the line between what is the intent and what is the implementation detail, what's common across platforms and what may be different on each, and implementing the former in Rust in the capability and the latter on the native side in the Shell, however is appropriate.
+Because Capabilities can own the "language" used to express intent, and the interface to request the execution of the effect, your Crux application code can be portable onto any platform capable of executing the effect in some way. Clearly, the number of different effects we can think of, and platforms we can target is enormous, and Crux doesn't want to force you to implement the entire portfolio of them on every platform. That's why Capabilities are delivered as separate modules, typically in crates, and apps can declare which ones they need. The Shell implementations need to know how to handle all requests from those capabilities, but can choose to provide only stub implementations where appropriate. For example the Cat Facts example, uses a key-value store capability for persisting the model after every interaction, which is crucial to make the CLI shell work statefully, but the other shells generally ignore the key-value requests, because state persistence across app launches is not crucial for them. The app itself (the Core) has no idea which is the case.
+In some cases, it may also make sense to implement an app-specific capability, for effects specific to your domain, which don't have a common implementation across platforms (e.g. registering a local user). Crux does not stop you from bundling a number of capabilities alongside your apps (i.e. they don't have to come from a crate). On the other hand, it might make sense to build a capability on top of an existing lower-level capability, for example a CRDT capability may use a general pub/sub capability as transport, or a specific protocol to speak to your synchronization server (e.g. over HTTP).
+There are clearly numerous scenarios, and the best rule of thumb we can think of is "focus on the intent". Provide an API to describe the intent of side-effects and then either pass the intent straight to the shell, or translate it to a sequence of more concrete intents for the Shell to execute. And keep in mind that the more complex the intent sent to the shell, the more complex the implementation on each platform. The translation between high-level intent and low level building blocks is why Capabilities exist.
+As we've already covered, the capabilities effectively straddle the FFI boundary between the Core and the Shell. On the Core side they mediate between the FFI boundary and the application code. On the shell-side the requests produced by the capability need to be actually executed and fulfilled. Each capability therefore extends the Core/Shell interface with a set of defined (and type checked) messages, in a way that allows Crux to leverage exhaustive pattern matching on the native side to ensure all necessary capabilities required by the Core are implemented.
+At the moment the Shell implementation is up to you, but we think in the future it's likely that capability crates will come with platform native code as well, making building both the Core and the Shells easier, and allow you to focus on application behaviour in the Core and look and feel in the Shell.
+Okay, time to get practical. We'll look at what it takes (and why) to use a capability, and in the next couple of chapters, we'll continue to build one and implement the Shell side of it.
+Firstly, we need to have access to an instance of the capability in our update
function. Recall that the function signature is:
fn update(&self, msg: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities)
+We get the capabilities in the caps
argument. You may be wondering why that's necessary. At first glance, we could be able to just create a capability instance ourselves, or not need one at all, after all they just provide API to make effects. There are a few reasons.
Firstly, capabilities need to be able to send a message to the shell, more precisely, they need to be able to add to the set of effects which result from the run of the update function. Sounds like a return value to you? It kind of is, and we tried that, and the type signatures involved quickly become quite unsightly. It's not the only reason though. They also need to be able to return information back to your app by queuing up events to be dispatched to the next run of the update
function. But to be really useful, they need to be able to do a series of these things and suspend their execution in the meantime.
In order to enable all that, Crux needs to be in charge of creating the instance of the capabilities to provide context to them, which they use to do the things we just listed. We'll see the details of this in the next chapter.
+Notice that the type of the argument is Self::Capabilities
— you own the type. This is to allow you to declare which capabilities you want to use in your app. That type will most likely be a struct looking like the following:
#[derive(Effect)]
+pub struct Capabilities {
+ pub http: Http<Event>,
+ pub render: Render<Event>,
+}
+Those two types come from crux_core
and crux_http
. Two things are suspicious about the above — the Event
type, which describes your app's events and the #[derive(Effect)]
derive macro.
The latter generates an Effect
enum for you, used as the payload of the messages to the Shell. It is one of the things you will need to expose via the FFI boundary. It's the type the Shell will use to understand what is being requested from it, and it mirrors the Capabilities
struct: for each field, there is a tuple variant in the Effect enum, with the respective capability's request as payload, i.e. the data describing what's being asked of the Shell.
The Event
type argument enables the "shell side" of these capabilities to send you your specific events back as the outcome of their work. Typically, you'd probably set up an Event
variant specifically for the individual uses of each capability, like this:
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
+pub enum Event {
+ Hello,
+ #[serde(skip)]
+ Set(crux_http::Result<crux_http::Response<Counter>>), // <- this
+}
+In a real app, you'd likely have more than one interaction with a HTTP server, and would most likely need one variant for each. (#[serde(skip)]
in the above code hides the variant from the type exposed to the Shell for direct calls – this event should not be dispatched directly. The other reason for it also has to do with serialization difficulties, which we'll eventually iron out).
That's it for linking the capability into our app, now we can use it in the update
function:
fn update(&self, msg: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities) {
+ match msg {
+ Event::Get => {
+ caps.http
+ .get(API_URL)
+ .expect_json::<Counter>()
+ .send(Event::Set);
+
+ caps.render.render();
+ }
+ // ...
+You can see the use of the Event::Set
variant we just discussed. Event::Set
is technically a function with this signature:
fn Event::Set(crux_http::Result<crux_http::Response<Counter>) -> Event
+Looks a lot like a callback, doesn't it. Yep. With the difference that the result is an Event
. Generally, you should be able to completely ignore this detail and just use your variant names and the code should read pretty clearly: "When done, send me Event::Set
".
The other nuance to be aware of is that the capability calls return immediately. This should hopefully be relatively obvious by now, but all that's happening is effects are getting queued up to be requested from the Shell. In a way, capability calls are implicitly asynchronous (but you can't await them).
+That's generally all there is to it. What you'll notice is that most capabilities have essentially request/response semantics — you use their APIs, and provide an event you want back, and eventually your update function will get called with that event. Most capabilities take inputs for their effect, and return output in their outcomes, but some capabilities don't do one or either of those things. Render is an example of a capability which doesn't take payload and never calls back. You'll likely see all the different variations in Crux apps.
+Now that we know how to use capabilities, we're ready to look at building our own ones. You may never need to do that, or it might be one of the first hurdles you'll come across (and if we're honest, given how young Crux is, it's more likely the latter). Either way, it's what we'll do in the next chapter.
+In the previous chapter, we looked at the purpose of Capabilities and using them +in Crux apps. In this one, we'll go through building our own. It will be a +simple one, but real enough to show the key parts.
+We'll extend the Counter example we've built in the +Hello World chapter and make it worse. Intentionally. We'll +add a random delay before we actually update the counter, just to annoy the user +(please don't do that in your real apps). It is a silly example, but it will +allow us to demonstrate a few things:
+In fact, let's start with that.
+The first job of our capability will be to pause for a given number of +milliseconds and then send an event to the app.
+There's a number of types and traits we will need to implement to make the +capability work with the rest of Crux, so let's quickly go over them before we +start. We will need
+Capability
traitLet's start with the payload:
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+pub struct DelayOperation {
+ millis: usize
+}
+The request is just a named type holding onto a number. It will need to cross +the FFI boundary, which is why it needs to be serializable, cloneable, etc.
+We will need our request to implement the Operation
trait, which links it with
+the type of the response we expect back. In our case we expect a response, but
+there is no data, so we'll use the unit type.
use crux_core::capability::Operation;
+
+impl Operation for DelayOperation {
+ type Output = ();
+}
+Now we can implement the capability:
+use crux_core::capability::CapabilityContext;
+use crux_macros::Capability;
+
+#[derive(Capability)]
+struct Delay<Ev> {
+ context: CapabilityContext<DelayOperation, Ev>,
+}
+
+impl<Ev> Delay<Ev>
+where
+ Ev: 'static,
+{
+ pub fn new(context: CapabilityContext<DelayOperation, Ev>) -> Self {
+ Self { context }
+ }
+
+ pub fn milliseconds(&self, millis: usize, event: Ev)
+ where
+ Ev: Send,
+ {
+ let ctx = self.context.clone();
+ self.context.spawn(async move {
+ ctx.request_from_shell(DelayOperation { millis }).await;
+
+ ctx.update_app(event);
+ });
+ }
+}
+There's a fair bit going on. The capability is generic over an event type Ev
+and holds on to a CapabilityContext
. The constructor will be called by Crux
+when starting an application that uses this capability.
The milliseconds
method is our capability's public API. It takes the delay in
+milliseconds and the event to send back. In this case, we don't expect any
+payload to return, so we take the Ev
type directly. We'll shortly see what an
+event with data looks like as well.
The implementation of the method has a little bit of boilerplate to enable us to
+use async
code. First we clone the context to be able to use it in the async
+block. Then we use the context to spawn an async move
code block in which
+we'll be able to use async
/await
. This bit of code will be the same in every
+part of your capability that needs to interact with the Shell.
You can see we use two APIs to orchestrate the interaction. First
+request_from_shell
sends the delay operation we made earlier to the Shell.
+This call returns a future, which we can .await
. Once done, we use the other
+API update_app
to dispatch the event we were given. At the .await
, the task
+will be suspended, Crux will pass the operation to the Shell wrapped in the
+Effect
type we talked about in the last chapter and the Shell will use it's
+native APIs to wait for the given duration, and eventually respond. This will
+wake our task up again and we can continue working.
Finally, we need to implement the Capability
trait. This is done for us by the
+#[derive(Capability)]
macro, but it is worth looking at the generated code:
impl<Ev> Capability<Ev> for Delay<Ev> {
+ type Operation = DelayOperation;
+ type MappedSelf<MappedEv> = Delay<MappedEv>;
+
+ fn map_event<F, NewEv>(&self, f: F) -> Self::MappedSelf<NewEv>
+ where
+ F: Fn(NewEv) -> Ev + Send + Sync + Copy + 'static,
+ Ev: 'static,
+ NewEv: 'static,
+ {
+ Delay::new(self.context.map_event(f))
+ }
+}
+What on earth is that for, you ask? This allows you to derive an instance of the
+Delay
capability from an existing one and adapt it to a different Event
+type. Yes, we know, don't read that sentence again. This will be useful to allow
+composing Crux apps from smaller Crux apps to automatically wrap the child
+events in the parent events.
We will cover this in depth in the chapter about +Composable applications.
+To make the example more contrived, but also more educational, we'll add the +random delay ability. This will
+First off, we need to add the new operation in. Here we have a choice, we can +add a random delay operation, or we can add a random number generation operation +and compose the two building blocks ourselves. We'll go for the second option +because... well because this is an example.
+Since we have multiple operations now, let's make our operation an enum
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+pub enum DelayOperation {
+ GetRandom(usize, usize),
+ Delay(usize),
+}
+We now also need an output type:
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+pub enum DelayOutput {
+ Random(usize),
+ TimeUp
+}
+And that changes the Operation
trait implementation:
impl Operation for DelayOperation {
+ type Output = DelayOutput;
+}
+The updated implementation looks like the following:
+impl<Ev> Delay<Ev>
+where
+ Ev: 'static,
+{
+ pub fn new(context: CapabilityContext<DelayOperation, Ev>) -> Self {
+ Self { context }
+ }
+
+ pub fn milliseconds(&self, millis: usize, event: Ev) {
+ let ctx = self.context.clone();
+ self.context.spawn(async move {
+ ctx.request_from_shell(DelayOperation::Delay(millis)).await; // Changed
+
+ ctx.update_app(event);
+ });
+ }
+
+ pub fn random<F>(&self, min: usize, max: usize, event: F)
+ where F: Fn(usize) -> Ev
+ {
+ let ctx = self.context.clone();
+ self.context.spawn(async move {
+ let response = ctx.request_from_shell(DelayOperation::GetRandom(min, max)).await;
+
+ let DelayOutput::Random(millis) = response else {
+ panic!("Expected a random number")
+ };
+ ctx.request_from_shell(DelayOperation::Delay(millis)).await;
+
+ ctx.update_app(event(millis));
+ });
+ }
+}
+In the new API, the event handling is a little different from the original.
+Because the event has a payload, we don't simply take an Ev
, we need a
+function that returns Ev
, if given the random number. Seems cumbersome but
+you'll see using it in the update
function of our app is quite natural:
fn update(&self, event: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities) {
+ match event {
+ //
+ // ... Some events omitted
+ //
+ Event::Increment => {
+ caps.delay.random(200, 800, Event::DoIncrement);
+ }
+ Event::DoIncrement(_millis) => {
+ // optimistic update
+ model.count.value += 1;
+ model.confirmed = Some(false);
+ caps.render.render();
+
+ // real update
+ let base = Url::parse(API_URL).unwrap();
+ let url = base.join("/inc").unwrap();
+ caps.http.post(url.as_str()).expect_json().send(Event::Set);
+ }
+ Event::Decrement => {
+ caps.delay.milliseconds(500, Event::DoIncrement);
+ }
+ Event::DoDecrement => {
+ // optimistic update
+ model.count.value -= 1;
+ model.confirmed = Some(false);
+ caps.render.render();
+
+ // real update
+ let base = Url::parse(API_URL).unwrap();
+ let url = base.join("/dec").unwrap();
+ caps.http.post(url.as_str()).expect_json().send(Event::Set);
+ }
+ }
+}
+That is essentially it for the capabilities. You can check out the complete +context API +in the docs.
+One of the most compelling consequences of the Crux architecture is that it +becomes trivial to comprehensively test your application. This is because the +core is pure and therefore completely deterministic — all the side effects are +pushed to the shell.
+It's straightforward to write an exhaustive set of unit tests that give you +complete confidence in the correctness of your application code — you can test +the behavior of your application independently of platform-specific UI and API +calls.
+There is no need to mock/stub anything, and there is no need to write +integration tests.
+Not only are the unit tests easy to write, but they run extremely quickly, and +can be run in parallel.
+For example, the +Notes example app +contains complex logic related to collaborative text-editing using Conflict-free +Replicated Data Types (CRDTs). The test suite consists of 25 tests that give us +high coverage and high confidence of correctness. Many of the tests include +instantiating two instances (alice and bob) and checking that, even during +complex edits, the synchronization between them works correctly.
+This test, for example, ensures that when Alice and Bob both insert text at the +same time, they both end up with the same result. It runs in 4 milliseconds.
+#[test]
+fn two_way_sync() {
+ let (mut alice, mut bob) = make_alice_and_bob();
+
+ alice.update(Event::Insert("world".to_string()));
+ let edits = alice.edits.drain(0..).collect::<Vec<_>>();
+
+ bob.send_edits(edits.as_ref());
+
+ // Alice's inserts should go in front of Bob's cursor
+ // so we break the ambiguity of same cursor position
+ // as quickly as possible
+ bob.update(Event::Insert("Hello ".to_string()));
+ let edits = bob.edits.drain(0..).collect::<Vec<_>>();
+
+ alice.send_edits(edits.as_ref());
+
+ let alice_view = alice.view();
+ let bob_view = bob.view();
+
+ assert_eq!(alice_view.text, "Hello world".to_string());
+ assert_eq!(alice_view.text, bob_view.text);
+}
+And the full suite of 25 tests runs in 16 milliseconds.
+cargo nextest run --release -p shared
+ Finished release [optimized] target(s) in 0.07s
+ Starting 25 tests across 2 binaries
+ PASS [ 0.005s] shared app::editing_tests::handles_emoji
+ PASS [ 0.005s] shared app::editing_tests::removes_character_before_cursor
+ PASS [ 0.005s] shared app::editing_tests::moves_cursor
+ PASS [ 0.006s] shared app::editing_tests::inserts_text_at_cursor_and_renders
+ PASS [ 0.005s] shared app::editing_tests::removes_selection_on_backspace
+ PASS [ 0.005s] shared app::editing_tests::removes_character_after_cursor
+ PASS [ 0.005s] shared app::editing_tests::removes_selection_on_delete
+ PASS [ 0.007s] shared app::editing_tests::changes_selection
+ PASS [ 0.006s] shared app::editing_tests::renders_text_and_cursor
+ PASS [ 0.006s] shared app::editing_tests::replaces_empty_range_and_renders
+ PASS [ 0.005s] shared app::editing_tests::replaces_range_and_renders
+ PASS [ 0.005s] shared app::note::test::splices_text
+ PASS [ 0.005s] shared app::editing_tests::replaces_selection_and_renders
+ PASS [ 0.004s] shared app::save_load_tests::opens_a_document
+ PASS [ 0.005s] shared app::note::test::inserts_text
+ PASS [ 0.005s] shared app::save_load_tests::saves_document_when_typing_stops
+ PASS [ 0.005s] shared app::save_load_tests::starts_a_timer_after_an_edit
+ PASS [ 0.006s] shared app::save_load_tests::creates_a_document_if_it_cant_open_one
+ PASS [ 0.005s] shared app::sync_tests::concurrent_clean_edits
+ PASS [ 0.005s] shared app::sync_tests::concurrent_conflicting_edits
+ PASS [ 0.005s] shared app::sync_tests::one_way_sync
+ PASS [ 0.005s] shared app::sync_tests::remote_delete_moves_cursor
+ PASS [ 0.005s] shared app::sync_tests::remote_insert_behind_cursor
+ PASS [ 0.004s] shared app::sync_tests::two_way_sync
+ PASS [ 0.005s] shared app::sync_tests::receiving_own_edits
+------------
+ Summary [ 0.016s] 25 tests run: 25 passed, 0 skipped
+
+Crux provides a simple test harness that we can use to write unit tests for our +application code. Strictly speaking it's not needed, but it makes it easier to +avoid boilerplate and to write tests that are easy to read and understand.
+Let's take a +really simple test +from the +Notes example app +and walk through it step by step — the test replaces some selected text in a +document and checks that the correct text is rendered.
+The first thing to do is create an instance of the AppTester
test harness,
+which runs our app (NoteEditor
) and makes it easy to analyze the Event
s and
+Effect
s that are generated.
let app = AppTester::<NoteEditor, _>::default();
+The Model
is normally private to the app (NoteEditor
), but AppTester
+allows us to set it up for our test. In this case the document contains the
+string "hello"
with the last three characters selected.
let mut model = Model {
+ note: Note::with_text("hello"),
+ cursor: TextCursor::Selection(3..5),
+ ..Default::default()
+};
+Let's insert the text under the selection range. We simply create an Event
+that captures the user's action and pass it into the app's update()
method,
+along with the Model we just created (which we will be able to inspect
+afterwards).
let event = Event::Insert("ter skelter".to_string());
+let update = app.update(event, &mut model);
+We can check that the shell was asked to render by using the
+assert_effect!
+macro, which panics if none of the effects generated by the update matches the
+specified pattern.
assert_effect!(update, Effect::Render(_));
+Finally we can ask the app for its ViewModel
and use it to check that the text
+was inserted correctly and that the cursor position was updated.
let view = app.view(&model);
+
+assert_eq!(view.text, "helter skelter".to_string());
+assert_eq!(view.cursor, TextCursor::Position(14));
+Now let's take a +more complicated test +and walk through that. This test checks that a "save" timer is restarted each +time the user edits the document (after a second of no activity the document is +stored). Note that the actual timer is run by the shell (because it is a side +effect, which would make it really tricky to test) — but all we need to do is +check that the behavior of the timer is correct (i.e. started, finished and +cancelled correctly).
+Again, the first thing we need to do is create an instance of the AppTester
+test harness, which runs our app (NoteEditor
) and makes it easy to analyze the
+Event
s and Effect
s that are generated.
let app = AppTester::<NoteEditor, _>::default();
+We again need to set up a Model
that we can pass to the update()
method.
let mut model = Model {
+ note: Note::with_text("hello"),
+ cursor: TextCursor::Selection(2..4),
+ ..Default::default()
+};
+We send an Event
(e.g. raised in response to a user action) into our app in
+order to check that it does the right thing.
Here we send an Insert event, which should start a timer. We filter out just the
+Effect
s that were created by the Timer
Capability, mapping them to their
+inner Request<TimerOperation>
type.
let requests = &mut app
+ .update(Event::Insert("something".to_string()), &mut model)
+ .into_effects()
+ .filter_map(Effect::into_timer);
+There are a few things to discuss here. Firstly, the update()
method returns
+an Update
struct, which contains vectors of Event
s and Effect
s. We are
+only interested in the Effect
s, so we call into_effects()
to consume them as
+an Iterator
(there are also effects()
and effects_mut()
methods that allow
+us to borrow the Effect
s instead of consuming them, but we don't need that
+here). Secondly, we use the filter_map()
method to filter out just the
+Effect
s that were created by the Timer
Capability, using
+Effect::into_timer
to map the Effect
s to their inner
+Request<TimerOperation>
.
The Effect
derive
+macro generates filters and maps for each capability that we are using. So if
+our Capabilities
struct looked like this...
+#[cfg_attr(feature = "typegen", derive(crux_macros::Export))]
+#[derive(Effect)]
+#[effect(app = "NoteEditor")]
+pub struct Capabilities {
+ timer: Timer<Event>,
+ render: Render<Event>,
+ pub_sub: PubSub<Event>,
+ key_value: KeyValue<Event>,
+}
+... we would get the following filters and filter_maps:
+// filters
+Effect::is_timer(&self) -> bool
+Effect::is_render(&self) -> bool
+Effect::is_pub_sub(&self) -> bool
+Effect::is_key_value(&self) -> bool
+// filter_maps
+Effect::into_timer(self) -> Option<Request<TimerOperation>>
+Effect::into_render(self) -> Option<Request<RenderOperation>>
+Effect::into_pub_sub(self) -> Option<Request<PubSubOperation>>
+Effect::into_key_value(self) -> Option<Request<KeyValueOperation>>
+We want to check that the first request is a Start
operation, and that the
+timer is set to fire in 1000 milliseconds. The macro
+assert_let!()
does a pattern
+match for us and assigns the id
to a local variable called first_id
, which
+we'll use later. Finally, we don't expect any more timer requests to have been
+generated.
let mut request = requests.next().unwrap(); // this is mutable so we can resolve it later
+assert_let!(
+ TimerOperation::Start {
+ id: first_id,
+ millis: 1000
+ },
+ request.operation.clone()
+);
+assert!(requests.next().is_none());
+At this point the shell would start the timer (this is something the core can't +do as it is a side effect) and so we need to tell the app that it was created. +We do this by "resolving" the request.
+Remember that Request
s either resolve zero times (fire-and-forget, e.g. for
+Render
), once (request/response, e.g. for Http
), or many times (for streams,
+e.g. Sse
— Server-Sent Events). The Timer
capability falls into the
+"request/response" category, so we need to resolve the Start
request with a
+Created
response. This tells the app that the timer has been started, and
+allows it to cancel the timer if necessary.
Note that resolving a request could call the app's update()
method resulting
+in more Event
s being generated, which we need to feed back into the app.
let update = app
+ .resolve(&mut request, TimerOutput::Created { id: first_id }).unwrap();
+for event in update.events {
+ app.update(event, &mut model);
+}
+Before the timer fires, we'll insert another character, which should cancel the +existing timer and start a new one.
+let mut requests = app
+ .update(Event::Replace(1, 2, "a".to_string()), &mut model)
+ .into_effects()
+ .filter_map(Effect::into_timer);
+
+let cancel_request = requests.next().unwrap();
+assert_let!(
+ TimerOperation::Cancel { id: cancel_id },
+ cancel_request.operation
+);
+assert_eq!(cancel_id, first_id);
+
+let start_request = &mut requests.next().unwrap(); // this is mutable so we can resolve it later
+assert_let!(
+ TimerOperation::Start {
+ id: second_id,
+ millis: 1000
+ },
+ start_request.operation.clone()
+);
+assert_ne!(first_id, second_id);
+
+assert!(requests.next().is_none());
+Now we need to tell the app that the second timer was created.
+let update = app
+ .resolve(start_request, TimerOutput::Created { id: second_id })
+ .unwrap();
+for event in update.events {
+ app.update(event, &mut model);
+}
+In the real world, time passes and the timer fires, but all we have to do is to
+resolve our start request again, but this time with a Finished
response.
let update = app
+ .resolve(start_request, TimerOutput::Finished { id: second_id })
+ .unwrap();
+for event in update.events {
+ app.update(event, &mut model);
+}
+Another edit should result in another timer, but not in a cancellation:
+let update = app.update(Event::Backspace, &mut model);
+let mut timer_requests = update.into_effects().filter_map(Effect::into_timer);
+
+assert_let!(
+ TimerOperation::Start {
+ id: third_id,
+ millis: 1000
+ },
+ timer_requests.next().unwrap().operation
+);
+assert!(timer_requests.next().is_none()); // no cancellation
+
+assert_ne!(third_id, second_id);
+Note that this test was not about testing whether the model was updated
+correctly (that is covered in other tests) so we don't call the app's view()
+method — it's just about checking that the timer is started, cancelled and
+restarted correctly.
So far in this book, we've been taking the perspective of being inside the core +looking out. It feels like it's now time to be in the shell, looking in.
+Interestingly, we think this is also the way to approach building apps with Crux. For any one feature, start in the middle and get your behaviour established first. Write the tests without the UI and the other side-effects in the way. Give yourself maximum confidence that the feature works exactly as you expect before you muddy the water with UI components, and their look and feel.
+OK, let's talk about the shell.
+The shell only has two responsibilities:
+We'll look at these separately. But first let's remind ourselves of how we +interact with the core (now would be a good time to read +Shared core and types if you haven't already).
+The interface is message based, and uses serialization to pass data back and +forth. The core exports the types for all the data so that it can be used and +created on the shell side with safety.
+An Event
can be passed in directly, as-is. Processing of Effect
s is a little
+more complicated, because the core needs to be able to pair the outcomes of the
+effects with the original capability call, so it can return them to the right
+caller. To do that, effects are wrapped in a Request
, which tags them with a
+UUID. To respond, the same UUID needs to be passed back in.
Requests from the core are emitted serialized, and need to be deserialized +first. Both events and effect outputs need to be serialized before being passed +back to the core.
+It is likely that this will become an implementation detail and instead, Crux will provide a more ergonomic shell-side API for the interaction, hiding both the UUID pairing and the serialization (and allowing us to iterate on the FFI implementation which, we think, could work better).
+There are only three touch-points with the core.
++#![allow(unused)] +fn main() { +pub fn process_event(data: &[u8]) -> Vec<u8> { todo!() } +pub fn handle_response(uuid: &[u8], data: &[u8]) -> Vec<u8> { todo!() } +pub fn view() -> Vec<u8> { todo!() } +}
The process_event
function takes a serialized Event
(from a UI interaction)
+and returns a serialized vector of Request
s that the shell can dispatch to the
+relevant capability's shell-side code (see the section below on how the shell
+handles capabilities).
The handle_response
function, used to return capability output back into the
+core, is similar to process_event
except that it also takes a uuid
, which
+ties the output (for example a HTTP response) being submitted with it's original
+Effect
which started it (and the corresponding request which the core wrapped
+it in).
The view
function simply retrieves the serialized view model (to which the UI
+is bound) and is called by the shell after it receives a Render
request. The
+view model is a projection of the app's state – it reflects what information the
+Core wants displayed on screen.
You're probably thinking, "Whoa! I just see slices and vectors of bytes, where's
+the type safety?". Well, the answer is that we also generate all the types that
+pass through the bridge, for each language, along with serialization and
+deserialization helpers. This is done by the serde-generate
crate (see the
+section on
+Create the shared types crate).
For now we have to manually invoke the serialization code in the shell. At some point this may be abstracted away.
+In this code snippet from the
+Counter example,
+notice that we call processEvent
and handleResponse
on the core depending on
+whether we received an Event
from the UI or from a capability, respectively.
+Regardless of which core function we call, we get back a bunch of requests,
+which we can iterate through and do the relevant thing (the following snippet
+triggers a render of the UI, or makes an HTTP call, or launches a task to wait
+for Server Sent Events, depending on what the core requested):
sealed class Outcome {
+ data class Http(val res: HttpResponse) : Outcome()
+ data class Sse(val res: SseResponse) : Outcome()
+}
+
+sealed class CoreMessage {
+ data class Event(val event: Evt) : CoreMessage()
+ data class Response(val uuid: List<UByte>, val outcome: Outcome) : CoreMessage()
+}
+
+class Model : ViewModel() {
+ var view: MyViewModel by mutableStateOf(MyViewModel("", false))
+ private set
+
+ suspend fun update(msg: CoreMessage) {
+ val requests: List<Req> =
+ when (msg) {
+ is CoreMessage.Event ->
+ Requests.bincodeDeserialize(
+ processEvent(msg.event.bincodeSerialize().toUByteArray().toList())
+ .toUByteArray()
+ .toByteArray()
+ )
+ is CoreMessage.Response ->
+ Requests.bincodeDeserialize(
+ handleResponse(
+ msg.uuid.toList(),
+ when (msg.outcome) {
+ is Outcome.Http -> msg.outcome.res.bincodeSerialize()
+ is Outcome.Sse -> msg.outcome.res.bincodeSerialize()
+ }.toUByteArray().toList()
+ ).toUByteArray().toByteArray()
+ )
+ }
+
+ for (req in requests) when (val effect = req.effect) {
+ is Effect.Render -> {
+ this.view = MyViewModel.bincodeDeserialize(view().toUByteArray().toByteArray())
+ }
+ is Effect.Http -> {
+ val response = http(httpClient, HttpMethod(effect.value.method), effect.value.url)
+ update(
+ CoreMessage.Response(
+ req.uuid.toByteArray().toUByteArray().toList(),
+ Outcome.Http(response)
+ )
+ )
+ }
+ is Effect.ServerSentEvents -> {
+ viewModelScope.launch {
+ sse(sseClient, effect.value.url) { event ->
+ update(
+ CoreMessage.Response(
+ req.uuid.toByteArray().toUByteArray().toList(),
+ Outcome.Sse(event)
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+
+Crux can work with any platform-specific UI library. We think it works best with +modern declarative UI frameworks such as +SwiftUI on iOS, +Jetpack Compose on Android, and +React/Vue or a Wasm based +framework (like Yew) on the web.
+These frameworks are all pretty much identical. If you're familiar with one, you +can work out the others easily. In the examples on this page, we'll work in an +Android shell with Kotlin.
+The components are bound to the view model, and they send events to the core.
+We've already seen a "hello world" example when we were
+setting up an Android project.
+Rather than print that out again here, we'll just look at how we need to enhance
+it to work with Kotlin coroutines. We'll probably need to do this with any real
+shell, because the update function that dispatches side effect requests from the
+core will likely need to be suspend
.
This is the View
from the
+Counter example
+in the Crux repository.
@Composable
+fun View(model: Model = viewModel()) {
+ val coroutineScope = rememberCoroutineScope()
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(10.dp),
+ ) {
+ Text(text = "Crux Counter Example", fontSize = 30.sp, modifier = Modifier.padding(10.dp))
+ Text(text = "Rust Core, Kotlin Shell (Jetpack Compose)", modifier = Modifier.padding(10.dp))
+ Text(text = model.view.text, color = if(model.view.confirmed) { Color.Black } else { Color.Gray }, modifier = Modifier.padding(10.dp))
+ Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+ Button(
+ onClick = { coroutineScope.launch { model.update(CoreMessage.Event(Evt.Decrement())) } },
+ colors = ButtonDefaults.buttonColors(containerColor = Color.hsl(44F, 1F, 0.77F))
+ ) { Text(text = "Decrement", color = Color.DarkGray) }
+ Button(
+ onClick = { coroutineScope.launch { model.update(CoreMessage.Event(Evt.Increment())) } },
+ colors =
+ ButtonDefaults.buttonColors(
+ containerColor = Color.hsl(348F, 0.86F, 0.61F)
+ )
+ ) { Text(text = "Increment", color = Color.White) }
+ }
+ }
+}
+
+Notice that the first thing we do is create a CoroutineScope that is scoped to
+the lifetime of the View (i.e. will be destroyed when the View
component is
+unmounted). Then we use this scope to launch asynchronous tasks to call the
+update
method with the specific event.
+Button(onClick = { coroutineScope.launch { model.update(CoreMessage.Event(Evt.Increment())) } })
.
+We can't call update
directly, because it is suspend
so we need to be in an
+asynchronous context to do so.
We want the shell to be as thin as possible, so we need to write as little +platform-specific code as we can because this work has to be duplicated for each +platform.
+In general, the more domain-aligned our capabilities are, the more code we'll
+write. When our capabilities are generic, and closer to the technical end of the
+spectrum, we get to write the least amount of shell code to support them.
+Getting the balance right can be tricky, and the right answer might be different
+depending on context. Obviously the Http
capability is very generic, but a CMS
+capability, for instance, might well be much more specific.
The shell-side code for the Http
capability can be very small. A (very) naive
+implementation for Android might look like this:
package com.example.counter
+
+import com.example.counter.shared_types.HttpHeader
+import com.example.counter.shared_types.HttpRequest
+import com.example.counter.shared_types.HttpResponse
+import io.ktor.client.HttpClient
+import io.ktor.client.call.body
+import io.ktor.client.request.headers
+import io.ktor.client.request.request
+import io.ktor.http.HttpMethod
+import io.ktor.util.flattenEntries
+
+suspend fun requestHttp(
+ client: HttpClient,
+ request: HttpRequest,
+): HttpResponse {
+ val response = client.request(request.url) {
+ this.method = HttpMethod(request.method)
+ this.headers {
+ for (header in request.headers) {
+ append(header.name, header.value)
+ }
+ }
+ }
+ val bytes: ByteArray = response.body()
+ val headers = response.headers.flattenEntries().map { HttpHeader(it.first, it.second) }
+ return HttpResponse(response.status.value.toShort(), headers, bytes.toList())
+}
+
+
+The shell-side code to support a capability (or "Port" in "Ports and Adapters"),
+is effectively just an "Adapter" (in the same terminology) to the native APIs.
+Note that it's the shell's responsibility to cater for threading and/or async
+coroutine requirements (so the above Kotlin function is suspend
for this
+reason).
The above function can then be called by the shell when an effect is emitted
+requesting an HTTP call. It can then post the response back to the core (along
+with the uuid
that is used by the core to tie the response up to its original
+request):
for (req in requests) when (val effect = req.effect) {
+ is Effect.Http -> {
+ val response = http(
+ httpClient,
+ HttpMethod(effect.value.method),
+ effect.value.url
+ )
+ update(
+ CoreMessage.Response(
+ req.uuid.toByteArray().toUByteArray().toList(),
+ Outcome.Http(response)
+ )
+ )
+ }
+ // ...
+}
+
+{view.count}
{view.count}
{$view.count}
\n{&view.count}