Skip to content

Commit 4f01daa

Browse files
authored
Merge pull request #28 from coderoad/validate-yaml
Validate yaml
2 parents 7a1ebe0 + 30985e4 commit 4f01daa

14 files changed

+560
-68
lines changed

Diff for: src/build.ts

+25-7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import * as util from "util";
55
import { parse } from "./utils/parse";
66
import { getArg } from "./utils/args";
77
import { getCommits, CommitLogObject } from "./utils/commits";
8+
import skeletonSchema from "./schema/skeleton";
9+
import tutorialSchema from "./schema/tutorial";
810
import { validateSchema } from "./utils/validateSchema";
911
import * as T from "../typings/tutorial";
1012

@@ -70,21 +72,37 @@ async function build(args: string[]) {
7072
return;
7173
}
7274

73-
// parse yaml config
74-
let config;
75+
// parse yaml skeleton config
76+
let skeleton;
7577
try {
76-
config = yamlParser.load(_yaml);
78+
skeleton = yamlParser.load(_yaml);
79+
if (!skeleton || !Object.keys(skeleton).length) {
80+
throw new Error(`Skeleton at "${options.yaml}" is invalid`);
81+
}
7782
} catch (e) {
7883
console.error("Error parsing yaml");
7984
console.error(e.message);
85+
return;
86+
}
87+
88+
// validate skeleton based on skeleton json schema
89+
try {
90+
const valid = validateSchema(skeletonSchema, skeleton);
91+
if (!valid) {
92+
console.error("Tutorial validation failed. See above to see what to fix");
93+
return;
94+
}
95+
} catch (e) {
96+
console.error("Error validating tutorial schema:");
97+
console.error(e.message);
8098
}
8199

