eclisp 0.2.0-beta.0

Bindings for Embeddable Common Lisp
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);
    }
}