Skip to content

Commit 93980ba

Browse files
committed
✨ Settle on wasm api design
... by implementing `UserId` as an example, on both the Rust and the TypeScript sides, since it's one of the simplest packets. `tsify-next` is used with `wasm-bindgen` for bindgen. One alternative that has been considered is using pure `wasm-bindgen`, but that does not support Rust-style `enum`s. We could use `serde-wasm-bindgen` to overcome that limitation, but doing so requires that we return `JsValue`s on the wasm-js boundary, which prevents strong typing and `.d.ts` generation. To return our `struct`s directly, we have to implement `IntoWasmAbi` and other traits manually, which is not quite ergonomic, and still does not allow `.d.ts` generation. `tsify-next` helps generate those trait implementations, as well as the `.d.ts` file, so it's considered the best solution at present. Note that `tsify-next` is a successor of `tsify`. The latter has been unmaintained for over 1.5 years at the time of this commit. We could switch to `tsify` if it gets updated in the future. Furthermore, if `wasm-bindgen` adds support for Rust-style `enum`s, we could stop using `tsify-next` (see ref for the tracking issue). `ts-rs` has also been considered, but it does not generate the `wasm-bindgen::convert` traits, and generates each `struct`s and `enum`s into their own `.d.ts` files, which is quite messy. Ref: rustwasm/wasm-bindgen#2407
1 parent 1f2e14a commit 93980ba

File tree

4 files changed

+124
-33
lines changed

4 files changed

+124
-33
lines changed

Cargo.lock

Lines changed: 36 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pgpvis-core/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ crate-type = ["cdylib"]
99
[dependencies]
1010
pgp = { version = "0.14.2", features = ["wasm"] }
1111
serde = { version = "1.0.216", features = ["derive"] }
12-
serde-wasm-bindgen = "0.6.5"
1312
thiserror = "2.0.8"
13+
tsify-next = { version = "0.5.4", features = ["js"], default-features = false }
1414
wasm-bindgen = "0.2.99"
1515

1616
[dev-dependencies]

pgpvis-core/src/lib.rs

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,80 @@
1-
use std::collections::HashMap;
21
use std::iter::Iterator;
32

4-
use pgp::{armor::Dearmor, packet::PacketParser};
5-
use serde::{Deserialize, Serialize};
3+
use pgp::{
4+
armor::Dearmor,
5+
packet::{PacketParser, PacketTrait},
6+
};
7+
use serde::Serialize;
8+
use tsify_next::{declare, Tsify};
69
use wasm_bindgen::prelude::*;
710

8-
#[derive(Debug, Serialize, Deserialize)]
9-
struct ParseOutput(HashMap<String, String>);
11+
#[declare]
12+
pub type Message = Vec<Packet>;
1013

