import { fileURLToPath } from "node:url" import path from "node:path" import fs from "node:fs" import { performance } from "node:perf_hooks" import chalk from "chalk" import ora from "ora" import { isExistingPath } from "../utils/isExistingPath.js" import { Challenge } from "./Challenge.js" import { copyDirectory } from "../utils/copyDirectory.js" import { template } from "./Template.js" import { docker } from "./Docker.js" import { Test } from "./Test.js" import { SolutionTestsResult } from "./SolutionTestsResult.js" import { TemporaryFolder } from "./TemporaryFolder.js" export interface GetSolutionOptions { programmingLanguageName: string challengeName: string name: string } export interface GenerateSolutionOptions extends GetSolutionOptions { githubUser: string } export interface SolutionOptions { programmingLanguageName: string challenge: Challenge name: string } export class Solution implements SolutionOptions { public programmingLanguageName: string public challenge: Challenge public name: string public path: string public temporaryFolder: TemporaryFolder constructor(options: SolutionOptions) { const { programmingLanguageName, challenge, name } = options this.programmingLanguageName = programmingLanguageName this.challenge = challenge this.name = name this.path = path.join( challenge.path, "solutions", programmingLanguageName, name, ) this.temporaryFolder = new TemporaryFolder() } private async setup(): Promise { await this.temporaryFolder.create() await copyDirectory(this.path, this.temporaryFolder.path) await template.docker({ programmingLanguage: this.programmingLanguageName, destination: this.temporaryFolder.path, }) process.chdir(this.temporaryFolder.path) try { await docker.build(this.temporaryFolder.id) } catch (error: any) { throw new Error(`solution: ${this.path}\n${error.message as string}\n`) } } public async test(): Promise { await this.setup() return await Test.runAll(this) } public async run(input: string, output: boolean = false): Promise { await this.setup() const loader = ora("Running...").start() try { const start = performance.now() const { stdout } = await docker.run(input, this.temporaryFolder.id) const end = performance.now() const elapsedTimeMilliseconds = end - start loader.succeed(chalk.bold.green("Success!")) SolutionTestsResult.printBenchmark(elapsedTimeMilliseconds) if (output) { console.log(chalk.bold("Output:")) console.log(stdout) } } catch (error: any) { loader.fail() throw new Error(`solution: ${this.path}\n${error.message as string}\n`) } } static async generate(options: GenerateSolutionOptions): Promise { const { name, challengeName, programmingLanguageName, githubUser } = options const challenge = new Challenge({ name: challengeName }) if (!(await isExistingPath(challenge.path))) { throw new Error(`The challenge doesn't exist yet: ${challenge.name}.`) } const solution = new Solution({ name, challenge, programmingLanguageName, }) if (await isExistingPath(solution.path)) { throw new Error(`The solution already exists: ${name}.`) } await template.solution({ challengeName: challenge.name, destination: solution.path, githubUser, programmingLanguageName: solution.programmingLanguageName, name: solution.name, }) return solution } static async get(options: GetSolutionOptions): Promise { const { name, challengeName, programmingLanguageName } = options const challenge = new Challenge({ name: challengeName, }) const solution = new Solution({ name, challenge, programmingLanguageName, }) if (!(await isExistingPath(solution.path))) { throw new Error("The solution was not found.") } return solution } static async getManyByChallenge(challenge: Challenge): Promise { const solutionsPath = path.join(challenge.path, "solutions") const languagesSolution = (await fs.promises.readdir(solutionsPath)).filter( (name) => { return name !== ".gitkeep" }, ) const paths: string[] = [] for (const language of languagesSolution) { const solutionPath = ( await fs.promises.readdir(path.join(solutionsPath, language)) ).map((solutionName) => { return `challenges/${challenge.name}/solutions/${language}/${solutionName}` }) paths.push(...solutionPath) } return await Solution.getManyByPaths(paths) } static async getManyByProgrammingLanguages( programmingLanguages?: string[], ): Promise { const languages = programmingLanguages ?? (await template.getProgrammingLanguages()) const challengesPath = fileURLToPath( new URL("../../challenges", import.meta.url), ) const challenges = await fs.promises.readdir(challengesPath) const paths: string[] = [] for (const challenge of challenges) { const solutionsPath = path.join(challengesPath, challenge, "solutions") const languagesSolution = ( await fs.promises.readdir(solutionsPath) ).filter((name) => { return name !== ".gitkeep" && languages.includes(name) }) for (const language of languagesSolution) { const solutionPath = ( await fs.promises.readdir(path.join(solutionsPath, language)) ).map((solutionName) => { return `challenges/${challenge}/solutions/${language}/${solutionName}` }) paths.push(...solutionPath) } } return await Solution.getManyByPaths(paths) } /** * Get Solutions by relative paths. * @param paths relative to `challenges` (e.g: `challenges/hello-world/solutions/c/function`) * @returns */ static async getManyByPaths(paths: string[]): Promise { const solutions: string[] = [] for (const path of paths) { if (await isExistingPath(path)) { solutions.push(path) } } return solutions.map((solution) => { const [, challengeName, , programmingLanguageName, solutionName] = solution.replaceAll("\\", "/").split("/") if ( challengeName == null || programmingLanguageName == null || solutionName == null ) { throw new Error(`Invalid solution path: ${solution}`) } return new Solution({ challenge: new Challenge({ name: challengeName, }), name: solutionName, programmingLanguageName, }) }) } }