Skip to content

Commit

Permalink
Add ffi to expose uri parser on wasm (#16)
Browse files Browse the repository at this point in the history
* Add ffi to expose uri parser on wasm

* Add docs bump version
  • Loading branch information
SHAcollision authored Feb 5, 2025
1 parent 26398e9 commit 006c098
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 21 deletions.
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"
);
}

0 comments on commit 006c098

Please sign in to comment.