#![cfg(feature = "bindings")]
use crate::core::{
event_data::{
case_centric::utils::activity_projection::EventLogActivityProjection,
object_centric::{
linked_ocel::{IndexLinkedOCEL, LinkedOCELAccess, SlimLinkedOCEL},
ocel_struct::OCEL,
},
},
io::ExtensionWithMime,
EventLog,
};
pub use macros_process_mining::{register_binding, CustomRegistryEntity, RegistryEntity};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::{collections::HashMap, fmt::Display};
use std::{str::FromStr, sync::RwLock};
fn case_centric_import_formats() -> Vec<ExtensionWithMime> {
if cfg!(feature = "extraction-blueprint") {
vec![
ExtensionWithMime::new("xes", "application/xml"),
ExtensionWithMime::new("xes.gz", "application/gzip"),
]
} else {
Vec::new()
}
}
#[cfg(feature = "extraction-blueprint")]
fn reads_as_case_centric(item_kind: &RegistryItemKind, format: &str) -> bool {
matches!(
item_kind,
RegistryItemKind::OCEL | RegistryItemKind::SlimLinkedOCEL
) && (format.ends_with("xes") || format.ends_with("xes.gz"))
}
#[cfg(feature = "extraction-blueprint")]
fn case_centric_as(item_kind: &RegistryItemKind, log: &EventLog) -> Result<RegistryItem, String> {
use crate::core::event_data::object_centric::extraction::{
event_log_to_ocel, event_log_to_slim_ocel,
};
match item_kind {
RegistryItemKind::OCEL => event_log_to_ocel(log).map(RegistryItem::OCEL),
_ => event_log_to_slim_ocel(log).map(RegistryItem::SlimLinkedOCEL),
}
.map_err(|e| e.to_string())
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant, missing_docs)]
pub enum RegistryItem {
TabularSource(TabularSource),
EventLogActivityProjection(EventLogActivityProjection),
IndexLinkedOCEL(IndexLinkedOCEL),
SlimLinkedOCEL(SlimLinkedOCEL),
EventLog(EventLog),
OCEL(OCEL),
Custom(Box<dyn CustomRegistryValue>),
}
impl From<EventLog> for RegistryItem {
fn from(value: EventLog) -> Self {
Self::EventLog(value)
}
}
impl From<EventLogActivityProjection> for RegistryItem {
fn from(value: EventLogActivityProjection) -> Self {
Self::EventLogActivityProjection(value)
}
}
impl From<IndexLinkedOCEL> for RegistryItem {
fn from(value: IndexLinkedOCEL) -> Self {
Self::IndexLinkedOCEL(value)
}
}
impl From<OCEL> for RegistryItem {
fn from(value: OCEL) -> Self {
Self::OCEL(value)
}
}
impl From<SlimLinkedOCEL> for RegistryItem {
fn from(value: SlimLinkedOCEL) -> Self {
Self::SlimLinkedOCEL(value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum RegistryItemKind {
TabularSource,
EventLogActivityProjection,
IndexLinkedOCEL,
SlimLinkedOCEL,
EventLog,
OCEL,
Custom(&'static str),
}
impl Display for RegistryItemKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
impl RegistryItemKind {
pub fn name(&self) -> &'static str {
match self {
RegistryItemKind::EventLogActivityProjection => "EventLogActivityProjection",
RegistryItemKind::IndexLinkedOCEL => "IndexLinkedOCEL",
RegistryItemKind::SlimLinkedOCEL => "SlimLinkedOCEL",
RegistryItemKind::EventLog => "EventLog",
RegistryItemKind::OCEL => "OCEL",
RegistryItemKind::TabularSource => "TabularSource",
RegistryItemKind::Custom(name) => name,
}
}
pub fn all_kinds() -> &'static [Self] {
&[
RegistryItemKind::OCEL,
RegistryItemKind::EventLog,
RegistryItemKind::EventLogActivityProjection,
RegistryItemKind::SlimLinkedOCEL,
RegistryItemKind::IndexLinkedOCEL,
RegistryItemKind::TabularSource,
]
}
pub fn all_registered_kinds() -> Vec<Self> {
Self::all_kinds()
.iter()
.copied()
.chain(custom_kinds().into_iter().map(|c| Self::Custom(c.name)))
.collect()
}
pub fn known_import_formats(&self) -> Vec<ExtensionWithMime> {
match self {
RegistryItemKind::EventLogActivityProjection => {
EventLogActivityProjection::known_import_formats()
}
RegistryItemKind::IndexLinkedOCEL => IndexLinkedOCEL::known_import_formats(),
RegistryItemKind::EventLog => EventLog::known_import_formats(),
RegistryItemKind::OCEL | RegistryItemKind::SlimLinkedOCEL => {
let mut formats = OCEL::known_import_formats();
formats.extend(case_centric_import_formats());
formats
}
RegistryItemKind::TabularSource => TabularSource::known_import_formats(),
RegistryItemKind::Custom(name) => custom_kind(name)
.map(|c| (c.import_formats)())
.unwrap_or_default(),
}
}
pub fn known_export_formats(&self) -> Vec<ExtensionWithMime> {
match self {
RegistryItemKind::EventLogActivityProjection => {
EventLogActivityProjection::known_export_formats()
}
RegistryItemKind::IndexLinkedOCEL => IndexLinkedOCEL::known_export_formats(),
RegistryItemKind::EventLog => EventLog::known_export_formats(),
RegistryItemKind::OCEL => OCEL::known_export_formats(),
RegistryItemKind::SlimLinkedOCEL => OCEL::known_export_formats(),
RegistryItemKind::TabularSource => Vec::new(),
RegistryItemKind::Custom(name) => custom_kind(name)
.map(|c| (c.export_formats)())
.unwrap_or_default(),
}
}
}
impl Serialize for RegistryItemKind {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.name())
}
}
impl<'de> Deserialize<'de> for RegistryItemKind {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
impl std::str::FromStr for RegistryItemKind {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"EventLogActivityProjection" => Ok(RegistryItemKind::EventLogActivityProjection),
"IndexLinkedOCEL" => Ok(RegistryItemKind::IndexLinkedOCEL),
"EventLog" => Ok(RegistryItemKind::EventLog),
"OCEL" => Ok(RegistryItemKind::OCEL),
"SlimLinkedOCEL" => Ok(RegistryItemKind::SlimLinkedOCEL),
"TabularSource" => Ok(RegistryItemKind::TabularSource),
_ => custom_kind(s)
.map(|c| RegistryItemKind::Custom(c.name))
.ok_or_else(|| format!("Unknown RegistryItemKind: {}", s)),
}
}
}
use crate::core::io::{Exportable, Importable};
use crate::core::tabular_source::TabularSource;
impl RegistryItem {
pub fn custom(value: impl CustomRegistryValue) -> Self {
RegistryItem::Custom(Box::new(value))
}
pub fn as_custom<T: CustomRegistryValue>(&self) -> Option<&T> {
match self {
RegistryItem::Custom(v) => (&**v as &dyn std::any::Any).downcast_ref::<T>(),
_ => None,
}
}
pub fn as_custom_mut<T: CustomRegistryValue>(&mut self) -> Option<&mut T> {
match self {
RegistryItem::Custom(v) => (&mut **v as &mut dyn std::any::Any).downcast_mut::<T>(),
_ => None,
}
}
pub fn to_value(&self) -> Result<Value, String> {
match self {
RegistryItem::EventLog(log) => serde_json::to_value(log).map_err(|e| e.to_string()),
RegistryItem::OCEL(ocel) => serde_json::to_value(ocel).map_err(|e| e.to_string()),
RegistryItem::IndexLinkedOCEL(locel) => {
serde_json::to_value(locel).map_err(|e| e.to_string())
}
RegistryItem::SlimLinkedOCEL(locel) => {
let ocel = locel.construct_ocel();
serde_json::to_value(ocel).map_err(|e| e.to_string())
}
RegistryItem::EventLogActivityProjection(proj) => {
serde_json::to_value(proj).map_err(|e| e.to_string())
}
RegistryItem::TabularSource(src) => Ok(serde_json::json!({
"format": src.format(),
"bytes": src.bytes().len(),
})),
RegistryItem::Custom(v) => v.to_value(),
}
}
pub fn load_from_path(item_kind: &RegistryItemKind, path: &str) -> Result<Self, String> {
let path = std::path::Path::new(path);
#[cfg(feature = "extraction-blueprint")]
if crate::core::io::infer_format_from_path(path)
.is_some_and(|format| reads_as_case_centric(item_kind, &format))
{
let log = EventLog::import_from_path(path).map_err(|e| e.to_string())?;
return case_centric_as(item_kind, &log);
}
match item_kind {
RegistryItemKind::EventLog => Ok(RegistryItem::EventLog(
EventLog::import_from_path(path).map_err(|e| e.to_string())?,
)),
RegistryItemKind::OCEL => Ok(RegistryItem::OCEL(
OCEL::import_from_path(path).map_err(|e| e.to_string())?,
)),
RegistryItemKind::SlimLinkedOCEL => Ok(RegistryItem::SlimLinkedOCEL({
SlimLinkedOCEL::import_from_path(path).map_err(|e| e.to_string())?
})),
RegistryItemKind::IndexLinkedOCEL => Ok(RegistryItem::IndexLinkedOCEL(
IndexLinkedOCEL::import_from_path(path).map_err(|e| e.to_string())?,
)),
RegistryItemKind::EventLogActivityProjection => {
Ok(RegistryItem::EventLogActivityProjection(
EventLogActivityProjection::import_from_path(path)
.map_err(|e| e.to_string())?,
))
}
RegistryItemKind::TabularSource => Ok(RegistryItem::TabularSource(
TabularSource::import_from_path(path).map_err(|e| e.to_string())?,
)),
RegistryItemKind::Custom(name) => {
let info = custom_kind(name).ok_or_else(|| unregistered_kind_msg(name))?;
(info.from_path)(path)
}
}
}
pub fn load_from_bytes(
item_kind: &RegistryItemKind,
data: &[u8],
format: &str,
) -> Result<Self, String> {
#[cfg(feature = "extraction-blueprint")]
if reads_as_case_centric(item_kind, format) {
let log = EventLog::import_from_bytes(data, format).map_err(|e| e.to_string())?;
return case_centric_as(item_kind, &log);
}
match item_kind {
RegistryItemKind::EventLog => Ok(RegistryItem::EventLog(
EventLog::import_from_bytes(data, format).map_err(|e| e.to_string())?,
)),
RegistryItemKind::OCEL => Ok(RegistryItem::OCEL(
OCEL::import_from_bytes(data, format).map_err(|e| e.to_string())?,
)),
RegistryItemKind::IndexLinkedOCEL => Ok(RegistryItem::IndexLinkedOCEL(
IndexLinkedOCEL::import_from_bytes(data, format).map_err(|e| e.to_string())?,
)),
RegistryItemKind::SlimLinkedOCEL => Ok(RegistryItem::SlimLinkedOCEL({
OCEL::import_from_bytes(data, format)
.map(SlimLinkedOCEL::from_ocel)
.map_err(|e| e.to_string())?
})),
RegistryItemKind::EventLogActivityProjection => {
Ok(RegistryItem::EventLogActivityProjection(
EventLogActivityProjection::import_from_bytes(data, format)
.map_err(|e| e.to_string())?,
))
}
RegistryItemKind::TabularSource => Ok(RegistryItem::TabularSource(
TabularSource::import_from_bytes(data, format).map_err(|e| e.to_string())?,
)),
RegistryItemKind::Custom(name) => {
let info = custom_kind(name).ok_or_else(|| unregistered_kind_msg(name))?;
(info.from_bytes)(data, format)
}
}
}
pub fn from_json_value(item_kind: &RegistryItemKind, value: &Value) -> Result<Self, String> {
match item_kind {
RegistryItemKind::EventLog => serde_json::from_value(value.clone())
.map(RegistryItem::EventLog)
.map_err(|e| e.to_string()),
RegistryItemKind::OCEL => serde_json::from_value(value.clone())
.map(RegistryItem::OCEL)
.map_err(|e| e.to_string()),
RegistryItemKind::IndexLinkedOCEL => serde_json::from_value(value.clone())
.map(RegistryItem::IndexLinkedOCEL)
.map_err(|e| e.to_string()),
RegistryItemKind::EventLogActivityProjection => serde_json::from_value(value.clone())
.map(RegistryItem::EventLogActivityProjection)
.map_err(|e| e.to_string()),
RegistryItemKind::SlimLinkedOCEL => serde_json::from_value::<OCEL>(value.clone())
.map(|ocel| RegistryItem::SlimLinkedOCEL(SlimLinkedOCEL::from_ocel(ocel)))
.map_err(|e| e.to_string()),
RegistryItemKind::TabularSource => Err("a data source has no JSON form".to_string()),
RegistryItemKind::Custom(name) => {
let info = custom_kind(name).ok_or_else(|| unregistered_kind_msg(name))?;
(info.from_value)(value)
}
}
}
pub fn kind(&self) -> RegistryItemKind {
match self {
RegistryItem::EventLogActivityProjection(_) => {
RegistryItemKind::EventLogActivityProjection
}
RegistryItem::IndexLinkedOCEL(_) => RegistryItemKind::IndexLinkedOCEL,
RegistryItem::EventLog(_) => RegistryItemKind::EventLog,
RegistryItem::OCEL(_) => RegistryItemKind::OCEL,
RegistryItem::SlimLinkedOCEL(_) => RegistryItemKind::SlimLinkedOCEL,
RegistryItem::TabularSource(_) => RegistryItemKind::TabularSource,
RegistryItem::Custom(v) => RegistryItemKind::Custom(v.kind()),
}
}
pub fn export_to_path(&self, path: impl AsRef<std::path::Path>) -> Result<(), String> {
let path = path.as_ref();
let inferred = match self {
RegistryItem::EventLog(_) => <EventLog as Exportable>::infer_format(path),
RegistryItem::OCEL(_) => <OCEL as Exportable>::infer_format(path),
RegistryItem::IndexLinkedOCEL(_) => <IndexLinkedOCEL as Exportable>::infer_format(path),
RegistryItem::SlimLinkedOCEL(_) => <SlimLinkedOCEL as Exportable>::infer_format(path),
RegistryItem::EventLogActivityProjection(_) => {
<EventLogActivityProjection as Exportable>::infer_format(path)
}
RegistryItem::TabularSource(_) | RegistryItem::Custom(_) => {
crate::core::io::infer_format_from_path(path)
}
};
let format = inferred
.ok_or_else(|| format!("Cannot infer format from path {}", path.to_string_lossy()))?;
self.export_to_path_as(path, &format)
}
pub fn export_to_path_as(
&self,
path: impl AsRef<std::path::Path>,
format: &str,
) -> Result<(), String> {
let path = path.as_ref();
match self {
RegistryItem::EventLog(x) => x
.export_to_path_as(path, format, ())
.map_err(|e| e.to_string()),
RegistryItem::OCEL(x) => x
.export_to_path_as(path, format, ())
.map_err(|e| e.to_string()),
RegistryItem::IndexLinkedOCEL(x) => x
.export_to_path_as(path, format, ())
.map_err(|e| e.to_string()),
RegistryItem::SlimLinkedOCEL(x) => x
.export_to_path_as(path, format, ())
.map_err(|e| e.to_string()),
RegistryItem::EventLogActivityProjection(x) => x
.export_to_path_as(path, format, ())
.map_err(|e| e.to_string()),
RegistryItem::TabularSource(_) => Err("a data source cannot be exported".to_string()),
RegistryItem::Custom(v) => {
std::fs::write(path, v.export_to_bytes(format)?).map_err(|e| e.to_string())
}
}
}
pub fn export_to_bytes(&self, format: &str) -> Result<Vec<u8>, String> {
let mut bytes = Vec::new();
match self {
RegistryItem::EventLog(x) => x
.export_to_writer(&mut bytes, format)
.map_err(|e| e.to_string())?,
RegistryItem::OCEL(x) => x
.export_to_writer(&mut bytes, format)
.map_err(|e| e.to_string())?,
RegistryItem::SlimLinkedOCEL(x) => x
.construct_ocel()
.export_to_writer(&mut bytes, format)
.map_err(|e| e.to_string())?,
RegistryItem::IndexLinkedOCEL(x) => x
.export_to_writer(&mut bytes, format)
.map_err(|e| e.to_string())?,
RegistryItem::EventLogActivityProjection(x) => x
.export_to_writer(&mut bytes, format)
.map_err(|e| e.to_string())?,
RegistryItem::TabularSource(_) => {
return Err("a data source cannot be exported".to_string())
}
RegistryItem::Custom(x) => return x.export_to_bytes(format),
};
Ok(bytes)
}
pub fn convert(&self, target_kind: RegistryItemKind) -> Result<Self, String> {
match (self, target_kind) {
(RegistryItem::EventLog(log), RegistryItemKind::EventLogActivityProjection) => {
Ok(RegistryItem::EventLogActivityProjection(log.into()))
}
(RegistryItem::OCEL(ocel), RegistryItemKind::IndexLinkedOCEL) => Ok(
RegistryItem::IndexLinkedOCEL(IndexLinkedOCEL::from_ocel(ocel.clone())),
),
(RegistryItem::IndexLinkedOCEL(locel), RegistryItemKind::OCEL) => {
Ok(RegistryItem::OCEL(locel.get_ocel_ref().clone()))
}
(RegistryItem::SlimLinkedOCEL(locel), RegistryItemKind::OCEL) => {
Ok(RegistryItem::OCEL(locel.construct_ocel()))
}
(RegistryItem::OCEL(ocel), RegistryItemKind::SlimLinkedOCEL) => Ok(
RegistryItem::SlimLinkedOCEL(SlimLinkedOCEL::from_ocel(ocel.clone())),
),
_ => Err(format!("Cannot convert {} to {}", self.kind(), target_kind)),
}
}
}
pub trait CustomRegistryKind {
fn kind(&self) -> &'static str;
}
impl<T: CustomRegistryValue> CustomRegistryKind for T {
fn kind(&self) -> &'static str {
T::kind_name()
}
}
pub trait CustomRegistryValue:
CustomRegistryKind + std::any::Any + Send + Sync + std::fmt::Debug
{
fn kind_name() -> &'static str
where
Self: Sized;
fn to_value(&self) -> Result<Value, String>;
fn from_value(_value: &Value) -> Result<Self, String>
where
Self: Sized,
{
Err(format!("{} cannot be read from JSON", Self::kind_name()))
}
fn from_bytes(_bytes: &[u8], format: &str) -> Result<Self, String>
where
Self: Sized,
{
Err(format!(
"{} cannot be read from '{}' bytes",
Self::kind_name(),
format
))
}
fn from_path(path: &std::path::Path) -> Result<Self, String>
where
Self: Sized,
{
let format = crate::core::io::infer_format_from_path(path)
.ok_or_else(|| format!("Cannot infer format from path {}", path.to_string_lossy()))?;
let bytes = std::fs::read(path).map_err(|e| e.to_string())?;
Self::from_bytes(&bytes, &format)
}
fn export_to_bytes(&self, format: &str) -> Result<Vec<u8>, String> {
Err(format!(
"{} cannot be exported as '{}'",
self.kind(),
format
))
}
fn known_import_formats() -> Vec<ExtensionWithMime>
where
Self: Sized,
{
Vec::new()
}
fn known_export_formats() -> Vec<ExtensionWithMime>
where
Self: Sized,
{
Vec::new()
}
}
#[derive(Debug)]
pub struct CustomKindInfo {
pub name: &'static str,
pub from_path: fn(&std::path::Path) -> Result<RegistryItem, String>,
pub from_bytes: fn(&[u8], &str) -> Result<RegistryItem, String>,
pub from_value: fn(&Value) -> Result<RegistryItem, String>,
pub import_formats: fn() -> Vec<ExtensionWithMime>,
pub export_formats: fn() -> Vec<ExtensionWithMime>,
}
inventory::collect!(CustomKindInfo);
pub fn custom_kind(name: &str) -> Option<&'static CustomKindInfo> {
inventory::iter::<CustomKindInfo>
.into_iter()
.find(|c| c.name == name)
}
pub fn custom_kinds() -> Vec<&'static CustomKindInfo> {
inventory::iter::<CustomKindInfo>.into_iter().collect()
}
fn unregistered_kind_msg(name: &str) -> String {
format!(
"'{}' is not a registered custom kind (see register_custom_registry_kind!)",
name
)
}
#[doc(hidden)]
pub fn __custom_from_path<T: CustomRegistryValue>(
path: &std::path::Path,
) -> Result<RegistryItem, String> {
T::from_path(path).map(RegistryItem::custom)
}
#[doc(hidden)]
pub fn __custom_from_bytes<T: CustomRegistryValue>(
bytes: &[u8],
format: &str,
) -> Result<RegistryItem, String> {
T::from_bytes(bytes, format).map(RegistryItem::custom)
}
#[doc(hidden)]
pub fn __custom_from_value<T: CustomRegistryValue>(value: &Value) -> Result<RegistryItem, String> {
T::from_value(value).map(RegistryItem::custom)
}
#[doc(hidden)]
pub fn __custom_import_formats<T: CustomRegistryValue>() -> Vec<ExtensionWithMime> {
T::known_import_formats()
}
#[doc(hidden)]
pub fn __custom_export_formats<T: CustomRegistryValue>() -> Vec<ExtensionWithMime> {
T::known_export_formats()
}
#[macro_export]
macro_rules! register_custom_registry_kind {
($t:ty) => {
$crate::register_custom_registry_kind!($t, ::core::stringify!($t));
};
($t:ty, $name:expr) => {
$crate::__private::inventory::submit! {
$crate::bindings::CustomKindInfo {
name: $name,
from_path: $crate::bindings::__custom_from_path::<$t>,
from_bytes: $crate::bindings::__custom_from_bytes::<$t>,
from_value: $crate::bindings::__custom_from_value::<$t>,
import_formats: $crate::bindings::__custom_import_formats::<$t>,
export_formats: $crate::bindings::__custom_export_formats::<$t>,
}
}
};
}
pub type InnerAppState = HashMap<String, RegistryItem>;
#[derive(Debug, Clone, Copy)]
pub struct StateRef<'a> {
items: &'a InnerAppState,
}
impl<'a> StateRef<'a> {
#[must_use]
pub fn new(items: &'a InnerAppState) -> Self {
Self { items }
}
#[must_use]
pub fn get(&self, id: &str) -> Option<&'a RegistryItem> {
self.items.get(id)
}
#[must_use]
pub fn contains(&self, id: &str) -> bool {
self.items.contains_key(id)
}
}
#[derive(Debug)]
pub struct StateRefMut<'a> {
items: &'a mut InnerAppState,
}
impl<'a> StateRefMut<'a> {
#[must_use]
pub fn new(items: &'a mut InnerAppState) -> Self {
Self { items }
}
#[must_use]
pub fn get(&self, id: &str) -> Option<&RegistryItem> {
self.items.get(id)
}
#[must_use]
pub fn get_mut(&mut self, id: &str) -> Option<&mut RegistryItem> {
self.items.get_mut(id)
}
#[must_use]
pub fn contains(&self, id: &str) -> bool {
self.items.contains_key(id)
}
pub fn remove(&mut self, id: &str) -> Option<RegistryItem> {
self.items.remove(id)
}
pub fn clear(&mut self) {
self.items.clear();
}
#[must_use]
pub fn len(&self) -> usize {
self.items.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn ids(&self) -> impl Iterator<Item = &String> {
self.items.keys()
}
}
#[derive(Debug, Default)]
pub struct AppState {
pub items: RwLock<InnerAppState>,
}
impl AppState {
pub fn read(&self) -> std::sync::RwLockReadGuard<'_, InnerAppState> {
self.items
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub fn write(&self) -> std::sync::RwLockWriteGuard<'_, InnerAppState> {
self.items
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub fn add(&self, id: impl Into<String>, item: impl Into<RegistryItem>) {
self.write().insert(id.into(), item.into());
}
pub fn remove(&self, id: &str) -> Option<RegistryItem> {
self.write().remove(id)
}
pub fn contains_key(&self, id: &str) -> bool {
self.read().contains_key(id)
}
}
#[derive(Debug)]
pub struct Binding {
pub id: &'static str,
pub name: &'static str,
pub handler: fn(&Value, &AppState) -> Result<Vec<u8>, String>,
pub docs: fn() -> Vec<String>,
pub module: &'static str,
pub source_path: &'static str,
pub source_line: u32,
pub args: fn() -> Vec<(String, Value)>,
pub required_args: fn() -> Vec<String>,
pub return_type: fn() -> Value,
}
inventory::collect!(Binding);
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct BindingMeta {
pub id: String,
pub name: String,
pub docs: Vec<String>,
pub module: String,
pub source_path: String,
pub source_line: u32,
pub args: Vec<(String, Value)>,
pub required_args: Vec<String>,
pub return_type: Value,
}
impl From<&Binding> for BindingMeta {
fn from(value: &Binding) -> Self {
Self {
id: value.id.to_string(),
name: value.name.to_string(),
docs: (value.docs)(),
module: value.module.to_string(),
source_path: value.source_path.to_string(),
source_line: value.source_line,
args: (value.args)(),
required_args: (value.required_args)(),
return_type: (value.return_type)(),
}
}
}
pub trait FromContext<'a>: Sized {
fn from_context(v: &Value, s: &'a InnerAppState) -> Result<Self, String>;
}
pub fn extract_param<'a, T: FromContext<'a>>(
m: &serde_json::Map<String, Value>,
k: &str,
s: &'a InnerAppState,
default: impl FnOnce() -> Option<T>,
) -> Result<T, String> {
if let Some(x) = m.get(k) {
if x.is_null() {
let d = default();
if let Some(d) = d {
return Ok(d);
}
}
T::from_context(x, s).map_err(|e| format!("Invalid Argument: {k}\n{e}"))
} else {
let r = default();
r.ok_or_else(|| format!("Missing required argument {k}"))
}
}
pub fn extract_param_json<T: serde::de::DeserializeOwned>(
m: &serde_json::Map<String, Value>,
k: &str,
default: impl FnOnce() -> Option<T>,
) -> Result<T, String> {
if let Some(x) = m.get(k) {
if x.is_null() {
if let Some(d) = default() {
return Ok(d);
}
}
serde_json::from_value(x.clone()).map_err(|e| format!("Invalid Argument: {k}\n{e}"))
} else {
default().ok_or_else(|| format!("Missing required argument {k}"))
}
}
impl<'a, T> FromContext<'a> for T
where
T: serde::de::DeserializeOwned,
{
fn from_context(v: &Value, _: &'a InnerAppState) -> Result<Self, String> {
serde_json::from_value(v.clone()).map_err(|e| e.to_string())
}
}
enum HandleArg<'a> {
Bare(&'a str),
Id(&'a str),
Path(&'a str),
Bytes {
b64: &'a str,
format: &'a str,
},
Inline(&'a Value),
}
fn classify_handle_arg(value: &Value) -> Option<HandleArg<'_>> {
match value {
Value::String(s) => Some(HandleArg::Bare(s)),
Value::Object(map) => {
let string_field = |k: &str| map.get(k).and_then(Value::as_str);
match map.len() {
1 => {
if let Some(id) = string_field("id") {
Some(HandleArg::Id(id))
} else if let Some(path) = string_field("path") {
Some(HandleArg::Path(path))
} else if let Some(inner) = map.get("inline") {
Some(HandleArg::Inline(inner))
} else {
Some(HandleArg::Inline(value))
}
}
2 => match (string_field("bytes"), string_field("format")) {
(Some(b64), Some(format)) => Some(HandleArg::Bytes { b64, format }),
_ => Some(HandleArg::Inline(value)),
},
_ => Some(HandleArg::Inline(value)),
}
}
Value::Array(_) => Some(HandleArg::Inline(value)),
_ => None,
}
}
fn decode_base64(s: &str) -> Result<Vec<u8>, String> {
use base64::Engine;
const ENGINE: base64::engine::GeneralPurpose = base64::engine::GeneralPurpose::new(
&base64::alphabet::STANDARD,
base64::engine::GeneralPurposeConfig::new()
.with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent),
);
ENGINE.decode(s).map_err(|e| e.to_string())
}
fn resolve_stored_id(id: &str, arg_ref: &str, state: &AppState) -> Result<Option<Value>, String> {
{
let items = state.read();
match items.get(id) {
None => return Ok(None),
Some(item) if item.kind().to_string() == arg_ref => {
return Ok(Some(Value::String(id.to_string())))
}
Some(_) => {}
}
}
let mut items = state.write();
let Some(item) = items.get(id) else {
return Ok(None);
};
if item.kind().to_string() == arg_ref {
return Ok(Some(Value::String(id.to_string())));
}
let target_kind = RegistryItemKind::from_str(arg_ref)?;
match item.convert(target_kind) {
Ok(converted) => {
let new_id = format!("{}_as_{}", id, arg_ref);
items.insert(new_id.clone(), converted);
Ok(Some(Value::String(new_id)))
}
Err(e) => Err(format!(
"Type mismatch for ID '{}': expected {}, found {}. Conversion failed: {}",
id,
arg_ref,
item.kind(),
e
)),
}
}
pub fn resolve_argument(
arg_name: &str,
value: Value,
schema: &Value,
state: &AppState,
) -> Result<Value, String> {
resolve_argument_tracked(arg_name, value, schema, state, &mut Vec::new())
}
fn resolve_argument_tracked(
arg_name: &str,
value: Value,
schema: &Value,
state: &AppState,
minted: &mut Vec<String>,
) -> Result<Value, String> {
let schema_obj = schema.as_object().ok_or("Invalid schema")?;
if let Some(arg_ref) = schema_obj.get("x-registry-ref").and_then(|r| r.as_str()) {
if let Some(handle_arg) = classify_handle_arg(&value) {
let invalid = |e: String| format!("Invalid Argument: {}\n{}", arg_name, e);
let target_kind = || RegistryItemKind::from_str(arg_ref).map_err(invalid);
let item = match handle_arg {
HandleArg::Bare(id) => {
if let Some(resolved) = resolve_stored_id(id, arg_ref, state)? {
return Ok(resolved);
}
RegistryItem::load_from_path(&RegistryItemKind::from_str(arg_ref)?, id)?
}
HandleArg::Id(id) => {
return resolve_stored_id(id, arg_ref, state)?.ok_or_else(|| {
invalid(format!("No {} is stored under the ID '{}'", arg_ref, id))
})
}
HandleArg::Path(path) => {
RegistryItem::load_from_path(&target_kind()?, path).map_err(invalid)?
}
HandleArg::Bytes { b64, format } => {
let bytes = decode_base64(b64)
.map_err(|e| invalid(format!("'bytes' is not valid base64: {}", e)))?;
RegistryItem::load_from_bytes(&target_kind()?, &bytes, format)
.map_err(invalid)?
}
HandleArg::Inline(inner) => {
RegistryItem::from_json_value(&target_kind()?, inner).map_err(invalid)?
}
};
let stored_name = format!("A{}_{}", arg_name, uuid::Uuid::new_v4());
state.add(&stored_name, item);
minted.push(stored_name.clone());
return Ok(serde_json::Value::String(stored_name));
}
}
if let Some(val_str) = value.as_str() {
if schema_obj.get("type") == Some(&serde_json::json!("object"))
&& val_str.ends_with(".json")
{
let file = std::fs::File::open(val_str)
.map_err(|e| format!("Failed to open JSON file: {}", e))?;
let reader = std::io::BufReader::new(file);
let loaded_val: Value = serde_json::from_reader(reader)
.map_err(|e| format!("Failed to parse JSON file: {}", e))?;
return Ok(loaded_val);
}
}
if let Some(val_str) = value.as_str() {
let type_field = schema_obj.get("type").and_then(|t| t.as_str());
if matches!(type_field, Some("object") | Some("array")) {
if let Ok(parsed) = serde_json::from_str::<Value>(val_str) {
return Ok(parsed);
}
}
}
Ok(value)
}
pub fn call(binding: &Binding, args: &Value, state: &AppState) -> Result<Vec<u8>, String> {
let called = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
(binding.handler)(args, state)
}));
called.unwrap_or_else(|payload| {
let what = payload
.downcast_ref::<&str>()
.map(|s| (*s).to_string())
.or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "panicked".to_string());
Err(format!("{} panicked: {}", binding.name, what))
})
}
pub fn call_resolved(binding: &Binding, args: &Value, state: &AppState) -> Result<Vec<u8>, String> {
let Some(passed) = args.as_object() else {
return call(binding, args, state);
};
let schemas = (binding.args)();
let mut resolved = serde_json::Map::with_capacity(passed.len());
let mut minted: Vec<String> = Vec::new();
for (name, value) in passed {
let value = match schemas.iter().find(|(n, _)| n == name) {
Some((_, schema)) => {
match resolve_argument_tracked(name, value.clone(), schema, state, &mut minted) {
Ok(value) => value,
Err(e) => {
drop_minted(state, &minted);
return Err(e);
}
}
}
None => value.clone(),
};
resolved.insert(name.clone(), value);
}
let result = call(binding, &Value::Object(resolved), state);
drop_minted(state, &minted);
result
}
fn drop_minted(state: &AppState, minted: &[String]) {
if minted.is_empty() {
return;
}
let mut items = state.write();
for id in minted {
items.remove(id);
}
}
pub fn list_functions() -> Vec<&'static Binding> {
inventory::iter::<Binding>.into_iter().collect()
}
pub fn list_functions_meta() -> Vec<BindingMeta> {
inventory::iter::<Binding>
.into_iter()
.map(BindingMeta::from)
.collect()
}
pub fn get_fn_binding(id: &str) -> Option<&'static Binding> {
inventory::iter::<Binding>.into_iter().find(|b| b.id == id)
}
#[cfg(feature = "extraction-blueprint")]
mod extraction_bindings;
#[cfg(feature = "extraction-dbcon")]
mod extraction_dbcon_bindings;
mod path_schema_bindings;
mod slim_ocel_bindings;
#[register_binding]
pub fn num_objects<'a>(ocel: &'a impl LinkedOCELAccess<'a>) -> usize {
ocel.get_num_obs()
}
#[register_binding]
pub fn num_events<'a>(ocel: &'a impl LinkedOCELAccess<'a>) -> usize {
ocel.get_num_evs()
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct OCELTypeStats {
pub event_type_counts: HashMap<String, usize>,
pub object_type_counts: HashMap<String, usize>,
}
#[register_binding]
pub fn ocel_type_stats<'a>(ocel: &'a impl LinkedOCELAccess<'a>) -> OCELTypeStats {
OCELTypeStats {
event_type_counts: ocel
.get_ev_types()
.map(|et| (et.to_string(), ocel.get_evs_of_type(et).count()))
.collect(),
object_type_counts: ocel
.get_ob_types()
.map(|ot| (ot.to_string(), ocel.get_obs_of_type(ot).count()))
.collect(),
}
}
#[register_binding]
pub fn index_link_ocel(ocel: &OCEL) -> IndexLinkedOCEL {
IndexLinkedOCEL::from_ocel(ocel.clone())
}
#[register_binding]
pub fn slim_link_ocel(ocel: &OCEL) -> SlimLinkedOCEL {
SlimLinkedOCEL::from_ocel(ocel.clone())
}
#[register_binding]
pub fn test_some_inputs(s: String, n: usize, i: i32, f: f64, b: bool) -> String {
format!("s={},n={},i={},f={},b={}", s, n, i, f, b)
}
#[cfg(test)]
mod tests {
use crate::test_utils::get_test_data_path;
use super::*;
use std::collections::HashSet;
#[test]
fn export_bindings() {
let bindings = list_functions_meta();
let file = std::fs::File::create(
get_test_data_path()
.join("export")
.join(format!("bindings-v{}.json", env!("CARGO_PKG_VERSION"))),
)
.unwrap();
serde_json::to_writer_pretty(&file, &bindings).unwrap();
}
#[test]
fn test_consistent_registry_item_variants() {
let variants = RegistryItemKind::all_kinds();
let variant_names: HashSet<String> = variants.iter().map(|v| v.to_string()).collect();
let macro_types: &[&str] = macros_process_mining::big_types_list!();
let macro_type_names: HashSet<String> = macro_types.iter().map(|s| s.to_string()).collect();
for macro_type in ¯o_type_names {
assert!(
variant_names.contains(macro_type),
"Macro expects type '{}' which is missing in RegistryItem enum",
macro_type
);
}
for variant in &variant_names {
assert!(
macro_type_names.contains(variant),
"RegistryItem has variant '{}' which is missing in macros_process_mining::BIG_TYPES_NAMES",
variant
);
}
assert_eq!(
variant_names.len(),
macro_type_names.len(),
"Mismatch in number of types between RegistryItem and macros_process_mining"
);
}
#[derive(Debug, Clone, PartialEq, CustomRegistryEntity)]
struct DummyHandle {
label: String,
hits: usize,
}
impl CustomRegistryValue for DummyHandle {
fn kind_name() -> &'static str {
"DummyHandle"
}
fn to_value(&self) -> Result<Value, String> {
Ok(serde_json::json!({ "label": self.label, "hits": self.hits }))
}
fn from_value(value: &Value) -> Result<Self, String> {
Ok(DummyHandle {
label: value["label"]
.as_str()
.ok_or("DummyHandle needs a string 'label'")?
.to_string(),
hits: value["hits"].as_u64().unwrap_or(0) as usize,
})
}
fn from_bytes(bytes: &[u8], format: &str) -> Result<Self, String> {
if format != "txt" {
return Err(format!("DummyHandle cannot be read from '{}'", format));
}
Ok(DummyHandle {
label: String::from_utf8_lossy(bytes).to_string(),
hits: 0,
})
}
fn export_to_bytes(&self, format: &str) -> Result<Vec<u8>, String> {
if format != "txt" {
return Err(format!("DummyHandle cannot be written as '{}'", format));
}
Ok(self.label.as_bytes().to_vec())
}
fn known_import_formats() -> Vec<ExtensionWithMime> {
vec![ExtensionWithMime::new("txt", "text/plain")]
}
fn known_export_formats() -> Vec<ExtensionWithMime> {
vec![ExtensionWithMime::new("txt", "text/plain")]
}
}
crate::register_custom_registry_kind!(DummyHandle);
#[register_binding]
fn dummy_label(#[bind(handle)] h: &DummyHandle) -> String {
h.label.clone()
}
#[register_binding]
fn dummy_bump(#[bind(handle)] h: &mut DummyHandle) -> usize {
h.hits += 1;
h.hits
}
#[register_binding(returns_handle)]
fn dummy_new(label: String) -> DummyHandle {
DummyHandle { label, hits: 0 }
}
#[register_binding(returns_handle)]
fn dummy_fork(#[bind(handle)] h: &mut DummyHandle) -> DummyHandle {
h.hits += 1;
h.clone()
}
#[register_binding]
fn dummy_log_of(#[bind(handle)] h: &mut DummyHandle) -> EventLog {
h.hits += 1;
EventLog::default()
}
#[register_binding]
fn dummy_clear_all(#[bind(state_mut)] mut state: StateRefMut<'_>) -> usize {
let n = state.len();
state.clear();
n
}
fn binding_named(name: &str) -> &'static Binding {
list_functions()
.into_iter()
.find(|b| b.name == name)
.unwrap_or_else(|| panic!("no binding named {}", name))
}
#[test]
fn custom_registry_kind_is_a_string_everywhere() {
let custom = RegistryItemKind::Custom("DummyHandle");
assert_eq!(custom.to_string(), "DummyHandle");
assert_eq!("DummyHandle".parse::<RegistryItemKind>().unwrap(), custom);
assert_eq!(
serde_json::to_value(custom).unwrap(),
serde_json::json!("DummyHandle")
);
assert_eq!(
serde_json::from_value::<RegistryItemKind>(serde_json::json!("DummyHandle")).unwrap(),
custom
);
assert_eq!(
serde_json::to_value(RegistryItemKind::OCEL).unwrap(),
serde_json::json!("OCEL")
);
assert!("NoSuchKind".parse::<RegistryItemKind>().is_err());
assert!(RegistryItemKind::all_registered_kinds().contains(&custom));
assert_eq!(RegistryItemKind::all_kinds().len(), 6);
assert_eq!(custom.known_import_formats().len(), 1);
assert_eq!(custom.known_export_formats().len(), 1);
}
#[test]
fn custom_registry_item_import_and_export() {
let kind: RegistryItemKind = "DummyHandle".parse().unwrap();
let item = RegistryItem::load_from_bytes(&kind, b"from-bytes", "txt").unwrap();
assert_eq!(item.kind(), kind);
assert_eq!(item.as_custom::<DummyHandle>().unwrap().label, "from-bytes");
assert_eq!(item.export_to_bytes("txt").unwrap(), b"from-bytes");
assert!(item.export_to_bytes("xes").is_err());
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("handle.txt");
item.export_to_path(&path).unwrap();
let reloaded = RegistryItem::load_from_path(&kind, path.to_str().unwrap()).unwrap();
assert_eq!(
reloaded.as_custom::<DummyHandle>().unwrap(),
item.as_custom::<DummyHandle>().unwrap()
);
}
#[test]
fn custom_handle_crosses_the_binding_boundary() {
let state = AppState::default();
state.add(
"d1",
RegistryItem::custom(DummyHandle {
label: "one".to_string(),
hits: 0,
}),
);
{
let items = state.items.read().unwrap();
let item = items.get("d1").unwrap();
assert_eq!(item.kind(), RegistryItemKind::Custom("DummyHandle"));
assert_eq!(item.as_custom::<DummyHandle>().unwrap().label, "one");
assert!(RegistryItem::EventLog(EventLog::default())
.as_custom::<DummyHandle>()
.is_none());
assert_eq!(
item.to_value().unwrap(),
serde_json::json!({ "label": "one", "hits": 0 })
);
}
let shared = binding_named("dummy_label");
assert_eq!(
(shared.args)()[0].1["x-registry-ref"],
serde_json::json!("DummyHandle")
);
let out = call(shared, &serde_json::json!({ "h": "d1" }), &state).unwrap();
assert_eq!(out, b"\"one\"");
let bump = binding_named("dummy_bump");
let out = call(bump, &serde_json::json!({ "h": "d1" }), &state).unwrap();
assert_eq!(out, b"1");
let out = call(bump, &serde_json::json!({ "h": "d1" }), &state).unwrap();
assert_eq!(out, b"2");
assert_eq!(
state
.items
.read()
.unwrap()
.get("d1")
.unwrap()
.as_custom::<DummyHandle>()
.unwrap()
.hits,
2
);
let make = binding_named("dummy_new");
assert_eq!(
(make.return_type)()["x-registry-ref"],
serde_json::json!("DummyHandle")
);
let out = call(make, &serde_json::json!({ "label": "two" }), &state).unwrap();
let new_id: String = serde_json::from_slice(&out).unwrap();
let items = state.items.read().unwrap();
let created = items.get(&new_id).unwrap();
assert_eq!(created.kind(), RegistryItemKind::Custom("DummyHandle"));
assert_eq!(
created.as_custom::<DummyHandle>().unwrap(),
&DummyHandle {
label: "two".to_string(),
hits: 0
}
);
assert!(call(shared, &serde_json::json!({ "h": "nope" }), &state).is_err());
}
#[test]
fn state_mut_reaches_every_item_by_id_not_just_one_named_argument() {
let state = AppState::default();
state.add(
"d1",
RegistryItem::custom(DummyHandle {
label: "one".to_string(),
hits: 0,
}),
);
state.add("d2", RegistryItem::EventLog(EventLog::default()));
let clear_all = binding_named("dummy_clear_all");
assert!((clear_all.args)().is_empty());
assert!((clear_all.required_args)().is_empty());
let out = call(clear_all, &serde_json::json!({}), &state).unwrap();
let n: usize = serde_json::from_slice(&out).unwrap();
assert_eq!(n, 2, "both items were counted before being cleared");
assert!(
state.items.read().unwrap().is_empty(),
"state_mut actually reached and cleared items no argument named"
);
}
fn arg_schema(binding: &str, arg: &str) -> Value {
(binding_named(binding).args)()
.into_iter()
.find(|(n, _)| n == arg)
.unwrap_or_else(|| panic!("{} has no argument {}", binding, arg))
.1
}
fn base64_of(bytes: &[u8]) -> String {
use base64::Engine;
base64::engine::general_purpose::STANDARD.encode(bytes)
}
fn stored_handle(state: &AppState, resolved: &Value) -> DummyHandle {
state
.items
.read()
.unwrap()
.get(resolved.as_str().unwrap())
.unwrap()
.as_custom::<DummyHandle>()
.unwrap()
.clone()
}
#[test]
fn handle_argument_accepts_every_documented_form() {
let schema = arg_schema("dummy_label", "h");
let state = AppState::default();
state.add(
"d1",
RegistryItem::custom(DummyHandle {
label: "stored".to_string(),
hits: 0,
}),
);
assert_eq!(
resolve_argument("h", serde_json::json!("d1"), &schema, &state).unwrap(),
serde_json::json!("d1")
);
assert_eq!(
resolve_argument("h", serde_json::json!({ "id": "d1" }), &schema, &state).unwrap(),
serde_json::json!("d1")
);
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("handle.txt");
std::fs::write(&path, b"from-path").unwrap();
let resolved = resolve_argument(
"h",
serde_json::json!({ "path": path.to_str().unwrap() }),
&schema,
&state,
)
.unwrap();
assert_eq!(stored_handle(&state, &resolved).label, "from-path");
let resolved = resolve_argument(
"h",
serde_json::json!({ "bytes": base64_of(b"from-bytes"), "format": "txt" }),
&schema,
&state,
)
.unwrap();
assert_eq!(stored_handle(&state, &resolved).label, "from-bytes");
let resolved = resolve_argument(
"h",
serde_json::json!({ "bytes": base64_of(b"abcde").trim_end_matches('='), "format": "txt" }),
&schema,
&state,
)
.unwrap();
assert_eq!(stored_handle(&state, &resolved).label, "abcde");
let resolved = resolve_argument(
"h",
serde_json::json!({ "inline": { "label": "wrapped", "hits": 3 } }),
&schema,
&state,
)
.unwrap();
assert_eq!(
stored_handle(&state, &resolved),
DummyHandle {
label: "wrapped".to_string(),
hits: 3
}
);
let resolved = resolve_argument(
"h",
serde_json::json!({ "label": "bare", "hits": 7 }),
&schema,
&state,
)
.unwrap();
assert_eq!(
stored_handle(&state, &resolved),
DummyHandle {
label: "bare".to_string(),
hits: 7
}
);
assert_eq!(
resolve_argument("h", Value::Null, &schema, &state).unwrap(),
Value::Null
);
let err = resolve_argument(
"h",
serde_json::json!({ "bytes": "not base64!!", "format": "txt" }),
&schema,
&state,
)
.unwrap_err();
assert!(err.starts_with("Invalid Argument: h\n"), "{}", err);
assert!(err.contains("base64"), "{}", err);
let err = resolve_argument(
"h",
serde_json::json!({ "bytes": base64_of(b"x"), "format": "xes" }),
&schema,
&state,
)
.unwrap_err();
assert!(err.starts_with("Invalid Argument: h\n"), "{}", err);
let err = resolve_argument("h", serde_json::json!({ "id": "nope" }), &schema, &state)
.unwrap_err();
assert!(err.contains("nope"), "{}", err);
let err =
resolve_argument("h", serde_json::json!({ "hits": 1 }), &schema, &state).unwrap_err();
assert!(err.starts_with("Invalid Argument: h\n"), "{}", err);
assert!(
resolve_argument("h", serde_json::json!("no/such/file.txt"), &schema, &state).is_err()
);
}
fn tiny_ocel_json() -> Value {
serde_json::json!({
"eventTypes": [],
"objectTypes": [{ "name": "item", "attributes": [] }],
"events": [],
"objects": [
{ "id": "i1", "type": "item" },
{ "id": "i2", "type": "item" }
]
})
}
#[test]
fn built_in_handle_argument_forms_convert() {
let schema = arg_schema("num_objects", "ocel");
assert_eq!(
schema["x-registry-ref"],
serde_json::json!("SlimLinkedOCEL")
);
let state = AppState::default();
let ocel: OCEL = serde_json::from_value(tiny_ocel_json()).unwrap();
state.add("o1", ocel);
let resolved = resolve_argument("ocel", serde_json::json!("o1"), &schema, &state).unwrap();
assert_eq!(resolved, serde_json::json!("o1_as_SlimLinkedOCEL"));
assert_eq!(
state.items.read().unwrap()["o1_as_SlimLinkedOCEL"].kind(),
RegistryItemKind::SlimLinkedOCEL
);
assert_eq!(
resolve_argument("ocel", serde_json::json!({ "id": "o1" }), &schema, &state).unwrap(),
serde_json::json!("o1_as_SlimLinkedOCEL")
);
for value in [
serde_json::json!({ "inline": tiny_ocel_json() }),
tiny_ocel_json(),
serde_json::json!({
"bytes": base64_of(serde_json::to_string(&tiny_ocel_json()).unwrap().as_bytes()),
"format": "json"
}),
] {
let resolved = resolve_argument("ocel", value, &schema, &state).unwrap();
let items = state.items.read().unwrap();
let item = &items[resolved.as_str().unwrap()];
assert_eq!(item.kind(), RegistryItemKind::SlimLinkedOCEL);
let RegistryItem::SlimLinkedOCEL(locel) = item else {
unreachable!()
};
assert_eq!(locel.get_num_obs(), 2);
}
let err = resolve_argument(
"ocel",
serde_json::json!({ "inline": { "not": "an ocel", "at": "all" } }),
&schema,
&state,
)
.unwrap_err();
assert!(err.starts_with("Invalid Argument: ocel\n"), "{}", err);
}
#[test]
fn call_resolved_accepts_the_new_forms() {
let state = AppState::default();
let out = call_resolved(
binding_named("dummy_label"),
&serde_json::json!({ "h": { "inline": { "label": "inline-label", "hits": 0 } } }),
&state,
)
.unwrap();
assert_eq!(out, b"\"inline-label\"");
let out = call_resolved(
binding_named("num_objects"),
&serde_json::json!({ "ocel": tiny_ocel_json() }),
&state,
)
.unwrap();
assert_eq!(out, b"2");
state.add(
"d1",
RegistryItem::custom(DummyHandle {
label: "stored".to_string(),
hits: 0,
}),
);
let out = call_resolved(
binding_named("dummy_label"),
&serde_json::json!({ "h": "d1" }),
&state,
)
.unwrap();
assert_eq!(out, b"\"stored\"");
}
fn called_id(name: &str, args: Value, state: &AppState) -> String {
let out = call(binding_named(name), &args, state).unwrap();
serde_json::from_slice(&out).unwrap()
}
fn stored_handle_at(state: &AppState, id: &str) -> DummyHandle {
state
.items
.read()
.unwrap()
.get(id)
.unwrap_or_else(|| panic!("nothing stored under {}", id))
.as_custom::<DummyHandle>()
.unwrap()
.clone()
}
#[test]
fn output_id_stores_the_result_under_exactly_that_id() {
let state = AppState::default();
state.add(
"d1",
RegistryItem::custom(DummyHandle {
label: "src".to_string(),
hits: 0,
}),
);
state.add(
"o1",
serde_json::from_value::<OCEL>(tiny_ocel_json()).unwrap(),
);
let id = called_id(
"dummy_new",
serde_json::json!({ "label": "two", "output_id": "chosen" }),
&state,
);
assert_eq!(id, "chosen");
assert_eq!(
stored_handle_at(&state, "chosen"),
DummyHandle {
label: "two".to_string(),
hits: 0
}
);
let id = called_id(
"dummy_fork",
serde_json::json!({ "h": "d1", "output_id": "forked" }),
&state,
);
assert_eq!(id, "forked");
assert_eq!(stored_handle_at(&state, "forked").hits, 1);
let id = called_id(
"dummy_log_of",
serde_json::json!({ "h": "d1", "output_id": "a_log" }),
&state,
);
assert_eq!(id, "a_log");
assert_eq!(
state.items.read().unwrap()["a_log"].kind(),
RegistryItemKind::EventLog
);
let id = called_id(
"index_link_ocel",
serde_json::json!({ "ocel": "o1", "output_id": "linked" }),
&state,
);
assert_eq!(id, "linked");
assert_eq!(
state.items.read().unwrap()["linked"].kind(),
RegistryItemKind::IndexLinkedOCEL
);
let before = state.items.read().unwrap().len();
let id = called_id(
"dummy_new",
serde_json::json!({ "label": "again", "output_id": "chosen" }),
&state,
);
assert_eq!(id, "chosen");
assert_eq!(stored_handle_at(&state, "chosen").label, "again");
assert_eq!(state.items.read().unwrap().len(), before);
let err = call(
binding_named("dummy_new"),
&serde_json::json!({ "label": "x", "output_id": 7 }),
&state,
)
.unwrap_err();
assert!(err.contains("output_id"), "{}", err);
}
#[test]
fn omitting_output_id_keeps_the_generated_id() {
let state = AppState::default();
state.add(
"d1",
RegistryItem::custom(DummyHandle {
label: "src".to_string(),
hits: 0,
}),
);
for args in [
serde_json::json!({ "label": "two" }),
serde_json::json!({ "label": "two", "output_id": null }),
] {
let id = called_id("dummy_new", args, &state);
assert!(id.starts_with("res_"), "got {}", id);
assert_eq!(stored_handle_at(&state, &id).label, "two");
}
let id = called_id("dummy_fork", serde_json::json!({ "h": "d1" }), &state);
assert!(id.starts_with("res_"), "got {}", id);
let id = called_id("dummy_log_of", serde_json::json!({ "h": "d1" }), &state);
assert!(id.starts_with("res_"), "got {}", id);
}
#[test]
fn output_id_is_declared_only_where_a_handle_is_returned() {
for name in [
"dummy_new",
"dummy_fork",
"dummy_log_of",
"index_link_ocel",
"slim_link_ocel",
] {
let binding = binding_named(name);
let schema = arg_schema(name, "output_id");
assert_eq!(schema["type"], serde_json::json!(["string", "null"]));
assert_eq!(schema["title"], serde_json::json!("output_id"));
assert!(schema["description"].is_string(), "{}", name);
assert!(
!(binding.required_args)().iter().any(|a| a == "output_id"),
"{} requires output_id",
name
);
let names: Vec<String> = (binding.args)().into_iter().map(|(n, _)| n).collect();
assert_eq!(names.last().unwrap(), "output_id");
}
assert_eq!(
(binding_named("dummy_new").args)()
.into_iter()
.map(|(n, _)| n)
.collect::<Vec<_>>(),
vec!["label", "output_id"]
);
assert_eq!(
(binding_named("dummy_new").required_args)(),
vec!["label".to_string()]
);
for name in [
"dummy_label",
"dummy_bump",
"num_objects",
"test_some_inputs",
] {
let binding = binding_named(name);
assert!(
!(binding.args)().iter().any(|(n, _)| n == "output_id"),
"{} grew an output_id",
name
);
}
assert_eq!((binding_named("num_objects").args)().len(), 1);
assert_eq!((binding_named("test_some_inputs").args)().len(), 5);
}
#[test]
fn a_concurrent_insert_disturbs_neither_side() {
use std::sync::Arc;
let state = Arc::new(AppState::default());
let other = Arc::clone(&state);
let writer = std::thread::spawn(move || {
for i in 0..64usize {
other.add(
format!("unrelated_{}", i),
RegistryItem::custom(DummyHandle {
label: format!("u{}", i),
hits: i,
}),
);
}
});
let id = called_id(
"dummy_new",
serde_json::json!({ "label": "named", "output_id": "chosen" }),
&state,
);
writer.join().unwrap();
assert_eq!(id, "chosen");
assert_eq!(stored_handle_at(&state, "chosen").label, "named");
let items = state.items.read().unwrap();
for i in 0..64usize {
assert_eq!(
items[&format!("unrelated_{}", i)]
.as_custom::<DummyHandle>()
.unwrap()
.hits,
i
);
}
assert_eq!(items.len(), 65);
}
}