pub struct Implementation {
pub key: &'static str,
pub display: &'static str,
pub tools: &'static [&'static str],
pub packages: &'static [&'static str],
pub artifacts: &'static [&'static str],
}
pub struct Capability {
pub key: &'static str,
pub outcome: &'static str,
pub implementations: &'static [Implementation],
pub requires: &'static [&'static str],
pub default_selected: bool,
}
impl Capability {
pub fn default_implementation(&self) -> &'static Implementation {
&self.implementations[0]
}
pub fn implementation(&self, key: &str) -> Option<&'static Implementation> {
self.implementations.iter().find(|i| i.key == key)
}
pub fn needs_an_implementation_prompt(&self) -> bool {
self.implementations.len() > 1
}
}
pub const CAPABILITIES: &[Capability] = &[
Capability {
key: "lint",
outcome: "Catch bugs and risky patterns before they ship",
implementations: &[Implementation {
key: "selene",
display: "Selene",
tools: &["selene"],
packages: &[],
artifacts: &["selene.toml"],
}],
requires: &[],
default_selected: true,
},
Capability {
key: "format",
outcome: "One consistent code style, applied automatically",
implementations: &[Implementation {
key: "stylua",
display: "StyLua",
tools: &["stylua"],
packages: &[],
artifacts: &["stylua.toml", ".gitattributes"],
}],
requires: &[],
default_selected: true,
},
Capability {
key: "typecheck",
outcome: "Strict Luau, so type errors are errors and not surprises",
implementations: &[Implementation {
key: "luau-lsp",
display: "luau-lsp",
tools: &["luau-lsp-cli"],
packages: &[],
artifacts: &[".luaurc"],
}],
requires: &[],
default_selected: true,
},
Capability {
key: "test",
outcome: "Write and run tests against your game's own code",
implementations: &[Implementation {
key: "testez",
display: "TestEZ",
tools: &[],
packages: &["testez"],
artifacts: &["tests", "testez.yml", "testez-companion.toml"],
}],
requires: &[],
default_selected: false,
},
Capability {
key: "gate",
outcome: "One command that runs every check above, in one pass",
implementations: &[Implementation {
key: "lute",
display: "Lute",
tools: &["lute"],
packages: &[],
artifacts: &[".lute/check.luau"],
}],
requires: &[],
default_selected: true,
},
Capability {
key: "ci",
outcome: "Run that same gate on GitHub for every push (default off)",
implementations: &[Implementation {
key: "github-actions",
display: "GitHub Actions",
tools: &[],
packages: &[],
artifacts: &[".github/workflows/ci.yml"],
}],
requires: &["gate"],
default_selected: false,
},
Capability {
key: "editor",
outcome: "VS Code resolves requires and sees what you build in Studio",
implementations: &[Implementation {
key: "vscode",
display: "VS Code + luau-lsp",
tools: &["rojo"],
packages: &[],
artifacts: &[".vscode/settings.json", "sourcemap.json"],
}],
requires: &[],
default_selected: true,
},
Capability {
key: "assets-2d",
outcome: "Upload images and reference them by name instead of by id",
implementations: &[Implementation {
key: "tarmac",
display: "Tarmac",
tools: &["tarmac"],
packages: &[],
artifacts: &["figma", "tarmac.toml"],
}],
requires: &[],
default_selected: false,
},
Capability {
key: "assets-3d",
outcome: "A Blender scene at Roblox's unit scale (1 stud = 0.28 m)",
implementations: &[Implementation {
key: "blender",
display: "Blender",
tools: &[],
packages: &[],
artifacts: &["blender"],
}],
requires: &[],
default_selected: false,
},
];
pub fn find(key: &str) -> Option<&'static Capability> {
CAPABILITIES.iter().find(|c| c.key == key)
}
pub fn offerable(chosen: &[String]) -> Vec<&'static Capability> {
CAPABILITIES
.iter()
.filter(|c| c.requires.iter().all(|r| chosen.iter().any(|k| k == r)))
.collect()
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct Derived {
pub tools: Vec<String>,
pub packages: Vec<String>,
pub artifacts: Vec<String>,
}
pub fn derive(selected: &[(String, Option<String>)]) -> Derived {
let mut out = Derived::default();
let keys: Vec<String> = selected.iter().map(|(k, _)| k.clone()).collect();
let live = offerable(&keys);
for (key, implementation) in selected {
let Some(capability) = live.iter().find(|c| c.key == key) else {
continue;
};
let implementation = match implementation {
Some(i) => match capability.implementation(i) {
Some(found) => found,
None => capability.default_implementation(),
},
None => capability.default_implementation(),
};
push_new(&mut out.tools, implementation.tools);
push_new(&mut out.packages, implementation.packages);
push_new(&mut out.artifacts, implementation.artifacts);
}
out
}
fn push_new(into: &mut Vec<String>, items: &[&str]) {
for item in items {
if !into.iter().any(|existing| existing == item) {
into.push((*item).to_string());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn chosen(keys: &[&str]) -> Vec<(String, Option<String>)> {
keys.iter().map(|k| (k.to_string(), None)).collect()
}
#[test]
fn every_key_is_unique() {
let mut keys: Vec<&str> = CAPABILITIES.iter().map(|c| c.key).collect();
let before = keys.len();
keys.sort_unstable();
keys.dedup();
assert_eq!(before, keys.len(), "duplicate capability key");
}
#[test]
fn every_capability_has_at_least_one_implementation() {
for capability in CAPABILITIES {
assert!(
!capability.implementations.is_empty(),
"{} has no implementation, so choosing it would do nothing",
capability.key
);
}
}
#[test]
fn every_requirement_resolves_and_points_backwards() {
for (i, capability) in CAPABILITIES.iter().enumerate() {
for required in capability.requires {
let at = CAPABILITIES.iter().position(|c| c.key == *required);
let at = at.unwrap_or_else(|| panic!("{} requires unknown {required}", capability.key));
assert!(
at < i,
"{} requires {required}, which is listed after it",
capability.key
);
}
}
}
#[test]
fn exactly_one_capability_offers_a_choice_of_implementation() {
let with_a_choice: Vec<&str> = CAPABILITIES
.iter()
.filter(|c| c.needs_an_implementation_prompt())
.map(|c| c.key)
.collect();
assert!(
with_a_choice.is_empty() || with_a_choice == ["test"],
"only `test` may ask which implementation (jest-lua is the peer): {with_a_choice:?}"
);
}
#[test]
fn every_implementation_names_itself() {
for capability in CAPABILITIES {
for implementation in capability.implementations {
assert!(!implementation.display.is_empty(), "{}", capability.key);
assert!(!implementation.key.is_empty(), "{}", capability.key);
}
}
}
#[test]
fn every_implementation_derives_something() {
for capability in CAPABILITIES {
for implementation in capability.implementations {
assert!(
!implementation.artifacts.is_empty()
|| !implementation.tools.is_empty()
|| !implementation.packages.is_empty(),
"{}/{} derives nothing",
capability.key,
implementation.key
);
}
}
}
#[test]
fn choosing_lint_derives_selene_and_its_config() {
let derived = derive(&chosen(&["lint"]));
assert_eq!(derived.tools, ["selene"]);
assert_eq!(derived.artifacts, ["selene.toml"]);
assert!(derived.packages.is_empty());
}
#[test]
fn choosing_test_derives_the_package_not_just_the_files() {
let derived = derive(&chosen(&["test"]));
assert_eq!(derived.packages, ["testez"]);
assert!(derived.artifacts.contains(&"tests".to_string()));
assert!(derived.artifacts.contains(&"testez.yml".to_string()));
}
#[test]
fn ci_without_the_gate_derives_nothing() {
let with = derive(&chosen(&["gate", "ci"]));
assert!(with.artifacts.contains(&".github/workflows/ci.yml".to_string()));
let without = derive(&chosen(&["ci"]));
assert!(
!without.artifacts.contains(&".github/workflows/ci.yml".to_string()),
"{without:?}"
);
assert!(!offerable(&["ci".to_string()]).iter().any(|c| c.key == "ci"));
}
#[test]
fn choosing_nothing_derives_nothing() {
assert_eq!(derive(&[]), Derived::default());
}
#[test]
fn a_tool_wanted_by_two_capabilities_is_pinned_once() {
let mut derived = Derived::default();
push_new(&mut derived.tools, &["rojo", "selene"]);
push_new(&mut derived.tools, &["rojo"]);
assert_eq!(derived.tools, ["rojo", "selene"]);
}
#[test]
fn an_unknown_implementation_falls_back_to_the_default() {
let selected = vec![("lint".to_string(), Some("clippy".to_string()))];
assert_eq!(derive(&selected).tools, ["selene"]);
}
#[test]
fn the_deliberately_off_capabilities_are_off() {
for key in ["test", "ci", "assets-2d", "assets-3d"] {
assert!(
!find(key).expect(key).default_selected,
"{key} must not be pre-checked"
);
}
}
}