///|
pub(all) enum BindingChange {
SetBinding(String, Value)
RemoveBinding(String)
}
///|
pub(all) enum LinkSessionError {
InvalidBudget
TooManyChanges
DuplicateChange(String)
InvalidBindings(ExpandError)
InvalidEndpoint(String, ExpandError)
TotalOutputTooLong
}
///|
pub(all) struct LinkChange {
name : String
before : String
after : String
}
///|
/// Immutable snapshot with a private dependency index and captured bindings.
pub struct LinkSession {
priv catalog : TemplateCatalog
priv dependencies : Map[String, Array[Int32]]
priv values : Map[String, Value]
priv outputs : Array[String]
priv total : Int32
priv each_limit : Int32
priv total_limit : Int32
priv binding_limit : Int32
priv value_limit : Int32
priv item_limit : Int32
}
///|
pub struct LinkUpdate {
priv session : LinkSession
priv changes : Array[LinkChange]
priv recomputed : Int32
}
///|
pub fn LinkUpdate::session(self : LinkUpdate) -> LinkSession {
self.session
}
///|
pub fn LinkUpdate::changes(self : LinkUpdate) -> Array[LinkChange] {
self.changes.copy()
}
///|
pub fn LinkUpdate::recomputed(self : LinkUpdate) -> Int32 {
self.recomputed
}
///|
pub fn LinkSession::links(self : LinkSession) -> Array[(String, String)] {
let links = []
for index, name in self.catalog.names {
links.push((name, self.outputs[index]))
}
links
}
///|
pub fn LinkSession::total_length(self : LinkSession) -> Int32 {
self.total
}
///|
fn Template::session_bindings(
self : Template,
values : Map[String, Value],
) -> Array[(String, Value)] {
let bindings = []
for name in self.variables {
if values.get(name) is Some(value) {
bindings.push((name, value))
}
}
bindings
}
///|
/// Capture shared bindings and generate every endpoint once. Unknown bindings
/// are rejected; missing variables retain ordinary RFC omission semantics.
pub fn TemplateCatalog::start_session(
self : TemplateCatalog,
bindings : Array[(String, Value)],
max_each_output~ : Int32 = 1048576,
max_total_output~ : Int32 = 16777216,
max_bindings~ : Int32 = 4096,
max_value_length~ : Int32 = 1048576,
max_items~ : Int32 = 4096,
) -> Result[LinkSession, LinkSessionError] {
if max_each_output < 0 ||
max_total_output < 0 ||
max_bindings < 0 ||
max_value_length < 0 ||
max_items < 0 {
return Err(InvalidBudget)
}
let raw = match
prepare_bindings(bindings, max_bindings, max_value_length, max_items) {
Ok(value) => value
Err(error) => return Err(InvalidBindings(error))
}
let dependencies : Map[String, Array[Int32]] = Map([])
for index, name in self.names {
let template = self.templates.get(name).unwrap()
for variable in template.variables {
match dependencies.get(variable) {
Some(indexes) => indexes.push(index)
None => dependencies[variable] = [index]
}
}
}
let values : Map[String, Value] = Map([])
for name, value in raw {
if !dependencies.contains(name) {
return Err(InvalidBindings(UnusedVariable(name)))
}
values[name] = value.deep_copy()
}
// Measure the entire snapshot before allocating expanded output strings.
let mut total = 0
for name in self.names {
let template = self.templates.get(name).unwrap()
let length = match
template.measure(
template.session_bindings(values),
max_output=max_each_output,
max_bindings~,
max_value_length~,
max_items~,
) {
Ok(length) => length
Err(error) => return Err(InvalidEndpoint(name, error))
}
if length > max_total_output - total {
return Err(TotalOutputTooLong)
}
total += length
}
let outputs = []
for name in self.names {
let template = self.templates.get(name).unwrap()
match
template.expand(
template.session_bindings(values),
max_output=max_each_output,
max_bindings~,
max_value_length~,
max_items~,
) {
Ok(value) => outputs.push(value)
Err(error) => return Err(InvalidEndpoint(name, error))
}
}
Ok({
catalog: self,
dependencies,
values,
outputs,
total,
each_limit: max_each_output,
total_limit: max_total_output,
binding_limit: max_bindings,
value_limit: max_value_length,
item_limit: max_items,
})
}
///|
fn Value::same_content(self : Value, other : Value) -> Bool {
match (self, other) {
(Scalar(a), Scalar(b)) => a == b
(List(a), List(b)) => a == b
(Assoc(a), Assoc(b)) => a == b
(SparseList(a), SparseList(b)) => a == b
(SparseAssoc(a), SparseAssoc(b)) => a == b
_ => false
}
}
///|
/// Transactional patch: failure leaves this snapshot intact. Each affected
/// endpoint is measured/rendered once, even if several variables change.
pub fn LinkSession::update(
self : LinkSession,
patch : Array[BindingChange],
) -> Result[LinkUpdate, LinkSessionError] {
if patch.length() > 4096 {
return Err(TooManyChanges)
}
let seen : Map[String, Bool] = Map([])
for change in patch {
let name = match change {
SetBinding(name, _) | RemoveBinding(name) => name
}
if seen.contains(name) {
return Err(DuplicateChange(name))
}
seen[name] = true
if !self.dependencies.contains(name) {
return Err(InvalidBindings(UnusedVariable(name)))
}
if change is SetBinding(_, value) {
match check_value(value, name, self.value_limit, self.item_limit) {
Ok(_) => ()
Err(error) => return Err(InvalidBindings(error))
}
}
}
let values : Map[String, Value] = Map([])
for name, value in self.values {
values[name] = value
}
let affected : Map[Int32, Bool] = Map([])
for change in patch {
let (name, changed) = match change {
SetBinding(name, value) => {
let changed = match values.get(name) {
Some(old) => !old.same_content(value)
None => true
}
if changed {
values[name] = value.deep_copy()
}
(name, changed)
}
RemoveBinding(name) => {
let changed = values.contains(name)
values.remove(name)
(name, changed)
}
}
if changed {
for index in self.dependencies.get(name).unwrap() {
affected[index] = true
}
}
}
if values.length() > self.binding_limit {
return Err(InvalidBindings(TooManyBindings))
}
let mut total = self.total
for index, _ in affected {
total -= self.outputs[index].length()
}
for index, name in self.catalog.names {
if !affected.contains(index) {
continue
}
let template = self.catalog.templates.get(name).unwrap()
let length = match
template.measure(
template.session_bindings(values),
max_output=self.each_limit,
max_bindings=self.binding_limit,
max_value_length=self.value_limit,
max_items=self.item_limit,
) {
Ok(value) => value
Err(error) => return Err(InvalidEndpoint(name, error))
}
if length > self.total_limit - total {
return Err(TotalOutputTooLong)
}
total += length
}
let outputs = self.outputs.copy()
let changes = []
for index, name in self.catalog.names {
if !affected.contains(index) {
continue
}
let template = self.catalog.templates.get(name).unwrap()
let output = match
template.expand(
template.session_bindings(values),
max_output=self.each_limit,
max_bindings=self.binding_limit,
max_value_length=self.value_limit,
max_items=self.item_limit,
) {
Ok(value) => value
Err(error) => return Err(InvalidEndpoint(name, error))
}
if output != outputs[index] {
changes.push({ name, before: outputs[index], after: output, })
}
outputs[index] = output
}
let session = { ..self, values, outputs, total, }
Ok({ session, changes, recomputed: affected.length(), })
}