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)]
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)]
34pub enum PartialRockspecError {
35 #[error("rockspec execution exceeded fuel limit of {ROCKSPEC_FUEL_LIMIT} steps")]
36 FuelLimitExceeded,
37 #[error("field '{0}' should not be declared in extra.rockspec")]
38 ExtraneousField(String),
39 #[error("error while parsing rockspec: {0}")]
40 Lua(#[from] ottavino::ExternError),
41 #[error(transparent)]
42 Io(#[from] std::io::Error),
43}
44
45impl PartialLuaRockspec {
46 pub fn new(rockspec_content: &str) -> Result<Self, PartialRockspecError> {
47 let mut lua = ottavino::Lua::core();
48
49 let rockspec = lua.try_enter(|ctx| {
50 let closure = Closure::load(ctx, None, rockspec_content.as_bytes())?;
51
52 let executor = Executor::start(ctx, closure.into(), ());
53
54 let output = executor.step(ctx, &mut Fuel::with(ROCKSPEC_FUEL_LIMIT))?;
55
56 if !output {
57 return Ok(Err(PartialRockspecError::FuelLimitExceeded));
58 }
59
60 let globals = ctx.globals();
61
62 if !matches!(globals.get_value(ctx, "version"), ottavino::Value::Nil) {
63 return Ok(Err(PartialRockspecError::ExtraneousField(
64 "version".to_string(),
65 )));
66 }
67 if !matches!(globals.get_value(ctx, "source"), ottavino::Value::Nil) {
68 return Ok(Err(PartialRockspecError::ExtraneousField(
69 "source".to_string(),
70 )));
71 }
72
73 let rockspec = PartialLuaRockspec {
74 rockspec_format: from_value(globals.get_value(ctx, "rockspec_format"))
75 .unwrap_or_default(),
76 package: from_value(globals.get_value(ctx, "package")).unwrap_or_default(),
77 description: parse_lua_tbl_or_default(ctx, "description").unwrap_or_default(),
78 supported_platforms: parse_lua_tbl_or_default(ctx, "supported_platforms")
79 .unwrap_or_default(),
80 dependencies: from_value(globals.get_value(ctx, "dependencies"))
81 .unwrap_or_default(),
82 build_dependencies: from_value(globals.get_value(ctx, "build_dependencies"))
83 .unwrap_or_default(),
84 test_dependencies: from_value(globals.get_value(ctx, "test_dependencies"))
85 .unwrap_or_default(),
86 external_dependencies: from_value(globals.get_value(ctx, "external_dependencies"))
87 .unwrap_or_default(),
88 build: from_value(globals.get_value(ctx, "build")).unwrap_or_default(),
89 test: from_value(globals.get_value(ctx, "test")).unwrap_or_default(),
90 deploy: from_value(globals.get_value(ctx, "deploy")).unwrap_or_default(),
91 };
92
93 Ok(Ok(rockspec))
94 })??;
95
96 Ok(rockspec)
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn parse_partial_rockspec() {
106 let partial_rockspec = r#"
107 package = "my-package"
108 "#;
109
110 PartialLuaRockspec::new(partial_rockspec).unwrap();
111
112 let full_rockspec = r#"
114 rockspec_format = "3.0"
115 package = "my-package"
116
117 description = {
118 summary = "A summary",
119 detailed = "A detailed description",
120 license = "MIT",
121 homepage = "https://example.com",
122 issues_url = "https://example.com/issues",
123 maintainer = "John Doe",
124 labels = {"label1", "label2"},
125 }
126
127 supported_platforms = {"linux", "!windows"}
128
129 dependencies = {
130 "lua 5.1",
131 "foo 1.0",
132 "bar >=2.0",
133 }
134
135 build_dependencies = {
136 "baz 1.0",
137 }
138
139 external_dependencies = {
140 foo = { header = "foo.h" },
141 bar = { library = "libbar.so" },
142 }
143
144 test_dependencies = {
145 "busted 1.0",
146 }
147
148 test = {
149 type = "command",
150 script = "test.lua",
151 flags = {"foo", "bar"},
152 }
153
154 build = {
155 type = "builtin",
156 }
157 "#;
158
159 let rockspec = PartialLuaRockspec::new(full_rockspec).unwrap();
160
161 assert!(rockspec.rockspec_format.is_some());
165 assert!(rockspec.package.is_some());
166 assert!(rockspec.description.is_some());
167 assert!(rockspec.supported_platforms.is_some());
168 assert!(rockspec.dependencies.is_some());
169 assert!(rockspec.build_dependencies.is_some());
170 assert!(rockspec.external_dependencies.is_some());
171 assert!(rockspec.test_dependencies.is_some());
172 assert!(rockspec.build.is_some());
173 assert!(rockspec.test.is_some());
174
175 let partial_rockspec = r#"
177 version = "2.0.0"
178 "#;
179
180 PartialLuaRockspec::new(partial_rockspec).unwrap_err();
181
182 let partial_rockspec = r#"
183 source = {
184 url = "https://example.com",
185 }
186 "#;
187
188 PartialLuaRockspec::new(partial_rockspec).unwrap_err();
189 }
190}