Skip to main content

arora_module_core/
lib.rs

1pub mod header;
2pub mod resolve;
3use arora_registry::{ModuleFrozen, ReadableRegistry, RegistryError, TypeDefinitionFrozen};
4use arora_types::module::high::ModuleDefinition;
5use arora_types::record::ty::FrozenTy;
6use arora_types::record::{module::frozen::Export, FrozenReference, Resolver};
7use arora_types::record::{RecordType, Selector};
8use arora_vfs::VfsError;
9use bytes::{Buf, BufMut};
10use derive_more::Display;
11use resolve::resolve_high_module;
12use semver::{Version, VersionReq};
13use serde::{de::DeserializeOwned, Deserialize, Serialize};
14use std::collections::HashSet;
15use std::fs::read_to_string;
16use std::path::Path;
17use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
18use uuid::Uuid;
19
20/// Analyzes a module from the path where it is written in the YAML format.
21/// See [`analyze_module`].
22pub async fn analyze_module_from_path<P: AsRef<Path>, R: ReadableRegistry + Resolver>(
23    path: P,
24    registry: &mut R,
25) -> Result<Vec<ModuleAsset>, ModuleDeclarationError> {
26    let module_yaml = read_to_string(path).map_err(ModuleDeclarationError::IoError)?;
27    let module_definition: ModuleDefinition =
28        serde_yaml::from_str(&module_yaml).map_err(ModuleDeclarationError::YAMLError)?;
29    analyze_module(module_definition, registry).await
30}
31
32/// Analyzes a module by reading its header and
33/// resolves its dependencies with the help of the provided registry.
34/// Produces a list of assets that can be used for code generation.
35/// First, the types, then the modules, then the imports.
36pub async fn analyze_module<R: ReadableRegistry + Resolver>(
37    module_definition: ModuleDefinition,
38    registry: &mut R,
39) -> Result<Vec<ModuleAsset>, ModuleDeclarationError> {
40    let module_id = module_definition.id;
41    let module_version = module_definition.version.clone();
42
43    // Resolve the module contents into a description compatible with the registry.
44    // It already includes the dependencies (internal and external) as references.
45    let executor_name = module_definition.executor.name.to_owned();
46    let resolved_module = resolve_high_module(module_definition, registry).await?;
47
48    // Collect the actual types behind the references. The module's declared
49    // dependencies only list the types referenced *directly* by its exports and
50    // imports; a `Track` export whose field is `Vec<Keypoint>` therefore names
51    // `Track` but not `Keypoint`, `Vec3`, or the enums those reach. We walk the
52    // reference graph so that every transitively-referenced structure and
53    // enumeration is emitted too — otherwise the generated code refers to
54    // modules for types that were never generated.
55    let mut assets = Vec::new();
56    let mut seen: HashSet<Uuid> = HashSet::new();
57    let mut pending: Vec<FrozenReference> = resolved_module.module.dependencies.to_vec();
58    while let Some(reference) = pending.pop() {
59        // `seen` also makes the walk cycle-safe: a self-referential type (a tree
60        // node holding `Vec<Self>`, say) is visited and emitted exactly once.
61        if !seen.insert(reference.id) {
62            continue;
63        }
64        // Well-known ids (primitives and the dynamic `Value`/`KeyValue`/
65        // `ArrayValue` escape hatches) are not user-declared records; they are
66        // handled inline by the code generator, so never resolve or emit them.
67        if arora_types::ty::WELL_KNOWN_IDS.contains(&reference.id) {
68            continue;
69        }
70        let selector = Selector::Id(reference.id);
71        let record_type = registry
72            .type_of(&selector)
73            .await
74            .map_err(ModuleDeclarationError::RegistryError)?;
75        if !matches!(record_type, RecordType::Structure | RecordType::Enumeration) {
76            continue;
77        }
78        let type_def = registry
79            .get_type(
80                &selector,
81                &VersionReq::parse(reference.version.to_string().as_str()).unwrap(),
82            )
83            .await
84            .map_err(ModuleDeclarationError::RegistryError)?;
85        // Enqueue the references this type itself depends on before emitting it.
86        collect_type_references(&type_def, &mut pending);
87        assets.push(ModuleAsset::Type(
88            reference.id,
89            reference.version.0.to_owned(),
90            type_def,
91        ));
92    }
93
94    // Then publish imports, and then this module.
95    assets.extend(resolved_module.imports.into_iter().map(ModuleAsset::Import));
96    assets.push(ModuleAsset::Module(
97        module_id,
98        module_version.into(),
99        resolved_module.module,
100        executor_name,
101    ));
102    Ok(assets)
103}
104
105/// Collects the pinned type references a frozen type expression depends on
106/// directly: a structure's field types and an enumeration's variant payload
107/// types. Feeds the transitive walk in [`analyze_module`].
108fn collect_type_references(type_def: &TypeDefinitionFrozen, out: &mut Vec<FrozenReference>) {
109    let push_ty = |ty: &FrozenTy, out: &mut Vec<FrozenReference>| match ty {
110        FrozenTy::Primitive(_) => {}
111        FrozenTy::FrozenScalar(scalar) => out.push(scalar.reference.to_owned()),
112        FrozenTy::FrozenArray(array) => out.push(array.reference.to_owned()),
113    };
114    match type_def {
115        TypeDefinitionFrozen::Primitive(_) => {}
116        TypeDefinitionFrozen::Structure(structure) => {
117            for field in structure.fields.values() {
118                push_ty(&field.ty, out);
119            }
120        }
121        TypeDefinitionFrozen::Enumeration(enumeration) => {
122            for variant in enumeration.variants.values() {
123                push_ty(&variant.ty, out);
124            }
125        }
126    }
127}
128
129/// Assets are records provided or referred to by a module.
130#[derive(Debug, Serialize, Deserialize)]
131pub enum ModuleAsset {
132    /// Type, including its identifier.
133    Type(Uuid, Version, TypeDefinitionFrozen),
134    /// Imported symbol, including the identifier of its origin module.
135    Import(ImportAsset),
136    /// Module, including its identifier, version and executor type.
137    Module(Uuid, Version, ModuleFrozen, String),
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct ImportAsset {
142    pub module_id: Uuid,
143    pub tag: Version,
144    pub module_name: String,
145    pub id: Uuid,
146    pub import: Export,
147}
148
149pub struct Writer<'a, W: AsyncWrite + Unpin> {
150    writer: &'a mut W,
151}
152
153impl<'a, W: AsyncWrite + Unpin> Writer<'a, W> {
154    pub fn new(writer: &'a mut W) -> Self {
155        Self { writer }
156    }
157
158    pub async fn write<T: Serialize>(&mut self, value: T) -> tokio::io::Result<()> {
159        let mut size = [0u8; 4];
160        let serialized = serde_json::to_string(&value).unwrap();
161        (&mut size[..]).put_u32(serialized.len() as u32);
162        self.writer.write_all(&size).await?;
163        self.writer.write_all(serialized.as_bytes()).await?;
164        Ok(())
165    }
166
167    pub async fn end(self) -> tokio::io::Result<()> {
168        let mut size = [0u8; 4];
169        (&mut size[..]).put_u32(0);
170        self.writer.write_all(&size).await?;
171        Ok(())
172    }
173}
174
175pub struct Reader<'a, R: AsyncRead + Unpin> {
176    reader: &'a mut R,
177}
178
179impl<'a, R: AsyncRead + Unpin> Reader<'a, R> {
180    pub fn new(reader: &'a mut R) -> Self {
181        Self { reader }
182    }
183
184    pub async fn read<T: DeserializeOwned>(&mut self) -> tokio::io::Result<Option<T>> {
185        let mut size = [0u8; 4];
186        self.reader.read_exact(&mut size).await?;
187        let size = (&size[..]).get_u32() as usize;
188        if size == 0 {
189            return Ok(None);
190        }
191
192        let mut buf = vec![0u8; size];
193        self.reader.read_exact(&mut buf).await?;
194        let value: T = serde_json::from_slice(&buf).unwrap();
195        Ok(Some(value))
196    }
197}
198
199#[derive(Display, Debug)]
200pub enum ModuleDeclarationError {
201    /// Record is not known to the registry or registry is not available.
202    RegistryError(RegistryError),
203
204    /// IO error.
205    IoError(std::io::Error),
206
207    /// Serialization / deserialization error.
208    YAMLError(serde_yaml::Error),
209
210    /// Virtual file system error.
211    VfsError(VfsError),
212
213    /// For any other error.
214    #[display("error: {}", _0)]
215    Generic(String),
216}
217
218impl std::error::Error for ModuleDeclarationError {}
219
220#[cfg(test)]
221mod tests {
222    use arora_types::module::high::ModuleDefinition;
223    use std::str::FromStr;
224    use uuid::Uuid;
225
226    #[test]
227    fn parse_uuid() {
228        let uuid_string = "b41899c3-66dc-40d4-ab61-d1ccf5231c88";
229        let expected = Uuid::from_str(uuid_string).unwrap();
230        let actual: Uuid = serde_yaml::from_str(uuid_string).unwrap();
231        assert!(actual == expected);
232    }
233
234    #[test]
235    fn load_simple_module() {
236        let module_string = "id: 325c5e47-32db-4e23-a38f-7a2849647e0c
237author: Semio
238description: Test C++ module
239license: Proprietary
240name: test-cpp-2
241version:
242  major: 0
243  minor: 1
244  patch: 0
245executor:
246  name: wasm
247exports:
248  - type: function
249    id: 07f5740c-ba4a-45af-8ec5-bedde5737e99
250    name: test
251    parameters:
252      - id: b41899c3-66dc-40d4-ab61-d1ccf5231c88
253        name: a
254        type:
255          kind: scalar
256          id: Status
257      - id: 63086e48-804f-403a-8862-3358ddedc08d
258        name: b
259        type:
260          kind: scalar
261          id: i32
262    ret:
263      kind: scalar
264      id: i32
265imports: []
266dependencies: []
267executable_mime: application/wasm";
268
269        let header: ModuleDefinition = serde_yaml::from_str(module_string).unwrap();
270        assert!(header.name == "test-cpp-2");
271    }
272}