pubky-app-specs 0.4.3

Pubky.app Data Model Specifications
Documentation
use crate::{traits::Validatable, ParsedUri, Resource};

pub mod blob;
pub mod bookmark;
pub mod feed;
pub mod file;
pub mod follow;
pub mod last_read;
pub mod mute;
pub mod post;
pub mod tag;
pub mod user;

use super::{
    PubkyAppBlob, PubkyAppBookmark, PubkyAppFeed, PubkyAppFile, PubkyAppFollow, PubkyAppLastRead,
    PubkyAppMute, PubkyAppPost, PubkyAppTag, PubkyAppUser,
};

/// A unified enum wrapping all PubkyApp objects.
#[derive(Debug, Clone)]
pub enum PubkyAppObject {
    User(user::PubkyAppUser),
    Post(post::PubkyAppPost),
    Follow(follow::PubkyAppFollow),
    Mute(mute::PubkyAppMute),
    Bookmark(bookmark::PubkyAppBookmark),
    Tag(tag::PubkyAppTag),
    File(file::PubkyAppFile),
    Blob(blob::PubkyAppBlob),
    Feed(feed::PubkyAppFeed),
    LastRead(last_read::PubkyAppLastRead),
}

impl PubkyAppObject {
    /// Given a URI and a blob (raw data from the homeserver),
    /// this function returns the fully formed PubkyAppObject.
    pub fn from_uri<S: AsRef<str>>(uri: S, blob: &[u8]) -> Result<Self, String> {
        let parsed_uri = ParsedUri::try_from(uri.as_ref())?;
        Self::from_resource(&parsed_uri.resource, blob)
    }

    /// Given a Resource and a blob (raw data from the homeserver),
    /// this function returns the fully formed PubkyAppObject.
    pub fn from_resource(resource: &Resource, blob: &[u8]) -> Result<Self, String> {
        match resource {
            Resource::User => {
                // For a user, no ID is needed (or you may use an empty string)
                let user = <PubkyAppUser as Validatable>::try_from(blob, "")?;
                Ok(PubkyAppObject::User(user))
            }
            Resource::Post(post_id) => {
                let post = <PubkyAppPost as Validatable>::try_from(blob, post_id)?;
                Ok(PubkyAppObject::Post(post))
            }
            Resource::Follow(follow_id) => {
                // Use the follow id from the parsed URI.
                let follow = <PubkyAppFollow as Validatable>::try_from(blob, follow_id)?;
                Ok(PubkyAppObject::Follow(follow))
            }
            Resource::Mute(muted_id) => {
                let mute = <PubkyAppMute as Validatable>::try_from(blob, muted_id)?;
                Ok(PubkyAppObject::Mute(mute))
            }
            Resource::Bookmark(bookmark_id) => {
                let bookmark = <PubkyAppBookmark as Validatable>::try_from(blob, bookmark_id)?;
                Ok(PubkyAppObject::Bookmark(bookmark))
            }
            Resource::Tag(tag_id) => {
                let tag = <PubkyAppTag as Validatable>::try_from(blob, tag_id)?;
                Ok(PubkyAppObject::Tag(tag))
            }
            Resource::File(file_id) => {
                let file = <PubkyAppFile as Validatable>::try_from(blob, file_id)?;
                Ok(PubkyAppObject::File(file))
            }
            Resource::Blob(blob_id) => {
                let blob_obj = <PubkyAppBlob as Validatable>::try_from(blob, blob_id)?;
                Ok(PubkyAppObject::Blob(blob_obj))
            }
            Resource::Feed(feed_id) => {
                let feed = <PubkyAppFeed as Validatable>::try_from(blob, feed_id)?;
                Ok(PubkyAppObject::Feed(feed))
            }
            Resource::LastRead => {
                let last_read = <PubkyAppLastRead as Validatable>::try_from(blob, "")?;
                Ok(PubkyAppObject::LastRead(last_read))
            }
            Resource::Unknown => Err(format!("Unrecognized resource {:?}", resource)),
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::utils::*;

    use super::*;

    // These tests assume that the respective try_from implementations for each model
    // parse the provided JSON. Adjust the JSON payloads as needed.

    #[test]
    fn test_import_user() {
        let uri = user_uri_builder("operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo".into());
        let user_json = r#"{
            "name": "Alice",
            "bio": "Hello, I am Alice",
            "image": "https://example.com/alice.png",
            "links": null,
            "status": "active"
        }"#;
        let result = PubkyAppObject::from_uri(uri, user_json.as_bytes());
        assert!(
            result.is_ok(),
            "Expected a successful import for user, got error: {:?}",
            result.err()
        );
        match result.unwrap() {
            PubkyAppObject::User(user) => {
                assert_eq!(user.name, "Alice", "User name mismatch");
                assert_eq!(
                    user.bio.unwrap_or_default(),
                    "Hello, I am Alice",
                    "User bio mismatch"
                );
            }
            other => panic!("Expected a User object, got {:?}", other),
        }
    }

    #[test]
    fn test_import_post() {
        let uri = post_uri_builder(
            "operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo".into(),
            "0032SSN7Q4EVG".into(),
        );
        let post_json = r#"{
            "content": "Hello World!",
            "kind": "short",
            "parent": null,
            "embed": null,
            "attachments": null
        }"#;
        let result = PubkyAppObject::from_uri(uri, post_json.as_bytes());
        assert!(
            result.is_ok(),
            "Expected a successful import for post, got error: {:?}",
            result.err()
        );
        match result.unwrap() {
            PubkyAppObject::Post(post) => {
                assert_eq!(post.content, "Hello World!", "Post content mismatch");
            }
            other => panic!("Expected a Post object, got {:?}", other),
        }
    }

