eclisp 0.2.0

Bindings for Embeddable Common Lisp
use eclisp::{
    Lisp, LispContext, LispError,
    clos::{Class, ObjectId, ObjectRegistry, Property},
};

struct Transform {
    x: f32,
    name: String,
}

fn test_property_getters(ctx: &LispContext<'_>) -> Result<(), LispError> {
    let transform = Transform {
        x: 42.0,
        name: "player".into(),
    };
    let x = Property::new("x").get_owned(|obj: &Transform| obj.x);
    let name = Property::new("name").get(|obj: &Transform| &obj.name);

    assert_eq!(x.read(&transform, ctx)?.unwrap().try_as_f64()?, 42.0);
    assert_eq!(
        name.read(&transform, ctx)?.unwrap().try_as_string()?,
        "player"
    );

    Ok(())
}

fn test_property_setters(ctx: &LispContext<'_>) -> Result<(), LispError> {
    let property = Property::new("x")
        .get(|obj: &Transform| &obj.x)
        .set(|obj: &mut Transform, x: f32| obj.x = x);

    let mut transform = Transform {
        x: 10.0,
        name: "player".into(),
    };

    let value = ctx.float(42.0)?;

    assert!(property.write(&mut transform, value.as_ref())?);
    assert_eq!(transform.x, 42.0);

    let result = property.read(&transform, ctx)?.unwrap();
    assert_eq!(result.try_as_f64()?, 42.0);

    Ok(())
}

fn test_class(ctx: &LispContext<'_>) -> Result<(), LispError> {
    let class = Class::<Transform>::new("transform")
        .property_with("x", |p| p.get(|obj| &obj.x).set(|obj, x: f32| obj.x = x))
        .property_with("name", |p| p.get(|obj| &obj.name));

    let mut transform = Transform {
        x: 10.0,
        name: "player".into(),
    };

    let value = class.read("x", &transform, ctx)?.unwrap();
    assert_eq!(value.try_as_f64()?, 10.0);

    let value = ctx.float(42.0)?;
    assert!(class.write("x", &mut transform, value.as_ref())?);
    assert_eq!(transform.x, 42.0);

    let value = class.read("name", &transform, ctx)?.unwrap();
    assert_eq!(value.try_as_string()?, "player");

    assert!(class.find_property("missing").is_none());

    Ok(())
}

fn test_registry(ctx: &LispContext<'_>) -> Result<(), LispError> {
    let class = Class::<Transform>::new("transform")
        .property_with("x", |p| p.get(|obj| &obj.x).set(|obj, x: f32| obj.x = x))
        .property_with("name", |p| p.get(|obj| &obj.name));

    let mut registry = ObjectRegistry::default();

    let id = registry.insert(Transform {
        x: 10.0,
        name: "player".into(),
    });

    let value = registry.read(id, &class, "x", ctx)?.unwrap();
    assert_eq!(value.try_as_f64()?, 10.0);

    let value = ctx.float(42.0)?;

    assert!(registry.write(id, &class, "x", value.as_ref())?);
    assert_eq!(registry.get::<Transform>(id).unwrap().x, 42.0);

    Ok(())
}

fn test_expose(ctx: &LispContext<'_>) -> Result<(), LispError> {
    let id = ctx.expose(Transform {
        x: 10.0,
        name: "player".into(),
    });

    let x = ctx.with_object::<Transform, _>(id, |obj| obj.x);

    assert_eq!(x, Some(10.0));

    ctx.with_object_mut::<Transform, _>(id, |obj| {
        obj.x = 42.0;
    });

    let x = ctx.with_object::<Transform, _>(id, |obj| obj.x);

    assert_eq!(x, Some(42.0));

    Ok(())
}

fn test_object_id(ctx: &LispContext<'_>) -> Result<(), LispError> {
    let id = ctx.expose(Transform {
        x: 10.0,
        name: "player".into(),
    });

    let value = id.to_lisp(ctx)?;
    let restored = ObjectId::from_lisp(value.as_ref())?;

    assert_eq!(id, restored);

    let invalid = ctx.eval("'(1 2 3)")?;
    assert!(ObjectId::from_lisp(invalid.as_ref()).is_err());

    let invalid = ctx.eval("'(-1 0)")?;
    assert!(ObjectId::from_lisp(invalid.as_ref()).is_err());

    Ok(())
}

fn test_register_class_manual(ctx: &LispContext<'_>) -> Result<(), LispError> {
    let first = ctx.expose(Transform {
        x: 10.0,
        name: "first".into(),
    });

    let second = ctx.expose(Transform {
        x: 20.0,
        name: "second".into(),
    });

    ctx.defun("transform-x", |ctx, args| {
        if args.len() != 1 {
            return Err(LispError::RuntimeError("Expected one object".into()));
        }

        let id = ObjectId::from_lisp(args[0])?;

        let x = ctx
            .with_object::<Transform, _>(id, |obj| obj.x)
            .ok_or_else(|| LispError::RuntimeError("Invalid Transform".into()))?;

        ctx.float(x as f64)
    })?;

    let first = first.to_lisp(ctx)?;
    let second = second.to_lisp(ctx)?;

    let first_result = ctx.call("transform-x", &[&first])?;
    let second_result = ctx.call("transform-x", &[&second])?;

    assert_eq!(first_result.try_as_f64()?, 10.0);
    assert_eq!(second_result.try_as_f64()?, 20.0);

    Ok(())
}

fn run(name: &str, lisp: &LispContext<'_>, test: fn(&LispContext<'_>) -> Result<(), LispError>) {
    print!("test {name} ... ");

    match test(lisp) {
        Ok(()) => println!("ok"),
        Err(error) => panic!("{name} failed: {error}"),
    }
}

fn main() -> Result<(), LispError> {
    let lisp = Lisp::new()?;

    lisp.enter(|ctx| {
        run("property_getters", &ctx, test_property_getters);
        run("property_setters", &ctx, test_property_setters);
        run("class", &ctx, test_class);
        run("registry", &ctx, test_registry);
        run("expose", &ctx, test_expose);
        run("object_id", &ctx, test_object_id);
        run("register_class_manual", &ctx, test_register_class_manual);
    });

    Ok(())
}