nostralink 0.2.1

Linked data library for nostr
Documentation
//! Nostralink IRIs

use crate::err::LDError;
use nostr::{
    event::EventId, nips::nip21::FromNostrUri, nips::nip21::ToNostrUri,
    PublicKey,
};
use oxigraph::model::*;
use oxiri::Iri as OxIri;
use rand_09::{distr::Alphanumeric, Rng};
use serde_json::Value;
use sha256::digest;
use sophia_iri::resolve::BaseIri;
use sophia_jsonld::vocabulary::ArcIri;
use url::Url;
use urn::UrnBuilder;

// Base Nostralink IRI, used in the JSON-LD contexts
pub const NOSTRA_BASE_IRI: &str = "http://nostralink.org/";

// NSS for the nostr URNs
const NSS_NOSTR_CONTENT: &str = "nostr-content";
const NSS_NOSTR_EVENT: &str = "nostr-event";

pub trait ToNamedNode {
    /// URN
    fn named_node(&self) -> Result<NamedNode, LDError>;

    /// URN with an f-component
    fn named_node_with_f(&self, _f_comp: &str) -> Result<NamedNode, LDError> {
        Err(LDError::NamedNodeConvertError)
    }
}

pub trait FromNamedNode {
    fn from_named_node(&self) -> Result<PublicKey, LDError>;
}

impl ToNamedNode for EventId {
    fn named_node(&self) -> Result<NamedNode, LDError> {
        let urn = UrnBuilder::new(NSS_NOSTR_EVENT, &self.to_hex())
            .build()
            .unwrap();

        Ok(NamedNode::new_unchecked(urn.as_str()))
    }

    fn named_node_with_f(&self, f_comp: &str) -> Result<NamedNode, LDError> {
        let mut urn = UrnBuilder::new(NSS_NOSTR_EVENT, &self.to_hex())
            .build()
            .unwrap();
        let _ = urn.set_f_component(Some(f_comp));

        Ok(NamedNode::new(urn.as_str()).unwrap())
    }
}

impl ToNamedNode for PublicKey {
    fn named_node(&self) -> Result<NamedNode, LDError> {
        match self.to_nostr_uri() {
            Ok(uri) => Ok(NamedNode::new_unchecked(uri)),
            Err(_err) => Err(LDError::NamedNodeConvertError),
        }
    }

    fn named_node_with_f(&self, f_comp: &str) -> Result<NamedNode, LDError> {
        match self.to_nostr_uri() {
            Ok(uri) => Ok(NamedNode::new_unchecked(format!("{uri}#{f_comp}"))),
            Err(_err) => Err(LDError::NamedNodeConvertError),
        }
    }
}

impl FromNamedNode for NamedNode {
    fn from_named_node(&self) -> Result<PublicKey, LDError> {
        match PublicKey::from_nostr_uri(&self.clone().into_string()) {
            Ok(pk) => Ok(pk),
            Err(_err) => Err(LDError::NamedNodeConvertError),
        }
    }
}

/// Computes a URN NamedNode from a serde Value
pub fn content_iri_for_value(value: Value) -> Result<String, LDError> {
    let content_digest = digest(value.to_string());

    let urn = UrnBuilder::new(NSS_NOSTR_CONTENT, &content_digest)
        .build()
        .map_err(|_| LDError::URNError)?;
    Ok(urn.as_str().into())
}

pub fn random_urn_nn() -> NamedNode {
    let nid: String = rand_09::rng()
        .sample_iter(&Alphanumeric)
        .take(128)
        .map(char::from)
        .collect();

    NamedNode::new_unchecked(format!("urn:rand:{nid}"))
}

/// Generates a NamedNode for: <https://w3id.org/nostr#fragment>
pub fn nostr_vocab_nn(fragment: &str) -> NamedNode {
    let mut url = Url::parse("https://w3id.org/nostr#").unwrap();
    url.set_fragment(Some(fragment));
    NamedNode::new_unchecked(url.to_string())
}

pub fn nl_base_iri() -> BaseIri<&'static str> {
    BaseIri::new(NOSTRA_BASE_IRI).unwrap()
}

pub fn nl_iri(path: &str) -> OxIri<String> {
    BaseIri::new(NOSTRA_BASE_IRI)
        .unwrap()
        .resolve_unchecked(path)
}

pub fn nl_arc_iri(path: &str) -> ArcIri {
    ArcIri::new_unchecked(nl_iri(path).to_string().into())
}

#[test]
fn test_iris() {
    assert_eq!(
        nl_iri("EventReaction").to_string(),
        "http://nostralink.org/EventReaction"
    );
    assert_eq!(nl_iri("Action").to_string(), "http://nostralink.org/Action");
    assert_eq!(
        nl_iri("Note#frag").to_string(),
        "http://nostralink.org/Note#frag"
    );
}