geam-json 0.2.1

Official Gleam JSON providers for Geam
Documentation
mod decode;
mod encode;

pub(super) use provider::__GeamStores as Stores;

use crate::{Component, GleamJsonHostProfile};

#[geam_macros::module(
    path = "gleam/json",
    crate_path = geam_core,
    profile = crate::GleamJsonHostProfile,
    component = crate::Component,
    stores = crate::provider_stores,
)]
pub(super) mod provider {
    use super::{decode, encode};
    use crate::BitArrayValue;
    use ecow::EcoString;
    use geam_core::provider::{ExternalPayload, HostResult};
    use geam_stdlib::provider_support::StoredStringTree;
    use num_bigint::BigInt;

    #[geam_macros::external(name = "Json", manual)]
    pub struct JsonPayload {
        tree: StoredStringTree,
    }

    impl JsonPayload {
        pub(crate) fn from_tree(tree: StoredStringTree) -> Self {
            Self { tree }
        }

        pub(crate) fn tree(&self) -> &StoredStringTree {
            &self.tree
        }
    }

    impl ExternalPayload for JsonPayload {
        fn source_equal(&self, other: &Self) -> bool {
            self.tree.structurally_equal(&other.tree)
        }

        fn source_hash(&self) -> u64 {
            self.tree.structural_hash()
        }

        fn inspect(&self) -> EcoString {
            format!("{:?}", self.tree.flatten()).into()
        }
    }

    #[geam_macros::custom]
    #[allow(dead_code)]
    pub(crate) enum DecodeError {
        UnexpectedEndOfInput,
        UnexpectedByte(EcoString),
        UnexpectedSequence(EcoString),
        UnableToDecode(Vec<geam_stdlib::provider_support::DynamicDecodeErrorValue>),
    }

    #[geam_macros::function]
    fn decode_to_dynamic(json: BitArrayValue) -> decode::DecodeOutput {
        decode::decode_to_dynamic(json)
    }

    #[geam_macros::function]
    fn do_to_string(json: &JsonPayload) -> EcoString {
        encode::do_to_string(json)
    }

    #[geam_macros::function]
    fn to_string_tree(json: &JsonPayload) -> geam_stdlib::provider_support::StringTreePayload {
        encode::to_string_tree(json)
    }

    #[geam_macros::function]
    fn do_string(value: EcoString) -> JsonPayload {
        encode::do_string(value)
    }

    #[geam_macros::function]
    fn do_bool(value: bool) -> JsonPayload {
        encode::do_bool(value)
    }

    #[geam_macros::function]
    fn do_int(value: BigInt) -> JsonPayload {
        encode::do_int(value)
    }

    #[geam_macros::function]
    fn do_float(value: f64) -> HostResult<JsonPayload> {
        encode::do_float(value)
    }

    #[geam_macros::function]
    fn do_null() -> JsonPayload {
        encode::do_null()
    }

    #[geam_macros::function]
    fn do_object(entries: geam_core::provider::List<(EcoString, JsonPayload)>) -> JsonPayload {
        let mut index = 0;
        let mut trees = vec![StoredStringTree::text("{".into())];
        while let Some((key, value)) = entries.get(index) {
            if index != 0 {
                trees.push(StoredStringTree::text(",".into()));
            }
            trees.push(StoredStringTree::text(encode::encode_string(&key)));
            trees.push(StoredStringTree::text(":".into()));
            trees.push(value.tree().clone());
            index += 1;
        }
        trees.push(StoredStringTree::text("}".into()));
        JsonPayload::from_tree(StoredStringTree::sequence(trees))
    }

    #[geam_macros::function]
    fn do_preprocessed_array(values: geam_core::provider::List<JsonPayload>) -> JsonPayload {
        let mut index = 0;
        let mut trees = vec![StoredStringTree::text("[".into())];
        while let Some(value) = values.get(index) {
            if index != 0 {
                trees.push(StoredStringTree::text(",".into()));
            }
            trees.push(value.tree().clone());
            index += 1;
        }
        trees.push(StoredStringTree::text("]".into()));
        JsonPayload::from_tree(StoredStringTree::sequence(trees))
    }
}

