geam 0.1.2

Experimental Rust-embedded execution runtime for typed Gleam programs
use super::{GleamTimeProfile, TimeSource, host_providers};
use crate::{
    HostModule, HostProviderSet, HostedExecution, ModuleSource, PackageSource,
    compile_typed_host_program, plan_host_program,
};
use ecow::EcoString;
use std::collections::VecDeque;
use std::time::SystemTime;

pub(super) const CALENDAR_SOURCE: &str = r#"
@external(erlang, "gleam_time_ffi", "local_time_offset_seconds")
@external(javascript, "../../gleam_time_ffi.mjs", "local_time_offset_seconds")
fn local_time_offset_seconds() -> Int

pub fn current_offset() -> Int {
  local_time_offset_seconds()
}
"#;

pub(super) const TIMESTAMP_SOURCE: &str = r#"
@external(erlang, "gleam_time_ffi", "system_time")
@external(javascript, "../../gleam_time_ffi.mjs", "system_time")
fn get_system_time() -> #(Int, Int)

pub fn current_parts() -> #(Int, Int) {
  get_system_time()
}
"#;

#[derive(Default)]
pub(super) struct ScriptedSource {
    pub(super) times: VecDeque<Result<SystemTime, crate::HostFailure>>,
    pub(super) offsets: VecDeque<Result<i32, crate::HostFailure>>,
}

impl TimeSource for ScriptedSource {
    fn system_time(&mut self) -> Result<SystemTime, crate::HostFailure> {
        self.times
            .pop_front()
            .unwrap_or_else(|| Err(crate::HostFailure::new("scripted system time exhausted")))
    }

    fn local_offset_seconds(&mut self) -> Result<i32, crate::HostFailure> {
        self.offsets
            .pop_front()
            .unwrap_or_else(|| Err(crate::HostFailure::new("scripted local offset exhausted")))
    }
}

pub(super) fn execution<Source>(
    source: &str,
    root_module: &str,
) -> HostedExecution<GleamTimeProfile<Source>>
where
    Source: TimeSource,
{
    let modules = if root_module == "main" {
        vec![
            ModuleSource::new(
                "gleam/time/calendar",
                "src/gleam/time/calendar.gleam",
                CALENDAR_SOURCE,
            ),
            ModuleSource::new(
                "gleam/time/timestamp",
                "src/gleam/time/timestamp.gleam",
                TIMESTAMP_SOURCE,
            ),
            ModuleSource::new("main", "src/main.gleam", source),
        ]
    } else {
        vec![
            ModuleSource::new(
                "gleam/time/calendar",
                "src/gleam/time/calendar.gleam",
                if root_module == "gleam/time/calendar" {
                    source
                } else {
                    CALENDAR_SOURCE
                },
            ),
            ModuleSource::new(
                "gleam/time/timestamp",
                "src/gleam/time/timestamp.gleam",
                if root_module == "gleam/time/timestamp" {
                    source
                } else {
                    TIMESTAMP_SOURCE
                },
            ),
        ]
    };
    let providers = host_providers::<GleamTimeProfile<Source>>()
        .expect("synthetic Time providers should register");
    let hosts = HostProviderSet::with_providers(
        Vec::<HostModule<GleamTimeProfile<Source>>>::new(),
        providers,
    )
    .expect("synthetic Time provider modules should be unique");
    let typed = compile_typed_host_program(
        "gleam_time",
        root_module,
        [PackageSource::new(
            "gleam_time",
            Vec::<EcoString>::new(),
            modules,
        )],
        hosts,
    )
    .expect("synthetic Time source should compile");
    let plan = plan_host_program(typed).expect("synthetic Time source should plan");
    HostedExecution::try_from_module_plan(plan).expect("synthetic Time execution should seal")
}

#[cfg(test)]
mod tests {
    use super::ScriptedSource;
    use crate::gleam_time::TimeSource;

    #[test]
    fn scripted_source_reports_exhausted_effect_queues() {
        let mut source = ScriptedSource::default();

        assert_eq!(
            source
                .system_time()
                .expect_err("empty time queue should fail")
                .message(),
            "scripted system time exhausted",
        );
        assert_eq!(
            source
                .local_offset_seconds()
                .expect_err("empty offset queue should fail")
                .message(),
            "scripted local offset exhausted",
        );
    }
}