From de60a5dc29d69d126f72dce8308a275b997b8fc6 Mon Sep 17 00:00:00 2001 From: Jiwon Kwon Date: Wed, 10 Jun 2026 23:33:02 +0900 Subject: [PATCH 1/3] Add core input action boundary Assisted-by: Codex:gpt-5.5 --- crates/feder-core/src/lib.rs | 157 +++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/crates/feder-core/src/lib.rs b/crates/feder-core/src/lib.rs index b82752b..5a13a69 100644 --- a/crates/feder-core/src/lib.rs +++ b/crates/feder-core/src/lib.rs @@ -1,3 +1,160 @@ //! Portable ActivityPub core logic for Feder. +#![no_std] + +extern crate alloc; + +use alloc::{string::String, vec::Vec}; pub use feder_vocab as vocab; + +/// Portable core state and decision logic. +#[derive(Debug, Default)] +pub struct FederCore; + +impl FederCore { + #[must_use] + pub fn new() -> Self { + Self + } + + /// Handle one core input and return runtime actions to perform later. + /// + /// This method intentionally performs no I/O. Follow acceptance, object + /// storage, and delivery behavior are added by later Phase 1 issues. + #[must_use] + pub fn handle(&mut self, input: Input) -> HandleResult { + match input { + Input::ReceivedFollow(_) | Input::UserCreateNote(_) => HandleResult::default(), + } + } +} + +/// Something entering the portable core from a runtime. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Input { + ReceivedFollow(vocab::Follow), + UserCreateNote(UserCreateNote), +} + +/// Runtime-provided data for creating a local note. +/// +/// IDs and timestamps are inputs so the core does not depend on clocks, +/// randomness, or platform-specific ID generation. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct UserCreateNote { + pub note_id: vocab::Iri, + pub create_id: vocab::Iri, + pub actor: vocab::Reference, + pub content: String, + pub published: Option, +} + +/// Something the runtime should perform after core handling. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Action { + StoreFollower(StoreFollower), + StoreObject(StoreObject), + SendActivity(SendActivity), +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct StoreFollower { + pub actor: vocab::Reference, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct StoreObject { + pub object: Object, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SendActivity { + pub activity: Activity, + pub target: vocab::Iri, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Activity { + Accept(vocab::Accept), + CreateNote(vocab::Create), +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Object { + Note(vocab::Note), +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct HandleResult { + pub actions: Vec, +} + +impl HandleResult { + #[must_use] + pub fn new(actions: Vec) -> Self { + Self { actions } + } + + #[must_use] + pub fn is_empty(&self) -> bool { + self.actions.is_empty() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::format; + use alloc::string::ToString; + + fn iri(value: &str) -> vocab::Iri { + value.parse().expect("valid test IRI") + } + + fn actor(id: &str) -> vocab::Actor { + vocab::Actor::person( + iri(id), + iri(&format!("{id}/inbox")), + iri(&format!("{id}/outbox")), + ) + } + + #[test] + fn received_follow_enters_core_without_runtime_io() { + let mut core = FederCore::new(); + let follow = vocab::Follow::new( + iri("https://remote.example/activities/follow/1"), + vocab::Reference::id(iri("https://remote.example/users/bob")), + vocab::Reference::object(actor("https://example.com/users/alice")), + ); + + let result = core.handle(Input::ReceivedFollow(follow)); + + assert!(result.is_empty()); + } + + #[test] + fn user_create_note_input_carries_nondeterministic_values() { + let input = UserCreateNote { + note_id: iri("https://example.com/notes/1"), + create_id: iri("https://example.com/activities/create/1"), + actor: vocab::Reference::id(iri("https://example.com/users/alice")), + content: "Hello from Feder.".to_string(), + published: Some("2026-06-10T00:00:00Z".to_string()), + }; + + let mut core = FederCore::new(); + let result = core.handle(Input::UserCreateNote(input)); + + assert!(result.is_empty()); + } + + #[test] + fn handle_result_wraps_action_lists() { + let result = HandleResult::new(Vec::from([Action::StoreFollower(StoreFollower { + actor: vocab::Reference::id(iri("https://remote.example/users/bob")), + })])); + + assert_eq!(result.actions.len(), 1); + } +} From 7af157ea18975e9a17100467e5000d7df1405abd Mon Sep 17 00:00:00 2001 From: Jiwon Kwon Date: Thu, 11 Jun 2026 14:54:19 +0900 Subject: [PATCH 2/3] Clarify core action payloads Assisted-by: Codex:gpt-5.5 --- crates/feder-core/src/lib.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/feder-core/src/lib.rs b/crates/feder-core/src/lib.rs index 5a13a69..2c82259 100644 --- a/crates/feder-core/src/lib.rs +++ b/crates/feder-core/src/lib.rs @@ -51,6 +51,7 @@ pub struct UserCreateNote { /// Something the runtime should perform after core handling. #[derive(Clone, Debug, Eq, PartialEq)] +#[non_exhaustive] pub enum Action { StoreFollower(StoreFollower), StoreObject(StoreObject), @@ -59,7 +60,8 @@ pub enum Action { #[derive(Clone, Debug, Eq, PartialEq)] pub struct StoreFollower { - pub actor: vocab::Reference, + pub follower: vocab::Reference, + pub following: vocab::Reference, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -70,16 +72,18 @@ pub struct StoreObject { #[derive(Clone, Debug, Eq, PartialEq)] pub struct SendActivity { pub activity: Activity, - pub target: vocab::Iri, + pub inbox: vocab::Iri, } #[derive(Clone, Debug, Eq, PartialEq)] +#[non_exhaustive] pub enum Activity { Accept(vocab::Accept), CreateNote(vocab::Create), } #[derive(Clone, Debug, Eq, PartialEq)] +#[non_exhaustive] pub enum Object { Note(vocab::Note), } @@ -152,7 +156,8 @@ mod tests { #[test] fn handle_result_wraps_action_lists() { let result = HandleResult::new(Vec::from([Action::StoreFollower(StoreFollower { - actor: vocab::Reference::id(iri("https://remote.example/users/bob")), + follower: vocab::Reference::id(iri("https://remote.example/users/bob")), + following: vocab::Reference::id(iri("https://example.com/users/alice")), })])); assert_eq!(result.actions.len(), 1); From 50baf2e5aa286bb9dbada0b47016b4bb3efb82d2 Mon Sep 17 00:00:00 2001 From: Jiwon Kwon Date: Fri, 12 Jun 2026 12:12:57 +0900 Subject: [PATCH 3/3] Mark core inputs non-exhaustive Assisted-by: Codex:gpt-5.5 --- crates/feder-core/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/feder-core/src/lib.rs b/crates/feder-core/src/lib.rs index 2c82259..7d4bc37 100644 --- a/crates/feder-core/src/lib.rs +++ b/crates/feder-core/src/lib.rs @@ -31,6 +31,7 @@ impl FederCore { /// Something entering the portable core from a runtime. #[derive(Clone, Debug, Eq, PartialEq)] +#[non_exhaustive] pub enum Input { ReceivedFollow(vocab::Follow), UserCreateNote(UserCreateNote),