Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ffi to expose uri parser on wasm #16

Merged
merged 3 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions pkg/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,30 @@ This library supports many more domain objects beyond `User` and `Post`. Here ar
- **LastRead**: `createLastRead(...)`

Each has a `meta` field for storing relevant IDs/paths and a typed data object.

## 📌 Parsing a Pubky URI

The `parse_uri()` function converts a Pubky URI string into a strongly typed object.

**Usage:**

```js
import { parse_uri } from "pubky-app-specs";

try {
const result = parse_uri("pubky://userID/pub/pubky.app/posts/postID");
console.log(result.user_id); // "userID"
console.log(result.resource); // e.g. "posts"
console.log(result.resource_id); // "postID" or null
} catch (error) {
console.error("URI parse error:", error);
}
```

**Returns:**

A `ParsedUriResult` object with:

- **user_id:** The parsed user identifier.
- **resource:** A string indicating the resource type.
- **resource_id:** An optional resource identifier.
2 changes: 1 addition & 1 deletion pkg/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "pubky-app-specs",
"type": "module",
"description": "Pubky.app Data Model Specifications",
"version": "0.3.0",
"version": "0.3.0-rc1",
"license": "MIT",
"collaborators": [
"SHAcollision"
Expand Down
7 changes: 3 additions & 4 deletions src/uri_parser.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
use serde::{Deserialize, Serialize};
use std::fmt;
use url::Url;

use crate::{
traits::{HasPath, HasPubkyIdPath},
PubkyAppBlob, PubkyAppBookmark, PubkyAppFeed, PubkyAppFile, PubkyAppFollow, PubkyAppLastRead,
PubkyAppMute, PubkyAppPost, PubkyAppTag, PubkyAppUser, PubkyId, APP_PATH, PROTOCOL,
PUBLIC_PATH,
};
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
use std::fmt;
use url::Url;

#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)]
pub enum Resource {
Expand Down
110 changes: 95 additions & 15 deletions src/wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub struct Meta {
// Implement wasm_bindgen methods to expose read-only fields.
#[wasm_bindgen]
impl Meta {
// Getters clone the data out because String/JsValue is not Copy.
// Getters clone the data out because String is not Copy.
#[wasm_bindgen(getter)]
pub fn id(&self) -> String {
self.id.clone()
Expand Down Expand Up @@ -147,12 +147,12 @@ impl PubkySpecsBuilder {
image: Option<String>,
links: JsValue, // a JS array of {title, url} or null
status: Option<String>,
) -> Result<UserResult, JsValue> {
) -> Result<UserResult, String> {
// 1) Convert JS 'links' -> Option<Vec<PubkyAppUserLink>>
let links_vec: Option<Vec<PubkyAppUserLink>> = if links.is_null() || links.is_undefined() {
None
} else {
from_value(links)?
from_value(links).map_err(|e| e.to_string())?
};

// 2) Build user domain object
Expand Down Expand Up @@ -180,11 +180,11 @@ impl PubkySpecsBuilder {
sort: String,
content: Option<String>,
name: String,
) -> Result<FeedResult, JsValue> {
) -> Result<FeedResult, String> {
let tags_vec: Option<Vec<String>> = if tags.is_null() || tags.is_undefined() {
None
} else {
from_value(tags)?
from_value(tags).map_err(|e| e.to_string())?
};

// Use `FromStr` to parse enums
Expand Down Expand Up @@ -219,7 +219,7 @@ impl PubkySpecsBuilder {
src: String,
content_type: String,
size: i64,
) -> Result<FileResult, JsValue> {
) -> Result<FileResult, String> {
let file = PubkyAppFile::new(name, src, content_type, size);
let file_id = file.create_id();
file.validate(&file_id)?;
Expand All @@ -242,7 +242,7 @@ impl PubkySpecsBuilder {
parent: Option<String>,
embed: Option<PubkyAppPostEmbed>,
attachments: Option<Vec<String>>,
) -> Result<PostResult, JsValue> {
) -> Result<PostResult, String> {
let post = PubkyAppPost::new(content, kind, parent, embed, attachments);
let post_id = post.create_id();
post.validate(&post_id)?;
Expand All @@ -258,7 +258,7 @@ impl PubkySpecsBuilder {
// -----------------------------------------------------------------------------

#[wasm_bindgen(js_name = createTag)]
pub fn create_tag(&self, uri: String, label: String) -> Result<TagResult, JsValue> {
pub fn create_tag(&self, uri: String, label: String) -> Result<TagResult, String> {
let tag = PubkyAppTag::new(uri, label);
let tag_id = tag.create_id();
tag.validate(&tag_id)?;
Expand All @@ -274,7 +274,7 @@ impl PubkySpecsBuilder {
// -----------------------------------------------------------------------------

#[wasm_bindgen(js_name = createBookmark)]
pub fn create_bookmark(&self, uri: String) -> Result<BookmarkResult, JsValue> {
pub fn create_bookmark(&self, uri: String) -> Result<BookmarkResult, String> {
let bookmark = PubkyAppBookmark::new(uri);
let bookmark_id = bookmark.create_id();
bookmark.validate(&bookmark_id)?;
Expand All @@ -290,7 +290,7 @@ impl PubkySpecsBuilder {
// -----------------------------------------------------------------------------

#[wasm_bindgen(js_name = createFollow)]
pub fn create_follow(&self, followee_id: String) -> Result<FollowResult, JsValue> {
pub fn create_follow(&self, followee_id: String) -> Result<FollowResult, String> {
let follow = PubkyAppFollow::new();
follow.validate(&followee_id)?; // No ID in follow, so we pass user ID or empty

Expand All @@ -306,7 +306,7 @@ impl PubkySpecsBuilder {
// -----------------------------------------------------------------------------

#[wasm_bindgen(js_name = createMute)]
pub fn create_mute(&self, mutee_id: String) -> Result<MuteResult, JsValue> {
pub fn create_mute(&self, mutee_id: String) -> Result<MuteResult, String> {
let mute = PubkyAppMute::new();
mute.validate(&mutee_id)?;

Expand All @@ -321,7 +321,7 @@ impl PubkySpecsBuilder {
// -----------------------------------------------------------------------------

#[wasm_bindgen(js_name = createLastRead)]
pub fn create_last_read(&self) -> Result<LastReadResult, JsValue> {
pub fn create_last_read(&self) -> Result<LastReadResult, String> {
let last_read = PubkyAppLastRead::new();
last_read.validate("")?;

Expand All @@ -336,10 +336,9 @@ impl PubkySpecsBuilder {
// -----------------------------------------------------------------------------

#[wasm_bindgen(js_name = createBlob)]
pub fn create_blob(&self, blob_data: JsValue) -> Result<BlobResult, JsValue> {
pub fn create_blob(&self, blob_data: JsValue) -> Result<BlobResult, String> {
// Convert from JsValue (Uint8Array in JS) -> Vec<u8> in Rust
let data_vec: Vec<u8> = from_value(blob_data)
.map_err(|e| JsValue::from_str(&format!("Invalid blob bytes: {}", e)))?;
let data_vec: Vec<u8> = from_value(blob_data).map_err(|e| e.to_string())?;

// Create the PubkyAppBlob
let blob = PubkyAppBlob(data_vec);
Expand All @@ -354,3 +353,84 @@ impl PubkySpecsBuilder {
Ok(BlobResult { blob, meta })
}
}

/// This object represents the result of parsing a Pubky URI. It contains:
/// - `user_id`: the parsed user ID as a string.
/// - `resource`: a string representing the kind of resource (derived from internal `Resource` enum Display).
/// - `resource_id`: an optional resource identifier (if applicable).
#[wasm_bindgen]
pub struct ParsedUriResult {
#[wasm_bindgen(skip)]
user_id: String,
#[wasm_bindgen(skip)]
resource: String,
#[wasm_bindgen(skip)]
resource_id: Option<String>,
}

#[wasm_bindgen]
impl ParsedUriResult {
/// Returns the user ID.
#[wasm_bindgen(getter)]
pub fn user_id(&self) -> String {
self.user_id.clone()
}

/// Returns the resource kind.
#[wasm_bindgen(getter)]
pub fn resource(&self) -> String {
self.resource.clone()
}

/// Returns the resource ID if present.
#[wasm_bindgen(getter)]
pub fn resource_id(&self) -> Option<String> {
self.resource_id.clone()
}
}

/// Parses a Pubky URI and returns a strongly typed `ParsedUriResult`.
///
/// This function wraps the internal ParsedUri ust parsing logic. It converts the result into a
/// strongly typed object that is easier to use in TypeScript.
///
/// # Parameters
///
/// - `uri`: A string slice representing the Pubky URI. The URI should follow the format:
/// `pubky://<user_id>/pub/pubky.app/<resource>[/<id>]`.
///
/// # Returns
///
/// On success, returns a `ParsedUriResult` with:
/// - `user_id`: the parsed user ID,
/// - `resource`: a string (derived from the Display implementation of internal `Resource` enum),
/// - `resource_id`: an optional resource identifier (if applicable).
///
/// On failure, returns a JavaScript error (`String`) containing an error message.
///
/// # Example (TypeScript)
///
/// ```typescript
/// import { parse_uri } from "pubky-app-specs";
///
/// try {
/// const result = parse_uri("pubky://user123/pub/pubky.app/posts/abc123");
/// console.log(result.user_id); // e.g. "user123"
/// console.log(result.resource); // e.g. "posts"
/// console.log(result.resource_id); // e.g. "abc123" or null
/// } catch (error) {
/// console.error("Error parsing URI:", error);
/// }
/// ```
#[wasm_bindgen]
pub fn parse_uri(uri: &str) -> Result<ParsedUriResult, String> {
// Attempt to parse the URI using ParsedUri logic.
let parsed = ParsedUri::try_from(uri)?;

// Build and return the strongly typed result.
Ok(ParsedUriResult {
user_id: parsed.user_id.to_string(),
resource: parsed.resource.to_string(),
resource_id: parsed.resource.id(),
})
}
31 changes: 30 additions & 1 deletion tests/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

extern crate wasm_bindgen_test;
use js_sys::Array;
use pubky_app_specs::{PubkyAppUserLink, PubkySpecsBuilder};
use pubky_app_specs::{parse_uri, PubkyAppUserLink, PubkySpecsBuilder};
use serde_wasm_bindgen::to_value;
use wasm_bindgen::JsValue;
use wasm_bindgen_test::*;
Expand Down Expand Up @@ -138,3 +138,32 @@ fn test_create_user_with_minimal_data() {
assert!(user.links().is_none());
assert_eq!(user.status(), None);
}

#[wasm_bindgen_test]
fn test_parse_uri() {
// A valid URI for a post resource.
let uri = "pubky://operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo/pub/pubky.app/posts/0032SSN7Q4EVG";

// Call the wasm-exposed parse_uri function.
let parsed = parse_uri(uri).expect("Expected valid URI parsing");

// Verify the user ID is correctly parsed.
assert_eq!(
parsed.user_id(),
"operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo",
"The user ID should match the host in the URI"
);

// Verify that the resource string indicates a post resource.
assert!(
parsed.resource().contains("posts"),
"The resource field should indicate a posts resource"
);

// Verify that the resource ID is correctly extracted.
assert_eq!(
parsed.resource_id().unwrap(),
"0032SSN7Q4EVG",
"The resource_id should match the post id provided in the URI"
);
}