    #[test]
    fn test_import_follow() {
        let uri = follow_uri_builder(
            "operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo".into(),
            "pxnu33x7jtpx9ar1ytsi4yxbp6a5o36gwhffs8zoxmbuptici1jy".into(),
        );
        let follow_json = r#"{
            "created_at": 1627849723
        }"#;
        let result = PubkyAppObject::from_uri(uri, follow_json.as_bytes());
        assert!(
            result.is_ok(),
            "Expected a successful import for follow, got error: {:?}",
            result.err()
        );
        match result.unwrap() {
            PubkyAppObject::Follow(follow) => {
                assert_eq!(follow.created_at, 1627849723, "Follow created_at mismatch");
            }
            other => panic!("Expected a Follow object, got {:?}", other),
        }
    }

    #[test]
    fn test_import_mute() {
        let uri = mute_uri_builder(
            "operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo".into(),
            "pxnu33x7jtpx9ar1ytsi4yxbp6a5o36gwhffs8zoxmbuptici1jy".into(),
        );
        let mute_json = r#"{
            "created_at": 1627849724
        }"#;
        let result = PubkyAppObject::from_uri(uri, mute_json.as_bytes());
        assert!(
            result.is_ok(),
            "Expected a successful import for mute, got error: {:?}",
            result.err()
        );
        match result.unwrap() {
            PubkyAppObject::Mute(mute) => {
                assert_eq!(mute.created_at, 1627849724, "Mute created_at mismatch");
            }
            other => panic!("Expected a Mute object, got {:?}", other),
        }
    }

    #[test]
    fn test_import_bookmark() {
        let post_uri = post_uri_builder(
            "operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo".into(),
            "0032SSN7Q4EVG".into(),
        );

        let uri = bookmark_uri_builder(
            "operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo".into(),
            "8Z8CWH8NVYQY39ZEBFGKQWWEKG".into(),
        );
        let bookmark_json = format!(
            r#"{{
                "uri": "{post_uri}",
                "created_at": 1627849725
            }}"#
        );
        let result = PubkyAppObject::from_uri(uri, bookmark_json.as_bytes());
        assert!(
            result.is_ok(),
            "Expected a successful import for bookmark, got error: {:?}",
            result.err()
        );
        match result.unwrap() {
            PubkyAppObject::Bookmark(bookmark) => {
                assert_eq!(bookmark.uri, post_uri, "Bookmark URI mismatch");
            }
            other => panic!("Expected a Bookmark object, got {:?}", other),
        }
    }

    #[test]
    fn test_import_tag() {
        let post_uri = post_uri_builder(
            "operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo".into(),
            "0032SSN7Q4EVG".into(),
        );

        let uri = tag_uri_builder(
            "operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo".into(),
            "86805FC1CSFZD4W6HZ09S24QWG".into(),
        );
        let tag_json = format!(
            r#"{{
            "uri": "{post_uri}",
            "label": "cool",
            "created_at": 1627849726
        }}"#
        );
        let result = PubkyAppObject::from_uri(uri, tag_json.as_bytes());
        assert!(
            result.is_ok(),
            "Expected a successful import for tag, got error: {:?}",
            result.err()
        );
        match result.unwrap() {
            PubkyAppObject::Tag(tag) => {
                assert_eq!(tag.label, "cool", "Tag label mismatch");
            }
            other => panic!("Expected a Tag object, got {:?}", other),
        }
    }

    #[test]
    fn test_import_file() {
        let uri = file_uri_builder(
            "operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo".into(),
            "0032SSN7Q4EVG".into(),
        );
        let file_json = r#"{
            "name": "example.png",
            "created_at": 1627849727,
            "src": "https://example.com/example.png",
            "content_type": "image/png",
            "size": 1024
        }"#;
        let result = PubkyAppObject::from_uri(uri, file_json.as_bytes());
        assert!(
            result.is_ok(),
            "Expected a successful import for file, got error: {:?}",
            result.err()
        );
        match result.unwrap() {
            PubkyAppObject::File(file) => {
                assert_eq!(file.name, "example.png", "File name mismatch");
                assert_eq!(
                    file.src, "https://example.com/example.png",
                    "File src mismatch"
                );
            }
            other => panic!("Expected a File object, got {:?}", other),
        }
    }

    #[test]
    fn test_import_blob() {
        let uri = blob_uri_builder(
            "operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo".into(),
            "CDW1T5RM4PHP64QT0P6RE4PNT0".into(),
        );
        // For a blob, assume the JSON is an array of numbers representing the data.
        let blob: Vec<u8> = vec![1, 2, 3, 4];
        let result = PubkyAppObject::from_uri(uri, &blob);
        assert!(
            result.is_ok(),
            "Expected a successful import for blob, got error: {:?}",
            result.err()
        );
        match result.unwrap() {
            PubkyAppObject::Blob(blob_obj) => {
                let data = blob_obj.0;
                assert_eq!(data, vec![1, 2, 3, 4], "Blob data mismatch");
            }
            other => panic!("Expected a Blob object, got {:?}", other),
        }
    }

    #[test]
    fn test_import_feed() {
        let uri = feed_uri_builder(
            "operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo".into(),
            "5F2NDB2HJGJ2HJBY6MPQ0H5R0G".into(),
        );
        let feed_json = r#"{
            "feed": {
                "tags": [],
                "reach": "following",
                "layout": "columns",
                "sort": "recent",
                "content": null
            },
            "name": "My Feed",
            "created_at": 1627849728
        }"#;
        let result = PubkyAppObject::from_uri(uri, feed_json.as_bytes());
        assert!(
            result.is_ok(),
            "Expected a successful import for feed, got error: {:?}",
            result.err()
        );
        match result.unwrap() {
            PubkyAppObject::Feed(feed) => {
                assert_eq!(feed.name, "My Feed", "Feed name mismatch");
            }
            other => panic!("Expected a Feed object, got {:?}", other),
        }
    }

    #[test]
    fn test_import_last_read() {
        let uri =
            last_read_uri_builder("operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo".into());
        let last_read_json = r#"{
            "timestamp": 1627849729
        }"#;
        let result = PubkyAppObject::from_uri(uri, last_read_json.as_bytes());
        assert!(
            result.is_ok(),
            "Expected a successful import for last_read, got error: {:?}",
            result.err()
        );
        match result.unwrap() {
            PubkyAppObject::LastRead(last_read) => {
                assert_eq!(
                    last_read.timestamp, 1627849729,
                    "LastRead timestamp mismatch"
                );
            }
            other => panic!("Expected a LastRead object, got {:?}", other),
        }
    }

    #[test]
    fn test_import_unknown_resource() {
        let uri =
            "pubky://operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo/pub/pubky.app/unknown/ID";
        let json = r#"{}"#;
        let result = PubkyAppObject::from_uri(uri, json.as_bytes());
        assert!(
            result.is_err(),
            "Expected an error for unknown resource, but got: {:?}",
            result.ok()
        );
        let err = result.err().unwrap();
        assert!(
            err.contains("Unrecognized resource"),
            "Error message does not contain expected text: {}",
            err
        );
    }
}