kct_package 0.4.0

KCP compiler
Documentation
mod fixtures;
mod helpers;

use fixtures::Fixture;
use kct_package::{error::Error, Package, Release};
use serde_json::{Map, Value};
use std::convert::TryFrom;
use std::fs;
use std::panic::panic_any;
use std::path::PathBuf;
use tempfile::TempDir;

fn package(with: Vec<(&str, &str)>, without: Vec<&str>) -> (Result<Package, Error>, TempDir) {
	use fs_extra::dir::{self as fsdir, CopyOptions};

	let dir = {
		let tempdir = TempDir::new().unwrap();
		let source = Fixture::path("kcp");

		let mut options = CopyOptions::new();
		options.content_only = true;
		fsdir::copy(source, tempdir.path(), &options).unwrap();

		tempdir
	};

	for (path, contents) in with {
		let to_add = dir.path().join(path);
		let parent = to_add.parent().unwrap();

		if !parent.exists() {
			fsdir::create_all(parent, false).unwrap();
		}

		fs::write(to_add, contents).unwrap();
	}

	for path in without {
		let to_remove = dir.path().join(path);

		if to_remove.is_dir() {
			fs::remove_dir_all(to_remove).unwrap();
		} else {
			fs::remove_file(to_remove).unwrap();
		}
	}

	let package = Package::try_from(PathBuf::from(dir.path()));

	(package, dir)
}

fn compile_with_example(pkg: Package, rel: Option<Release>) -> Result<Value, Error> {
	let input = pkg.example.clone().unwrap();

	pkg.compile(Some(input), rel)
}

mod try_from {
	use super::*;

	#[test]
	fn can_be_created() {
		let (package, _dir) = package(vec![], vec![]);

		assert!(package.is_ok());
	}

	#[test]
	fn from_archive() {
		let cwd = TempDir::new().unwrap();
		let (package, _dir) = package(vec![], vec![]);
		let package = package.unwrap();
		let archive = package.archive(&PathBuf::from(cwd.path())).unwrap();

		let package = Package::try_from(archive);

		assert!(package.is_ok())
	}

	#[test]
	fn need_spec() {
		let (package, _dir) = package(vec![], vec!["kcp.json"]);

		assert!(package.is_err());
		assert_eq!(package.unwrap_err(), Error::NoSpec)
	}

	#[test]
	fn requests_example_for_schema() {
		let (package, _dir) = package(vec![], vec!["example.json"]);

		assert!(package.is_err());
		assert_eq!(package.unwrap_err(), Error::NoExample)
	}

	#[test]
	fn request_schema_for_input() {
		let (package, _dir) = package(vec![], vec!["schema.json"]);

		assert!(package.is_err());
		assert_eq!(package.unwrap_err(), Error::NoSchema)
	}

