├── test ├── fixtures │ ├── node_modules │ │ └── code-challenge-example │ │ │ └── .gitkeep │ ├── active-model.rb │ └── active-model.out ├── support │ └── globals.js └── spec │ ├── utils │ ├── render-file.js │ ├── render.js │ └── resolve-challenges.js │ └── challenge │ └── index.js ├── .gitignore ├── lib ├── utils │ ├── log │ │ ├── format │ │ │ ├── heading.js │ │ │ ├── cross.js │ │ │ ├── check.js │ │ │ └── command.js │ │ ├── log.js │ │ ├── instruction.js │ │ ├── heading.js │ │ ├── separator.js │ │ ├── command-error.js │ │ ├── exercise │ │ │ ├── no-next.js │ │ │ ├── empty.js │ │ │ ├── no-previous.js │ │ │ ├── missing.js │ │ │ ├── pass.js │ │ │ └── fail.js │ │ ├── challenge │ │ │ ├── empty.js │ │ │ ├── none.js │ │ │ └── missing.js │ │ ├── command.js │ │ └── help.js │ ├── challenge │ │ ├── instance.js │ │ ├── find-exercise.js │ │ ├── require.js │ │ ├── complete.js │ │ └── resolve.js │ ├── cli │ │ ├── current-challenge.js │ │ ├── current-exercise.js │ │ ├── print-challenge.js │ │ ├── print-exercise.js │ │ └── list.js │ ├── render │ │ ├── file.js │ │ └── content.js │ └── store │ │ ├── complete.js │ │ ├── current.js │ │ └── data.js ├── cli │ ├── reset.js │ ├── help.js │ ├── ls.js │ ├── print.js │ ├── run.js │ ├── index.js │ ├── verify.js │ ├── next.js │ ├── prev.js │ ├── exercise.js │ └── select.js └── challenge │ ├── exercise.js │ └── index.js ├── bin └── challenge.js ├── .travis.yml ├── index.js ├── .jshintrc ├── package.json └── README.md /test/fixtures/node_modules/code-challenge-example/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | coverage 4 | !test/fixtures/node_modules 5 | -------------------------------------------------------------------------------- /lib/utils/log/format/heading.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk'); 2 | 3 | module.exports = chalk.bold.yellow; 4 | -------------------------------------------------------------------------------- /lib/utils/log/log.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Export the standard logging function as a utility. 3 | * 4 | * @type {Function} 5 | */ 6 | module.exports = console.log.bind(console); 7 | -------------------------------------------------------------------------------- /bin/challenge.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var cli = require('../lib/cli'); 4 | var argv = require('yargs').argv; 5 | 6 | /** 7 | * Pass all arguments off to the CLI module. 8 | */ 9 | cli.argv(argv); 10 | -------------------------------------------------------------------------------- /test/support/globals.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var sinonChai = require('sinon-chai'); 3 | var chaiAsPromised = require('chai-as-promised'); 4 | 5 | chai.use(sinonChai); 6 | chai.use(chaiAsPromised); 7 | -------------------------------------------------------------------------------- /lib/cli/reset.js: -------------------------------------------------------------------------------- 1 | var data = require('../utils/store/data'); 2 | 3 | /** 4 | * Reset all challenge data. 5 | * 6 | * @return {Promise} 7 | */ 8 | module.exports = function () { 9 | return data.reset(); 10 | }; 11 | -------------------------------------------------------------------------------- /lib/utils/log/instruction.js: -------------------------------------------------------------------------------- 1 | var log = require('./log'); 2 | 3 | /** 4 | * Log an instruction to the console. 5 | * 6 | * @param {String} instruction 7 | */ 8 | module.exports = function (instruction) { 9 | log(' ' + instruction); 10 | }; 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | 4 | notifications: 5 | email: 6 | on_success: never 7 | on_failure: change 8 | 9 | node_js: 10 | - "stable" 11 | 12 | after_script: "npm install coveralls@2 && cat ./coverage/lcov.info | coveralls" 13 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Challenge = require('./lib/challenge'); 2 | 3 | /** 4 | * Exports a static challenge instance. Having multiple instances internally 5 | * makes testing somewhat easier to handle. 6 | * 7 | * @type {Challenge} 8 | */ 9 | module.exports = new Challenge(); 10 | -------------------------------------------------------------------------------- /lib/cli/help.js: -------------------------------------------------------------------------------- 1 | var Bluebird = require('bluebird'); 2 | var helpLog = require('../utils/log/help'); 3 | 4 | /** 5 | * Provide the user with help documentation. 6 | * 7 | * @return {Promise} 8 | */ 9 | module.exports = function () { 10 | return Bluebird.resolve(helpLog()); 11 | }; 12 | -------------------------------------------------------------------------------- /lib/utils/log/format/cross.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk'); 2 | 3 | /** 4 | * Prepend the cross to any text. 5 | * 6 | * @param {String} text 7 | * @return {String} 8 | */ 9 | module.exports = function (text) { 10 | return chalk.bold.red('✗') + (text ? ' ' + text : ''); 11 | }; 12 | -------------------------------------------------------------------------------- /lib/utils/log/format/check.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk'); 2 | 3 | /** 4 | * Prepend the checkmark to any text. 5 | * 6 | * @param {String} text 7 | * @return {String} 8 | */ 9 | module.exports = function (text) { 10 | return chalk.bold.green('✓') + (text ? ' ' + text : ''); 11 | }; 12 | -------------------------------------------------------------------------------- /lib/utils/log/heading.js: -------------------------------------------------------------------------------- 1 | var log = require('./log'); 2 | var format = require('./format/heading'); 3 | 4 | /** 5 | * Log a heading to the console. 6 | * 7 | * @param {String} heading 8 | */ 9 | module.exports = function (heading) { 10 | log(format(heading)); 11 | log(); 12 | }; 13 | -------------------------------------------------------------------------------- /lib/utils/log/separator.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk'); 2 | var repeat = require('repeat-string'); 3 | var log = require('./log'); 4 | 5 | /** 6 | * Log a separator to the terminal interface. 7 | */ 8 | module.exports = function () { 9 | log(chalk.dim(repeat('-', process.stdout.columns))); 10 | }; 11 | -------------------------------------------------------------------------------- /lib/utils/log/command-error.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk'); 2 | var log = require('./log'); 3 | var cross = require('./format/cross'); 4 | 5 | module.exports = function (err) { 6 | log(); 7 | log(cross('An error occured while executing your command:')); 8 | log(); 9 | log(chalk.gray(err.stack)); 10 | log(); 11 | }; 12 | -------------------------------------------------------------------------------- /lib/utils/log/exercise/no-next.js: -------------------------------------------------------------------------------- 1 | var log = require('../log'); 2 | var command = require('../command'); 3 | 4 | module.exports = function () { 5 | log(); 6 | log('No more exercises in the current challenge.'); 7 | log('Maybe it\'s time to try a new challenge?'); 8 | log(); 9 | command('select'); 10 | log(); 11 | }; 12 | -------------------------------------------------------------------------------- /lib/utils/log/challenge/empty.js: -------------------------------------------------------------------------------- 1 | var log = require('../log'); 2 | var command = require('../command'); 3 | 4 | /** 5 | * Prompt the user to select a challenge. 6 | */ 7 | module.exports = function () { 8 | log(); 9 | log('No challenge has been selected. To select a challenge:'); 10 | log(); 11 | command('select'); 12 | log(); 13 | }; 14 | -------------------------------------------------------------------------------- /lib/utils/log/command.js: -------------------------------------------------------------------------------- 1 | var log = require('./log'); 2 | var format = require('./format/command'); 3 | var __slice = Array.prototype.slice; 4 | 5 | /** 6 | * Log the command to the console. 7 | * 8 | * @param {String} [..args] 9 | */ 10 | module.exports = function (/* ...args */) { 11 | log(' ' + format(__slice.call(arguments))); 12 | }; 13 | -------------------------------------------------------------------------------- /lib/utils/log/exercise/empty.js: -------------------------------------------------------------------------------- 1 | var log = require('../log'); 2 | var command = require('../command'); 3 | 4 | /** 5 | * Prompt the user to select an exercise. 6 | */ 7 | module.exports = function () { 8 | log(); 9 | log('No exercise has been selected. To select an exercise:'); 10 | log(); 11 | command('exercise'); 12 | log(); 13 | }; 14 | -------------------------------------------------------------------------------- /lib/cli/ls.js: -------------------------------------------------------------------------------- 1 | var resolveChallenges = require('../utils/challenge/resolve'); 2 | 3 | /** 4 | * List all found modules. Useful for debugging. 5 | * 6 | * @return {Promise} 7 | */ 8 | module.exports = function () { 9 | return resolveChallenges(process.cwd()) 10 | .then(function (dirs) { 11 | console.log(dirs.join('\n')); 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /lib/utils/log/exercise/no-previous.js: -------------------------------------------------------------------------------- 1 | var log = require('../log'); 2 | var command = require('../command'); 3 | 4 | module.exports = function () { 5 | log(); 6 | log('You are currently at the first exercise in the challenge.'); 7 | log('Did you want to look at all the exercises instead?'); 8 | log(); 9 | command('exercise'); 10 | log(); 11 | }; 12 | -------------------------------------------------------------------------------- /lib/utils/challenge/instance.js: -------------------------------------------------------------------------------- 1 | var Bluebird = require('bluebird'); 2 | var resolve = Bluebird.promisify(require('resolve')); 3 | 4 | /** 5 | * Resolve the modules code challenge instance. 6 | * 7 | * @param {String} dirname 8 | * @return {Promise} 9 | */ 10 | module.exports = function (dirname) { 11 | return resolve('code-challenge', { basedir: dirname }); 12 | }; 13 | -------------------------------------------------------------------------------- /lib/utils/log/format/command.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk'); 2 | 3 | /** 4 | * Format the command line arguments for the console. 5 | * 6 | * @param {Array} args 7 | */ 8 | module.exports = function (args) { 9 | var command = chalk.bold.green('challenge'); 10 | 11 | if (args && args.length) { 12 | command += ' ' + chalk.bold(args.join(' ')); 13 | } 14 | 15 | return command; 16 | }; 17 | -------------------------------------------------------------------------------- /lib/utils/challenge/find-exercise.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Find an exercise by name on a challenge instance. 3 | * 4 | * @param {Object} challenge 5 | * @param {String} name 6 | * @return {Object} 7 | */ 8 | module.exports = function (challenge, name) { 9 | for (var i = 0; i < challenge.exercises.length; i++) { 10 | var exercise = challenge.exercises[i]; 11 | 12 | if (exercise.name === name) { 13 | return exercise; 14 | } 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /lib/cli/print.js: -------------------------------------------------------------------------------- 1 | var printExercise = require('../utils/cli/print-exercise'); 2 | var getCurrentExercise = require('../utils/cli/current-exercise'); 3 | 4 | /** 5 | * Print the current challenge to the user. 6 | * 7 | * @return {Promise} 8 | */ 9 | module.exports = function () { 10 | return getCurrentExercise() 11 | .then(function (exercise) { 12 | if (exercise) { 13 | return printExercise(exercise); 14 | } 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /lib/cli/run.js: -------------------------------------------------------------------------------- 1 | var getCurrentExercise = require('../utils/cli/current-exercise'); 2 | 3 | /** 4 | * Run the current exercise. 5 | * 6 | * @param {Array} args 7 | * @return {Promise} 8 | */ 9 | module.exports = function (args) { 10 | return getCurrentExercise() 11 | .then(function (exercise) { 12 | if (!exercise) { 13 | return; 14 | } 15 | 16 | return exercise.start('verify', args).catch(function () {}); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /lib/utils/log/challenge/none.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk'); 2 | var log = require('../log'); 3 | var cross = require('../format/cross'); 4 | 5 | /** 6 | * Tell the user when there are no challenges installed yet. 7 | */ 8 | module.exports = function (dirname) { 9 | log(); 10 | log(cross('No challenges are installed. Try Project Euler:' 11 | )); 12 | log(); 13 | log(chalk.bold.yellow(' npm install -g code-challenge-euler')); 14 | log(); 15 | }; 16 | -------------------------------------------------------------------------------- /lib/utils/challenge/require.js: -------------------------------------------------------------------------------- 1 | var resolveInstance = require('./instance'); 2 | 3 | module.exports = function (basedir) { 4 | // Require the main challenge file before loading the challenge data. 5 | require(basedir); 6 | 7 | // Resolve the challenge instance and require. 8 | return resolveInstance(basedir).then(function (module) { 9 | var challenge = require(module[0]); 10 | 11 | challenge.dirname = basedir; 12 | 13 | return challenge; 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /lib/utils/log/exercise/missing.js: -------------------------------------------------------------------------------- 1 | var log = require('../log'); 2 | var command = require('../command'); 3 | 4 | /** 5 | * Log missing exercises for user action. 6 | * 7 | * @param {String} exerciseName 8 | */ 9 | module.exports = function (exerciseName) { 10 | log(); 11 | log(cross( 12 | 'The exercise named "' + chalk.bold(exerciseName) + '" has disappeared' 13 | )); 14 | log(); 15 | log('Please select a new exercise:'); 16 | log(); 17 | command('exercise'); 18 | log(); 19 | }; 20 | -------------------------------------------------------------------------------- /lib/utils/log/challenge/missing.js: -------------------------------------------------------------------------------- 1 | var log = require('../log'); 2 | var command = require('../command'); 3 | var cross = require('../format/cross'); 4 | 5 | /** 6 | * Log missing challenges and prompt user action. 7 | * 8 | * @param {String} dirname 9 | */ 10 | module.exports = function (dirname) { 11 | log(); 12 | log(cross('The challenge at "' + chalk.bold(dirname) + '" no longer exists')); 13 | log(); 14 | log('To select a new challenge:'); 15 | log(); 16 | command('select'); 17 | log(); 18 | }; 19 | -------------------------------------------------------------------------------- /lib/utils/challenge/complete.js: -------------------------------------------------------------------------------- 1 | var complete = require('../store/complete'); 2 | var requireChallenge = require('./require'); 3 | 4 | /** 5 | * Check whether a challenge has been completed. 6 | * 7 | * @param {Object} challenge 8 | * @return {Promise} 9 | */ 10 | module.exports = function (challenge) { 11 | return complete.get(challenge.dirname) 12 | .then(function (completed) { 13 | return challenge.exercises.every(function (exercise) { 14 | return completed.indexOf(exercise.name) !== -1; 15 | }); 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /test/spec/utils/render-file.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var join = require('path').join; 3 | var expect = require('chai').expect; 4 | var renderFile = require('../../../lib/utils/render/file'); 5 | 6 | describe('render file', function () { 7 | it('should render the file based on file type', function () { 8 | var input = join(__dirname, '../../fixtures/active-model.rb'); 9 | var output = join(__dirname, '../../fixtures/active-model.out'); 10 | 11 | return renderFile(input) 12 | .then(function (content) { 13 | expect(content.toString()).to.equal(fs.readFileSync(output, 'utf-8')); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /test/spec/utils/render.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var join = require('path').join; 3 | var expect = require('chai').expect; 4 | var render = require('../../../lib/utils/render/content'); 5 | 6 | describe('render', function () { 7 | it('should render content with a language specification', function () { 8 | var input = join(__dirname, '../../fixtures/active-model.rb'); 9 | var output = join(__dirname, '../../fixtures/active-model.out'); 10 | 11 | return render(fs.readFileSync(input, 'utf-8'), 'rb') 12 | .then(function (content) { 13 | expect(content.toString()).to.equal(fs.readFileSync(output, 'utf-8')); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /lib/utils/cli/current-challenge.js: -------------------------------------------------------------------------------- 1 | var current = require('../store/current'); 2 | var requireChallenge = require('../challenge/require'); 3 | var logEmptyChallenge = require('../log/challenge/empty'); 4 | var logMissingChallenge = require('../log/challenge/missing'); 5 | 6 | /** 7 | * Attempt to load the current challenge, logging any required user action. 8 | * 9 | * @return {Promise} 10 | */ 11 | module.exports = function () { 12 | return current.get('challenge') 13 | .then(function (dirname) { 14 | if (!dirname) { 15 | return logEmptyChallenge(); 16 | } 17 | 18 | return requireChallenge(dirname) 19 | .catch(function () { 20 | return logMissingChallenge(dirname); 21 | }); 22 | }) 23 | }; 24 | -------------------------------------------------------------------------------- /test/spec/utils/resolve-challenges.js: -------------------------------------------------------------------------------- 1 | var join = require('path').join; 2 | var expect = require('chai').expect; 3 | var resolve = require('../../../lib/utils/challenge/resolve'); 4 | 5 | /** 6 | * Path to the test fixtures directory. 7 | * 8 | * @type {String} 9 | */ 10 | var FIXTURES_DIR = join(__dirname, '../../fixtures'); 11 | 12 | /** 13 | * Get the directory of the example challenge for testing. 14 | * 15 | * @type {String} 16 | */ 17 | var EXAMPLE_CHALLENGE_DIR = join( 18 | FIXTURES_DIR, 'node_modules/code-challenge-example' 19 | ); 20 | 21 | describe('resolve challenges', function () { 22 | it('should resolve local modules', function () { 23 | return resolve(FIXTURES_DIR) 24 | .then(function (modules) { 25 | expect(modules).to.contain(EXAMPLE_CHALLENGE_DIR); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /lib/utils/render/file.js: -------------------------------------------------------------------------------- 1 | var Bluebird = require('bluebird'); 2 | var map = require('language-map'); 3 | var detect = Bluebird.promisify(require('language-detect')); 4 | var readFile = Bluebird.promisify(require('fs').readFile); 5 | var render = require('./content'); 6 | 7 | /** 8 | * Render a file with syntax highlighting. 9 | * 10 | * @param {String} file 11 | * @param {String} [format] 12 | * @return {Promise} 13 | */ 14 | module.exports = function (file, format) { 15 | return detect(file) 16 | .then(function (language) { 17 | var extensions = (map[language] || {}).extensions; 18 | var shortName = extensions && extensions[0].substr(1); 19 | 20 | return readFile(file, 'utf-8') 21 | .then(function (content) { 22 | return render(content, shortName, format); 23 | }); 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /lib/cli/index.js: -------------------------------------------------------------------------------- 1 | var logCommandError = require('../utils/log/command-error'); 2 | 3 | /** 4 | * Map of available commands. 5 | * 6 | * @type {Object} 7 | */ 8 | var COMMANDS = { 9 | ls: require('./ls'), 10 | run: require('./run'), 11 | next: require('./next'), 12 | prev: require('./prev'), 13 | help: require('./help'), 14 | reset: require('./reset'), 15 | print: require('./print'), 16 | select: require('./select'), 17 | verify: require('./verify'), 18 | exercise: require('./exercise') 19 | }; 20 | 21 | /** 22 | * Correctly route command line arguments. 23 | * 24 | * @param {Object} argv 25 | */ 26 | exports.argv = function (argv) { 27 | var command = argv._[0]; 28 | var args = argv._.slice(1); 29 | 30 | if (!COMMANDS.hasOwnProperty(command)) { 31 | command = 'help'; 32 | } 33 | 34 | return COMMANDS[command](args).catch(logCommandError); 35 | }; 36 | -------------------------------------------------------------------------------- /lib/utils/render/content.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var Bluebird = require('bluebird'); 3 | var readFile = Bluebird.promisify(require('fs').readFile); 4 | var pygmentize = Bluebird.promisify(require('pygmentize-bundled')); 5 | 6 | /** 7 | * Render static content with syntax highlighting. 8 | * 9 | * @param {String} content 10 | * @param {String} language 11 | * @param {String} [format] 12 | * @return {Promise} 13 | */ 14 | module.exports = function (content, language, format) { 15 | format = format || 'terminal256'; 16 | 17 | return pygmentize({ lang: language, format: format }, content) 18 | .catch(isOperationError, _.constant(content)); 19 | }; 20 | 21 | /** 22 | * Check if an error is an operational error instance. 23 | * 24 | * @param {Error} err 25 | * @return {Boolean} 26 | */ 27 | function isOperationError (err) { 28 | return err.name === 'OperationalError'; 29 | }; 30 | -------------------------------------------------------------------------------- /lib/cli/verify.js: -------------------------------------------------------------------------------- 1 | var complete = require('../utils/store/complete'); 2 | var exerciseFailLog = require('../utils/log/exercise/fail'); 3 | var exercisePassLog = require('../utils/log/exercise/pass'); 4 | var getCurrentExercise = require('../utils/cli/current-exercise'); 5 | 6 | /** 7 | * Verify an exercise is working and passes successfully. 8 | * 9 | * @param {Array} args 10 | * @return {Promise} 11 | */ 12 | module.exports = function (args) { 13 | return getCurrentExercise() 14 | .then(function (exercise) { 15 | if (!exercise) { 16 | return; 17 | } 18 | 19 | // Run the exercise and pretty print any errors that occur. 20 | return exercise.start('verify', args) 21 | .then(function () { 22 | complete.add(exercise.challenge.dirname, exercise.name); 23 | 24 | return exercisePassLog(exercise); 25 | }, function (err) { 26 | return exerciseFailLog(exercise, err); 27 | }); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /lib/cli/next.js: -------------------------------------------------------------------------------- 1 | var current = require('../utils/store/current'); 2 | var printExercise = require('../utils/cli/print-exercise'); 3 | var getCurrentExercise = require('../utils/cli/current-exercise'); 4 | var logNoNextExercise = require('../utils/log/exercise/no-next'); 5 | 6 | /** 7 | * Proceed to the next exercise. 8 | * 9 | * @return {Promise} 10 | */ 11 | module.exports = function () { 12 | return getCurrentExercise() 13 | .then(function (exercise) { 14 | if (!exercise) { 15 | return; 16 | } 17 | 18 | // Select the next exercise in the challenge. 19 | var nextExercise = exercise.challenge.exercises[exercise.index + 1]; 20 | 21 | // If this is the last exercise in the challenge, notify the user. 22 | if (!nextExercise) { 23 | return logNoNextExercise(); 24 | } 25 | 26 | // Log the exercise to the user. 27 | printExercise(nextExercise); 28 | 29 | return current.set('exercise', nextExercise.name); 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /lib/cli/prev.js: -------------------------------------------------------------------------------- 1 | var current = require('../utils/store/current'); 2 | var printExercise = require('../utils/cli/print-exercise'); 3 | var getCurrentExercise = require('../utils/cli/current-exercise'); 4 | var logNoPrevExercise = require('../utils/log/exercise/no-previous'); 5 | 6 | /** 7 | * Revert to the previous exercise in the challenge. 8 | * 9 | * @return {Promise} 10 | */ 11 | module.exports = function () { 12 | return getCurrentExercise() 13 | .then(function (exercise) { 14 | if (!exercise) { 15 | return; 16 | } 17 | 18 | // Select the next exercise in the challenge. 19 | var prevExercise = exercise.challenge.exercises[exercise.index - 1]; 20 | 21 | // If this is the last exercise in the challenge, notify the user. 22 | if (!prevExercise) { 23 | return logNoPrevExercise(); 24 | } 25 | 26 | // Log the exercise to the user. 27 | printExercise(prevExercise); 28 | 29 | return current.set('exercise', prevExercise.name); 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /lib/utils/log/exercise/pass.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk'); 2 | var log = require('../log'); 3 | var check = require('../format/check'); 4 | var command = require('../command'); 5 | var separator = require('../separator'); 6 | 7 | /** 8 | * Log an error message to the user about the failed exercise. 9 | * 10 | * @param {Object} exercise 11 | * @param {Error} err 12 | */ 13 | module.exports = function (exercise, err) { 14 | log(); 15 | log(check('Your submission succeeded')); 16 | log(); 17 | log(chalk.bold.green('# PASS')); 18 | log(); 19 | log('Your solution to "' + chalk.bold(exercise.name) + '" worked!'); 20 | log(); 21 | separator(); 22 | log(); 23 | 24 | if (exercise.index < exercise.challenge.exercises.length - 1) { 25 | log('Proceed to the next exercise:'); 26 | command('next'); 27 | log('Go back to the previous exercise:'); 28 | command('prev'); 29 | } else { 30 | log('All exercises complete! Try a new challenge:'); 31 | command('select'); 32 | } 33 | 34 | log(); 35 | }; 36 | -------------------------------------------------------------------------------- /lib/utils/cli/current-exercise.js: -------------------------------------------------------------------------------- 1 | var current = require('../store/current'); 2 | var findExercise = require('../challenge/find-exercise'); 3 | var logEmptyExercise = require('../log/exercise/empty'); 4 | var logMissingExercise = require('../log/exercise/missing'); 5 | var getCurrentChallenge = require('./current-challenge'); 6 | 7 | /** 8 | * Attempt to load the current exercise, logging any required user action. 9 | * 10 | * @return {Promise} 11 | */ 12 | module.exports = function () { 13 | return getCurrentChallenge() 14 | .then(function (challenge) { 15 | if (!challenge) { 16 | return; 17 | } 18 | 19 | return current.get('exercise') 20 | .then(function (exerciseName) { 21 | if (!exerciseName) { 22 | return logEmptyExercise(); 23 | } 24 | 25 | var exercise = findExercise(challenge, exerciseName); 26 | 27 | if (!exercise) { 28 | return logEmptyExercise(exerciseName); 29 | } 30 | 31 | return exercise; 32 | }); 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /lib/utils/cli/print-challenge.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var chalk = require('chalk'); 3 | var log = require('../log/log'); 4 | var separator = require('../log/separator'); 5 | var command = require('../log/command'); 6 | var complete = require('../store/complete'); 7 | 8 | /** 9 | * Print a challenge to the command line. 10 | * 11 | * @param {Object} challenge 12 | * @return {Promise} 13 | */ 14 | module.exports = function (challenge) { 15 | return complete.get(challenge.dirname) 16 | .then(function (exercises) { 17 | var total = challenge.exercises.length; 18 | var names = _.pluck(challenge.exercises, 'name'); 19 | var completed = _.intersection(exercises, names).length; 20 | 21 | log(); 22 | log(chalk.green.bold.underline(challenge.name)); 23 | log(); 24 | log(chalk.yellow(total + ' Exercises (' + completed + ' Completed)')); 25 | log(); 26 | separator(); 27 | log(); 28 | log('Select an exercise:'); 29 | command('exercise'); 30 | log('Select a different challenge:'); 31 | command('select'); 32 | log('For help:'); 33 | command('help'); 34 | log(); 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /lib/utils/log/help.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk'); 2 | var log = require('./log'); 3 | var heading = require('./heading'); 4 | var command = require('./command'); 5 | var instruction = require('./instruction'); 6 | 7 | /** 8 | * Log the help menu for the user. 9 | */ 10 | module.exports = function () { 11 | log(); 12 | heading('Usage: challenge '); 13 | 14 | command('select'); 15 | instruction('Show a menu to select the challenge you would like to try.'); 16 | 17 | command('exercise'); 18 | instruction('Show a menu to select an exercise from the current challenge.'); 19 | 20 | command('print'); 21 | instruction('Print any instructions for the current exercise.'); 22 | 23 | command('run', '[ARGS]'); 24 | instruction('Run your solution to the exercise in a test environment.'); 25 | 26 | command('verify', '[ARGS]'); 27 | instruction('Verify whether your solution to the exercise works.'); 28 | 29 | command('reset'); 30 | instruction('Reset all challenge progress.'); 31 | 32 | command('next'); 33 | instruction('Proceed to the next exercise in the challenge.'); 34 | 35 | command('prev'); 36 | instruction('Return to the previous exercise in the challenge.'); 37 | log(); 38 | }; 39 | -------------------------------------------------------------------------------- /lib/utils/cli/print-exercise.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk'); 2 | var log = require('../log/log'); 3 | var separator = require('../log/separator'); 4 | var command = require('../log/command'); 5 | 6 | /** 7 | * Print the exercise to the command line. 8 | * 9 | * @param {Object} exercise 10 | * @return {Promise} 11 | */ 12 | module.exports = function (exercise) { 13 | var challenge = exercise.challenge; 14 | var totalExercises = challenge.exercises.length; 15 | var currentExercise = exercise.index + 1; 16 | 17 | log(); 18 | log(chalk.green.bold.underline(challenge.name)); 19 | log(); 20 | log(chalk.yellow.bold(exercise.name)); 21 | log(chalk.yellow('Exercise ' + currentExercise + ' of ' + totalExercises)); 22 | log(); 23 | separator(); 24 | log(); 25 | 26 | // Run the print script and log content afterward. 27 | return exercise.start('print') 28 | .then(function () { 29 | log(); 30 | separator(); 31 | log(); 32 | log('Print these instructions again:'); 33 | command('print'); 34 | log('Execute your solution in a test environment:'); 35 | command('run', '[ARGS]'); 36 | log('Verify your solution:'); 37 | command('verify', '[ARGS]'); 38 | log('For help:'); 39 | command('help'); 40 | log(); 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /lib/utils/store/complete.js: -------------------------------------------------------------------------------- 1 | var data = require('./data'); 2 | 3 | /** 4 | * Create a key to store results. 5 | * 6 | * @param {String} name 7 | * @return {String} 8 | */ 9 | var key = function (name) { 10 | return 'completed:' + name; 11 | }; 12 | 13 | /** 14 | * Add an exercise to the completed array of a challenge. 15 | * 16 | * @param {String} challenge 17 | * @param {String} exercise 18 | * @return {Promise} 19 | */ 20 | exports.add = function (challenge, exercise) { 21 | return this.get(challenge).then(function (completed) { 22 | // If the exercise has not already been completed, push it into storage. 23 | if (completed.indexOf(exercise) === -1) { 24 | completed.push(exercise); 25 | 26 | return data.set(key(challenge), completed); 27 | } 28 | 29 | return completed; 30 | }); 31 | }; 32 | 33 | /** 34 | * Get all completed exercises for a challenge. 35 | * 36 | * @param {String} challenge 37 | * @return {Promise} 38 | */ 39 | exports.get = function (challenge) { 40 | return data.get(key(challenge)).then(function (completed) { 41 | return completed || []; 42 | }); 43 | }; 44 | 45 | /** 46 | * Remove the results for a challenge. 47 | * 48 | * @param {String} challenge 49 | * @return {Promise} 50 | */ 51 | exports.remove = function (challenge) { 52 | return data.remove(key(challenge)); 53 | }; 54 | -------------------------------------------------------------------------------- /lib/utils/store/current.js: -------------------------------------------------------------------------------- 1 | var data = require('./data'); 2 | 3 | /** 4 | * Get the current persistent data. 5 | * 6 | * @param {String} [name] 7 | * @return {Promise} 8 | */ 9 | exports.get = function (name) { 10 | return data.get('current').then(function (current) { 11 | current = current || {}; 12 | 13 | return name ? current[name] : current; 14 | }); 15 | }; 16 | 17 | /** 18 | * Set the current persistent state. 19 | * 20 | * @param {String} name 21 | * @param {*} value 22 | */ 23 | exports.set = function (name, value) { 24 | // Override the entire object. 25 | if (typeof name === 'object') { 26 | return data.set('current', name); 27 | } 28 | 29 | // Set a single value. 30 | return this.get().then(function (current) { 31 | current[name] = value; 32 | 33 | return data.set('current', current); 34 | }); 35 | }; 36 | 37 | /** 38 | * Remove current persistence state. 39 | * 40 | * @param {String} name 41 | * @return {Promise} 42 | */ 43 | exports.remove = function (name) { 44 | // Remove all current data. 45 | if (!name) { 46 | return data.remove('current'); 47 | } 48 | 49 | // Remove a single value. 50 | return this.get().then(function (current) { 51 | // The value exists, so we need to persist deletion. 52 | if (current.hasOwnProperty(name)) { 53 | delete current[name]; 54 | 55 | return data.set('current', current); 56 | } 57 | 58 | return current; 59 | }); 60 | }; 61 | -------------------------------------------------------------------------------- /lib/challenge/exercise.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var util = require('util'); 3 | var Bluebird = require('bluebird'); 4 | var Orchestrator = require('orchestrator'); 5 | 6 | /** 7 | * Create an exercise for the user by defining tasks using standardised names. 8 | * This is essentially just a dumb wrapper around Orchestrator. 9 | * 10 | * @param {string} name 11 | */ 12 | var Exercise = module.exports = function (name) { 13 | Orchestrator.call(this); 14 | 15 | this.name = name; 16 | }; 17 | 18 | /** 19 | * Exercises inherit from orchestrator, which gives us a nice async flow. 20 | */ 21 | util.inherits(Exercise, Orchestrator); 22 | 23 | /** 24 | * Make ochestrator methods return promises for cleaner flow control. 25 | * 26 | * @param {String} name 27 | * @param {Array} opts 28 | */ 29 | Exercise.prototype.start = function (tasks, opts) { 30 | var self = this; 31 | var args = Array.isArray(tasks) ? _.clone(tasks) : [tasks]; 32 | 33 | // Allow arguments to be defined on the context object for tasks. 34 | this._ = opts || []; 35 | 36 | return new Bluebird(function (resolve, reject) { 37 | /** 38 | * Passes a callback function to the ochestrator task runner. The callback 39 | * will then resolve the promise object. 40 | * 41 | * @param {Error} err 42 | */ 43 | args.push(function (err) { 44 | // Delete the arguments from the context object. 45 | delete self._; 46 | 47 | // Resolve the promise. 48 | return err ? reject(err) : resolve(); 49 | }); 50 | 51 | return Orchestrator.prototype.start.apply(self, args); 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "maxerr": 100, // Maximum errors before stopping. 3 | "maxlen": 80, // Maximum line length. 4 | "quotmark": "single", // Consistent quotation mark usage. 5 | "bitwise": false, // Prohibit bitwise operators (&, |, ^, etc.). 6 | "curly": true, // Require {} for every new block or scope. 7 | "eqeqeq": true, // Require triple equals i.e. `===`. 8 | "forin": false, // Tolerate `for in` loops without `hasOwnPrototype`. 9 | "immed": true, // Require immediate invocations to be wrapped in parens. E.g. `(function(){})();`. 10 | "latedef": false, // Prohibit variable use before definition. 11 | "newcap": true, // Require capitalization of all constructor functions. E.g. `new F()`. 12 | "noarg": true, // Prohibit use of `arguments.caller` and `arguments.callee`. 13 | "noempty": true, // Prohibit use of empty blocks. 14 | "nonew": true, // Prohibit use of constructors for side-effects. 15 | "undef": true, // Require all non-global variables be declared before they are used. 16 | "unused": true, // Warn when creating a variable but never using it. 17 | "plusplus": false, // Prohibit use of `++` & `--`. 18 | "regexp": false, // Prohibit `.` and `[^...]` in regular expressions. 19 | "strict": false, // Require `use strict` pragma in every file. 20 | "trailing": true, // Prohibit trailing whitespaces. 21 | "boss": true, // Tolerate assignments inside if, for and while. Usually conditions and loops are for comparison, not assignments. 22 | "multistr": false, // Prohibit the use of multi-line strings. 23 | "eqnull": true, // Tolerate use of `== null`. 24 | "expr": true, // Tolerate `ExpressionStatement` as Programs. 25 | "node": true // Enable standard globals available when code is running inside of the NodeJS runtime environment. 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code-challenge", 3 | "description": "Command line interface for running code challenges", 4 | "version": "0.0.5", 5 | "main": "./index.js", 6 | "scripts": { 7 | "test": "npm run test-lint && npm run test-spec", 8 | "test-lint": "jshint *.js lib/**/*.js", 9 | "test-spec": "istanbul cover node_modules/mocha/bin/_mocha test/spec/**/*.js --require test/support/globals.js -- -R spec" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/blakeembrey/code-challenge.git" 14 | }, 15 | "bin": { 16 | "challenge": "./bin/challenge.js" 17 | }, 18 | "keywords": [ 19 | "code", 20 | "challenge", 21 | "problem", 22 | "practice", 23 | "interview" 24 | ], 25 | "author": { 26 | "name": "Blake Embrey", 27 | "email": "hello@blakeembrey.com", 28 | "url": "http://blakeembrey.me" 29 | }, 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/blakeembrey/code-challenge/issues" 33 | }, 34 | "homepage": "https://github.com/blakeembrey/code-challenge", 35 | "dependencies": { 36 | "bluebird": "^3.4.6", 37 | "chalk": "^2.0.0", 38 | "diff": "^3.0.1", 39 | "glob": "^7.1.1", 40 | "javascript-stringify": "^1.0.0", 41 | "language-detect": "^1.0.0", 42 | "language-detect-exec": "^1.0.0", 43 | "language-detect-spawn": "^1.0.0", 44 | "language-map": "^1.0.0", 45 | "lodash": "^4.16.5", 46 | "mkdirp": "^0.5.0", 47 | "orchestrator": "^0.3.3", 48 | "pygmentize-bundled": "^2.1.1", 49 | "repeat-string": "^1.6.1", 50 | "resolve": "^1.0.0", 51 | "term-list-scrollable": "^0.1.3", 52 | "yargs": "^8.0.1" 53 | }, 54 | "devDependencies": { 55 | "chai": "^4.0.2", 56 | "chai-as-promised": "^7.0.0", 57 | "istanbul": "^0.4.5", 58 | "jshint": "^2.9.4", 59 | "mocha": "^3.1.2", 60 | "sinon": "^2.2.0", 61 | "sinon-chai": "^2.5.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/utils/cli/list.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk'); 2 | var Bluebird = require('bluebird'); 3 | var ScrollableList = require('term-list-scrollable'); 4 | 5 | /** 6 | * Create a CLI list and resolve on selection. Makes a more usable promise 7 | * based UI than previously provided. 8 | * 9 | * @param {Array} items 10 | * @param {Object} opts 11 | * @return {Promise} 12 | */ 13 | module.exports = function (items, opts) { 14 | return new Bluebird(function (resolve, reject) { 15 | var padding = 4 + (opts.footer ? 2 : 0); 16 | 17 | var list = new ScrollableList({ 18 | marker: chalk.red('› '), 19 | markerLength: 2, 20 | viewportSize: Math.max(4, process.stdout.rows - padding) 21 | }); 22 | 23 | // Resolve immediately when no items are present. 24 | if (!items || items.length === 0) { 25 | return resolve({ action: 'empty' }); 26 | } 27 | 28 | // Iterate over each item in the list and render. 29 | items.forEach(function (item) { 30 | list.add(item[0], item[1]); 31 | }); 32 | 33 | // Listen for hitting enter. 34 | list.on('keypress', function (key, item) { 35 | if (key.name === 'return') { 36 | list.stop(); 37 | 38 | return resolve({ 39 | item: item, 40 | action: 'select' 41 | }); 42 | } 43 | 44 | if (key.name === 'c' && key.ctrl) { 45 | list.stop(); 46 | 47 | return resolve({ 48 | item: item, 49 | action: 'exit' 50 | }); 51 | } 52 | }); 53 | 54 | // Support empty lists. 55 | list.on('empty', function () { 56 | list.stop(); 57 | 58 | return resolve({ action: 'empty' }); 59 | }); 60 | 61 | // An existing element can be set to selected. 62 | if (opts.selected) { 63 | list.select(opts.selected); 64 | } 65 | 66 | // Append a fixed footer to the menu. 67 | if (opts.footer) { 68 | list.footer('\n ' + chalk.bold(opts.footer)); 69 | } 70 | 71 | // Start the list rendering. 72 | list.start(); 73 | }); 74 | }; 75 | -------------------------------------------------------------------------------- /lib/utils/challenge/resolve.js: -------------------------------------------------------------------------------- 1 | var glob = require('glob'); 2 | var path = require('path'); 3 | var Bluebird = require('bluebird'); 4 | var isWin = process.platform === 'win32'; 5 | 6 | /** 7 | * Convert a directory lookup into a global node modules path lookup. 8 | * 9 | * @param {String} dir 10 | * @return {Array} 11 | */ 12 | var toNpmPaths = function (dir) { 13 | // Initialize the paths to every directory level until the root directory. 14 | var paths = dir.split(path.sep).map(function (part, index, parts) { 15 | return path.resolve.apply( 16 | path, ['/'].concat(parts.slice(0, index + 1)).concat('node_modules') 17 | ); 18 | }).reverse(); 19 | 20 | // Push the node path environment variable onto the path stack. 21 | if (process.env.NODE_PATH) { 22 | paths.push.apply(paths, process.env.NODE_PATH.split(path.delimiter)); 23 | } 24 | 25 | // Global node modules will normally be up from the current directory. 26 | paths.push(path.join(__dirname, '../../../../../node_modules')); 27 | 28 | // Push the global path based on the current executable path. 29 | paths.push(path.join( 30 | process.execPath, isWin ? '..' : '../../lib', 'node_modules' 31 | )); 32 | 33 | // Filter duplicate paths. 34 | return paths.filter(function (path, index, paths) { 35 | return paths.indexOf(path) === index; 36 | }); 37 | }; 38 | 39 | /** 40 | * Resolve requirable challanges. 41 | * 42 | * @param {String} baseDir 43 | * @return {Promise} 44 | */ 45 | module.exports = function (baseDir) { 46 | return new Bluebird(function (resolve, reject) { 47 | (function search (matches, paths) { 48 | if (!paths.length) { 49 | return resolve(matches); 50 | } 51 | 52 | return glob('code-challenge-*', { cwd: paths[0] }, function (err, found) { 53 | if (err) { 54 | return reject(err); 55 | } 56 | 57 | return search(matches.concat(found.map(function (module) { 58 | return path.join(paths[0], module); 59 | })), paths.slice(1)); 60 | }); 61 | })([], toNpmPaths(baseDir)); 62 | }); 63 | }; 64 | -------------------------------------------------------------------------------- /lib/challenge/index.js: -------------------------------------------------------------------------------- 1 | var Bluebird = require('bluebird'); 2 | var Exercise = require('./exercise'); 3 | var log = require('../utils/log/log'); 4 | var render = require('../utils/render/content'); 5 | var renderFile = require('../utils/render/file'); 6 | 7 | /** 8 | * Export a challenge constructor, used for setting up the code challenge. 9 | */ 10 | var Challenge = module.exports = function (name) { 11 | this.name = name; 12 | this.exercises = []; 13 | }; 14 | 15 | /** 16 | * Expose constructor functions. 17 | * 18 | * @type {Function} 19 | */ 20 | Challenge.prototype.Exercise = Exercise; 21 | Challenge.prototype.Challenge = Challenge; 22 | 23 | /** 24 | * Create a new exercise to complete in the challenge. 25 | * 26 | * @param {String} name 27 | * @return {Exercise} 28 | */ 29 | Challenge.prototype.exercise = function (name) { 30 | var exercise = new Exercise(name); 31 | 32 | // Alias the challenge instance and index found. 33 | exercise.challenge = this; 34 | exercise.index = this.exercises.length; 35 | 36 | // Push into the exercise into the array. 37 | this.exercises.push(exercise); 38 | 39 | return exercise; 40 | }; 41 | 42 | /** 43 | * Print content to stdout with an optional language. 44 | * 45 | * @param {String} content 46 | * @param {String} language 47 | * @return {Promise} 48 | */ 49 | Challenge.prototype.print = function (content, language) { 50 | return render(content, language).then(log); 51 | }; 52 | 53 | /** 54 | * Print a file to stdout with automatic language detection. 55 | * 56 | * @param {String} file 57 | * @return {Promise} 58 | */ 59 | Challenge.prototype.printFile = function (file) { 60 | return renderFile(file).then(log); 61 | }; 62 | 63 | /** 64 | * Execute a file and return the response as a promise. 65 | * 66 | * @type {Function} 67 | */ 68 | Challenge.prototype.execFile = Bluebird.promisify( 69 | require('language-detect-exec') 70 | ); 71 | 72 | /** 73 | * Spawn a file and return the child process object stream. 74 | * 75 | * @type {Function} 76 | */ 77 | Challenge.prototype.spawnFile = require('language-detect-spawn'); 78 | -------------------------------------------------------------------------------- /lib/utils/store/data.js: -------------------------------------------------------------------------------- 1 | var join = require('path').join; 2 | var crypto = require('crypto'); 3 | var Bluebird = require('bluebird'); 4 | var mkdirp = Bluebird.promisify(require('mkdirp')); 5 | var unlink = Bluebird.promisify(require('fs').unlink); 6 | var readdir = Bluebird.promisify(require('fs').readdir); 7 | var readFile = Bluebird.promisify(require('fs').readFile); 8 | var writeFile = Bluebird.promisify(require('fs').writeFile); 9 | 10 | /** 11 | * Set the configuration directory to save data. 12 | * 13 | * @type {String} 14 | */ 15 | var HOME_DIR = process.env.HOME || process.env.USERPROFILE; 16 | var CONFIG_DIR = join(HOME_DIR, '.config', 'code-challenge'); 17 | 18 | /** 19 | * Return the file name of a string. 20 | * 21 | * @param {String} name 22 | * @return {String} 23 | */ 24 | var filename = function (name) { 25 | return crypto.createHash('sha1').update(name).digest('hex') + '.json'; 26 | }; 27 | 28 | /** 29 | * Get a value persisted in the file system. 30 | * 31 | * @param {String} name 32 | * @return {Promise} 33 | */ 34 | exports.get = function (name) { 35 | return readFile(join(CONFIG_DIR, filename(name)), 'utf8') 36 | .then(JSON.parse) 37 | .catch(function () { 38 | return null; 39 | }); 40 | }; 41 | 42 | /** 43 | * Set a value to be persisted in the file system. 44 | * 45 | * @param {String} name 46 | * @param {Object} value 47 | * @param {Promise} 48 | */ 49 | exports.set = function (name, value) { 50 | return mkdirp(CONFIG_DIR) 51 | .then(function () { 52 | return writeFile(join(CONFIG_DIR, filename(name)), JSON.stringify(value)) 53 | .return(value); 54 | }); 55 | }; 56 | 57 | /** 58 | * Remove a value from persistence. 59 | * 60 | * @param {String} name 61 | * @return {Promise} 62 | */ 63 | exports.remove = function (name) { 64 | return unlink(join(CONFIG_DIR, filename(name))); 65 | }; 66 | 67 | /** 68 | * Reset everything. 69 | * 70 | * @return {[type]} [description] 71 | */ 72 | exports.reset = function () { 73 | return readdir(CONFIG_DIR) 74 | .then(function (files) { 75 | return Bluebird.all(files.map(function (file) { 76 | return unlink(join(CONFIG_DIR, file)); 77 | })); 78 | }); 79 | }; 80 | -------------------------------------------------------------------------------- /lib/cli/exercise.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk'); 2 | var check = require('../utils/log/format/check'); 3 | var current = require('../utils/store/current'); 4 | var complete = require('../utils/store/complete'); 5 | var list = require('../utils/cli/list'); 6 | var getCurrentChallenge = require('../utils/cli/current-challenge'); 7 | var printExercise = require('../utils/cli/print-exercise'); 8 | var findExercise = require('../utils/challenge/find-exercise'); 9 | 10 | /** 11 | * Select an exercise from the current challenge. 12 | * 13 | * @return {Promise} 14 | */ 15 | module.exports = function () { 16 | return getCurrentChallenge() 17 | .then(function (challenge) { 18 | if (!challenge) { 19 | return; 20 | } 21 | 22 | return complete.get(challenge.dirname) 23 | .then(function (completed) { 24 | // Map the exercises to their names, keeping the same order. 25 | var items = challenge.exercises.map(function (exercise) { 26 | var name = chalk.stripColor(exercise.name); 27 | 28 | // Render the name with a checkmark when completed. 29 | if (completed.indexOf(exercise.name) > -1) { 30 | name = check(name); 31 | } 32 | 33 | return [exercise.name, name]; 34 | }); 35 | 36 | // List the exercises with the current exercise selected. 37 | return current.get('exercise') 38 | .then(function (currentExercise) { 39 | return list(items, { 40 | selected: currentExercise, 41 | footer: 'Press ENTER to use an exercise' 42 | }); 43 | }) 44 | .then(function (select) { 45 | if (select.action !== 'select') { 46 | return; 47 | } 48 | 49 | // Find the exercise in the current challenge. 50 | var exercise = findExercise(challenge, select.item); 51 | 52 | // Print the exercise, then persist the selection. 53 | return printExercise(exercise) 54 | .then(function () { 55 | return current.set('exercise', exercise.name); 56 | }); 57 | }); 58 | }); 59 | }); 60 | }; 61 | -------------------------------------------------------------------------------- /test/spec/challenge/index.js: -------------------------------------------------------------------------------- 1 | var sinon = require('sinon'); 2 | var expect = require('chai').expect; 3 | var Challenge = require('../../../lib/challenge'); 4 | 5 | describe('challenge', function () { 6 | var challenge; 7 | 8 | beforeEach(function () { 9 | challenge = new Challenge(); 10 | }); 11 | 12 | describe('define an exercise', function () { 13 | var exercise; 14 | 15 | beforeEach(function () { 16 | exercise = challenge.exercise('test'); 17 | }); 18 | 19 | it('should create a exercise instance', function () { 20 | expect(exercise).to.be.an.instanceOf(challenge.Exercise); 21 | expect(exercise.name).to.equal('test'); 22 | expect(challenge.exercises).to.have.length(1); 23 | expect(challenge.exercises[0]).to.equal(exercise); 24 | }); 25 | 26 | it('should run a task', function () { 27 | var spy = sinon.spy(function (done) { 28 | return process.nextTick(done); 29 | }); 30 | 31 | exercise.add('test', spy); 32 | 33 | return exercise 34 | .start('test') 35 | .then(function () { 36 | expect(spy).to.have.been.calledOnce; 37 | }); 38 | }); 39 | 40 | it('should reject the promise when an error occurs', function () { 41 | var spy = sinon.spy(function () { 42 | throw new Error('test'); 43 | }); 44 | 45 | exercise.add('test', spy); 46 | 47 | return expect(exercise.start('test')).to.be.rejectedWith(Error, 'test'); 48 | }); 49 | 50 | describe('task arguments', function () { 51 | it('should pass arguments as the context', function () { 52 | var spy = sinon.spy(function (done) { 53 | expect(this._).to.deep.equal([1, 2, 3]); 54 | 55 | return process.nextTick(done); 56 | }); 57 | 58 | exercise.add('dep', spy); 59 | exercise.add('test', ['dep'], spy); 60 | 61 | return exercise 62 | .start('test', [1, 2, 3]) 63 | .then(function () { 64 | expect(exercise._).to.not.exist; 65 | expect(spy).to.have.been.calledTwice; 66 | }); 67 | }); 68 | 69 | it('should set arguments to an empty array when not passed', function () { 70 | var spy = sinon.spy(function (done) { 71 | expect(this._).to.deep.equal([]); 72 | 73 | return process.nextTick(done); 74 | }); 75 | 76 | exercise.add('test', spy); 77 | 78 | return exercise 79 | .start('test') 80 | .then(function () { 81 | expect(exercise._).to.not.exist; 82 | expect(spy).to.have.been.calledOnce; 83 | }); 84 | }); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /lib/cli/select.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk'); 2 | var Bluebird = require('bluebird'); 3 | var list = require('../utils/cli/list'); 4 | var current = require('../utils/store/current'); 5 | var check = require('../utils/log/format/check'); 6 | var logNoChallenges = require('../utils/log/challenge/none'); 7 | var printChallenge = require('../utils/cli/print-challenge'); 8 | var requireChallenge = require('../utils/challenge/require'); 9 | var resolveChallenges = require('../utils/challenge/resolve'); 10 | var challengeCompleted = require('../utils/challenge/complete'); 11 | 12 | /** 13 | * Select a challenge from the CLI and persist selection. 14 | * 15 | * @return {Promise} 16 | */ 17 | module.exports = function () { 18 | return resolveChallenges(process.cwd()) 19 | .then(function (dirs) { 20 | if (!dirs.length) { 21 | return logNoChallenges(); 22 | } 23 | 24 | // Then resolve each local modules challenge. 25 | return Bluebird.all(dirs.map(requireChallenge)) 26 | .then(function (challenges) { 27 | return Bluebird.all(challenges.map(challengeCompleted)) 28 | .then(function (completed) { 29 | // Iterate over each of module and create the list items. 30 | var items = challenges.map(function (challenge, index) { 31 | var dir = dirs[index]; 32 | var name = chalk.stripColor(challenge.name); 33 | 34 | // If the challenge has been fully completed, check it. 35 | if (completed[index]) { 36 | name = check(name); 37 | } 38 | 39 | return [dir, name]; 40 | }).sort(function (a, b) { 41 | return a[1] > b[1]; 42 | }); 43 | 44 | // Render the list with the current challenge selected. 45 | return current.get('challenge') 46 | .then(function (currentChallenge) { 47 | return list(items, { 48 | selected: currentChallenge, 49 | footer: 'Press ENTER to select the challenge' 50 | }); 51 | }); 52 | }) 53 | .then(function (select) { 54 | if (select.action !== 'select') { 55 | return; 56 | } 57 | 58 | // Print the current challenge to the command line. 59 | printChallenge(challenges[dirs.indexOf(select.item)]) 60 | .then(function () { 61 | return current.set('challenge', select.item); 62 | }); 63 | }); 64 | }); 65 | }); 66 | }; 67 | -------------------------------------------------------------------------------- /lib/utils/log/exercise/fail.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk'); 2 | var stringify = require('javascript-stringify'); 3 | var createPatch = require('diff').createPatch; 4 | var log = require('../log'); 5 | var cross = require('../format/cross'); 6 | var _toString = Object.prototype.toString; 7 | 8 | /** 9 | * Create a diffable string of the value. 10 | * 11 | * @param {*} value 12 | * @return {String} 13 | */ 14 | var diffStringify = function (value) { 15 | return stringify(value, function (value, indent, stringify) { 16 | // Sort object properties consistently by name. 17 | if (typeof value === 'object' && value !== null) { 18 | var canonicalObj = {}; 19 | 20 | Object.keys(value).sort().forEach(function (key) { 21 | canonicalObj[key] = value[key]; 22 | }); 23 | 24 | return stringify(canonicalObj); 25 | } 26 | 27 | return stringify(value); 28 | }, 2); 29 | }; 30 | 31 | /** 32 | * Print the diff between two strings. 33 | * 34 | * @param {String} actual 35 | * @param {String} expected 36 | */ 37 | var printDiff = function (actual, expected) { 38 | var indent = ' '; 39 | var length = Math.max(actual.length, expected.length); 40 | var diff = []; 41 | 42 | // Print basic instructions on reading the diff. 43 | log(' ' + chalk.bgGreen('+ expected') + ' ' + chalk.bgRed('- actual')); 44 | log(); 45 | 46 | // Create the diff patch to render. 47 | var diff = createPatch('string', actual, expected) 48 | .split('\n') 49 | .splice(4) 50 | .map(function (line) { 51 | if (line[0] === '+') { 52 | return indent + chalk.bgGreen(line); 53 | } 54 | 55 | if (line[0] === '-') { 56 | return indent + chalk.bgRed(line); 57 | 58 | } 59 | 60 | if (/\@\@/.test(line) || /\\ No newline/.test(line)) { 61 | return null; 62 | } 63 | 64 | return indent + line; 65 | }) 66 | .filter(function (line) { 67 | return line != null; 68 | }) 69 | .join('\n'); 70 | 71 | log(diff); 72 | }; 73 | 74 | /** 75 | * Print the diff comparison of an error. 76 | * 77 | * @param {Error} err 78 | */ 79 | var printError = function (err) { 80 | // Log the error name with a description and optional diff. 81 | log(cross(err.name) + ': ' + chalk.red(err.message)); 82 | 83 | // Log diffs when available. 84 | if (err.showDiff) { 85 | var actual = err.actual; 86 | var expected = err.expected; 87 | 88 | if (_toString.call(actual) === _toString.call(expected)) { 89 | actual = diffStringify(actual); 90 | expected = diffStringify(expected); 91 | } 92 | 93 | // Print the diff if both types are strings. 94 | if (typeof actual === 'string' && typeof expected === 'string') { 95 | log(); 96 | printDiff(actual, expected); 97 | } 98 | } 99 | }; 100 | 101 | /** 102 | * Log an error message to the user about the failed exercise. 103 | * 104 | * @param {Object} exercise 105 | * @param {Error} err 106 | */ 107 | module.exports = function (exercise, err) { 108 | log(); 109 | printError(err); 110 | log(); 111 | log(chalk.bold.red('# FAIL')); 112 | log(); 113 | log( 114 | 'Your solution to "' + chalk.bold(exercise.name) + '" didn\'t work.', 115 | 'Give it another go!' 116 | ); 117 | log(); 118 | }; 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Code Challenge 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![Build status][travis-image]][travis-url] 5 | [![Test coverage][coveralls-image]][coveralls-url] 6 | [![Greenkeeper badge](https://badges.greenkeeper.io/blakeembrey/code-challenge.svg)](https://greenkeeper.io/) 7 | 8 | Install, run and share command line challenges. Perfect for learning, practicing or teaching others how to code. 9 | 10 | ## Installation 11 | 12 | You will need to install [node.js](http://nodejs.org/). Once done, you can use `npm` to install the module globally and use the command line interface. 13 | 14 | ```sh 15 | npm install -g code-challenge 16 | ``` 17 | 18 | ## Usage 19 | 20 | ```bash 21 | challenge select # Select a challenge to use 22 | challenge exercise # Select an exercise to run 23 | 24 | challenge print # Print the currently selected exercise 25 | challenge run [args] # Run your solution in a test environment 26 | challenge verify [args] # Check your solution is correct 27 | 28 | challenge help # Help menu 29 | challenge reset # Reset everything 30 | 31 | challenge next # Move to the next exercise 32 | challenge prev # Move to the previous exercise 33 | ``` 34 | 35 | To get started, try installing your first challenge. I'd recommend [Project Euler](https://projecteuler.net/) using `npm install -g code-challenge-euler`. 36 | 37 | ## Create Your Own Challenge 38 | 39 | To create a custom challenge pack to share, you'll need to create a new npm module and add `code-challenge` as a dependency. The module name must be prefixed with `code-challenge-` to automatically be loaded. The modules main file should require `code-challenge`, set the challenge title and add exercises. 40 | 41 | ```javascript 42 | var assert = require('assert'); 43 | var challenge = require('code-challenge'); 44 | 45 | challenge.title = 'Example Challenge'; 46 | 47 | challenge.exercise('First Exercise') 48 | .add('print', function () { 49 | return challenge.renderFile(__dirname + '/exercises/01.md'); 50 | }) 51 | .add('verify', function () { 52 | return challenge.execFile(this._[0]) 53 | .spread(function (stdout) { 54 | assert.equal(stdout, 'Example output\n'); 55 | }); 56 | }); 57 | 58 | ... 59 | 60 | challenge.exercise('Tenth Exercise') 61 | .add('print', ...) 62 | .add('verify', ...); 63 | ``` 64 | 65 | ### Rendering Any File 66 | 67 | Challenge comes with a `renderFile` method that supports rending content from a file with standard syntax highlighting and colours. 68 | 69 | ### Executing Any File 70 | 71 | Challenge support executing most programming languages based on the file name automatically. By using the `execFile` or `spawnFile` methods, your challenge will be future proof and support all possible languages. 72 | 73 | ### Failing an Exercise 74 | 75 | When running the verification task, you can throw an error, reject a promise, error in a stream or pass an error as the first parameter of the callback to provide feedback. 76 | 77 | ## Inspiration 78 | 79 | Code challenge was originally inspired by all the work put into [learnyounode](https://github.com/rvagg/learnyounode), [stream-adventure](https://github.com/substack/stream-adventure) and [gulp](https://github.com/gulpjs/gulp). 80 | 81 | ## License 82 | 83 | MIT 84 | 85 | [npm-image]: https://img.shields.io/npm/v/code-challenge.svg?style=flat 86 | [npm-url]: https://npmjs.org/package/code-challenge 87 | [travis-image]: https://img.shields.io/travis/blakeembrey/code-challenge.svg?style=flat 88 | [travis-url]: https://travis-ci.org/blakeembrey/code-challenge 89 | [coveralls-image]: https://img.shields.io/coveralls/blakeembrey/code-challenge.svg?style=flat 90 | [coveralls-url]: https://coveralls.io/r/blakeembrey/code-challenge?branch=master 91 | -------------------------------------------------------------------------------- /test/fixtures/active-model.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/array/extract_options' 2 | require 'active_support/core_ext/class/attribute' 3 | require 'active_support/core_ext/hash/keys' 4 | require 'active_support/core_ext/hash/except' 5 | require 'active_model/errors' 6 | require 'active_model/validations/callbacks' 7 | 8 | module ActiveModel 9 | 10 | # == Active Model Validations 11 | # 12 | # Provides a full validation framework to your objects. 13 | # 14 | # A minimal implementation could be: 15 | # 16 | # class Person 17 | # include ActiveModel::Validations 18 | # 19 | # attr_accessor :first_name, :last_name 20 | # 21 | # validates_each :first_name, :last_name do |record, attr, value| 22 | # record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z 23 | # end 24 | # end 25 | # 26 | # Which provides you with the full standard validation stack that you 27 | # know from Active Record: 28 | # 29 | # person = Person.new 30 | # person.valid? # => true 31 | # person.invalid? # => false 32 | # 33 | # person.first_name = 'zoolander' 34 | # person.valid? # => false 35 | # person.invalid? # => true 36 | # person.errors # => #["starts with z."]}> 37 | # 38 | # Note that ActiveModel::Validations automatically adds an +errors+ method 39 | # to your instances initialized with a new ActiveModel::Errors object, so 40 | # there is no need for you to do this manually. 41 | # 42 | module Validations 43 | extend ActiveSupport::Concern 44 | 45 | included do 46 | extend ActiveModel::Callbacks 47 | extend ActiveModel::Translation 48 | 49 | extend HelperMethods 50 | include HelperMethods 51 | 52 | attr_accessor :validation_context 53 | define_callbacks :validate, :scope => :name 54 | 55 | extend ActiveModel::Configuration 56 | config_attribute :_validators 57 | self._validators = Hash.new { |h,k| h[k] = [] } 58 | end 59 | 60 | module ClassMethods 61 | # Validates each attribute against a block. 62 | # 63 | # class Person 64 | # include ActiveModel::Validations 65 | # 66 | # attr_accessor :first_name, :last_name 67 | # 68 | # validates_each :first_name, :last_name do |record, attr, value| 69 | # record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z 70 | # end 71 | # end 72 | # 73 | # Options: 74 | # * :on - Specifies the context where this validation is active 75 | # (e.g. :on => :create or :on => :custom_validation_context) 76 | # * :allow_nil - Skip validation if attribute is +nil+. 77 | # * :allow_blank - Skip validation if attribute is blank. 78 | # * :if - Specifies a method, proc or string to call to determine 79 | # if the validation should occur (e.g. :if => :allow_validation, 80 | # or :if => Proc.new { |user| user.signup_step > 2 }). The method, 81 | # proc or string should return or evaluate to a true or false value. 82 | # * :unless - Specifies a method, proc or string to call to determine if the validation should 83 | # not occur (e.g. :unless => :skip_validation, or 84 | # :unless => Proc.new { |user| user.signup_step <= 2 }). The 85 | # method, proc or string should return or evaluate to a true or false value. 86 | def validates_each(*attr_names, &block) 87 | options = attr_names.extract_options!.symbolize_keys 88 | validates_with BlockValidator, options.merge(:attributes => attr_names.flatten), &block 89 | end 90 | 91 | # Adds a validation method or block to the class. This is useful when 92 | # overriding the +validate+ instance method becomes too unwieldy and 93 | # you're looking for more descriptive declaration of your validations. 94 | # 95 | # This can be done with a symbol pointing to a method: 96 | # 97 | # class Comment 98 | # include ActiveModel::Validations 99 | # 100 | # validate :must_be_friends 101 | # 102 | # def must_be_friends 103 | # errors.add(:base, "Must be friends to leave a comment") unless commenter.friend_of?(commentee) 104 | # end 105 | # end 106 | # 107 | # With a block which is passed with the current record to be validated: 108 | # 109 | # class Comment 110 | # include ActiveModel::Validations 111 | # 112 | # validate do |comment| 113 | # comment.must_be_friends 114 | # end 115 | # 116 | # def must_be_friends 117 | # errors.add(:base, "Must be friends to leave a comment") unless commenter.friend_of?(commentee) 118 | # end 119 | # end 120 | # 121 | # Or with a block where self points to the current record to be validated: 122 | # 123 | # class Comment 124 | # include ActiveModel::Validations 125 | # 126 | # validate do 127 | # errors.add(:base, "Must be friends to leave a comment") unless commenter.friend_of?(commentee) 128 | # end 129 | # end 130 | # 131 | def validate(*args, &block) 132 | options = args.extract_options! 133 | if options.key?(:on) 134 | options = options.dup 135 | options[:if] = Array(options[:if]) 136 | options[:if].unshift("validation_context == :#{options[:on]}") 137 | end 138 | args << options 139 | set_callback(:validate, *args, &block) 140 | end 141 | 142 | # List all validators that are being used to validate the model using 143 | # +validates_with+ method. 144 | def validators 145 | _validators.values.flatten.uniq 146 | end 147 | 148 | # List all validators that being used to validate a specific attribute. 149 | def validators_on(*attributes) 150 | attributes.map do |attribute| 151 | _validators[attribute.to_sym] 152 | end.flatten 153 | end 154 | 155 | # Check if method is an attribute method or not. 156 | def attribute_method?(attribute) 157 | method_defined?(attribute) 158 | end 159 | 160 | # Copy validators on inheritance. 161 | def inherited(base) 162 | dup = _validators.dup 163 | base._validators = dup.each { |k, v| dup[k] = v.dup } 164 | super 165 | end 166 | end 167 | 168 | # Returns the +Errors+ object that holds all information about attribute error messages. 169 | def errors 170 | @errors ||= Errors.new(self) 171 | end 172 | 173 | # Runs all the specified validations and returns true if no errors were added 174 | # otherwise false. Context can optionally be supplied to define which callbacks 175 | # to test against (the context is defined on the validations using :on). 176 | def valid?(context = nil) 177 | current_context, self.validation_context = validation_context, context 178 | errors.clear 179 | run_validations! 180 | ensure 181 | self.validation_context = current_context 182 | end 183 | 184 | # Performs the opposite of valid?. Returns true if errors were added, 185 | # false otherwise. 186 | def invalid?(context = nil) 187 | !valid?(context) 188 | end 189 | 190 | # Hook method defining how an attribute value should be retrieved. By default 191 | # this is assumed to be an instance named after the attribute. Override this 192 | # method in subclasses should you need to retrieve the value for a given 193 | # attribute differently: 194 | # 195 | # class MyClass 196 | # include ActiveModel::Validations 197 | # 198 | # def initialize(data = {}) 199 | # @data = data 200 | # end 201 | # 202 | # def read_attribute_for_validation(key) 203 | # @data[key] 204 | # end 205 | # end 206 | # 207 | alias :read_attribute_for_validation :send 208 | 209 | protected 210 | 211 | def run_validations! 212 | run_callbacks :validate 213 | errors.empty? 214 | end 215 | end 216 | end 217 | 218 | Dir[File.dirname(__FILE__) + "/validations/*.rb"].sort.each do |path| 219 | filename = File.basename(path) 220 | require "active_model/validations/#{filename}" 221 | end 222 | -------------------------------------------------------------------------------- /test/fixtures/active-model.out: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/array/extract_options' 2 | require 'active_support/core_ext/class/attribute' 3 | require 'active_support/core_ext/hash/keys' 4 | require 'active_support/core_ext/hash/except' 5 | require 'active_model/errors' 6 | require 'active_model/validations/callbacks' 7 | 8 | module ActiveModel 9 | 10 | # == Active Model Validations 11 | # 12 | # Provides a full validation framework to your objects. 13 | # 14 | # A minimal implementation could be: 15 | # 16 | # class Person 17 | # include ActiveModel::Validations 18 | # 19 | # attr_accessor :first_name, :last_name 20 | # 21 | # validates_each :first_name, :last_name do |record, attr, value| 22 | # record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z 23 | # end 24 | # end 25 | # 26 | # Which provides you with the full standard validation stack that you 27 | # know from Active Record: 28 | # 29 | # person = Person.new 30 | # person.valid? # => true 31 | # person.invalid? # => false 32 | # 33 | # person.first_name = 'zoolander' 34 | # person.valid? # => false 35 | # person.invalid? # => true 36 | # person.errors # => #["starts with z."]}> 37 | # 38 | # Note that ActiveModel::Validations automatically adds an +errors+ method 39 | # to your instances initialized with a new ActiveModel::Errors object, so 40 | # there is no need for you to do this manually. 41 | # 42 | module Validations 43 | extend ActiveSupport::Concern 44 | 45 | included do 46 | extend ActiveModel::Callbacks 47 | extend ActiveModel::Translation 48 | 49 | extend HelperMethods 50 | include HelperMethods 51 | 52 | attr_accessor :validation_context 53 | define_callbacks :validate, :scope => :name 54 | 55 | extend ActiveModel::Configuration 56 | config_attribute :_validators 57 | self._validators = Hash.new { |h,k| h[k] = [] } 58 | end 59 | 60 | module ClassMethods 61 | # Validates each attribute against a block. 62 | # 63 | # class Person 64 | # include ActiveModel::Validations 65 | # 66 | # attr_accessor :first_name, :last_name 67 | # 68 | # validates_each :first_name, :last_name do |record, attr, value| 69 | # record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z 70 | # end 71 | # end 72 | # 73 | # Options: 74 | # * :on - Specifies the context where this validation is active 75 | # (e.g. :on => :create or :on => :custom_validation_context) 76 | # * :allow_nil - Skip validation if attribute is +nil+. 77 | # * :allow_blank - Skip validation if attribute is blank. 78 | # * :if - Specifies a method, proc or string to call to determine 79 | # if the validation should occur (e.g. :if => :allow_validation, 80 | # or :if => Proc.new { |user| user.signup_step > 2 }). The method, 81 | # proc or string should return or evaluate to a true or false value. 82 | # * :unless - Specifies a method, proc or string to call to determine if the validation should 83 | # not occur (e.g. :unless => :skip_validation, or 84 | # :unless => Proc.new { |user| user.signup_step <= 2 }). The 85 | # method, proc or string should return or evaluate to a true or false value. 86 | def validates_each(*attr_names, &block) 87 | options = attr_names.extract_options!.symbolize_keys 88 | validates_with BlockValidator, options.merge(:attributes => attr_names.flatten), &block 89 | end 90 | 91 | # Adds a validation method or block to the class. This is useful when 92 | # overriding the +validate+ instance method becomes too unwieldy and 93 | # you're looking for more descriptive declaration of your validations. 94 | # 95 | # This can be done with a symbol pointing to a method: 96 | # 97 | # class Comment 98 | # include ActiveModel::Validations 99 | # 100 | # validate :must_be_friends 101 | # 102 | # def must_be_friends 103 | # errors.add(:base, "Must be friends to leave a comment") unless commenter.friend_of?(commentee) 104 | # end 105 | # end 106 | # 107 | # With a block which is passed with the current record to be validated: 108 | # 109 | # class Comment 110 | # include ActiveModel::Validations 111 | # 112 | # validate do |comment| 113 | # comment.must_be_friends 114 | # end 115 | # 116 | # def must_be_friends 117 | # errors.add(:base, "Must be friends to leave a comment") unless commenter.friend_of?(commentee) 118 | # end 119 | # end 120 | # 121 | # Or with a block where self points to the current record to be validated: 122 | # 123 | # class Comment 124 | # include ActiveModel::Validations 125 | # 126 | # validate do 127 | # errors.add(:base, "Must be friends to leave a comment") unless commenter.friend_of?(commentee) 128 | # end 129 | # end 130 | # 131 | def validate(*args, &block) 132 | options = args.extract_options! 133 | if options.key?(:on) 134 | options = options.dup 135 | options[:if] = Array(options[:if]) 136 | options[:if].unshift("validation_context == :#{options[:on]}") 137 | end 138 | args << options 139 | set_callback(:validate, *args, &block) 140 | end 141 | 142 | # List all validators that are being used to validate the model using 143 | # +validates_with+ method. 144 | def validators 145 | _validators.values.flatten.uniq 146 | end 147 | 148 | # List all validators that being used to validate a specific attribute. 149 | def validators_on(*attributes) 150 | attributes.map do |attribute| 151 | _validators[attribute.to_sym] 152 | end.flatten 153 | end 154 | 155 | # Check if method is an attribute method or not. 156 | def attribute_method?(attribute) 157 | method_defined?(attribute) 158 | end 159 | 160 | # Copy validators on inheritance. 161 | def inherited(base) 162 | dup = _validators.dup 163 | base._validators = dup.each { |k, v| dup[k] = v.dup } 164 | super 165 | end 166 | end 167 | 168 | # Returns the +Errors+ object that holds all information about attribute error messages. 169 | def errors 170 | @errors ||= Errors.new(self) 171 | end 172 | 173 | # Runs all the specified validations and returns true if no errors were added 174 | # otherwise false. Context can optionally be supplied to define which callbacks 175 | # to test against (the context is defined on the validations using :on). 176 | def valid?(context = nil) 177 | current_context, self.validation_context = validation_context, context 178 | errors.clear 179 | run_validations! 180 | ensure 181 | self.validation_context = current_context 182 | end 183 | 184 | # Performs the opposite of valid?. Returns true if errors were added, 185 | # false otherwise. 186 | def invalid?(context = nil) 187 | !valid?(context) 188 | end 189 | 190 | # Hook method defining how an attribute value should be retrieved. By default 191 | # this is assumed to be an instance named after the attribute. Override this 192 | # method in subclasses should you need to retrieve the value for a given 193 | # attribute differently: 194 | # 195 | # class MyClass 196 | # include ActiveModel::Validations 197 | # 198 | # def initialize(data = {}) 199 | # @data = data 200 | # end 201 | # 202 | # def read_attribute_for_validation(key) 203 | # @data[key] 204 | # end 205 | # end 206 | # 207 | alias :read_attribute_for_validation :send 208 | 209 | protected 210 | 211 | def run_validations! 212 | run_callbacks :validate 213 | errors.empty? 214 | end 215 | end 216 | end 217 | 218 | Dir[File.dirname(__FILE__) + "/validations/*.rb"].sort.each do |path| 219 | filename = File.basename(path) 220 | require "active_model/validations/#{filename}" 221 | end 222 | --------------------------------------------------------------------------------