Skip to main content

morphir_core/ir/v4/
module.rs

1//! Module types for Morphir IR V4
2//!
3//! This module contains ModuleSpecification, ModuleDefinition, and related types.
4
5use indexmap::IndexMap;
6use serde::{Deserialize, Serialize};
7
8use super::access::AccessControlled;
9use super::types::{TypeDefinition, TypeSpecification};
10use super::value::{ValueDefinition, ValueSpecification};
11
12/// Documentation: one string, with `\r\n` normalised to `\n` (definitions-0028; decision 0010).
13///
14/// An array of lines is tolerated only inside a module manifest file of a document tree, where it
15/// is joined with `\n` on the way in; everywhere else `doc` is a single string.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct Documentation(String);
18
19impl Documentation {
20    /// Construct documentation from a string, normalizing `\r\n` to `\n`.
21    pub fn new(text: impl Into<String>) -> Self {
22        Self(text.into().replace("\r\n", "\n"))
23    }
24
25    /// Return the normalized documentation text.
26    pub fn text(&self) -> &str {
27        &self.0
28    }
29
30    /// Return the normalized documentation as an iterator of lines.
31    pub fn lines(&self) -> std::str::Lines<'_> {
32        self.0.lines()
33    }
34}
35
36impl From<String> for Documentation {
37    fn from(value: String) -> Self {
38        Self::new(value)
39    }
40}
41
42impl From<&str> for Documentation {
43    fn from(value: &str) -> Self {
44        Self::new(value)
45    }
46}
47
48impl Serialize for Documentation {
49    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
50    where
51        S: serde::Serializer,
52    {
53        serializer.serialize_str(&self.0)
54    }
55}
56
57impl<'de> Deserialize<'de> for Documentation {
58    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
59    where
60        D: serde::Deserializer<'de>,
61    {
62        String::deserialize(deserializer).map(Self::new)
63    }
64}
65
66/// Optional documentation paired with an IR definition or specification.
67#[derive(Debug, Clone, PartialEq)]
68pub struct Documented<T> {
69    pub doc: Option<Documentation>,
70    pub value: T,
71}
72
73impl<T> Documented<T> {
74    pub fn new(doc: Option<Documentation>, value: T) -> Self {
75        Self { doc, value }
76    }
77}
78
79impl<T: Serialize> Serialize for Documented<T> {
80    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
81    where
82        S: serde::Serializer,
83    {
84        let Some(doc) = &self.doc else {
85            return self.value.serialize(serializer);
86        };
87
88        // Decision 0010: `doc` is a flattened member placed first beside the documented node's
89        // own members, rather than a wrapper the node sits under.
90        let written = serde_json::to_value(&self.value).map_err(serde::ser::Error::custom)?;
91        let doc = serde_json::to_value(doc).map_err(serde::ser::Error::custom)?;
92        match written {
93            serde_json::Value::Object(members) => {
94                let mut flattened = serde_json::Map::with_capacity(members.len() + 1);
95                flattened.insert("doc".to_owned(), doc);
96                flattened.extend(members);
97                serde_json::Value::Object(flattened).serialize(serializer)
98            }
99            // `{ "doc", "value" }` is the pre-decision spelling: a reader still accepts it for
100            // the window of decision 0006, and no writer in this workspace emits one. A node
101            // that is not an object has nowhere to flatten `doc` into, so it is refused rather
102            // than written the old way.
103            other => Err(serde::ser::Error::custom(format!(
104                "documentation is flattened beside the node it documents, so a documented node \
105                 must serialize as an object; this one wrote {other}"
106            ))),
107        }
108    }
109}
110
111impl<'de, T> Deserialize<'de> for Documented<T>
112where
113    T: for<'value> Deserialize<'value>,
114{
115    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
116    where
117        D: serde::Deserializer<'de>,
118    {
119        use super::serde_document::{carried, recover};
120
121        let value = serde_json::Value::deserialize(deserializer)?;
122        super::serde_document::decode_documented(&value, "", |payload, cursor| {
123            serde_json::from_value::<T>(payload.clone()).map_err(|error| recover(&error, cursor))
124        })
125        .map_err(carried)
126    }
127}
128
129/// Module specification (public API only)
130#[derive(Debug, Clone, PartialEq, Serialize)]
131#[serde(rename_all = "camelCase")]
132pub struct ModuleSpecification {
133    /// The annotations on the module's public face, written first and only when non-empty
134    /// (definitions-0022).
135    #[serde(skip_serializing_if = "super::annotation::Annotations::is_empty")]
136    pub annotations: super::annotation::Annotations,
137    pub types: IndexMap<String, Documented<TypeSpecification>>,
138    pub values: IndexMap<String, Documented<ValueSpecification>>,
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub doc: Option<Documentation>,
141}
142
143impl<'de> Deserialize<'de> for ModuleSpecification {
144    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
145    where
146        D: serde::Deserializer<'de>,
147    {
148        super::serde_document::deserialize_standalone_with(
149            deserializer,
150            super::serde_document::decode_module_specification,
151        )
152    }
153}
154
155/// Module definition
156#[derive(Debug, Clone, PartialEq, Serialize)]
157#[serde(rename_all = "camelCase")]
158pub struct ModuleDefinition {
159    pub types: IndexMap<String, AccessControlled<Documented<TypeDefinition>>>,
160    pub values: IndexMap<String, AccessControlled<Documented<ValueDefinition>>>,
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub doc: Option<Documentation>,
163}
164
165impl<'de> Deserialize<'de> for ModuleDefinition {
166    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
167    where
168        D: serde::Deserializer<'de>,
169    {
170        super::serde_document::deserialize_standalone_with(
171            deserializer,
172            super::serde_document::decode_module_definition,
173        )
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn documentation_is_flattened_beside_the_node_it_documents() {
183        let documented = Documented::new(
184            Some(Documentation::from("What this names.".to_owned())),
185            serde_json::json!({ "TypeAliasDefinition": { "typeParams": [] } }),
186        );
187
188        assert_eq!(
189            serde_json::to_value(&documented).unwrap(),
190            serde_json::json!({
191                "doc": "What this names.",
192                "TypeAliasDefinition": { "typeParams": [] }
193            })
194        );
195    }
196
197    #[test]
198    fn a_node_that_is_not_an_object_has_nowhere_to_flatten_documentation_into() {
199        // The pre-decision `{ "doc", "value" }` wrapper is a spelling a reader still accepts and
200        // no writer emits, so a node that cannot carry a flattened `doc` is a serialization
201        // failure rather than a quiet fallback to the older form.
202        let documented = Documented::new(
203            Some(Documentation::from("What this names.".to_owned())),
204            serde_json::json!("morphir/SDK:string#string"),
205        );
206
207        let error = serde_json::to_value(&documented).unwrap_err().to_string();
208        assert!(
209            error.contains("must serialize as an object"),
210            "unexpected error: {error}"
211        );
212        assert!(!error.contains("\"value\""), "unexpected error: {error}");
213    }
214
215    #[test]
216    fn an_undocumented_node_is_written_as_itself() {
217        let documented = Documented::new(None, serde_json::json!({ "Unit": {} }));
218
219        assert_eq!(
220            serde_json::to_value(&documented).unwrap(),
221            serde_json::json!({ "Unit": {} })
222        );
223    }
224}