use std::{collections::HashSet, marker::PhantomData};
use super::Property;
use crate::{LispContext, LispError, Value, ValueRef, clos::Methods};
pub trait LispClass: Sized + 'static {
const NAME: &'static str;
fn class() -> Class<Self>;
}
pub struct Class<T> {
name: String,
properties: Vec<Property<T>>,
implementations: Vec<Methods<T>>,
_marker: PhantomData<fn() -> T>,
}
impl<T> Class<T> {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
properties: Vec::new(),
implementations: Vec::new(),
_marker: PhantomData,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn property(mut self, property: Property<T>) -> Self {
self.properties.push(property);
self
}
pub fn properties(&self) -> &[Property<T>] {
&self.properties
}
pub fn find_property(&self, name: &str) -> Option<&Property<T>> {
self.properties
.iter()
.find(|property| property.name() == name)
}
pub fn property_with(
mut self,
name: impl Into<String>,
build: impl FnOnce(Property<T>) -> Property<T>,
) -> Self {
self.properties.push(build(Property::new(name)));
self
}
pub fn methods(mut self, methods: Methods<T>) -> Self {
self.implementations.push(methods);
self
}
pub fn implementations(&self) -> &[Methods<T>] {
&self.implementations
}
pub fn into_implementations(self) -> Vec<Methods<T>> {
self.implementations
}
pub fn read<'rt>(
&self,
name: &str,
obj: &T,
ctx: &LispContext<'rt>,
) -> Result<Option<Value<'rt>>, LispError> {
match self.find_property(name) {
Some(property) => property.read(obj, ctx),
None => Ok(None),
}
}
pub fn write(&self, name: &str, obj: &mut T, value: ValueRef<'_>) -> Result<bool, LispError> {
match self.find_property(name) {
Some(property) => property.write(obj, value),
None => Ok(false),
}
}
pub fn validate_implementations(&self) -> Result<(), LispError> {
let mut names = HashSet::new();
for implementation in &self.implementations {
for method in implementation.methods() {
let name = method.name().to_ascii_uppercase();
if !names.insert(name.clone()) {
return Err(LispError::RuntimeError(format!(
"Duplicate Lisp method: {name}"
)));
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(dead_code)]
struct Transform {
x: f32,
y: f32,
}
#[test]
fn name() {
let class = Class::<Transform>::new("transform");
assert_eq!(class.name(), "transform");
}
#[test]
fn class_properties() {
let class = Class::<Transform>::new("transform")
.property(Property::new("x"))
.property(Property::new("y"));
assert_eq!(class.name(), "transform");
assert_eq!(class.properties().len(), 2);
assert_eq!(class.properties()[0].name(), "x");
assert_eq!(class.properties()[1].name(), "y");
}
#[test]
fn class_implementations() {
let class = Class::<Transform>::new("transform")
.methods(Methods::new())
.methods(Methods::new());
assert_eq!(class.implementations().len(), 2);
}
#[test]
fn class_without_implementations() {
let class = Class::<Transform>::new("transform");
assert!(class.implementations().is_empty());
}
#[test]
fn consume_implementations() {
let class = Class::<Transform>::new("transform").methods(Methods::new());
let implementations = class.into_implementations();
assert_eq!(implementations.len(), 1);
}
}