├── i18n └── en.json ├── start.png ├── .travis.yml ├── .eslintrc ├── problems ├── transport │ ├── solution │ │ ├── solution.js │ │ └── math.js │ ├── problem.md │ └── exercise.js ├── sum │ ├── solution │ │ └── solution.js │ ├── problem.md │ └── exercise.js ├── plugin │ ├── solution │ │ └── solution.js │ ├── seneca-plugin-executor.js │ ├── exercise.js │ └── problem.md ├── menu.json ├── client │ ├── solution │ │ ├── math.js │ │ └── solution.js │ ├── exercise.js │ └── problem.md ├── transport_client │ ├── solution │ │ ├── solution.js │ │ └── plugin │ │ │ └── math.js │ ├── exercise.js │ └── problem.md ├── roles │ ├── solution │ │ └── solution.js │ ├── problem.md │ └── exercise.js ├── seneca-client-executor.js ├── utils.js ├── override │ ├── seneca-override-executor-run.js │ ├── seneca-override-executor.js │ ├── solution │ │ └── solution.js │ ├── exercise.js │ └── problem.md ├── prior_actions │ ├── seneca-prior-actions-executor.js │ ├── solution │ │ └── solution.js │ ├── problem.md │ └── exercise.js ├── extend │ ├── solution │ │ └── solution.js │ ├── problem.md │ └── exercise.js ├── extend_client │ ├── solution │ │ ├── math.js │ │ └── solution.js │ ├── problem.md │ └── exercise.js ├── pin │ ├── seneca-pin-executor-run.js │ ├── seneca-pin-executor.js │ ├── solution │ │ └── solution.js │ ├── exercise.js │ └── problem.md ├── decorate │ ├── seneca-decorate-executor.js │ ├── exercise.js │ ├── problem.md │ └── solution │ │ └── solution.js ├── mem_store │ ├── seneca-memstore-executor.js │ ├── exercise.js │ ├── solution │ │ └── solution.js │ └── problem.md └── comparestdout-filterlogs.js ├── .gitignore ├── credits.txt ├── credits.js ├── runners.js ├── LICENSE ├── help.txt ├── package.json └── README.md /i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "menu": { 3 | "credits": "CREDITS" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/senecajs/seneca-in-practice/HEAD/start.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | script: 5 | - npm run lint 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "rules": { 4 | "no-labels": 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /problems/transport/solution/solution.js: -------------------------------------------------------------------------------- 1 | require('seneca')().use('./math').listen({ 2 | port: process.argv[2], 3 | host: '127.0.0.1' 4 | }) 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | test/db 3 | *.old 4 | *.bak 5 | *.tmp 6 | *.log 7 | node_modules 8 | README.html 9 | *.off 10 | *-off 11 | npm-debug.log 12 | -------------------------------------------------------------------------------- /problems/sum/solution/solution.js: -------------------------------------------------------------------------------- 1 | const seneca = require('seneca')() 2 | 3 | seneca.add({role: 'math', cmd: 'sum'}, (msg, respond) => { 4 | const sum = msg.left + msg.right 5 | respond(null, {answer: sum}) 6 | }) 7 | 8 | module.exports = seneca 9 | -------------------------------------------------------------------------------- /problems/plugin/solution/solution.js: -------------------------------------------------------------------------------- 1 | module.exports = function math (options) { 2 | this.add({role:'math', cmd:'sum'}, (msg, respond) => { 3 | var sum = msg.left + msg.right 4 | respond(null, {answer: sum}) 5 | }) 6 | return 'operations' 7 | } 8 | -------------------------------------------------------------------------------- /problems/menu.json: -------------------------------------------------------------------------------- 1 | [ 2 | "Sum", 3 | "Client", 4 | "Plugin", 5 | "Roles", 6 | "Extend", 7 | "Extend Client", 8 | "Override", 9 | "Pin", 10 | "Transport", 11 | "Transport Client", 12 | "Mem Store", 13 | "Decorate" 14 | ] 15 | -------------------------------------------------------------------------------- /problems/client/solution/math.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const seneca = require('seneca')() 4 | 5 | seneca.add({ role: 'math', cmd: 'sum' }, (msg, respond) => { 6 | const sum = msg.left + msg.right 7 | respond(null, {answer: sum}) 8 | }) 9 | 10 | module.exports = seneca 11 | -------------------------------------------------------------------------------- /credits.txt: -------------------------------------------------------------------------------- 1 | 2 | {yellow}{bold}seneca-in-practice is brought to you by the following dedicated hackers:{/bold}{/yellow} 3 | 4 | {bold}Name GitHub Username{/bold} 5 | ----------------------------------- 6 | Marco Piraccini @marcopiraccini 7 | Michele Capra @piccoloaiutante 8 | -------------------------------------------------------------------------------- /problems/client/solution/solution.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const seneca = require('./math') 4 | const left = parseInt(process.argv[2], 10) 5 | const right = parseInt(process.argv[3], 10) 6 | 7 | seneca.act({role: 'math', cmd: 'sum', left, right}, (err, result) => { 8 | if (err) return console.error(err) 9 | console.log(result) 10 | }) 11 | -------------------------------------------------------------------------------- /problems/transport_client/solution/solution.js: -------------------------------------------------------------------------------- 1 | var seneca = require('seneca')() 2 | seneca 3 | .client({type: 'tcp', host: '127.0.0.1'}) 4 | .act({role: 'math', cmd: 'sum', left: process.argv[2], right: process.argv[3]}, function (err, result) { 5 | if (err) return console.error(err) 6 | console.log(result) 7 | this.close() 8 | }) 9 | -------------------------------------------------------------------------------- /credits.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var path = require('path') 3 | var colorsTmpl = require('colors-tmpl') 4 | 5 | function credits () { 6 | fs.readFile(path.join(__dirname, './credits.txt'), 'utf8', function (err, data) { 7 | if (err) { 8 | throw err 9 | } 10 | console.log(colorsTmpl(data)) 11 | }) 12 | } 13 | 14 | module.exports = credits 15 | -------------------------------------------------------------------------------- /problems/roles/solution/solution.js: -------------------------------------------------------------------------------- 1 | module.exports = function math (options) { 2 | this.add({role: 'math', cmd: 'sum'}, (msg, respond) => { 3 | var sum = msg.left + msg.right 4 | respond(null, {answer: sum}) 5 | }) 6 | 7 | this.add({role: 'math', cmd: 'product'}, (msg, respond) => { 8 | var product = msg.left * msg.right 9 | respond(null, {answer: product}) 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /problems/seneca-client-executor.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Temporary, see: https://github.com/senecajs/seneca/issues/566 4 | process.removeAllListeners('warning') 5 | 6 | /** 7 | * Executes the submitted and the solution, to compare the stdout. 8 | * The module to be require as the first param. 9 | */ 10 | var client = process.argv[2] 11 | process.argv = ['', '', process.argv[3], process.argv[4]] 12 | require(client) 13 | -------------------------------------------------------------------------------- /problems/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | exports.getRandomInt = (min = 0, max = 100) => { 4 | min = Math.ceil(min) 5 | max = Math.floor(max) 6 | return Math.floor(Math.random() * (max - min)) + min 7 | } 8 | 9 | exports.getRandomFloat = (min = 0, max = 100) => { 10 | min = Math.ceil(min) 11 | max = Math.floor(max * 10) 12 | const num = Math.floor(Math.random() * (max - min)) + min 13 | return Math.round(num * 10) / 100 14 | } 15 | -------------------------------------------------------------------------------- /problems/override/seneca-override-executor-run.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Executes the solution 5 | */ 6 | 7 | // Temporary, see: https://github.com/senecajs/seneca/issues/566 8 | process.removeAllListeners('warning') 9 | 10 | var seneca = require('seneca')() 11 | seneca.use(process.argv[2]) 12 | 13 | seneca.act({role: 'math', cmd: 'sum', left: parseInt(process.argv[3]), right: parseInt(process.argv[4])}, function (err, result) { 14 | if (err) return console.log(err) 15 | console.log(result) 16 | }) 17 | -------------------------------------------------------------------------------- /problems/prior_actions/seneca-prior-actions-executor.js: -------------------------------------------------------------------------------- 1 | // Temporary, see: https://github.com/senecajs/seneca/issues/566 2 | process.removeAllListeners('warning') 3 | 4 | var seneca = require('seneca')() 5 | seneca.use(process.argv[2]) 6 | 7 | seneca.act({role: 'math', cmd: 'sum', left: 4, right: 2}, function (err, result) { 8 | if (err) return console.error(err) 9 | console.log(result) 10 | }) 11 | seneca.act({role: 'math', cmd: 'sum', integer: true, left: 14.8, right: 19.5}, function (err, result) { 12 | if (err) return console.error(err) 13 | console.log(result) 14 | }) 15 | -------------------------------------------------------------------------------- /problems/extend/solution/solution.js: -------------------------------------------------------------------------------- 1 | module.exports = function math (options) { 2 | this.add({role: 'math', cmd: 'sum'}, (msg, respond) => { 3 | var sum = msg.left + msg.right 4 | respond(null, {answer: sum}) 5 | }) 6 | 7 | this.add({role: 'math', cmd: 'product'}, (msg, respond) => { 8 | var product = msg.left * msg.right 9 | respond(null, {answer: product}) 10 | }) 11 | 12 | this.add({role: 'math', cmd: 'sum', integer: 'true'}, (msg, respond) => { 13 | var sum = Math.floor(msg.left) + Math.floor(msg.right) 14 | respond(null, {answer: sum}) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /problems/extend_client/solution/math.js: -------------------------------------------------------------------------------- 1 | module.exports = function math (options) { 2 | this.add({role: 'math', cmd: 'sum'}, (msg, respond) => { 3 | var sum = msg.left + msg.right 4 | respond(null, {answer: sum}) 5 | }) 6 | 7 | this.add({role: 'math', cmd: 'product'}, (msg, respond) => { 8 | var product = msg.left * msg.right 9 | respond(null, {answer: product}) 10 | }) 11 | 12 | this.add({role: 'math', cmd: 'sum', integer: 'true'}, (msg, respond) => { 13 | var sum = Math.floor(msg.left) + Math.floor(msg.right) 14 | respond(null, {answer: sum}) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /problems/pin/seneca-pin-executor-run.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Executes the submitted and the solution, to compare the stdout. 5 | * The module to be require as the first param. 6 | */ 7 | var mod = require(process.argv[2]) 8 | var seneca = require('seneca')() 9 | 10 | // Temporary, see: https://github.com/senecajs/seneca/issues/566 11 | process.removeAllListeners('warning') 12 | 13 | seneca.use(mod).act({role: 'math', cmd: process.argv[3], left: process.argv[4], right: process.argv[5]}, function (err, result) { 14 | if (err) return console.error(err) 15 | console.log(result) 16 | }) 17 | -------------------------------------------------------------------------------- /problems/prior_actions/solution/solution.js: -------------------------------------------------------------------------------- 1 | module.exports = function math (options) { 2 | this.add({role:'math', cmd:'sum'}, function (msg, respond) { 3 | var sum = msg.left + msg.right 4 | respond(null, {answer: sum}) 5 | }) 6 | 7 | this.add({role:'math', cmd:'sum'}, function (msg, respond) { 8 | this.prior(msg, function (err, out) { 9 | if (err) return respond(err) 10 | 11 | if (parseInt(msg.left || msg.right, 10)) { 12 | var sum = Math.floor(msg.left) + Math.floor(msg.right) 13 | } 14 | 15 | respond(null, {answer: sum}) 16 | }) 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /problems/extend_client/solution/solution.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var seneca = require('seneca') 4 | 5 | const left = Number(process.argv[2]) 6 | const right = Number(process.argv[3]) 7 | 8 | seneca.use('./math') 9 | .act({role: 'math', cmd: 'sum', left, right}, function (err, result) { 10 | if (err) return console.error(err) 11 | console.log(result) 12 | }).act({ 13 | role: 'math', 14 | cmd: 'sum', 15 | integer: true, 16 | left: process.argv[2], 17 | right: process.argv[3] 18 | }, (err, result) => { 19 | if (err) return console.error(err) 20 | console.log(result) 21 | } 22 | ) 23 | -------------------------------------------------------------------------------- /problems/decorate/seneca-decorate-executor.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Executes the submitted and the solution, to compare the stdout. 5 | * The module to be require as the first param. 6 | */ 7 | 8 | // Temporary, see: https://github.com/senecajs/seneca/issues/566 9 | process.removeAllListeners('warning') 10 | 11 | var seneca = require('seneca')() 12 | const mode = process.argv[2] 13 | seneca.use('basic').use('entity').use(process.argv[2]).ready(() => { 14 | const res = seneca.availableOperations() 15 | if (mode === 'run') { 16 | console.log(`Invocation of availableOperations returned: ${JSON.stringify(res)}`) 17 | } else { 18 | console.log(JSON.stringify(res)) 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /problems/pin/seneca-pin-executor.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Executes the submitted and the solution, to compare the stdout. 5 | * The module to be require as the first param. 6 | */ 7 | var mod = require(process.argv[2]) 8 | var seneca = require('seneca')() 9 | 10 | // Temporary, see: https://github.com/senecajs/seneca/issues/566 11 | process.removeAllListeners('warning') 12 | 13 | seneca.use(mod).act({role: 'math', cmd: 'sum', left: '3', right: '5'}, function (err, result) { 14 | if (err) return console.error(err) 15 | console.log(result) 16 | seneca.use(mod).act({role: 'math', cmd: 'product', left: '2', right: '15'}, function (err, result) { 17 | if (err) return console.error(err) 18 | console.log(result) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /problems/plugin/seneca-plugin-executor.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Executes the submitted and the solution, to compare the stdout. 5 | * The module to be require as the first param. 6 | */ 7 | 8 | // Temporary, see: https://github.com/senecajs/seneca/issues/566 9 | process.removeAllListeners('warning') 10 | 11 | var mod = require(process.argv[2]) 12 | 13 | var seneca = require('seneca')() 14 | 15 | const left = process.argv[3] 16 | const right = process.argv[4] 17 | 18 | seneca.use(mod).ready(function (err) { 19 | seneca.act(`role:math, cmd:sum, left: ${left}, right:${right}`, (err, res) => { 20 | if (err) { 21 | return console.log(err) 22 | } 23 | 24 | console.log(`PLUGIN NAME CORRECT: ${seneca.hasplugin('operations')}`) 25 | console.log(res) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /problems/override/seneca-override-executor.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Executes the submitted and the solution, to compare the stdout. 5 | * The module to be require as the first param. 6 | */ 7 | 8 | // Temporary, see: https://github.com/senecajs/seneca/issues/566 9 | process.removeAllListeners('warning') 10 | 11 | var seneca = require('seneca')() 12 | seneca.use(process.argv[2]) 13 | 14 | seneca.act({role: 'math', cmd: 'sum', left: 'michele', right: 2.5}, function (err, result) { 15 | if (err && err.toString().indexOf('Expected left and right to be numbers.') > 0) { 16 | return console.log('Expected left and right to be numbers.') 17 | } 18 | return console.log('false') 19 | }) 20 | 21 | seneca.act({role: 'math', cmd: 'sum', left: parseInt(process.argv[3]), right: parseInt(process.argv[4])}, function (err, result) { 22 | if (err) return console.log(err) 23 | console.log(result) 24 | }) 25 | -------------------------------------------------------------------------------- /runners.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var workshopper = require('workshopper') 4 | var path = require('path') 5 | var credits = require('./credits') 6 | var hooray = require('workshopper-hooray') 7 | var more = require('workshopper-more') 8 | 9 | var fpath = function (f) { 10 | return path.join(__dirname, f) 11 | } 12 | 13 | workshopper({ 14 | name: 'seneca-in-practice', 15 | title: 'SENECA IN PRACTICE!', 16 | subtitle: 'Learn how to make Microservices with Seneca', 17 | exerciseDir: fpath('/problems/'), 18 | appDir: __dirname, 19 | languages: ['en'], 20 | menu: { 21 | bg: 'red' 22 | }, 23 | helpFile: path.join(__dirname, 'help.txt'), 24 | commands: [{ 25 | name: 'credits', 26 | handler: credits 27 | }, { 28 | name: 'more', 29 | menu: false, 30 | short: 'm', 31 | handler: more 32 | }], 33 | onComplete: hooray 34 | }) 35 | 36 | // Temporary, see: https://github.com/senecajs/seneca/issues/566 37 | process.removeAllListeners('warning') 38 | -------------------------------------------------------------------------------- /problems/prior_actions/problem.md: -------------------------------------------------------------------------------- 1 | ++++++++++++++++++++++++++ 2 | TO DO 3 | 4 | [text definitely not final] 5 | You need to write a program, that will override, detect if the inputs are integers. 6 | if it is not an integer, it will make make the inputs integers [Math.floor()] 7 | ++++++++++++++++++++++++++ 8 | 9 | The goal of this exercise is to override the sum plugin, with the addition checking if the numbers are integers, if they are not the override should convert them to integers before adding them. If the numbers checked were not integers, the function should reference the overridden function using the *this.prior* function. 10 | 11 | When you have completed your program, you can run it in the test environment with: 12 | 13 | {bold}{appname} run program.js{/bold} 14 | 15 | And once you are happy that it is correct then run: 16 | 17 | {bold}{appname} verify program.js{/bold} 18 | 19 | And your submission will be verified for correctness. After you have 20 | a correct solution, run `{bold}{appname}{/bold}` again and select the next problem! 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Marco 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /help.txt: -------------------------------------------------------------------------------- 1 | {yellow}{bold}Having trouble with a {appname} exercise?{/bold}{/yellow} 2 | 3 | A team of expert helper elves is eagerly waiting to assist you in 4 | mastering the basics of Node.js, or SenecaJS in 5 | particular, simply go to: 6 | https://github.com/nodeschool/discussions/issues 7 | and add a {bold}New Issue{/bold} and let us know what you're having trouble 8 | with. There are no {italic}dumb{/italic} questions! You may be 9 | redirected to one of the support channels below, depending on how 10 | complex your question is. 11 | 12 | If you have a specific question about SenecaJS or one of the level* 13 | packages used in {appname}, you could consider one of the following 14 | support channels instead: 15 | 16 | - Gitter: https://gitter.im/senecajs/seneca 17 | - Seneca Github: https://github.com/senecajs/seneca/issues/new 18 | - Workshopper Github: https://github.com/senecajs/seneca-in-practice/issues/new 19 | 20 | {yellow}{bold}Found a bug with {appname} or just want to contribute?{/bold}{/yellow} 21 | 22 | The official repository for {appname} is: 23 | https://github.com/senecajs/seneca 24 | Feel free to file a bug report or (preferably) a pull request. 25 | -------------------------------------------------------------------------------- /problems/override/solution/solution.js: -------------------------------------------------------------------------------- 1 | module.exports = function math (options) { 2 | this.add({role: 'math', cmd: 'sum'}, (msg, respond) => { 3 | var sum = msg.left + msg.right 4 | respond(null, {answer: sum}) 5 | }) 6 | 7 | this.add({role: 'math', cmd: 'product'}, (msg, respond) => { 8 | var product = msg.left * msg.right 9 | respond(null, {answer: product}) 10 | }) 11 | 12 | this.add({role: 'math', cmd: 'sum', integer: 'true'}, (msg, respond) => { 13 | var sum = Math.floor(msg.left) + Math.floor(msg.right) 14 | respond(null, {answer: sum}) 15 | }) 16 | 17 | // override role:math,cmd:sum with additional functionality 18 | this.add({role: 'math', cmd: 'sum'}, function (msg, respond) { // THIS CANNOT BE A FAT ARROW 19 | // bail out early if there's a problem 20 | if (!isFinite(msg.left) || !isFinite(msg.right)) { 21 | return respond(new Error('Expected left and right to be numbers.')) 22 | } 23 | 24 | // call previous action function for role:math,cmd:sum 25 | this.prior(msg, (err, result) => { 26 | if (err) return respond(err) 27 | result.info = `${msg.left} + ${msg.right}` 28 | respond(null, result) 29 | }) 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /problems/pin/solution/solution.js: -------------------------------------------------------------------------------- 1 | module.exports = function math (options) { 2 | this.add({role: 'math', cmd: 'sum'}, (msg, respond) => { 3 | var sum = msg.left + msg.right 4 | respond(null, {answer: sum}) 5 | }) 6 | 7 | this.add({role: 'math', cmd: 'product'}, (msg, respond) => { 8 | var product = msg.left * msg.right 9 | respond(null, {answer: product}) 10 | }) 11 | 12 | this.add({role: 'math', cmd: 'sum', integer: 'true'}, (msg, respond) => { 13 | var sum = Math.floor(msg.left) + Math.floor(msg.right) 14 | respond(null, {answer: sum}) 15 | }) 16 | 17 | // override role:math,cmd:sum with additional functionality 18 | this.add({role: 'math', cmd: 'sum'}, function (msg, respond) { 19 | // bail out early if there's a problem 20 | if (!isFinite(msg.left) || !isFinite(msg.right)) { 21 | return respond(new Error('Expected left and right to be numbers.')) 22 | } 23 | 24 | // call previous action function for role:math,cmd:sum 25 | this.prior(msg, (err, result) => { 26 | if (err) return respond(err) 27 | result.info = `${msg.left} + ${msg.right}` 28 | respond(null, result) 29 | }) 30 | }) 31 | 32 | this.wrap({role: 'math'}, function (msg, respond) { 33 | msg.left = Number(msg.left).valueOf() 34 | msg.right = Number(msg.right).valueOf() 35 | this.prior(msg, respond) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /problems/transport/solution/math.js: -------------------------------------------------------------------------------- 1 | module.exports = function math (options) { 2 | this.add({role: 'math', cmd: 'sum'}, (msg, respond) => { 3 | var sum = msg.left + msg.right 4 | respond(null, {answer: sum}) 5 | }) 6 | 7 | this.add({role: 'math', cmd: 'product'}, (msg, respond) => { 8 | var product = msg.left * msg.right 9 | respond(null, {answer: product}) 10 | }) 11 | 12 | this.add({role: 'math', cmd: 'sum', integer: 'true'}, (msg, respond) => { 13 | var sum = Math.floor(msg.left) + Math.floor(msg.right) 14 | respond(null, {answer: sum}) 15 | }) 16 | 17 | // override role:math,cmd:sum with additional functionality 18 | this.add({role: 'math', cmd: 'sum'}, function (msg, respond) { 19 | // bail out early if there's a problem 20 | if (!isFinite(msg.left) || !isFinite(msg.right)) { 21 | return respond(new Error('Expected left and right to be numbers.')) 22 | } 23 | 24 | // call previous action function for role:math,cmd:sum 25 | this.prior(msg, (err, result) => { 26 | if (err) return respond(err) 27 | result.info = `${msg.left} + ${msg.right}` 28 | respond(null, result) 29 | }) 30 | }) 31 | 32 | this.wrap({role: 'math'}, function (msg, respond) { 33 | msg.left = Number(msg.left).valueOf() 34 | msg.right = Number(msg.right).valueOf() 35 | this.prior(msg, respond) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /problems/transport_client/solution/plugin/math.js: -------------------------------------------------------------------------------- 1 | module.exports = function math (options) { 2 | this.add({role: 'math', cmd: 'sum'}, (msg, respond) => { 3 | var sum = msg.left + msg.right 4 | respond(null, {answer: sum}) 5 | }) 6 | 7 | this.add({role: 'math', cmd: 'product'}, (msg, respond) => { 8 | var product = msg.left * msg.right 9 | respond(null, {answer: product}) 10 | }) 11 | 12 | this.add({role: 'math', cmd: 'sum', integer: 'true'}, (msg, respond) => { 13 | var sum = Math.floor(msg.left) + Math.floor(msg.right) 14 | respond(null, {answer: sum}) 15 | }) 16 | 17 | // override role:math,cmd:sum with additional functionality 18 | this.add({role: 'math', cmd: 'sum'}, function (msg, respond) { 19 | // bail out early if there's a problem 20 | if (!isFinite(msg.left) || !isFinite(msg.right)) { 21 | return respond(new Error('Expected left and right to be numbers.')) 22 | } 23 | 24 | // call previous action function for role:math,cmd:sum 25 | this.prior(msg, (err, result) => { 26 | if (err) return respond(err) 27 | result.info = `${msg.left} + ${msg.right}` 28 | respond(null, result) 29 | }) 30 | }) 31 | 32 | this.wrap({role: 'math'}, function (msg, respond) { 33 | msg.left = Number(msg.left).valueOf() 34 | msg.right = Number(msg.right).valueOf() 35 | this.prior(msg, respond) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /problems/prior_actions/exercise.js: -------------------------------------------------------------------------------- 1 | var exercise = require('workshopper-exercise')() 2 | var filecheck = require('workshopper-exercise/filecheck') 3 | var execute = require('workshopper-exercise/execute') 4 | var comparestdout = require('workshopper-exercise/comparestdout') 5 | 6 | // checks that the submission file actually exists 7 | exercise = filecheck(exercise) 8 | 9 | // execute the solution and submission in parallel with spawn() 10 | exercise = execute(exercise) 11 | 12 | // compare stdout of solution and submission 13 | exercise = comparestdout(exercise) 14 | 15 | /** 16 | * Uses seneca-executor.js and pass the module to be required as param. 17 | * The executoor will require the module and then execute it using seneca. 18 | * (note that this is quite different from the "normal" workshopper-exercise). 19 | * The seneca log is set to "quiet" to have a clean comparation of stdouts. 20 | */ 21 | exercise.addSetup(function (mode, callback) { 22 | this.solutionArgs = [this.solution, '--seneca.log.quiet'] 23 | this.submissionArgs = [process.cwd() + '/' + this.submission, '--seneca.log.quiet'] 24 | this.solution = __dirname + '/seneca-prior-actions-executor.js' 25 | this.submission = __dirname + '/seneca-prior-actions-executor.js' 26 | callback(null) 27 | }) 28 | 29 | // cleanup for both run and verify 30 | exercise.addCleanup(function (mode, passed, callback) { /* Do nothing */ }) 31 | 32 | module.exports = exercise 33 | -------------------------------------------------------------------------------- /problems/mem_store/seneca-memstore-executor.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var async = require('async') 4 | 5 | // Temporary, see: https://github.com/senecajs/seneca/issues/566 6 | process.removeAllListeners('warning') 7 | 8 | /** 9 | * Executes the submitted and the solution, to compare the stdout. 10 | * The module to be require as the first param. 11 | */ 12 | var seneca = require('seneca')() 13 | 14 | seneca.use('basic') 15 | seneca.use('entity') 16 | seneca.use(process.argv[2]) 17 | 18 | async.waterfall([ 19 | function (callback) { 20 | seneca.act({role: 'math', cmd: 'sum', left: '3', right: '5'}, function (err, result) { 21 | if (err) return console.error(err) 22 | callback() 23 | }) 24 | }, 25 | function (callback) { 26 | seneca.act({role: 'math', cmd: 'sum', left: '13', right: '55'}, function (err, result) { 27 | if (err) return console.error(err) 28 | callback() 29 | }) 30 | }, 31 | function (callback) { 32 | seneca.act({role: 'math', cmd: 'product', left: '13', right: '55'}, function (err, result) { 33 | if (err) return console.error(err) 34 | callback() 35 | }) 36 | } 37 | ] 38 | , function () { 39 | seneca.act({role: 'math', cmd: 'operation-history'}, function (err, result) { 40 | if (err) return console.error(err) 41 | result.answer.forEach(function (en) { 42 | console.log(`Operation executed: ${en.cmd} on (${en.left}, ${en.right})`) 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /problems/mem_store/exercise.js: -------------------------------------------------------------------------------- 1 | var exercise = require('workshopper-exercise')() 2 | var filecheck = require('workshopper-exercise/filecheck') 3 | var execute = require('workshopper-exercise/execute') 4 | const comparestdout = require('../comparestdout-filterlogs') 5 | const path = require('path') 6 | 7 | // checks that the submission file actually exists 8 | exercise = filecheck(exercise) 9 | 10 | // execute the solution and submission in parallel with spawn() 11 | exercise = execute(exercise) 12 | 13 | // compare stdout of solution and submission 14 | exercise = comparestdout(exercise) 15 | 16 | /** 17 | * Uses seneca-executor.js and pass the module to be required as param. 18 | * The executoor will require the module and then execute it using seneca. 19 | * (note that this is quite different from the "normal" workshopper-exercise). 20 | * The seneca log is set to "quiet" to have a clean comparation of stdouts. 21 | */ 22 | exercise.addSetup(function (mode, callback) { 23 | const submissionFilePath = path.join(process.cwd(), this.submission) 24 | 25 | this.solutionArgs = [this.solution] 26 | this.submissionArgs = [submissionFilePath] 27 | this.solution = path.join(__dirname, '/seneca-memstore-executor.js') 28 | this.submission = path.join(__dirname, '/seneca-memstore-executor.js') 29 | callback() 30 | }) 31 | 32 | // cleanup for both run and verify 33 | exercise.addCleanup(function (mode, passed, callback) { 34 | // Do nothing 35 | }) 36 | 37 | module.exports = exercise 38 | -------------------------------------------------------------------------------- /problems/decorate/exercise.js: -------------------------------------------------------------------------------- 1 | var exercise = require('workshopper-exercise')() 2 | var filecheck = require('workshopper-exercise/filecheck') 3 | var execute = require('workshopper-exercise/execute') 4 | const comparestdout = require('../comparestdout-filterlogs') 5 | const path = require('path') 6 | 7 | // checks that the submission file actually exists 8 | exercise = filecheck(exercise) 9 | 10 | // execute the solution and submission in parallel with spawn() 11 | exercise = execute(exercise) 12 | 13 | // compare stdout of solution and submission 14 | exercise = comparestdout(exercise) 15 | 16 | /** 17 | * Uses seneca-executor.js and pass the module to be required as param. 18 | * The executoor will require the module and then execute it using seneca. 19 | * (note that this is quite different from the "normal" workshopper-exercise). 20 | * The seneca log is set to "quiet" to have a clean comparation of stdouts. 21 | */ 22 | exercise.addSetup(function (mode, callback) { 23 | const submissionFilePath = path.join(process.cwd(), this.submission) 24 | 25 | this.solutionArgs = [this.solution, mode] 26 | this.submissionArgs = [submissionFilePath, mode] 27 | this.solution = path.join(__dirname, '/seneca-decorate-executor.js') 28 | this.submission = path.join(__dirname, '/seneca-decorate-executor.js') 29 | callback() 30 | }) 31 | 32 | // cleanup for both run and verify 33 | exercise.addCleanup(function (mode, passed, callback) { 34 | // Do nothing 35 | }) 36 | 37 | module.exports = exercise 38 | -------------------------------------------------------------------------------- /problems/decorate/problem.md: -------------------------------------------------------------------------------- 1 | Decorations are a good way to mix in commonly used functionality with your Seneca 2 | instance, using: `decorate(name, handler)` 3 | 4 | Where: 5 | - `name`: The name you wish to call the decorated function or object. 6 | - `handler`: The handler the decorate seneca with. This can be a function or an object. 7 | 8 | For instance here we add a stamp function that will print a time stamp when invoked: 9 | 10 | ```javascript 11 | var seneca = require('seneca') 12 | 13 | seneca.decorate('stamp', (pattern) => { 14 | console.log(Date.now(), pattern) 15 | }) 16 | 17 | seneca.stamp('role:echo') 18 | ``` 19 | 20 | Also, Plugins are a good place where to add decorations. See below: 21 | 22 | ```javascript 23 | function plugin (opts) { 24 | var seneca = this 25 | 26 | seneca.decorate('stamp', (pattern) => { 27 | console.log(Date.now(), pattern) 28 | }) 29 | 30 | // (...) 31 | } 32 | ``` 33 | 34 | The goal of the exercise is to make the math plugin developed so far to decorate 35 | Seneca with a `availableOperations` function, that must return the list of the 36 | available operations (i.e. `['sum', 'product', 'operation-history']`) 37 | 38 | When you have completed your program, you can run it with: 39 | 40 | {bold}seneca-in-practice run program.js{/bold} 41 | 42 | And once you are happy that it is correct then run: 43 | 44 | {bold}seneca-in-practice verify program.js{/bold} 45 | 46 | And your submission will be verified for correctness. 47 | After you have a correct solution, run `{bold}{appname}{/bold}` again and 48 | select the next problem! 49 | -------------------------------------------------------------------------------- /problems/extend_client/problem.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | After having extended the type of messages supported by our services we now 4 | need to invoke it. 5 | 6 | So, if we want to call our newly extended hello services we should do 7 | something like this: 8 | 9 | ```javascript 10 | var seneca = require('seneca')() 11 | 12 | var plugin = function( options ) { ... } // as above 13 | 14 | seneca.use( plugin, {} ) 15 | seneca.ready(function (err) { 16 | seneca.act({role:'greetings', cmd:'hello', name:'michele'}, console.log ) 17 | seneca.act({role:'greetings', cmd:'hello', lang:'it', name:'michele'}, console.log ) 18 | }) 19 | 20 | ``` 21 | 22 | This code should produce: 23 | 24 | ```javascript 25 | Hello Michele 26 | Ciao Michele 27 | 28 | ``` 29 | 30 | The goal of the exercise is to extend the client that we developed, so that it 31 | calls the two patterns that we created at previous step in order, i.e. 32 | first calling first version of `sum` then calling the version of `sum` with `integer: true` 33 | and printing out on console both results (in the same order). 34 | 35 | Keep in mind that numbers will be passed as parameters (use `process.argv`) 36 | and that you might want to transform them into float numbers. 37 | 38 | When you have completed your program, you can run it using with: 39 | 40 | {bold}seneca-in-practice run program.js{/bold} 41 | 42 | And once you are happy that it is correct then run: 43 | 44 | {bold}seneca-in-practice verify program.js{/bold} 45 | 46 | And your submission will be verified for correctness. 47 | After you have a correct solution, run `{bold}{appname}{/bold}` again and 48 | select the next problem! 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "seneca-in-practice", 3 | "version": "0.3.10", 4 | "description": "Seneca.js (http://senecajs.org/) NodeSchool workshop", 5 | "main": "runners.js", 6 | "dependencies": { 7 | "async": "2.0.1", 8 | "chalk": "^1.1.3", 9 | "colors-tmpl": "^1.0.0", 10 | "end-of-stream": "^1.1.0", 11 | "hyperquest": "2.0.0", 12 | "lodash": "^4.15.0", 13 | "seneca": "^3.2.1", 14 | "seneca-basic": "^0.5.0", 15 | "seneca-entity": "1.3.0", 16 | "split2": "^2.1.0", 17 | "through2": "^2.0.1", 18 | "through2-filter": "^2.0.0", 19 | "tuple": "0.0.1", 20 | "wcstring": "^2.1.1", 21 | "workshopper": "2.7.0", 22 | "workshopper-boilerplate": "1.1.2", 23 | "workshopper-exercise": "2.7.0", 24 | "workshopper-hooray": "^1.1.0", 25 | "workshopper-more": "^1.0.1", 26 | "workshopper-wrappedexec": "^0.1.2" 27 | }, 28 | "bin": { 29 | "seneca-in-practice": "runners.js" 30 | }, 31 | "devDependencies": { 32 | "eslint": "3.4.0", 33 | "eslint-config-standard": "6.0.0", 34 | "eslint-plugin-promise": "^2.0.1", 35 | "eslint-plugin-standard": "^2.0.0" 36 | }, 37 | "scripts": { 38 | "start": "node runners", 39 | "test": "echo \"No test specified\"", 40 | "lint": "eslint **/*.js" 41 | }, 42 | "keywords": [ 43 | "seneca.js", 44 | "nodeSchool", 45 | "nodejs" 46 | ], 47 | "repository": { 48 | "type": "git", 49 | "url": "https://github.com/senecajs/seneca-in-practice.git" 50 | }, 51 | "author": "Marco Piraccini ", 52 | "contributors": [ 53 | "Michele Capra " 54 | ], 55 | "license": "MIT" 56 | } 57 | -------------------------------------------------------------------------------- /problems/roles/problem.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | There are no hard and fast rules for naming your action pattern, 4 | however, there are some conventions that help to organize them. 5 | 6 | Indeed, a plugin: 7 | - Provides a functionality to the system 8 | - This functionality fulfills a role in the system 9 | 10 | So it makes sense to use `role:plugin-name` as part of your action pattern. 11 | This creates a pattern namespace to avoid clashes with other plugin patterns. 12 | The use of the word “role” also indicates that other plugins may override some 13 | aspects of this role (that is, aspects of this functionality) by providing 14 | extensions to some of the action patterns (we'll see how to extend microservices 15 | in the next exercise). 16 | 17 | Another common convention is to use the property “cmd” for the main public 18 | commands exposed by the plugin. So, you might have, for example: 19 | 20 | ```javascript 21 | var plugin = function (options) { 22 | this.add( {role:'greetings', cmd:'hey'}, function( args, respond ) { 23 | var hey = "Hey " + args.name; 24 | respond( null, { answer: hey }); 25 | }) 26 | } 27 | ``` 28 | 29 | The goal of the exercise is to add to the `math` module from the previous 30 | exercise a new command `product` with the same role `math`. This command will 31 | multiply two numbers. 32 | 33 | When you have completed your program, you can run it using with: 34 | 35 | {bold}{appname} run program.js{/bold} 36 | 37 | And once you are happy that it is correct then run: 38 | 39 | {bold}{appname} verify program.js{/bold} 40 | 41 | And your submission will be verified for correctness. 42 | After you have a correct solution, run `{bold}{appname}{/bold}` again and 43 | select the next problem! 44 | -------------------------------------------------------------------------------- /problems/client/exercise.js: -------------------------------------------------------------------------------- 1 | let exercise = require('workshopper-exercise')() 2 | const filecheck = require('workshopper-exercise/filecheck') 3 | const execute = require('workshopper-exercise/execute') 4 | const comparestdout = require('../comparestdout-filterlogs') 5 | const {getRandomInt} = require('../utils') 6 | const path = require('path') 7 | 8 | // cleanup for both run and verify 9 | exercise.addCleanup(function (mode, passed, callback) { /* Do nothing */ }) 10 | 11 | // checks that the submission file actually exists 12 | exercise = filecheck(exercise) 13 | 14 | // execute the solution and submission in parallel with spawn() 15 | exercise = execute(exercise) 16 | 17 | exercise.addSetup(function (mode, callback) { 18 | let a, b 19 | const submissionFilePath = path.join(process.cwd(), this.submission) 20 | 21 | // Test arguments to be summed 22 | if (mode === 'run') { 23 | // run 24 | if (process.argv.length < 6) { 25 | a = getRandomInt() 26 | b = getRandomInt() 27 | console.log(`Two arguments must be provided, generating random: ${a}, ${b}`) 28 | } else { 29 | a = process.argv[4] 30 | b = process.argv[5] 31 | } 32 | this.submissionArgs = [submissionFilePath, a, b] 33 | } else { 34 | // verify 35 | a = getRandomInt() 36 | b = getRandomInt() 37 | 38 | this.solutionArgs = [this.solution, a, b] 39 | this.submissionArgs = [submissionFilePath, a, b] 40 | } 41 | this.solution = path.join(exercise.dir, '../seneca-client-executor.js') 42 | this.submission = path.join(exercise.dir, '../seneca-client-executor.js') 43 | callback() 44 | }) 45 | 46 | // compare stdout of solution and submission 47 | exercise = comparestdout(exercise) 48 | 49 | module.exports = exercise 50 | -------------------------------------------------------------------------------- /problems/transport_client/exercise.js: -------------------------------------------------------------------------------- 1 | var exercise = require('workshopper-exercise')() 2 | var filecheck = require('workshopper-exercise/filecheck') 3 | var execute = require('workshopper-exercise/execute') 4 | const {getRandomInt} = require('../utils') 5 | const comparestdout = require('../comparestdout-filterlogs') 6 | 7 | // checks that the submission file actually exists 8 | exercise = filecheck(exercise) 9 | 10 | // execute the solution and submission in parallel with spawn() 11 | exercise = execute(exercise) 12 | 13 | // compare stdout of solution and submission 14 | exercise = comparestdout(exercise) 15 | 16 | let a, b 17 | 18 | /** 19 | * The seneca log is set to "quiet" to have a clean comparation of stdouts. 20 | */ 21 | exercise.addSetup(function (mode, callback) { 22 | if (mode === 'run') { 23 | if (process.argv.length < 6) { 24 | a = getRandomInt() 25 | b = getRandomInt() 26 | console.log(`Two arguments must be provided, generating random: ${a}, ${b}`) 27 | } else { 28 | a = process.argv[4] 29 | b = process.argv[5] 30 | } 31 | } else { 32 | // verify 33 | a = getRandomInt() 34 | b = getRandomInt() 35 | } 36 | 37 | this.seneca = require('seneca')() 38 | // Start the Seneca Microservice 39 | this.seneca.use('solution/plugin/math.js').listen({type: 'tcp'}) 40 | 41 | var testArgs = [a, b] 42 | this.submissionArgs = this.submissionArgs.concat(testArgs) 43 | this.solutionArgs = this.solutionArgs.concat(testArgs) 44 | 45 | setTimeout(function () { 46 | callback() 47 | }, 2000) 48 | }) 49 | 50 | // cleanup for both run and verify 51 | exercise.addCleanup(function (mode, passed, callback) { 52 | // Closes seneca 53 | this.seneca.close(callback) 54 | }) 55 | 56 | module.exports = exercise 57 | -------------------------------------------------------------------------------- /problems/extend_client/exercise.js: -------------------------------------------------------------------------------- 1 | let exercise = require('workshopper-exercise')() 2 | const filecheck = require('workshopper-exercise/filecheck') 3 | const execute = require('workshopper-exercise/execute') 4 | const comparestdout = require('../comparestdout-filterlogs') 5 | const {getRandomFloat} = require('../utils') 6 | const path = require('path') 7 | 8 | // cleanup for both run and verify 9 | exercise.addCleanup(function (mode, passed, callback) { /* Do nothing */ }) 10 | 11 | // checks that the submission file actually exists 12 | exercise = filecheck(exercise) 13 | 14 | // execute the solution and submission in parallel with spawn() 15 | exercise = execute(exercise) 16 | 17 | exercise.addSetup(function (mode, callback) { 18 | let a, b 19 | const submissionFilePath = path.join(process.cwd(), this.submission) 20 | 21 | // Test arguments to be summed 22 | if (mode === 'run') { 23 | // run 24 | if (process.argv.length < 6) { 25 | a = getRandomFloat() 26 | b = getRandomFloat() 27 | console.log(`Two arguments must be provided, generating random: ${a}, ${b}`) 28 | } else { 29 | a = process.argv[4] 30 | b = process.argv[5] 31 | } 32 | this.submissionArgs = [submissionFilePath, a, b] 33 | } else { 34 | // verify 35 | a = getRandomFloat() 36 | b = getRandomFloat() 37 | console.log('this.solution', this.solution) 38 | console.log('this.submissionFilePath', submissionFilePath) 39 | 40 | this.solutionArgs = [this.solution, a, b] 41 | this.submissionArgs = [submissionFilePath, a, b] 42 | } 43 | this.solution = path.join(exercise.dir, '../seneca-client-executor.js') 44 | this.submission = path.join(exercise.dir, '../seneca-client-executor.js') 45 | callback() 46 | }) 47 | 48 | // compare stdout of solution and submission 49 | exercise = comparestdout(exercise) 50 | 51 | module.exports = exercise 52 | -------------------------------------------------------------------------------- /problems/plugin/exercise.js: -------------------------------------------------------------------------------- 1 | let exercise = require('workshopper-exercise')() 2 | const filecheck = require('workshopper-exercise/filecheck') 3 | const execute = require('workshopper-exercise/execute') 4 | const comparestdout = require('../comparestdout-filterlogs') 5 | const path = require('path') 6 | const {getRandomInt} = require('../utils') 7 | 8 | // checks that the submission file actually exists 9 | exercise = filecheck(exercise) 10 | 11 | // execute the solution and submission in parallel with spawn() 12 | exercise = execute(exercise) 13 | 14 | // compare stdout of solution and submission 15 | exercise = comparestdout(exercise) 16 | 17 | /** 18 | * Uses seneca-executor.js and pass the module to be required as param. 19 | * The executoor will require the module and then execute it using seneca. 20 | * (note that this is quite different from the "normal" workshopper-exercise). 21 | * The seneca log is set to "quiet" to have a clean comparation of stdouts. 22 | */ 23 | exercise.addSetup(function (mode, callback) { 24 | const left = getRandomInt(0, 100) 25 | const right = getRandomInt(0, 100) 26 | const submissionFilePath = path.join(process.cwd(), this.submission) 27 | 28 | if (mode === 'run') { 29 | // run 30 | console.log(`Calling plugin with left: ${left}, right: ${right}`) 31 | } 32 | this.solutionArgs = [this.solution, left, right] 33 | this.submissionArgs = [submissionFilePath, left, right] 34 | 35 | // this.solutionArgs = [this.solution, '--seneca.log.quiet'] 36 | // this.submissionArgs = [process.cwd() + '/' + this.submission, '--seneca.log.quiet'] 37 | this.solution = path.join(exercise.dir, '/seneca-plugin-executor.js') 38 | this.submission = path.join(exercise.dir, '/seneca-plugin-executor.js') 39 | callback() 40 | }) 41 | 42 | // cleanup for both run and verify 43 | exercise.addCleanup(function (mode, passed, callback) { 44 | // Do nothing 45 | }) 46 | 47 | module.exports = exercise 48 | -------------------------------------------------------------------------------- /problems/transport_client/problem.md: -------------------------------------------------------------------------------- 1 | The same **Transport independence** concept of Seneca applies also for 2 | Microservice's clients. 3 | 4 | As we just see, with Seneca, you create microservices by calling `seneca.listen`. 5 | To talk with the services we use `seneca.client`. 6 | 7 | Both `seneca.client` and `seneca.listen` accept the following parameters: 8 | * `port`: optional integer; port number. 9 | * `host`: optional string; host IP address. 10 | * `spec`: optional object; full specification object. 11 | 12 | For instance, we can have this microservice: 13 | 14 | ```javascript 15 | require('seneca')() 16 | .use( 'myplugin' ) 17 | .listen( { type:'tcp'} ) 18 | ``` 19 | ...and a client: 20 | 21 | ```javascript 22 | require('seneca')() 23 | .client({ type:'tcp'}) 24 | .act({role: 'greetings', cmd: 'hello', name: 'Marco'}, console.log) 25 | ``` 26 | 27 | The goal of the exercise is to write a client for the previous `math` plugin using 28 | the `sum` command exposed on TCP on the default port that prints on `console.log` 29 | the microservice answer. 30 | 31 | To solve the exercise create the solution which call the sum service using the first 32 | two arguments (use `process.argv`). 33 | 34 | Some Notes: 35 | 36 | Since we have to require seneca, the seneca module must be available. 37 | For that, just install it in the local folder using `npm i seneca`. That will 38 | create a `node_modules` folder with seneca and all its dependencies. 39 | 40 | If no `host` is specified, the client uses `0.0.0.0`. This will not work on 41 | some versions of Windows. To fix that, simply specify `host: '127.0.0.1'` on 42 | client connection. 43 | 44 | Remember to `close` seneca when the client has received the answer, otherwise 45 | the process will hang (and you have to terminate it manually). 46 | 47 | If you want to test it manually, you can also change (or make a copy) of the solution 48 | of the previous exercise and make it expose the microservice through TCP, launch 49 | it with node and then launch your solution directly (`node program.js`). 50 | -------------------------------------------------------------------------------- /problems/pin/exercise.js: -------------------------------------------------------------------------------- 1 | let exercise = require('workshopper-exercise')() 2 | const filecheck = require('workshopper-exercise/filecheck') 3 | const execute = require('workshopper-exercise/execute') 4 | const path = require('path') 5 | const {getRandomInt} = require('../utils') 6 | const comparestdout = require('../comparestdout-filterlogs') 7 | 8 | // checks that the submission file actually exists 9 | exercise = filecheck(exercise) 10 | 11 | // execute the solution and submission in parallel with spawn() 12 | exercise = execute(exercise) 13 | 14 | // compare stdout of solution and submission 15 | exercise = comparestdout(exercise) 16 | 17 | /** 18 | * Uses seneca-executor.js and pass the module to be required as param. 19 | * The executoor will require the module and then execute it using seneca. 20 | * (note that this is quite different from the "normal" workshopper-exercise). 21 | * The seneca log is set to "quiet" to have a clean comparation of stdouts. 22 | */ 23 | exercise.addSetup(function (mode, callback) { 24 | const submissionFilePath = path.join(process.cwd(), this.submission) 25 | let cmd, a, b 26 | if (mode === 'run') { 27 | // run 28 | if (process.argv.length < 6) { 29 | a = getRandomInt() 30 | b = getRandomInt() 31 | cmd = 'sum' 32 | console.log(`Tree arguments must be provided, using sum with generating random: ${a}, ${b}`) 33 | } else { 34 | cmd = process.argv[4] 35 | a = process.argv[5] 36 | b = process.argv[6] 37 | } 38 | 39 | this.submissionArgs = [submissionFilePath, cmd, a, b] 40 | this.solution = path.join(__dirname, '/seneca-pin-executor-run.js') 41 | this.submission = path.join(__dirname, '/seneca-pin-executor-run.js') 42 | } else { 43 | this.solutionArgs = [this.solution] 44 | this.submissionArgs = [submissionFilePath] 45 | this.solution = path.join(__dirname, '/seneca-pin-executor.js') 46 | this.submission = path.join(__dirname, '/seneca-pin-executor.js') 47 | } 48 | callback(null) 49 | }) 50 | 51 | // cleanup for both run and verify 52 | exercise.addCleanup(function (mode, passed, callback) { /* Do nothing */ }) 53 | 54 | module.exports = exercise 55 | -------------------------------------------------------------------------------- /problems/client/problem.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | We know how to create a Seneca Microservice. 4 | The next step is to write code to use this Microservice as client. 5 | 6 | The method to be used for this purpose is `seneca.act`, which is used to submit 7 | a message to act on. It has two parameters: 8 | * `msg`: the message object. 9 | * `response_callback`: a function that receives the message response, if any. 10 | 11 | The response callback is a function you provide with the standard `(error, 12 | result)` signature. If there is a problem (say, the message matches no patterns), 13 | then the first argument is an Error object. 14 | If everything goes according to plan, the second argument is the result object. 15 | 16 | ```javascript 17 | seneca.act({role: 'greetings', cmd: 'hello', name: 'Marco'}, 18 | function (err, result) { 19 | if (err) return console.error (err) 20 | console.log(result) 21 | }) 22 | ``` 23 | The goal of the exercise is to build a simple {italic}sum{/italic} service 24 | client for the pattern defined in the previous exercise. 25 | The client must add the two numbers passed as parameters (use `process.argv`) and 26 | print out the result obtained from Seneca using `console.log`. 27 | Keep in mind that process.argv are passed as string so you might want to transform 28 | them into numbers and that the first parameter will be on position 2 of argv. 29 | 30 | Also, copy or rename the solution of the previous exercise in a `math.js` module, 31 | so it can be required: 32 | 33 | ``` javascript 34 | var seneca = require('./math') 35 | 36 | (...) 37 | ``` 38 | In this way we are using the Seneca instance from the previous exercise, which 39 | is exported from `math.js`. Again, that will change using Seneca plugins. 40 | 41 | When you have completed your program, you can run it with: 42 | 43 | {bold}seneca-in-practice run program.js{/bold} 44 | 45 | And once you are happy that it is correct then run: 46 | 47 | {bold}seneca-in-practice verify program.js{/bold} 48 | 49 | And your submission will be verified for correctness. 50 | After you have a correct solution, run `{bold}{appname}{/bold}` again and 51 | select the next problem! 52 | -------------------------------------------------------------------------------- /problems/override/exercise.js: -------------------------------------------------------------------------------- 1 | let exercise = require('workshopper-exercise')() 2 | const filecheck = require('workshopper-exercise/filecheck') 3 | const execute = require('workshopper-exercise/execute') 4 | const path = require('path') 5 | const {getRandomInt} = require('../utils') 6 | const comparestdout = require('../comparestdout-filterlogs') 7 | 8 | // checks that the submission file actually exists 9 | exercise = filecheck(exercise) 10 | 11 | // execute the solution and submission in parallel with spawn() 12 | exercise = execute(exercise) 13 | 14 | // compare stdout of solution and submission 15 | exercise = comparestdout(exercise) 16 | 17 | /** 18 | * Uses seneca-executor.js and pass the module to be required as param. 19 | * The executoor will require the module and then execute it using seneca. 20 | * (note that this is quite different from the "normal" workshopper-exercise). 21 | * The seneca log is set to "quiet" to have a clean comparation of stdouts. 22 | */ 23 | exercise.addSetup(function (mode, callback) { 24 | const submissionFilePath = path.join(process.cwd(), this.submission) 25 | let a, b 26 | if (mode === 'run') { 27 | // run 28 | if (process.argv.length < 6) { 29 | a = getRandomInt() 30 | b = getRandomInt() 31 | console.log(`Two arguments must be provided, generating random: ${a}, ${b}`) 32 | } else { 33 | a = process.argv[4] 34 | b = process.argv[5] 35 | } 36 | this.submissionArgs = [submissionFilePath, a, b] 37 | this.solution = path.join(__dirname, '/seneca-override-executor-run.js') 38 | this.submission = path.join(__dirname, '/seneca-override-executor-run.js') 39 | } else { 40 | a = getRandomInt() 41 | b = getRandomInt() 42 | this.solutionArgs = [this.solution, a, b] 43 | this.submissionArgs = [submissionFilePath, a, b] 44 | this.solution = path.join(__dirname, '/seneca-override-executor.js') 45 | this.submission = path.join(__dirname, '/seneca-override-executor.js') 46 | } 47 | callback(null) 48 | }) 49 | 50 | // cleanup for both run and verify 51 | exercise.addCleanup(function (mode, passed, callback) { /* Do nothing */ }) 52 | 53 | module.exports = exercise 54 | -------------------------------------------------------------------------------- /problems/transport/problem.md: -------------------------------------------------------------------------------- 1 | Up to now, we had everything running in the same process. In Seneca we have 2 | **transport independence**. This means that you can send messages between services in 3 | many ways, all hidden from your business logic. It's possible to change that 4 | using the `listen` method: 5 | 6 | ```javascript 7 | require('seneca')().use('myplugin').listen() 8 | ``` 9 | Running this code starts a Microservice process that listens on port 10101 10 | (the default) for HTTP requests. This is not a web server. 11 | In this case, HTTP is being used as the transport mechanism for messages. 12 | 13 | Note that if no host is specified, seneca will try to connect to host at 0.0.0.0, 14 | which does not work on Windows. To avoid that, just pass options to listen, e.g.: 15 | 16 | ```javascript 17 | seneca.listen({port: 8080, host:'127.0.0.1'}) 18 | ``` 19 | 20 | The HTTP transport provides an easy way to integrate with Seneca Microservices, 21 | but it has all the HTTP overhead. 22 | Another transport that you can use is direct TCP connections. Seneca provides 23 | both HTTP and TCP options via the built-in transport. Let’s move to TCP: 24 | 25 | ```javascript 26 | seneca.listen({type:'tcp'}) 27 | ``` 28 | 29 | The goal of the exercise is to expose the `math` plugin already implemented using 30 | HTTP Transport on a port specified as parameter (read using `process.argv`). 31 | To solve this exercise you can simply require the plugin and expose it. 32 | 33 | Also, you can test this Microservice using the browser or curl doing a GET, for 34 | instance you can run directly the solution using:`node program.js 8888` (note that 35 | if you use `seneca-in-practice run program.js`, it uses a random port, invokes 36 | the microservice automatically and then ends, so you cannot test it directly). 37 | 38 | Following this example you can use the URL directly with cURL: 39 | 40 | `curl -d '{"cmd":"sum","left":1,"right":2}' http://localhost:8888/act?role=math` 41 | 42 | And once you are happy that it is correct then run: 43 | 44 | {bold}{appname} verify program.js{/bold} 45 | 46 | And your submission will be verified for correctness. 47 | After you have a correct solution, run `{bold}{appname}{/bold}` again and 48 | select the next problem! 49 | -------------------------------------------------------------------------------- /problems/mem_store/solution/solution.js: -------------------------------------------------------------------------------- 1 | module.exports = function math (options) { 2 | this.add({role: 'math', cmd: 'sum'}, function (msg, respond) { 3 | var operation = this.make$('operation') 4 | operation.cmd = 'sum' 5 | operation.left = msg.left 6 | operation.right = msg.right 7 | operation.save$(function (err, operation) { 8 | if (err) { 9 | return respond(err) 10 | } 11 | return respond(null, {answer: msg.left + msg.right}) 12 | }) 13 | }) 14 | 15 | this.add({role: 'math', cmd: 'product'}, function (msg, respond) { 16 | var operation = this.make$('operation') 17 | operation.cmd = 'product' 18 | operation.left = msg.left 19 | operation.right = msg.right 20 | operation.save$((err, operation) => { 21 | if (err) { 22 | return respond(err) 23 | } 24 | return respond(null, {answer: msg.left * msg.right}) 25 | }) 26 | }) 27 | 28 | this.add({role: 'math', cmd: 'sum', integer: 'true'}, function (msg, respond) { 29 | var sum = Math.floor(msg.left) + Math.floor(msg.right) 30 | respond(null, {answer: sum}) 31 | }) 32 | 33 | // override role:math,cmd:sum with additional functionality 34 | this.add({role: 'math', cmd: 'sum'}, function (msg, respond) { 35 | // bail out early if there's a problem 36 | if (!isFinite(msg.left) || !isFinite(msg.right)) { 37 | return respond(new Error('Expected left and right to be numbers.')) 38 | } 39 | 40 | // call previous action function for role:math,cmd:sum 41 | this.prior(msg, (err, result) => { 42 | if (err) return respond(err) 43 | result.info = `${msg.left} + ${msg.right}` 44 | respond(null, result) 45 | }) 46 | }) 47 | 48 | this.wrap({role: 'math'}, function (msg, respond) { 49 | msg.left = Number(msg.left).valueOf() 50 | msg.right = Number(msg.right).valueOf() 51 | this.prior(msg, respond) 52 | }) 53 | 54 | this.add({role: 'math', cmd: 'operation-history'}, function product (msg, respond) { 55 | var operation = this.make$('operation') 56 | operation.list$({}, function (err, list) { 57 | if (err) { 58 | return respond(err) 59 | } 60 | return respond(null, {answer: list}) 61 | }) 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /problems/decorate/solution/solution.js: -------------------------------------------------------------------------------- 1 | module.exports = function math (options) { 2 | this.add({role: 'math', cmd: 'sum'}, function (msg, respond) { 3 | var operation = this.make$('operation') 4 | operation.cmd = 'sum' 5 | operation.left = msg.left 6 | operation.right = msg.right 7 | operation.save$(function (err, operation) { 8 | if (err) { 9 | return respond(err) 10 | } 11 | return respond(null, {answer: msg.left + msg.right}) 12 | }) 13 | }) 14 | 15 | this.add({role: 'math', cmd: 'product'}, function (msg, respond) { 16 | var operation = this.make$('operation') 17 | operation.cmd = 'product' 18 | operation.left = msg.left 19 | operation.right = msg.right 20 | operation.save$((err, operation) => { 21 | if (err) { 22 | return respond(err) 23 | } 24 | return respond(null, {answer: msg.left * msg.right}) 25 | }) 26 | }) 27 | 28 | this.add({role: 'math', cmd: 'sum', integer: 'true'}, function (msg, respond) { 29 | var sum = Math.floor(msg.left) + Math.floor(msg.right) 30 | respond(null, {answer: sum}) 31 | }) 32 | 33 | // override role:math,cmd:sum with additional functionality 34 | this.add({role: 'math', cmd: 'sum'}, function (msg, respond) { 35 | // bail out early if there's a problem 36 | if (!isFinite(msg.left) || !isFinite(msg.right)) { 37 | return respond(new Error('Expected left and right to be numbers.')) 38 | } 39 | 40 | // call previous action function for role:math,cmd:sum 41 | this.prior(msg, (err, result) => { 42 | if (err) return respond(err) 43 | result.info = `${msg.left} + ${msg.right}` 44 | respond(null, result) 45 | }) 46 | }) 47 | 48 | this.wrap({role: 'math'}, function (msg, respond) { 49 | msg.left = Number(msg.left).valueOf() 50 | msg.right = Number(msg.right).valueOf() 51 | this.prior(msg, respond) 52 | }) 53 | 54 | this.add({role: 'math', cmd: 'operation-history'}, function product (msg, respond) { 55 | var operation = this.make$('operation') 56 | operation.list$({}, function (err, list) { 57 | if (err) { 58 | return respond(err) 59 | } 60 | return respond(null, {answer: list}) 61 | }) 62 | }) 63 | 64 | this.decorate('availableOperations', () => { 65 | return ['sum', 'product', 'operation-history'] 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /problems/sum/problem.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | ## Introduction 4 | Seneca lets you build a Microservice system without worrying about how the 5 | parts fit together. 6 | 7 | Every Microservice in Seneca has to consume JSON messages. Unlike other 8 | systems, in Seneca the producer of such a JSON message does not specify 9 | which service should process the message. In Seneca, each Microservice 10 | specifies **patterns** to match messages against and Seneca makes sure 11 | that each message is processed by the best matching service. 12 | 13 | The pattern definition is just a list of key-value pairs that the top level 14 | properties of the JSON message document must match. 15 | 16 | This is an example service for a simple `hello` _command_ (cmd) with the 17 | _role_ `greetings` (so configuring the Microservice to answer to a 18 | `role:greetings,cmd:hello` pattern) 19 | 20 | ```javascript 21 | var seneca = require('seneca')() 22 | seneca.add( {role: 'greetings', cmd: 'hello'}, function( msg, respond ) { 23 | var hello = "Hello " + msg.name; 24 | respond( null, { answer: hello }); 25 | }); 26 | ``` 27 | In the above example the pattern has been expressed as `Object` but 28 | you can also specify patterns as `String` like this: 29 | 30 | ```javascript 31 | seneca.add( {role:'greetings', cmd:'hello'}, ...); 32 | ``` 33 | 34 | The challenge for this step is to build a simple Seneca service 35 | for the role `math` and command `sum`. 36 | The service has to calculate the sum of the `left` and `right` property of 37 | the message and return it in the form `{answer: sum}` 38 | For the purpose of this exercise, at the end of the solution you have to 39 | export Seneca. Note that **this is usually not necessary** since we can 40 | organize Microservices in plugins (see the plugin exercise about that). 41 | Also, to require Seneca, it must be installed in the local folder 42 | e.g. running `npm i seneca` 43 | 44 | ``` javascript 45 | var seneca = require('seneca')() 46 | 47 | seneca.add( // TODO 48 | ) 49 | 50 | module.exports = seneca 51 | ``` 52 | 53 | When you have completed your program, you can run it with: 54 | 55 | {bold}seneca-in-practice run program.js{/bold} 56 | 57 | And once you are happy that it is correct then run: 58 | 59 | {bold}seneca-in-practice verify program.js{/bold} 60 | 61 | And your submission will be verified for correctness. 62 | After you have a correct solution, run `{bold}{appname}{/bold}` again and 63 | select the next problem! 64 | -------------------------------------------------------------------------------- /problems/extend/problem.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | Patterns make it easy for you to extend your functionality. Instead of adding if 4 | statements and complex logic, you simply add more patterns. 5 | 6 | So for instance, thinking about hello example, we could extend the service to 7 | support the italian language. To do this, we can add a new property, `lang:it`, 8 | to the message object. Then we provide a new action for messages that have 9 | this property: 10 | 11 | ```javascript 12 | var seneca = require('seneca')() 13 | 14 | seneca.add({role:'greetings', cmd:'hello'}, function( msg, respond ) { 15 | var hello = "Hello " + msg.name; 16 | respond( null, { answer: hello }); 17 | }); 18 | 19 | seneca.add({role:'greetings', cmd:'hello', lang:'it'}, function( msg, respond ) { 20 | var hello = "Ciao " + msg.name; 21 | respond( null, { answer: hello }); 22 | }); 23 | 24 | ``` 25 | 26 | Now, this message: 27 | 28 | ```javascript 29 | {role: 'greetings', cmd: 'hello', name: 'Michele', lang: 'it'} 30 | 31 | ``` 32 | 33 | will produce: 34 | 35 | ```javascript 36 | {answer: 'Ciao Michele'} 37 | 38 | ``` 39 | 40 | One thing that we should keep in mind when extending pattern is that order is important. 41 | So for instance if we had this code: 42 | 43 | ```javascript 44 | var seneca = require('seneca')() 45 | 46 | seneca.add({role:'greetings', cmd:'hello'}, function( msg, respond ) { 47 | var hello = "Hello from first"; 48 | respond( null, { answer: hello }); 49 | }); 50 | 51 | seneca.add({role:'greetings', cmd:'hello'}, function( msg, respond ) { 52 | var hello = "Hello from second"; 53 | respond( null, { answer: hello }); 54 | }); 55 | 56 | ``` 57 | sending `{role:'greetings', cmd:'hello'}` we would actually receive : 58 | 59 | ```javascript 60 | {answer: 'Hello from second'} 61 | 62 | ``` 63 | 64 | That's because if you declare multiple pattern for the same message, only 65 | the last one will be the one invoked by Seneca. 66 | 67 | The goal of the exercise is to extend the sum plugin so that it supports the 68 | ability to force integer-only arithmetic. 69 | To do this, you add a new property, `integer:true`, to the message object and 70 | then apply Math.floor to both numbers. 71 | 72 | When you have completed your program, you can run it using with: 73 | 74 | {bold}{appname} run program.js{/bold} 75 | 76 | And once you are happy that it is correct then run: 77 | 78 | {bold}{appname} verify program.js{/bold} 79 | 80 | And your submission will be verified for correctness. 81 | After you have a correct solution, run `{bold}{appname}{/bold}` again and 82 | select the next problem! 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Seneca](http://senecajs.org/files/assets/seneca-logo.png) 2 | > **A [Seneca.js](http://senecajs.org) NodeSchool workshop** 3 | 4 | # Seneca in Practice 5 | 6 | [![travis][travis-badge]][travis-url] 7 | [![npm][npm-badge]][npm-url] 8 | [![standard][standard-badge]][standard-url] 9 | [![nearform][nearform-badge]][nearform-url] 10 | [![Join the chat at https://gitter.im/senecajs/seneca][gitter-badge]][gitter-url] 11 | 12 | > **In [_stoic_ philosophy](https://en.wikipedia.org/wiki/Stoicism), _study_ and _learning_ are important.** 13 | 14 | ![Enjoy seneca.js framework!](https://raw.githubusercontent.com/senecajs/seneca-in-practice/master/start.png) 15 | 16 | ### How to run it 17 | 18 | 1. Install [Node.js](http://nodejs.org/) 19 | 2. Install `seneca-in-practice` globally, e.g.: `npm install seneca-in-practice -g` 20 | 3. Run `seneca-in-practice` 21 | 22 | __`seneca-in-practice`__ will guide you through seneca.js framework to reach a greater knowledge of Microservice-based software architectures with NodeJS. 23 | 24 | ## Contributors 25 | 26 | __`seneca-in-practice`__ has been created by: 27 | 28 | 29 | 30 | 31 |
Marco PiracciniGitHub/marcopiracciniTwitter/@marcopiraccini
Michele CapraGitHub/piccoloaiutanteTwitter/@piccoloaiutante
32 | 33 | ## License 34 | 35 | **seneca-in-practice** is Copyright (c) 2013-2016 36 | seneca-in-practice contributors (listed above) and licenced under the MIT licence. All rights not explicitly granted in the MIT license are reserved. See the included [LICENSE.md](./LICENSE.md) file for more details. 37 | 38 | 39 | [travis-badge]: https://img.shields.io/travis/senecajs/seneca-in-practice.svg?style=flat-square 40 | [travis-url]: https://travis-ci.org/senecajs/seneca-in-practice 41 | [npm-badge]: https://img.shields.io/npm/v/seneca-in-practice.svg?style=flat-square 42 | [npm-url]: https://npmjs.org/package/seneca-in-practice 43 | [standard-badge]: https://img.shields.io/badge/code%20style-standard-blue.svg?style=flat-square 44 | [standard-url]: https://npmjs.org/package/standard 45 | [nearform-badge]: https://img.shields.io/badge/sponsored%20by-nearForm-red.svg?style=flat-square 46 | [nearform-url]: http://nearform.com 47 | [gitter-badge]: https://img.shields.io/gitter/room/senecajs/seneca.svg?style=flat-square 48 | [gitter-url]: https://gitter.im/senecajs/seneca 49 | -------------------------------------------------------------------------------- /problems/pin/problem.md: -------------------------------------------------------------------------------- 1 | Let's consider a situation in which we want to execute a set of operations 2 | for each invocation. On the Microservice side, this can be done using `wrap`, 3 | that is a method which matches a set of patterns and overrides all of them with 4 | the same action extension function. This is the same as calling `seneca.add` 5 | manually for each one. It takes the following two parameters: 6 | 7 | * `pin`: a pin is a pattern-matching pattern. 8 | * action: action extension function. 9 | 10 | Here an example of the "greetings" plugin which transform every name passed 11 | in uppercase: 12 | 13 | ```javascript 14 | module.exports = function greetings(options) { 15 | 16 | this.add({role:'greetings', cmd:'hello'}, function(args, done) { 17 | var hey = "Hello " + msg.name; 18 | respond(null, { answer: hey }); 19 | }) 20 | 21 | this.add({role:'greetings', cmd:'hey'}, function(args, done) { 22 | var hey = "Hey " + msg.name; 23 | respond(null, { answer: hey }); 24 | }) 25 | 26 | this.wrap({role:'greetings'}, function (msg, respond) { 27 | msg.name = msg.name.toUpperCase(); 28 | this.prior(msg, respond) 29 | }) 30 | 31 | } 32 | ``` 33 | 34 | In this case, the pin `role:greetings` matches the patterns `role:greetings,cmd:hello` 35 | and `role:greetings,cmd:hey` that are registered with Seneca. 36 | 37 | Note the `prior` call. Each time you override an action pattern, you get a prior. 38 | This prior may have its own prior from a previous definition of the action pattern. 39 | So this is the way of calling the overriden function. In this sense the `wrap` 40 | actually acts as a wrapper for a set of patterns/actions. 41 | 42 | Also `pin` can be used to specify the set of patterns that is associated with a client. 43 | ``` 44 | require('seneca')().use('greetings' ).listen({type:'tcp', pin:'role:greetings'}) 45 | ``` 46 | 47 | The goal of the exercise is to extend the math plugin using `wrap` with a `pin` 48 | to convert the passed parameters to Number, so that the plugin works also if 49 | the numbers to be added / multiplied are strings. 50 | Use `process.argv` params, so that the first param is `sum` or `product` strings 51 | and the other two are the numbers on which the operation must be applied. 52 | 53 | When you have completed your program, you can run it with: 54 | 55 | {bold}seneca-in-practice run program.js{/bold} 56 | 57 | And once you are happy that it is correct then run: 58 | 59 | {bold}seneca-in-practice verify program.js{/bold} 60 | 61 | And your submission will be verified for correctness. 62 | After you have a correct solution, run `{bold}{appname}{/bold}` again and 63 | select the next problem! 64 | -------------------------------------------------------------------------------- /problems/plugin/problem.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | When you use the Seneca framework, you write plugins all the time. This is an 4 | easy way to organize your action patterns. 5 | 6 | A Seneca plugin is just a function that gets passed an {italic}options{/italic} 7 | object, and has a Seneca instance as its {italic}this{/italic} variable. 8 | You then {italic}add{/italic} some action patterns in the body of the function, 9 | and you’re done. There is no callback. 10 | 11 | So an example of seneca plugin could be something like this: 12 | 13 | ```javascript 14 | var plugin = function(options) { 15 | 16 | this.add({role:'greetings', cmd:'hello'}, function(msg, respond) { 17 | var hello = "Hello " + msg.name; 18 | respond(null, {answer: hello }); 19 | }); 20 | } 21 | ``` 22 | 23 | It's possible to assign a name to a plugin. In order to do that you need return 24 | a string from the plugin definition function, like this: 25 | 26 | ```javascript 27 | var plugin = function(options) { 28 | 29 | this.add({role:'greetings', cmd:'hello'}, function(msg, respond) { 30 | var hello = "Hello " + msg.name; 31 | respond(null, {answer: hello }); 32 | }); 33 | 34 | return 'interaction' 35 | } 36 | ``` 37 | 38 | Once defined the above plugin could be loaded in this way: 39 | 40 | ```javascript 41 | var seneca = require('seneca')() 42 | 43 | var plugin = function(options) { ... } // as above 44 | 45 | seneca.use( plugin, {} ) 46 | seneca.ready(function(err) { 47 | seneca.act( {role:'greetings', cmd:'hello', name:'michele'}, console.log ) 48 | }) 49 | 50 | ``` 51 | 52 | The `seneca.use` is going to load the plugin: `seneca.ready` will provide the callback 53 | for when the plugin has been loaded. Any error that will happen while loading will be passed 54 | via the `err` param. One thing that we should be aware is that order matter when 55 | loading plugins. So for instance if you load two plugins declaring the same message pattern 56 | only the last one will be the one handling the message. Keep it in mind! 57 | 58 | The goal of the exercise is to write a plugin called `operations` that sums two 59 | numbers, as we did for the first step. For the purpose of this exercise, ***do not 60 | require Seneca directly*** just create a module that exports a function that defines 61 | the patterns, using `this.add` instead of `seneca.add` as we did in previous step. 62 | 63 | When you have completed your program, you can run it with: 64 | 65 | {bold}{appname} run program.js{/bold} 66 | 67 | And once you are happy with it, you can run: 68 | 69 | {bold}{appname} verify program.js{/bold} 70 | 71 | And your submission will be verified for correctness. 72 | After you have a correct solution, run `{bold}{appname}{/bold}` again and 73 | select the next problem! 74 | -------------------------------------------------------------------------------- /problems/sum/exercise.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const filecheck = require('workshopper-exercise/filecheck') 4 | const fs = require('fs') 5 | const path = require('path') 6 | const async = require('async') 7 | const _ = require('lodash') 8 | const {getRandomInt} = require('../utils') 9 | let exercise = require('workshopper-exercise')() 10 | 11 | // cleanup for both run and verify 12 | exercise.addCleanup((mode, passed, callback) => { /* Do nothing */ }) 13 | 14 | // checks that the submission file actually exists 15 | exercise = filecheck(exercise) 16 | 17 | // Test params 18 | let a, b 19 | 20 | /** 21 | * If mode === run, get the params from args 22 | */ 23 | exercise.addSetup(function (mode, cb) { 24 | a = getRandomInt(0, 100) 25 | b = getRandomInt(0, 100) 26 | this.solutionModule = require(getSolutionPath() + 'solution.js') 27 | this.submissionModule = require([process.cwd(), this.args[0]].join('/')) 28 | cb() 29 | }) 30 | 31 | /** 32 | * Processor 33 | */ 34 | exercise.addProcessor(function (mode, callback) { 35 | let solutionResult, submissionResult 36 | const that = this 37 | let pass = true 38 | async.series([ 39 | cb => { 40 | that.submissionModule.act({role: 'math', cmd: 'sum', left: a, right: b}, cb) 41 | }, 42 | cb => { 43 | if (mode === 'verify') { 44 | return that.solutionModule.act({role: 'math', cmd: 'sum', left: a, right: b}, cb) 45 | } 46 | cb() 47 | } 48 | ], (err, results) => { 49 | submissionResult = results[0] 50 | if (mode === 'run') { 51 | console.log(`Execution with left: ${a}, right: ${b} returned: ${JSON.stringify(submissionResult)}`) 52 | } else { 53 | solutionResult = results[1] 54 | if (!_.isEqual(solutionResult, submissionResult)) { 55 | exercise.emit('fail', `Expected result: ${JSON.stringify(solutionResult)}` + 56 | `, Actual result: ${JSON.stringify(submissionResult)}`) 57 | pass = false 58 | } else { 59 | exercise.emit('success', `Expected result: ${JSON.stringify(solutionResult)} ` + 60 | `Actual result: ${JSON.stringify(submissionResult)}`) 61 | pass = true 62 | } 63 | return callback(err, pass) 64 | } 65 | }) 66 | }) 67 | 68 | // Print out the suggested solution when the student passes. This is copied from 69 | // workshopper-exercise/execute because the rest of execute is not relevant to 70 | // the way this is tested. 71 | exercise.getSolutionFiles = function (callback) { 72 | var solutionDir = getSolutionPath() 73 | fs.readdir(solutionDir, function (err, list) { 74 | if (err) { 75 | return callback(err) 76 | } 77 | list = list 78 | .filter(function (f) { return (/\.js$/).test(f) }) 79 | .map(function (f) { return path.join(solutionDir, f) }) 80 | callback(null, list) 81 | }) 82 | } 83 | 84 | function getSolutionPath () { 85 | return path.join(exercise.dir, './solution/') 86 | } 87 | 88 | module.exports = exercise 89 | -------------------------------------------------------------------------------- /problems/transport/exercise.js: -------------------------------------------------------------------------------- 1 | const through2 = require('through2') 2 | const hyperquest = require('hyperquest') 3 | let exercise = require('workshopper-exercise')() 4 | const filecheck = require('workshopper-exercise/filecheck') 5 | const execute = require('workshopper-exercise/execute') 6 | const comparestdout = require('../comparestdout-filterlogs') 7 | const eos = require('end-of-stream') 8 | const {getRandomInt} = require('../utils') 9 | 10 | // checks that the submission file actually exists 11 | exercise = filecheck(exercise) 12 | 13 | // execute the solution and submission in parallel with spawn() 14 | exercise = execute(exercise) 15 | 16 | function rndport () { 17 | return 1024 + Math.floor(Math.random() * 64511) 18 | } 19 | 20 | let a, b, cmd 21 | 22 | /** 23 | * We use a random port to avoid collision with possible hanged process on 24 | * ports. 25 | */ 26 | exercise.addSetup(function (mode, callback) { 27 | this.submissionPort = rndport() 28 | this.solutionPort = this.submissionPort + 1 29 | this.submissionArgs = [this.submissionPort] 30 | a = getRandomInt() 31 | b = getRandomInt() 32 | cmd = 'sum' 33 | if (mode === 'verify') { 34 | this.solutionArgs = [this.solutionPort] 35 | } 36 | process.nextTick(callback) 37 | }) 38 | 39 | // add a processor for both run and verify calls, added *before* 40 | // the comparestdout processor so we can mess with the stdouts 41 | exercise.addProcessor(function (mode, callback) { 42 | this.submissionStdout.pipe(process.stdout) 43 | // replace stdout with our own streams 44 | this.submissionStdout = through2() 45 | 46 | if (mode === 'verify') { 47 | this.solutionStdout = through2() 48 | } 49 | 50 | // After 5 secs, try to query. We have to wait so much for Win... 51 | setTimeout(query.bind(this, mode, callback), 5000) 52 | console.log(`Invoking ${cmd} with random generated: ${a}, ${b}`) 53 | 54 | process.nextTick(function () { 55 | callback(null, true) 56 | }) 57 | }) 58 | 59 | // compare stdout of solution and submission 60 | exercise = comparestdout(exercise) 61 | 62 | // cleanup for both run and verify 63 | exercise.addCleanup(function (mode, passed, callback) { /* Do nothing */ }) 64 | 65 | // delayed for 500ms to wait for servers to start so we can start 66 | // playing with them 67 | function query (mode, callback) { 68 | // Should we pass the port? 69 | function connect (port, stream) { 70 | var input = through2() 71 | 72 | var url = `http://127.0.0.1:${port}/act?role=math&cmd=${cmd}&left=${a}&right=${b}` 73 | eos(input, function () { 74 | // Sena CTRL-C after 500 millis 75 | setTimeout(function () { 76 | console.log('\n') 77 | process.kill(process.pid, 'SIGINT') 78 | }, 500) 79 | }) 80 | 81 | input.pipe(hyperquest.post(url)).pipe(stream) 82 | input.end() 83 | } 84 | 85 | connect(this.submissionPort, this.submissionStdout) 86 | if (mode === 'verify') { 87 | connect(this.solutionPort, this.solutionStdout) 88 | } 89 | } 90 | 91 | module.exports = exercise 92 | -------------------------------------------------------------------------------- /problems/roles/exercise.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const filecheck = require('workshopper-exercise/filecheck') 4 | const fs = require('fs') 5 | const path = require('path') 6 | const async = require('async') 7 | const _ = require('lodash') 8 | const {getRandomInt} = require('../utils') 9 | let exercise = require('workshopper-exercise')() 10 | 11 | // cleanup for both run and verify 12 | exercise.addCleanup((mode, passed, callback) => { /* Do nothing */ }) 13 | 14 | // checks that the submission file actually exists 15 | exercise = filecheck(exercise) 16 | 17 | // Test params 18 | let a, b 19 | 20 | /** 21 | * If mode === run, get the params from args 22 | */ 23 | exercise.addSetup(function (mode, cb) { 24 | a = getRandomInt(0, 100) 25 | b = getRandomInt(0, 100) 26 | this.solutionModule = require(getSolutionPath() + 'solution.js') 27 | this.submissionModule = require([process.cwd(), this.args[0]].join('/')) 28 | cb() 29 | }) 30 | 31 | /** 32 | * Processor 33 | */ 34 | exercise.addProcessor(function (mode, callback) { 35 | let solutionResult, submissionResult 36 | const that = this 37 | let pass = true 38 | var seneca = require('seneca')() 39 | async.series([ 40 | cb => { 41 | return seneca.use(that.submissionModule).act({role: 'math', cmd: 'product', left: a, right: b}, cb) 42 | }, 43 | cb => { 44 | if (mode === 'verify') { 45 | return seneca.use(that.solutionModule).act({role: 'math', cmd: 'product', left: a, right: b}, cb) 46 | } 47 | cb() 48 | } 49 | ], (err, results) => { 50 | submissionResult = results[0] 51 | if (mode === 'run') { 52 | console.log(`Execution with left: ${a}, right: ${b} returned: ${JSON.stringify(submissionResult)}`) 53 | } else { 54 | solutionResult = results[1] 55 | if (!_.isEqual(solutionResult, submissionResult)) { 56 | exercise.emit('fail', `Expected result: ${JSON.stringify(solutionResult)}` + 57 | `, Actual result: ${JSON.stringify(submissionResult)}`) 58 | pass = false 59 | } else { 60 | exercise.emit('success', `Expected result: ${JSON.stringify(solutionResult)} ` + 61 | `Actual result: ${JSON.stringify(submissionResult)}`) 62 | pass = true 63 | } 64 | return callback(err, pass) 65 | } 66 | }) 67 | }) 68 | 69 | // Print out the suggested solution when the student passes. This is copied from 70 | // workshopper-exercise/execute because the rest of execute is not relevant to 71 | // the way this is tested. 72 | exercise.getSolutionFiles = function (callback) { 73 | var solutionDir = getSolutionPath() 74 | fs.readdir(solutionDir, function (err, list) { 75 | if (err) { 76 | return callback(err) 77 | } 78 | list = list 79 | .filter(function (f) { return (/\.js$/).test(f) }) 80 | .map(function (f) { return path.join(solutionDir, f) }) 81 | callback(null, list) 82 | }) 83 | } 84 | 85 | function getSolutionPath () { 86 | return path.join(exercise.dir, './solution/') 87 | } 88 | 89 | module.exports = exercise 90 | -------------------------------------------------------------------------------- /problems/extend/exercise.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const filecheck = require('workshopper-exercise/filecheck') 4 | const fs = require('fs') 5 | const path = require('path') 6 | const async = require('async') 7 | const _ = require('lodash') 8 | const {getRandomFloat} = require('../utils') 9 | let exercise = require('workshopper-exercise')() 10 | 11 | // cleanup for both run and verify 12 | exercise.addCleanup((mode, passed, callback) => { /* Do nothing */ }) 13 | 14 | // checks that the submission file actually exists 15 | exercise = filecheck(exercise) 16 | 17 | // Test params 18 | let a, b 19 | 20 | /** 21 | * If mode === run, get the params from args 22 | */ 23 | exercise.addSetup(function (mode, cb) { 24 | a = getRandomFloat(0, 100) 25 | b = getRandomFloat(0, 100) 26 | this.solutionModule = require(getSolutionPath() + 'solution.js') 27 | this.submissionModule = require([process.cwd(), this.args[0]].join('/')) 28 | cb() 29 | }) 30 | 31 | /** 32 | * Processor 33 | */ 34 | exercise.addProcessor(function (mode, callback) { 35 | let solutionResult, submissionResult 36 | const that = this 37 | let pass = true 38 | var seneca = require('seneca')() 39 | async.series([ 40 | cb => { 41 | return seneca.use(that.submissionModule).act({role: 'math', cmd: 'sum', integer: true, left: a, right: b}, cb) 42 | }, 43 | cb => { 44 | if (mode === 'verify') { 45 | return seneca.use(that.solutionModule).act({role: 'math', cmd: 'sum', integer: true, left: a, right: b}, cb) 46 | } 47 | cb() 48 | } 49 | ], (err, results) => { 50 | submissionResult = results[0] 51 | if (mode === 'run') { 52 | console.log(`Execution with left: ${a}, right: ${b} returned: ${JSON.stringify(submissionResult)}`) 53 | } else { 54 | solutionResult = results[1] 55 | if (!_.isEqual(solutionResult, submissionResult)) { 56 | exercise.emit('fail', `Expected result: ${JSON.stringify(solutionResult)}` + 57 | `, Actual result: ${JSON.stringify(submissionResult)}`) 58 | pass = false 59 | } else { 60 | exercise.emit('success', `Expected result: ${JSON.stringify(solutionResult)} ` + 61 | `Actual result: ${JSON.stringify(submissionResult)}`) 62 | pass = true 63 | } 64 | return callback(err, pass) 65 | } 66 | }) 67 | }) 68 | 69 | // Print out the suggested solution when the student passes. This is copied from 70 | // workshopper-exercise/execute because the rest of execute is not relevant to 71 | // the way this is tested. 72 | exercise.getSolutionFiles = function (callback) { 73 | var solutionDir = getSolutionPath() 74 | fs.readdir(solutionDir, function (err, list) { 75 | if (err) { 76 | return callback(err) 77 | } 78 | list = list 79 | .filter(function (f) { return (/\.js$/).test(f) }) 80 | .map(function (f) { return path.join(solutionDir, f) }) 81 | callback(null, list) 82 | }) 83 | } 84 | 85 | function getSolutionPath () { 86 | return path.join(exercise.dir, './solution/') 87 | } 88 | 89 | module.exports = exercise 90 | -------------------------------------------------------------------------------- /problems/mem_store/problem.md: -------------------------------------------------------------------------------- 1 | At some time we'll need to persist data. Also for Data Storage 2 | Seneca follows the pattern matching approach. 3 | 4 | Seneca provides a simple data abstraction layer (“ORM”), based on the following 5 | operations: 6 | 7 | * load: load an entity by identifier 8 | * save: create or update (if you provide an identifier) an entity 9 | * list: list entities matching a simple query 10 | * remove: delete an entity by identifier 11 | 12 | The patterns are: 13 | 14 | * load: `role:entity,cmd:load,name:` 15 | * save: `role:entity,cmd:save,name:` 16 | * list: `role:entity,cmd:list,name:` 17 | * remove: `role:entity,cmd:remove,name:` 18 | 19 | So, it's very easy: a store is a plugin that provides implementations of these 20 | patterns. 21 | 22 | Seneca comes with a built-in data persistence plugin: `mem-store`. 23 | This plugin just stores the data in-memory. It can be used for rapid testing 24 | or prototyping. Since all data operations go via the same set of messages, 25 | you can very easily swap databases, at any time an use other stores 26 | if needed. 27 | 28 | Using the data persistence patterns directly can become tedious, so Seneca also 29 | provides a more familiar ActiveRecord-style interface. 30 | To create a record object, you call the `seneca.make` method. 31 | The record object has methods `load$`, `save$`, `list$` and `remove$` 32 | (the trailing $ avoids clashes with data fields). 33 | The data fields are just the object properties. 34 | See 35 | [http://senecajs.org/docs/tutorials/understanding-data-entities.html] 36 | for full documentation. 37 | 38 | Here an example on how to use the store: 39 | 40 | ```javascript 41 | var seneca = require('seneca')() 42 | seneca.use('basic') 43 | seneca.use('entity') 44 | 45 | var product = seneca.make('product') 46 | product.name = 'Apple' 47 | product.price = 1.99 48 | 49 | // sends role:entity,cmd:save,name:product messsage 50 | product.save$( console.log ) 51 | ``` 52 | 53 | The goal of the exercise is to extend the math plugin from the last 54 | exercise to "store" the operations done by the plugin using these objects: 55 | 56 | `{cmd: 'sum', left: 4. right: 5}` 57 | 58 | Then add to the plugin a `operation-history` command that returns the list of 59 | executed operations so far. For this, please look that `list$` first parameter is a 60 | "query" object, that for this exercise can be left empty (`{}`), e.g. 61 | ```javascript 62 | product.list$({}, function(err, products){ ... }) 63 | ``` 64 | 65 | One last note: with Seneca 3, entity is enabled installing the package: 66 | ``` 67 | npm install --save seneca-entity 68 | ``` 69 | ...and then activating it, like in the example above: 70 | 71 | ```javascript 72 | seneca.use('basic') 73 | seneca.use('entity') 74 | ``` 75 | In this exercise, this is not necessary, since we are adding the "store" 76 | mechanism to a plugin, without instantiate directly Seneca. 77 | 78 | When you have completed your program, you can run it with: 79 | 80 | {bold}seneca-in-practice run program.js{/bold} 81 | 82 | And once you are happy that it is correct then run: 83 | 84 | {bold}seneca-in-practice verify program.js{/bold} 85 | 86 | And your submission will be verified for correctness. 87 | After you have a correct solution, run `{bold}{appname}{/bold}` again and 88 | select the next problem! 89 | -------------------------------------------------------------------------------- /problems/override/problem.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | The action patterns that we define are unique. They can trigger only one 4 | function. The patterns resolve using the following rules: 5 | 6 | * More properties win. 7 | * If the patterns have the same number of properties, they are matched 8 | in alphabetical order. 9 | * If you declare two times the same pattern, the latest will be the one 10 | used in a instance, as there can't be two equal patterns in it. 11 | 12 | These rules are designed to be simple so that you can run them in your head. 13 | It’s very easy to understand which pattern will trigger which action function. 14 | 15 | It is sometimes useful to have a way of enhancing the behavior of an action 16 | without rewriting it fully. For example, we might want to perform custom 17 | validation of the message properties, capture message statistics, 18 | add additional information to action results, or throttle message flow rates. 19 | 20 | So for instance, thinking about hello example, we could override the service 21 | to save statistics about the names. 22 | 23 | 24 | ```javascript 25 | var seneca = require('seneca')() 26 | var names = {} 27 | 28 | seneca.add({role:'greetings', cmd:'hello'}, function( msg, respond ) { 29 | var hello = "Hello " + msg.name; 30 | respond( null, { answer: hello }); 31 | }); 32 | 33 | seneca.add({role:'greetings', cmd:'hello'}, function( msg, respond ) { 34 | 35 | // saving names statistics in names array 36 | names[msg.name]= names[msg.name] ? names[msg.name] + 1 : 1 37 | 38 | // call previous action function for role:greetings,cmd:hello 39 | this.prior({ 40 | role: 'greetings', 41 | cmd: 'hello', 42 | name: msg.name 43 | }, function( err, result ) { 44 | if( err ) return respond( err ) 45 | 46 | respond( null, { answer: result.answer }); 47 | }) 48 | }); 49 | 50 | ``` 51 | 52 | The Seneca instance provided to an action function via the the `this` context 53 | variable has a special `prior` method that calls the previous action definition 54 | for the current action pattern. 55 | 56 | The prior function has two parameters: 57 | 58 | * msg: the msg object, which you may have modified. 59 | * response_callback: a callback function where you can modify the result. 60 | 61 | The goal of the exercise is to override the sum plugin with the "addition" action 62 | expecting the left and right properties to be finite numbers. 63 | If one of the number is not a finite number the plugin should throw a 64 | `new Error('Expected left and right to be numbers.')`. 65 | You could use `isFinite` global function to do validate numbers (dont' use 66 | `Number.isFinite` since it doesn't do the type conversion needed on args). 67 | In order to verify that the `prior` call has been done, we would like to add 68 | logging info to the result. So add a `info` property to the result that will 69 | concat numbers in this way: `msg.left + ' + ' + msg.right`. Override the `sum` 70 | already defined and not the sum extended to force integer-only arithmetic. 71 | 72 | When you have completed your program, you can run it with: 73 | 74 | {bold}seneca-in-practice run program.js{/bold} 75 | 76 | And once you are happy that it is correct then run: 77 | 78 | {bold}seneca-in-practice verify program.js{/bold} 79 | 80 | And your submission will be verified for correctness. 81 | After you have a correct solution, run `{bold}{appname}{/bold}` again and 82 | select the next problem! 83 | -------------------------------------------------------------------------------- /problems/comparestdout-filterlogs.js: -------------------------------------------------------------------------------- 1 | // Compare stdout that ignores seneca log. 2 | // It's from 'workshopper-exercise/comparestdout' 3 | // Customized to skip seneca logs. 4 | 5 | const chalk = require('chalk') 6 | const split = require('split2') 7 | const tuple = require('tuple-stream') 8 | const through2 = require('through2') 9 | const wcstring = require('wcstring') 10 | const filter = require('through2-filter') 11 | 12 | // Works only with seneca3.* logs (JSONs) and with chunk which is a line (so -for instance- 13 | // after a split) 14 | const checkSenecaLogs = chunk => { 15 | try { 16 | const line = JSON.parse(chunk) 17 | if (line.level) { // we assume seneca log has this prop 18 | return false 19 | } 20 | } catch (e) {} 21 | return true 22 | } 23 | 24 | function comparestdout (exercise) { 25 | return exercise.addProcessor(processor) 26 | } 27 | 28 | function colourfn (type) { 29 | return type === 'PASS' ? chalk.green : chalk.red 30 | } 31 | 32 | function repeat (ch, sz) { 33 | return new Array(sz + 1).join(ch) 34 | } 35 | 36 | function center (s, sz) { 37 | var sps = Math.floor((sz - wcstring(s).size()) / 2) 38 | var sp = repeat(' ', sps) 39 | return sp + s + sp + (sp.length !== sps ? ' ' : '') 40 | } 41 | 42 | function wrap (s_, n) { 43 | var s = String(s_) 44 | return s + repeat(' ', Math.max(0, n + 1 - wcstring(s).size())) 45 | } 46 | 47 | function processor (mode, callback) { 48 | this.submissionChild.stderr.pipe(process.stderr) 49 | 50 | if (mode === 'run' || !this.solutionChild) { 51 | // no compare needed 52 | this.submissionStdout.pipe(process.stdout) 53 | return this.on('executeEnd', function () { 54 | callback(null, true) 55 | }) 56 | } 57 | 58 | var equal = true 59 | var line = 1 60 | var outputStream 61 | 62 | function transform (chunk, enc, callback) { 63 | if (line === 1) { 64 | outputStream.push('\n' + this.__('compare.title') + '\n\n') 65 | if (!this.longCompareOutput) { 66 | outputStream.push(chalk.yellow(center(this.__('compare.actual'), 40) + center(this.__('compare.expected'), 40) + '\n')) 67 | } 68 | outputStream.push(chalk.yellow(repeat('\u2500', 80)) + '\n\n') 69 | } 70 | 71 | // If seneca log, skip the line 72 | const eq = chunk[0] === chunk[1] 73 | const lineStr = wrap(String(line++ + '.'), 3) 74 | const _colourfn = colourfn(eq ? 'PASS' : 'FAIL') 75 | const actual = chunk[0] === null ? '' : JSON.stringify(chunk[0]) 76 | const expected = chunk[1] === null ? '' : JSON.stringify(chunk[1]) 77 | let output 78 | 79 | equal = equal && eq 80 | 81 | if (this.longCompareOutput) { 82 | output = 83 | chalk.yellow(wrap(lineStr + this.__('compare.actual') + ':', 14)) + 84 | _colourfn(actual) + '\n' + 85 | chalk.yellow(wrap(lineStr + this.__('compare.expected') + ':', 14)) + 86 | _colourfn(expected) + '\n\n' 87 | } else { 88 | output = ' ' + 89 | _colourfn(wrap(actual, 34)) + 90 | _colourfn(chalk.bold(eq ? ' == ' : ' != ')) + ' ' + 91 | _colourfn(wrap(expected, 34)) + '\n' 92 | } 93 | callback(null, output) 94 | } 95 | 96 | function flush (_callback) { 97 | outputStream.push('\n' + chalk.yellow(repeat('\u2500', 80)) + '\n\n') 98 | this.emit(equal ? 'pass' : 'fail', this.__(equal ? 'compare.pass' : 'compare.fail')) 99 | _callback(null) 100 | callback(null, equal) // process() callback 101 | } 102 | 103 | outputStream = through2.obj(transform.bind(this), flush.bind(this)) 104 | 105 | tuple(this.submissionStdout.pipe(split()).pipe(filter.obj(checkSenecaLogs)), 106 | this.solutionStdout.pipe(split()).pipe(filter.obj(checkSenecaLogs))) 107 | .pipe(outputStream) 108 | .pipe(process.stdout) 109 | } 110 | 111 | module.exports = comparestdout 112 | --------------------------------------------------------------------------------