diff --git a/src/schema/skeleton.ts b/src/schema/skeleton.ts index 5a128df..1d654d3 100644 --- a/src/schema/skeleton.ts +++ b/src/schema/skeleton.ts @@ -1,208 +1,257 @@ -import meta from './meta' +import meta from "./meta"; export default { - title: 'Skeleton Schema', + title: "Skeleton Schema", description: - 'A CodeRoad tutorial config schema. This data is paired up with the markdown to create a tutorial', + "A CodeRoad tutorial config schema. This data is paired up with the markdown to create a tutorial", ...meta, - type: 'object', + 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'] + $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', + type: "object", properties: { testRunner: { - type: 'object', - description: 'The test runner configuration', + type: "object", + description: "The test runner configuration", properties: { command: { - type: 'string', - description: 'Command line to start the test runner', - examples: ['./node_modules/.bin/mocha'] + type: "string", + description: "Command line to start the test runner", + examples: ["./node_modules/.bin/mocha"], }, args: { - type: 'object', + type: "object", description: - 'A configuration of command line args for your test runner', + "A configuration of command line args for your test runner", properties: { filter: { - type: 'string', + type: "string", description: - 'the command line arg for filtering tests with a regex pattern', - examples: ['--grep'] + "the command line arg for filtering tests with a regex pattern", + examples: ["--grep"], }, tap: { - type: 'string', + 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'] - } + "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'] + required: ["tap"], }, directory: { - type: 'string', - description: 'An optional folder for the test runner', - examples: ['coderoad'] - } + type: "string", + description: "An optional folder for the test runner", + examples: ["coderoad"], + }, }, - required: ['command', 'args'] + required: ["command", "args"], }, setup: { - type: 'object', + type: "object", description: - 'Setup commits or commands used for setting up the test runner on tutorial launch', + "Setup commits or commands used for setting up the test runner on tutorial launch", properties: { commits: { - $ref: '#/definitions/commit_array' + $ref: "#/definitions/commit_array", }, commands: { - $ref: '#/definitions/command_array' + $ref: "#/definitions/command_array", }, vscodeCommands: { - $ref: '#/definitions/vscode_command_array' - } - } + $ref: "#/definitions/vscode_command_array", + }, + }, }, repo: { - type: 'object', - description: 'The repo holding the git commits for the tutorial', + 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'] + 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'] - } + "The branch of the repo where the tutorial config file exists", + type: "string", + examples: ["master"], + }, }, additionalProperties: false, - required: ['uri', 'branch'] + required: ["uri", "branch"], }, reset: { - type: 'object', - description: 'Configuration options for resetting a tutorial', + type: "object", + description: "Configuration options for resetting a tutorial", properties: { commands: { - $ref: '#/definitions/command_array' + $ref: "#/definitions/command_array", }, vscodeCommands: { - $ref: '#/definitions/vscode_command_array' - } + $ref: "#/definitions/vscode_command_array", + }, }, - additionalProperties: false + additionalProperties: false, }, dependencies: { - type: 'array', - description: 'A list of tutorial dependencies', + type: "array", + description: "A list of tutorial dependencies", items: { - type: 'object', + type: "object", properties: { name: { - type: 'string', + type: "string", description: - 'The command line process name of the dependency. It will be checked by running `name --version`', - examples: ['node', 'python'] + "The command line process name of the dependency. It will be checked by running `name --version`", + examples: ["node", "python"], }, version: { - type: 'string', + type: "string", description: - 'The version requirement. See https://github.com/npm/node-semver for options', - examples: ['>=10'] - } + "The version requirement. See https://github.com/npm/node-semver for options", + examples: [">=10"], + }, }, - required: ['name', 'version'] - } + required: ["name", "version"], + }, }, appVersions: { - type: 'object', + type: "object", description: - 'A list of compatable coderoad versions. Currently only a VSCode extension.', + "A list of compatable coderoad versions. Currently only a VSCode extension.", properties: { vscode: { - type: 'string', + type: "string", description: - 'The version range for coderoad-vscode that this tutorial is compatable with', - examples: ['>=0.7.0'] - } - } - } + "The version range for coderoad-vscode that this tutorial is compatable with", + examples: [">=0.7.0"], + }, + }, + }, + webhook: { + type: "object", + description: + "An optional configuration for webhooks triggered by events such as init, step/level/tutorial complete", + properties: { + url: { + type: "string", + description: "URL for POST restful webhook request", + examples: ["https://example.com/webhook"], + }, + events: { + type: "object", + description: "An object of events to trigger on", + properties: { + init: { + type: "boolean", + description: + "An event triggered on tutorial startup. Sends tutorialId", + }, + reset: { + type: "boolean", + description: + "An event triggered on reset of a tutorial. Sends tutorialId", + }, + step_complete: { + type: "boolean", + description: + "An event triggered on completion of a step. Sends tutorialId, levelId & stepId", + }, + level_complete: { + step_complete: { + type: "boolean", + description: + "An event triggered on completion of a level. Sends tutorialId & levelId", + }, + }, + tutorial_complete: { + step_complete: { + type: "boolean", + description: + "An event triggered on completion of a tutorial. Sends tutorialId", + }, + }, + }, + additionalProperties: false, + }, + }, + required: ["url", "events"], + }, }, additionalProperties: false, - required: ['testRunner', 'repo'] + required: ["testRunner", "repo"], }, // levels levels: { - type: 'array', + 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', + type: "object", properties: { id: { - type: 'string', - description: 'A level id', - examples: ['1', '11'] + type: "string", + description: "A level id", + examples: ["1", "11"], }, setup: { - $ref: '#/definitions/setup_action_without_commits', + $ref: "#/definitions/setup_action_without_commits", description: - 'An optional point for running actions, commands or opening files' + "An optional point for running actions, commands or opening files", }, steps: { - type: 'array', + type: "array", items: { - type: 'object', + type: "object", properties: { id: { - type: 'string', - description: 'A level id', - examples: ['1.1', '11.12'] + type: "string", + description: "A level id", + examples: ["1.1", "11.12"], }, setup: { allOf: [ { - $ref: '#/definitions/setup_action_without_commits', + $ref: "#/definitions/setup_action_without_commits", description: - 'A point for running actions, commands and/or opening files' - } - ] + "A point for running actions, commands and/or opening files", + }, + ], }, solution: { allOf: [ { - $ref: '#/definitions/setup_action_without_commits', + $ref: "#/definitions/setup_action_without_commits", description: - 'The solution can be loaded if the user gets stuck. It can run actions, commands and/or open files' + "The solution can be loaded if the user gets stuck. It can run actions, commands and/or open files", }, { - required: [] - } - ] - } + required: [], + }, + ], + }, }, - required: ['id'] - } - } + required: ["id"], + }, + }, }, - required: ['id'] + required: ["id"], }, - minItems: 1 - } + minItems: 1, + }, }, additionalProperties: false, - required: ['version', 'config', 'levels'] -} + required: ["version", "config", "levels"], +}; diff --git a/src/schema/tutorial.ts b/src/schema/tutorial.ts index 7c8b677..035d164 100644 --- a/src/schema/tutorial.ts +++ b/src/schema/tutorial.ts @@ -1,247 +1,296 @@ -import meta from './meta' +import meta from "./meta"; export default { - title: 'Tutorial Schema', + title: "Tutorial Schema", description: - 'A CodeRoad tutorial schema data. This JSON data is converted into a tutorial with the CodeRoad editor extension', + "A CodeRoad tutorial schema data. This JSON data is converted into a tutorial with the CodeRoad editor extension", ...meta, - type: 'object', + 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'] + $ref: "#/definitions/semantic_version", + description: "The tutorial version. Must be unique for the tutorial.", + examples: ["0.1.0", "1.0.0"], }, // summary summary: { - type: 'object', + type: "object", properties: { title: { - $ref: '#/definitions/title', - description: 'The title of tutorial' + $ref: "#/definitions/title", + description: "The title of tutorial", }, description: { - type: 'string', - description: 'A summary of the the tutorial', + type: "string", + description: "A summary of the the tutorial", minLength: 10, - maxLength: 400 - } + maxLength: 400, + }, }, additionalProperties: false, - required: ['title', 'description'] + required: ["title", "description"], }, // config config: { - type: 'object', + type: "object", properties: { testRunner: { - type: 'object', - description: 'The test runner configuration', + type: "object", + description: "The test runner configuration", properties: { command: { - type: 'string', - description: 'Command line to start the test runner', - examples: ['./node_modules/.bin/mocha'] + type: "string", + description: "Command line to start the test runner", + examples: ["./node_modules/.bin/mocha"], }, args: { - type: 'object', + type: "object", description: - 'A configuration of command line args for your test runner', + "A configuration of command line args for your test runner", properties: { filter: { - type: 'string', + type: "string", description: - 'the command line arg for filtering tests with a regex pattern', - examples: ['--grep'] + "the command line arg for filtering tests with a regex pattern", + examples: ["--grep"], }, tap: { - type: 'string', + 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'] - } + "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'] + required: ["tap"], }, directory: { - type: 'string', - description: 'An optional folder for the test runner', - examples: ['coderoad'] - } + type: "string", + description: "An optional folder for the test runner", + examples: ["coderoad"], + }, }, - required: ['command', 'args'] + required: ["command", "args"], }, setup: { - type: 'object', + type: "object", description: - 'Setup commits or commands used for setting up the test runner on tutorial launch', + "Setup commits or commands used for setting up the test runner on tutorial launch", properties: { commits: { - $ref: '#/definitions/commit_array' + $ref: "#/definitions/commit_array", }, commands: { - $ref: '#/definitions/command_array' + $ref: "#/definitions/command_array", }, vscodeCommands: { - $ref: '#/definitions/vscode_command_array' - } - } + $ref: "#/definitions/vscode_command_array", + }, + }, }, repo: { - type: 'object', - description: 'The repo holding the git commits for the tutorial', + 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'] + 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'] - } + "The branch of the repo where the tutorial config file exists", + type: "string", + examples: ["master"], + }, }, additionalProperties: false, - required: ['uri', 'branch'] + required: ["uri", "branch"], }, reset: { - type: 'object', - description: 'Configuration options for resetting a tutorial', + type: "object", + description: "Configuration options for resetting a tutorial", properties: { commands: { - $ref: '#/definitions/command_array' + $ref: "#/definitions/command_array", }, vscodeCommands: { - $ref: '#/definitions/vscode_command_array' - } + $ref: "#/definitions/vscode_command_array", + }, }, - additionalProperties: false + additionalProperties: false, }, dependencies: { - type: 'array', - description: 'A list of tutorial dependencies', + type: "array", + description: "A list of tutorial dependencies", items: { - type: 'object', + type: "object", properties: { name: { - type: 'string', + type: "string", description: - 'The command line process name of the dependency. It will be checked by running `name --version`', - examples: ['node', 'python'] + "The command line process name of the dependency. It will be checked by running `name --version`", + examples: ["node", "python"], }, version: { - type: 'string', + type: "string", description: - 'The version requirement. See https://github.com/npm/node-semver for options', - examples: ['>=10'] - } + "The version requirement. See https://github.com/npm/node-semver for options", + examples: [">=10"], + }, }, - required: ['name', 'version'] - } + required: ["name", "version"], + }, }, appVersions: { - type: 'object', + type: "object", description: - 'A list of compatable coderoad versions. Currently only a VSCode extension.', + "A list of compatable coderoad versions. Currently only a VSCode extension.", properties: { vscode: { - type: 'string', + type: "string", description: - 'The version range for coderoad-vscode that this tutorial is compatable with', - examples: ['>=0.7.0'] - } - } - } + "The version range for coderoad-vscode that this tutorial is compatable with", + examples: [">=0.7.0"], + }, + }, + }, + webhook: { + type: "object", + description: + "An optional configuration for webhooks triggered by events such as init, step/level/tutorial complete", + properties: { + url: { + type: "string", + description: "URL for POST restful webhook request", + examples: ["https://example.com/webhook"], + }, + events: { + type: "object", + description: "An object of events to trigger on", + properties: { + init: { + type: "boolean", + description: + "An event triggered on tutorial startup. Sends tutorialId", + }, + reset: { + type: "boolean", + description: + "An event triggered on reset of a tutorial. Sends tutorialId", + }, + step_complete: { + type: "boolean", + description: + "An event triggered on completion of a step. Sends tutorialId, levelId & stepId", + }, + level_complete: { + step_complete: { + type: "boolean", + description: + "An event triggered on completion of a level. Sends tutorialId & levelId", + }, + }, + tutorial_complete: { + step_complete: { + type: "boolean", + description: + "An event triggered on completion of a tutorial. Sends tutorialId", + }, + }, + }, + additionalProperties: false, + }, + }, + required: ["url", "events"], + }, }, additionalProperties: false, - required: ['testRunner', 'repo'] + required: ["testRunner", "repo"], }, // levels levels: { - type: 'array', + 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', + type: "object", properties: { title: { - $ref: '#/definitions/title', - description: 'A title for the level' + $ref: "#/definitions/title", + description: "A title for the level", }, summary: { - type: 'string', - description: 'A high-level summary of the level', - maxLength: 250 + type: "string", + description: "A high-level summary of the level", + maxLength: 250, }, content: { - type: 'string', - description: 'Content for a tutorial written as Markdown' + type: "string", + description: "Content for a tutorial written as Markdown", }, setup: { - $ref: '#/definitions/setup_action', + $ref: "#/definitions/setup_action", description: - 'An optional point for loading commits, running commands or opening files' + "An optional point for loading commits, running commands or opening files", }, steps: { - type: 'array', + type: "array", items: { - type: 'object', + type: "object", properties: { content: { - type: 'string', + type: "string", description: - 'The text displayed explaining information about the current task, written as markdown' + "The text displayed explaining information about the current task, written as markdown", }, setup: { allOf: [ { - $ref: '#/definitions/setup_action', + $ref: "#/definitions/setup_action", description: - 'A point for loading commits. It can also run commands and/or open files' + "A point for loading commits. It can also run commands and/or open files", }, { - required: ['commits'] - } - ] + required: ["commits"], + }, + ], }, solution: { allOf: [ { - $ref: '#/definitions/setup_action', + $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' + "The solution commits that can be loaded if the user gets stuck. It can also run commands and/or open files", }, { - required: [] - } - ] - } + required: [], + }, + ], + }, }, hints: { - type: 'array', + type: "array", description: - 'An optional array of hints to provide helpful feedback to users', + "An optional array of hints to provide helpful feedback to users", items: { - type: 'string', - description: 'A hint to provide to the user', - examples: ['Have you tried doing X?'] - } + type: "string", + description: "A hint to provide to the user", + examples: ["Have you tried doing X?"], + }, }, - required: ['content', 'setup'] - } - } + required: ["content", "setup"], + }, + }, }, - required: ['title', 'summary', 'content'] - } - } + required: ["title", "summary", "content"], + }, + }, }, additionalProperties: false, - required: ['version', 'summary', 'config', 'levels'] -} + required: ["version", "summary", "config", "levels"], +}; diff --git a/tests/skeleton.test.ts b/tests/skeleton.test.ts index 758b83e..01f4ad1 100644 --- a/tests/skeleton.test.ts +++ b/tests/skeleton.test.ts @@ -25,7 +25,17 @@ const validJson = { dependencies: [], appVersions: { vscode: '>=0.7.0' - } + }, + webhook: { + url: 'https://example.com/webhook', + events: { + init: true, + reset: false, + step_complete: false, + level_complete: false, + tutorial_complete: true, + } + }, }, levels: [ { @@ -187,6 +197,38 @@ describe('validate skeleton', () => { const valid = validateSkeleton(json) expect(valid).toBe(false) }) + it('should fail if webhook url is missing', () => { + const json = { + ...validJson, + config: { + ...validJson.config, + webhook: { + events: {} + } + } + } + + const valid = validateSkeleton(json) + expect(valid).toBe(false) + }) + it('should fail if webhook events include non-listed events', () => { + const json = { + ...validJson, + config: { + ...validJson.config, + webhook: { + ...validJson.config.webhook, + events: { + ...validJson.config.webhook.events, + not_an_event: true, + } + } + } + } + + const valid = validateSkeleton(json) + expect(valid).toBe(false) + }) it('should fail if level is missing id', () => { const level1 = { ...validJson.levels[0], id: undefined } const json = { diff --git a/tests/tutorial.test.ts b/tests/tutorial.test.ts index 3c2526e..c78001c 100644 --- a/tests/tutorial.test.ts +++ b/tests/tutorial.test.ts @@ -25,7 +25,17 @@ const validJson: Partial = { dependencies: [{ name: 'name', version: '>=1' }], appVersions: { vscode: '>=0.7.0' - } + }, + webhook: { + url: 'https://example.com/webhook', + events: { + init: true, + reset: false, + step_complete: false, + level_complete: false, + tutorial_complete: true, + } + }, }, levels: [ { diff --git a/typings/tutorial.d.ts b/typings/tutorial.d.ts index 2f51657..5eee642 100644 --- a/typings/tutorial.d.ts +++ b/typings/tutorial.d.ts @@ -11,6 +11,7 @@ export type TutorialConfig = { dependencies?: TutorialDependency[] reset?: ConfigReset setup?: StepActions + webhook?: WebhookConfig } /** Logical groupings of tasks */ @@ -91,3 +92,15 @@ export interface TutorialSkeleton { config: TutorialConfig levels: Level[] } + +export interface WebhookConfigEvents { + init?: boolean + reset?: boolean + step_complete?: boolean + level_complete?: boolean + tutorial_complete?: boolean +} +export interface WebhookConfig { + url: string + events?: WebhookConfigEvents +}