	#[test]
	fn example_should_be_valid_input() {
		let (package, _dir) = package(vec![("example.json", r#"{"nothing": "none"}"#)], vec![]);

		assert!(package.is_err());
		assert_eq!(package.unwrap_err(), Error::InvalidExample);
	}

	#[test]
	fn needs_a_main_file() {
		let (package, _dir) = package(vec![], vec!["templates/main.jsonnet"]);

		assert!(package.is_err());
		assert_eq!(package.unwrap_err(), Error::NoMain);
	}
}

mod archive {
	use super::*;

	#[test]
	fn creates_a_file_on_provided_dir() {
		let cwd = TempDir::new().unwrap();
		let (package, _dir) = package(vec![], vec![]);
		let package = package.unwrap();

		let compressed = package.archive(&PathBuf::from(cwd.path()));

		assert!(compressed.is_ok());
		assert!(compressed.unwrap().starts_with(cwd.path()));
	}

	#[test]
	fn creates_archive_with_spec_info() {
		let cwd = TempDir::new().unwrap();
		let (package, _dir) = package(vec![], vec![]);
		let package = package.unwrap();
		let name = format!("{}_{}", package.spec.name, package.spec.version);

		let compressed = package.archive(&PathBuf::from(cwd.path()));

		assert!(compressed.is_ok());
		assert_eq!(
			name,
			compressed.unwrap().file_stem().unwrap().to_str().unwrap()
		);
	}

	#[test]
	fn can_be_compiled_after_archived() {
		let cwd = TempDir::new().unwrap();
		let (package, _dir) = package(vec![], vec![]);
		let package = package.unwrap();

		let compressed = package.archive(&PathBuf::from(cwd.path())).unwrap();
		let package = Package::try_from(compressed).unwrap();
		let compiled = compile_with_example(package, None);

		assert!(compiled.is_ok());
	}
}

mod compile {
	use super::*;

	mod input {
		use super::*;

		#[test]
		fn renders_with_null() {
			let (package, _dir) = package(
				vec![("templates/main.jsonnet", "_.input")],
				vec!["example.json", "schema.json"],
			);
			let package = package.unwrap();

			let rendered = package.compile(None, None);

			assert_eq!(rendered.unwrap(), Value::Null);
		}

		#[test]
		fn doesnt_merge_input_with_defaults() {
			let input: Value = helpers::json(
				r#"{ "database": { "port": 5432, "host": "localhost", "credentials": { "user": "admin", "pass": "admin" } } }"#,
			);

			let (package, _dir) = package(vec![("templates/main.jsonnet", "_.input")], vec![]);
			let package = package.unwrap();

			let rendered = package.compile(Some(input.clone()), None);

			assert_eq!(rendered.unwrap(), input);
		}
	}

	mod jsonnet {
		use super::*;

		#[test]
		#[should_panic(expected = "manifest function")]
		fn disallows_top_level_functions() {
			let (package, _dir) = package(
				vec![(
					"templates/main.jsonnet",
					"function(input = null, files = null) { input: input }",
				)],
				vec![],
			);

			let package = package.unwrap();
			let rendered = compile_with_example(package, None).unwrap_err();

			match rendered {
				Error::RenderIssue(err) => panic_any(err),
				_ => panic!("It should be a render issue!"),
			}
		}

		#[test]
		fn renders_imports() {
			let (package, _dir) = package(
				vec![
					(
						"templates/main.jsonnet",
						"local valid = import './input/entry.jsonnet'; valid",
					),
					("templates/input/entry.jsonnet", "import '../input.jsonnet'"),
					("templates/input.jsonnet", "_.input"),
				],
				vec![],
			);

			let package = package.unwrap();
			let input = package.example.clone().unwrap();
			let rendered = compile_with_example(package, None);

			assert_eq!(rendered.unwrap(), input);
		}

		#[test]
		#[should_panic(expected = "can't resolve input.jsonnet")]
		fn doesnt_include_templates_on_imports() {
			let (package, _dir) = package(
				vec![
					(
						"templates/main.jsonnet",
						"local valid = import './input/entry.jsonnet'; valid",
					),
					("templates/input/entry.jsonnet", "import 'input.jsonnet'"),
					("templates/input.jsonnet", "_.input"),
				],
				vec![],
			);

			let package = package.unwrap();
			let rendered = compile_with_example(package, None).unwrap_err();

			match rendered {
				Error::RenderIssue(err) => panic_any(err),
				_ => panic!("It should be a render issue!"),
			}
		}

		#[test]
		fn includes_vendor_for_imports() {
			let (package, _dir) = package(
				vec![
					(
						"templates/main.jsonnet",
						"local valid = import 'ksonnet/ksonnet.beta.4/k8s.libjsonnet'; valid",
					),
					("vendor/ksonnet/ksonnet.beta.4/k8s.libjsonnet", "_.input"),
				],
				vec![],
			);

			let package = package.unwrap();
			let input = package.example.clone().unwrap();
			let rendered = compile_with_example(package, None);

			assert_eq!(rendered.unwrap(), input);
		}

		#[test]
		fn includes_lib_for_aliasing() {
			let (package, _dir) = package(
				vec![
					(
						"templates/main.jsonnet",
						"local valid = import 'k.libjsonnet'; valid",
					),
					("vendor/ksonnet/ksonnet.beta.4/k8s.libjsonnet", "_.input"),
					(
						"lib/k.libjsonnet",
						"import 'ksonnet/ksonnet.beta.4/k8s.libjsonnet'",
					),
				],
				vec![],
			);

			let package = package.unwrap();
			let input = package.example.clone().unwrap();
			let rendered = compile_with_example(package, None);

			assert_eq!(rendered.unwrap(), input);
		}
	}

	mod file_templates {
		use super::*;

		#[test]
		fn renders_templates() {
			let (package, _dir) = package(
				vec![("templates/main.jsonnet", "_.files('database.toml')")],
				vec![],
			);
			let package = package.unwrap();
			let input = package.example.clone().unwrap();
			let template = helpers::template(&Fixture::contents("kcp/files/database.toml"), &input);
			let rendered = package.compile(Some(input), None);

			assert_eq!(rendered.unwrap(), Value::String(template));
		}

		#[test]
		fn renders_multiple_templates() {
			let (package, _dir) = package(
				vec![("templates/main.jsonnet", "_.files('**/*.toml')")],
				vec![],
			);
			let package = package.unwrap();
			let input = package.example.clone().unwrap();

			let db_template =
				helpers::template(&Fixture::contents("kcp/files/database.toml"), &input);
			let evt_template =
				helpers::template(&Fixture::contents("kcp/files/events/settings.toml"), &input);

			let rendered = compile_with_example(package, None);

			assert_eq!(
				rendered.unwrap(),
				Value::Array(vec![
					Value::String(db_template),
					Value::String(evt_template),
				])
			);
		}

		#[test]
		#[should_panic(expected = "Unable to compile templates")]
		fn fails_on_invalid_templates() {
			let (package, _dir) = package(
				vec![("templates/main.jsonnet", "_.files('invalid.ini')")],
				vec![],
			);
			let package = package.unwrap();

			let rendered = compile_with_example(package, None).unwrap_err();

			match rendered {
				Error::RenderIssue(err) => panic_any(err),
				_ => panic!("It should be a render issue!"),
			}
		}

		#[test]
		fn compiles_templates_with_empty_input() {
			let (package, _dir) = package(
				vec![("templates/main.jsonnet", "_.files('no-params.txt')")],
				vec!["example.json", "schema.json"],
			);
			let package = package.unwrap();

			let template = helpers::template(
				&Fixture::contents("kcp/files/no-params.txt"),
				&Value::Object(Map::new()),
			);

			let rendered = package.compile(None, None);

			assert_eq!(rendered.unwrap(), Value::String(template));
		}

		#[test]
		#[should_panic(expected = "No files folder to search for templates")]
		fn fails_on_empty_templates_folder() {
			let (package, _dir) = package(vec![], vec!["files"]);
			let package = package.unwrap();

			let rendered = compile_with_example(package, None).unwrap_err();

			match rendered {
				Error::RenderIssue(err) => panic_any(err),
				_ => panic!("It should be a render issue!"),
			}
		}

		#[test]
		#[should_panic(expected = "No template found for glob")]
		fn fails_on_not_found_template() {
			let (package, _dir) = package(
				vec![("templates/main.jsonnet", "_.files('*.json')")],
				vec![],
			);
			let package = package.unwrap();

			let rendered = compile_with_example(package, None).unwrap_err();

			match rendered {
				Error::RenderIssue(err) => panic_any(err),
				_ => panic!("It should be a render issue!"),
			}
		}
	}

	mod release {
		use super::*;

		#[test]
		fn prefixes_package_name() {
			let release_name = "rc";
			let (package, _dir) = package(vec![("templates/main.jsonnet", "_.package")], vec![]);
			let package = package.unwrap();
			let expected = format!("{}-{}", release_name, package.spec.name);

			let rendered = compile_with_example(
				package,
				Some(Release {
					name: String::from(release_name),
				}),
			)
			.unwrap();
			let actual = rendered.get("fullName").unwrap().as_str().unwrap();

			assert_eq!(actual, expected);
		}

		#[test]
		fn is_injected_on_global() {
			let release = Release {
				name: String::from("rc"),
			};
			let (package, _dir) = package(vec![("templates/main.jsonnet", "_.release")], vec![]);
			let package = package.unwrap();

			let json = format!(r#"{{ "name": "{0}" }}"#, release.name);
			let rendered = compile_with_example(package, Some(release));

			let result = helpers::json(&json);

			assert_eq!(rendered.unwrap(), result);
		}
	}

	mod package {
		use super::*;

		#[test]
		fn is_injected_on_global() {
			let (package, _dir) = package(vec![("templates/main.jsonnet", "_.package")], vec![]);
			let package = package.unwrap();

			let json = format!(
				r#"{{ "name": "{0}", "fullName": "{1}", "version": "{2}" }}"#,
				package.spec.name, package.spec.name, package.spec.version
			);
			let rendered = compile_with_example(package, None);

			let result = helpers::json(&json);

			assert_eq!(rendered.unwrap(), result);
		}
	}

	mod subpackage {
		use super::*;

		fn subpackage(dir: &TempDir, name: &str, with: Vec<(&str, &str)>, without: Vec<&str>) {
			use fs_extra::dir::{create_all, move_dir, CopyOptions};
			let (_package, source) = package(with, without);

			let path = dir.path().join("vendor").join(name);
			create_all(&path, true)
				.expect("Failed to create subpackage directory as vendor of parent");

			let mut options = CopyOptions::new();
			options.content_only = true;
			move_dir(source.into_path(), path, &options)
				.expect("Failed to move subpackage into parent");
		}

		#[test]
		fn are_rendered_with_include() {
			let (root, dir) = package(
				vec![("templates/main.jsonnet", "_.include('sub', _.input)")],
				vec![],
			);
			subpackage(
				&dir,
				"sub",
				vec![("templates/main.jsonnet", "_.input")],
				vec![],
			);
			let package = root.unwrap();
			let rendered = compile_with_example(package, None);

			let result = helpers::json(&Fixture::contents("kcp/example.json"));

			assert_eq!(rendered.unwrap(), result);
		}

		// NOTE: As subpackages are supposed to be normal packages, we don't
		// need to validate all the same cases as we do for packages. This test
		// is usefull to guarantee that we pass the correct input while
		// compiling the subpackage, not to check the realm of invalid packages
		#[test]
		#[should_panic(expected = "input provided doesn't match the schema")]
		fn validate_input() {
			let (root, dir) = package(
				vec![(
					"templates/main.jsonnet",
					"_.include('sub', { database: null })",
				)],
				vec![],
			);
			let _archive = subpackage(
				&dir,
				"sub",
				vec![("templates/main.jsonnet", "_.input")],
				vec![],
			);
			let package = root.unwrap();

			let rendered = compile_with_example(package, None).unwrap_err();

			match rendered {
				Error::RenderIssue(err) => panic_any(err),
				_ => panic!("It should be a render issue!"),
			}
		}

		#[test]
		fn has_same_release() {
			let name = "rc";
			let release = Release {
				name: String::from(name),
			};
			let (root, dir) = package(
				vec![("templates/main.jsonnet", "_.include('sub', _.input)")],
				vec![],
			);
			let _archive = subpackage(
				&dir,
				"sub",
				vec![("templates/main.jsonnet", "_.release")],
				vec![],
			);
			let package = root.unwrap();

			let rendered = compile_with_example(package, Some(release)).unwrap();

			let actual = rendered.get("name").unwrap().as_str().unwrap();
			assert_eq!(actual, name);
		}

		#[test]
		fn can_render_own_subpackages() {
			let contents = r#"{"omae_wha": "mou shindeiru"}"#;
			let (root, dir) = package(
				vec![("templates/main.jsonnet", "_.include('dep', _.input)")],
				vec![],
			);
			subpackage(
				&dir,
				"dep",
				vec![("templates/main.jsonnet", "_.include('transient', _.input)")],
				vec![],
			);
			subpackage(
				&dir,
				"transient",
				vec![("templates/main.jsonnet", contents)],
				vec![],
			);
			let package = root.unwrap();
			let rendered = compile_with_example(package, None);

			let result = helpers::json(contents);

			assert_eq!(rendered.unwrap(), result);
		}
	}
}