1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
//! Utilities for generate ink! project files.

use self::snippets::{CARGO_TOML_PLAIN, CARGO_TOML_SNIPPET, CONTRACT_PLAIN, CONTRACT_SNIPPET};
use crate::utils;

pub mod snippets;

/// Code stubs/snippets for creating an ink! project
/// (i.e. code stubs/snippets for `lib.rs` and `Cargo.toml`).
#[derive(Debug, PartialEq, Eq)]
pub struct Project {
    /// The `lib.rs` content.
    pub lib: ProjectFile,
    /// The `Cargo.toml` content.
    pub cargo: ProjectFile,
}

/// Code stubs/snippets for creating a file in an ink! project
/// (e.g. `lib.rs` or `Cargo.toml` for an ink! contract).
#[derive(Debug, PartialEq, Eq)]
pub struct ProjectFile {
    /// A plain text code stub.
    pub plain: String,
    /// A snippet (i.e. with tab stops and/or placeholders).
    pub snippet: Option<String>,
}

/// An ink! project error.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Error {
    /// Invalid package name.
    ///
    /// Ref: <https://doc.rust-lang.org/cargo/reference/manifest.html#the-name-field>.
    PackageName,
    /// Invalid contract name.
    ///
    /// Ref: <https://github.com/paritytech/cargo-contract/blob/v3.2.0/crates/build/src/new.rs#L34-L52>.
    ContractName,
}

/// Returns code stubs/snippets for creating a new ink! project given a name.
pub fn new_project(name: String) -> Result<Project, Error> {
    // Validates that name is a valid Rust package name.
    // Ref: <https://doc.rust-lang.org/cargo/reference/manifest.html#the-name-field>.
    if name.is_empty()
        || !name
            .chars()
            .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
    {
        return Err(Error::PackageName);
    }

    // Validates that name is a valid ink! contract name (i.e. contract names must additionally begin with an alphabetic character).
    // Ref: <https://github.com/paritytech/cargo-contract/blob/v3.2.0/crates/build/src/new.rs#L34-L52>.
    if !name.chars().next().map_or(false, char::is_alphabetic) {
        return Err(Error::ContractName);
    }

    // Generates `mod` and storage `struct` names for the contract.
    let module_name = name.replace('-', "_");
    let struct_name = utils::pascal_case(&module_name);

    // Returns project code stubs/snippets.
    Ok(Project {
        // Generates `lib.rs`.
        lib: ProjectFile {
            plain: CONTRACT_PLAIN
                .replace("my_contract", &module_name)
                .replace("MyContract", &struct_name),
            snippet: Some(
                CONTRACT_SNIPPET
                    .replace("my_contract", &module_name)
                    .replace("MyContract", &struct_name),
            ),
        },
        // Generates `Cargo.toml`.
        cargo: ProjectFile {
            plain: CARGO_TOML_PLAIN.replace("my_contract", &name),
            snippet: Some(CARGO_TOML_SNIPPET.replace("my_contract", &name)),
        },
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::Analysis;

    // Ref: <https://doc.rust-lang.org/cargo/reference/manifest.html#the-name-field>.
    // Ref: <https://github.com/paritytech/cargo-contract/blob/v3.2.0/crates/build/src/new.rs#L34-L52>.
    #[test]
    fn invalid_project_name_fails() {
        for (name, expected_error) in [
            // Empty.
            ("", Error::PackageName),
            // Disallowed characters (i.e not alphanumeric, `-` or `_`).
            ("hello!", Error::PackageName),
            ("hello world", Error::PackageName),
            ("💝", Error::PackageName),
            // Starts with non-alphabetic character.
            ("1hello", Error::ContractName),
            ("-hello", Error::ContractName),
            ("_hello", Error::ContractName),
        ] {
            assert_eq!(new_project(name.to_string()), Err(expected_error));
        }
    }

    #[test]
    fn valid_project_name_works() {
        for name in ["hello", "hello_world", "hello-world"] {
            // Generates an ink! contract project.
            let result = new_project(name.to_string());
            assert!(result.is_ok());

            // Verifies that the generated code stub is a valid contract.
            let contract_code = result.unwrap().lib.plain;
            let analysis = Analysis::new(&contract_code);
            assert_eq!(analysis.diagnostics().len(), 0);
        }
    }
}