use crate::cli::InitCommand;
use crate::utils::defaults;
use anyhow::{anyhow, Context, Result};
use forc_util::validate_name;
use serde::Deserialize;
use std::fs;
use std::fs::File;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use sway_utils::constants;
use url::Url;
#[derive(Debug)]
struct GitPathInfo {
owner: String,
repo_name: String,
example_name: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
enum FileType {
File,
Dir,
}
#[allow(dead_code)]
#[derive(serde::Deserialize, Debug)]
struct Links {
git: String,
html: String,
#[serde(rename = "self")]
cur: String,
}
#[allow(dead_code)]
#[derive(serde::Deserialize, Debug)]
struct ContentResponse {
#[serde(rename = "_links")]
links: Links,
download_url: Option<String>,
git_url: String,
html_url: String,
name: String,
path: String,
sha: String,
size: u64,
#[serde(rename = "type")]
file_type: FileType,
url: String,
}
pub fn init(command: InitCommand) -> Result<()> {
let project_name = command.project_name;
validate_name(&project_name, "project name")?;
match command.template {
Some(template) => {
let template_url = match template.as_str() {
"counter" => {
Url::parse("https://github.com/FuelLabs/sway/tree/master/examples/hello_world")?
}
_ => {
return Err(anyhow!(
"Unrecognized template: \n Example Templates:\n - counter"
));
}
};
init_from_git_template(project_name, &template_url)
}
None => init_new_project(project_name),
}
}
pub(crate) fn init_new_project(project_name: String) -> Result<()> {
let neat_name: String = project_name.split('/').last().unwrap().to_string();
fs::create_dir_all(Path::new(&project_name).join("src"))?;
fs::create_dir_all(Path::new(&project_name).join("tests"))?;
fs::write(
Path::new(&project_name).join(constants::MANIFEST_FILE_NAME),
defaults::default_manifest(&neat_name),
)?;
fs::write(
Path::new(&project_name).join(constants::TEST_MANIFEST_FILE_NAME),
defaults::default_tests_manifest(&neat_name),
)?;
fs::write(
Path::new(&project_name).join("src").join("main.sw"),
defaults::default_program(),
)?;
fs::write(
Path::new(&project_name).join("tests").join("harness.rs"),
defaults::default_test_program(&project_name),
)?;
fs::write(
Path::new(&project_name).join(".gitignore"),
defaults::default_gitignore(),
)?;
Ok(())
}
pub(crate) fn init_from_git_template(project_name: String, example_url: &Url) -> Result<()> {
let git = parse_github_link(example_url)?;
let custom_url = format!(
"https://api.github.com/repos/{}/{}/contents/{}",
git.owner, git.repo_name, git.example_name
);
let path = std::env::current_dir()?;
let out_dir = path.join(&project_name);
let real_name = whoami::realname();
let responses: Vec<ContentResponse> = ureq::get(&custom_url).call()?.into_json()?;
let valid_sway_project = responses
.iter()
.any(|response| response.name == "Forc.toml");
if !valid_sway_project {
return Err(anyhow!(
"The provided github URL: {} does not contain a Forc.toml file at the root",
example_url
));
}
download_contents(&custom_url, &out_dir, &responses)
.with_context(|| format!("couldn't download from: {}", &custom_url))?;
edit_forc_toml(&out_dir, &project_name, &real_name)?;
edit_cargo_toml(&out_dir, &project_name, &real_name)?;
Ok(())
}
fn parse_github_link(url: &Url) -> Result<GitPathInfo> {
let mut path_segments = url.path_segments().context("cannot be base")?;
let owner_name = path_segments
.next()
.context("Cannot parse owner name from github URL")?;
let repo_name = path_segments
.next()
.context("Cannot repository name from github URL")?;
let example_name = match path_segments
.skip(2)
.map(|s| s.to_string())
.reduce(|cur: String, nxt: String| format!("{}/{}", cur, nxt))
{
Some(example_name) => example_name,
None => "".to_string(),
};
Ok(GitPathInfo {
owner: owner_name.to_string(),
repo_name: repo_name.to_string(),
example_name,
})
}
fn edit_forc_toml(out_dir: &Path, project_name: &str, real_name: &str) -> Result<()> {
let mut file = File::open(out_dir.join(constants::MANIFEST_FILE_NAME))?;
let mut toml = String::new();
file.read_to_string(&mut toml)?;
let mut manifest_toml = toml.parse::<toml_edit::Document>()?;
let mut authors = Vec::new();
let forc_toml: toml::Value = toml::de::from_str(&toml)?;
if let Some(table) = forc_toml.as_table() {
if let Some(package) = table.get("project") {
if let Some(toml::Value::Array(authors_vec)) = package.get("authors") {
for author in authors_vec {
if let toml::value::Value::String(name) = &author {
authors.push(name.clone());
}
}
}
}
}
if authors.iter().any(|e| e != real_name) {
authors.push(real_name.to_string());
}
let authors: toml_edit::Array = authors.iter().collect();
manifest_toml["project"]["authors"] = toml_edit::value(authors);
manifest_toml["project"]["name"] = toml_edit::value(project_name);
let mut file = File::create(out_dir.join(constants::MANIFEST_FILE_NAME))?;
file.write_all(manifest_toml.to_string().as_bytes())?;
Ok(())
}
fn edit_cargo_toml(out_dir: &Path, project_name: &str, real_name: &str) -> Result<()> {
let mut file = File::open(out_dir.join(constants::TEST_MANIFEST_FILE_NAME))?;
let mut toml = String::new();
file.read_to_string(&mut toml)?;
let mut updated_authors = toml_edit::Array::default();
let cargo_toml: toml::Value = toml::de::from_str(&toml)?;
if let Some(table) = cargo_toml.as_table() {
if let Some(package) = table.get("package") {
if let Some(toml::Value::Array(authors_vec)) = package.get("authors") {
for author in authors_vec {
if let toml::value::Value::String(name) = &author {
updated_authors.push(name);
}
}
}
}
}
updated_authors.push(real_name);
let mut manifest_toml = toml.parse::<toml_edit::Document>()?;
manifest_toml["package"]["authors"] = toml_edit::value(updated_authors);
manifest_toml["package"]["name"] = toml_edit::value(project_name);
let mut file = File::create(out_dir.join(constants::TEST_MANIFEST_FILE_NAME))?;
file.write_all(manifest_toml.to_string().as_bytes())?;
Ok(())
}
fn download_file(url: &str, file_name: &str, out_dir: &Path) -> Result<PathBuf> {
let mut data = Vec::new();
let resp = ureq::get(url).call()?;
resp.into_reader().read_to_end(&mut data)?;
let path = out_dir.canonicalize()?.join(file_name);
let mut file = File::create(&path)?;
file.write_all(&data[..])?;
Ok(path)
}
fn download_contents(url: &str, out_dir: &Path, responses: &[ContentResponse]) -> Result<()> {
if !out_dir.exists() {
fs::create_dir(out_dir)?;
}
for response in responses {
match &response.file_type {
FileType::File => {
if let Some(url) = &response.download_url {
download_file(url, &response.name, out_dir)?;
}
}
FileType::Dir => {
match &response.name.as_str() {
&constants::SRC_DIR | &constants::TEST_DIRECTORY => {
let dir = out_dir.join(&response.name);
let url = format!("{}/{}", url, response.name);
let responses: Vec<ContentResponse> =
ureq::get(&url).call()?.into_json()?;
download_contents(&url, &dir, &responses)?;
}
_ => (),
}
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::parse_github_link;
use url::Url;
#[test]
fn test_github_link_parsing() {
let example_url =
Url::parse("https://github.com/FuelLabs/sway/tree/master/examples/hello_world")
.unwrap();
let git = parse_github_link(&example_url).unwrap();
assert_eq!(git.owner, "FuelLabs");
assert_eq!(git.repo_name, "sway");
assert_eq!(git.example_name, "examples/hello_world");
let example_url =
Url::parse("https://github.com/FuelLabs/swayswap-demo/tree/master/contracts").unwrap();
let git = parse_github_link(&example_url).unwrap();
assert_eq!(git.owner, "FuelLabs");
assert_eq!(git.repo_name, "swayswap-demo");
assert_eq!(git.example_name, "contracts");
}
}