Skip to main content

miden_project/ast/
workspace.rs

1use super::{
2    parsing::{MaybeInherit, SetSourceId, Validate},
3    *,
4};
5use crate::{SourceId, Span, Uri};
6
7/// Represents the contents of the `[workspace]` table
8#[derive(Debug, Clone)]
9#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
10#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
11pub struct WorkspaceTable {
12    /// The relative paths of all workspace members
13    #[cfg_attr(feature = "serde", serde(default))]
14    pub members: Vec<Span<Uri>>,
15    /// The contents of the `[workspace.package]` table
16    #[cfg_attr(feature = "serde", serde(default))]
17    pub package: PackageDetail,
18    /// The contents of the `[workspace]` table that are shared with `[package]`
19    #[cfg_attr(feature = "serde", serde(flatten, default))]
20    pub config: PackageConfig,
21}
22
23impl SetSourceId for WorkspaceTable {
24    fn set_source_id(&mut self, source_id: SourceId) {
25        let Self { members, package, config } = self;
26        members.set_source_id(source_id);
27        package.set_source_id(source_id);
28        config.set_source_id(source_id);
29    }
30}
31
32/// Represents a workspace-level `miden-project.toml` file
33#[derive(Debug, Clone)]
34#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
35#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
36pub struct WorkspaceFile {
37    /// The source file this was parsed from, if applicable/known
38    #[cfg_attr(feature = "serde", serde(skip, default))]
39    pub source_file: Option<Arc<SourceFile>>,
40    /// The contents of the `[workspace]` table
41    pub workspace: WorkspaceTable,
42    /// The contents of the `[profile]` table
43    #[cfg_attr(
44        feature = "serde",
45        serde(
46            default,
47            rename = "profile",
48            deserialize_with = "profile::deserialize_profiles_table",
49            skip_serializing_if = "Vec::is_empty"
50        )
51    )]
52    pub profiles: Vec<Profile>,
53}
54
55/// Parsing
56impl WorkspaceFile {
57    /// Parse a [ProjectFile] from the provided TOML source file, generally `miden-project.toml`
58    ///
59    /// If successful, the contents of the manifest are semantically valid, with the following
60    /// caveats:
61    ///
62    /// * Inherited properties from the workspace-level are assumed to exist and be correct. It is
63    ///   up to the caller to compute the concrete property values and validate them at that point.
64    #[cfg(feature = "serde")]
65    pub fn parse(source: Arc<SourceFile>) -> Result<Self, Report> {
66        use parsing::{SetSourceId, Validate};
67
68        let source_id = source.id();
69
70        // Parse the unvalidated project from source
71        let mut workspace = toml::from_str::<Self>(source.as_str()).map_err(|err| {
72            let span = err
73                .span()
74                .map(|span| {
75                    let start = span.start as u32;
76                    let end = span.end as u32;
77                    SourceSpan::new(source_id, start..end)
78                })
79                .unwrap_or_default();
80            Report::from(ProjectFileError::ParseError {
81                message: err.message().to_string(),
82                source_file: source.clone(),
83                span,
84            })
85        })?;
86
87        workspace.source_file = Some(source.clone());
88        workspace.set_source_id(source_id);
89        workspace.validate(source)?;
90
91        Ok(workspace)
92    }
93}
94
95impl SetSourceId for WorkspaceFile {
96    fn set_source_id(&mut self, source_id: SourceId) {
97        let Self { source_file: _, workspace, profiles } = self;
98        workspace.set_source_id(source_id);
99        profiles.set_source_id(source_id);
100    }
101}
102
103impl Validate for WorkspaceFile {
104    fn validate(&self, source: Arc<SourceFile>) -> Result<(), Report> {
105        // Validate that none of the package detail fields try to inherit from a workspace
106        if let Some(span) = self.workspace.package.version.as_ref().and_then(|v| {
107            if matches!(v.inner(), MaybeInherit::Inherit) {
108                Some(v.span())
109            } else {
110                None
111            }
112        }) {
113            return Err(ProjectFileError::NotAWorkspace { source_file: source, span }.into());
114        }
115
116        if let Some(description) = self.workspace.package.description.as_ref()
117            && matches!(description.inner(), MaybeInherit::Inherit)
118        {
119            return Err(ProjectFileError::NotAWorkspace {
120                source_file: source,
121                span: description.span(),
122            }
123            .into());
124        }
125
126        // Validate that workspace-level dependencies are all valid at that level
127        for dependency in self.workspace.config.dependencies.values() {
128            if dependency.inherits_workspace_version() {
129                let label = if dependency.version().is_none()
130                    && !dependency.is_git()
131                    && !dependency.is_path()
132                {
133                    "expected 'version', 'digest', or 'path' here"
134                } else {
135                    "cannot use the 'workspace' option in a workspace-level dependency spec"
136                };
137                return Err(Report::from(ProjectFileError::InvalidWorkspaceDependency {
138                    source_file: source.clone(),
139                    label: Label::new(dependency.span(), label),
140                }));
141            }
142        }
143
144        Ok(())
145    }
146}