Skip to main content

afia_component/
input.rs

1//! Types related to component inputs.
2//!
3//! NOTE: There is an open issue that changes how component inputs work.
4//!  `Remove __afia__$allocate` https://github.com/afia/afia/issues/2519
5//!  The current plan is that WebAssembly imports such as `fn _afia_get_color_inputs()` will be used
6//!  to retrieve component inputs.
7//!  So, it's possible that `ComponentInputs` ends up getting deleted and then the behavior gets
8//!  moved into `ComponentImports`. I have not thought about this. For now I am just implementing
9//!  abstractions on top of the current system (where we have an `inputs_ptr: *const u8`, while
10//!  still trying to ensure that the pieces of the design will be easy to move around to other
11//!  places if needed.
12//!  See the `Components` chapter in the internal book for more information about component inputs.
13
14use std::ops::RangeInclusive;
15use std::{ptr, slice};
16
17use crate::_macro::concat_byte_slices;
18use crate::{ComponentOutputArgs, FromComponentOutputArgs};
19
20pub mod mock;
21
22/// Used to retrieve the component's inputs.
23pub struct ComponentInputs {
24    /// An arbitrary context object used when determining how to retrieve the component input.
25    pub ctx: *mut std::ffi::c_void,
26    /// Free the context object.
27    pub free_ctx: fn(ctx: *mut std::ffi::c_void),
28    /// Get one of the component's text inputs.
29    pub text: fn(ctx: *mut std::ffi::c_void, input_id: TextInputId) -> String,
30}
31
32impl Drop for ComponentInputs {
33    fn drop(&mut self) {
34        (self.free_ctx)(self.ctx)
35    }
36}
37
38impl FromComponentOutputArgs for ComponentInputs {
39    fn from_args(args: ComponentOutputArgs) -> Self {
40        Self::new(args.inputs_ptr)
41    }
42}
43
44/// Describes one of a component's inputs.
45pub struct ComponentInput {
46    /// The input's ID.
47    pub input_id: u16,
48    /// The input's type, such as Text or Color.
49    pub ty: ValueType,
50    /// The name for this input.
51    pub name: &'static str,
52    /// The input's description.
53    pub description: &'static str,
54}
55
56/// Describes a component input's type, such as Text or Color.
57pub enum ValueType {
58    /// Describes a textual component input.
59    Text {
60        /// The minimum and maximum length allowed for values connected to this text input.
61        length: RangeInclusive<u32>,
62    },
63}
64impl ValueType {
65    // Used by the `define_*_input` macros when encoding the input descriptor to binary.
66    #[doc(hidden)]
67    pub const fn encode_text_bytes(&self) -> [u8; 10] {
68        let encoding_version = 0;
69        let text_header = 12;
70
71        match self {
72            ValueType::Text { length } => concat_byte_slices::<10>(&[
73                &[encoding_version, text_header],
74                &length.start().to_le_bytes(),
75                &length.end().to_le_bytes(),
76            ]),
77        }
78    }
79}
80
81/// Used to retrieve one of the component's text inputs.
82#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
83pub struct TextInputId(u16);
84impl TextInputId {
85    // Used by the `define_text_input` macro.
86    #[doc(hidden)]
87    pub const fn _private_new(id: u16) -> Self {
88        Self(id)
89    }
90}
91
92impl ComponentInputs {
93    /// Create a `ComponentInputs` that can read inputs from the inputs pointer that Afia provides.
94    pub fn new(inputs_ptr: *const u8) -> Self {
95        Self {
96            ctx: inputs_ptr as *mut std::ffi::c_void,
97            free_ctx: |_inputs_ptr| {},
98            text: |ctx, _input_id| {
99                // This logic is untested because we are planning to move away from copying all
100                //  inputs from Afia to the component and as we are currently, and instead letting
101                //  the component retrieve its inputs by calling functions such as
102                //  `__afia__$get_text_input(123, ...)`.
103                //  So, until then, not bothering to test this `inputs_ptr` decoding code since we
104                //  are moving away from using `inputs_ptr`s.
105                //  This transition work is happening in the issue.
106                //  "Remove __afia__$allocate from site component ABI"
107                //  https://github.com/afia/afia/issues/2519
108
109                let inputs_ptr: *const u8 = ctx.cast();
110
111                // Skip the 3 leading bytes in the `MaybeValue`
112                // 1 byte version
113                // 1 byte `MaybeValue::Value` variant
114                // 1 byte `Value::Text` variant
115                let text_len_ptr = unsafe { inputs_ptr.add(3) };
116
117                // Decode the 4 length bytes.
118                let text_len = {
119                    let text_len_ptr: *const u32 = text_len_ptr.cast();
120                    unsafe { ptr::read_unaligned(text_len_ptr) }
121                };
122
123                // Skip the 4 text length bytes.
124                let text_ptr = unsafe { text_len_ptr.add(4) };
125
126                // Read the text bytes.
127                let text_bytes = unsafe { slice::from_raw_parts(text_ptr, text_len as usize) };
128                // SAFETY: Safe if the Afia host application is properly implemented and always
129                // passes us valid UTF-8 text.
130                let text_bytes = unsafe { str::from_utf8_unchecked(text_bytes) };
131
132                // TODO: Think through / experiment with avoiding this allocation.
133                //  We are currently returning a `-> String`. We should explore ways to instead
134                //  make use of a `&str`.
135                //  Holding off on this because there is already work underway to change how we
136                //  retrieve component inputs.
137                //  See: `Remove __afia__$allocate` https://github.com/afia/afia/issues/2519 for
138                //  the issue where we are changing how component inputs work.
139                //  So, rather than try to figure out the best way to make use of buffers and
140                //  designs that allow users to avoid allocating, we are saving that work for after
141                //  we figure out our patterns for how guest components to retrieve inputs from the
142                //  Afia host.
143                text_bytes.to_owned()
144            },
145        }
146    }
147
148    /// Get the text input's value.
149    pub fn get_text(&self, text_id: TextInputId) -> String {
150        (self.text)(self.ctx, text_id)
151    }
152}
153
154/// Define a text input for the component.
155///
156/// ```
157/// # use afia_component::input::{ComponentInput, TextInputId, ValueType};
158/// # use afia_component::define_text_input;
159///
160/// const MY_TEXT_INPUT: ComponentInput = ComponentInput {
161///     input_id: 123,
162///     ty: ValueType::Text { length: 0..=40 },
163///     name: "My Text",
164///     description: "Text that will be shown inside the paragraph.",
165/// };
166/// const MY_TEXT_INPUT_ID: TextInputId = define_text_input!(MY_TEXT_INPUT);
167/// ```
168#[macro_export]
169macro_rules! define_text_input {
170    ($definition:path) => {
171        {
172            #[cfg_attr(not(target_arch = "wasm32"), expect(unused, reason = "Rust does not consider this as used for some reason."))]
173            const BYTE_COUNT: usize =
174              // Version byte count
175              1
176              // ComponentInputID bytes
177              + 2
178              // ValueType::Text bytes (version byte, text variant byte, min u32, max u32)
179              + 10
180              // Name len byte + name bytes
181              + 1 + $definition.name.as_bytes().len()
182              // Description len bytes + description bytes
183              + 2 + $definition.description.as_bytes().len()
184            ;
185
186            #[cfg_attr(target_arch = "wasm32", used)]
187            #[cfg_attr(target_arch = "wasm32", unsafe(link_section = "__afia__$input_descriptors"))]
188            static _AFIA_INPUT_DESCRIPTOR: [u8; BYTE_COUNT] =
189                afia_component::_macro::concat_byte_slices(&[
190                    &[0],
191                    &$definition.input_id.to_le_bytes(),
192                    &$definition.ty.encode_text_bytes(),
193                    &[$definition.name.len() as u8],
194                    $definition.name.as_bytes(),
195                    &($definition.description.len() as u16).to_le_bytes(),
196                    $definition.description.as_bytes(),
197                ]);
198
199            const _ASSERT_IS_COMPONENT_INPUT: $crate::input::ComponentInput = $definition;
200
201            const _ASSERT_INPUT_TYPE_IS_TEXT: () = {
202                assert!(matches!($definition.ty, $crate::input::ValueType::Text { .. }));
203            };
204
205            $crate::input::TextInputId::_private_new($definition.input_id)
206        }
207    };
208}