Skip to content

Commit ff25fcb

Browse files
committed
Add docs to spin-app crate
Signed-off-by: Lann Martin <lann.martin@fermyon.com>
1 parent 2b1dbc6 commit ff25fcb

File tree

4 files changed

+103
-9
lines changed

4 files changed

+103
-9
lines changed

crates/app/src/host_component.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@ use spin_core::{EngineBuilder, HostComponent, HostComponentsData};
44

55
use crate::AppComponent;
66

7+
/// A trait for "dynamic" Spin host components.
8+
///
9+
/// This extends [`HostComponent`] to support per-[`AppComponent`] dynamic
10+
/// runtime configuration.
711
pub trait DynamicHostComponent: HostComponent {
12+
/// Called on [`AppComponent`] instance initialization.
13+
///
14+
/// The `data` returned by [`HostComponent::build_data`] is passed, along
15+
/// with a reference to the `component` being instantiated.
816
fn update_data(&self, data: &mut Self::Data, component: &AppComponent) -> anyhow::Result<()>;
917
}
1018

crates/app/src/lib.rs

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
//! Spin internal application interfaces
2+
//!
3+
//! This crate contains interfaces to Spin application configuration to be used
4+
//! by crates that implement Spin execution environments: trigger executors and
5+
//! host components, in particular.
6+
7+
#![deny(missing_docs)]
8+
19
mod host_component;
210
pub mod locked;
311
pub mod values;
@@ -13,37 +21,53 @@ pub use async_trait::async_trait;
1321
pub use host_component::DynamicHostComponent;
1422
pub use locked::Variable;
1523

24+
/// A trait for implementing the low-level operations needed to load an [`App`].
1625
// TODO(lann): Should this migrate to spin-loader?
1726
#[async_trait]
1827
pub trait Loader {
28+
/// Called with an implementation-defined `uri` pointing to some
29+
/// representation of a [`LockedApp`], which will be loaded.
1930
async fn load_app(&self, uri: &str) -> anyhow::Result<LockedApp>;
2031

32+
/// Called with a [`LockedComponentSource`] pointing to a Wasm module
33+
/// binary, which will be loaded.
2134
async fn load_module(
2235
&self,
2336
engine: &wasmtime::Engine,
2437
source: &LockedComponentSource,
2538
) -> anyhow::Result<spin_core::Module>;
2639

40+
/// Called with an [`AppComponent`]; any `files` configured with the
41+
/// component should be "mounted" into the `store_builder`, via e.g.
42+
/// [`StoreBuilder::read_only_preopened_dir`].
2743
async fn mount_files(
2844
&self,
2945
store_builder: &mut StoreBuilder,
3046
component: &AppComponent,
3147
) -> anyhow::Result<()>;
3248
}
3349

50+
/// An `AppLoader` holds an implementation of [`Loader`] along with
51+
/// [`DynamicHostComponents`] configuration.
3452
pub struct AppLoader {
3553
inner: Box<dyn Loader + Send + Sync>,
3654
dynamic_host_components: DynamicHostComponents,
3755
}
3856

3957
impl AppLoader {
58+
/// Creates a new [`AppLoader`].
4059
pub fn new(loader: impl Loader + Send + Sync + 'static) -> Self {
4160
Self {
4261
inner: Box::new(loader),
4362
dynamic_host_components: Default::default(),
4463
}
4564
}
4665

66+
/// Adds a [`DynamicHostComponent`] to the given [`EngineBuilder`] and
67+
/// configures this [`AppLoader`] to update it on component instantiation.
68+
///
69+
/// This calls [`EngineBuilder::add_host_component`] for you; it should not
70+
/// be called separately.
4771
pub fn add_dynamic_host_component<T: Send + Sync, DHC: DynamicHostComponent>(
4872
&mut self,
4973
engine_builder: &mut EngineBuilder<T>,
@@ -53,6 +77,7 @@ impl AppLoader {
5377
.add_dynamic_host_component(engine_builder, host_component)
5478
}
5579

80+
/// Loads an [`App`] from the given `Loader`-implementation-specific `uri`.
5681
pub async fn load_app(&self, uri: String) -> Result<App> {
5782
let locked = self
5883
.inner
@@ -66,6 +91,8 @@ impl AppLoader {
6691
})
6792
}
6893

94+
/// Loads an [`OwnedApp`] from the given `Loader`-implementation-specific
95+
/// `uri`; the [`OwnedApp`] takes ownership of this [`AppLoader`].
6996
pub async fn load_owned_app(self, uri: String) -> Result<OwnedApp> {
7097
OwnedApp::try_new_async(self, |loader| Box::pin(loader.load_app(uri))).await
7198
}
@@ -88,11 +115,13 @@ pub struct OwnedApp {
88115
}
89116

90117
impl OwnedApp {
118+
/// Returns a reference to the owned [`App`].
91119
pub fn borrowed(&self) -> &App {
92120
self.borrow_app()
93121
}
94122
}
95123

124+
/// An `App` holds loaded configuration for a Spin application.
96125
#[derive(Debug)]
97126
pub struct App<'a> {
98127
loader: &'a AppLoader,
@@ -101,10 +130,16 @@ pub struct App<'a> {
101130
}
102131

