afia-component 0.0.4

A high-level Rust wrapper for `libafia_component`.
Documentation
//! Types related to component inputs.
//!
//! NOTE: There is an open issue that changes how component inputs work.
//!  `Remove __afia__$allocate` https://github.com/afia/afia/issues/2519
//!  The current plan is that WebAssembly imports such as `fn _afia_get_color_inputs()` will be used
//!  to retrieve component inputs.
//!  So, it's possible that `ComponentInputs` ends up getting deleted and then the behavior gets
//!  moved into `ComponentImports`. I have not thought about this. For now I am just implementing
//!  abstractions on top of the current system (where we have an `inputs_ptr: *const u8`, while
//!  still trying to ensure that the pieces of the design will be easy to move around to other
//!  places if needed.
//!  See the `Components` chapter in the internal book for more information about component inputs.

use std::ops::RangeInclusive;
use std::{ptr, slice};

use crate::_macro::concat_byte_slices;
use crate::{ComponentOutputArgs, FromComponentOutputArgs};

pub mod mock;

/// Used to retrieve the component's inputs.
pub struct ComponentInputs {
    /// An arbitrary context object used when determining how to retrieve the component input.
    pub ctx: *mut std::ffi::c_void,
    /// Free the context object.
    pub free_ctx: fn(ctx: *mut std::ffi::c_void),
    /// Get one of the component's text inputs.
    pub text: fn(ctx: *mut std::ffi::c_void, input_id: TextInputId) -> String,
}

impl Drop for ComponentInputs {
    fn drop(&mut self) {
        (self.free_ctx)(self.ctx)
    }
}

impl FromComponentOutputArgs for ComponentInputs {
    fn from_args(args: ComponentOutputArgs) -> Self {
        Self::new(args.inputs_ptr)
    }
}

/// Describes one of a component's inputs.
pub struct ComponentInput {
    /// The input's ID.
    pub input_id: u16,
    /// The input's type, such as Text or Color.
    pub ty: ValueType,
    /// The name for this input.
    pub name: &'static str,
    /// The input's description.
    pub description: &'static str,
}

/// Describes a component input's type, such as Text or Color.
pub enum ValueType {
    /// Describes a textual component input.
    Text {
        /// The minimum and maximum length allowed for values connected to this text input.
        length: RangeInclusive<u32>,
    },
}
impl ValueType {
    // Used by the `define_*_input` macros when encoding the input descriptor to binary.
    #[doc(hidden)]
    pub const fn encode_text_bytes(&self) -> [u8; 10] {
        let encoding_version = 0;
        let text_header = 12;

        match self {
            ValueType::Text { length } => concat_byte_slices::<10>(&[
                &[encoding_version, text_header],
                &length.start().to_le_bytes(),
                &length.end().to_le_bytes(),
            ]),
        }
    }
}

/// Used to retrieve one of the component's text inputs.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct TextInputId(u16);
impl TextInputId {
    // Used by the `define_text_input` macro.
    #[doc(hidden)]
    pub const fn _private_new(id: u16) -> Self {
        Self(id)
    }
}