82100
// load git commits to use in parse step
83101
let commits: CommitLogObject;
84102
try {
85103
commits = await getCommits({
86104
localDir: localPath,
87-
codeBranch: config.config.repo.branch,
105+
codeBranch: skeleton.config.repo.branch,
88106
});
89107
} catch (e) {
90108
console.error("Error loading commits:");
@@ -97,7 +115,7 @@ async function build(args: string[]) {
97115
try {
98116
tutorial = await parse({
99117
text: _markdown,
100-
config,
118+
skeleton,
101119
commits,
102120
});
103121
} catch (e) {
@@ -106,9 +124,9 @@ async function build(args: string[]) {
106124
return;
107125
}
108126

109-
// validate tutorial based on json schema
127+
// validate tutorial based on tutorial json schema
110128
try {
111-
const valid = validateSchema(tutorial);
129+
const valid = validateSchema(tutorialSchema, tutorial);
112130
if (!valid) {
113131
console.error("Tutorial validation failed. See above to see what to fix");
114132
return;

Diff for: src/schema/meta.ts

-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
export default {
22
$schema: "http://json-schema.org/draft-07/schema#",
33
$id: "https://coderoad.io/tutorial-schema.json",
4-
title: "Tutorial Schema",
5-
description:
6-
"A CodeRoad tutorial schema data. This JSON data is converted into a tutorial with the CodeRoad editor extension",
74
definitions: {
85
semantic_version: {
96
type: "string",
@@ -48,7 +45,6 @@ export default {
4845
"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",
4946
items: {
5047
type: "string",
51-
enum: ["npm install"],
5248
},
5349
},
5450
commit_array: {

Diff for: src/schema/skeleton.ts

+185
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import meta from "./meta";
2+
3+
export default {
4+
title: "Skeleton Schema",
5+
description:
6+
"A CodeRoad tutorial config schema. This data is paired up with the markdown to create a tutorial",
7+
...meta,
8+
type: "object",
9+
properties: {
10+
version: {
11+
$ref: "#/definitions/semantic_version",
12+
description: "The tutorial version. Must be unique for the tutorial.",
13+
examples: ["0.1.0", "1.0.0"],
14+
},
15+
16+
// config
17+
config: {
18+
type: "object",
19+
properties: {
20+
testRunner: {
21+
type: "object",
22+
description: "The test runner configuration",
23+
properties: {
24+
command: {
25+
type: "string",
26+
description: "Command line to start the test runner",
27+
examples: ["./node_modules/.bin/mocha"],
28+
},
29+
args: {
30+
type: "object",
31+
description:
32+
"A configuration of command line args for your test runner",
33+
properties: {
34+
filter: {
35+
type: "string",
36+
description:
37+
"the command line arg for filtering tests with a regex pattern",
38+
examples: ["--grep"],
39+
},
40+
tap: {
41+
type: "string",
42+
description:
43+
"The command line arg for configuring a TAP reporter. See https://github.com/sindresorhus/awesome-tap for examples.",
44+
examples: ["--reporter=mocha-tap-reporter"],
45+
},
46+
},
47+
additionalProperties: false,
48+
required: ["tap"],
49+
},
50+
directory: {
51+
type: "string",
52+
description: "An optional folder for the test runner",
53+
examples: ["coderoad"],
54+
},
55+
setup: {
56+
$ref: "#/definitions/setup_action",
57+
description:
58+
"Setup commits or commands used for setting up the test runner on tutorial launch",
59+
},
60+
},
61+
required: ["command", "args"],
62+
},
63+
repo: {
64+
type: "object",
65+
description: "The repo holding the git commits for the tutorial",
66+
properties: {
67+
uri: {
68+
type: "string",
69+
description: "The uri source of the tutorial",
70+
format: "uri",
71+
examples: ["https://github.com/name/tutorial-name.git"],
72+
},
73+
branch: {
74+
description:
75+
"The branch of the repo where the tutorial config file exists",
76+
type: "string",
77+
examples: ["master"],
78+
},
79+
},
80+
additionalProperties: false,
81+
required: ["uri", "branch"],
82+
},
83+
84+
dependencies: {
85+
type: "array",
86+
description: "A list of tutorial dependencies",
87+
items: {
88+
type: "object",
89+
properties: {
90+
name: {
91+
type: "string",
92+
description:
93+
"The command line process name of the dependency. It will be checked by running `name --version`",
94+
examples: ["node", "python"],
95+
},
96+
version: {
97+
type: "string",
98+
description:
99+
"The version requirement. See https://github.com/npm/node-semver for options",
100+
examples: [">=10"],
101+
},
102+
},
103+
required: ["name", "version"],
104+
},
105+
},
106+
appVersions: {
107+
type: "object",
108+
description:
109+
"A list of compatable coderoad versions. Currently only a VSCode extension.",
110+
properties: {
111+
vscode: {
112+
type: "string",
113+
description:
114+
"The version range for coderoad-vscode that this tutorial is compatable with",
115+
examples: [">=0.7.0"],
116+
},
117+
},
118+
},
119+
},
120+
additionalProperties: false,
121+
required: ["testRunner", "repo"],
122+
},
123+
124+
// levels
125+
levels: {
126+
type: "array",
127+
description:
128+
'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',
129+
items: {
130+
type: "object",
131+
properties: {
132+
id: {
133+
type: "string",
134+
description: "A level id",
135+
examples: ["L1", "L11"],
136+
},
137+
setup: {
138+
$ref: "#/definitions/setup_action",
139+
description:
140+
"An optional point for loading commits, running commands or opening files",
141+
},
142+
steps: {
143+
type: "array",
144+
items: {
145+
type: "object",
146+
properties: {
147+
id: {
148+
type: "string",
149+
description: "A level id",
150+
examples: ["L1S1", "L11S12"],
151+
},
152+
setup: {
153+
allOf: [
154+
{
155+
$ref: "#/definitions/setup_action",
156+
description:
157+
"A point for loading commits. It can also run commands and/or open files",
158+
},
159+
],
160+
},
161+
solution: {
162+
allOf: [
163+
{
164+
$ref: "#/definitions/setup_action",
165+
description:
166+
"The solution commits that can be loaded if the user gets stuck. It can also run commands and/or open files",
167+
},
168+
{
169+
required: [],
170+
},
171+
],
172+
},
173+
},
174+
required: ["id", "setup"],
175+
},
176+
},
177+
},
178+
required: ["id"],
179+
},
180+
minItems: 1,
181+
},
182+
},
183+
additionalProperties: false,
184+
required: ["version", "config", "levels"],
185+
};

Diff for: src/schema/index.ts renamed to src/schema/tutorial.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import meta from "./meta";
22

33
export default {
4+
title: "Tutorial Schema",
5+
description:
6+
"A CodeRoad tutorial schema data. This JSON data is converted into a tutorial with the CodeRoad editor extension",
47
...meta,
58
type: "object",
69
properties: {

Diff for: src/templates/js-mocha/coderoad.yaml

+4-5
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,7 @@ levels:
9292
## Example Four: Subtasks
9393
- id: L1S4
9494
setup:
95-
commands:
96-
## A filter is a regex that limits the test results
97-
- filter: "^Example 2"
98-
## A feature that shows subtasks: all filtered active test names and the status of the tests (pass/fail).
99-
- subtasks: true
95+
## A filter is a regex that limits the test results
96+
filter: "^Example 2"
97+
## A feature that shows subtasks: all filtered active test names and the status of the tests (pass/fail).
98+
subtasks: true

Diff for: src/utils/commits.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as fs from "fs";
22
import util from "util";
33
import * as path from "path";
44
import gitP, { SimpleGit } from "simple-git/promise";
5-
import { addToCommitOrder, validateCommitOrder } from "./commitOrder";
5+
import { validateCommitOrder } from "./validateCommits";
66

77
const mkdir = util.promisify(fs.mkdir);
88
const exists = util.promisify(fs.exists);

Diff for: src/utils/parse.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -90,23 +90,22 @@ export function parseMdContent(md: string): TutorialFrame | never {
9090

9191
type ParseParams = {
9292
text: string;
93-
config: Partial<T.Tutorial | any>;
93+
skeleton: Partial<T.Tutorial | any>;
9494
commits: CommitLogObject;
9595
};
9696

9797
export function parse(params: ParseParams): any {
9898
const mdContent: TutorialFrame = parseMdContent(params.text);
9999

100100
const parsed: Partial<T.Tutorial> = {
101-
version: params.config.version,
101+
version: params.skeleton.version,
102102
summary: mdContent.summary,
103-
config: params.config.config || {},
103+
config: params.skeleton.config || {},
104104
levels: [],
105105
};
106106

107107
// add init commits
108108
if (params.commits.INIT && params.commits.INIT.length) {
109-
console.log(JSON.stringify(parsed.config?.testRunner));
110109
// @ts-ignore
111110
parsed.config.testRunner.setup = {
112111
...(parsed.config?.testRunner?.setup || {}),
@@ -115,8 +114,8 @@ export function parse(params: ParseParams): any {
115114
}
116115

117116
// merge content and tutorial
118-
if (params.config.levels && params.config.levels.length) {
119-
parsed.levels = params.config.levels
117+
if (params.skeleton.levels && params.skeleton.levels.length) {
118+
parsed.levels = params.skeleton.levels
120119
.map((level: T.Level, levelIndex: number) => {
121120
const levelContent = mdContent.levels[level.id];
122121

Diff for: src/utils/commitOrder.ts renamed to src/utils/validateCommits.ts

+1-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
// should flag commits that are out of order based on the previous commit
22
// position is a string like 'INIT', 'L1', 'L1S1'
3-
export function addToCommitOrder(position: string) {
4-
// add position to list
5-
}
6-
73
export function validateCommitOrder(positions: string[]): boolean {
84
// loop over positions
95
const errors: number[] = [];
@@ -12,7 +8,6 @@ export function validateCommitOrder(positions: string[]): boolean {
128
positions.forEach((position: string, index: number) => {
139
if (position === "INIT") {
1410
if (previous.level !== 0 && previous.step !== 0) {
15-
console.log("ERROR HERE");
1611
errors.push(index);
1712
}
1813
current = { level: 0, step: 0 };
@@ -46,7 +41,7 @@ export function validateCommitOrder(positions: string[]): boolean {
4641
previous = current;
4742
});
4843

49-
if (errors.length) {
44+
if (errors.length && process.env.NODE_ENV !== "test") {
5045
console.warn("Found commit positions out of order");
5146
positions.forEach((position, index) => {
5247
if (errors.includes(index)) {

Diff for: src/utils/validateSchema.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import schema from "../schema";
2-
31
// https://www.npmjs.com/package/ajv
42
// @ts-ignore ajv typings not working
53
import JsonSchema from "ajv";
64

7-
export function validateSchema(json: any): boolean | PromiseLike<boolean> {
5+
export function validateSchema(
6+
schema: any,
7+
json: any
8+
): boolean | PromiseLike<boolean> {
89
// validate using https://json-schema.org/
910
const jsonSchema = new JsonSchema({
1011
allErrors: true,

0 commit comments

Comments
 (0)