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