pub mod tsify;
use std::sync::Arc;
use ankurah_proto::{CollectionId, EntityId, State};
use crate::entity::Entity;
use crate::error::StateError;
use crate::property::PropertyError;
use anyhow::Result;
#[cfg(feature = "wasm")]
use js_sys;
#[cfg(feature = "wasm")]
use wasm_bindgen;
#[cfg(feature = "wasm")]
use wasm_bindgen::JsCast;
pub trait Model: Sized {
type View: View;
type Mutable: Mutable;
#[cfg(feature = "wasm")]
type RefWrapper: From<crate::property::Ref<Self>> + Into<crate::property::Ref<Self>>;
fn collection() -> CollectionId;
fn initialize_new_entity(&self, entity: &Entity);
}
pub trait View {
type Model: Model;
type Mutable: Mutable;
fn id(&self) -> EntityId { self.entity().id() }
fn collection() -> CollectionId { <Self::Model as Model>::collection() }
fn entity(&self) -> &Entity;
fn from_entity(inner: Entity) -> Self;
fn to_model(&self) -> Result<Self::Model, PropertyError>;
}
#[derive(Debug)]
pub struct MutableBorrow<'rec, T: Mutable> {
mutable: T,
_entity_ref: &'rec Entity,
}
impl<'rec, T: Mutable> MutableBorrow<'rec, T> {
pub fn new(entity_ref: &'rec Entity) -> Self { Self { mutable: T::new(entity_ref.clone()), _entity_ref: entity_ref } }
pub fn into_core(self) -> T { self.mutable }
}
impl<'rec, T: Mutable> std::ops::Deref for MutableBorrow<'rec, T> {
type Target = T;
fn deref(&self) -> &Self::Target { &self.mutable }
}
impl<'rec, T: Mutable> std::ops::DerefMut for MutableBorrow<'rec, T> {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.mutable }
}
pub trait Mutable {
type Model: Model;
type View: View;
fn id(&self) -> EntityId { self.entity().id() }
fn collection() -> CollectionId { <Self::Model as Model>::collection() }
fn entity(&self) -> &Entity;
fn new(entity: Entity) -> Self
where Self: Sized;
fn state(&self) -> Result<State, StateError> { self.entity().to_state() }
fn read(&self) -> Self::View {
let inner = self.entity();
let new_inner = match &inner.kind {
crate::entity::EntityKind::Transacted { upstream, .. } => upstream.clone(),
crate::entity::EntityKind::Primary => inner.clone(),
};
Self::View::from_entity(new_inner)
}
}
#[doc(hidden)]
#[cfg(feature = "wasm")]
pub fn wasm_prop<T>(result: Result<T, PropertyError>, property: &'static str, model: &'static str) -> Result<T, wasm_bindgen::JsValue> {
result.map_err(|err| match err {
PropertyError::Missing => wasm_bindgen::JsValue::from_str(&format!("property '{}' is missing in model '{}'", property, model)),
_ => wasm_bindgen::JsValue::from_str(&err.to_string()),
})
}
#[doc(hidden)]
pub fn view_subscribe<V, F>(view: &V, listener: F) -> ankurah_signals::SubscriptionGuard
where
V: ankurah_signals::Signal + View + Clone + Send + Sync + 'static,
F: ankurah_signals::subscribe::IntoSubscribeListener<V>,
{
let listener = listener.into_subscribe_listener();
let view_clone = view.clone();
let subscription = view.listen(Arc::new(move |_| {
listener(view_clone.clone());
}));
ankurah_signals::SubscriptionGuard::new(subscription)
}
#[doc(hidden)]
pub fn view_subscribe_no_clone<V, F>(view: &V, listener: F) -> ankurah_signals::SubscriptionGuard
where
V: ankurah_signals::Signal + View + Send + Sync + 'static,
F: ankurah_signals::subscribe::IntoSubscribeListener<()>,
{
let listener = listener.into_subscribe_listener();
let subscription = view.listen(Arc::new(move |_| {
listener(());
}));
ankurah_signals::SubscriptionGuard::new(subscription)
}
#[doc(hidden)]
#[cfg(feature = "wasm")]
pub fn js_preprocess_ref_field(obj: &wasm_bindgen::JsValue, field_name: &str) -> Result<(), wasm_bindgen::JsValue> {
let field_key = wasm_bindgen::JsValue::from_str(field_name);
if let Ok(v) = js_sys::Reflect::get(obj, &field_key) {
if v.as_string().is_some() {
return Ok(());
}
let id_key = wasm_bindgen::JsValue::from_str("id");
if let Ok(id_value) = js_sys::Reflect::get(&v, &id_key) {
let base64_key = wasm_bindgen::JsValue::from_str("to_base64");
if let Ok(to_base64_fn) = js_sys::Reflect::get(&id_value, &base64_key) {
if let Some(func) = to_base64_fn.dyn_ref::<js_sys::Function>() {
if let Ok(result) = func.call0(&id_value) {
if let Some(id_str) = result.as_string() {
js_sys::Reflect::set(obj, &field_key, &wasm_bindgen::JsValue::from_str(&id_str))?;
return Ok(());
}
}
}
}
}
if !v.is_undefined() && !v.is_null() {
return Err(wasm_bindgen::JsValue::from_str(&format!("Field '{}' must be a View, Ref, or base64 string", field_name)));
}
}
Ok(())
}
#[doc(hidden)]
#[cfg(feature = "wasm")]
pub fn js_populate_predicate(
predicate: ankql::ast::Predicate,
substitution_values: &wasm_bindgen::JsValue,
) -> Result<ankql::ast::Predicate, wasm_bindgen::JsValue> {
let args_array: js_sys::Array =
substitution_values.clone().try_into().map_err(|_| wasm_bindgen::JsValue::from_str("Invalid arguments array"))?;
let exprs = args_array
.iter()
.map(|value| match js_entity_id(&value) {
Some(id) => Ok(ankql::ast::Expr::from(&id)),
None => ankql::ast::Expr::try_from(value),
})
.collect::<Result<Vec<_>, ankql::error::ParseError>>()
.map_err(|e| wasm_bindgen::JsValue::from_str(&e.to_string()))?;
predicate.populate(exprs).map_err(|e| wasm_bindgen::JsValue::from_str(&e.to_string()))
}
#[cfg(feature = "wasm")]
fn js_entity_id(value: &wasm_bindgen::JsValue) -> Option<ankurah_proto::EntityId> {
use wasm_bindgen::JsCast;
if value.as_string().is_some() || !value.is_object() {
return None;
}
let to_base64 = js_sys::Reflect::get(value, &wasm_bindgen::JsValue::from_str("to_base64")).ok()?;
let func = to_base64.dyn_ref::<js_sys::Function>()?;
let result = func.call0(value).ok()?;
ankurah_proto::EntityId::from_base64(result.as_string()?).ok()
}
#[doc(hidden)]
#[cfg(feature = "wasm")]
pub fn js_resultset_map<V>(resultset: &crate::resultset::ResultSet<V>, callback: &js_sys::Function) -> js_sys::Array
where V: View + Clone + 'static + Into<wasm_bindgen::JsValue> {
use ankurah_signals::Get;
let items = resultset.get();
let result_array = js_sys::Array::new();
for item in items {
let js_item = item.into();
if let Ok(mapped_value) = callback.call1(&wasm_bindgen::JsValue::NULL, &js_item) {
result_array.push(&mapped_value);
}
}
result_array
}
#[doc(hidden)]
#[cfg(feature = "wasm")]
pub fn js_livequery_subscribe<V, W, F>(
livequery: &crate::livequery::LiveQuery<V>,
callback: js_sys::Function,
immediate: bool,
wrap_changeset: F,
) -> ankurah_signals::SubscriptionGuard
where
V: View + Clone + Send + Sync + 'static,
W: Into<wasm_bindgen::JsValue>,
F: Fn(crate::changes::ChangeSet<V>) -> W + Send + Sync + 'static,
{
use ankurah_signals::{Peek, Subscribe};
if immediate {
let current_items = livequery.peek();
let changes = current_items.into_iter().map(|item| crate::changes::ItemChange::Add { item, events: vec![] }).collect();
let initial_changeset = crate::changes::ChangeSet { resultset: livequery.resultset(), changes };
let wrapped = wrap_changeset(initial_changeset);
let _ = callback.call1(&wasm_bindgen::JsValue::NULL, &wrapped.into());
}
let callback = ::send_wrapper::SendWrapper::new(callback);
livequery.subscribe(move |changeset: crate::changes::ChangeSet<V>| {
let wrapped_changeset = wrap_changeset(changeset);
let _ = callback.call1(&wasm_bindgen::JsValue::NULL, &wrapped_changeset.into());
})
}