103132
impl<'a> App<'a> {
133+
/// Returns a [`Loader`]-implementation-specific URI for this app.
104134
pub fn uri(&self) -> &str {
105135
&self.uri
106136
}
107137

138+
/// Deserializes typed metadata for this app.
139+
///
140+
/// Returns `Ok(None)` if there is no metadata for the given `key` and an
141+
/// `Err` only if there _is_ a value for the `key` but the typed
142+
/// deserialization failed.
108143
pub fn get_metadata<'this, T: Deserialize<'this>>(&'this self, key: &str) -> Result<Option<T>> {
109144
self.locked
110145
.metadata
@@ -113,76 +148,100 @@ impl<'a> App<'a> {
113148
.transpose()
114149
}
115150

151+
/// Deserializes typed metadata for this app.
152+
///
153+
/// Like [`App::get_metadata`], but returns an `Err` if there is no metadata
154+
/// for the given `key`.
116155
pub fn require_metadata<'this, T: Deserialize<'this>>(&'this self, key: &str) -> Result<T> {
117156
self.get_metadata(key)?
118-
.ok_or_else(|| Error::ManifestError(format!("missing required {key:?}")))
157+
.ok_or_else(|| Error::MetadataError(format!("missing required {key:?}")))
119158
}
120159

160+
/// Returns an iterator of custom config [`Variable`]s defined for this app.
121161
pub fn variables(&self) -> impl Iterator<Item = (&String, &Variable)> {
122162
self.locked.variables.iter()
123163
}
124164

165+
/// Returns an iterator of [`AppComponent`]s defined for this app.
125166
pub fn components(&self) -> impl Iterator<Item = AppComponent> {
126167
self.locked
127168
.components
128169
.iter()
129170
.map(|locked| AppComponent { app: self, locked })
130171
}
131172

173+
/// Returns the [`AppComponent`] with the given `component_id`, or `None`
174+
/// if it doesn't exist.
132175
pub fn get_component(&self, component_id: &str) -> Option<AppComponent> {
133176
self.components()
134177
.find(|component| component.locked.id == component_id)
135178
}
136179

180+
/// Returns an iterator of [`AppTrigger`]s defined for this app.
137181
pub fn triggers(&self) -> impl Iterator<Item = AppTrigger> {
138182
self.locked
139183
.triggers
140184
.iter()
141185
.map(|locked| AppTrigger { app: self, locked })
142186
}
143187

188+
/// Returns an iterator of [`AppTrigger`]s defined for this app with
189+
/// the given `trigger_type`.
144190
pub fn triggers_with_type(&'a self, trigger_type: &'a str) -> impl Iterator<Item = AppTrigger> {
145191
self.triggers()
146192
.filter(move |trigger| trigger.locked.trigger_type == trigger_type)
147193
}
148194
}
149195

196+
/// An `AppComponent` holds configuration for a Spin application component.
150197
pub struct AppComponent<'a> {
198+
/// The app this component belongs to.
151199
pub app: &'a App<'a>,
152200
locked: &'a LockedComponent,
153201
}
154202