11-
impl From<HashMap<String, String>> for ParseOutput {
12-
fn from(value: HashMap<String, String>) -> Self {
13-
Self(value)
14+
#[derive(Debug, Serialize, Tsify)]
15+
#[tsify(into_wasm_abi)]
16+
pub enum Packet {
17+
UserId(UserId),
18+
Unknown,
19+
}
20+
21+
impl From<pgp::packet::Packet> for Packet {
22+
fn from(value: pgp::packet::Packet) -> Self {
23+
use pgp::packet::Packet as RpgpPacket;
24+
match value {
25+
RpgpPacket::UserId(user_id) => Self::UserId(user_id.into()),
26+
_ => Self::Unknown,
27+
}
28+
}
29+
}
30+
31+
#[derive(Debug, Serialize, Tsify)]
32+
pub struct UserId {
33+
pub packet_version: Version,
34+
pub id: String,
35+
}
36+
37+
impl From<pgp::packet::UserId> for UserId {
38+
fn from(value: pgp::packet::UserId) -> Self {
39+
Self {
40+
packet_version: value.packet_version().into(),
41+
id: value.id().to_string(),
42+
}
1443
}
1544
}
1645

17-
impl TryFrom<ParseOutput> for JsValue {
18-
type Error = Error;
46+
#[derive(Debug, Serialize, Tsify)]
47+
pub enum Version {
48+
Old,
49+
New,
50+
}
1951

20-
fn try_from(value: ParseOutput) -> Result<Self, Self::Error> {
21-
Ok(serde_wasm_bindgen::to_value(&value)?)
52+
impl From<pgp::types::Version> for Version {
53+
fn from(value: pgp::types::Version) -> Self {
54+
match value {
55+
pgp::types::Version::Old => Self::Old,
56+
pgp::types::Version::New => Self::New,
57+
}
2258
}
2359
}
2460

2561
#[derive(Debug, thiserror::Error)]
2662
enum Error {
27-
#[error("failed to serialize to JsValue")]
28-
Serialization(#[from] serde_wasm_bindgen::Error),
2963
#[error("failed to parse packet")]
3064
Parse(#[from] pgp::errors::Error),
3165
}
3266

33-
#[wasm_bindgen(js_name = parse_armored)]
34-
pub fn parse_armored_js(message: &str) -> Result<JsValue, JsError> {
35-
Ok(parse_armored(message)?.try_into()?)
36-
}
37-
38-
fn parse_armored(message: &str) -> Result<ParseOutput, Error> {
67+
#[wasm_bindgen]
68+
pub fn parse_armored(message: &str) -> Result<Message, JsError> {
3969
let dearmor = Dearmor::new(message.as_bytes());
4070
let packet_parser = PacketParser::new(dearmor);
41-
let parse_output: HashMap<_, _> = packet_parser
42-
.enumerate()
71+
let parse_output: Result<Vec<Packet>, Error> = packet_parser
4372
.map(|packet| match packet {
44-
(idx, Ok(packet)) => (idx.to_string(), format!("{packet:?}")),
45-
(idx, Err(err)) => (idx.to_string(), Error::from(err).to_string()),
73+
Ok(packet) => Ok(packet.into()),
74+
Err(err) => Err(err.into()),
4675
})
4776
.collect();
48-
Ok(parse_output.into())
77+
Ok(parse_output?)
4978
}
5079

5180
#[cfg(test)]
@@ -57,6 +86,12 @@ mod tests {
5786
#[wasm_bindgen_test]
5887
fn test_parse_armored_js() {
5988
let parse_output = parse_armored(include_str!("../tests/data/linus.gpg.asc")).unwrap();
60-
console_log!("{parse_output:#?}")
89+
for packet in parse_output {
90+
if let Packet::UserId(user_id) = packet {
91+
let packet_version = user_id.packet_version;
92+
let id = user_id.id;
93+
console_log!("{packet_version:?}: {id}");
94+
}
95+
}
6196
}
6297
}

pgpvis-ui/src/App.vue

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,37 @@
11
<script setup lang="ts">
2-
import { ref } from "vue";
2+
import { computed, ref } from "vue";
33
4-
import { parse_armored } from "pgpvis-core";
4+
import { type Message, parse_armored } from "pgpvis-core";
55
66
const message = ref("");
7-
const packets = ref<Map<string, string>>();
7+
const packets = ref<Message>();
8+
9+
const user_ids = computed(() => {
10+
if (packets.value === undefined) {
11+
return [];
12+
}
13+
14+
const user_ids = [];
15+
for (let packet of packets.value) {
16+
if (packet === "Unknown") {
17+
continue;
18+
}
19+
if ("UserId" in packet) {
20+
user_ids.push(packet.UserId);
21+
}
22+
}
23+
return user_ids;
24+
});
25+
26+
function parse() {
27+
packets.value = parse_armored(message.value);
28+
}
829
</script>
930

1031
<template>
1132
<textarea v-model="message"></textarea>
12-
<button @click="packets = parse_armored(message)">Parse</button>
13-
<li v-for="[index, packet] in packets" :key="index">{{ index }} | {{ packet }}</li>
33+
<button @click="parse">Parse</button>
34+
<li v-for="(user_id, index) in user_ids" :key="index">{{ user_id.id }}</li>
1435
</template>
1536

1637
<style scoped></style>

0 commit comments

Comments
 (0)