Skip to main content

morphir_core/ir/v4/
distribution.rs

1//! Distribution types for Morphir IR V4
2//!
3//! This module contains the Distribution enum and related content types
4//! (LibraryContent, SpecsContent, ApplicationContent).
5
6use indexmap::IndexMap;
7use serde::ser::{SerializeMap, Serializer};
8use serde::{Deserialize, Serialize};
9
10use super::package::{PackageDefinition, PackageSpecification};
11use crate::naming::PackageName;
12
13/// A distribution, written as the single-member wrapper its kind names.
14///
15/// `{ "Library": { "packageName", "dependencies", "def" } }`,
16/// `{ "Specs": { "packageName", "dependencies", "spec" } }` or
17/// `{ "Application": { "packageName", "dependencies", "def", "entryPoints" } }`. A version 3
18/// tagged array is not a version 4 distribution.
19#[derive(Debug, Clone, PartialEq)]
20pub enum Distribution {
21    Library(LibraryContent),
22    Specs(SpecsContent),
23    Application(ApplicationContent),
24}
25
26impl Distribution {
27    /// The package this distribution publishes.
28    pub fn package_name(&self) -> &PackageName {
29        match self {
30            Distribution::Library(content) => &content.package_name,
31            Distribution::Specs(content) => &content.package_name,
32            Distribution::Application(content) => &content.package_name,
33        }
34    }
35}
36
37impl Serialize for Distribution {
38    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
39    where
40        S: Serializer,
41    {
42        let mut map = serializer.serialize_map(Some(1))?;
43        match self {
44            Distribution::Library(content) => {
45                map.serialize_entry("Library", content)?;
46            }
47            Distribution::Specs(content) => {
48                map.serialize_entry("Specs", content)?;
49            }
50            Distribution::Application(content) => {
51                map.serialize_entry("Application", content)?;
52            }
53        }
54        map.end()
55    }
56}
57
58impl<'de> Deserialize<'de> for Distribution {
59    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
60    where
61        D: serde::Deserializer<'de>,
62    {
63        super::serde_document::deserialize_standalone_with(
64            deserializer,
65            super::serde_document::decode_distribution,
66        )
67    }
68}
69
70/// Library distribution content
71#[derive(Debug, Clone, PartialEq, Serialize)]
72#[serde(rename_all = "camelCase")]
73pub struct LibraryContent {
74    pub package_name: PackageName,
75    pub dependencies: Dependencies,
76    pub def: PackageDefinition,
77}
78
79/// Specs distribution content (public interfaces only)
80///
81/// A `Specs` distribution publishes a package's public face and nothing else, so it carries a
82/// `spec` where a library carries a `def`; a `def` beside it is an unknown member.
83#[derive(Debug, Clone, PartialEq, Serialize)]
84#[serde(rename_all = "camelCase")]
85pub struct SpecsContent {
86    pub package_name: PackageName,
87    pub dependencies: Dependencies,
88    pub spec: PackageSpecification,
89}
90
91/// Application distribution content
92#[derive(Debug, Clone, PartialEq, Serialize)]
93#[serde(rename_all = "camelCase")]
94pub struct ApplicationContent {
95    pub package_name: PackageName,
96    pub dependencies: DefinitionDependencies,
97    pub def: PackageDefinition,
98    pub entry_points: EntryPoints,
99}
100
101/// Dependencies keyed by canonical package name: `{ "morphir/SDK": { "modules": {} } }`.
102///
103/// Decision 0011 spells the SDK `morphir/SDK`. The key is the package's canonical string, which
104/// a reader checks: `morphir/sdk` is a valid name for some other package, so it is read as one
105/// rather than refused.
106pub type Dependencies = IndexMap<String, PackageSpecification>;
107
108/// An application's dependencies, each a package definition (distributions-0010).
109///
110/// An `Application` links its dependencies statically, so it carries their definitions —
111/// access-controlled modules — where a `Library` or `Specs` carries their public faces. The key
112/// is read the same way: it is the dependency's canonical package name.
113pub type DefinitionDependencies = IndexMap<String, PackageDefinition>;
114
115/// Entry points for Application distribution
116pub type EntryPoints = IndexMap<String, EntryPoint>;
117
118/// Entry point definition
119///
120/// Keyed by a name the author chooses, holding the value it names, the kind of entry it is, and
121/// an optional `doc`, which is written only when it is present.
122#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
123#[serde(rename_all = "camelCase")]
124pub struct EntryPoint {
125    /// The entry point's target, a canonical FQName string.
126    pub target: String,
127    pub kind: EntryPointKind,
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub doc: Option<String>,
130}
131
132/// Entry point kind, drawn from a fixed set.
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
134#[serde(rename_all = "lowercase")]
135pub enum EntryPointKind {
136    Main,
137    Command,
138    Handler,
139    Job,
140    Policy,
141}
142
143impl EntryPointKind {
144    /// The kinds an entry point may name, in the order the specification lists them.
145    pub const ALL: &'static [EntryPointKind] = &[
146        EntryPointKind::Main,
147        EntryPointKind::Command,
148        EntryPointKind::Handler,
149        EntryPointKind::Job,
150        EntryPointKind::Policy,
151    ];
152
153    /// The lowercase wire spelling of this kind.
154    pub fn as_str(self) -> &'static str {
155        match self {
156            EntryPointKind::Main => "main",
157            EntryPointKind::Command => "command",
158            EntryPointKind::Handler => "handler",
159            EntryPointKind::Job => "job",
160            EntryPointKind::Policy => "policy",
161        }
162    }
163
164    /// The kind `text` names, if it names one.
165    pub fn parse(text: &str) -> Option<EntryPointKind> {
166        Self::ALL.iter().copied().find(|kind| kind.as_str() == text)
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use crate::naming::Path;
174
175    #[test]
176    fn test_distribution_library_serialization() {
177        let dist = Distribution::Library(LibraryContent {
178            package_name: PackageName::new(Path::new("my/pkg")),
179            dependencies: IndexMap::new(),
180            def: PackageDefinition {
181                modules: IndexMap::new(),
182            },
183        });
184        let json = serde_json::to_string(&dist).unwrap();
185        assert!(json.contains("\"Library\""));
186        assert!(json.contains("packageName"));
187    }
188}