use crate::error::Error;
use std::{collections::HashMap, fs, path::PathBuf};
use walkdir::WalkDir;
#[derive(Debug, Default, Clone)]
pub struct Discovered {
pub templates: HashMap<String, String>,
pub files: HashMap<String, PathBuf>,
}
#[derive(Debug, Default)]
pub struct Discovery {
registries: HashMap<String, Registry>,
}
impl Discovery {
pub fn register(&mut self, name: String, registry: Registry) {
self.registries.insert(name, registry);
}
pub fn resolve(&self, tpls: &[String]) -> Result<Discovered, Error> {
let mut templates: HashMap<String, String> = HashMap::new();
let mut files: HashMap<String, PathBuf> = HashMap::new();
for template in tpls {
let parts = template.split("::").collect::<Vec<&str>>();
let realpath = if let [registry, path] = parts[..] {
let r = self
.registries
.get(registry)
.ok_or_else(|| Error::DiscoveryNoRegistry(registry.to_string()))?;
let mut p = r.path.clone();
p.push(path);
p
} else if let [path] = parts[..] {
PathBuf::from(path)
} else {
return Err(Error::NotImplemented);
};
let prefix = realpath.to_string_lossy();
for entry in WalkDir::new(realpath.clone())
.follow_links(false)
.into_iter()
.filter_map(|e| e.ok())
.filter(|d| d.file_type().is_file() || d.file_type().is_symlink())
{
let relative = entry
.path()
.strip_prefix(prefix.to_string())
.unwrap()
.to_string_lossy();
if relative.starts_with(".git") {
continue;
}
let path = if entry.path_is_symlink() {
fs::read_link(entry.path()).map_err(Error::DiscoverySymlinkError)?
} else {
entry.clone().into_path()
};
if relative.ends_with(".j2") {
let content = fs::read_to_string(path).map_err(Error::DiscoveryReadFile)?;
templates.insert(relative.to_string(), content);
} else {
files.insert(relative.to_string(), path);
}
}
}
Ok(Discovered { templates, files })
}
}
#[derive(Debug)]
pub struct Registry {
pub path: PathBuf,
}
pub enum GitCheckoutType {
Rev(String),
Branch(String),
Tag(String),
}
impl Registry {
pub fn new(path: PathBuf) -> Self {
Self { path }
}
pub fn get_file(&self, path: &str) -> Result<String, Error> {
let mut filepath = self.path.clone();
filepath.push(path);
fs::read_to_string(filepath).map_err(Error::DiscoveryReadFile)
}
}
#[cfg(feature = "git2")]
pub fn discover_git(
repository: &str,
source: GitCheckoutType,
no_cache: bool,
) -> Result<Registry, Error> {
let mut directory = std::env::temp_dir();
let mut refspecs: Vec<String> = vec![];
let revparse = match source {
GitCheckoutType::Tag(tag) => {
refspecs.push(format!("refs/tags/{tag}:refs/remotes/origin/tags/{tag}"));
format!("refs/remotes/origin/tags/{tag}")
}
GitCheckoutType::Rev(rev) => {
refspecs.push(String::from("refs/heads/*:refs/remotes/origin/*"));
refspecs.push(String::from("HEAD:refs/remotes/origin/HEAD"));
rev
}
GitCheckoutType::Branch(branch) => {
refspecs.push(format!("refs/heads/{branch}:refs/remotes/origin/{branch}"));
format!("refs/remotes/origin/{branch}")
}
};
let digest = md5::compute(&revparse);
directory.push("schema-tools");
directory.push(format!("{digest:x}"));
if directory.exists() && no_cache {
fs::remove_dir_all(directory.as_path()).map_err(Error::DiscoveryCleanRegistryError)?;
} else if directory.exists() {
log::debug!("already exists: {:?}", directory);
return Ok(Registry::new(directory));
} else {
fs::create_dir_all(&directory).map_err(Error::DiscoveryCacheRegistryError)?;
}
log::debug!("checking out: {:?}", directory);
let repo = git2::Repository::init(directory.clone()).map_err(Error::GitDiscoveryError)?;
let mut opts = git2::FetchOptions::new();
repo.remote_anonymous(repository)
.map_err(Error::GitDiscoveryError)?
.fetch(&refspecs, Some(&mut opts), None)
.map_err(Error::GitDiscoveryError)?;
let obj = repo
.revparse_single(&revparse)
.map_err(Error::GitDiscoveryError)?;
repo.checkout_tree(&obj, None)
.map_err(Error::GitDiscoveryError)?;
Ok(Registry::new(directory))
}
#[cfg(test)]
#[cfg(feature = "git2")]
mod tests {
use super::*;
use serial_test::serial;
#[test]
#[serial]
fn test_discovery_git_inherit_templates() {
let mut discovery = Discovery::default();
let registry = discover_git(
"https://github.com/kstasik/schema-tools.git",
GitCheckoutType::Rev("a279f3b54bc7b03af83162fbf027eb781db1e046".to_string()),
false,
)
.unwrap();
discovery.register("testing".to_string(), registry);
let result = discovery
.resolve(&[
"testing::.".to_string(),
"resources/test/discovery/test2/".to_string(),
])
.unwrap();
assert_eq!(result.files.len(), 1);
assert!(result.files.contains_key("README.md"));
let content = fs::read_to_string(result.files.get("README.md").unwrap()).unwrap();
assert_eq!(content.replace('\r', ""), "# Schema Tools\n".to_string());
let template = result.templates.get("test.j2").unwrap();
assert_eq!(template, "# just test");
}
#[test]
#[serial]
fn test_discovery_git_inherit() {
let mut discovery = Discovery::default();
let registry = discover_git(
"https://github.com/kstasik/schema-tools.git",
GitCheckoutType::Rev("a279f3b54bc7b03af83162fbf027eb781db1e046".to_string()),
false,
)
.unwrap();
discovery.register("testing".to_string(), registry);
let result = discovery
.resolve(&[
"testing::.".to_string(),
"resources/test/discovery/test1/".to_string(),
])
.unwrap();
assert_eq!(result.files.len(), 1);
assert!(result.files.contains_key("README.md"));
let content = fs::read_to_string(result.files.get("README.md").unwrap()).unwrap();
assert_eq!(content, "# just a test case".to_string());
}
#[test]
#[serial]
fn test_discovery_git() {
let mut discovery = Discovery::default();
let registry = discover_git(
"https://github.com/kstasik/schema-tools.git",
GitCheckoutType::Rev("a279f3b54bc7b03af83162fbf027eb781db1e046".to_string()),
false,
)
.unwrap();
discovery.register("testing".to_string(), registry);
let result = discovery.resolve(&["testing::.".to_string()]).unwrap();
assert_eq!(result.files.len(), 1);
assert!(result.files.contains_key("README.md"));
}
#[test]
fn test_discovery_file() {
let discovery = Discovery::default();
let result = discovery
.resolve(&["./resources/test/".to_string()])
.unwrap();
let mut path = PathBuf::from("json-schemas");
path.push("01-simple.json");
let expected = path.to_str().unwrap();
assert!(
result.files.contains_key(expected),
"cant find {} in {:?}",
expected,
result.files
);
}
#[test]
#[serial]
fn test_discover_git_hash() {
let registry = discover_git(
"https://github.com/kstasik/schema-tools.git",
GitCheckoutType::Rev("a279f3b54bc7b03af83162fbf027eb781db1e046".to_string()),
false,
)
.unwrap();
let data = registry.get_file("README.md").unwrap();
let expected = "# Schema Tools\n";
assert_eq!(data.replace('\r', ""), expected);
}
#[test]
fn test_discover_git_branch() {
let registry = discover_git(
"https://github.com/kstasik/schema-tools.git",
GitCheckoutType::Branch("bugfix/title-conflict".to_string()),
false,
)
.unwrap();
let data = registry.get_file("Cargo.toml").unwrap();
let expected = r#"[package]
name = "schematools"
version = "0.1.0"
authors = ["Kacper Stasik <kacper@stasik.eu>"]
edition = "2018"
[dependencies]
clap = "3.0.0-beta.2"
serde_json = { version = "1", features = ["preserve_order"] }
serde_yaml = "0.8"
url = "2"
lazy_static = "1.4.0"
regex = "1"
thiserror = "1.0"
log = "0.4"
env_logger = "0.8.2"
jsonschema = "0.4"
reqwest = { version = ">= 0.10", features = ["blocking"] }
tera = { version = "1", default-features = false }
serde = { version = "1.0", features = ["derive"] }
walkdir = "2"
json-patch = "*"
[dev-dependencies]
test-case = "1""#;
assert_eq!(data.replace('\r', ""), expected);
}
#[test]
fn test_discover_git_tag() {
let registry = discover_git(
"https://github.com/kstasik/schema-tools.git",
GitCheckoutType::Tag("v0.0.1".to_string()),
false,
)
.unwrap();
let data = registry.get_file("Cargo.toml").unwrap();
let expected = r#"[package]
name = "schematools"
version = "0.1.0"
authors = ["Kacper Stasik <kacper@stasik.eu>"]
edition = "2018"
[dependencies]
clap = "3.0.0-beta.2"
serde_json = "1"
serde_yaml = "0.8"
url = "2"
lazy_static = "1.4.0"
regex = "1"
thiserror = "1.0"
log = "0.4"
env_logger = "0.8.2"
jsonschema = "0.4"
reqwest = { version = ">= 0.10", features = ["blocking"] }
[dev-dependencies]
test-case = "1""#;
assert_eq!(data.replace('\r', ""), expected);
}
#[test]
fn test_discover_git_tag_return_already_existing_registry() {
testing_logger::setup();
discover_git(
"https://github.com/kstasik/schema-tools.git",
GitCheckoutType::Tag("v0.0.2".to_string()),
true,
)
.unwrap();
discover_git(
"https://github.com/kstasik/schema-tools.git",
GitCheckoutType::Tag("v0.0.2".to_string()),
false,
)
.unwrap();
testing_logger::validate(|captured_logs| {
assert_eq!(captured_logs.len(), 2);
assert!(captured_logs[0].body.contains("checking out:"));
assert!(captured_logs[1].body.contains("already exists:"));
});
}
#[test]
fn test_discover_git_tag_clean_existing_registry() {
testing_logger::setup();
discover_git(
"https://github.com/kstasik/schema-tools.git",
GitCheckoutType::Tag("v0.0.3".to_string()),
true,
)
.unwrap();
discover_git(
"https://github.com/kstasik/schema-tools.git",
GitCheckoutType::Tag("v0.0.3".to_string()),
true,
)
.unwrap();
testing_logger::validate(|captured_logs| {
assert_eq!(captured_logs.len(), 2);
assert!(captured_logs[0].body.contains("checking out:"));
assert!(captured_logs[1].body.contains("checking out:"));
});
}
}