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}