geam 0.1.1

Experimental Rust-embedded execution runtime for typed Gleam programs
use super::JsonProvider;
use crate::gleam_json::GleamJsonHostProfile;
use crate::gleam_json::schema::{Json, ObjectEntry};
use crate::gleam_json::storage::JsonPayload;
use crate::gleam_stdlib::{StoredStringTree, StringTree, StringTreePayload};
use crate::{HostCall, HostCallCompletion, HostCallError, HostExternal, HostFailure, HostList};
use ecow::EcoString;
use num_bigint::BigInt;

pub(in crate::gleam_json) fn do_to_string<'call, Profile>(
    call: HostCall<'call, Profile, JsonProvider<Profile>, EcoString>,
    json: HostExternal<'call, Json>,
) -> Result<HostCallCompletion<'call, EcoString>, HostCallError>
where
    Profile: GleamJsonHostProfile,
{
    let text = call.external_payload(json).tree.flatten();
    Ok(call.return_value(text))
}

pub(in crate::gleam_json) fn to_string_tree<'call, Profile>(
    mut call: HostCall<'call, Profile, JsonProvider<Profile>, StringTree>,
    json: HostExternal<'call, Json>,
) -> Result<HostCallCompletion<'call, StringTree>, HostCallError>
where
    Profile: GleamJsonHostProfile,
{
    let tree = call.external_payload(json).tree.clone();
    let tree = call.create_external(StringTreePayload { tree });
    Ok(call.return_value(tree))
}

pub(in crate::gleam_json) fn do_string<'call, Profile>(
    mut call: HostCall<'call, Profile, JsonProvider<Profile>, Json>,
    value: EcoString,
) -> Result<HostCallCompletion<'call, Json>, HostCallError>
where
    Profile: GleamJsonHostProfile,
{
    let json = call.create_external(JsonPayload {
        tree: StoredStringTree::text(encode_string(&value)),
    });
    Ok(call.return_value(json))
}

pub(in crate::gleam_json) fn do_bool<'call, Profile>(
    mut call: HostCall<'call, Profile, JsonProvider<Profile>, Json>,
    value: bool,
) -> Result<HostCallCompletion<'call, Json>, HostCallError>
where
    Profile: GleamJsonHostProfile,
{
    let text = if value { "true" } else { "false" };
    let json = call.create_external(JsonPayload {
        tree: StoredStringTree::text(text.into()),
    });
    Ok(call.return_value(json))
}

pub(in crate::gleam_json) fn do_int<'call, Profile>(
    mut call: HostCall<'call, Profile, JsonProvider<Profile>, Json>,
    value: BigInt,
) -> Result<HostCallCompletion<'call, Json>, HostCallError>
where
    Profile: GleamJsonHostProfile,
{
    let json = call.create_external(JsonPayload {
        tree: StoredStringTree::text(value.to_string().into()),
    });
    Ok(call.return_value(json))
}

pub(in crate::gleam_json) fn do_float<'call, Profile>(
    mut call: HostCall<'call, Profile, JsonProvider<Profile>, Json>,
    value: f64,
) -> Result<HostCallCompletion<'call, Json>, HostCallError>
where
    Profile: GleamJsonHostProfile,
{
    if !value.is_finite() {
        return Err(HostFailure::new("JSON cannot encode a non-finite Float").into());
    }
    let json = call.create_external(JsonPayload {
        tree: StoredStringTree::text(encode_float(value).into()),
    });
    Ok(call.return_value(json))
}

pub(in crate::gleam_json) fn do_null<'call, Profile>(
    mut call: HostCall<'call, Profile, JsonProvider<Profile>, Json>,
) -> Result<HostCallCompletion<'call, Json>, HostCallError>
where
    Profile: GleamJsonHostProfile,
{
    let json = call.create_external(JsonPayload {
        tree: StoredStringTree::text("null".into()),
    });
    Ok(call.return_value(json))
}