impl ComponentInputs {
    /// Create a `ComponentInputs` that can read inputs from the inputs pointer that Afia provides.
    pub fn new(inputs_ptr: *const u8) -> Self {
        Self {
            ctx: inputs_ptr as *mut std::ffi::c_void,
            free_ctx: |_inputs_ptr| {},
            text: |ctx, _input_id| {
                // This logic is untested because we are planning to move away from copying all
                //  inputs from Afia to the component and as we are currently, and instead letting
                //  the component retrieve its inputs by calling functions such as
                //  `__afia__$get_text_input(123, ...)`.
                //  So, until then, not bothering to test this `inputs_ptr` decoding code since we
                //  are moving away from using `inputs_ptr`s.
                //  This transition work is happening in the issue.
                //  "Remove __afia__$allocate from site component ABI"
                //  https://github.com/afia/afia/issues/2519

                let inputs_ptr: *const u8 = ctx.cast();

                // Skip the 3 leading bytes in the `MaybeValue`
                // 1 byte version
                // 1 byte `MaybeValue::Value` variant
                // 1 byte `Value::Text` variant
                let text_len_ptr = unsafe { inputs_ptr.add(3) };

                // Decode the 4 length bytes.
                let text_len = {
                    let text_len_ptr: *const u32 = text_len_ptr.cast();
                    unsafe { ptr::read_unaligned(text_len_ptr) }
                };

                // Skip the 4 text length bytes.
                let text_ptr = unsafe { text_len_ptr.add(4) };

                // Read the text bytes.
                let text_bytes = unsafe { slice::from_raw_parts(text_ptr, text_len as usize) };
                // SAFETY: Safe if the Afia host application is properly implemented and always
                // passes us valid UTF-8 text.
                let text_bytes = unsafe { str::from_utf8_unchecked(text_bytes) };

                // TODO: Think through / experiment with avoiding this allocation.
                //  We are currently returning a `-> String`. We should explore ways to instead
                //  make use of a `&str`.
                //  Holding off on this because there is already work underway to change how we
                //  retrieve component inputs.
                //  See: `Remove __afia__$allocate` https://github.com/afia/afia/issues/2519 for
                //  the issue where we are changing how component inputs work.
                //  So, rather than try to figure out the best way to make use of buffers and
                //  designs that allow users to avoid allocating, we are saving that work for after
                //  we figure out our patterns for how guest components to retrieve inputs from the
                //  Afia host.
                text_bytes.to_owned()
            },
        }
    }

    /// Get the text input's value.
    pub fn get_text(&self, text_id: TextInputId) -> String {
        (self.text)(self.ctx, text_id)
    }
}

/// Define a text input for the component.
///
/// ```
/// # use afia_component::input::{ComponentInput, TextInputId, ValueType};
/// # use afia_component::define_text_input;
///
/// const MY_TEXT_INPUT: ComponentInput = ComponentInput {
///     input_id: 123,
///     ty: ValueType::Text { length: 0..=40 },
///     name: "My Text",
///     description: "Text that will be shown inside the paragraph.",
/// };
/// const MY_TEXT_INPUT_ID: TextInputId = define_text_input!(MY_TEXT_INPUT);
/// ```
#[macro_export]
macro_rules! define_text_input {
    ($definition:path) => {
        {
            #[cfg_attr(not(target_arch = "wasm32"), expect(unused, reason = "Rust does not consider this as used for some reason."))]
            const BYTE_COUNT: usize =
              // Version byte count
              1
              // ComponentInputID bytes
              + 2
              // ValueType::Text bytes (version byte, text variant byte, min u32, max u32)
              + 10
              // Name len byte + name bytes
              + 1 + $definition.name.as_bytes().len()
              // Description len bytes + description bytes
              + 2 + $definition.description.as_bytes().len()
            ;

            #[cfg_attr(target_arch = "wasm32", used)]
            #[cfg_attr(target_arch = "wasm32", unsafe(link_section = "__afia__$input_descriptors"))]
            static _AFIA_INPUT_DESCRIPTOR: [u8; BYTE_COUNT] =
                afia_component::_macro::concat_byte_slices(&[
                    &[0],
                    &$definition.input_id.to_le_bytes(),
                    &$definition.ty.encode_text_bytes(),
                    &[$definition.name.len() as u8],
                    $definition.name.as_bytes(),
                    &($definition.description.len() as u16).to_le_bytes(),
                    $definition.description.as_bytes(),
                ]);

            const _ASSERT_IS_COMPONENT_INPUT: $crate::input::ComponentInput = $definition;

            const _ASSERT_INPUT_TYPE_IS_TEXT: () = {
                assert!(matches!($definition.ty, $crate::input::ValueType::Text { .. }));
            };

            $crate::input::TextInputId::_private_new($definition.input_id)
        }
    };
}