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 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 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 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}