Skip to main content

lux_lib/lua_rockspec/
mod.rs

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/// This error can be returned within `ottavino::Lua::try_enter`.
128/// ottavino wraps it in an [`ottavino::ExternError`], which Lux wraps inn
129/// a [`LuaRockspecError::ExecutionError`]
130#[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/// RockSpec for a local rock installation, deserialized from a `.rockspec` file
151#[derive(Clone, Debug)]
152#[cfg_attr(test, derive(PartialEq))]
153pub struct LocalLuaRockspec {
154    /// The file format version. Example: "1.0"
155    rockspec_format: Option<RockspecFormat>,
156    /// The name of the package. Example: "luasocket"
157    package: PackageName,
158    /// The version of the package, plus a suffix indicating the revision of the rockspec. Example: "2.0.1-1"
159    version: PackageVersion,
160    description: RockDescription,
161    supported_platforms: PlatformSupport,
162    /// The Lua version requirement for this rock
163    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    /// The original content of this rockspec, needed by luarocks
173    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    /// Builds a [`LocalLuaRockspec`] from an already-parsed project.
210    /// FIXME(vhyrro): investigate if necessary or can be deduplicated
211    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    /// Wraps this rockspec into a [`RemoteLuaRockspec`], overriding its source
236    /// with the given `source`.
237    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/// RockSpec for a remote rock, deserialized from a `.rockspec` file
443#[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            // NOTE: Calling this outside of `lua.try_enter` reduces error message repetition
477            local: LocalLuaRockspec::new(rockspec_content, ProjectRoot::new())?,
478            source,
479        })
480    }
481
482    /// Builds a rockspec from a source spec and dependencies.
483    ///
484    /// With no modules, the rockspec uses the `source` build backend.
485    /// Otherwise, it uses the `builtin` build backend with the given modules.
486    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            // Rockspec formats < 3.0 don't support `build_dependencies`,
601            // so we have to return regular dependencies if the build backend might need to use them.
602            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/// A rock's metadata, to be displayed on the remote package server
690#[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    /// A one-line description of the package.
695    pub summary: Option<String>,
696    /// A longer description of the package.
697    pub detailed: Option<String>,
698    /// The license used by the package.
699    pub license: Option<String>,
700    /// An URL for the project. This is not the URL for the tarball, but the address of a website.
701    #[serde(default, deserialize_with = "deserialize_url")]
702    #[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
703    pub homepage: Option<Url>,
704    /// An URL for the project's issue tracker.
705    #[serde(default, deserialize_with = "deserialize_url")]
706    #[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
707    pub issues_url: Option<Url>,
708    /// Contact information for the rockspec maintainer.
709    pub maintainer: Option<String>,
710    /// A list of short strings that specify labels for categorization of this rock.
711    #[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        // platform overrides
1292        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    // Luarocks allows the `install.bin` field to be a list, even though it
1793    // should only allow a table.
1794    #[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        // An empty description produces an empty assignment; restore from empty table.
1949        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}