use sim_kernel::{Cx, Error, Expr, Result, Symbol};
use sim_lib_view::{LensRegistry, Mode, universal_scene};
use crate::transport::{SessionStatus, Transport};
const MAX_PANES: usize = 64;
const MAX_PANE_NAME: usize = 128;
const MAX_RESOURCE_NAME: usize = 512;
fn validate_pane_name(pane: &Symbol) -> Result<()> {
let name = pane.as_qualified_str();
if name.is_empty() || name.len() > MAX_PANE_NAME {
return Err(Error::HostError(format!(
"pane name must be 1..={MAX_PANE_NAME} bytes, got {}",
name.len()
)));
}
if !name.bytes().all(|byte| byte.is_ascii_graphic()) {
return Err(Error::HostError(
"pane name must be printable ASCII without spaces".to_owned(),
));
}
Ok(())
}
fn validate_resource_name(resource: &Symbol) -> Result<()> {
let name = resource.as_qualified_str();
if name.is_empty() || name.len() > MAX_RESOURCE_NAME {
return Err(Error::HostError(format!(
"resource name must be 1..={MAX_RESOURCE_NAME} bytes, got {}",
name.len()
)));
}
Ok(())
}
struct Subscription {
pane: Symbol,
resource: Symbol,
view_lens: Symbol,
editor_lens: Symbol,
last_scene: Expr,
}
#[derive(Clone, Debug)]
pub struct SceneUpdate {
pub pane: Symbol,
pub scene: Expr,
pub diff: Expr,
}
pub struct Session<T: Transport> {
transport: T,
subscriptions: Vec<Subscription>,
mode: Mode,
}
impl<T: Transport> Session<T> {
pub fn new(transport: T) -> Self {
Self {
transport,
subscriptions: Vec::new(),
mode: Mode::Builder,
}
}
pub fn status(&self) -> SessionStatus {
self.transport.status()
}
pub fn mode(&self) -> Mode {
self.mode
}
pub fn set_mode(&mut self, intent: &Expr) -> Result<()> {
match sim_value::access::field(intent, "kind") {
Some(Expr::Symbol(kind)) if &*kind.name == "set-mode" => {}
_ => {
return Err(Error::HostError(
"set_mode expects an intent/set-mode".to_owned(),
));
}
}
let mode = match sim_value::access::field(intent, "mode") {
Some(Expr::Symbol(symbol)) => Mode::from_name(&symbol.name),
_ => None,
};
self.mode = mode.ok_or_else(|| {
Error::HostError(
"intent/set-mode 'mode' must be household, builder, or systems".to_owned(),
)
})?;
Ok(())
}
pub fn render_universal(&self, value: &Expr) -> Expr {
universal_scene(value, self.mode)
}
pub fn transport_mut(&mut self) -> &mut T {
&mut self.transport
}
pub fn open(
&mut self,
cx: &mut Cx,
registry: &LensRegistry,
pane: Symbol,
resource: Symbol,
view_lens: Symbol,
editor_lens: Symbol,
) -> Result<Expr> {
validate_pane_name(&pane)?;
validate_resource_name(&resource)?;
let replacing = self.subscriptions.iter().any(|sub| sub.pane == pane);
if !replacing && self.subscriptions.len() >= MAX_PANES {
return Err(Error::HostError(format!(
"session is at its pane limit ({MAX_PANES}); close a pane before opening another"
)));
}
let value = self.transport.read(&resource)?;
let scene = registry.render(cx, &view_lens, &value)?;
self.subscriptions.retain(|sub| sub.pane != pane);
self.subscriptions.push(Subscription {
pane,
resource,
view_lens,
editor_lens,
last_scene: scene.clone(),
});
Ok(scene)
}
pub fn submit_intent(
&mut self,
cx: &mut Cx,
registry: &LensRegistry,
pane: &Symbol,
intent: &Expr,
) -> Result<()> {
let (resource, editor) = {
let sub = self
.subscriptions
.iter()
.find(|sub| &sub.pane == pane)
.ok_or_else(|| Error::HostError(format!("pane '{pane}' is not open")))?;
(sub.resource.clone(), sub.editor_lens.clone())
};
let value = self.transport.read(&resource)?;
let draft = registry.propose(cx, &editor, &value, intent)?;
let operation = registry.commit(cx, &editor, &draft)?;
self.transport.realize(&resource, &operation.form)?;
Ok(())
}
pub fn pump(&mut self, cx: &mut Cx, registry: &LensRegistry) -> Result<Vec<SceneUpdate>> {
let events = self.transport.drain_events();
let mut updates = Vec::new();
let Self {
transport,
subscriptions,
..
} = self;
for event in events {
for sub in subscriptions
.iter_mut()
.filter(|sub| sub.resource == event.resource)
{
let value = transport.read(&sub.resource)?;
let scene = registry.render(cx, &sub.view_lens, &value)?;
let diff = sim_lib_scene::diff(&sub.last_scene, &scene);
sub.last_scene = scene.clone();
updates.push(SceneUpdate {
pane: sub.pane.clone(),
scene,
diff,
});
}
}
Ok(updates)
}
}
#[cfg(test)]
mod tests {
use sim_kernel::{Cx, Expr, Symbol};
use sim_lib_view::{
LensRegistry, UNIVERSAL_EDITOR_ID, UNIVERSAL_VIEW_ID, register_universal_default,
};
use super::{MAX_PANES, Session};
use crate::fixture::FixtureTransport;
use sim_value::build::keyword as sym;
use sim_kernel::testing::eager_cx as cx;
fn registry() -> LensRegistry {
let mut registry = LensRegistry::new();
register_universal_default(&mut registry, false);
registry
}
fn open(
session: &mut Session<FixtureTransport>,
cx: &mut Cx,
registry: &LensRegistry,
pane: &str,
) -> sim_kernel::Result<Expr> {
session.open(
cx,
registry,
sym(pane),
sym("doc"),
Symbol::new(UNIVERSAL_VIEW_ID),
Symbol::new(UNIVERSAL_EDITOR_ID),
)
}
#[test]
fn open_bounds_the_number_of_panes() {
let mut cx = cx();
let registry = registry();
let mut session = Session::new(FixtureTransport::new().with(sym("doc"), Expr::Nil));
for index in 0..MAX_PANES {
open(&mut session, &mut cx, ®istry, &format!("pane-{index}")).unwrap();
}
assert!(
open(&mut session, &mut cx, ®istry, "pane-overflow").is_err(),
"opening past the pane cap must be refused"
);
open(&mut session, &mut cx, ®istry, "pane-0").unwrap();
}
#[test]
fn open_rejects_untrusted_pane_names() {
let mut cx = cx();
let registry = registry();
let mut session = Session::new(FixtureTransport::new().with(sym("doc"), Expr::Nil));
assert!(
open(&mut session, &mut cx, ®istry, "").is_err(),
"empty pane"
);
let huge = "p".repeat(super::MAX_PANE_NAME + 1);
assert!(
open(&mut session, &mut cx, ®istry, &huge).is_err(),
"over-long pane name"
);
assert!(
open(&mut session, &mut cx, ®istry, "has space").is_err(),
"pane name with a space"
);
}
}