use crate::error::ResolveError;
use crate::resolver::{ResolveOptions, ResolveOutcome, Resolver};
use crate::transport::{HttpResponse, HttpTransport, TransportError};
use async_trait::async_trait;
use base64::Engine;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
#[derive(Default)]
pub struct FetchTransport;
impl FetchTransport {
async fn request(
&self,
method: &str,
url: &str,
body: Option<String>,
) -> Result<HttpResponse, TransportError> {
let opts = web_sys::RequestInit::new();
opts.set_method(method);
opts.set_mode(web_sys::RequestMode::Cors);
if let Some(b) = body {
let headers = web_sys::Headers::new().map_err(js_err)?;
headers
.set("content-type", "application/json")
.map_err(js_err)?;
opts.set_headers(&headers);
opts.set_body(&JsValue::from_str(&b));
}
let request = web_sys::Request::new_with_str_and_init(url, &opts).map_err(js_err)?;
let global = js_sys::global();
let fetch_fn = js_sys::Reflect::get(&global, &JsValue::from_str("fetch"))
.ok()
.and_then(|f| f.dyn_into::<js_sys::Function>().ok())
.ok_or_else(|| TransportError("no global fetch available".into()))?;
let promise = fetch_fn
.call1(&global, &request)
.map_err(js_err)?
.dyn_into::<js_sys::Promise>()
.map_err(|_| TransportError("fetch did not return a Promise".into()))?;
let resp_value = JsFuture::from(promise).await.map_err(js_err)?;
let resp: web_sys::Response = resp_value
.dyn_into()
.map_err(|_| TransportError("fetch response was not a Response".into()))?;
let status = resp.status();
let mut headers = Vec::new();
if let Ok(Some(ct)) = resp.headers().get("content-type") {
headers.push(("content-type".to_string(), ct));
}
if let Ok(Some(v)) = resp.headers().get("x-dig-verified") {
headers.push(("x-dig-verified".to_string(), v));
}
let buf = JsFuture::from(resp.array_buffer().map_err(js_err)?)
.await
.map_err(js_err)?;
let body = js_sys::Uint8Array::new(&buf).to_vec();
Ok(HttpResponse {
status,
headers,
body,
})
}
}
fn js_err(v: JsValue) -> TransportError {
TransportError(
v.as_string()
.or_else(|| js_sys::Error::from(v).message().as_string())
.unwrap_or_else(|| "fetch failed".into()),
)
}
#[async_trait(?Send)]
impl HttpTransport for FetchTransport {
async fn get(&self, url: &str) -> Result<HttpResponse, TransportError> {
self.request("GET", url, None).await
}
async fn post_json(&self, url: &str, body: String) -> Result<HttpResponse, TransportError> {
self.request("POST", url, Some(body)).await
}
}
fn opt(s: Option<String>) -> Option<String> {
s.filter(|v| !v.trim().is_empty())
}
fn options(
endpoint: Option<String>,
connect_url: Option<String>,
cache_path: Option<String>,
) -> ResolveOptions {
ResolveOptions {
endpoint: opt(endpoint),
connect_url: opt(connect_url),
cache_path: opt(cache_path),
}
}
async fn do_resolve(
urn: String,
endpoint: Option<String>,
connect_url: Option<String>,
cache_path: Option<String>,
) -> Result<ResolveOutcome, ResolveError> {
Resolver::with_options(FetchTransport, options(endpoint, connect_url, cache_path))
.resolve(&urn)
.await
}
fn connect_or_default(connect_url: Option<String>) -> String {
opt(connect_url).unwrap_or_else(|| crate::pages::DEFAULT_CONNECT_URL.to_string())
}
#[wasm_bindgen]
pub struct ResolveResult {
outcome: String,
bytes: Vec<u8>,
content_type: String,
}
#[wasm_bindgen]
impl ResolveResult {
#[wasm_bindgen(getter)]
pub fn outcome(&self) -> String {
self.outcome.clone()
}
#[wasm_bindgen(getter)]
pub fn bytes(&self) -> Vec<u8> {
self.bytes.clone()
}
#[wasm_bindgen(getter, js_name = contentType)]
pub fn content_type(&self) -> String {
self.content_type.clone()
}
}
fn outcome_to_result(outcome: &ResolveOutcome, connect_url: &str) -> ResolveResult {
let rendered = outcome.render(connect_url);
ResolveResult {
outcome: outcome.kind().to_string(),
bytes: rendered.bytes,
content_type: rendered.content_type,
}
}
#[wasm_bindgen(typescript_custom_section)]
const TS_DIG_NETWORK_OPTIONS: &'static str = r#"
/** Options for the {@link DigNetwork} constructor. Every field is optional; an
* omitted field keeps its §5.3 default. */
export interface DigNetworkOptions {
/** An explicit node/gateway endpoint override — WINS over the auto-ladder. A
* loopback host may use the node path; any other host is a client-verified rpc
* endpoint. */
endpoint?: string;
/** The "Connect to Node" target shown on the unreachable page. */
connectUrl?: string;
/** A disk-cache directory (persisted, re-verified on read). Absent ⇒ the
* in-memory LRU only. Ignored in the browser (no filesystem). */
cachePath?: string;
}
"#;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(typescript_type = "DigNetworkOptions")]
pub type DigNetworkOptions;
}
#[derive(Default, serde::Deserialize)]
#[serde(rename_all = "camelCase", default)]
struct ParsedOptions {
endpoint: Option<String>,
connect_url: Option<String>,
cache_path: Option<String>,
}
#[wasm_bindgen]
#[derive(Clone)]
pub struct DigNetwork {
endpoint: Option<String>,
connect_url: Option<String>,
cache_path: Option<String>,
}
impl DigNetwork {
fn from_parts(
endpoint: Option<String>,
connect_url: Option<String>,
cache_path: Option<String>,
) -> DigNetwork {
DigNetwork {
endpoint: opt(endpoint),
connect_url: opt(connect_url),
cache_path: opt(cache_path),
}
}
}
#[wasm_bindgen]
impl DigNetwork {
#[wasm_bindgen(constructor)]
pub fn new(options: Option<DigNetworkOptions>) -> DigNetwork {
let parsed: ParsedOptions = options
.and_then(|o| serde_wasm_bindgen::from_value(JsValue::from(o)).ok())
.unwrap_or_default();
DigNetwork::from_parts(parsed.endpoint, parsed.connect_url, parsed.cache_path)
}
#[wasm_bindgen(getter)]
pub fn endpoint(&self) -> Option<String> {
self.endpoint.clone()
}
#[wasm_bindgen(getter, js_name = connectUrl)]
pub fn connect_url(&self) -> Option<String> {
self.connect_url.clone()
}
#[wasm_bindgen(getter, js_name = cachePath)]
pub fn cache_path(&self) -> Option<String> {
self.cache_path.clone()
}
pub async fn resolve(&self, urn: String) -> Result<ResolveResult, JsError> {
let connect = self.connect_url.clone();
let outcome = do_resolve(
urn,
self.endpoint.clone(),
connect.clone(),
self.cache_path.clone(),
)
.await
.map_err(|e| JsError::new(&e.to_string()))?;
Ok(outcome_to_result(&outcome, &connect_or_default(connect)))
}
#[wasm_bindgen(js_name = resolveImageUrl)]
pub async fn resolve_image_url(&self, urn: String) -> Result<String, JsError> {
match do_resolve(
urn,
self.endpoint.clone(),
self.connect_url.clone(),
self.cache_path.clone(),
)
.await
{
Ok(ResolveOutcome::Success(data)) => Ok(img_url(&data.bytes, &data.content_type)),
Ok(other) => Ok(crate::images::data_uri(crate::images::for_outcome(&other))),
Err(e) => Ok(crate::images::data_uri(crate::images::for_error(&e))),
}
}
}
#[wasm_bindgen]
pub async fn resolve(
urn: String,
endpoint: Option<String>,
connect_url: Option<String>,
) -> Result<ResolveResult, JsError> {
DigNetwork::from_parts(endpoint, connect_url, None)
.resolve(urn)
.await
}
#[wasm_bindgen(js_name = resolveObjectUrl)]
pub async fn resolve_object_url(
urn: String,
endpoint: Option<String>,
connect_url: Option<String>,
) -> Result<String, JsError> {
DigNetwork::from_parts(endpoint, connect_url, None)
.resolve_image_url(urn)
.await
}
fn img_url(bytes: &[u8], content_type: &str) -> String {
match object_url(bytes, content_type) {
Ok(url) => url,
Err(_) => data_url(bytes, content_type),
}
}
fn data_url(bytes: &[u8], content_type: &str) -> String {
let b64 = base64::engine::general_purpose::STANDARD.encode(bytes);
format!("data:{content_type};base64,{b64}")
}
fn object_url(bytes: &[u8], content_type: &str) -> Result<String, TransportError> {
let array = js_sys::Uint8Array::from(bytes);
let parts = js_sys::Array::new();
parts.push(&array);
let bag = web_sys::BlobPropertyBag::new();
bag.set_type(content_type);
let blob =
web_sys::Blob::new_with_u8_array_sequence_and_options(&parts, &bag).map_err(js_err)?;
web_sys::Url::create_object_url_with_blob(&blob).map_err(js_err)
}
#[wasm_bindgen(js_name = version)]
pub fn version() -> String {
crate::version().to_string()
}