use std::ops::Deref;
use crate::{Error, TulispContext, TulispObject};
pub fn plist_from<const N: usize>(input: [(TulispObject, TulispObject); N]) -> TulispObject {
let mut builder = crate::cons::ListBuilder::new();
for (key, value) in input.into_iter() {
builder.push(key);
builder.push(value);
}
builder.build()
}
pub fn plist_get(plist: &TulispObject, property: &TulispObject) -> Result<TulispObject, Error> {
let mut slow = plist.clone();
let mut cur = plist.clone();
loop {
if !cur.consp() {
return Ok(TulispObject::nil());
}
if cur.car_and_then(|car| Ok(car.eq(property)))? {
return cur.cadr();
}
cur = cur.cddr()?;
slow = slow.cdr()?;
if slow.eq_ptr(&cur) {
return Err(Error::out_of_range("Circular plist".to_string()));
}
}
}
pub struct Plist<T: Plistable> {
plist: T,
}
impl<T> Plist<T>
where
T: Plistable,
{
pub(crate) fn new(ctx: &mut TulispContext, args: &[TulispObject]) -> Result<Self, Error> {
Ok(Self {
plist: T::from_plist_as_slice(ctx, args)?,
})
}
pub fn into_inner(self) -> T {
self.plist
}
}
impl<T> Deref for Plist<T>
where
T: Plistable,
{
type Target = T;
fn deref(&self) -> &Self::Target {
&self.plist
}
}
pub trait Plistable {
fn from_plist_as_slice(ctx: &mut TulispContext, kvs: &[TulispObject]) -> Result<Self, Error>
where
Self: Sized;
fn from_plist(ctx: &mut TulispContext, obj: &TulispObject) -> Result<Self, Error>
where
Self: Sized;
fn into_plist(self, ctx: &mut TulispContext) -> TulispObject;
}
#[macro_export]
macro_rules! AsPlist {
(@key-name
$field:ident<$field_key:literal>) => {
$field_key
};
(@key-name $field:ident) => {
concat!(":", stringify!($field))
};
(@missing-field $key_name:expr, $default:expr) => {
Ok($default)
};
(@missing-field $key_name:expr,) => {
Err($crate::Error::plist_error(concat!("Missing ", $key_name, " field")))
};
(@extract-field $value:ident, None) => {
if $value.null() {
None
} else {
Some($value.try_into()?)
}
};
(@extract-field $value:ident, Some($e: expr)) => {
if $value.null() {
None
} else {
Some($value.try_into()?)
}
};
(@extract-field $value:ident, $e: expr) => {
$value.try_into()?
};
(@extract-field $value:ident) => {
$value.try_into()?
};
(
$( #[$meta:meta] )*
$vis:vis struct $struct_name:ident {
$(
$( #[$($field_meta:tt)+] )*
$field_vis:vis $field:ident$(<$field_key:literal>)? : $type:ty
$({= $($default:tt)+ })?
),+ $(,)?
}
) => {
$( #[$meta] )*
$vis struct $struct_name {
$($( #[$($field_meta)+] )* $field_vis $field: $type),+
}
impl $crate::Plistable for $struct_name {
fn from_plist_as_slice(
ctx: &mut TulispContext,
kvs: &[$crate::TulispObject],
) -> Result<Self, $crate::Error> {
#[derive(Default)]
struct Builder {
$($field: Option<$type>),+
}
impl Builder {
fn build(self) -> Result<$struct_name, $crate::Error> {
Ok($struct_name {
$($field: if let Some(f) = self.$field { f } else {
$crate::AsPlist!(
@missing-field
$crate::AsPlist!(@key-name $field $(<$field_key>)?),
$( $($default)+ )?
)?}),+
})
}
}
let symbols = $crate::intern!(ctx => {
$($field: $crate::AsPlist!(@key-name $field $(<$field_key>)?)),+
});
let (kvpairs, extra) = kvs.as_chunks::<2>();
if !extra.is_empty() {
return Err($crate::Error::plist_error(
"Expected an even number of items in the plist",
));
}
let mut builder = Builder::default();
for [key, value] in kvpairs {
$(if key.eq(&symbols.$field) {
let value = value.clone();
builder.$field = Some($crate::AsPlist!(@extract-field value $(, $($default)+)?));
} else)+ {
return Err($crate::Error::plist_error(format!(
"Unexpected key in plist: {}",
key
)));
}
}
builder.build()
}
fn from_plist(
ctx: &mut TulispContext,
obj: &$crate::TulispObject,
) -> Result<Self, $crate::Error> {
#[derive(Default)]
struct Builder {
$($field: Option<$type>),+
}
impl Builder {
fn build(self) -> Result<$struct_name, $crate::Error> {
Ok($struct_name {
$($field: if let Some(f) = self.$field { f } else {
$crate::AsPlist!(
@missing-field
$crate::AsPlist!(@key-name $field $(<$field_key>)?),
$( $($default)+ )?
)?}),+
})
}
}
let symbols = $crate::intern!(ctx => {
$($field: $crate::AsPlist!(@key-name $field $(<$field_key>)?)),+
});
let mut iter = obj.base_iter();
let mut builder = Builder::default();
while let Some(key) = iter.next() {
let Some(value) = iter.next() else {
return Err($crate::Error::plist_error(
"Expected an even number of items in the plist",
));
};
$(if key.eq(&symbols.$field) {
builder.$field = Some($crate::AsPlist!(@extract-field value $(, $($default)+)?));
} else)+ {
return Err($crate::Error::plist_error(format!(
"Unexpected key in plist: {}",
key
)));
}
}
builder.build()
}
fn into_plist(self, ctx: &mut TulispContext) -> $crate::TulispObject {
let symbols = $crate::intern!(ctx => {
$($field: $crate::AsPlist!(@key-name $field $(<$field_key>)?)),+
});
$crate::plist::plist_from([
$((symbols.$field.clone(), self.$field.into())),+
])
}
}
};
}
#[cfg(test)]
mod tests {
use super::{plist_from, plist_get};
use crate::{
Error, Plist, Plistable, TulispContext,
test_utils::{eval_assert_equal, eval_assert_error},
};
#[test]
fn test_plist_primitives() -> Result<(), Error> {
let mut ctx = TulispContext::new();
let a = ctx.intern("a");
let b = ctx.intern("b");
let c = ctx.intern("c");
let d = ctx.intern("d");
let list = plist_from([
(a.clone(), 20.into()),
(b.clone(), 30.into()),
(c.clone(), 40.into()),
]);
assert!(plist_get(&list, &b)?.equal(&30.into()));
assert!(plist_get(&list, &d)?.null());
Ok(())
}
#[test]
fn test_plist_get_detects_cycle() {
let mut ctx = TulispContext::new();
ctx.eval_string(
r#"
(setq x (list :a 1 :b 2))
(setcdr (cdr (cdr (cdr x))) x)
"#,
)
.unwrap();
let x = ctx.eval_string("x").unwrap();
let missing = ctx.intern(":missing");
let err = plist_get(&x, &missing).unwrap_err();
let msg = err.format(&ctx);
assert!(msg.contains("Circular plist"), "got: {msg}");
}
AsPlist! {
#[derive(Default)]
struct Person {
name<":first-name">: String,
age: i64,
addr: Vec<String>,
education<":edu">: Option<String> {= None},
place: Option<String> {= Some("Home".to_string())},
answer: i64 {= 42},
}
}
#[test]
fn test_plistable() -> Result<(), Error> {
let mut ctx = TulispContext::new();
ctx.defun("get-name", |person: Plist<Person>| person.name.clone())
.defun("get-age", |p: Plist<Person>| -> i64 { p.age })
.defun("get-ans", |p: Plist<Person>| -> i64 { p.answer })
.defun("get-place", |p: Plist<Person>| p.place.clone().unwrap())
.defun("get-edu", |p: Plist<Person>| {
p.education.clone().unwrap_or("Unknown".to_string())
})
.defun("get-addr", |p: Plist<Person>| {
p.addr.last().unwrap().clone()
});
eval_assert_equal(
&mut ctx,
r#"(get-name :first-name "Alice" :age 30 :addr nil)"#,
r#""Alice""#,
);
eval_assert_equal(
&mut ctx,
r#"(get-age :first-name "Alice" :age 30 :addr nil)"#,
r#"30"#,
);
eval_assert_equal(
&mut ctx,
r#"(get-edu :first-name "Alice" :age 30 :addr nil)"#,
r#""Unknown""#,
);
eval_assert_equal(
&mut ctx,
r#"(get-edu :first-name "Alice" :age 30 :addr nil :edu "School")"#,
r#""School""#,
);
eval_assert_equal(
&mut ctx,
r#"(get-edu :first-name "Alice" :age 30 :addr nil :edu nil)"#,
r#""Unknown""#,
);
eval_assert_equal(
&mut ctx,
r#"(get-place :first-name "Alice" :age 30 :addr nil)"#,
r#""Home""#,
);
eval_assert_equal(
&mut ctx,
r#"(get-place :first-name "Alice" :age 30 :addr nil :place "Office")"#,
r#""Office""#,
);
eval_assert_equal(
&mut ctx,
r#"(get-ans :first-name "Alice" :age 30 :addr nil)"#,
r#"42"#,
);
eval_assert_equal(
&mut ctx,
r#"(get-ans :first-name "Alice" :age 30 :addr nil :place "Office" :answer 5)"#,
r#"5"#,
);
eval_assert_equal(
&mut ctx,
r#"(get-addr :first-name "Alice" :age 30 :addr '("street" "other street"))"#,
r#""other street""#,
);
eval_assert_equal(
&mut ctx,
r#"(get-addr :first-name "Alice" :age 30 :addr '("street" "other street"))"#,
r#""other street""#,
);
eval_assert_error(
&mut ctx,
r#"(get-age :first-name "Alice" :age 30 :other 10)"#,
r#"ERR PlistError: Unexpected key in plist: :other
<eval_string>:1.1-1.47: at (get-age :first-name "Alice" :age 30 :other 10)
"#,
);
eval_assert_error(
&mut ctx,
r#"(get-age :first-name "Alice")"#,
r#"ERR PlistError: Missing :age field
<eval_string>:1.1-1.29: at (get-age :first-name "Alice")
"#,
);
Ok(())
}
#[test]
fn test_plistable_round_trip() -> Result<(), Error> {
let mut ctx = TulispContext::new();
let p = Person {
name: "Carol".into(),
age: 40,
addr: vec!["Pine St".into()],
education: None,
place: Some("Home".into()),
answer: 42,
};
let obj = p.into_plist(&mut ctx);
let q = Person::from_plist(&mut ctx, &obj)?;
assert_eq!(q.name, "Carol");
assert_eq!(q.age, 40);
assert_eq!(q.addr, vec!["Pine St".to_string()]);
assert_eq!(q.education, None);
assert_eq!(q.place.as_deref(), Some("Home"));
assert_eq!(q.answer, 42);
Ok(())
}
#[test]
fn test_plistable_from_lisp_value_no_eval() -> Result<(), Error> {
let mut ctx = TulispContext::new();
ctx.eval_string(
r#"(setq x '(:first-name "Bob"
:age 25
:addr ("Main St" "Oak Ave")
:place "Office"))"#,
)?;
let x = ctx.eval_string("x")?;
let p = Person::from_plist(&mut ctx, &x)?;
assert_eq!(p.name, "Bob");
assert_eq!(p.age, 25);
assert_eq!(p.addr, vec!["Main St".to_string(), "Oak Ave".to_string()]);
assert_eq!(p.place.as_deref(), Some("Office"));
assert_eq!(p.education, None);
Ok(())
}
}