1use serde_json::Value;
9use std::path::Path;
10
11use crate::convert::{ConvertOptions, PageConsumer, read_limited_file};
12use crate::document::html::{HtmlBlock, render_blocks_to_pages};
13use crate::error::{Error, Result};
14use crate::table::{TableAlign, TableData};
15
16const MAX_OCEL_BYTES: u64 = 64 * 1024 * 1024;
17const MAX_OCEL_DEPTH: usize = 100;
18const MAX_OCEL_VALUES: usize = 300_000;
19const MAX_OCEL_ROWS: usize = 200_000;
20const MAX_OCEL_NODES: usize = 100_000;
21const MAX_OCEL_STRING_BYTES: usize = 2 * 1024 * 1024;
22const MAX_OCEL_DISPLAY_BYTES: usize = 512;
23const ATTRIBUTE_TYPES: &[&str] = &["string", "time", "integer", "float", "boolean"];
24
25pub(crate) fn looks_like_prefix(prefix: &[u8]) -> bool {
26 let text = String::from_utf8_lossy(prefix);
27 let trimmed = text.trim_start_matches('\u{feff}').trim_start();
28 if !trimmed.starts_with('{') {
29 return false;
30 }
31 if let Ok(Value::Object(object)) = serde_json::from_str::<Value>(trimmed) {
32 return ["eventTypes", "events", "objectTypes", "objects"]
33 .iter()
34 .all(|key| object.get(*key).and_then(Value::as_array).is_some());
35 }
36 ["eventTypes", "events", "objectTypes", "objects"]
37 .iter()
38 .all(|key| text.contains(&format!("\"{key}\"")))
39}
40
41struct OcelPageSink<'a> {
42 inner: &'a mut dyn PageConsumer,
43 warnings: &'a [String],
44}
45
46impl PageConsumer for OcelPageSink<'_> {
47 fn consume(&mut self, mut page: crate::ir::Page) -> Result<()> {
48 page.source_format = "ocel-json".into();
49 if page.title.is_empty() {
50 page.title = "OCEL 2.0 JSON".into();
51 }
52 page.description =
53 "OCEL 2.0 event and object metadata is rendered inertly; attribute values, qualifiers and external references are not displayed or resolved".into();
54 for warning in self.warnings {
55 page.warn(warning.clone());
56 }
57 self.inner.consume(page)
58 }
59}
60
61pub(crate) fn convert(
62 path: &Path,
63 options: &ConvertOptions,
64 sink: &mut dyn PageConsumer,
65) -> Result<Vec<String>> {
66 let bytes = read_limited_file(
67 path,
68 options.max_input_bytes.min(MAX_OCEL_BYTES),
69 "OCEL JSON input",
70 )?;
71 let text = String::from_utf8(bytes)
72 .map_err(|error| Error::InvalidInput(format!("OCEL JSON must be UTF-8 JSON: {error}")))?;
73 let (table, metadata, warnings) = parse(&text)?;
74 let blocks = vec![
75 HtmlBlock::Heading {
76 level: 1,
77 text: "OCEL 2.0 JSON".into(),
78 },
79 HtmlBlock::Paragraph { text: metadata },
80 HtmlBlock::Table(table),
81 ];
82 let mut page_sink = OcelPageSink {
83 inner: sink,
84 warnings: &warnings,
85 };
86 render_blocks_to_pages(&blocks, &mut page_sink, options)?;
87 Ok(warnings)
88}
89
90#[derive(Default)]
91struct Summary {
92 rows: Vec<Vec<String>>,
93 event_types: usize,
94 object_types: usize,
95 events: usize,
96 objects: usize,
97 event_attributes: usize,
98 object_attributes: usize,
99 event_relationships: usize,
100 object_relationships: usize,
101}
102
103fn parse(text: &str) -> Result<(TableData, String, Vec<String>)> {
104 if text.len() as u64 > MAX_OCEL_BYTES {
105 return Err(Error::LimitExceeded(format!(
106 "OCEL JSON exceeds {MAX_OCEL_BYTES} bytes"
107 )));
108 }
109 preflight_depth(text)?;
110 let value: Value = serde_json::from_str(text)
111 .map_err(|error| Error::InvalidInput(format!("invalid OCEL JSON: {error}")))?;
112 let mut values = 0usize;
113 count_values(&value, 0, &mut values)?;
114 let root = value
115 .as_object()
116 .ok_or_else(|| Error::InvalidInput("OCEL JSON root must be an object".into()))?;
117 let event_types = required_array(root, "eventTypes")?;
118 let events = required_array(root, "events")?;
119 let object_types = required_array(root, "objectTypes")?;
120 let objects = required_array(root, "objects")?;
121 if event_types
122 .len()
123 .saturating_add(events.len())
124 .saturating_add(object_types.len())
125 .saturating_add(objects.len())
126 > MAX_OCEL_NODES
127 {
128 return Err(Error::LimitExceeded(format!(
129 "OCEL nodes exceed {MAX_OCEL_NODES}"
130 )));
131 }
132 let mut summary = Summary::default();
133 let mut warnings = Vec::new();
134 for (index, event_type) in event_types.iter().enumerate() {
135 let object = event_type.as_object().ok_or_else(|| {
136 Error::InvalidInput(format!(
137 "OCEL eventTypes item {} must be an object",
138 index + 1
139 ))
140 })?;
141 let name = required_string(object, "name", "event type", index)?;
142 let attrs = definition_attributes(object, "event type", index)?;
143 summary.event_types = summary.event_types.saturating_add(1);
144 push_row(
145 &mut summary,
146 vec![
147 "event-type".into(),
148 truncate(name),
149 "—".into(),
150 format!("{attrs} attrs"),
151 "—".into(),
152 ],
153 )?;
154 }
155 for (index, object_type) in object_types.iter().enumerate() {
156 let object = object_type.as_object().ok_or_else(|| {
157 Error::InvalidInput(format!(
158 "OCEL objectTypes item {} must be an object",
159 index + 1
160 ))
161 })?;
162 let name = required_string(object, "name", "object type", index)?;
163 let attrs = definition_attributes(object, "object type", index)?;
164 summary.object_types = summary.object_types.saturating_add(1);
165 push_row(
166 &mut summary,
167 vec![
168 "object-type".into(),
169 truncate(name),
170 "—".into(),
171 format!("{attrs} attrs"),
172 "—".into(),
173 ],
174 )?;
175 }
176 for (index, event) in events.iter().enumerate() {
177 let object = event.as_object().ok_or_else(|| {
178 Error::InvalidInput(format!("OCEL events item {} must be an object", index + 1))
179 })?;
180 let id = required_string(object, "id", "event", index)?;
181 let event_type = required_string(object, "type", "event", index)?;
182 let time = required_string(object, "time", "event", index)?;
183 let attrs = attribute_array(object, "attributes", "event", index)?;
184 let relationships = relationship_array(object, "relationships", "event", index)?;
185 for relationship in relationships {
186 validate_relationship(relationship, "event relationship", index)?;
187 }
188 summary.events = summary.events.saturating_add(1);
189 summary.event_attributes = summary.event_attributes.saturating_add(attrs.len());
190 summary.event_relationships = summary
191 .event_relationships
192 .saturating_add(relationships.len());
193 push_row(
194 &mut summary,
195 vec![
196 "event".into(),
197 truncate(id),
198 truncate(event_type),
199 truncate(time),
200 format!("attrs {} · objects {}", attrs.len(), relationships.len()),
201 ],
202 )?;
203 }
204 for (index, item) in objects.iter().enumerate() {
205 let object = item.as_object().ok_or_else(|| {
206 Error::InvalidInput(format!("OCEL objects item {} must be an object", index + 1))
207 })?;
208 let id = required_string(object, "id", "object", index)?;
209 let object_type = required_string(object, "type", "object", index)?;
210 let attrs = attribute_array(object, "attributes", "object", index)?;
211 let relationships = relationship_array(object, "relationships", "object", index)?;
212 for relationship in relationships {
213 validate_relationship(relationship, "object relationship", index)?;
214 }
215 summary.objects = summary.objects.saturating_add(1);
216 summary.object_attributes = summary.object_attributes.saturating_add(attrs.len());
217 summary.object_relationships = summary
218 .object_relationships
219 .saturating_add(relationships.len());
220 push_row(
221 &mut summary,
222 vec![
223 "object".into(),
224 truncate(id),
225 truncate(object_type),
226 format!("{} attrs", attrs.len()),
227 format!("objects {}", relationships.len()),
228 ],
229 )?;
230 }
231 if summary.rows.is_empty() {
232 return Err(Error::InvalidInput(
233 "OCEL JSON contains no event, object or type rows".into(),
234 ));
235 }
236 let metadata = format!(
237 "Event types: {}\nObject types: {}\nEvents: {}\nObjects: {}\nEvent attributes: {}\nObject attributes: {}\nEvent→object relationships: {}\nObject→object relationships: {}",
238 summary.event_types,
239 summary.object_types,
240 summary.events,
241 summary.objects,
242 summary.event_attributes,
243 summary.object_attributes,
244 summary.event_relationships,
245 summary.object_relationships
246 );
247 warnings.push("OCEL attribute values, qualifiers, object IDs in relationship payloads, timestamps beyond their inert text, URLs and external resources are omitted; no process-mining discovery, filtering or execution runs".into());
248 warnings.push("OCEL 2.0 JSON arrays and relationship counts are bounded; event/object type names are shown as labels without dereferencing or semantic evaluation".into());
249 Ok((
250 TableData {
251 headers: vec![
252 "Kind".into(),
253 "ID / name".into(),
254 "Type".into(),
255 "Time / attrs".into(),
256 "Relations".into(),
257 ],
258 rows: summary.rows,
259 alignments: vec![TableAlign::Left; 5],
260 raw_source: String::new(),
261 },
262 metadata,
263 warnings,
264 ))
265}
266
267fn required_array<'a>(root: &'a serde_json::Map<String, Value>, key: &str) -> Result<&'a [Value]> {
268 root.get(key)
269 .and_then(Value::as_array)
270 .map(Vec::as_slice)
271 .ok_or_else(|| Error::InvalidInput(format!("OCEL JSON requires {key} array")))
272}
273
274fn required_string<'a>(
275 object: &'a serde_json::Map<String, Value>,
276 key: &str,
277 kind: &str,
278 index: usize,
279) -> Result<&'a str> {
280 let value = object.get(key).and_then(Value::as_str).ok_or_else(|| {
281 Error::InvalidInput(format!("OCEL {kind} {} requires string {key}", index + 1))
282 })?;
283 if value.is_empty() {
284 return Err(Error::InvalidInput(format!(
285 "OCEL {kind} {} {key} must not be empty",
286 index + 1
287 )));
288 }
289 Ok(value)
290}
291
292fn definition_attributes(
293 object: &serde_json::Map<String, Value>,
294 kind: &str,
295 index: usize,
296) -> Result<usize> {
297 let Some(attributes) = object.get("attributes") else {
298 return Ok(0);
299 };
300 let attributes = attributes.as_array().ok_or_else(|| {
301 Error::InvalidInput(format!(
302 "OCEL {kind} {} attributes must be an array",
303 index + 1
304 ))
305 })?;
306 for attribute in attributes {
307 let attribute = attribute.as_object().ok_or_else(|| {
308 Error::InvalidInput(format!(
309 "OCEL {kind} {} attribute must be an object",
310 index + 1
311 ))
312 })?;
313 required_string(attribute, "name", "attribute", index)?;
314 let value_type = required_string(attribute, "type", "attribute", index)?;
315 if !ATTRIBUTE_TYPES.contains(&value_type) {
316 return Err(Error::Unsupported(format!(
317 "OCEL attribute type '{value_type}' is unsupported"
318 )));
319 }
320 }
321 Ok(attributes.len())
322}
323
324fn attribute_array<'a>(
325 object: &'a serde_json::Map<String, Value>,
326 key: &str,
327 kind: &str,
328 index: usize,
329) -> Result<&'a [Value]> {
330 object.get(key).map_or(Ok(&[]), |value| {
331 value.as_array().map(Vec::as_slice).ok_or_else(|| {
332 Error::InvalidInput(format!("OCEL {kind} {} {key} must be an array", index + 1))
333 })
334 })
335}
336
337fn relationship_array<'a>(
338 object: &'a serde_json::Map<String, Value>,
339 key: &str,
340 kind: &str,
341 index: usize,
342) -> Result<&'a [Value]> {
343 object.get(key).map_or(Ok(&[]), |value| {
344 value.as_array().map(Vec::as_slice).ok_or_else(|| {
345 Error::InvalidInput(format!("OCEL {kind} {} {key} must be an array", index + 1))
346 })
347 })
348}
349
350fn validate_relationship(value: &Value, kind: &str, index: usize) -> Result<()> {
351 let object = value.as_object().ok_or_else(|| {
352 Error::InvalidInput(format!("OCEL {kind} {} must be an object", index + 1))
353 })?;
354 required_string(object, "objectId", kind, index)?;
355 Ok(())
356}
357
358fn push_row(summary: &mut Summary, row: Vec<String>) -> Result<()> {
359 if summary.rows.len() >= MAX_OCEL_ROWS {
360 return Err(Error::LimitExceeded(format!(
361 "OCEL rows exceed {MAX_OCEL_ROWS}"
362 )));
363 }
364 summary.rows.push(row);
365 Ok(())
366}
367
368fn truncate(value: &str) -> String {
369 if value.len() <= MAX_OCEL_DISPLAY_BYTES {
370 return value.to_owned();
371 }
372 let mut end = MAX_OCEL_DISPLAY_BYTES;
373 while !value.is_char_boundary(end) {
374 end -= 1;
375 }
376 format!("{}…", &value[..end])
377}
378
379fn count_values(value: &Value, depth: usize, count: &mut usize) -> Result<()> {
380 if depth > MAX_OCEL_DEPTH {
381 return Err(Error::LimitExceeded(format!(
382 "OCEL JSON nesting exceeds {MAX_OCEL_DEPTH} levels"
383 )));
384 }
385 *count = count.saturating_add(1);
386 if *count > MAX_OCEL_VALUES {
387 return Err(Error::LimitExceeded(format!(
388 "OCEL JSON contains more than {MAX_OCEL_VALUES} values"
389 )));
390 }
391 match value {
392 Value::Array(values) => {
393 for item in values {
394 count_values(item, depth + 1, count)?;
395 }
396 }
397 Value::Object(map) => {
398 for item in map.values() {
399 count_values(item, depth + 1, count)?;
400 }
401 }
402 Value::String(value) if value.len() > MAX_OCEL_STRING_BYTES => {
403 return Err(Error::LimitExceeded(format!(
404 "OCEL JSON string exceeds {MAX_OCEL_STRING_BYTES} bytes"
405 )));
406 }
407 _ => {}
408 }
409 Ok(())
410}
411
412fn preflight_depth(text: &str) -> Result<()> {
413 let mut depth = 0usize;
414 let mut quoted = false;
415 let mut escaped = false;
416 for byte in text.bytes() {
417 if quoted {
418 if escaped {
419 escaped = false;
420 } else if byte == b'\\' {
421 escaped = true;
422 } else if byte == b'"' {
423 quoted = false;
424 }
425 continue;
426 }
427 match byte {
428 b'"' => quoted = true,
429 b'{' | b'[' => {
430 depth += 1;
431 if depth > MAX_OCEL_DEPTH {
432 return Err(Error::LimitExceeded(format!(
433 "OCEL JSON nesting exceeds {MAX_OCEL_DEPTH} levels"
434 )));
435 }
436 }
437 b'}' | b']' => depth = depth.saturating_sub(1),
438 _ => {}
439 }
440 }
441 Ok(())
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447
448 #[test]
449 fn recognizes_ocel_root() {
450 assert!(looks_like_prefix(
451 br#"{"eventTypes":[],"events":[],"objectTypes":[],"objects":[]}"#
452 ));
453 assert!(!looks_like_prefix(br#"{"events":[]}"#));
454 }
455
456 #[test]
457 fn summarizes_event_object_relationships_without_values() {
458 let (table, metadata, warnings) = parse(
459 r#"{"eventTypes":[{"name":"create-order","attributes":[{"name":"total","type":"integer"}]}],"objectTypes":[{"name":"order","attributes":[{"name":"secret","type":"string"}]}],"events":[{"id":"e1","type":"create-order","time":"2024-01-01T00:00:00Z","attributes":[{"name":"total","value":7}],"relationships":[{"objectId":"o1","qualifier":"item"}]}],"objects":[{"id":"o1","type":"order","attributes":[{"name":"secret","time":"2024-01-01T00:00:00Z","value":"very-secret"}]}]}"#,
460 ).unwrap();
461 assert_eq!(table.rows.len(), 4);
462 assert!(metadata.contains("Event→object relationships: 1"));
463 assert!(
464 !table
465 .rows
466 .iter()
467 .flatten()
468 .any(|value| value.contains("very-secret") || value.contains("7"))
469 );
470 assert!(
471 warnings
472 .iter()
473 .any(|warning| warning.contains("attribute values"))
474 );
475 }
476}