Skip to content

Commit 355685f

Browse files
committed
ClassExtender plugin first draft #73
1 parent 5814afb commit 355685f

File tree

20 files changed

+2505
-85
lines changed

20 files changed

+2505
-85
lines changed

Cargo.lock

Lines changed: 1326 additions & 69 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[workspace]
22
resolver = "2"
3-
members = ["server", "cli", "lib"]
3+
members = ["server", "cli", "lib", "plugin-examples/random-folder-extender"]
44
# Tauri build is deprecated, see
55
# https://github.com/atomicdata-dev/atomic-server/issues/718
66
exclude = ["desktop"]

docs/src/plugins.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,27 @@ When a plugin is installed, the Server needs to be aware of when the functionali
3333

3434
Is run before a Commit is applied.
3535
Useful for performing authorization or data shape checks.
36+
37+
## Wasm class extenders
38+
39+
Atomic Server can load class extenders that are compiled to WASM + WASI Preview 2 (aka wasip2).
40+
Every extender implements the [`class-extender.wit`](../../lib/wit/class-extender.wit) world and exports:
41+
42+
- `class-url` – the Subject URL of the class to extend
43+
- `on-resource-get`
44+
- `before-commit`
45+
- `after-commit`
46+
47+
Handlers receive JSON-AD payloads that describe the Resource or Commit they should work with and can return an updated JSON-AD document. See the WIT file for the exact record layouts.
48+
49+
### Installing a WASM class extender
50+
51+
1. Build a component that targets `wasm32-wasip2`. Use `wit-bindgen` or `cargo component` to satisfy the interface defined in `lib/wit/class-extender.wit`.
52+
2. Copy the resulting `.wasm` file into the `wasm-class-extenders/` directory inside your Atomic data directory (next to the sled store).
53+
3. Restart `atomic-server` (or recreate the `Db`) so it scans the folder and instantiates your component.
54+
55+
All `.wasm` files in that folder are loaded on startup. Errors are logged but do not prevent the server from running, making it safe to iterate on plugins.
56+
57+
### Sample Wasm extender
58+
59+
See `wasm-plugins/examples/random-folder-extender` for a minimal Rust project that implements the `class-extender` WIT interface. It appends a random suffix to the `name` property of every `https://atomicdata.dev/classes/Folder` resource whenever it is fetched. Build it with `cargo component build --release -p random-folder-extender --target wasm32-wasip2` and copy the resulting `.wasm` into your `wasm-class-extenders/` directory to try it out.

lib/Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ url = "2"
4040
urlencoding = "2"
4141
ulid = "1.1.3"
4242
yrs = "0.24.0"
43+
wasmtime = { version = "39.0.1", optional = true, features = [
44+
"component-model",
45+
] }
46+
wasmtime-wasi = { version = "39.0.1", optional = true, features = ["p2"] }
4347

4448
[dev-dependencies]
4549
criterion = "0.5"
@@ -49,6 +53,7 @@ ntest = "0.9"
4953

5054
[features]
5155
config = ["directories", "toml"]
52-
db = ["sled", "rmp-serde", "bincode1"]
56+
db = ["sled", "rmp-serde", "bincode1", "wasm-plugins"]
5357
html = ["kuchikiki", "lol_html", "html2md"]
5458
rdf = ["rio_api", "rio_turtle"]
59+
wasm-plugins = ["wasmtime", "wasmtime-wasi"]

lib/src/class_extender.rs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::sync::Arc;
2+
13
use crate::{
24
agents::ForAgent, errors::AtomicResult, storelike::ResourceResponse, urls, Commit, Db, Resource,
35
};
@@ -15,12 +17,16 @@ pub struct CommitExtenderContext<'a> {
1517
pub resource: &'a Resource,
1618
}
1719

20+
pub type ResourceGetHandler =
21+
Arc<dyn Fn(GetExtenderContext) -> AtomicResult<ResourceResponse> + Send + Sync>;
22+
pub type CommitHandler = Arc<dyn Fn(CommitExtenderContext) -> AtomicResult<()> + Send + Sync>;
23+
1824
#[derive(Clone)]
1925
pub struct ClassExtender {
2026
pub class: String,
21-
pub on_resource_get: Option<fn(GetExtenderContext) -> AtomicResult<ResourceResponse>>,
22-
pub before_commit: Option<fn(CommitExtenderContext) -> AtomicResult<()>>,
23-
pub after_commit: Option<fn(CommitExtenderContext) -> AtomicResult<()>>,
27+
pub on_resource_get: Option<ResourceGetHandler>,
28+
pub before_commit: Option<CommitHandler>,
29+
pub after_commit: Option<CommitHandler>,
2430
}
2531

2632
impl ClassExtender {
@@ -31,4 +37,18 @@ impl ClassExtender {
3137

3238
Ok(is_a.to_subjects(None)?.iter().any(|c| c == &self.class))
3339
}
40+
41+
pub fn wrap_get_handler<F>(handler: F) -> ResourceGetHandler
42+
where
43+
F: Fn(GetExtenderContext) -> AtomicResult<ResourceResponse> + Send + Sync + 'static,
44+
{
45+
Arc::new(handler)
46+
}
47+
48+
pub fn wrap_commit_handler<F>(handler: F) -> CommitHandler
49+
where
50+
F: Fn(CommitExtenderContext) -> AtomicResult<()> + Send + Sync + 'static,
51+
{
52+
Arc::new(handler)
53+
}
3454
}

