1mod build;
2mod dependency;
3mod deploy;
4mod partial;
5mod platform;
6mod rock_source;
7mod serde_util;
8mod test_spec;
9
10use std::{
11 collections::HashMap, convert::Infallible, fmt::Display, io, path::PathBuf, str::FromStr,
12};
13
14use ottavino::{Closure, Executor, Fuel};
15use ottavino_util::serde::from_value;
16use serde::{de::DeserializeOwned, Deserialize, Serialize};
17
18pub use build::*;
19pub use dependency::*;
20pub use deploy::*;
21use miette::{Diagnostic, NamedSource, SourceSpan};
22pub use partial::*;
23pub use platform::*;
24pub use rock_source::*;
25use ssri::Integrity;
26pub use test_spec::*;
27use thiserror::Error;
28use url::Url;
29
30pub(crate) use lux_macros::DisplayAsLuaKV;
31pub(crate) use serde_util::*;
32
33impl DisplayAsLuaValue for Url {
34 fn display_lua_value(&self) -> DisplayLuaValue {
35 DisplayLuaValue::String(self.to_string())
36 }
37}
38
39use crate::{
40 hash::HasIntegrity,
41 lua_version::{LuaVersion, LuaVersionUnset},
42 package::{PackageName, PackageSpec, PackageVersion, PackageVersionReq},
43 project::{project_toml::ProjectTomlError, ProjectRoot},
44 rockspec::{lua_dependency::LuaDependencySpec, Rockspec},
45 ROCKSPEC_FUEL_LIMIT,
46};
47
48#[derive(Error, Debug, Diagnostic)]
49#[non_exhaustive]
50pub enum LuaRockspecError {
51 #[error("manifest exceeds computational limit of {ROCKSPEC_FUEL_LIMIT} steps")]
52 #[diagnostic(
53 code(lux_lib::lua_rockspec::fuel_limit_exceeded),
54 help(
55 r#"the rockspec may be too complex or contain too many dependencies.
56try reducing the number of dependencies or simplifying build instructions.
57"#
58 )
59 )]
60 FuelLimitExceeded,
61 #[error("could not parse rockspec")]
62 #[diagnostic(code(lux_lib::lua_rockspec::execution),
63 help("check the rockspec or lux.toml for valid syntax and make sure it matches the specification."),
64 url("https://lux.lumen-labs.org/reference/lux-toml")
65 )]
66 ExecutionError {
67 cause: Box<String>,
68 #[source_code]
69 content: NamedSource<String>,
70 #[label("this package")]
71 pkg_span: SourceSpan,
72 #[label("{cause}")]
73 err_span: Option<SourceSpan>,
74 },
75 #[error("{}copy_directories cannot contain the rockspec name", ._0.as_ref().map(|p| format!("{p}: ")).unwrap_or_default())]
76 CopyDirectoriesContainRockspecName(Option<String>),
77 #[error("cannot create Lua rockspec with off-spec dependency")]
78 #[diagnostic(
79 code(lux_lib::lua_rockspec::off_spec_dependency),
80 url("https://lux.lumen-labs.org/reference/lux-toml#dependencies")
81 )]
82 OffSpecDependency(PackageName),
83 #[error("cannot create Lua rockspec with off-spec build dependency")]
84 #[diagnostic(
85 code(lux_lib::lua_rockspec::off_spec_build_dependency),
86 url("https://lux.lumen-labs.org/reference/lux-toml#dependencies")
87 )]
88 OffSpecBuildDependency(PackageName),
89 #[error("cannot create Lua rockspec with off-spec test dependency")]
90 #[diagnostic(
91 code(lux_lib::lua_rockspec::off_spec_test_dependency),
92 url("https://lux.lumen-labs.org/reference/lux-toml#dependencies")
93 )]
94 OffSpecTestDependency(PackageName),
95 #[error(transparent)]
96 #[diagnostic(transparent)]
97 ProjectToml(#[from] ProjectTomlError),
98}
99
100impl LuaRockspecError {
101 fn from_ottavino(cause: ottavino::ExternError, rockspec_content: &str) -> Self {
102 let msg = match &cause {
103 ottavino::ExternError::Runtime(rt) => rt.to_string(),
104 _ => cause.to_string(),
105 };
106 let field = msg
107 .strip_prefix("invalid rockspec field '")
108 .and_then(|rest| rest.split('\'').next())
109 .map(|f| f.to_owned());
110 let err_span = field.as_deref().and_then(|field| {
111 let span = find_key_span(rockspec_content, field);
112 if span.is_empty() {
113 None
114 } else {
115 Some(span)
116 }
117 });
118 LuaRockspecError::ExecutionError {
119 cause: Box::new(msg),
120 content: NamedSource::new("rockspec", rockspec_content.to_string()),
121 pkg_span: find_value_span(rockspec_content, "package"),
122 err_span,
123 }
124 }
125}
126
127#[derive(Error, Debug, Diagnostic)]
131#[non_exhaustive]
132enum ParseLuaRockspecFieldError {
133 #[error("invalid rockspec field '{field}': {source}")]
134 Deserialize {
135 field: String,
136 source: Box<ottavino_util::serde::de::Error>,
137 },
138 #[error(
139 r#"invalid rockspec field '{field}'.
140Failed to parse a table field: {source}"#
141 )]
142 DeserializeTable {
143 field: String,
144 source: Box<ottavino_util::serde::de::Error>,
145 },
146 #[error("invalid rockspec field '{field}'. expected a table, but got: {invalid_type}")]
147 NotATable { field: String, invalid_type: String },
148}
149
150#[derive(Clone, Debug)]
152#[cfg_attr(test, derive(PartialEq))]
153pub struct LocalLuaRockspec {
154 rockspec_format: Option<RockspecFormat>,
156 package: PackageName,
158 version: PackageVersion,
160 description: RockDescription,
161 supported_platforms: PlatformSupport,
162 lua: PackageVersionReq,
164 dependencies: PerPlatform<Vec<LuaDependencySpec>>,
165 build_dependencies: PerPlatform<Vec<LuaDependencySpec>>,
166 external_dependencies: PerPlatform<HashMap<String, ExternalDependencySpec>>,
167 test_dependencies: PerPlatform<Vec<LuaDependencySpec>>,
168 build: PerPlatform<BuildSpec>,
169 source: PerPlatform<RemoteRockSource>,
170 test: PerPlatform<TestSpec>,
171 deploy: PerPlatform<DeploySpec>,
172 raw_content: String,
174}
175
176trait HasRockspecKey<'gc> {
177 fn get_rockspec_key<V: Deserialize<'gc>>(
178 &self,
179 ctx: ottavino::Context<'gc>,
180 key: &str,
181 ) -> Result<V, ParseLuaRockspecFieldError>;
182}
183
184impl<'gc> HasRockspecKey<'gc> for ottavino::Table<'gc> {
185 fn get_rockspec_key<V: Deserialize<'gc>>(
186 &self,
187 ctx: ottavino::Context<'gc>,
188 key: &str,
189 ) -> Result<V, ParseLuaRockspecFieldError> {
190 let value = self.get_value(ctx, key.to_string());
191 let is_table = matches!(&value, ottavino::Value::Table(_));
192 from_value(value).map_err(|source| {
193 if is_table {
194 ParseLuaRockspecFieldError::DeserializeTable {
195 field: key.into(),
196 source: Box::new(source),
197 }
198 } else {
199 ParseLuaRockspecFieldError::Deserialize {
200 field: key.into(),
201 source: Box::new(source),
202 }
203 }
204 })
205 }
206}
207
208impl LocalLuaRockspec {
209 pub(crate) fn from_project_toml(
212 project: &crate::project::project_toml::LocalProjectToml,
213 ) -> Self {
214 use crate::rockspec::Rockspec;
215
216 Self {
217 rockspec_format: project.format().clone(),
218 package: project.package().clone(),
219 version: project.version().clone(),
220 description: project.description().clone(),
221 supported_platforms: project.supported_platforms().clone(),
222 lua: project.lua().clone(),
223 dependencies: project.dependencies().clone(),
224 build_dependencies: project.build_dependencies().clone(),
225 external_dependencies: project.external_dependencies().clone(),
226 test_dependencies: project.test_dependencies().clone(),
227 build: project.build().clone(),
228 source: project.source().clone(),
229 test: project.test().clone(),
230 deploy: project.deploy().clone(),
231 raw_content: String::new(),
232 }
233 }
234
235 pub fn into_remote_with_source(
238 self,
239 source: PerPlatform<RemoteRockSource>,
240 ) -> RemoteLuaRockspec {
241 RemoteLuaRockspec {
242 local: self,
243 source,
244 }
245 }
246
247 pub fn new(
248 rockspec_content: &str,
249 project_root: ProjectRoot,
250 ) -> Result<Self, LuaRockspecError> {
251 let mut lua = ottavino::Lua::core();
252
253 let rockspec = lua
254 .try_enter(|ctx| {
255 let closure = Closure::load(ctx, None, rockspec_content.as_bytes())?;
256
257 let executor = Executor::start(ctx, closure.into(), ());
258
259 let output = executor.step(ctx, &mut Fuel::with(ROCKSPEC_FUEL_LIMIT))?;
260
261 if !output {
262 return Ok(Err(LuaRockspecError::FuelLimitExceeded));
263 }
264
265 let globals = ctx.globals();
266
267 let dependencies: PerPlatform<Vec<LuaDependencySpec>> =
268 globals.get_rockspec_key(ctx, "dependencies")?;
269
270 let lua_version_req = dependencies
271 .current_platform()
272 .iter()
273 .find(|dep| dep.name().to_string() == "lua")
274 .cloned()
275 .map(|dep| dep.version_req().clone())
276 .unwrap_or(PackageVersionReq::Any);
277
278 fn strip_lua(
279 dependencies: PerPlatform<Vec<LuaDependencySpec>>,
280 ) -> PerPlatform<Vec<LuaDependencySpec>> {
281 dependencies.map(|deps| {
282 deps.iter()
283 .filter(|dep| dep.name().to_string() != "lua")
284 .cloned()
285 .collect()
286 })
287 }
288
289 let build_dependencies: PerPlatform<Vec<LuaDependencySpec>> =
290 globals.get_rockspec_key(ctx, "build_dependencies")?;
291
292 let test_dependencies: PerPlatform<Vec<LuaDependencySpec>> =
293 globals.get_rockspec_key(ctx, "test_dependencies")?;
294
295 let source: PerPlatform<RemoteRockSource> = match globals.get_value(ctx, "source") {
296 ottavino::Value::Nil => {
297 PerPlatform::new(RockSourceSpec::File(project_root.to_path_buf()).into())
298 }
299 value => from_value(value).map_err(|source| {
300 ParseLuaRockspecFieldError::Deserialize {
301 field: "source".into(),
302 source: Box::new(source),
303 }
304 })?,
305 };
306
307 let rockspec = LocalLuaRockspec {
308 rockspec_format: globals.get_rockspec_key(ctx, "rockspec_format")?,
309 package: globals.get_rockspec_key(ctx, "package")?,
310 version: globals.get_rockspec_key(ctx, "version")?,
311 description: parse_lua_tbl_or_default(ctx, "description")?,
312 supported_platforms: parse_lua_tbl_or_default(ctx, "supported_platforms")?,
313 lua: lua_version_req,
314 dependencies: strip_lua(dependencies),
315 build_dependencies: strip_lua(build_dependencies),
316 test_dependencies: strip_lua(test_dependencies),
317 external_dependencies: globals
318 .get_rockspec_key(ctx, "external_dependencies")?,
319 build: globals.get_rockspec_key(ctx, "build")?,
320 test: globals.get_rockspec_key(ctx, "test")?,
321 deploy: globals.get_rockspec_key(ctx, "deploy")?,
322 raw_content: rockspec_content.into(),
323
324 source,
325 };
326
327 Ok(Ok(rockspec))
328 })
329 .map_err(|cause| LuaRockspecError::from_ottavino(cause, rockspec_content))??;
330
331 let rockspec_file_name = format!("{}-{}.rockspec", rockspec.package(), rockspec.version());
332
333 if rockspec
334 .build()
335 .default
336 .copy_directories
337 .contains(&PathBuf::from(&rockspec_file_name))
338 {
339 return Err(LuaRockspecError::CopyDirectoriesContainRockspecName(None));
340 }
341
342 for (platform, build_override) in &rockspec.build().per_platform {
343 if build_override
344 .copy_directories
345 .contains(&PathBuf::from(&rockspec_file_name))
346 {
347 return Err(LuaRockspecError::CopyDirectoriesContainRockspecName(Some(
348 platform.to_string(),
349 )));
350 }
351 }
352 Ok(rockspec)
353 }
354}
355
356impl Rockspec for LocalLuaRockspec {
357 type Error = Infallible;
358
359 fn package(&self) -> &PackageName {
360 &self.package
361 }
362
363 fn version(&self) -> &PackageVersion {
364 &self.version
365 }
366
367 fn description(&self) -> &RockDescription {
368 &self.description
369 }
370
371 fn supported_platforms(&self) -> &PlatformSupport {
372 &self.supported_platforms
373 }
374
375 fn lua(&self) -> &PackageVersionReq {
376 &self.lua
377 }
378
379 fn dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
380 &self.dependencies
381 }
382
383 fn build_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
384 &self.build_dependencies
385 }
386
387 fn external_dependencies(&self) -> &PerPlatform<HashMap<String, ExternalDependencySpec>> {
388 &self.external_dependencies
389 }
390
391 fn test_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
392 &self.test_dependencies
393 }
394
395 fn build(&self) -> &PerPlatform<BuildSpec> {
396 &self.build
397 }
398
399 fn test(&self) -> &PerPlatform<TestSpec> {
400 &self.test
401 }
402
403 fn source(&self) -> &PerPlatform<RemoteRockSource> {
404 &self.source
405 }
406
407 fn deploy(&self) -> &PerPlatform<DeploySpec> {
408 &self.deploy
409 }
410
411 fn build_mut(&mut self) -> &mut PerPlatform<BuildSpec> {
412 &mut self.build
413 }
414
415 fn test_mut(&mut self) -> &mut PerPlatform<TestSpec> {
416 &mut self.test
417 }
418
419 fn source_mut(&mut self) -> &mut PerPlatform<RemoteRockSource> {
420 &mut self.source
421 }
422
423 fn deploy_mut(&mut self) -> &mut PerPlatform<DeploySpec> {
424 &mut self.deploy
425 }
426
427 fn format(&self) -> &Option<RockspecFormat> {
428 &self.rockspec_format
429 }
430
431 fn to_lua_remote_rockspec_string(&self) -> Result<String, Self::Error> {
432 Ok(self.raw_content.clone())
433 }
434}
435
436impl HasIntegrity for LocalLuaRockspec {
437 async fn hash(&self) -> io::Result<Integrity> {
438 Ok(Integrity::from(&self.raw_content))
439 }
440}
441
442#[derive(Clone, Debug)]
444#[cfg_attr(test, derive(PartialEq))]
445pub struct RemoteLuaRockspec {
446 local: LocalLuaRockspec,
447 source: PerPlatform<RemoteRockSource>,
448}
449
450impl RemoteLuaRockspec {
451 pub fn new(rockspec_content: &str) -> Result<Self, LuaRockspecError> {
452 let mut lua = ottavino::Lua::core();
453
454 let source = lua
455 .try_enter(|ctx| {
456 let closure = Closure::load(ctx, None, rockspec_content.as_bytes())?;
457
458 let executor = Executor::start(ctx, closure.into(), ());
459
460 let output = executor.step(ctx, &mut Fuel::with(ROCKSPEC_FUEL_LIMIT))?;
461
462 if !output {
463 return Ok(Err(LuaRockspecError::FuelLimitExceeded));
464 }
465
466 let globals = ctx.globals();
467
468 let source: PerPlatform<RemoteRockSource> =
469 globals.get_rockspec_key(ctx, "source")?;
470
471 Ok(Ok(source))
472 })
473 .map_err(|cause| LuaRockspecError::from_ottavino(cause, rockspec_content))??;
474
475 Ok(RemoteLuaRockspec {
476 local: LocalLuaRockspec::new(rockspec_content, ProjectRoot::new())?,
478 source,
479 })
480 }
481
482 pub(crate) fn from_source_spec(
487 package_spec: PackageSpec,
488 source_spec: RockSourceSpec,
489 dependencies: Vec<LuaDependencySpec>,
490 modules: HashMap<LuaModule, ModuleSpec>,
491 ) -> Self {
492 let version = package_spec.version().clone();
493 let rockspec_format = RockspecFormat::default();
494 let (build_backend, build_lua) = if modules.is_empty() {
495 (BuildBackendSpec::Source, r#"type = "source""#.to_string())
496 } else {
497 let modules_lua = modules_lua(&modules);
498 (
499 BuildBackendSpec::Builtin(BuiltinBuildSpec { modules }),
500 format!("type = \"builtin\",\n modules = {{\n{modules_lua}\n }}"),
501 )
502 };
503 let build_spec = BuildSpec {
504 build_backend: Some(build_backend),
505 ..Default::default()
506 };
507 let raw_content = rockspec_content(&package_spec, &source_spec, &build_lua);
508 let source: RemoteRockSource = source_spec.into();
509
510 let local = LocalLuaRockspec {
511 rockspec_format: Some(rockspec_format),
512 package: package_spec.name().clone(),
513 version,
514 description: RockDescription::default(),
515 supported_platforms: PlatformSupport::default(),
516 lua: PackageVersionReq::Any,
517 dependencies: PerPlatform::new(dependencies),
518 build_dependencies: PerPlatform::default(),
519 external_dependencies: PerPlatform::default(),
520 test_dependencies: PerPlatform::default(),
521 build: PerPlatform::new(build_spec),
522 source: PerPlatform::new(source.clone()),
523 test: PerPlatform::default(),
524 deploy: PerPlatform::default(),
525 raw_content,
526 };
527 Self {
528 local,
529 source: PerPlatform::new(source),
530 }
531 }
532}
533
534fn rockspec_content(
535 package_spec: &PackageSpec,
536 source_spec: &RockSourceSpec,
537 build_lua: &str,
538) -> String {
539 format!(
540 r#"
541rockspec_format = "{}"
542package = "{}"
543version = "{}"
544{}
545build = {{
546{}
547}}"#,
548 RockspecFormat::default(),
549 package_spec.name(),
550 package_spec.version(),
551 source_spec.display_lua(),
552 build_lua,
553 )
554}
555
556fn modules_lua(modules: &HashMap<LuaModule, ModuleSpec>) -> String {
557 let mut entries: Vec<_> = modules.iter().collect();
558 entries.sort_by_key(|(module, _)| module.as_str());
559 DisplayLuaValue::Table(
560 entries
561 .into_iter()
562 .map(|(module, spec)| DisplayLuaKV {
563 key: module.to_string(),
564 value: spec.display_lua_value(),
565 })
566 .collect(),
567 )
568 .to_string()
569}
570
571impl Rockspec for RemoteLuaRockspec {
572 type Error = Infallible;
573
574 fn package(&self) -> &PackageName {
575 self.local.package()
576 }
577
578 fn version(&self) -> &PackageVersion {
579 self.local.version()
580 }
581
582 fn description(&self) -> &RockDescription {
583 self.local.description()
584 }
585
586 fn supported_platforms(&self) -> &PlatformSupport {
587 self.local.supported_platforms()
588 }
589
590 fn lua(&self) -> &PackageVersionReq {
591 self.local.lua()
592 }
593
594 fn dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
595 self.local.dependencies()
596 }
597
598 fn build_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
599 match self.format() {
600 Some(RockspecFormat::_1_0 | RockspecFormat::_2_0)
603 if self
604 .build()
605 .current_platform()
606 .build_backend
607 .as_ref()
608 .is_some_and(|build_backend| build_backend.can_use_build_dependencies()) =>
609 {
610 self.local.dependencies()
611 }
612 _ => self.local.build_dependencies(),
613 }
614 }
615
616 fn external_dependencies(&self) -> &PerPlatform<HashMap<String, ExternalDependencySpec>> {
617 self.local.external_dependencies()
618 }
619
620 fn test_dependencies(&self) -> &PerPlatform<Vec<LuaDependencySpec>> {
621 self.local.test_dependencies()
622 }
623
624 fn build(&self) -> &PerPlatform<BuildSpec> {
625 self.local.build()
626 }
627
628 fn test(&self) -> &PerPlatform<TestSpec> {
629 self.local.test()
630 }
631
632 fn source(&self) -> &PerPlatform<RemoteRockSource> {
633 &self.source
634 }
635
636 fn deploy(&self) -> &PerPlatform<DeploySpec> {
637 self.local.deploy()
638 }
639
640 fn build_mut(&mut self) -> &mut PerPlatform<BuildSpec> {
641 self.local.build_mut()
642 }
643
644 fn test_mut(&mut self) -> &mut PerPlatform<TestSpec> {
645 self.local.test_mut()
646 }
647
648 fn source_mut(&mut self) -> &mut PerPlatform<RemoteRockSource> {
649 &mut self.source
650 }
651
652 fn deploy_mut(&mut self) -> &mut PerPlatform<DeploySpec> {
653 self.local.deploy_mut()
654 }
655
656 fn format(&self) -> &Option<RockspecFormat> {
657 self.local.format()
658 }
659
660 fn to_lua_remote_rockspec_string(&self) -> Result<String, Self::Error> {
661 Ok(self.local.raw_content.clone())
662 }
663}
664
665#[derive(Error, Debug, Diagnostic)]
666#[non_exhaustive]
667pub enum LuaVersionError {
668 #[error(
669 r#"
670The lua version {0} is not supported by {1} version {2}.
671
672HINT: If Lux has auto-detected an incompatible Lua installation,
673 use `--lua-version` to specify the Lua version to use.
674 Valid versions are: '5.1', '5.2', '5.3', '5.4', '5.5', 'jit', 'jit52' and 'luau'.
675"#
676 )]
677 LuaVersionUnsupported(LuaVersion, PackageName, PackageVersion),
678 #[error(transparent)]
679 #[diagnostic(transparent)]
680 LuaVersionUnset(#[from] LuaVersionUnset),
681}
682
683impl HasIntegrity for RemoteLuaRockspec {
684 async fn hash(&self) -> io::Result<Integrity> {
685 Ok(Integrity::from(&self.local.raw_content))
686 }
687}
688
689#[derive(Clone, Deserialize, Debug, PartialEq, Default, lux_macros::DisplayAsLuaKV)]
691#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
692#[display_lua(key = "description")]
693pub struct RockDescription {
694 pub summary: Option<String>,
696 pub detailed: Option<String>,
698 pub license: Option<String>,
700 #[serde(default, deserialize_with = "deserialize_url")]
702 #[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
703 pub homepage: Option<Url>,
704 #[serde(default, deserialize_with = "deserialize_url")]
706 #[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
707 pub issues_url: Option<Url>,
708 pub maintainer: Option<String>,
710 #[serde(default)]
712 pub labels: Vec<String>,
713}
714
715fn find_key_span(rockspec_content: &str, key: &str) -> SourceSpan {
716 rockspec_content
717 .match_indices(key)
718 .find(|&(start, _)| {
719 let end = start + key.len();
720 let prefix_ok =
721 start == 0 || !rockspec_content.as_bytes()[start - 1].is_ascii_alphanumeric();
722 let suffix_ok = rockspec_content
723 .get(end..)
724 .is_some_and(|s| s.starts_with('=') || s.trim_start().starts_with('='));
725 prefix_ok && suffix_ok
726 })
727 .map(|(start, _)| (start..start + key.len()).into())
728 .unwrap_or((0..0).into())
729}
730
731fn find_value_span(rockspec_content: &str, key: &str) -> SourceSpan {
732 let key = find_key_span(rockspec_content, key);
733 if key.is_empty() {
734 return key;
735 }
736 let key_offset = key.offset();
737 let key_end = key_offset + key.len();
738 let eq_start = key_end + rockspec_content[key_end..].find('=').unwrap_or(0);
739 let value_offset = eq_start + 1;
740 let line_end = rockspec_content[value_offset..]
741 .find('\n')
742 .map(|i| value_offset + i)
743 .unwrap_or(rockspec_content.len());
744 let trimmed = &rockspec_content[value_offset..line_end];
745 let start = value_offset + (trimmed.len() - trimmed.trim_start().len());
746 let end = line_end - (trimmed.len() - trimmed.trim_end().len());
747 (start..end).into()
748}
749
750fn deserialize_url<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
751where
752 D: serde::Deserializer<'de>,
753{
754 let s = Option::<String>::deserialize(deserializer)?;
755 s.map(|s| Url::parse(&s).map_err(serde::de::Error::custom))
756 .transpose()
757}
758
759#[derive(Error, Debug, Diagnostic)]
760#[non_exhaustive]
761#[error("invalid rockspec format")]
762pub struct InvalidRockspecFormat(String);
763
764#[derive(Default, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
765#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
766pub enum RockspecFormat {
767 #[serde(rename = "1.0")]
768 _1_0,
769 #[serde(rename = "2.0")]
770 _2_0,
771 #[serde(rename = "3.0")]
772 #[default]
773 _3_0,
774}
775
776impl FromStr for RockspecFormat {
777 type Err = InvalidRockspecFormat;
778
779 fn from_str(s: &str) -> Result<Self, Self::Err> {
780 match s {
781 "1.0" => Ok(Self::_1_0),
782 "2.0" => Ok(Self::_2_0),
783 "3.0" => Ok(Self::_3_0),
784 txt => Err(InvalidRockspecFormat(txt.to_string())),
785 }
786 }
787}
788
789impl Display for RockspecFormat {
790 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
791 match self {
792 Self::_1_0 => write!(f, "1.0"),
793 Self::_2_0 => write!(f, "2.0"),
794 Self::_3_0 => write!(f, "3.0"),
795 }
796 }
797}
798
799fn parse_lua_tbl_or_default<T>(
800 ctx: ottavino::Context<'_>,
801 field: &str,
802) -> Result<T, ParseLuaRockspecFieldError>
803where
804 T: Default,
805 T: DeserializeOwned,
806{
807 let ret = match ctx.globals().get_value(ctx, field.to_string()) {
808 ottavino::Value::Nil => T::default(),
809 value @ ottavino::Value::Table(_) => {
810 from_value(value).map_err(|source| ParseLuaRockspecFieldError::DeserializeTable {
811 field: field.to_string(),
812 source: Box::new(source),
813 })?
814 }
815 value => Err(ParseLuaRockspecFieldError::NotATable {
816 field: field.to_string(),
817 invalid_type: value.type_name().to_string(),
818 })?,
819 };
820 Ok(ret)
821}
822
823#[cfg(test)]
824mod tests {
825 use std::path::PathBuf;
826
827 use crate::git::GitSource;
828 use crate::lua_rockspec::PlatformIdentifier;
829 use crate::package::PackageSpec;
830
831 use super::*;
832
833 #[test]
834 pub fn parse_rockspec() {
835 let rockspec_content = "
836 rockspec_format = '1.0'\n
837 package = 'foo'\n
838 version = '1.0.0-1'\n
839 source = {\n
840 url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
841 }\n
842 "
843 .to_string();
844 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
845 assert_eq!(rockspec.local.rockspec_format, Some(RockspecFormat::_1_0));
846 assert_eq!(rockspec.local.package, "foo".into());
847 assert_eq!(rockspec.local.version, "1.0.0-1".parse().unwrap());
848 assert_eq!(rockspec.local.description, RockDescription::default());
849
850 let rockspec_content = "
851 package = 'bar'\n
852 version = '2.0.0-1'\n
853 description = {}\n
854 source = {\n
855 url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
856 }\n
857 "
858 .to_string();
859 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
860 assert_eq!(rockspec.local.rockspec_format, None);
861 assert_eq!(rockspec.local.package, "bar".into());
862 assert_eq!(rockspec.local.version, "2.0.0-1".parse().unwrap());
863 assert_eq!(rockspec.local.description, RockDescription::default());
864
865 let rockspec_content = "
866 package = 'rocks.nvim'\n
867 version = '3.0.0-1'\n
868 description = {\n
869 summary = 'some summary',
870 detailed = 'some detailed description',
871 license = 'MIT',
872 homepage = 'https://github.com/lumen-oss/rocks.nvim',
873 issues_url = 'https://github.com/lumen-oss/rocks.nvim/issues',
874 maintainer = 'Lumen Labs',
875 }\n
876 source = {\n
877 url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
878 }\n
879 "
880 .to_string();
881 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
882 assert_eq!(rockspec.local.rockspec_format, None);
883 assert_eq!(rockspec.local.package, "rocks.nvim".into());
884 assert_eq!(rockspec.local.version, "3.0.0-1".parse().unwrap());
885 let expected_description = RockDescription {
886 summary: Some("some summary".into()),
887 detailed: Some("some detailed description".into()),
888 license: Some("MIT".into()),
889 homepage: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim").unwrap()),
890 issues_url: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim/issues").unwrap()),
891 maintainer: Some("Lumen Labs".into()),
892 labels: Vec::new(),
893 };
894 assert_eq!(rockspec.local.description, expected_description);
895
896 let rockspec_content = "
897 package = 'rocks.nvim'\n
898 version = '3.0.0-1'\n
899 description = {\n
900 summary = 'some summary',
901 detailed = 'some detailed description',
902 license = 'MIT',
903 homepage = 'https://github.com/lumen-oss/rocks.nvim',
904 issues_url = 'https://github.com/lumen-oss/rocks.nvim/issues',
905 maintainer = 'Lumen Labs',
906 labels = {},
907 }\n
908 external_dependencies = { FOO = { library = 'foo' } }\n
909 source = {\n
910 url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
911 }\n
912 "
913 .to_string();
914 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
915 assert_eq!(rockspec.local.rockspec_format, None);
916 assert_eq!(rockspec.local.package, "rocks.nvim".into());
917 assert_eq!(rockspec.local.version, "3.0.0-1".parse().unwrap());
918 let expected_description = RockDescription {
919 summary: Some("some summary".into()),
920 detailed: Some("some detailed description".into()),
921 license: Some("MIT".into()),
922 homepage: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim").unwrap()),
923 issues_url: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim/issues").unwrap()),
924 maintainer: Some("Lumen Labs".into()),
925 labels: Vec::new(),
926 };
927 assert_eq!(rockspec.local.description, expected_description);
928 assert_eq!(
929 *rockspec
930 .local
931 .external_dependencies
932 .default
933 .get("FOO")
934 .unwrap(),
935 ExternalDependencySpec {
936 library: Some("foo".into()),
937 header: None
938 }
939 );
940
941 let rockspec_content = "
942 package = 'rocks.nvim'\n
943 version = '3.0.0-1'\n
944 description = {\n
945 summary = 'some summary',
946 detailed = 'some detailed description',
947 license = 'MIT',
948 homepage = 'https://github.com/lumen-oss/rocks.nvim',
949 issues_url = 'https://github.com/lumen-oss/rocks.nvim/issues',
950 maintainer = 'Lumen Labs',
951 labels = { 'package management', },
952 }\n
953 supported_platforms = { 'unix', '!windows' }\n
954 dependencies = { 'neorg ~> 6' }\n
955 build_dependencies = { 'foo' }\n
956 external_dependencies = { FOO = { header = 'foo.h' } }\n
957 test_dependencies = { 'busted >= 2.0.0' }\n
958 source = {\n
959 url = 'git+https://github.com/lumen-oss/rocks.nvim',\n
960 hash = 'sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=',\n
961 }\n
962 "
963 .to_string();
964 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
965 assert_eq!(rockspec.local.rockspec_format, None);
966 assert_eq!(rockspec.local.package, "rocks.nvim".into());
967 assert_eq!(rockspec.local.version, "3.0.0-1".parse().unwrap());
968 let expected_description = RockDescription {
969 summary: Some("some summary".into()),
970 detailed: Some("some detailed description".into()),
971 license: Some("MIT".into()),
972 homepage: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim").unwrap()),
973 issues_url: Some(Url::parse("https://github.com/lumen-oss/rocks.nvim/issues").unwrap()),
974 maintainer: Some("Lumen Labs".into()),
975 labels: vec!["package management".into()],
976 };
977 assert_eq!(rockspec.local.description, expected_description);
978 assert!(rockspec
979 .local
980 .supported_platforms
981 .is_supported(&PlatformIdentifier::Unix));
982 assert!(!rockspec
983 .local
984 .supported_platforms
985 .is_supported(&PlatformIdentifier::Windows));
986 let neorg = PackageSpec::parse("neorg".into(), "6.0.0".into()).unwrap();
987 assert!(rockspec
988 .local
989 .dependencies
990 .default
991 .into_iter()
992 .any(|dep| dep.matches(&neorg)));
993 let foo = PackageSpec::parse("foo".into(), "1.0.0".into()).unwrap();
994 assert!(rockspec
995 .local
996 .build_dependencies
997 .default
998 .into_iter()
999 .any(|dep| dep.matches(&foo)));
1000 let busted = PackageSpec::parse("busted".into(), "2.2.0".into()).unwrap();
1001 assert_eq!(
1002 *rockspec
1003 .local
1004 .external_dependencies
1005 .default
1006 .get("FOO")
1007 .unwrap(),
1008 ExternalDependencySpec {
1009 header: Some("foo.h".into()),
1010 library: None
1011 }
1012 );
1013 assert!(rockspec
1014 .local
1015 .test_dependencies
1016 .default
1017 .into_iter()
1018 .any(|dep| dep.matches(&busted)));
1019
1020 let rockspec_content = "
1021 rockspec_format = '1.0'\n
1022 package = 'foo'\n
1023 version = '1.0.0-1'\n
1024 source = {\n
1025 url = 'git+https://hub.com/owner/example-project/',\n
1026 branch = 'bar',\n
1027 }\n
1028 "
1029 .to_string();
1030 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1031 assert_eq!(
1032 rockspec.local.source.default.source_spec,
1033 RockSourceSpec::Git(GitSource {
1034 url: "https://hub.com/owner/example-project/".parse().unwrap(),
1035 git_ref: Some(GitRef::Branch("bar".into()))
1036 })
1037 );
1038 assert_eq!(rockspec.local.test, PerPlatform::default());
1039 let rockspec_content = "
1040 rockspec_format = '1.0'\n
1041 package = 'foo'\n
1042 version = '1.0.0-1'\n
1043 source = {\n
1044 url = 'git+https://hub.com/owner/example-project/',\n
1045 tag = 'bar',\n
1046 }\n
1047 "
1048 .to_string();
1049 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1050 assert_eq!(
1051 rockspec.local.source.default.source_spec,
1052 RockSourceSpec::Git(GitSource {
1053 url: "https://hub.com/owner/example-project/".parse().unwrap(),
1054 git_ref: Some(GitRef::Tag("bar".into()))
1055 })
1056 );
1057 let rockspec_content = "
1058 rockspec_format = '1.0'\n
1059 package = 'foo'\n
1060 version = '1.0.0-1'\n
1061 source = {\n
1062 url = 'git+https://hub.com/owner/example-project/',\n
1063 branch = 'bar',\n
1064 tag = 'baz',\n
1065 }\n
1066 "
1067 .to_string();
1068 let _rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap_err();
1069 let rockspec_content = "
1070 rockspec_format = '1.0'\n
1071 package = 'foo'\n
1072 version = '1.0.0-1'\n
1073 source = {\n
1074 url = 'git+https://hub.com/owner/example-project/',\n
1075 tag = 'bar',\n
1076 file = 'foo.tar.gz',\n
1077 }\n
1078 build = {\n
1079 install = {\n
1080 conf = {['foo.bar'] = 'config/bar.toml'},\n
1081 },\n
1082 }\n
1083 "
1084 .to_string();
1085 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1086 assert_eq!(
1087 rockspec.local.source.default.archive_name,
1088 Some("foo.tar.gz".into())
1089 );
1090 let foo_bar_path = rockspec
1091 .local
1092 .build
1093 .default
1094 .install
1095 .conf
1096 .get("foo.bar")
1097 .unwrap();
1098 assert_eq!(*foo_bar_path, PathBuf::from("config/bar.toml"));
1099 let rockspec_content = "
1100 rockspec_format = '1.0'\n
1101 package = 'foo'\n
1102 version = '1.0.0-1'\n
1103 source = {\n
1104 url = 'git+https://hub.com/example-project/foo.zip',\n
1105 }\n
1106 build = {\n
1107 install = {\n
1108 lua = {\n
1109 'foo.lua',\n
1110 ['foo.bar'] = 'src/bar.lua',\n
1111 },\n
1112 bin = {['foo.bar'] = 'bin/bar'},\n
1113 },\n
1114 }\n
1115 "
1116 .to_string();
1117 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1118 assert!(matches!(
1119 rockspec.local.build.default.build_backend,
1120 Some(BuildBackendSpec::Builtin { .. })
1121 ));
1122 let install_lua_spec = rockspec.local.build.default.install.lua;
1123 let foo_bar_path = install_lua_spec
1124 .get(&LuaModule::from_str("foo.bar").unwrap())
1125 .unwrap();
1126 assert_eq!(*foo_bar_path, PathBuf::from("src/bar.lua"));
1127 let foo_path = install_lua_spec
1128 .get(&LuaModule::from_str("foo").unwrap())
1129 .unwrap();
1130 assert_eq!(*foo_path, PathBuf::from("foo.lua"));
1131 let foo_bar_path = rockspec
1132 .local
1133 .build
1134 .default
1135 .install
1136 .bin
1137 .get("foo.bar")
1138 .unwrap();
1139 assert_eq!(*foo_bar_path, PathBuf::from("bin/bar"));
1140 let rockspec_content = "
1141 rockspec_format = '1.0'\n
1142 package = 'foo'\n
1143 version = '1.0.0-1'\n
1144 source = {\n
1145 url = 'git+https://hub.com/example-project/',\n
1146 }\n
1147 build = {\n
1148 copy_directories = { 'lua' },\n
1149 }\n
1150 "
1151 .to_string();
1152 let _rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap_err();
1153 let rockspec_content = "
1154 rockspec_format = '1.0'\n
1155 package = 'foo'\n
1156 version = '1.0.0-1'\n
1157 source = {\n
1158 url = 'git+https://hub.com/example-project/',\n
1159 }\n
1160 build = {\n
1161 copy_directories = { 'lib' },\n
1162 }\n
1163 "
1164 .to_string();
1165 let _rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap_err();
1166 let rockspec_content = "
1167 rockspec_format = '1.0'\n
1168 package = 'foo'\n
1169 version = '1.0.0-1'\n
1170 source = {\n
1171 url = 'git+https://hub.com/example-project/',\n
1172 }\n
1173 build = {\n
1174 copy_directories = { 'rock_manifest' },\n
1175 }\n
1176 "
1177 .to_string();
1178 let _rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap_err();
1179 let rockspec_content = "
1180 rockspec_format = '1.0'\n
1181 package = 'foo'\n
1182 version = '1.0.0-1'\n
1183 source = {\n
1184 url = 'git+https://hub.com/example-project/foo.zip',\n
1185 dir = 'baz',\n
1186 }\n
1187 build = {\n
1188 type = 'make',\n
1189 install = {\n
1190 lib = {['foo.so'] = 'lib/bar.so'},\n
1191 },\n
1192 copy_directories = {\n
1193 'plugin',\n
1194 'ftplugin',\n
1195 },\n
1196 patches = {\n
1197 ['lua51-support.diff'] = [[\n
1198 --- before.c\n
1199 +++ path/to/after.c\n
1200 ]],\n
1201 },\n
1202 }\n
1203 "
1204 .to_string();
1205 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1206 assert_eq!(rockspec.local.source.default.unpack_dir, Some("baz".into()));
1207 assert_eq!(
1208 rockspec.local.build.default.build_backend,
1209 Some(BuildBackendSpec::Make(MakeBuildSpec::default()))
1210 );
1211 let foo_bar_path = rockspec
1212 .local
1213 .build
1214 .default
1215 .install
1216 .lib
1217 .get("foo.so")
1218 .unwrap();
1219 assert_eq!(*foo_bar_path, PathBuf::from("lib/bar.so"));
1220 let copy_directories = rockspec.local.build.default.copy_directories;
1221 assert_eq!(
1222 copy_directories,
1223 vec![PathBuf::from("plugin"), PathBuf::from("ftplugin")]
1224 );
1225 let patches = rockspec.local.build.default.patches;
1226 let _patch = patches.get(&PathBuf::from("lua51-support.diff")).unwrap();
1227 let rockspec_content = "
1228 rockspec_format = '1.0'\n
1229 package = 'foo'\n
1230 version = '1.0.0-1'\n
1231 source = {\n
1232 url = 'git+https://hub.com/example-project/foo.zip',\n
1233 }\n
1234 build = {\n
1235 type = 'cmake',\n
1236 }\n
1237 "
1238 .to_string();
1239 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1240 assert_eq!(
1241 rockspec.local.build.default.build_backend,
1242 Some(BuildBackendSpec::CMake(CMakeBuildSpec::default()))
1243 );
1244 let rockspec_content = "
1245 rockspec_format = '1.0'\n
1246 package = 'foo'\n
1247 version = '1.0.0-1'\n
1248 source = {\n
1249 url = 'git+https://hub.com/example-project/foo.zip',\n
1250 }\n
1251 build = {\n
1252 type = 'command',\n
1253 build_command = 'foo',\n
1254 install_command = 'bar',\n
1255 }\n
1256 "
1257 .to_string();
1258 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1259 assert!(matches!(
1260 rockspec.local.build.default.build_backend,
1261 Some(BuildBackendSpec::Command(CommandBuildSpec { .. }))
1262 ));
1263 let rockspec_content = "
1264 rockspec_format = '1.0'\n
1265 package = 'foo'\n
1266 version = '1.0.0-1'\n
1267 source = {\n
1268 url = 'git+https://hub.com/example-project/foo.zip',\n
1269 }\n
1270 build = {\n
1271 type = 'command',\n
1272 install_command = 'foo',\n
1273 }\n
1274 "
1275 .to_string();
1276 RemoteLuaRockspec::new(&rockspec_content).unwrap();
1277 let rockspec_content = "
1278 rockspec_format = '1.0'\n
1279 package = 'foo'\n
1280 version = '1.0.0-1'\n
1281 source = {\n
1282 url = 'git+https://hub.com/example-project/foo.zip',\n
1283 }\n
1284 build = {\n
1285 type = 'command',\n
1286 build_command = 'foo',\n
1287 }\n
1288 "
1289 .to_string();
1290 RemoteLuaRockspec::new(&rockspec_content).unwrap();
1291 let rockspec_content = "
1293 package = 'rocks'\n
1294 version = '3.0.0-1'\n
1295 dependencies = {\n
1296 'neorg ~> 6',\n
1297 'toml-edit ~> 1',\n
1298 platforms = {\n
1299 windows = {\n
1300 'neorg = 5.0.0',\n
1301 'toml = 1.0.0',\n
1302 },\n
1303 unix = {\n
1304 'neorg = 5.0.0',\n
1305 },\n
1306 linux = {\n
1307 'toml = 1.0.0',\n
1308 },\n
1309 },\n
1310 }\n
1311 source = {\n
1312 url = 'git+https://github.com/lumen-oss/rocks.nvim',\n
1313 hash = 'sha256-uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=',\n
1314 }\n
1315 "
1316 .to_string();
1317 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1318 let neorg_override = PackageSpec::parse("neorg".into(), "5.0.0".into()).unwrap();
1319 let toml_edit = PackageSpec::parse("toml-edit".into(), "1.0.0".into()).unwrap();
1320 let toml = PackageSpec::parse("toml".into(), "1.0.0".into()).unwrap();
1321 assert_eq!(rockspec.local.dependencies.default.len(), 2);
1322 let per_platform = &rockspec.local.dependencies.per_platform;
1323 assert_eq!(
1324 per_platform
1325 .get(&PlatformIdentifier::Windows)
1326 .unwrap()
1327 .iter()
1328 .filter(|dep| dep.matches(&neorg_override)
1329 || dep.matches(&toml_edit)
1330 || dep.matches(&toml))
1331 .count(),
1332 3
1333 );
1334 assert_eq!(
1335 per_platform
1336 .get(&PlatformIdentifier::Unix)
1337 .unwrap()
1338 .iter()
1339 .filter(|dep| dep.matches(&neorg_override)
1340 || dep.matches(&toml_edit)
1341 || dep.matches(&toml))
1342 .count(),
1343 2
1344 );
1345 assert_eq!(
1346 per_platform
1347 .get(&PlatformIdentifier::Linux)
1348 .unwrap()
1349 .iter()
1350 .filter(|dep| dep.matches(&neorg_override)
1351 || dep.matches(&toml_edit)
1352 || dep.matches(&toml))
1353 .count(),
1354 3
1355 );
1356 let rockspec_content = "
1357 package = 'rocks'\n
1358 version = '3.0.0-1'\n
1359 external_dependencies = {\n
1360 FOO = { library = 'foo' },\n
1361 platforms = {\n
1362 windows = {\n
1363 FOO = { library = 'foo.dll' },\n
1364 },\n
1365 unix = {\n
1366 BAR = { header = 'bar.h' },\n
1367 },\n
1368 linux = {\n
1369 FOO = { library = 'foo.so' },\n
1370 },\n
1371 },\n
1372 }\n
1373 source = {\n
1374 url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
1375 }\n
1376 "
1377 .to_string();
1378 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1379 assert_eq!(
1380 *rockspec
1381 .local
1382 .external_dependencies
1383 .default
1384 .get("FOO")
1385 .unwrap(),
1386 ExternalDependencySpec {
1387 library: Some("foo".into()),
1388 header: None
1389 }
1390 );
1391 let per_platform = rockspec.local.external_dependencies.per_platform;
1392 assert_eq!(
1393 *per_platform
1394 .get(&PlatformIdentifier::Windows)
1395 .and_then(|it| it.get("FOO"))
1396 .unwrap(),
1397 ExternalDependencySpec {
1398 library: Some("foo.dll".into()),
1399 header: None
1400 }
1401 );
1402 assert_eq!(
1403 *per_platform
1404 .get(&PlatformIdentifier::Unix)
1405 .and_then(|it| it.get("FOO"))
1406 .unwrap(),
1407 ExternalDependencySpec {
1408 library: Some("foo".into()),
1409 header: None
1410 }
1411 );
1412 assert_eq!(
1413 *per_platform
1414 .get(&PlatformIdentifier::Unix)
1415 .and_then(|it| it.get("BAR"))
1416 .unwrap(),
1417 ExternalDependencySpec {
1418 header: Some("bar.h".into()),
1419 library: None
1420 }
1421 );
1422 assert_eq!(
1423 *per_platform
1424 .get(&PlatformIdentifier::Linux)
1425 .and_then(|it| it.get("BAR"))
1426 .unwrap(),
1427 ExternalDependencySpec {
1428 header: Some("bar.h".into()),
1429 library: None
1430 }
1431 );
1432 assert_eq!(
1433 *per_platform
1434 .get(&PlatformIdentifier::Linux)
1435 .and_then(|it| it.get("FOO"))
1436 .unwrap(),
1437 ExternalDependencySpec {
1438 library: Some("foo.so".into()),
1439 header: None
1440 }
1441 );
1442 let rockspec_content = "
1443 rockspec_format = '1.0'\n
1444 package = 'foo'\n
1445 version = '1.0.0-1'\n
1446 source = {\n
1447 url = 'git+https://hub.com/example-project/.git',\n
1448 branch = 'bar',\n
1449 platforms = {\n
1450 macosx = {\n
1451 branch = 'mac',\n
1452 },\n
1453 windows = {\n
1454 url = 'git+https://winhub.com/example-project/.git',\n
1455 branch = 'win',\n
1456 },\n
1457 },\n
1458 }\n
1459 "
1460 .to_string();
1461 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1462 assert_eq!(
1463 rockspec.local.source.default.source_spec,
1464 RockSourceSpec::Git(GitSource {
1465 url: "https://hub.com/example-project/.git".parse().unwrap(),
1466 git_ref: Some(GitRef::Branch("bar".into()))
1467 })
1468 );
1469 assert_eq!(
1470 rockspec
1471 .source
1472 .per_platform
1473 .get(&PlatformIdentifier::MacOSX)
1474 .map(|it| it.source_spec.clone())
1475 .unwrap(),
1476 RockSourceSpec::Git(GitSource {
1477 url: "https://hub.com/example-project/.git".parse().unwrap(),
1478 git_ref: Some(GitRef::Branch("mac".into()))
1479 })
1480 );
1481 assert_eq!(
1482 rockspec
1483 .source
1484 .per_platform
1485 .get(&PlatformIdentifier::Windows)
1486 .map(|it| it.source_spec.clone())
1487 .unwrap(),
1488 RockSourceSpec::Git(GitSource {
1489 url: "https://winhub.com/example-project/.git".parse().unwrap(),
1490 git_ref: Some(GitRef::Branch("win".into()))
1491 })
1492 );
1493 let rockspec_content = "
1494 rockspec_format = '1.0'\n
1495 package = 'foo'\n
1496 version = '1.0.0-1'\n
1497 source = { url = 'git+https://hub.com/example-project/foo.zip' }\n
1498 build = {\n
1499 type = 'make',\n
1500 install = {\n
1501 lib = {['foo.bar'] = 'lib/bar.so'},\n
1502 },\n
1503 copy_directories = { 'plugin' },\n
1504 platforms = {\n
1505 unix = {\n
1506 copy_directories = { 'ftplugin' },\n
1507 },\n
1508 linux = {\n
1509 copy_directories = { 'foo' },\n
1510 },\n
1511 },\n
1512 }\n
1513 "
1514 .to_string();
1515 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1516 let per_platform = rockspec.local.build.per_platform;
1517 let unix = per_platform.get(&PlatformIdentifier::Unix).unwrap();
1518 assert_eq!(
1519 unix.copy_directories,
1520 vec![PathBuf::from("plugin"), PathBuf::from("ftplugin")]
1521 );
1522 let linux = per_platform.get(&PlatformIdentifier::Linux).unwrap();
1523 assert_eq!(
1524 linux.copy_directories,
1525 vec![
1526 PathBuf::from("plugin"),
1527 PathBuf::from("foo"),
1528 PathBuf::from("ftplugin")
1529 ]
1530 );
1531 let rockspec_content = "
1532 package = 'foo'\n
1533 version = '1.0.0-1'\n
1534 source = { url = 'git+https://hub.com/example-project/foo.zip' }\n
1535 build = {\n
1536 type = 'builtin',\n
1537 modules = {\n
1538 cjson = {\n
1539 sources = { 'lua_cjson.c', 'strbuf.c', 'fpconv.c' },\n
1540 }\n
1541 },\n
1542 platforms = {\n
1543 win32 = { modules = { cjson = { defines = {\n
1544 'DISABLE_INVALID_NUMBERS', 'USE_INTERNAL_ISINF'\n
1545 } } } }\n
1546 },\n
1547 }\n
1548 "
1549 .to_string();
1550 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1551 let win32 = rockspec.local.build.get(&PlatformIdentifier::Windows);
1552 assert_eq!(
1553 win32.build_backend,
1554 Some(BuildBackendSpec::Builtin(BuiltinBuildSpec {
1555 modules: vec![(
1556 LuaModule::from_str("cjson").unwrap(),
1557 ModuleSpec::ModulePaths(ModulePaths {
1558 sources: vec!["lua_cjson.c".into(), "strbuf.c".into(), "fpconv.c".into()],
1559 libraries: Vec::default(),
1560 defines: vec![
1561 ("DISABLE_INVALID_NUMBERS".into(), None),
1562 ("USE_INTERNAL_ISINF".into(), None)
1563 ],
1564 incdirs: Vec::default(),
1565 libdirs: Vec::default(),
1566 })
1567 )]
1568 .into_iter()
1569 .collect()
1570 }))
1571 );
1572 let rockspec_content = "
1573 rockspec_format = '1.0'\n
1574 package = 'foo'\n
1575 version = '1.0.0-1'\n
1576 deploy = {\n
1577 wrap_bin_scripts = false,\n
1578 }\n
1579 source = { url = 'git+https://hub.com/example-project/foo.zip' }\n
1580 ";
1581 let rockspec = RemoteLuaRockspec::new(rockspec_content).unwrap();
1582 let deploy_spec = &rockspec.deploy().current_platform();
1583 assert!(!deploy_spec.wrap_bin_scripts);
1584 }
1585
1586 #[test]
1587 pub fn parse_scm_rockspec() {
1588 let rockspec_content = "
1589 package = 'foo'\n
1590 version = 'scm-1'\n
1591 source = {\n
1592 url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
1593 }\n
1594 "
1595 .to_string();
1596 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1597 assert_eq!(rockspec.local.package, "foo".into());
1598 assert_eq!(rockspec.local.version, "scm-1".parse().unwrap());
1599 }
1600
1601 #[test]
1602 pub fn regression_luasystem() {
1603 let rockspec_content = String::from_utf8(
1604 std::fs::read(
1605 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1606 .join("resources/test/luasystem-0.4.4-1.rockspec"),
1607 )
1608 .unwrap(),
1609 )
1610 .unwrap();
1611 let rockspec = RemoteLuaRockspec::new(&rockspec_content).unwrap();
1612 let build_spec = rockspec.local.build.current_platform();
1613 assert!(matches!(
1614 build_spec.build_backend,
1615 Some(BuildBackendSpec::Builtin { .. })
1616 ));
1617 if let Some(BuildBackendSpec::Builtin(BuiltinBuildSpec { modules })) =
1618 &build_spec.build_backend
1619 {
1620 assert_eq!(
1621 modules.get(&LuaModule::from_str("system.init").unwrap()),
1622 Some(&ModuleSpec::SourcePath("system/init.lua".into()))
1623 );
1624 assert_eq!(
1625 modules.get(&LuaModule::from_str("system.core").unwrap()),
1626 Some(&ModuleSpec::ModulePaths(ModulePaths {
1627 sources: vec![
1628 "src/core.c".into(),
1629 "src/compat.c".into(),
1630 "src/time.c".into(),
1631 "src/environment.c".into(),
1632 "src/random.c".into(),
1633 "src/term.c".into(),
1634 "src/bitflags.c".into(),
1635 "src/wcwidth.c".into(),
1636 ],
1637 defines: luasystem_expected_defines(),
1638 libraries: luasystem_expected_libraries(),
1639 incdirs: luasystem_expected_incdirs(),
1640 libdirs: luasystem_expected_libdirs(),
1641 }))
1642 );
1643 }
1644 if let Some(BuildBackendSpec::Builtin(BuiltinBuildSpec { modules })) = &rockspec
1645 .local
1646 .build
1647 .get(&PlatformIdentifier::Windows)
1648 .build_backend
1649 {
1650 if let ModuleSpec::ModulePaths(paths) = modules
1651 .get(&LuaModule::from_str("system.core").unwrap())
1652 .unwrap()
1653 {
1654 assert_eq!(paths.libraries, luasystem_expected_windows_libraries());
1655 };
1656 }
1657 if let Some(BuildBackendSpec::Builtin(BuiltinBuildSpec { modules })) = &rockspec
1658 .local
1659 .build
1660 .get(&PlatformIdentifier::Win32)
1661 .build_backend
1662 {
1663 if let ModuleSpec::ModulePaths(paths) = modules
1664 .get(&LuaModule::from_str("system.core").unwrap())
1665 .unwrap()
1666 {
1667 assert_eq!(paths.libraries, luasystem_expected_windows_libraries());
1668 };
1669 }
1670 }
1671
1672 fn luasystem_expected_defines() -> Vec<(String, Option<String>)> {
1673 if cfg!(target_os = "windows") {
1674 vec![
1675 ("WINVER".into(), Some("0x0600".into())),
1676 ("_WIN32_WINNT".into(), Some("0x0600".into())),
1677 ]
1678 } else {
1679 Vec::default()
1680 }
1681 }
1682
1683 fn luasystem_expected_windows_libraries() -> Vec<PathBuf> {
1684 vec!["advapi32".into(), "winmm".into()]
1685 }
1686 fn luasystem_expected_libraries() -> Vec<PathBuf> {
1687 if cfg!(any(target_os = "linux", target_os = "android")) {
1688 vec!["rt".into()]
1689 } else if cfg!(target_os = "windows") {
1690 luasystem_expected_windows_libraries()
1691 } else {
1692 Vec::default()
1693 }
1694 }
1695
1696 fn luasystem_expected_incdirs() -> Vec<PathBuf> {
1697 Vec::default()
1698 }
1699
1700 fn luasystem_expected_libdirs() -> Vec<PathBuf> {
1701 Vec::default()
1702 }
1703
1704 #[test]
1705 pub fn rust_mlua_rockspec() {
1706 let rockspec_content = "
1707 package = 'foo'\n
1708 version = 'scm-1'\n
1709 source = {\n
1710 url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
1711 }\n
1712 build = {
1713 type = 'rust-mlua',
1714 modules = {
1715 'foo',
1716 bar = 'baz',
1717 },
1718 target_path = 'path/to/cargo/target/directory',
1719 default_features = false,
1720 include = {
1721 'file.lua',
1722 ['path/to/another/file.lua'] = 'another-file.lua',
1723 },
1724 features = {'extra', 'features'},
1725 }
1726 ";
1727 let rockspec = RemoteLuaRockspec::new(rockspec_content).unwrap();
1728 let build_spec = rockspec.local.build.current_platform();
1729 if let Some(BuildBackendSpec::RustMlua(build_spec)) = build_spec.build_backend.to_owned() {
1730 assert_eq!(
1731 build_spec.modules.get("foo").unwrap(),
1732 &PathBuf::from(format!("libfoo.{}", std::env::consts::DLL_EXTENSION))
1733 );
1734 assert_eq!(
1735 build_spec.modules.get("bar").unwrap(),
1736 &PathBuf::from(format!("libbaz.{}", std::env::consts::DLL_EXTENSION))
1737 );
1738 assert_eq!(
1739 build_spec.include.get(&PathBuf::from("file.lua")).unwrap(),
1740 &PathBuf::from("file.lua")
1741 );
1742 assert_eq!(
1743 build_spec
1744 .include
1745 .get(&PathBuf::from("path/to/another/file.lua"))
1746 .unwrap(),
1747 &PathBuf::from("another-file.lua")
1748 );
1749 } else {
1750 panic!("Expected RustMlua build backend");
1751 }
1752 }
1753
1754 #[test]
1755 pub fn rust_binary_rockspec() {
1756 let rockspec_content = "
1757 package = 'foo'\n
1758 version = 'scm-1'\n
1759 source = {\n
1760 url = 'https://github.com/lumen-oss/rocks.nvim/archive/1.0.0/rocks.nvim.zip',\n
1761 }\n
1762 build = {\n
1763 type = 'rust-binary',\n
1764 package = 'foo',\n
1765 features = {'extra', 'features'},\n
1766 }\n
1767 ";
1768 let rockspec = RemoteLuaRockspec::new(rockspec_content).unwrap();
1769 let build_spec = rockspec.local.build.current_platform();
1770 if let Some(BuildBackendSpec::RustBinary(build_spec)) = build_spec.build_backend.to_owned()
1771 {
1772 assert_eq!(build_spec.package, Some("foo".to_string()));
1773 assert_eq!(build_spec.features, vec!["extra", "features"]);
1774 } else {
1775 panic!("Expected RustBinary build backend");
1776 }
1777 }
1778
1779 #[tokio::test]
1780 pub async fn regression_ltui() {
1781 let content = String::from_utf8(
1782 std::fs::read(
1783 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1784 .join("resources/test/ltui-2.8-2.rockspec"),
1785 )
1786 .unwrap(),
1787 )
1788 .unwrap();
1789 RemoteLuaRockspec::new(&content).unwrap();
1790 }
1791
1792 #[test]
1795 pub fn regression_off_spec_install_binaries() {
1796 let rockspec_content = r#"
1797 package = "WSAPI"
1798 version = "1.7-1"
1799
1800 source = {
1801 url = "git://github.com/keplerproject/wsapi",
1802 tag = "v1.7",
1803 }
1804
1805 build = {
1806 type = "builtin",
1807 modules = {
1808 ["wsapi"] = "src/wsapi.lua",
1809 },
1810 -- Offending Line
1811 install = { bin = { "src/launcher/wsapi.cgi" } }
1812 }
1813 "#;
1814
1815 let rockspec = RemoteLuaRockspec::new(rockspec_content).unwrap();
1816
1817 assert_eq!(
1818 rockspec.build().current_platform().install.bin,
1819 HashMap::from([("wsapi.cgi".into(), PathBuf::from("src/launcher/wsapi.cgi"))])
1820 );
1821 }
1822
1823 #[test]
1824 pub fn regression_external_dependencies() {
1825 let content = String::from_utf8(
1826 std::fs::read(
1827 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1828 .join("resources/test/luaossl-20220711-0.rockspec"),
1829 )
1830 .unwrap(),
1831 )
1832 .unwrap();
1833 let rockspec = RemoteLuaRockspec::new(&content).unwrap();
1834 if cfg!(target_family = "unix") {
1835 assert_eq!(
1836 rockspec
1837 .local
1838 .external_dependencies
1839 .current_platform()
1840 .get("OPENSSL")
1841 .unwrap(),
1842 &ExternalDependencySpec {
1843 library: Some("ssl".into()),
1844 header: Some("openssl/ssl.h".into()),
1845 }
1846 );
1847 }
1848 let per_platform = rockspec.local.external_dependencies.per_platform;
1849 assert_eq!(
1850 *per_platform
1851 .get(&PlatformIdentifier::Windows)
1852 .and_then(|it| it.get("OPENSSL"))
1853 .unwrap(),
1854 ExternalDependencySpec {
1855 library: Some("libeay32".into()),
1856 header: Some("openssl/ssl.h".into()),
1857 }
1858 );
1859 }
1860
1861 #[test]
1862 pub fn remote_lua_rockspec_from_package_and_source_spec() {
1863 let package_req = "foo@1.0.5".parse().unwrap();
1864 let source = GitSource {
1865 url: "https://hub.com/owner/example-project.git".parse().unwrap(),
1866 git_ref: Some(GitRef::Tag("1.0.5".into())),
1867 };
1868 let source_spec = RockSourceSpec::Git(source);
1869 let rockspec = RemoteLuaRockspec::from_source_spec(
1870 package_req,
1871 source_spec.clone(),
1872 Vec::new(),
1873 HashMap::new(),
1874 );
1875 let generated_rockspec_str = rockspec.local.raw_content;
1876 let rockspec2 = RemoteLuaRockspec::new(&generated_rockspec_str).unwrap();
1877 assert_eq!(rockspec2.local.package, "foo".into());
1878 assert_eq!(rockspec2.local.version, "1.0.5".parse().unwrap());
1879 assert_eq!(rockspec2.local.source, PerPlatform::new(source_spec.into()));
1880 }
1881
1882 #[test]
1883 pub fn regression_complex_source_field() {
1884 let rockspec_content = r#"
1885 package = "say"
1886 local rock_version = "1.4.1"
1887 local rock_release = "3"
1888 local namespace = "lunarmodules"
1889 local repository = package
1890
1891 version = ("%s-%s"):format(rock_version, rock_release)
1892
1893 source = {
1894 url = ("git+https://github.com/%s/%s.git"):format(namespace, repository),
1895 branch = rock_version == "scm" and "master" or nil,
1896 tag = rock_version ~= "scm" and "v"..rock_version or nil,
1897 }
1898
1899 description = {
1900 summary = "Lua string hashing/indexing library",
1901 }
1902
1903 dependencies = {
1904 "lua >= 5.1",
1905 }
1906
1907 build = {
1908 type = "builtin",
1909 }
1910 "#
1911 .to_string();
1912 RemoteLuaRockspec::new(&rockspec_content).unwrap();
1913 }
1914
1915 fn eval_lua_global<T: serde::de::DeserializeOwned>(code: &str, key: &'static str) -> T {
1916 use ottavino::{Closure, Executor, Fuel, Lua};
1917 use ottavino_util::serde::from_value;
1918 Lua::core()
1919 .try_enter(|ctx| {
1920 let closure = Closure::load(ctx, None, code.as_bytes())?;
1921 let executor = Executor::start(ctx, closure.into(), ());
1922 executor.step(ctx, &mut Fuel::with(i32::MAX))?;
1923 from_value(ctx.globals().get_value(ctx, key)).map_err(ottavino::Error::from)
1924 })
1925 .unwrap()
1926 }
1927
1928 #[test]
1929 pub fn rock_description_roundtrip() {
1930 let desc = RockDescription {
1931 summary: Some("A great package".into()),
1932 detailed: Some("Detailed description here".into()),
1933 license: Some("MIT".into()),
1934 homepage: Some("https://example.com".parse().unwrap()),
1935 issues_url: Some("https://example.com/issues".parse().unwrap()),
1936 maintainer: Some("Some Maintainer <maintainer@example.com>".into()),
1937 labels: vec!["neovim".into(), "lua".into()],
1938 };
1939 let lua = desc.display_lua().to_string();
1940 let restored: RockDescription = eval_lua_global(&lua, "description");
1941 assert_eq!(desc, restored);
1942 }
1943
1944 #[test]
1945 pub fn rock_description_empty_roundtrip() {
1946 let desc = RockDescription::default();
1947 let lua = desc.display_lua().to_string();
1948 let lua = if lua.trim().is_empty() {
1950 "description = {}".to_string()
1951 } else {
1952 lua
1953 };
1954 let restored: RockDescription = eval_lua_global(&lua, "description");
1955 assert_eq!(desc, restored);
1956 }
1957
1958 #[test]
1959 pub fn regression_luasec() {
1960 let rockspec_content = r#"
1961package = "LuaSec"
1962version = "1.3.2-1"
1963source = {
1964 url = "git+https://github.com/brunoos/luasec",
1965 tag = "v1.3.2",
1966}
1967external_dependencies = {
1968 platforms = {
1969 unix = {
1970 OPENSSL = {
1971 header = "openssl/ssl.h",
1972 library = "ssl"
1973 }
1974 },
1975 windows = {
1976 OPENSSL = {
1977 header = "openssl/ssl.h",
1978 }
1979 },
1980 }
1981}
1982"#;
1983 let rockspec = RemoteLuaRockspec::new(rockspec_content).unwrap();
1984 let linux_external_deps = rockspec
1985 .external_dependencies()
1986 .get(&PlatformIdentifier::Linux);
1987 assert!(linux_external_deps.get("OPENSSL").is_some());
1988 let windows_external_deps = rockspec
1989 .external_dependencies()
1990 .get(&PlatformIdentifier::Windows);
1991 assert!(windows_external_deps.get("OPENSSL").is_some());
1992 }
1993
1994 #[test]
1995 pub fn find_value_span_spaces() {
1996 let source = "package = 'foo'\nversion = '1.0'";
1997 let span = find_value_span(source, "package");
1998 assert_eq!(&source[span.offset()..span.offset() + span.len()], "'foo'");
1999 }
2000
2001 #[test]
2002 pub fn find_value_span_no_spaces() {
2003 let source = "package='foo'";
2004 let span = find_value_span(source, "package");
2005 assert_eq!(&source[span.offset()..span.offset() + span.len()], "'foo'");
2006 }
2007
2008 #[test]
2009 pub fn find_value_span_returns_empty_when_not_found() {
2010 let source = "version = '1.0'";
2011 let span = find_value_span(source, "package");
2012 assert_eq!(span.len(), 0);
2013 }
2014
2015 #[test]
2016 pub fn find_value_span_skips_url_containing_field() {
2017 let source = "homepage = 'https://luapackager.com'\npackage = 'foo'";
2018 let span = find_value_span(source, "package");
2019 assert_eq!(&source[span.offset()..span.offset() + span.len()], "'foo'");
2020 }
2021
2022 #[test]
2023 pub fn find_value_span_double_quoted() {
2024 let source = r#"package = "foo""#;
2025 let span = find_value_span(source, "package");
2026 assert_eq!(
2027 &source[span.offset()..span.offset() + span.len()],
2028 r#""foo""#
2029 );
2030 }
2031
2032 #[test]
2033 pub fn find_value_span_table() {
2034 let source = "description = {\n summary = 'bar'\n}";
2035 let span = find_value_span(source, "description");
2036 assert_eq!(&source[span.offset()..span.offset() + span.len()], "{");
2037 }
2038
2039 #[test]
2040 pub fn parse_lua_rockspec_field_error() {
2041 let rockspec_content = "package = 'my-lua-lib'\nfoo = 123";
2042 let extern_err = ottavino::Lua::core()
2043 .try_enter(|_ctx| {
2044 Err(ParseLuaRockspecFieldError::Deserialize {
2045 field: "foo".into(),
2046 source: Box::new(ottavino_util::serde::de::Error::Message(
2047 "expected bar".into(),
2048 )),
2049 })?;
2050 Ok(())
2051 })
2052 .unwrap_err();
2053 let err = LuaRockspecError::from_ottavino(extern_err, rockspec_content);
2054 match err {
2055 LuaRockspecError::ExecutionError {
2056 cause,
2057 err_span: Some(span),
2058 ..
2059 } => {
2060 assert!(cause.contains("invalid rockspec field 'foo'"));
2061 assert_eq!(
2062 &rockspec_content[span.offset()..span.offset() + span.len()],
2063 "foo"
2064 );
2065 }
2066 other => panic!("expected ExecutionError with err_span, got: {other:?}"),
2067 }
2068 let extern_err = ottavino::Lua::core()
2069 .try_enter(|_ctx| {
2070 Err(ParseLuaRockspecFieldError::DeserializeTable {
2071 field: "foo".into(),
2072 source: Box::new(ottavino_util::serde::de::Error::Message(
2073 "expected bar".into(),
2074 )),
2075 })?;
2076 Ok(())
2077 })
2078 .unwrap_err();
2079 let err = LuaRockspecError::from_ottavino(extern_err, rockspec_content);
2080 match err {
2081 LuaRockspecError::ExecutionError {
2082 cause,
2083 err_span: Some(span),
2084 ..
2085 } => {
2086 assert!(cause.contains("invalid rockspec field 'foo'"));
2087 assert_eq!(
2088 &rockspec_content[span.offset()..span.offset() + span.len()],
2089 "foo"
2090 );
2091 }
2092 other => panic!("expected ExecutionError with err_span, got: {other:?}"),
2093 }
2094 let extern_err = ottavino::Lua::core()
2095 .try_enter(|_ctx| {
2096 Err(ParseLuaRockspecFieldError::NotATable {
2097 field: "foo".into(),
2098 invalid_type: "integer".into(),
2099 })?;
2100 Ok(())
2101 })
2102 .unwrap_err();
2103 let err = LuaRockspecError::from_ottavino(extern_err, rockspec_content);
2104 match err {
2105 LuaRockspecError::ExecutionError {
2106 cause,
2107 err_span: Some(span),
2108 ..
2109 } => {
2110 assert!(cause.contains("invalid rockspec field 'foo'"));
2111 assert_eq!(
2112 &rockspec_content[span.offset()..span.offset() + span.len()],
2113 "foo"
2114 );
2115 }
2116 other => panic!("expected ExecutionError with err_span, got: {other:?}"),
2117 }
2118 }
2119}