diff --git a/src/build.ts b/src/build.ts index dfce242..b129b52 100644 --- a/src/build.ts +++ b/src/build.ts @@ -5,6 +5,8 @@ import * as util from "util"; import { parse } from "./utils/parse"; import { getArg } from "./utils/args"; import { getCommits, CommitLogObject } from "./utils/commits"; +import skeletonSchema from "./schema/skeleton"; +import tutorialSchema from "./schema/tutorial"; import { validateSchema } from "./utils/validateSchema"; import * as T from "../typings/tutorial"; @@ -70,13 +72,29 @@ async function build(args: string[]) { return; } - // parse yaml config - let config; + // parse yaml skeleton config + let skeleton; try { - config = yamlParser.load(_yaml); + skeleton = yamlParser.load(_yaml); + if (!skeleton || !Object.keys(skeleton).length) { + throw new Error(`Skeleton at "${options.yaml}" is invalid`); + } } catch (e) { console.error("Error parsing yaml"); console.error(e.message); + return; + } + + // validate skeleton based on skeleton json schema + try { + const valid = validateSchema(skeletonSchema, skeleton); + if (!valid) { + console.error("Tutorial validation failed. See above to see what to fix"); + return; + } + } catch (e) { + console.error("Error validating tutorial schema:"); + console.error(e.message); } // load git commits to use in parse step @@ -84,7 +102,7 @@ async function build(args: string[]) { try { commits = await getCommits({ localDir: localPath, - codeBranch: config.config.repo.branch, + codeBranch: skeleton.config.repo.branch, }); } catch (e) { console.error("Error loading commits:"); @@ -97,7 +115,7 @@ async function build(args: string[]) { try { tutorial = await parse({ text: _markdown, - config, + skeleton, commits, }); } catch (e) { @@ -106,9 +124,9 @@ async function build(args: string[]) { return; } - // validate tutorial based on json schema + // validate tutorial based on tutorial json schema try { - const valid = validateSchema(tutorial); + const valid = validateSchema(tutorialSchema, tutorial); if (!valid) { console.error("Tutorial validation failed. See above to see what to fix"); return; diff --git a/src/schema/meta.ts b/src/schema/meta.ts index 4fd1520..71bf7b8 100644 --- a/src/schema/meta.ts +++ b/src/schema/meta.ts @@ -1,9 +1,6 @@ export default { $schema: "http://json-schema.org/draft-07/schema#", $id: "https://coderoad.io/tutorial-schema.json", - title: "Tutorial Schema", - description: - "A CodeRoad tutorial schema data. This JSON data is converted into a tutorial with the CodeRoad editor extension", definitions: { semantic_version: { type: "string", @@ -48,7 +45,6 @@ export default { "An array of command line commands that will be called when the user enters the level or step. Currently commands are limited for security purposes", items: { type: "string", - enum: ["npm install"], }, }, commit_array: { diff --git a/src/schema/skeleton.ts b/src/schema/skeleton.ts new file mode 100644 index 0000000..a44e461 --- /dev/null +++ b/src/schema/skeleton.ts @@ -0,0 +1,185 @@ +import meta from "./meta"; + +export default { + title: "Skeleton Schema", + description: + "A CodeRoad tutorial config schema. This data is paired up with the markdown to create a tutorial", + ...meta, + type: "object", + properties: { + version: { + $ref: "#/definitions/semantic_version", + description: "The tutorial version. Must be unique for the tutorial.", + examples: ["0.1.0", "1.0.0"], + }, + + // config + config: { + type: "object", + properties: { + testRunner: { + type: "object", + description: "The test runner configuration", + properties: { + command: { + type: "string", + description: "Command line to start the test runner", + examples: ["./node_modules/.bin/mocha"], + }, + args: { + type: "object", + description: + "A configuration of command line args for your test runner", + properties: { + filter: { + type: "string", + description: + "the command line arg for filtering tests with a regex pattern", + examples: ["--grep"], + }, + tap: { + type: "string", + description: + "The command line arg for configuring a TAP reporter. See https://github.com/sindresorhus/awesome-tap for examples.", + examples: ["--reporter=mocha-tap-reporter"], + }, + }, + additionalProperties: false, + required: ["tap"], + }, + directory: { + type: "string", + description: "An optional folder for the test runner", + examples: ["coderoad"], + }, + setup: { + $ref: "#/definitions/setup_action", + description: + "Setup commits or commands used for setting up the test runner on tutorial launch", + }, + }, + required: ["command", "args"], + }, + repo: { + type: "object", + description: "The repo holding the git commits for the tutorial", + properties: { + uri: { + type: "string", + description: "The uri source of the tutorial", + format: "uri", + examples: ["https://github.com/name/tutorial-name.git"], + }, + branch: { + description: + "The branch of the repo where the tutorial config file exists", + type: "string", + examples: ["master"], + }, + }, + additionalProperties: false, + required: ["uri", "branch"], + }, + + dependencies: { + type: "array", + description: "A list of tutorial dependencies", + items: { + type: "object", + properties: { + name: { + type: "string", + description: + "The command line process name of the dependency. It will be checked by running `name --version`", + examples: ["node", "python"], + }, + version: { + type: "string", + description: + "The version requirement. See https://github.com/npm/node-semver for options", + examples: [">=10"], + }, + }, + required: ["name", "version"], + }, + }, + appVersions: { + type: "object", + description: + "A list of compatable coderoad versions. Currently only a VSCode extension.", + properties: { + vscode: { + type: "string", + description: + "The version range for coderoad-vscode that this tutorial is compatable with", + examples: [">=0.7.0"], + }, + }, + }, + }, + additionalProperties: false, + required: ["testRunner", "repo"], + }, + + // levels + levels: { + type: "array", + description: + 'Levels are the stages a user goes through in the tutorial. A level may contain a group of tasks called "steps" that must be completed to proceed', + items: { + type: "object", + properties: { + id: { + type: "string", + description: "A level id", + examples: ["L1", "L11"], + }, + setup: { + $ref: "#/definitions/setup_action", + description: + "An optional point for loading commits, running commands or opening files", + }, + steps: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "string", + description: "A level id", + examples: ["L1S1", "L11S12"], + }, + setup: { + allOf: [ + { + $ref: "#/definitions/setup_action", + description: + "A point for loading commits. It can also run commands and/or open files", + }, + ], + }, + solution: { + allOf: [ + { + $ref: "#/definitions/setup_action", + description: + "The solution commits that can be loaded if the user gets stuck. It can also run commands and/or open files", + }, + { + required: [], + }, + ], + }, + }, + required: ["id", "setup"], + }, + }, + }, + required: ["id"], + }, + minItems: 1, + }, + }, + additionalProperties: false, + required: ["version", "config", "levels"], +}; diff --git a/src/schema/index.ts b/src/schema/tutorial.ts similarity index 97% rename from src/schema/index.ts rename to src/schema/tutorial.ts index 98d3fd6..ed3cc15 100644 --- a/src/schema/index.ts +++ b/src/schema/tutorial.ts @@ -1,6 +1,9 @@ import meta from "./meta"; export default { + title: "Tutorial Schema", + description: + "A CodeRoad tutorial schema data. This JSON data is converted into a tutorial with the CodeRoad editor extension", ...meta, type: "object", properties: { diff --git a/src/templates/js-mocha/coderoad.yaml b/src/templates/js-mocha/coderoad.yaml index dbe78b1..9d50acf 100644 --- a/src/templates/js-mocha/coderoad.yaml +++ b/src/templates/js-mocha/coderoad.yaml @@ -92,8 +92,7 @@ levels: ## Example Four: Subtasks - id: L1S4 setup: - commands: - ## A filter is a regex that limits the test results - - filter: "^Example 2" - ## A feature that shows subtasks: all filtered active test names and the status of the tests (pass/fail). - - subtasks: true + ## A filter is a regex that limits the test results + filter: "^Example 2" + ## A feature that shows subtasks: all filtered active test names and the status of the tests (pass/fail). + subtasks: true diff --git a/src/utils/commits.ts b/src/utils/commits.ts index 2072993..15bb312 100644 --- a/src/utils/commits.ts +++ b/src/utils/commits.ts @@ -2,7 +2,7 @@ import * as fs from "fs"; import util from "util"; import * as path from "path"; import gitP, { SimpleGit } from "simple-git/promise"; -import { addToCommitOrder, validateCommitOrder } from "./commitOrder"; +import { validateCommitOrder } from "./validateCommits"; const mkdir = util.promisify(fs.mkdir); const exists = util.promisify(fs.exists); diff --git a/src/utils/parse.ts b/src/utils/parse.ts index 59886bc..a6db41f 100644 --- a/src/utils/parse.ts +++ b/src/utils/parse.ts @@ -90,7 +90,7 @@ export function parseMdContent(md: string): TutorialFrame | never { type ParseParams = { text: string; - config: Partial; + skeleton: Partial; commits: CommitLogObject; }; @@ -98,15 +98,14 @@ export function parse(params: ParseParams): any { const mdContent: TutorialFrame = parseMdContent(params.text); const parsed: Partial = { - version: params.config.version, + version: params.skeleton.version, summary: mdContent.summary, - config: params.config.config || {}, + config: params.skeleton.config || {}, levels: [], }; // add init commits if (params.commits.INIT && params.commits.INIT.length) { - console.log(JSON.stringify(parsed.config?.testRunner)); // @ts-ignore parsed.config.testRunner.setup = { ...(parsed.config?.testRunner?.setup || {}), @@ -115,8 +114,8 @@ export function parse(params: ParseParams): any { } // merge content and tutorial - if (params.config.levels && params.config.levels.length) { - parsed.levels = params.config.levels + if (params.skeleton.levels && params.skeleton.levels.length) { + parsed.levels = params.skeleton.levels .map((level: T.Level, levelIndex: number) => { const levelContent = mdContent.levels[level.id]; diff --git a/src/utils/commitOrder.ts b/src/utils/validateCommits.ts similarity index 92% rename from src/utils/commitOrder.ts rename to src/utils/validateCommits.ts index dae894d..9535968 100644 --- a/src/utils/commitOrder.ts +++ b/src/utils/validateCommits.ts @@ -1,9 +1,5 @@ // should flag commits that are out of order based on the previous commit // position is a string like 'INIT', 'L1', 'L1S1' -export function addToCommitOrder(position: string) { - // add position to list -} - export function validateCommitOrder(positions: string[]): boolean { // loop over positions const errors: number[] = []; @@ -12,7 +8,6 @@ export function validateCommitOrder(positions: string[]): boolean { positions.forEach((position: string, index: number) => { if (position === "INIT") { if (previous.level !== 0 && previous.step !== 0) { - console.log("ERROR HERE"); errors.push(index); } current = { level: 0, step: 0 }; @@ -46,7 +41,7 @@ export function validateCommitOrder(positions: string[]): boolean { previous = current; }); - if (errors.length) { + if (errors.length && process.env.NODE_ENV !== "test") { console.warn("Found commit positions out of order"); positions.forEach((position, index) => { if (errors.includes(index)) { diff --git a/src/utils/validateSchema.ts b/src/utils/validateSchema.ts index a7ec4fc..aeca139 100644 --- a/src/utils/validateSchema.ts +++ b/src/utils/validateSchema.ts @@ -1,10 +1,11 @@ -import schema from "../schema"; - // https://www.npmjs.com/package/ajv // @ts-ignore ajv typings not working import JsonSchema from "ajv"; -export function validateSchema(json: any): boolean | PromiseLike { +export function validateSchema( + schema: any, + json: any +): boolean | PromiseLike { // validate using https://json-schema.org/ const jsonSchema = new JsonSchema({ allErrors: true, diff --git a/src/validate.ts b/src/validate.ts index 029a501..4da3048 100644 --- a/src/validate.ts +++ b/src/validate.ts @@ -1,23 +1,46 @@ import * as path from "path"; import * as fs from "fs"; import util from "util"; +import * as yamlParser from "js-yaml"; +import { getArg } from "./utils/args"; import gitP, { SimpleGit } from "simple-git/promise"; import { getCommits, CommitLogObject } from "./utils/commits"; const mkdir = util.promisify(fs.mkdir); const exists = util.promisify(fs.exists); const rmdir = util.promisify(fs.rmdir); +const read = util.promisify(fs.readFile); async function validate(args: string[]) { // dir - default . const dir = !args.length || args[0].match(/^-/) ? "." : args[0]; - console.warn("Not yet implemented. Coming soon"); - const localDir = path.join(process.cwd(), dir); - const codeBranch = ""; - const commits = getCommits({ localDir, codeBranch }); + // -y --yaml - default coderoad-config.yml + const options = { + yaml: getArg(args, { name: "yaml", alias: "y" }) || "coderoad.yaml", + }; + + const _yaml = await read(path.join(localDir, options.yaml), "utf8"); + + // parse yaml config + let config; + try { + config = yamlParser.load(_yaml); + // TODO: validate yaml + if (!config || !config.length) { + throw new Error("Invalid yaml file contents"); + } + } catch (e) { + console.error("Error parsing yaml"); + console.error(e.message); + } + + const codeBranch: string = config.config.repo.branch; + // VALIDATE SKELETON WITH COMMITS + const commits = getCommits({ localDir, codeBranch }); + // parse tutorial skeleton for order and commands // on error, warn missing level/step diff --git a/tests/commitOrder.test.ts b/tests/commitOrder.test.ts index ac483e0..38e1035 100644 --- a/tests/commitOrder.test.ts +++ b/tests/commitOrder.test.ts @@ -1,4 +1,4 @@ -import { validateCommitOrder } from "../src/utils/commitOrder"; +import { validateCommitOrder } from "../src/utils/validateCommits"; describe("commitOrder", () => { it("should return true if order is valid", () => { diff --git a/tests/parse.test.ts b/tests/parse.test.ts index ea84002..767b70c 100644 --- a/tests/parse.test.ts +++ b/tests/parse.test.ts @@ -9,10 +9,10 @@ Short description to be shown as a tutorial's subtitle. `; - const config = { version: "0.1.0" }; + const skeleton = { version: "0.1.0" }; const result = parse({ text: md, - config, + skeleton, commits: {}, }); const expected = { @@ -37,13 +37,13 @@ Description. Some text `; - const config = { + const skeleton = { levels: [{ id: "L1" }], }; const result = parse({ text: md, - config, + skeleton, commits: {}, }); const expected = { @@ -73,7 +73,7 @@ Description. Some text `; - const config = { + const skeleton = { levels: [ { id: "L1", @@ -85,7 +85,7 @@ Some text }; const result = parse({ text: md, - config, + skeleton, commits: {}, }); const expected = { @@ -115,10 +115,10 @@ Description. Some text that becomes the summary `; - const config = { levels: [{ id: "L1" }] }; + const skeleton = { levels: [{ id: "L1" }] }; const result = parse({ text: md, - config, + skeleton, commits: {}, }); const expected = { @@ -145,10 +145,10 @@ Description. Some text that becomes the summary and goes beyond the maximum length of 80 so that it gets truncated at the end `; - const config = { levels: [{ id: "L1" }] }; + const skeleton = { levels: [{ id: "L1" }] }; const result = parse({ text: md, - config, + skeleton, commits: {}, }); const expected = { @@ -180,10 +180,10 @@ Second line Third line `; - const config = { levels: [{ id: "L1" }] }; + const skeleton = { levels: [{ id: "L1" }] }; const result = parse({ text: md, - config, + skeleton, commits: {}, }); const expected = { @@ -215,7 +215,7 @@ First line The first step `; - const config = { + const skeleton = { levels: [ { id: "L1", @@ -229,7 +229,7 @@ The first step }; const result = parse({ text: md, - config, + skeleton, commits: { L1S1Q: ["abcdefg1"], }, @@ -271,7 +271,7 @@ First line The first step `; - const config = { + const skeleton = { levels: [ { id: "L1", @@ -285,7 +285,7 @@ The first step }; const result = parse({ text: md, - config, + skeleton, commits: { L1S1Q: ["abcdefg1", "123456789"], }, @@ -327,7 +327,7 @@ First line The first step `; - const config = { + const skeleton = { levels: [ { id: "L1", @@ -336,7 +336,7 @@ The first step }; const result = parse({ text: md, - config, + skeleton, commits: { L1: ["abcdefg1"], }, @@ -372,7 +372,7 @@ First line The first step `; - const config = { + const skeleton = { levels: [ { id: "L1", @@ -397,7 +397,7 @@ The first step }; const result = parse({ text: md, - config, + skeleton, commits: { L1S1Q: ["abcdefg1", "123456789"], L1S1A: ["1gfedcba", "987654321"], @@ -462,7 +462,7 @@ Second level content. The third step `; - const config = { + const skeleton = { levels: [ { id: "L1", @@ -522,7 +522,7 @@ The third step }; const result = parse({ text: md, - config, + skeleton, commits: { L1S1Q: ["abcdef1", "123456789"], L1S1A: ["1fedcba", "987654321"], @@ -623,7 +623,7 @@ First level content. The first step `; - const config = { + const skeleton = { levels: [ { id: "L1", @@ -637,7 +637,7 @@ The first step }; const result = parse({ text: md, - config, + skeleton, commits: { L1S1Q: ["abcdef1", "123456789"], }, @@ -674,7 +674,7 @@ The first step Description. `; - const config = { + const skeleton = { config: { testRunner: { command: "./node_modules/.bin/mocha", @@ -704,7 +704,7 @@ Description. }; const result = parse({ text: md, - config, + skeleton, commits: {}, }); const expected = { @@ -747,7 +747,7 @@ Description. Description. `; - const config = { + const skeleton = { config: { testRunner: { command: "./node_modules/.bin/mocha", @@ -774,7 +774,7 @@ Description. }; const result = parse({ text: md, - config, + skeleton, commits: { INIT: ["abcdef1", "123456789"], }, diff --git a/tests/skeleton.test.ts b/tests/skeleton.test.ts new file mode 100644 index 0000000..fdcfeb2 --- /dev/null +++ b/tests/skeleton.test.ts @@ -0,0 +1,270 @@ +import { validateSchema } from "../src/utils/validateSchema"; +import skeletonSchema from "../src/schema/skeleton"; + +const validateSkeleton = (json: any) => validateSchema(skeletonSchema, json); + +const validJson = { + version: "0.1.0", + config: { + testRunner: { + directory: "coderoad", + setup: { + commands: [], + }, + args: { + filter: "--grep", + tap: "--reporter=mocha-tap-reporter", + }, + command: "./node_modules/.bin/mocha", + }, + repo: { + uri: "http://github.com/somePath/toRepo.git", + branch: "codeBranch", + }, + dependencies: [], + appVersions: { + vscode: ">=0.7.0", + }, + }, + levels: [ + { + steps: [ + { + id: "L1S1", + setup: { + files: ["package.json"], + }, + solution: { + files: ["package.json"], + }, + }, + { + id: "L1S2", + setup: { + commands: ["npm install"], + }, + solution: { + commands: ["npm install"], + }, + }, + { + id: "L1S3", + setup: { + files: ["package.json"], + watchers: ["package.json", "node_modules/some-package"], + }, + solution: { + files: ["package.json"], + }, + }, + { + id: "L1S4", + setup: { + commands: [], + filter: "^Example 2", + subtasks: true, + }, + }, + ], + id: "L1", + }, + ], +}; + +describe("validate skeleton", () => { + it("should fail an empty skeleton file", () => { + const json = {}; + + const valid = validateSkeleton(json); + expect(valid).toBe(false); + }); + it("should parse a valid skeleton file", () => { + const json = { ...validJson }; + + const valid = validateSkeleton(json); + expect(valid).toBe(true); + }); + it("should fail if version is invalid", () => { + const json = { ...validJson, version: "NOT A VERSION" }; + + const valid = validateSkeleton(json); + expect(valid).toBe(false); + }); + it("should fail if version is missing", () => { + const json = { ...validJson, version: undefined }; + + const valid = validateSkeleton(json); + expect(valid).toBe(false); + }); + it("should fail if config is missing", () => { + const json = { ...validJson, config: undefined }; + + const valid = validateSkeleton(json); + expect(valid).toBe(false); + }); + it("should fail if config testRunner is missing", () => { + const json = { + ...validJson, + config: { ...validJson.config, testRunner: undefined }, + }; + + const valid = validateSkeleton(json); + expect(valid).toBe(false); + }); + it("should fail if config testRunner command is missing", () => { + const json = { + ...validJson, + config: { + ...validJson.config, + testRunner: { ...validJson.config.testRunner, command: undefined }, + }, + }; + + const valid = validateSkeleton(json); + expect(valid).toBe(false); + }); + it("should fail if config testRunner args tap is missing", () => { + const json = { + ...validJson, + config: { + ...validJson.config, + testRunner: { + ...validJson.config.testRunner, + args: { ...validJson.config.testRunner.args, tap: undefined }, + }, + }, + }; + + const valid = validateSkeleton(json); + expect(valid).toBe(false); + }); + it("should fail if repo is missing", () => { + const json = { + ...validJson, + config: { + ...validJson.config, + repo: undefined, + }, + }; + + const valid = validateSkeleton(json); + expect(valid).toBe(false); + }); + it("should fail if repo uri is missing", () => { + const json = { + ...validJson, + config: { + ...validJson.config, + repo: { ...validJson.config.repo, uri: undefined }, + }, + }; + + const valid = validateSkeleton(json); + expect(valid).toBe(false); + }); + it("should fail if repo uri is invalid", () => { + const json = { + ...validJson, + config: { + ...validJson.config, + repo: { ...validJson.config.repo, uri: "NOT A VALID URI" }, + }, + }; + + const valid = validateSkeleton(json); + expect(valid).toBe(false); + }); + it("should fail if repo branch is missing", () => { + const json = { + ...validJson, + config: { + ...validJson.config, + repo: { ...validJson.config.repo, branch: undefined }, + }, + }; + + const valid = validateSkeleton(json); + expect(valid).toBe(false); + }); + it("should fial if level is missing id", () => { + const level1 = { ...validJson.levels[0], id: undefined }; + const json = { + ...validJson, + levels: [level1], + }; + + const valid = validateSkeleton(json); + expect(valid).toBe(false); + }); + it("should fail if level setup is invalid", () => { + const level1 = { ...validJson.levels[0], setup: { invalidThing: [] } }; + const json = { + ...validJson, + levels: [level1], + }; + + const valid = validateSkeleton(json); + expect(valid).toBe(false); + }); + it("should fail if step is missing id", () => { + const step1 = { ...validJson.levels[0].steps[0], id: undefined }; + const level1 = { ...validJson.levels[0], steps: [step1] }; + const json = { + ...validJson, + levels: [level1], + }; + + const valid = validateSkeleton(json); + expect(valid).toBe(false); + }); + it("should fail if step setup is missing", () => { + const step1 = { ...validJson.levels[0].steps[0], setup: undefined }; + const level1 = { ...validJson.levels[0], steps: [step1] }; + const json = { + ...validJson, + levels: [level1], + }; + + const valid = validateSkeleton(json); + expect(valid).toBe(false); + }); + it("should fail if step setup is invalid", () => { + const step1 = { + ...validJson.levels[0].steps[0], + setup: { invalidThing: [] }, + }; + const level1 = { ...validJson.levels[0], steps: [step1] }; + const json = { + ...validJson, + levels: [level1], + }; + + const valid = validateSkeleton(json); + expect(valid).toBe(false); + }); + it("should not fail if step solution is missing", () => { + const step1 = { ...validJson.levels[0].steps[0], solution: undefined }; + const level1 = { ...validJson.levels[0], steps: [step1] }; + const json = { + ...validJson, + levels: [level1], + }; + + const valid = validateSkeleton(json); + expect(valid).toBe(true); + }); + it("should fail if step solution is invalid", () => { + const step1 = { + ...validJson.levels[0].steps[0], + solution: { invalidThing: [] }, + }; + const level1 = { ...validJson.levels[0], steps: [step1] }; + const json = { + ...validJson, + levels: [level1], + }; + + const valid = validateSkeleton(json); + expect(valid).toBe(false); + }); +}); diff --git a/tests/validate.test.ts b/tests/validate.test.ts index 1355fae..2f7dde7 100644 --- a/tests/validate.test.ts +++ b/tests/validate.test.ts @@ -1,11 +1,14 @@ import * as T from "../typings/tutorial"; +import tutorialSchema from "../src/schema/tutorial"; import { validateSchema } from "../src/utils/validateSchema"; -describe("validate", () => { +const validateTutorial = (json: any) => validateSchema(tutorialSchema, json); + +describe("validate tutorial", () => { it("should reject an empty tutorial", () => { const json = { version: "here" }; - const valid = validateSchema(json); + const valid = validateTutorial(json); expect(valid).toBe(false); }); it("should return true for a valid tutorial", () => { @@ -45,7 +48,7 @@ describe("validate", () => { ], }; - const valid = validateSchema(json); + const valid = validateTutorial(json); expect(valid).toBe(true); }); });