use kui_value::Value as KValue;
use crate::{self as channel, Cell, Channel, Column};
use mlua::{Lua, MetaMethod, Table, UserData, UserDataFields, UserDataMethods, Value};
#[derive(Clone)]
pub struct LuaChannel(pub Channel);
fn cell_to_lua(lua: &Lua, c: Cell<'_>) -> mlua::Result<Value> {
Ok(match c {
Cell::Null => Value::Nil,
Cell::F64(x) => Value::Number(x),
Cell::I64(x) => Value::Integer(x),
Cell::Bool(x) => Value::Boolean(x),
Cell::Str(s) => Value::String(lua.create_string(s)?),
})
}
fn column_index(ch: &Channel, col: &Value) -> mlua::Result<usize> {
match col {
Value::String(s) => {
let name = s.to_str()?;
ch.col(&name).ok_or_else(|| {
mlua::Error::runtime(format!(
"the channel has no column {:?}; it has {}",
&*name,
ch.names().join(", ")
))
})
}
Value::Integer(i) if *i >= 0 => Ok(*i as usize),
other => Err(mlua::Error::runtime(format!(
"a channel's column is a name or a 0-based number, not {}",
other.type_name()
))),
}
}
fn row_index(i: &Value) -> mlua::Result<usize> {
match i {
Value::Integer(i) if *i >= 0 => Ok(*i as usize),
Value::Number(n) if *n >= 0.0 && n.is_finite() => Ok(*n as usize),
other => Err(mlua::Error::runtime(format!(
"a channel's row is a 0-based number, not {}",
other.type_name()
))),
}
}
impl UserData for LuaChannel {
fn add_fields<F: UserDataFields<Self>>(f: &mut F) {
f.add_field_method_get("rev", |_, this| Ok(this.0.rev()));
f.add_field_method_get("id", |_, this| Ok(this.0.id()));
f.add_field_method_get("names", |lua, this| {
lua.create_sequence_from(this.0.names().iter().map(String::as_str))
});
}
fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
m.add_meta_method(MetaMethod::Len, |_, this, ()| Ok(this.0.len()));
m.add_meta_method(MetaMethod::Eq, |_, this, other: mlua::AnyUserData| {
Ok(other.borrow::<LuaChannel>().is_ok_and(|o| o.0 == this.0))
});
m.add_method("get", |lua, this, (row, col): (Value, Value)| {
let c = column_index(&this.0, &col)?;
cell_to_lua(lua, this.0.get(row_index(&row)?, c))
});
m.add_method("row", |lua, this, row: Value| {
let i = row_index(&row)?;
let t = lua.create_table_with_capacity(0, this.0.names().len())?;
for (c, name) in this.0.names().iter().enumerate() {
t.raw_set(name.as_str(), cell_to_lua(lua, this.0.get(i, c))?)?;
}
Ok(t)
});
m.add_method("column", |lua, this, col: Value| {
let c = column_index(&this.0, &col)?;
let column = this.0.column(c).expect("an index column_index answered");
let t = lua.create_table_with_capacity(column.len(), 0)?;
for i in 0..column.len() {
t.raw_set(i + 1, cell_to_lua(lua, column.get(i))?)?;
}
Ok(t)
});
m.add_method(
"reduce",
|lua, this, (col, from, to, n): (Value, Value, Value, Value)| {
let c = column_index(&this.0, &col)?;
let buckets = this
.0
.reduce(c, row_index(&from)?, row_index(&to)?, row_index(&n)?)
.map_err(|e| mlua::Error::runtime(format!("reduce: {e}")))?;
reduction_to_lua(lua, &buckets)
},
);
m.add_method(
"lanes",
|_, this, (start, len, lane): (Value, Value, Option<Value>)| {
let s = column_index(&this.0, &start)?;
let l = (!len.is_nil())
.then(|| column_index(&this.0, &len))
.transpose()?;
let d = lane
.filter(|v| !v.is_nil())
.map(|c| column_index(&this.0, &c))
.transpose()?;
this.0
.lanes(s, l, d)
.map(|lanes| LuaLanes(lanes, std::cell::RefCell::new(Vec::new())))
.map_err(|e| mlua::Error::runtime(format!("lanes: {e}")))
},
);
m.add_method("append", |_, this, (spec, cap): (Table, Option<usize>)| {
let cols = columns_of(&spec)?;
this.0
.append(cols, cap)
.map(LuaChannel)
.map_err(|e| mlua::Error::runtime(format!("append: {e}")))
});
m.add_method(
"filter",
|lua, this, (col, m, within): (Value, Table, Option<Table>)| {
let c = column_index(&this.0, &col)?;
let m = match_of(&m)?;
let within = within.map(|t| rows_of(&t)).transpose()?;
let rows = this
.0
.filter(c, &m, within.as_deref())
.map_err(|e| mlua::Error::runtime(format!("filter: {e}")))?;
lua.create_sequence_from(rows)
},
);
m.add_method(
"sort",
|lua, this, (col, descending, within): (Value, Option<bool>, Option<Table>)| {
let c = column_index(&this.0, &col)?;
let within = within.map(|t| rows_of(&t)).transpose()?;
let rows = this
.0
.sort(c, descending.unwrap_or(false), within.as_deref())
.map_err(|e| mlua::Error::runtime(format!("sort: {e}")))?;
lua.create_sequence_from(rows)
},
);
m.add_method("take", |_, this, rows: Table| {
this.0
.take(&rows_of(&rows)?)
.map(LuaChannel)
.map_err(|e| mlua::Error::runtime(format!("take: {e}")))
});
m.add_method("publish", |_, this, spec: Table| {
let cols = columns_of(&spec)?;
this.0
.publish(cols)
.map(LuaChannel)
.map_err(|e| mlua::Error::runtime(format!("publish: {e}")))
});
}
}
pub struct LuaLanes(pub crate::Lanes, std::cell::RefCell<Vec<crate::Run>>);
impl UserData for LuaLanes {
fn add_fields<F: UserDataFields<Self>>(f: &mut F) {
f.add_field_method_get("lo", |_, this| Ok(this.0.extent().0));
f.add_field_method_get("hi", |_, this| Ok(this.0.extent().1));
f.add_field_method_get("len", |_, this| Ok(this.0.len()));
}
fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
m.add_meta_method(MetaMethod::Len, |_, this, ()| Ok(this.0.count()));
m.add_method(
"window",
|lua, this, (lane, v0, v1, min): (usize, f64, f64, f64)| {
let mut runs = this.1.borrow_mut();
this.0.window(lane, v0, v1, min, &mut runs);
let t = lua.create_table_with_capacity(0, 4)?;
let list =
|f: &dyn Fn(&crate::Run) -> Value| lua.create_sequence_from(runs.iter().map(f));
t.raw_set("s", list(&|r| Value::Number(r.start))?)?;
t.raw_set("e", list(&|r| Value::Number(r.end))?)?;
t.raw_set(
"row",
list(&|r| Value::Integer(r.row.map_or(-1, |x| x as i64)))?,
)?;
t.raw_set("n", list(&|r| Value::Integer(r.count as i64))?)?;
Ok(t)
},
);
}
}
fn rows_of(t: &Table) -> mlua::Result<Vec<usize>> {
t.sequence_values::<Value>()
.map(|v| row_index(&v?))
.collect()
}
fn match_of(t: &Table) -> mlua::Result<channel::Match> {
if let Some(text) = t.get::<Option<String>>("contains")? {
return Ok(channel::Match::Contains {
text,
case: t.get::<Option<bool>>("case")?.unwrap_or(false),
});
}
let eq: Value = t.get("eq")?;
if !eq.is_nil() {
return Ok(channel::Match::Eq(kui_value::lua::from_lua(&eq)?));
}
let (min, max) = (t.get::<Option<f64>>("min")?, t.get::<Option<f64>>("max")?);
if min.is_some() || max.is_some() {
return Ok(channel::Match::Range { min, max });
}
Err(mlua::Error::runtime(
"a filter is { eq = v }, { contains = text, case = bool } or { min = a, max = b }",
))
}
fn reduction_to_lua(lua: &Lua, buckets: &[channel::Bucket]) -> mlua::Result<Table> {
let t = lua.create_table_with_capacity(0, 8)?;
let field = |name: &str, get: &dyn Fn(&channel::Bucket) -> Value| -> mlua::Result<()> {
t.raw_set(name, lua.create_sequence_from(buckets.iter().map(get))?)
};
field("min", &|b| Value::Number(b.min))?;
field("max", &|b| Value::Number(b.max))?;
field("first", &|b| Value::Number(b.first))?;
field("last", &|b| Value::Number(b.last))?;
field("min_row", &|b| Value::Integer(b.min_row as i64))?;
field("max_row", &|b| Value::Integer(b.max_row as i64))?;
field("first_row", &|b| Value::Integer(b.first_row as i64))?;
field("last_row", &|b| Value::Integer(b.last_row as i64))?;
Ok(t)
}
fn columns_of(spec: &Table) -> mlua::Result<Vec<(String, Column)>> {
let mut cols = Vec::new();
for pair in spec.pairs::<String, Table>() {
let (name, list) = pair?;
let column = column_from(&list)?
.map_err(|e| mlua::Error::runtime(format!("channel column `{name}`: {e}")))?;
cols.push((name, column));
}
cols.sort_by(|a, b| a.0.cmp(&b.0));
Ok(cols)
}
fn column_from(list: &Table) -> mlua::Result<Result<Column, channel::ChannelError>> {
enum Build {
Empty,
Ints(Vec<i64>),
Floats(Vec<f64>),
Bools(Vec<bool>),
Strs(channel::Strs),
}
let kind = |v: &Value| match v {
Value::Nil => "nothing",
Value::Boolean(_) => "a boolean",
Value::Integer(_) | Value::Number(_) => "a number",
Value::String(_) => "a string",
Value::Table(_) => "a list",
_ => "a value",
};
let bad = |i: usize, v: &Value, want: &str| {
Ok(Err(channel::ChannelError(format!(
"row {} is {}, in a column of {want}s",
i + 1,
kind(v)
))))
};
let mut b = Build::Empty;
for (i, v) in list.sequence_values::<Value>().enumerate() {
let v = v?;
b = match (b, &v) {
(Build::Empty, Value::Integer(x)) => Build::Ints(vec![*x]),
(Build::Empty, Value::Number(x)) => Build::Floats(vec![*x]),
(Build::Empty, Value::Boolean(x)) => Build::Bools(vec![*x]),
(Build::Empty, Value::String(x)) => {
let mut out = channel::Strs::default();
out.push(&x.to_str()?);
Build::Strs(out)
}
(Build::Empty, _) => return bad(0, &v, "number, boolean or string"),
(Build::Ints(mut out), Value::Integer(x)) => {
out.push(*x);
Build::Ints(out)
}
(Build::Ints(out), Value::Number(x)) => {
let mut f: Vec<f64> = out.into_iter().map(|n| n as f64).collect();
f.push(*x);
Build::Floats(f)
}
(Build::Floats(mut out), Value::Integer(x)) => {
out.push(*x as f64);
Build::Floats(out)
}
(Build::Floats(mut out), Value::Number(x)) => {
out.push(*x);
Build::Floats(out)
}
(Build::Ints(_) | Build::Floats(_), _) => return bad(i, &v, "number"),
(Build::Bools(mut out), Value::Boolean(x)) => {
out.push(*x);
Build::Bools(out)
}
(Build::Bools(_), _) => return bad(i, &v, "boolean"),
(Build::Strs(mut out), Value::String(x)) => {
out.push(&x.to_str()?);
Build::Strs(out)
}
(Build::Strs(_), _) => return bad(i, &v, "string"),
};
}
Ok(Ok(match b {
Build::Empty => Column::F64(Vec::new()),
Build::Ints(v) => Column::I64(v),
Build::Floats(v) => Column::F64(v),
Build::Bools(v) => Column::Bool(v),
Build::Strs(v) => Column::Str(v),
}))
}
pub fn register(lua: &Lua) -> mlua::Result<()> {
let make = lua.create_function(|_, spec: Table| {
Channel::table(columns_of(&spec)?)
.map(LuaChannel)
.map_err(|e| mlua::Error::runtime(format!("channel: {e}")))
})?;
lua.globals().set("channel", make)
}
pub fn handle_to_lua(lua: &Lua, v: &KValue) -> Option<mlua::Result<Value>> {
if !channel::is_handle(v) {
return None;
}
Some(match Channel::resolve(v) {
Some(ch) => lua.create_userdata(LuaChannel(ch)).map(Value::UserData),
None => Ok(Value::Nil),
})
}
pub fn userdata_value(ud: &mlua::AnyUserData) -> Option<mlua::Result<KValue>> {
ud.is::<LuaChannel>()
.then(|| ud.borrow::<LuaChannel>().map(|c| c.0.handle()))
}
pub fn from_lua(v: &Value) -> mlua::Result<KValue> {
kui_value::lua::from_lua_with(v, &userdata_value)
}
pub fn to_lua(lua: &Lua, v: &KValue) -> mlua::Result<Value> {
kui_value::lua::to_lua_with(lua, v, &handle_to_lua)
}