Skip to main content

lux_lib/lua_rockspec/
partial.rs

1use std::collections::HashMap;
2
3use ottavino::{Closure, Executor, Fuel};
4use ottavino_util::serde::from_value;
5use thiserror::Error;
6
7use crate::{
8    lua_rockspec::RockspecFormat, package::PackageName,
9    rockspec::lua_dependency::LuaDependencySpec, ROCKSPEC_FUEL_LIMIT,
10};
11
12use super::{
13    parse_lua_tbl_or_default, BuildSpecInternal, DeploySpec, ExternalDependencySpec,
14    PlatformSupport, RockDescription, TestSpecInternal,
15};
16
17#[derive(Debug)]
18pub struct PartialLuaRockspec {
19    pub(crate) rockspec_format: Option<RockspecFormat>,
20    pub(crate) package: Option<PackageName>,
21    pub(crate) build: Option<BuildSpecInternal>,
22    pub(crate) deploy: Option<DeploySpec>,
23    pub(crate) description: Option<RockDescription>,
24    pub(crate) supported_platforms: Option<PlatformSupport>,
25    pub(crate) dependencies: Option<Vec<LuaDependencySpec>>,
26    pub(crate) build_dependencies: Option<Vec<LuaDependencySpec>>,
27    pub(crate) external_dependencies: Option<HashMap<String, ExternalDependencySpec>>,
28    pub(crate) test_dependencies: Option<Vec<LuaDependencySpec>>,
29    pub(crate) test: Option<TestSpecInternal>,
30}
31
32#[derive(Debug, Error)]
33pub enum PartialRockspecError {
34    #[error("rockspec execution exceeded fuel limit of {ROCKSPEC_FUEL_LIMIT} steps")]
35    FuelLimitExceeded,
36    #[error("field `{0}` should not be declared in extra.rockspec")]
37    ExtraneousField(String),
38    #[error("error while parsing rockspec: {0}")]
39    Lua(#[from] ottavino::ExternError),
40    #[error(transparent)]
41    Io(#[from] std::io::Error),
42}
43
44impl PartialLuaRockspec {
45    pub fn new(rockspec_content: &str) -> Result<Self, PartialRockspecError> {
46        let mut lua = ottavino::Lua::core();
47
48        let rockspec = lua.try_enter(|ctx| {
49            let closure = Closure::load(ctx, None, rockspec_content.as_bytes())?;
50
51            let executor = Executor::start(ctx, closure.into(), ());
52
53            let output = executor.step(ctx, &mut Fuel::with(ROCKSPEC_FUEL_LIMIT))?;
54
55            if !output {
56                return Ok(Err(PartialRockspecError::FuelLimitExceeded));
57            }
58
59            let globals = ctx.globals();
60
61            if !matches!(globals.get_value(ctx, "version"), ottavino::Value::Nil) {
62                return Ok(Err(PartialRockspecError::ExtraneousField(
63                    "version".to_string(),
64                )));
65            }
66            if !matches!(globals.get_value(ctx, "source"), ottavino::Value::Nil) {
67                return Ok(Err(PartialRockspecError::ExtraneousField(
68                    "source".to_string(),
69                )));
70            }
71
72            let rockspec = PartialLuaRockspec {
73                rockspec_format: from_value(globals.get_value(ctx, "rockspec_format"))
74                    .unwrap_or_default(),
75                package: from_value(globals.get_value(ctx, "package")).unwrap_or_default(),
76                description: parse_lua_tbl_or_default(ctx, "description").unwrap_or_default(),
77                supported_platforms: parse_lua_tbl_or_default(ctx, "supported_platforms")
78                    .unwrap_or_default(),
79                dependencies: from_value(globals.get_value(ctx, "dependencies"))
80                    .unwrap_or_default(),
81                build_dependencies: from_value(globals.get_value(ctx, "build_dependencies"))
82                    .unwrap_or_default(),
83                test_dependencies: from_value(globals.get_value(ctx, "test_dependencies"))
84                    .unwrap_or_default(),
85                external_dependencies: from_value(globals.get_value(ctx, "external_dependencies"))
86                    .unwrap_or_default(),
87                build: from_value(globals.get_value(ctx, "build")).unwrap_or_default(),
88                test: from_value(globals.get_value(ctx, "test")).unwrap_or_default(),
89                deploy: from_value(globals.get_value(ctx, "deploy")).unwrap_or_default(),
90            };
91
92            Ok(Ok(rockspec))
93        })??;
94
95        Ok(rockspec)
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn parse_partial_rockspec() {
105        let partial_rockspec = r#"
106            package = "my-package"
107        "#;
108
109        PartialLuaRockspec::new(partial_rockspec).unwrap();
110
111        // Whether the partial rockspec format can still support entire rockspecs
112        let full_rockspec = r#"
113            rockspec_format = "3.0"
114            package = "my-package"
115
116            description = {
117                summary = "A summary",
118                detailed = "A detailed description",
119                license = "MIT",
120                homepage = "https://example.com",
121                issues_url = "https://example.com/issues",
122                maintainer = "John Doe",
123                labels = {"label1", "label2"},
124            }
125
126            supported_platforms = {"linux", "!windows"}
127
128            dependencies = {
129                "lua 5.1",
130                "foo 1.0",
131                "bar >=2.0",
132            }
133
134            build_dependencies = {
135                "baz 1.0",
136            }
137
138            external_dependencies = {
139                foo = { header = "foo.h" },
140                bar = { library = "libbar.so" },
141            }
142
143            test_dependencies = {
144                "busted 1.0",
145            }
146
147            test = {
148                type = "command",
149                script = "test.lua",
150                flags = {"foo", "bar"},
151            }
152
153            build = {
154                type = "builtin",
155            }
156        "#;
157
158        let rockspec = PartialLuaRockspec::new(full_rockspec).unwrap();
159
160        // No need to verify if the fields were parsed correctly, but worth checking if they were
161        // parsed at all.
162
163        assert!(rockspec.rockspec_format.is_some());
164        assert!(rockspec.package.is_some());
165        assert!(rockspec.description.is_some());
166        assert!(rockspec.supported_platforms.is_some());
167        assert!(rockspec.dependencies.is_some());
168        assert!(rockspec.build_dependencies.is_some());
169        assert!(rockspec.external_dependencies.is_some());
170        assert!(rockspec.test_dependencies.is_some());
171        assert!(rockspec.build.is_some());
172        assert!(rockspec.test.is_some());
173
174        // We don't allow version and source in extra.rockspec
175        let partial_rockspec = r#"
176            version = "2.0.0"
177        "#;
178
179        PartialLuaRockspec::new(partial_rockspec).unwrap_err();
180
181        let partial_rockspec = r#"
182            source = {
183                url = "https://example.com",
184            }
185        "#;
186
187        PartialLuaRockspec::new(partial_rockspec).unwrap_err();
188    }
189}