ambient_package/
manifest.rs

1use std::{collections::HashMap, fmt::Display, path::PathBuf};
2
3use indexmap::IndexMap;
4use rand::Rng;
5use semver::{Version, VersionReq};
6use serde::{Deserialize, Serialize};
7use sha2::Digest;
8use thiserror::Error;
9
10use crate::{
11    Component, Concept, Enum, ItemPathBuf, Message, PascalCaseIdentifier, SnakeCaseIdentifier,
12};
13
14#[derive(Error, Debug, PartialEq)]
15pub enum ManifestParseError {
16    #[error("manifest was not valid TOML: {0}")]
17    TomlError(#[from] toml::de::Error),
18    #[error("manifest contains a project and/or an ember section; projects/embers have been renamed to packages")]
19    ProjectEmberRenamedToPackageError,
20}
21
22#[derive(Deserialize, Clone, Debug, Default, PartialEq, Serialize)]
23pub struct Manifest {
24    pub package: Package,
25    #[serde(default)]
26    pub build: Build,
27    #[serde(default)]
28    #[serde(alias = "component")]
29    pub components: IndexMap<ItemPathBuf, Component>,
30    #[serde(default)]
31    #[serde(alias = "concept")]
32    pub concepts: IndexMap<ItemPathBuf, Concept>,
33    #[serde(default)]
34    #[serde(alias = "message")]
35    pub messages: IndexMap<ItemPathBuf, Message>,
36    #[serde(default)]
37    #[serde(alias = "enum")]
38    pub enums: IndexMap<PascalCaseIdentifier, Enum>,
39    #[serde(default)]
40    pub includes: HashMap<SnakeCaseIdentifier, PathBuf>,
41    #[serde(default)]
42    pub dependencies: IndexMap<SnakeCaseIdentifier, Dependency>,
43}
44impl Manifest {
45    pub fn parse(manifest: &str) -> Result<Self, ManifestParseError> {
46        let raw = toml::from_str::<toml::Table>(manifest)?;
47        if raw.contains_key("project") || raw.contains_key("ember") {
48            return Err(ManifestParseError::ProjectEmberRenamedToPackageError);
49        }
50
51        Ok(toml::from_str(manifest)?)
52    }
53
54    pub fn to_toml_string(&self) -> String {
55        toml::to_string_pretty(self).unwrap()
56    }
57}
58
59#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq, Hash, Default, Serialize)]
60#[serde(transparent)]
61/// A checksummed package ID. Guaranteed to be a valid `SnakeCaseIdentifier` as well.
62pub struct PackageId(pub(crate) String);
63impl<'de> Deserialize<'de> for PackageId {
64    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
65    where
66        D: serde::Deserializer<'de>,
67    {
68        PackageId::new(&String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
69    }
70}
71impl PackageId {
72    const DATA_LENGTH: usize = 12;
73    const CHECKSUM_LENGTH: usize = 8;
74    const TOTAL_LENGTH: usize = Self::DATA_LENGTH + Self::CHECKSUM_LENGTH;
75    // to ensure that the first character is always alphabetic we have to make sure that the highest 5 bits of the
76    // first byte are at most 25 (as Base32 encodes every 5 bits as 1 character => 0-25 as A-Z and 26-31 as digits)
77    // so the max value looks like this:
78    // 11001    = 25 (Base32 'Z') on highest 5 bits
79    //      111 = max for the lowest 3 since it can be anything
80    #[allow(clippy::unusual_byte_groupings)]
81    const MAX_VALUE_FOR_FIRST_BYTE: u8 = 0b11001_111;
82
83    pub fn as_str(&self) -> &str {
84        &self.0
85    }
86
87    /// Attempts to create a new package ID from a string.
88    pub fn new(id: &str) -> Result<Self, String> {
89        Self::validate(id)?;
90        Ok(Self(id.to_string()))
91    }
92
93    /// Generates a new package ID.
94    pub fn generate() -> Self {
95        let mut data: [u8; Self::DATA_LENGTH] = rand::random();
96        data[0] = rand::thread_rng().gen_range(0..=Self::MAX_VALUE_FOR_FIRST_BYTE);
97        let checksum: [u8; Self::CHECKSUM_LENGTH] = sha2::Sha256::digest(data)
98            [0..Self::CHECKSUM_LENGTH]
99            .try_into()
100            .unwrap();
101
102        let mut bytes = [0u8; Self::TOTAL_LENGTH];
103        bytes[0..Self::DATA_LENGTH].copy_from_slice(&data);
104        bytes[Self::DATA_LENGTH..].copy_from_slice(&checksum);
105
106        let output = data_encoding::BASE32_NOPAD
107            .encode(&bytes)
108            .to_ascii_lowercase();
109
110        assert!(output.chars().next().unwrap().is_ascii_alphabetic());
111        Self(output)
112    }
113
114    /// Validate that a package ID is correct.
115    pub fn validate(id: &str) -> Result<(), String> {
116        let cmd =
117            "Use `ambient package regenerate-id` to regenerate the package ID with the new format.";
118
119        let bytes = data_encoding::BASE32_NOPAD
120            .decode(id.to_ascii_uppercase().as_bytes())
121            .map_err(|e| format!("Package ID contained invalid characters: {e}. {cmd}"))?;
122
123        let data = &bytes[0..Self::DATA_LENGTH];
124        let checksum = &bytes[Self::DATA_LENGTH..];
125
126        let expected_checksum = &sha2::Sha256::digest(data)[0..Self::CHECKSUM_LENGTH];
127        if checksum != expected_checksum {
128            return Err(format!(
129                "Package ID contained invalid checksum: expected {:?}, got {:?}. {cmd}",
130                expected_checksum, checksum
131            ));
132        }
133
134        Ok(())
135    }
136}
137impl Display for PackageId {
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        self.0.fmt(f)
140    }
141}
142impl From<PackageId> for SnakeCaseIdentifier {
143    fn from(id: PackageId) -> Self {
144        SnakeCaseIdentifier(id.0)
145    }
146}
147
148#[derive(Deserialize, Clone, Debug, PartialEq, Serialize)]
149pub struct Package {
150    /// The ID can be optional if and only if the package is `ambient_core` or an include.
151    #[serde(default)]
152    pub id: Option<PackageId>,
153    pub name: String,
154    pub version: Version,
155    pub description: Option<String>,
156    pub repository: Option<String>,
157    pub ambient_version: Option<VersionReq>,
158    #[serde(default)]
159    pub authors: Vec<String>,
160    pub content: PackageContent,
161    #[serde(default = "return_true")]
162    pub public: bool,
163}
164impl Default for Package {
165    fn default() -> Self {
166        Self {
167            id: Default::default(),
168            name: Default::default(),
169            version: Version::parse("0.0.0").unwrap(),
170            description: Default::default(),
171            repository: Default::default(),
172            ambient_version: Default::default(),
173            authors: Default::default(),
174            content: Default::default(),
175            public: true,
176        }
177    }
178}
179
180fn return_true() -> bool {
181    true
182}
183
184// ----- NOTE: Update docs/reference/package.md when changing this ----
185
186#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
187#[serde(tag = "type")]
188pub enum PackageContent {
189    Playable {
190        #[serde(default)]
191        example: bool,
192    },
193    /// Assets are something that you can use as a dependency in your package
194    Asset {
195        #[serde(default)]
196        models: bool,
197        #[serde(default)]
198        animations: bool,
199        #[serde(default)]
200        textures: bool,
201        #[serde(default)]
202        materials: bool,
203        #[serde(default)]
204        audio: bool,
205        #[serde(default)]
206        fonts: bool,
207        #[serde(default)]
208        code: bool,
209        #[serde(default)]
210        schema: bool,
211    },
212    Tool,
213    Mod {
214        /// List of package ids that this mod is applicable to
215        #[serde(default)]
216        for_playables: Vec<String>,
217    },
218}
219impl Default for PackageContent {
220    fn default() -> Self {
221        Self::Playable { example: false }
222    }
223}
224
225// -----------------------------------------------------------------
226
227#[derive(Deserialize, Clone, Debug, PartialEq, Default, Serialize)]
228pub struct Build {
229    #[serde(default)]
230    pub rust: BuildRust,
231}
232
233#[derive(Deserialize, Clone, Debug, PartialEq, Serialize)]
234pub struct BuildRust {
235    #[serde(rename = "feature-multibuild")]
236    pub feature_multibuild: Vec<String>,
237}
238impl Default for BuildRust {
239    fn default() -> Self {
240        Self {
241            feature_multibuild: vec!["client".to_string(), "server".to_string()],
242        }
243    }
244}
245
246#[derive(Deserialize, Clone, Debug, PartialEq, Serialize)]
247pub struct Dependency {
248    #[serde(default)]
249    pub path: Option<PathBuf>,
250    #[serde(default)]
251    pub deployment: Option<String>,
252    #[serde(default)]
253    pub enabled: Option<bool>,
254}
255impl Dependency {
256    pub fn has_remote_dependency(&self) -> bool {
257        self.deployment.is_some()
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use std::path::PathBuf;
264
265    use indexmap::IndexMap;
266
267    use crate::{
268        Build, BuildRust, Component, ComponentType, Components, Concept, ConceptValue,
269        ContainerType, Dependency, Enum, Identifier, ItemPathBuf, Manifest, ManifestParseError,
270        Package, PackageId, PascalCaseIdentifier, SnakeCaseIdentifier,
271    };
272    use semver::Version;
273
274    fn i(s: &str) -> Identifier {
275        Identifier::new(s).unwrap()
276    }
277
278    fn sci(s: &str) -> SnakeCaseIdentifier {
279        SnakeCaseIdentifier::new(s).unwrap()
280    }
281
282    fn pci(s: &str) -> PascalCaseIdentifier {
283        PascalCaseIdentifier::new(s).unwrap()
284    }
285
286    fn ipb(s: &str) -> ItemPathBuf {
287        ItemPathBuf::new(s).unwrap()
288    }
289
290    #[test]
291    fn can_parse_minimal_toml() {
292        const TOML: &str = r#"
293        [package]
294        id = "lktsfudbjw2qikhyumt573ozxhadkiwm"
295        name = "Test"
296        version = "0.0.1"
297        content = { type = "Playable" }
298        "#;
299
300        assert_eq!(
301            Manifest::parse(TOML),
302            Ok(Manifest {
303                package: Package {
304                    id: Some(PackageId("lktsfudbjw2qikhyumt573ozxhadkiwm".to_string())),
305                    name: "Test".to_string(),
306                    version: Version::parse("0.0.1").unwrap(),
307                    ..Default::default()
308                },
309                ..Default::default()
310            })
311        );
312    }
313
314    #[test]
315    fn will_fail_on_legacy_project_toml() {
316        const TOML: &str = r#"
317        [project]
318        id = "lktsfudbjw2qikhyumt573ozxhadkiwm"
319        name = "Test"
320        version = "0.0.1"
321        "#;
322
323        assert_eq!(
324            Manifest::parse(TOML),
325            Err(ManifestParseError::ProjectEmberRenamedToPackageError)
326        )
327    }
328
329    #[test]
330    fn can_parse_tictactoe_toml() {
331        const TOML: &str = r#"
332        [package]
333        id = "lktsfudbjw2qikhyumt573ozxhadkiwm"
334        name = "Tic Tac Toe"
335        version = "0.0.1"
336        content = { type = "Playable" }
337
338        [components]
339        cell = { type = "i32", name = "Cell", description = "The ID of the cell this player is in", attributes = ["store"] }
340
341        [concepts.Cell]
342        name = "Cell"
343        description = "A cell object"
344        [concepts.Cell.components.required]
345        cell = {}
346        "#;
347
348        assert_eq!(
349            Manifest::parse(TOML),
350            Ok(Manifest {
351                package: Package {
352                    id: Some(PackageId("lktsfudbjw2qikhyumt573ozxhadkiwm".to_string())),
353                    name: "Tic Tac Toe".to_string(),
354                    version: Version::parse("0.0.1").unwrap(),
355                    ..Default::default()
356                },
357                build: Build {
358                    rust: BuildRust {
359                        feature_multibuild: vec!["client".to_string(), "server".to_string()]
360                    }
361                },
362                components: IndexMap::from_iter([(
363                    ipb("cell"),
364                    Component {
365                        name: Some("Cell".to_string()),
366                        description: Some("The ID of the cell this player is in".to_string()),
367                        type_: ComponentType::Item(i("i32").into()),
368                        attributes: vec![i("store").into()],
369                        default: None,
370                    }
371                )]),
372                concepts: IndexMap::from_iter([(
373                    ipb("Cell"),
374                    Concept {
375                        name: Some("Cell".to_string()),
376                        description: Some("A cell object".to_string()),
377                        extends: vec![],
378                        components: Components {
379                            required: IndexMap::from_iter([(ipb("cell"), ConceptValue::default())]),
380                            optional: Default::default()
381                        }
382                    }
383                )]),
384                messages: Default::default(),
385                enums: Default::default(),
386                includes: Default::default(),
387                dependencies: Default::default(),
388            })
389        )
390    }
391
392    #[test]
393    fn can_parse_rust_build_settings() {
394        const TOML: &str = r#"
395        [package]
396        id = "lktsfudbjw2qikhyumt573ozxhadkiwm"
397        name = "Tic Tac Toe"
398        version = "0.0.1"
399        content = { type = "Playable" }
400        ambient_version = "0.3.0-nightly-2023-08-31"
401
402        [build.rust]
403        feature-multibuild = ["client"]
404        "#;
405
406        assert_eq!(
407            Manifest::parse(TOML),
408            Ok(Manifest {
409                package: Package {
410                    id: Some(PackageId("lktsfudbjw2qikhyumt573ozxhadkiwm".to_string())),
411                    name: "Tic Tac Toe".to_string(),
412                    version: Version::parse("0.0.1").unwrap(),
413                    ambient_version: Some(
414                        semver::VersionReq::parse("0.3.0-nightly-2023-08-31").unwrap()
415                    ),
416                    ..Default::default()
417                },
418                build: Build {
419                    rust: BuildRust {
420                        feature_multibuild: vec!["client".to_string()]
421                    }
422                },
423                ..Default::default()
424            })
425        )
426    }
427
428    #[test]
429    fn can_parse_concepts_with_documented_namespace_from_manifest() {
430        use toml::Value;
431
432        const TOML: &str = r#"
433        [package]
434        id = "lktsfudbjw2qikhyumt573ozxhadkiwm"
435        name = "My Package"
436        version = "0.0.1"
437        content = { type = "Playable" }
438
439        [components]
440        "core::transform::rotation" = { type = "quat", name = "Rotation", description = "" }
441        "core::transform::scale" = { type = "vec3", name = "Scale", description = "" }
442        "core::transform::spherical_billboard" = { type = "empty", name = "Spherical billboard", description = "" }
443        "core::transform::translation" = { type = "vec3", name = "Translation", description = "" }
444
445        [concepts."ns::Transformable"]
446        name = "Transformable"
447        description = "Can be translated, rotated and scaled."
448
449        [concepts."ns::Transformable".components.required]
450        # This is intentionally out of order to ensure that order is preserved
451        "core::transform::translation" = { suggested = [0, 0, 0] }
452        "core::transform::scale" = { suggested = [1, 1, 1] }
453        "core::transform::rotation" = { suggested = [0, 0, 0, 1] }
454
455        [concepts."ns::Transformable".components.optional]
456        "core::transform::inv_local_to_world" = { description = "If specified, will be automatically updated" }
457        "#;
458
459        let manifest = Manifest::parse(TOML).unwrap();
460        assert_eq!(
461            manifest,
462            Manifest {
463                package: Package {
464                    id: Some(PackageId("lktsfudbjw2qikhyumt573ozxhadkiwm".to_string())),
465                    name: "My Package".to_string(),
466                    version: Version::parse("0.0.1").unwrap(),
467                    ..Default::default()
468                },
469                build: Build {
470                    rust: BuildRust {
471                        feature_multibuild: vec!["client".to_string(), "server".to_string()]
472                    }
473                },
474                components: IndexMap::from_iter([
475                    (
476                        ipb("core::transform::rotation"),
477                        Component {
478                            name: Some("Rotation".to_string()),
479                            description: Some("".to_string()),
480                            type_: ComponentType::Item(i("quat").into()),
481                            attributes: vec![],
482                            default: None,
483                        }
484                    ),
485                    (
486                        ipb("core::transform::scale"),
487                        Component {
488                            name: Some("Scale".to_string()),
489                            description: Some("".to_string()),
490                            type_: ComponentType::Item(i("vec3").into()),
491                            attributes: vec![],
492                            default: None,
493                        }
494                    ),
495                    (
496                        ipb("core::transform::spherical_billboard"),
497                        Component {
498                            name: Some("Spherical billboard".to_string()),
499                            description: Some("".to_string()),
500                            type_: ComponentType::Item(i("empty").into()),
501                            attributes: vec![],
502                            default: None,
503                        }
504                    ),
505                    (
506                        ipb("core::transform::translation"),
507                        Component {
508                            name: Some("Translation".to_string()),
509                            description: Some("".to_string()),
510                            type_: ComponentType::Item(i("vec3").into()),
511                            attributes: vec![],
512                            default: None,
513                        }
514                    ),
515                ]),
516                concepts: IndexMap::from_iter([(
517                    ipb("ns::Transformable"),
518                    Concept {
519                        name: Some("Transformable".to_string()),
520                        description: Some("Can be translated, rotated and scaled.".to_string()),
521                        extends: vec![],
522                        components: Components {
523                            required: IndexMap::from_iter([
524                                (
525                                    ipb("core::transform::translation"),
526                                    ConceptValue {
527                                        suggested: Some(Value::Array(vec![
528                                            Value::Integer(0),
529                                            Value::Integer(0),
530                                            Value::Integer(0)
531                                        ])),
532                                        ..Default::default()
533                                    }
534                                ),
535                                (
536                                    ipb("core::transform::scale"),
537                                    ConceptValue {
538                                        suggested: Some(Value::Array(vec![
539                                            Value::Integer(1),
540                                            Value::Integer(1),
541                                            Value::Integer(1)
542                                        ])),
543                                        ..Default::default()
544                                    }
545                                ),
546                                (
547                                    ipb("core::transform::rotation"),
548                                    ConceptValue {
549                                        suggested: Some(Value::Array(vec![
550                                            Value::Integer(0),
551                                            Value::Integer(0),
552                                            Value::Integer(0),
553                                            Value::Integer(1)
554                                        ])),
555                                        ..Default::default()
556                                    }
557                                ),
558                            ]),
559                            optional: IndexMap::from_iter([(
560                                ipb("core::transform::inv_local_to_world"),
561                                ConceptValue {
562                                    description: Some(
563                                        "If specified, will be automatically updated".to_string()
564                                    ),
565                                    ..Default::default()
566                                },
567                            )])
568                        }
569                    }
570                )]),
571                messages: Default::default(),
572                enums: Default::default(),
573                includes: Default::default(),
574                dependencies: Default::default(),
575            }
576        );
577
578        assert_eq!(
579            manifest
580                .concepts
581                .first()
582                .unwrap()
583                .1
584                .components
585                .required
586                .keys()
587                .collect::<Vec<_>>(),
588            vec![
589                &ipb("core::transform::translation"),
590                &ipb("core::transform::scale"),
591                &ipb("core::transform::rotation"),
592            ]
593        );
594    }
595
596    #[test]
597    fn can_parse_enums() {
598        const TOML: &str = r#"
599        [package]
600        id = "lktsfudbjw2qikhyumt573ozxhadkiwm"
601        name = "Tic Tac Toe"
602        version = "0.0.1"
603        content = { type = "Playable" }
604
605        [enums.CellState]
606        description = "The current cell state"
607        [enums.CellState.members]
608        Taken = "The cell is taken"
609        Free = "The cell is free"
610        "#;
611
612        assert_eq!(
613            Manifest::parse(TOML),
614            Ok(Manifest {
615                package: Package {
616                    id: Some(PackageId("lktsfudbjw2qikhyumt573ozxhadkiwm".to_string())),
617                    name: "Tic Tac Toe".to_string(),
618                    version: Version::parse("0.0.1").unwrap(),
619                    ..Default::default()
620                },
621                build: Build::default(),
622                components: Default::default(),
623                concepts: Default::default(),
624                messages: Default::default(),
625                enums: IndexMap::from_iter([(
626                    pci("CellState"),
627                    Enum {
628                        description: Some("The current cell state".to_string()),
629                        members: IndexMap::from_iter([
630                            (pci("Taken"), "The cell is taken".to_string()),
631                            (pci("Free"), "The cell is free".to_string()),
632                        ])
633                    }
634                )]),
635                includes: Default::default(),
636                dependencies: Default::default(),
637            })
638        )
639    }
640
641    #[test]
642    fn can_parse_container_types() {
643        const TOML: &str = r#"
644        [package]
645        id = "lktsfudbjw2qikhyumt573ozxhadkiwm"
646        name = "Test"
647        version = "0.0.1"
648        content = { type = "Playable" }
649
650        [components]
651        test = { type = "I32", name = "Test", description = "Test" }
652        vec_test = { type = { container_type = "Vec", element_type = "I32" }, name = "Test", description = "Test" }
653        option_test = { type = { container_type = "Option", element_type = "I32" }, name = "Test", description = "Test" }
654
655        "#;
656
657        assert_eq!(
658            Manifest::parse(TOML),
659            Ok(Manifest {
660                package: Package {
661                    id: Some(PackageId("lktsfudbjw2qikhyumt573ozxhadkiwm".to_string())),
662                    name: "Test".to_string(),
663                    version: Version::parse("0.0.1").unwrap(),
664                    ..Default::default()
665                },
666                build: Build {
667                    rust: BuildRust {
668                        feature_multibuild: vec!["client".to_string(), "server".to_string()]
669                    }
670                },
671                components: IndexMap::from_iter([
672                    (
673                        ipb("test"),
674                        Component {
675                            name: Some("Test".to_string()),
676                            description: Some("Test".to_string()),
677                            type_: ComponentType::Item(i("I32").into()),
678                            attributes: vec![],
679                            default: None,
680                        }
681                    ),
682                    (
683                        ipb("vec_test"),
684                        Component {
685                            name: Some("Test".to_string()),
686                            description: Some("Test".to_string()),
687                            type_: ComponentType::Contained {
688                                type_: ContainerType::Vec,
689                                element_type: i("I32").into()
690                            },
691                            attributes: vec![],
692                            default: None,
693                        }
694                    ),
695                    (
696                        ipb("option_test"),
697                        Component {
698                            name: Some("Test".to_string()),
699                            description: Some("Test".to_string()),
700                            type_: ComponentType::Contained {
701                                type_: ContainerType::Option,
702                                element_type: i("I32").into()
703                            },
704                            attributes: vec![],
705                            default: None,
706                        }
707                    )
708                ]),
709                concepts: Default::default(),
710                messages: Default::default(),
711                enums: Default::default(),
712                includes: Default::default(),
713                dependencies: Default::default(),
714            })
715        )
716    }
717
718    #[test]
719    fn can_parse_dependencies() {
720        const TOML: &str = r#"
721        [package]
722        id = "lktsfudbjw2qikhyumt573ozxhadkiwm"
723        name = "dependencies"
724        version = "0.0.1"
725        content = { type = "Playable" }
726
727        [dependencies]
728        deps_assets = { path = "deps/assets" }
729        deps_code = { path = "deps/code" }
730        deps_ignore_me = { path = "deps/ignore_me", enabled = false }
731        deps_remote_deployment = { deployment = "jhsdfu574S" }
732
733        "#;
734
735        assert_eq!(
736            Manifest::parse(TOML),
737            Ok(Manifest {
738                package: Package {
739                    id: Some(PackageId("lktsfudbjw2qikhyumt573ozxhadkiwm".to_string())),
740                    name: "dependencies".to_string(),
741                    version: Version::parse("0.0.1").unwrap(),
742                    ..Default::default()
743                },
744                build: Default::default(),
745                components: Default::default(),
746                concepts: Default::default(),
747                messages: Default::default(),
748                enums: Default::default(),
749                includes: Default::default(),
750                dependencies: IndexMap::from_iter([
751                    (
752                        sci("deps_assets"),
753                        Dependency {
754                            path: Some(PathBuf::from("deps/assets")),
755                            deployment: None,
756                            enabled: None,
757                        }
758                    ),
759                    (
760                        sci("deps_code"),
761                        Dependency {
762                            path: Some(PathBuf::from("deps/code")),
763                            deployment: None,
764                            enabled: None,
765                        }
766                    ),
767                    (
768                        sci("deps_ignore_me"),
769                        Dependency {
770                            path: Some(PathBuf::from("deps/ignore_me")),
771                            deployment: None,
772                            enabled: Some(false),
773                        }
774                    ),
775                    (
776                        sci("deps_remote_deployment"),
777                        Dependency {
778                            path: None,
779                            deployment: Some("jhsdfu574S".to_owned()),
780                            enabled: None,
781                        }
782                    )
783                ])
784            })
785        )
786    }
787}