lib/src/db.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ use crate::{
3030
},
3131
endpoints::{Endpoint, HandleGetContext},
3232
errors::{AtomicError, AtomicResult},
33-
plugins::plugins,
33+
plugins::{plugins, wasm},
3434
resources::PropVals,
3535
storelike::{Query, QueryResult, ResourceResponse, Storelike},
3636
values::SortableValue,
@@ -108,6 +108,9 @@ impl Db {
108108
let query_index = db.open_tree(Tree::QueryMembers)?;
109109
let prop_val_sub_index = db.open_tree(Tree::PropValSub)?;
110110
let watched_queries = db.open_tree(Tree::WatchedQueries)?;
111+
let mut class_extenders = plugins::default_class_extenders();
112+
class_extenders.extend(wasm::load_wasm_class_extenders(path));
113+
111114
let store = Db {
112115
path: path.into(),
113116
db,
@@ -119,7 +122,7 @@ impl Db {
119122
server_url,
120123
watched_queries,
121124
endpoints: plugins::default_endpoints(),
122-
class_extenders: plugins::default_class_extenders(),
125+
class_extenders,
123126
on_commit: None,
124127
};
125128
migrate_maybe(&store).map(|e| format!("Error during migration of database: {:?}", e))?;
@@ -688,7 +691,7 @@ impl Storelike for Db {
688691
if let Some(resource_new) = &commit_response.resource_new {
689692
for extender in self.class_extenders.iter() {
690693
if extender.resource_has_extender(resource_new)? {
691-
let Some(handler) = extender.before_commit else {
694+
let Some(handler) = extender.before_commit.as_ref() else {
692695
continue;
693696
};
694697

@@ -753,7 +756,7 @@ impl Storelike for Db {
753756
if extender.resource_has_extender(resource_new)? {
754757
use crate::class_extender::CommitExtenderContext;
755758

756-
let Some(handler) = extender.after_commit else {
759+
let Some(handler) = extender.after_commit.as_ref() else {
757760
continue;
758761
};
759762

@@ -853,7 +856,7 @@ impl Storelike for Db {
853856
return Ok(resource.into());
854857
}
855858

856-
if let Some(handler) = extender.on_resource_get {
859+
if let Some(handler) = extender.on_resource_get.as_ref() {
857860
let resource_response = (handler)(GetExtenderContext {
858861
store: self,
859862
url: &url,

lib/src/errors.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,17 @@ impl From<std::string::FromUtf8Error> for AtomicError {
203203
}
204204
}
205205

206+
#[cfg(feature = "wasm-plugins")]
207+
impl From<wasmtime::Error> for AtomicError {
208+
fn from(error: wasmtime::Error) -> Self {
209+
AtomicError {
210+
message: error.to_string(),
211+
error_type: AtomicErrorType::OtherError,
212+
subject: None,
213+
}
214+
}
215+
}
216+
206217
impl From<ParseFloatError> for AtomicError {
207218
fn from(error: ParseFloatError) -> Self {
208219
AtomicError {

lib/src/plugins/chatroom.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ pub fn after_apply_commit_message(context: CommitExtenderContext) -> AtomicResul
131131
pub fn build_chatroom_extender() -> ClassExtender {
132132
ClassExtender {
133133
class: urls::CHATROOM.to_string(),
134-
on_resource_get: Some(construct_chatroom),
134+
on_resource_get: Some(ClassExtender::wrap_get_handler(construct_chatroom)),
135135
before_commit: None,
136136
after_commit: None,
137137
}
@@ -142,6 +142,8 @@ pub fn build_message_extender() -> ClassExtender {
142142
class: urls::MESSAGE.to_string(),
143143
on_resource_get: None,
144144
before_commit: None,
145-
after_commit: Some(after_apply_commit_message),
145+
after_commit: Some(ClassExtender::wrap_commit_handler(
146+
after_apply_commit_message,
147+
)),
146148
}
147149
}

lib/src/plugins/collections.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,17 @@ use crate::{
99
pub fn build_collection_extender() -> ClassExtender {
1010
ClassExtender {
1111
class: urls::COLLECTION.to_string(),
12-
on_resource_get: Some(|context| -> AtomicResult<ResourceResponse> {
12+
on_resource_get: Some(ClassExtender::wrap_get_handler(
13+
|context| -> AtomicResult<ResourceResponse> {
1314
let GetExtenderContext {
1415
store,
1516
url,
1617
db_resource: resource,
1718
for_agent,
1819
} = context;
1920
construct_collection_from_params(store, url.query_pairs(), resource, for_agent)
20-
}),
21+
},
22+
)),
2123
before_commit: None,
2224
after_commit: None,
2325
}

lib/src/plugins/invite.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,8 @@ pub fn before_apply_commit(context: CommitExtenderContext) -> AtomicResult<()> {
174174
pub fn build_invite_extender() -> ClassExtender {
175175
ClassExtender {
176176
class: urls::INVITE.to_string(),
177-
on_resource_get: Some(construct_invite_redirect),
178-
before_commit: Some(before_apply_commit),
177+
on_resource_get: Some(ClassExtender::wrap_get_handler(construct_invite_redirect)),
178+
before_commit: Some(ClassExtender::wrap_commit_handler(before_apply_commit)),
179179
after_commit: None,
180180
}
181181
}

0 commit comments

Comments
 (0)