Skip to main content

mif_core/
urn.rs

1//! Concept URNs (`urn:mif:<uuid>`, MIF spec ยง6.1).
2//!
3//! MIF 1.4.0 requires a concept `@id` to be `urn:mif:<uuid>`; the
4//! `mif.schema.json` pattern rejects slug or structured suffixes such as
5//! `urn:mif:my-note` or `urn:mif:source:<ns>:<slug>`. Producers that key
6//! concepts by a stable name derive the UUID with [`concept_uuid`], a version-5 UUID
7//! in the same namespace the MIF repo's `scripts/migrate_0_1_to_1_0.py`
8//! uses, so the same name always yields the same URN across tools.
9
10use uuid::Uuid;
11
12/// The URN prefix of a concept `@id`.
13pub const CONCEPT_URN_PREFIX: &str = "urn:mif:";
14
15/// The version-5 UUID namespace for derived concept ids:
16/// `uuid5(NAMESPACE_URL, "https://mif-spec.dev")`.
17#[must_use]
18pub fn mif_namespace() -> Uuid {
19    Uuid::new_v5(&Uuid::NAMESPACE_URL, b"https://mif-spec.dev")
20}
21
22/// The deterministic concept UUID for `name` (version 5, in [`mif_namespace`]).
23#[must_use]
24pub fn concept_uuid(name: &str) -> Uuid {
25    Uuid::new_v5(&mif_namespace(), name.as_bytes())
26}
27
28/// The concept URN `urn:mif:<uuid>` for `name`.
29///
30/// ```
31/// let urn = mif_core::concept_urn("source:topic:paper");
32/// assert!(mif_core::is_concept_urn(&urn));
33/// assert_eq!(urn, mif_core::concept_urn("source:topic:paper"));
34/// ```
35#[must_use]
36pub fn concept_urn(name: &str) -> String {
37    format!("{CONCEPT_URN_PREFIX}{}", concept_uuid(name).hyphenated())
38}
39
40/// Whether `id` is a valid concept URN: `urn:mif:` followed by a hyphenated
41/// UUID, the form `mif.schema.json`'s `@id` pattern accepts.
42#[must_use]
43pub fn is_concept_urn(id: &str) -> bool {
44    id.strip_prefix(CONCEPT_URN_PREFIX)
45        .is_some_and(|rest| rest.len() == 36 && Uuid::try_parse(rest).is_ok())
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    #[test]
53    fn namespace_matches_mif_migration_script() {
54        // python3 -c 'import uuid;print(uuid.uuid5(uuid.NAMESPACE_URL,"https://mif-spec.dev"))'
55        assert_eq!(
56            mif_namespace().to_string(),
57            "2d637db4-b33e-5a8f-9663-b8dff8cf358b"
58        );
59    }
60
61    #[test]
62    fn concept_urn_matches_python_uuid5() {
63        // python3 -c 'import uuid;n=uuid.uuid5(uuid.NAMESPACE_URL,"https://mif-spec.dev");print(uuid.uuid5(n,"memory:test-001"))'
64        assert_eq!(
65            concept_urn("memory:test-001"),
66            "urn:mif:89886348-4773-5732-88b5-84366da76468"
67        );
68    }
69
70    #[test]
71    fn is_concept_urn_accepts_uuid_and_rejects_slugs() {
72        assert!(is_concept_urn(
73            "urn:mif:550e8400-e29b-41d4-a716-446655440000"
74        ));
75        assert!(!is_concept_urn("urn:mif:my-note"));
76        assert!(!is_concept_urn("urn:mif:550e8400"));
77        assert!(!is_concept_urn(
78            "urn:mif:entity:550e8400-e29b-41d4-a716-446655440000"
79        ));
80        assert!(!is_concept_urn("550e8400-e29b-41d4-a716-446655440000"));
81        // Simple (unhyphenated) form: valid UUID, but not the schema's pattern.
82        assert!(!is_concept_urn("urn:mif:550e8400e29b41d4a716446655440000"));
83    }
84}