155203
impl<'a> AppComponent<'a> {
204+
/// Returns this component's app-unique ID.
156205
pub fn id(&self) -> &str {
157206
&self.locked.id
158207
}
159208

209+
/// Returns this component's Wasm module source.
160210
pub fn source(&self) -> &LockedComponentSource {
161211
&self.locked.source
162212
}
163213

214+
/// Returns an iterator of [`ContentPath`]s for this component's configured
215+
/// "directory mounts".
164216
pub fn files(&self) -> std::slice::Iter<ContentPath> {
165217
self.locked.files.iter()
166218
}
167219

220+
/// Deserializes typed metadata for this component.
221+
///
222+
/// Returns `Ok(None)` if there is no metadata for the given `key` and an
223+
/// `Err` only if there _is_ a value for the `key` but the typed
224+
/// deserialization failed.
168225
pub fn get_metadata<T: Deserialize<'a>>(&self, key: &str) -> Result<Option<T>> {
169226
self.locked
170227
.metadata
171228
.get(key)
172229
.map(|value| {
173230
T::deserialize(value).map_err(|err| {
174-
Error::ManifestError(format!(
231+
Error::MetadataError(format!(
175232
"failed to deserialize {key:?} = {value:?}: {err:?}"
176233
))
177234
})
178235
})
179236
.transpose()
180237
}
181238

239+
/// Returns an iterator of custom config values for this component.
182240
pub fn config(&self) -> impl Iterator<Item = (&String, &String)> {
183241
self.locked.config.iter()
184242
}
185243

244+
/// Loads and returns the [`spin_core::Module`] for this component.
186245
pub async fn load_module<T: Send + Sync>(
187246
&self,
188247
engine: &Engine<T>,
@@ -195,6 +254,11 @@ impl<'a> AppComponent<'a> {
195254
.map_err(Error::LoaderError)
196255
}
197256

257+
/// Updates the given [`StoreBuilder`] with configuration for this component.
258+
///
259+
/// In particular, the WASI 'env' and "preloaded dirs" are set up, and any
260+
/// [`DynamicHostComponent`]s associated with the source [`AppLoader`] are
261+
/// configured.
198262
pub async fn apply_store_config(&self, builder: &mut StoreBuilder) -> Result<()> {
199263
builder.env(&self.locked.env).map_err(Error::CoreError)?;
200264

@@ -214,58 +278,74 @@ impl<'a> AppComponent<'a> {
214278
}
215279
}
216280

281+
/// An `AppTrigger` holds configuration for a Spin application trigger.
217282
pub struct AppTrigger<'a> {
283+
/// The app this trigger belongs to.
218284
pub app: &'a App<'a>,
219285
locked: &'a LockedTrigger,
220286
}
221287

222288
impl<'a> AppTrigger<'a> {
289+
/// Returns this trigger's app-unique ID.
223290
pub fn id(&self) -> &str {
224291
&self.locked.id
225292
}
226293

294+
/// Returns the Trigger's type.
227295
pub fn trigger_type(&self) -> &str {
228296
&self.locked.trigger_type
229297
}
230298

299+
/// Returns a reference to the [`AppComponent`] configured for this trigger.
300+
///
301+
/// This is a convenience wrapper that looks up the component based on the
302+
/// 'component' metadata value which is conventionally a component ID.
231303
pub fn component(&self) -> Result<AppComponent<'a>> {
232304
let component_id = self.locked.trigger_config.get("component").ok_or_else(|| {
233-
Error::ManifestError(format!(
305+
Error::MetadataError(format!(
234306
"trigger {:?} missing 'component' config field",
235307
self.locked.id
236308
))
237309
})?;
238310
let component_id = component_id.as_str().ok_or_else(|| {
239-
Error::ManifestError(format!(
311+
Error::MetadataError(format!(
240312
"trigger {:?} 'component' field has unexpected value {:?}",
241313
self.locked.id, component_id
242314
))
243315
})?;
244316
self.app.get_component(component_id).ok_or_else(|| {
245-
Error::ManifestError(format!(
317+
Error::MetadataError(format!(
246318
"missing component {:?} configured for trigger {:?}",
247319
component_id, self.locked.id
248320
))
249321
})
250322
}
251323

324+
/// Deserializes this trigger's configuration into a typed value.
252325
pub fn typed_config<Config: Deserialize<'a>>(&self) -> Result<Config> {
253326
Ok(Config::deserialize(&self.locked.trigger_config)?)
254327
}
255328
}
256329

330+
/// Type alias for a [`Result`]s with [`Error`].
257331
pub type Result<T> = std::result::Result<T, Error>;
258332

333+
/// Errors returned by methods in this crate.
259334
#[derive(Debug, thiserror::Error)]
260335
pub enum Error {
336+
/// An error propagated from the [`spin_core`] crate.
261337
#[error("spin core error: {0:#}")]
262338
CoreError(anyhow::Error),
339+
/// An error from a [`DynamicHostComponent`].
263340
#[error("host component error: {0:#}")]
264341
HostComponentError(anyhow::Error),
342+
/// An error from a [`Loader`] implementation.
265343
#[error("loader error: {0:#}")]
266344
LoaderError(anyhow::Error),
267-
#[error("manifest error: {0}")]
268-
ManifestError(String),
345+
/// An error indicating missing or unexpected metadata.
346+
#[error("metadata error: {0}")]
347+
MetadataError(String),
348+
/// An error indicating failed JSON (de)serialization.
269349
#[error("json error: {0}")]
270350
JsonError(#[from] serde_json::Error),
271351
}

crates/app/src/locked.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
//! Spin lock file (spin.lock) serialization models.
2+
13
use std::path::PathBuf;
24

35
use serde::{Deserialize, Serialize};
46
use serde_json::Value;
57

68
use crate::values::ValuesMap;
79

8-
// LockedMap gives deterministic encoding, which we want.
10+
/// A String-keyed map with deterministic serialization order.
911
pub type LockedMap<T> = std::collections::BTreeMap<String, T>;
1012

1113
/// A LockedApp represents a "fully resolved" Spin application.
@@ -26,10 +28,12 @@ pub struct LockedApp {
2628
}
2729

2830
impl LockedApp {
31+
/// Deserializes a [`LockedApp`] from the given JSON data.
2932
pub fn from_json(contents: &[u8]) -> serde_json::Result<Self> {
3033
serde_json::from_slice(contents)
3134
}
3235

36+
/// Serializes the [`LockedApp`] into JSON data.
3337
pub fn to_json(&self) -> serde_json::Result<Vec<u8>> {
3438
serde_json::to_vec_pretty(&self)
3539
}

crates/app/src/values.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
//! Dynamically-typed value helpers.
2+
13
use serde::Serialize;
24
use serde_json::Value;
35

4-
// ValuesMap stores dynamically-typed values.
6+
/// A String-keyed map with dynamically-typed values.
57
pub type ValuesMap = serde_json::Map<String, Value>;
68

79
/// ValuesMapBuilder assists in building a ValuesMap.

0 commit comments

Comments
 (0)