import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; /** Project-owned configuration for the shared engineering mechanisms. */ final class Recipe { private Recipe() {} static Map environment(final Path root, final Map input) throws Exception { final var result = new HashMap(input); final Path file = root.resolve("engineering.properties"); if (Files.exists(file)) { final var properties = new Properties(); try (final var stream = Files.newInputStream(file)) { properties.load(stream); } for (final String key : properties.stringPropertyNames()) { result.putIfAbsent( key, properties.getProperty(key).replace("${user.home}", System.getProperty("user.home"))); } } if (result.containsKey("PROJECT_NAME")) { project(result); for (final String role : roles(result)) { final Path module = Path.of(result.get(role.toUpperCase() + "_MODULE")); Pipeline.check( !module.isAbsolute() && !module.normalize().startsWith(".."), "Module must stay in the project"); } Pipeline.check( !enabled(result, "BROWSER_TESTS") || roles(result).size() == 2, "Browser coverage requires both backend and frontend"); } return result; } static String project(final Map env) { final String name = Setup.required(env, "PROJECT_NAME"); Pipeline.check(name.matches("[a-z][a-z0-9-]*"), "Invalid project name"); return name; } static List roles(final Map env) { final var roles = new ArrayList(); for (final String role : List.of("backend", "frontend")) { if (!env.getOrDefault(role.toUpperCase() + "_MODULE", "").isBlank()) { roles.add(role); } } Pipeline.check(!roles.isEmpty(), "Configure BACKEND_MODULE, FRONTEND_MODULE, or both"); return roles; } static boolean enabled(final Map env, final String key) { return Boolean.parseBoolean(env.getOrDefault(key, "false")); } }