pub(in crate::gleam_json) fn do_object<'call, Profile>(
    mut call: HostCall<'call, Profile, JsonProvider<Profile>, Json>,
    entries: HostList<'call, ObjectEntry>,
) -> Result<HostCallCompletion<'call, Json>, HostCallError>
where
    Profile: GleamJsonHostProfile,
{
    let mut index = 0;
    let mut trees = vec![StoredStringTree::text("{".into())];
    while let Some(entry) = call.list_item::<ObjectEntry>(entries, index) {
        let (key, (value, ())) = call.tuple_values(entry);
        if index != 0 {
            trees.push(StoredStringTree::text(",".into()));
        }
        trees.push(StoredStringTree::text(encode_string(&key)));
        trees.push(StoredStringTree::text(":".into()));
        trees.push(call.external_payload(value).tree.clone());
        index += 1;
    }
    trees.push(StoredStringTree::text("}".into()));
    let json = call.create_external(JsonPayload {
        tree: StoredStringTree::sequence(trees),
    });
    Ok(call.return_value(json))
}

pub(in crate::gleam_json) fn do_preprocessed_array<'call, Profile>(
    mut call: HostCall<'call, Profile, JsonProvider<Profile>, Json>,
    values: HostList<'call, Json>,
) -> Result<HostCallCompletion<'call, Json>, HostCallError>
where
    Profile: GleamJsonHostProfile,
{
    let mut index = 0;
    let mut trees = vec![StoredStringTree::text("[".into())];
    while let Some(value) = call.list_item(values, index) {
        if index != 0 {
            trees.push(StoredStringTree::text(",".into()));
        }
        trees.push(call.external_payload(value).tree.clone());
        index += 1;
    }
    trees.push(StoredStringTree::text("]".into()));
    let json = call.create_external(JsonPayload {
        tree: StoredStringTree::sequence(trees),
    });
    Ok(call.return_value(json))
}

fn encode_string(value: &str) -> EcoString {
    let mut output = String::with_capacity(value.len() + 2);
    output.push('"');
    for character in value.chars() {
        match character {
            '"' => output.push_str("\\\""),
            '\\' => output.push_str("\\\\"),
            '\u{0008}' => output.push_str("\\b"),
            '\t' => output.push_str("\\t"),
            '\n' => output.push_str("\\n"),
            '\u{000c}' => output.push_str("\\f"),
            '\r' => output.push_str("\\r"),
            character if character <= '\u{001f}' => {
                push_control_escape(&mut output, character);
            }
            character => output.push(character),
        }
    }
    output.push('"');
    output.into()
}

fn push_control_escape(output: &mut String, character: char) {
    const HEX: &[u8; 16] = b"0123456789ABCDEF";
    let value = u32::from(character) as usize;
    output.push_str("\\u00");
    output.push(char::from(HEX[(value >> 4) & 0x0f]));
    output.push(char::from(HEX[value & 0x0f]));
}

fn encode_float(value: f64) -> String {
    let mut buffer = ryu::Buffer::new();
    let encoded = buffer.format_finite(value);
    let Some(exponent_index) = encoded.find('e') else {
        return encoded.to_owned();
    };
    let (mantissa, exponent) = encoded.split_at(exponent_index);
    if mantissa.contains('.') {
        encoded.to_owned()
    } else {
        format!("{mantissa}.0{exponent}")
    }
}

#[cfg(test)]
mod tests {
    use super::{encode_float, encode_string};

    #[test]
    fn encodes_strings_with_the_otp_json_escape_set() {
        assert_eq!(
            encode_string("\"\\/\u{0008}\t\n\u{000c}\r\u{0000}\u{001f}é"),
            r#""\"\\/\b\t\n\f\r\u0000\u001Fé""#,
        );
    }

    #[test]
    fn normalizes_finite_float_exponents_to_the_otp_shape() {
        assert_eq!(encode_float(1.0), "1.0");
        assert_eq!(encode_float(-0.0), "-0.0");
        assert_eq!(encode_float(1.0e20), "1.0e20");
        assert_eq!(encode_float(1.0e-7), "1.0e-7");
        assert_eq!(encode_float(1.25e20), "1.25e20");
    }
}