Skip to main content

oxirs_core/jsonld/flattening/
node_map.rs

1//! Node map generation for JSON-LD 1.1 Flattening.
2//!
3//! The node map is the intermediate representation produced during flattening.
4//! Every named-graph-scoped subject appears as an entry in the node map for
5//! its graph (`@default` for the top-level graph).
6//!
7//! Spec reference:
8//! <https://www.w3.org/TR/json-ld11-api/#node-map-generation>
9
10use super::{FlatteningError, FlatteningOptions, JsonLdValue};
11use indexmap::IndexMap;
12
13// ============================================================================
14// BlankNodeIdMapper
15// ============================================================================
16
17/// Canonically renames blank node identifiers in encounter order.
18///
19/// The first blank node seen gets `_:b0`, the second `_:b1`, and so on.
20/// Mapping is idempotent: mapping the same original ID twice returns the
21/// same canonical name.
22#[derive(Debug, Default)]
23pub struct BlankNodeIdMapper {
24    mapping: IndexMap<String, String>,
25    counter: u64,
26}
27
28impl BlankNodeIdMapper {
29    /// Create a new, empty mapper.
30    pub fn new() -> Self {
31        Self::default()
32    }
33
34    /// Map `original` to a canonical blank-node ID.
35    ///
36    /// If `original` has been seen before the previously assigned name is
37    /// returned (idempotent).  Otherwise a new `_:bN` name is assigned.
38    pub fn map(&mut self, original: &str) -> String {
39        if let Some(mapped) = self.mapping.get(original) {
40            return mapped.clone();
41        }
42        let new_id = format!("_:b{}", self.counter);
43        self.counter += 1;
44        self.mapping.insert(original.to_string(), new_id.clone());
45        new_id
46    }
47
48    /// Reset the mapper so it starts again from `_:b0`.
49    ///
50    /// All previous mappings are discarded.
51    pub fn reset(&mut self) {
52        self.mapping.clear();
53        self.counter = 0;
54    }
55}
56
57// ============================================================================
58// NodeObject
59// ============================================================================
60
61/// A single RDF subject collected during node-map generation.
62#[derive(Debug, Clone, Default)]
63pub struct NodeObject {
64    /// Canonical subject identifier (may be a blank node `_:bN`).
65    pub id: String,
66    /// List of `@type` IRIs for this subject.
67    pub types: Vec<String>,
68    /// Property → array-of-values map.
69    pub properties: IndexMap<String, Vec<JsonLdValue>>,
70}
71
72impl NodeObject {
73    /// Create a new, empty `NodeObject` with the given identifier.
74    pub fn new(id: impl Into<String>) -> Self {
75        Self {
76            id: id.into(),
77            ..Default::default()
78        }
79    }
80
81    /// Merge a value into one of this node's property arrays.
82    ///
83    /// Duplicate `{"@value": …}` literals are not inserted twice.
84    pub fn merge_property(&mut self, property: &str, value: JsonLdValue) {
85        let values = self.properties.entry(property.to_string()).or_default();
86        merge_value(values, value);
87    }
88}
89
90// ============================================================================
91// GraphNodeMap
92// ============================================================================
93
94/// A subject-keyed map for one named graph (or the `@default` graph).
95#[derive(Debug, Clone, Default)]
96pub struct GraphNodeMap {
97    /// Subject ID → node object.
98    pub nodes: IndexMap<String, NodeObject>,
99}
100
101impl GraphNodeMap {
102    /// Create an empty graph node map.
103    pub fn new() -> Self {
104        Self::default()
105    }
106
107    /// Get or create the [`NodeObject`] for `id`.
108    pub fn get_or_create(&mut self, id: &str) -> &mut NodeObject {
109        self.nodes
110            .entry(id.to_string())
111            .or_insert_with(|| NodeObject::new(id))
112    }
113}
114
115// ============================================================================
116// NodeMap
117// ============================================================================
118
119/// The complete node map: one [`GraphNodeMap`] per named graph plus `@default`.
120#[derive(Debug, Clone, Default)]
121pub struct NodeMap {
122    /// Graph name → per-graph node map.  Always contains at least `@default`.
123    pub graphs: IndexMap<String, GraphNodeMap>,
124}
125
126impl NodeMap {
127    /// Create a new node map, pre-seeded with a `@default` graph.
128    pub fn new() -> Self {
129        let mut nm = Self::default();
130        nm.graphs
131            .insert("@default".to_string(), GraphNodeMap::new());
132        nm
133    }
134
135    /// Get the mutable reference to a graph, creating it if absent.
136    pub fn get_or_create_graph(&mut self, graph_name: &str) -> &mut GraphNodeMap {
137        self.graphs.entry(graph_name.to_string()).or_default()
138    }
139
140    /// Shorthand accessor for the `@default` graph.
141    pub fn default_graph(&self) -> &GraphNodeMap {
142        self.graphs
143            .get("@default")
144            .expect("@default graph always present")
145    }
146
147    /// Mutable shorthand accessor for the `@default` graph.
148    pub fn default_graph_mut(&mut self) -> &mut GraphNodeMap {
149        self.graphs
150            .get_mut("@default")
151            .expect("@default graph always present")
152    }
153}
154
155// ============================================================================
156// generate_node_map — public API
157// ============================================================================
158
159/// Generate a node map from an array of expanded JSON-LD node objects.
160///
161/// This implements the W3C JSON-LD 1.1 Node-Map Generation algorithm.
162///
163/// # Arguments
164///
165/// * `expanded`        — The expanded JSON-LD document (top-level array).
166/// * `options`         — Flattening options (used for processing mode).
167///
168/// # Returns
169///
170/// A [`NodeMap`] where every node has been collected by subject and graph.
171pub fn generate_node_map(
172    expanded: &[JsonLdValue],
173    options: &FlatteningOptions,
174) -> Result<NodeMap, FlatteningError> {
175    let mut node_map = NodeMap::new();
176    let mut blank_mapper = BlankNodeIdMapper::new();
177
178    for element in expanded {
179        generate_node_map_element(
180            element,
181            &mut node_map,
182            "@default",
183            None,
184            None,
185            &mut blank_mapper,
186            options,
187        )?;
188    }
189
190    Ok(node_map)
191}
192
193// ============================================================================
194// generate_node_map_element — recursive traversal
195// ============================================================================
196
197/// Process one JSON-LD element during node-map generation.
198///
199/// This is the core recursive step of the algorithm.  It handles:
200///
201/// * **Scalar values** — ignored at top level; stored as property values when
202///   nested.
203/// * **`@graph` objects** — recurse into the named graph.
204/// * **`@value` objects** — literal value; stored directly on the property.
205/// * **`@list` objects** — list node; each element is processed recursively.
206/// * **Node objects** — collected into the node map; properties are recursed.
207/// * **`@reverse` properties** — recorded as reverse links.
208#[allow(clippy::too_many_arguments)]
209pub fn generate_node_map_element(
210    element: &JsonLdValue,
211    node_map: &mut NodeMap,
212    active_graph: &str,
213    active_subject: Option<&str>,
214    active_property: Option<&str>,
215    blank_mapper: &mut BlankNodeIdMapper,
216    _options: &FlatteningOptions,
217) -> Result<(), FlatteningError> {
218    match element {
219        // ── Array ────────────────────────────────────────────────────────────
220        JsonLdValue::Array(items) => {
221            for item in items {
222                generate_node_map_element(
223                    item,
224                    node_map,
225                    active_graph,
226                    active_subject,
227                    active_property,
228                    blank_mapper,
229                    _options,
230                )?;
231            }
232            return Ok(());
233        }
234
235        // ── Non-object scalars ────────────────────────────────────────────────
236        JsonLdValue::Null | JsonLdValue::Bool(_) | JsonLdValue::Number(_) | JsonLdValue::Str(_) => {
237            // Scalars can appear as property values; store them if we have context.
238            if let (Some(subj), Some(prop)) = (active_subject, active_property) {
239                let graph = node_map.get_or_create_graph(active_graph);
240                graph
241                    .get_or_create(subj)
242                    .merge_property(prop, element.clone());
243            }
244            return Ok(());
245        }
246
247        JsonLdValue::Object(_) => {} // handled below
248    }
249
250    let obj = match element {
251        JsonLdValue::Object(m) => m,
252        _ => unreachable!("handled above"),
253    };
254
255    // ── @value object (literal) ───────────────────────────────────────────────
256    if obj.contains_key("@value") {
257        if let (Some(subj), Some(prop)) = (active_subject, active_property) {
258            let graph = node_map.get_or_create_graph(active_graph);
259            graph
260                .get_or_create(subj)
261                .merge_property(prop, element.clone());
262        }
263        return Ok(());
264    }
265
266    // ── @list object ─────────────────────────────────────────────────────────
267    if obj.contains_key("@list") {
268        // Process each item in the list, then store the whole list as a value.
269        let list_items = match obj.get("@list") {
270            Some(JsonLdValue::Array(items)) => items.clone(),
271            _ => Vec::new(),
272        };
273
274        // Collect processed list items.
275        let mut processed: Vec<JsonLdValue> = Vec::new();
276        for item in &list_items {
277            let item_copy = resolve_node_reference(item, node_map, active_graph, blank_mapper);
278            processed.push(item_copy);
279        }
280
281        let list_obj = {
282            let mut m: IndexMap<String, JsonLdValue> = IndexMap::new();
283            m.insert("@list".to_string(), JsonLdValue::Array(processed));
284            JsonLdValue::Object(m)
285        };
286
287        if let (Some(subj), Some(prop)) = (active_subject, active_property) {
288            let graph = node_map.get_or_create_graph(active_graph);
289            graph.get_or_create(subj).merge_property(prop, list_obj);
290        }
291        return Ok(());
292    }
293
294    // ── Determine subject ID ──────────────────────────────────────────────────
295    let id = get_or_assign_id(obj, blank_mapper);
296
297    // Ensure the node exists in the current graph.
298    node_map
299        .get_or_create_graph(active_graph)
300        .get_or_create(&id);
301
302    // If we are nested under a subject/property, link the parent to this node.
303    if let (Some(subj), Some(prop)) = (active_subject, active_property) {
304        let ref_obj = {
305            let mut m: IndexMap<String, JsonLdValue> = IndexMap::new();
306            m.insert("@id".to_string(), JsonLdValue::Str(id.clone()));
307            JsonLdValue::Object(m)
308        };
309        let graph = node_map.get_or_create_graph(active_graph);
310        graph.get_or_create(subj).merge_property(prop, ref_obj);
311    }
312
313    // ── Named graph (`@graph`) ────────────────────────────────────────────────
314    if let Some(graph_val) = obj.get("@graph") {
315        // Ensure the graph is registered in @default.
316        {
317            let def_graph = node_map.default_graph_mut();
318            def_graph.get_or_create(&id);
319        }
320        // Recurse into the named graph.
321        let graph_items = match graph_val {
322            JsonLdValue::Array(items) => items.clone(),
323            single => vec![single.clone()],
324        };
325        for item in &graph_items {
326            generate_node_map_element(item, node_map, &id, None, None, blank_mapper, _options)?;
327        }
328    }
329
330    // ── @type ─────────────────────────────────────────────────────────────────
331    if let Some(type_val) = obj.get("@type") {
332        let type_iris: Vec<String> = match type_val {
333            JsonLdValue::Array(items) => items
334                .iter()
335                .filter_map(|v| v.as_str())
336                .map(|s| {
337                    if s.starts_with("_:") {
338                        blank_mapper.map(s)
339                    } else {
340                        s.to_string()
341                    }
342                })
343                .collect(),
344            JsonLdValue::Str(s) => {
345                let mapped = if s.starts_with("_:") {
346                    blank_mapper.map(s)
347                } else {
348                    s.to_string()
349                };
350                vec![mapped]
351            }
352            _ => Vec::new(),
353        };
354
355        let graph = node_map.get_or_create_graph(active_graph);
356        let node = graph.get_or_create(&id);
357        for t in type_iris {
358            if !node.types.contains(&t) {
359                node.types.push(t.clone());
360            }
361            merge_value(
362                node.properties.entry("@type".to_string()).or_default(),
363                JsonLdValue::Str(t),
364            );
365        }
366    }
367
368    // ── @reverse properties ───────────────────────────────────────────────────
369    if let Some(JsonLdValue::Object(rev_obj)) = obj.get("@reverse") {
370        for (rev_prop, rev_val) in rev_obj {
371            let rev_items: Vec<JsonLdValue> = match rev_val {
372                JsonLdValue::Array(a) => a.clone(),
373                single => vec![single.clone()],
374            };
375            for rev_item in &rev_items {
376                // The reverse item is the *subject*; `id` is the object.
377                let rev_item_id = match rev_item {
378                    JsonLdValue::Object(m) => get_or_assign_id(m, blank_mapper),
379                    _ => continue,
380                };
381                // Ensure rev_item_id exists.
382                node_map
383                    .get_or_create_graph(active_graph)
384                    .get_or_create(&rev_item_id);
385                // Record the forward property on rev_item_id pointing to id.
386                let ref_obj = {
387                    let mut m: IndexMap<String, JsonLdValue> = IndexMap::new();
388                    m.insert("@id".to_string(), JsonLdValue::Str(id.clone()));
389                    JsonLdValue::Object(m)
390                };
391                let graph = node_map.get_or_create_graph(active_graph);
392                graph
393                    .get_or_create(&rev_item_id)
394                    .merge_property(rev_prop, ref_obj);
395            }
396        }
397    }
398
399    // ── Other properties ──────────────────────────────────────────────────────
400    for (prop, value) in obj {
401        // Skip already-handled keywords.
402        match prop.as_str() {
403            "@id" | "@type" | "@graph" | "@reverse" | "@context" => continue,
404            _ => {}
405        }
406
407        let values: Vec<JsonLdValue> = match value {
408            JsonLdValue::Array(a) => a.clone(),
409            single => vec![single.clone()],
410        };
411
412        for v in values {
413            generate_node_map_element(
414                &v,
415                node_map,
416                active_graph,
417                Some(&id),
418                Some(prop),
419                blank_mapper,
420                _options,
421            )?;
422        }
423    }
424
425    Ok(())
426}
427
428// ============================================================================
429// node_map_to_flat_array — serialise back to JSON-LD
430// ============================================================================
431
432/// Serialise the `@default` graph of a node map to a flat array of node objects.
433///
434/// Each [`NodeObject`] in the default graph is converted to a JSON-LD object
435/// with `@id`, optionally `@type`, and all recorded properties.
436///
437/// # Arguments
438///
439/// * `node_map` — The node map to serialise.
440/// * `ordered`  — If `true`, sort subjects lexicographically.
441pub fn node_map_to_flat_array(node_map: &NodeMap, ordered: bool) -> Vec<JsonLdValue> {
442    let default_graph = node_map.default_graph();
443
444    let mut subjects: Vec<&str> = default_graph.nodes.keys().map(String::as_str).collect();
445    if ordered {
446        subjects.sort_unstable();
447    }
448
449    let mut result: Vec<JsonLdValue> = Vec::with_capacity(subjects.len());
450
451    for subj_id in subjects {
452        let node_obj = &default_graph.nodes[subj_id];
453        let mut map: IndexMap<String, JsonLdValue> = IndexMap::new();
454
455        map.insert("@id".to_string(), JsonLdValue::Str(subj_id.to_string()));
456
457        // Include @type if present.
458        if !node_obj.types.is_empty() {
459            let types: Vec<JsonLdValue> = node_obj
460                .types
461                .iter()
462                .map(|t| JsonLdValue::Str(t.clone()))
463                .collect();
464            map.insert("@type".to_string(), JsonLdValue::Array(types));
465        }
466
467        // Include all other properties (skip @type — already handled above).
468        let mut props: Vec<&str> = node_obj.properties.keys().map(String::as_str).collect();
469        if ordered {
470            props.sort_unstable();
471        }
472        for prop in props {
473            if prop == "@type" {
474                continue;
475            }
476            let values = &node_obj.properties[prop];
477            if !values.is_empty() {
478                map.insert(prop.to_string(), JsonLdValue::Array(values.clone()));
479            }
480        }
481
482        // If this subject is a named graph, include its `@graph` member.
483        if node_map.graphs.contains_key(subj_id) && subj_id != "@default" {
484            let inner_graph = &node_map.graphs[subj_id];
485            let mut inner_subjects: Vec<&str> =
486                inner_graph.nodes.keys().map(String::as_str).collect();
487            if ordered {
488                inner_subjects.sort_unstable();
489            }
490            let inner_array: Vec<JsonLdValue> = inner_subjects
491                .iter()
492                .map(|s| {
493                    let inner_node = &inner_graph.nodes[*s];
494                    serialise_node_object(inner_node, ordered)
495                })
496                .collect();
497            map.insert("@graph".to_string(), JsonLdValue::Array(inner_array));
498        }
499
500        result.push(JsonLdValue::Object(map));
501    }
502
503    result
504}
505
506// ============================================================================
507// Internal helpers
508// ============================================================================
509
510/// Serialise a [`NodeObject`] to a JSON-LD object value.
511fn serialise_node_object(node: &NodeObject, ordered: bool) -> JsonLdValue {
512    let mut map: IndexMap<String, JsonLdValue> = IndexMap::new();
513
514    map.insert("@id".to_string(), JsonLdValue::Str(node.id.clone()));
515
516    if !node.types.is_empty() {
517        let types: Vec<JsonLdValue> = node
518            .types
519            .iter()
520            .map(|t| JsonLdValue::Str(t.clone()))
521            .collect();
522        map.insert("@type".to_string(), JsonLdValue::Array(types));
523    }
524
525    let mut props: Vec<&str> = node.properties.keys().map(String::as_str).collect();
526    if ordered {
527        props.sort_unstable();
528    }
529    for prop in props {
530        if prop == "@type" {
531            continue;
532        }
533        let values = &node.properties[prop];
534        if !values.is_empty() {
535            map.insert(prop.to_string(), JsonLdValue::Array(values.clone()));
536        }
537    }
538
539    JsonLdValue::Object(map)
540}
541
542/// Get `@id` from a node object, or assign a blank-node ID via the mapper.
543///
544/// If the object already has a string `@id` the value is returned (with
545/// blank-node renaming applied if it starts with `_:`).  If there is no `@id`
546/// a fresh blank-node ID is generated.
547fn get_or_assign_id(
548    obj: &IndexMap<String, JsonLdValue>,
549    blank_mapper: &mut BlankNodeIdMapper,
550) -> String {
551    match obj.get("@id") {
552        Some(JsonLdValue::Str(id)) => {
553            if id.starts_with("_:") {
554                blank_mapper.map(id)
555            } else {
556                id.clone()
557            }
558        }
559        _ => {
560            // Generate an anonymous blank-node ID.
561            let anon = format!("_:anon_{}", blank_mapper.counter);
562            blank_mapper.map(&anon)
563        }
564    }
565}
566
567/// Resolve a value that may be a node reference, ensuring the referenced node
568/// exists and returning the canonical `{"@id": "..."}` reference.
569///
570/// For non-node values (value objects, scalars) the original value is returned
571/// unchanged.
572fn resolve_node_reference(
573    value: &JsonLdValue,
574    node_map: &mut NodeMap,
575    active_graph: &str,
576    blank_mapper: &mut BlankNodeIdMapper,
577) -> JsonLdValue {
578    match value {
579        JsonLdValue::Object(m) if !m.contains_key("@value") && !m.contains_key("@list") => {
580            let id = get_or_assign_id(m, blank_mapper);
581            node_map
582                .get_or_create_graph(active_graph)
583                .get_or_create(&id);
584            let mut ref_map: IndexMap<String, JsonLdValue> = IndexMap::new();
585            ref_map.insert("@id".to_string(), JsonLdValue::Str(id));
586            JsonLdValue::Object(ref_map)
587        }
588        other => other.clone(),
589    }
590}
591
592/// Merge a value into a property array without duplicating identical `@value` literals.
593///
594/// Duplicate detection rules:
595/// - Two `{"@value": v}` objects are duplicates if all their entries are equal.
596/// - Any other two values are compared by structural equality (`PartialEq`).
597pub fn merge_value(into: &mut Vec<JsonLdValue>, value: JsonLdValue) {
598    // Check for exact structural duplicates.
599    if into.contains(&value) {
600        return;
601    }
602    into.push(value);
603}