pub(super) fn host_provider<Profile>()
-> Result<crate::HostProviderModule<Profile>, crate::HostRegistrationError>
where
    Profile: GleamJsonHostProfile,
{
    provider::__geam_module::<Profile>()
}

#[cfg(test)]
type Json = crate::HostExternalType<provider::__GeamExternalSchema0>;

#[cfg(test)]
impl
    crate::HostExternalBinding<
        crate::GleamJsonProfile,
        geam_stdlib::provider_support::DynamicSchema,
    > for provider::__GeamProvider
{
    type Storage = geam_stdlib::provider_support::DynamicExternalStorage;
}

#[cfg(test)]
impl crate::HostExternalBinding<crate::GleamJsonProfile, geam_stdlib::provider_support::DictSchema>
    for provider::__GeamProvider
{
    type Storage = geam_stdlib::provider_support::DictExternalStorage;
}

#[cfg(test)]
impl
    crate::HostExternalBinding<
        crate::GleamJsonProfile,
        geam_stdlib::provider_support::StringTreeSchema,
    > for provider::__GeamProvider
{
    type Storage = geam_stdlib::provider_support::StringTreeExternalStorage;
}

#[cfg(test)]
fn json_source_hash<'call>(
    call: crate::HostCall<
        'call,
        crate::GleamJsonProfile,
        provider::__GeamProvider,
        num_bigint::BigInt,
    >,
    json: crate::HostExternal<'call, Json>,
) -> Result<crate::HostCallCompletion<'call, num_bigint::BigInt>, crate::HostCallError> {
    let hash = num_bigint::BigInt::from(call.source_hash::<Json>(json));
    Ok(call.return_value(hash))
}

#[cfg(test)]
pub(super) fn test_host_providers() -> [crate::HostProviderModule<crate::GleamJsonProfile>; 4] {
    use geam_stdlib::provider_support::{DictSchema, DynamicSchema, StringTreeSchema};

    let json = host_provider::<crate::GleamJsonProfile>()
        .expect("official JSON provider should register")
        .with_scoped_function::<provider::__GeamProvider, (Json,), num_bigint::BigInt, _>(
            "test_source_hash",
            json_source_hash,
        )
        .expect("synthetic JSON hash function should register");

    [
        crate::HostProviderModule::new("gleam_stdlib", "gleam/dynamic")
            .and_then(crate::HostProviderModule::with_external_type::<
                provider::__GeamProvider,
                DynamicSchema,
            >)
            .expect("synthetic Dynamic declaration should register"),
        crate::HostProviderModule::new("gleam_stdlib", "gleam/dict")
            .and_then(crate::HostProviderModule::with_external_type::<
                provider::__GeamProvider,
                DictSchema,
            >)
            .expect("synthetic Dict declaration should register"),
        crate::HostProviderModule::new("gleam_stdlib", "gleam/string_tree")
            .and_then(crate::HostProviderModule::with_external_type::<
                provider::__GeamProvider,
                StringTreeSchema,
            >)
            .expect("synthetic StringTree declaration should register"),
        json,
    ]
}

#[cfg(test)]
mod tests {
    use super::provider::{
        __GeamExternalSchema0 as JsonSchema, __GeamExternalStorage0 as JsonStorage,
        __GeamProvider as JsonProvider, JsonPayload,
    };
    use crate::test_support::{CustomProfile, CustomStores, execution, run_state};
    use crate::{GleamJsonProfile, GleamJsonProfileStores, HostExternalStorage, HostProvider};
    use geam_core::provider::ExternalPayload;
    use geam_stdlib::provider_support::StoredStringTree;

    #[test]
    fn projects_only_the_json_component_state() {
        let mut state = run_state([0; 32]);
        let json = &mut state.json as *mut ();
        let projected = <JsonProvider as HostProvider<GleamJsonProfile>>::project(&mut state);

        assert!(std::ptr::eq(projected, json));
    }

    #[test]
    fn json_payload_owns_structural_source_semantics_and_canonical_inspection() {
        let segmented = JsonPayload::from_tree(StoredStringTree::sequence([
            StoredStringTree::text("[".into()),
            StoredStringTree::text("1".into()),
            StoredStringTree::text("]".into()),
        ]));
        let same = JsonPayload::from_tree(StoredStringTree::sequence([
            StoredStringTree::text("[".into()),
            StoredStringTree::text("1".into()),
            StoredStringTree::text("]".into()),
        ]));
        let flat = JsonPayload::from_tree(StoredStringTree::text("[1]".into()));

        assert!(segmented.source_equal(&same));
        assert!(!segmented.source_equal(&flat));
        assert_eq!(segmented.source_hash(), same.source_hash());
        assert_ne!(segmented.source_hash(), flat.source_hash());
        assert_eq!(segmented.inspect(), r#""[1]""#);

        let stores = GleamJsonProfileStores::default();
        let first =
            <JsonStorage as HostExternalStorage<GleamJsonProfile, JsonSchema>>::store(&stores);
        let repeated =
            <JsonStorage as HostExternalStorage<GleamJsonProfile, JsonSchema>>::store(&stores);
        assert!(std::ptr::eq(first, repeated));

        let custom = CustomStores::default();
        let custom =
            <JsonStorage as HostExternalStorage<CustomProfile, JsonSchema>>::store(&custom);
        assert!(!std::ptr::eq(first, custom));
    }

    #[test]
    fn escaped_json_remains_self_contained_after_execution_and_state_drop() {
        let value = {
            let execution = execution(
                r#"
pub fn main() {
  do_object([#("items", do_preprocessed_array([do_int(1), do_int(2)]))])
}
"#,
            );
            let mut state = run_state([0; 32]);
            let value = execution
                .run_main(&mut state, &mut Vec::new())
                .expect("JSON value should escape the run");
            drop(state);
            drop(execution);
            value
        };

        assert_eq!(value.inspect().to_string(), r#""{\"items\":[1,2]}""#);
        assert_eq!(value.clone(), value);
    }

    #[test]
    fn json_values_use_structural_hashing_as_dict_keys() {
        let execution = execution(
            r#"
pub fn main() {
  let segmented = do_preprocessed_array([do_int(1)])
  let same = do_preprocessed_array([do_int(1)])
  assert segmented == same
  assert segmented != do_int(1)
  assert test_source_hash(segmented) == test_source_hash(same)
  segmented
}
"#,
        );
        let value = execution
            .run_main(&mut run_state([0; 32]), &mut Vec::new())
            .expect("JSON values should expose their structural source hash");

        assert_eq!(value.inspect().to_string(), r#""[1]""#);
    }

    #[test]
    fn repeated_execution_is_independent_of_the_caller_owned_run_state() {
        let execution = execution(
            r#"
pub fn main() {
  do_object([#("items", do_preprocessed_array([do_int(1), do_int(2)]))])
}
"#,
        );
        let mut first_state = run_state([1; 32]);
        let mut second_state = run_state([2; 32]);

        let first = execution
            .run_main(&mut first_state, &mut Vec::new())
            .expect("first JSON execution should run");
        let repeated = execution
            .run_main(&mut first_state, &mut Vec::new())
            .expect("repeated JSON execution should run");
        let independent = execution
            .run_main(&mut second_state, &mut Vec::new())
            .expect("independent JSON execution should run");

        let first_inspection = first.inspect().to_string();
        assert_ne!(first, repeated);
        assert_ne!(first, independent);
        assert_eq!(first_inspection, repeated.inspect().to_string());
        assert_eq!(first_inspection, independent.inspect().to_string());
        assert_eq!(first_inspection, r#""{\"items\":[1,2]}""#);
    }
}