diff --git a/.vscode/launch.json b/.vscode/launch.json index 5c075a55..2253084f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,6 +2,12 @@ { "version": "0.2.0", "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Current Opened File", + "program": "${file}" + }, { "type": "node", "request": "launch", diff --git a/01 Setup.md b/01 Setup.md index a1a4e284..7f94b616 100644 --- a/01 Setup.md +++ b/01 Setup.md @@ -3,7 +3,9 @@ In this worksheet you will learn how to configure your work environment using VS Code. You should start by installing **Visual Studio Code** (NOT VISUAL STUDIO!) from the [website](https://code.visualstudio.com), note that it might already be installed. If you are using a Coventry University computer and the software is not installed you can do this using AppsAnywhere. -If you are using Windows 10 you will also need to install [Git](https://git-scm.com/download/win), this may already be installed on a Coventry University computer. +If you are using Windows 10 you will also need to install [Git](https://git-scm.com/download/win), this may already be installed on a Coventry University computer. If you are on MacOS you may already have it installed as it comes with the XCode IDE but if you have recently updated your OS you may need to run the `xcode-select --install` command to update the **Xcode Command-line Tools** but if you don't want to install XCode you can install git using [HomeBrew](http://brew.sh/) using the `brew install git` command. If you are running Ubuntu you can install it using the `sudo apt install git` command. + +DO NOT INSTALL THE GIT GUI TOOLS! Visual Studio Code comes with an integrated **Terminal** that can be used instead of the standard _Command Prompt_ or _Terminal_. If you are using Linux or MacOS this will give you a Bash prompt however on Windows 10 it defaults to the _Command Prompt_ and will need to be changed to the _Bash Shell_. diff --git a/06 Code Quality.md b/06 Code Quality.md index 9e99995f..a65ca43d 100644 --- a/06 Code Quality.md +++ b/06 Code Quality.md @@ -135,7 +135,16 @@ The callbacks are already nested 3 deep. To test your knowledge of deeply nested 1. modify the script to ask for the currency to convert to and display only the one conversion rate. 2. instead of printing the exchange rate, ask for the amount to be converted and them return the equivalent in the chosen currency -3. use the [OpenExchangeRates](https://openexchangerates.org/api/currencies.json) API to display the full name of the chosen currency. +3. The `currencies.json` file contains a map between the currency code and the country name. Load this file into the script using the [`fs`](https://nodejs.org/api/fs.html) module, convert to a JavaScript object using the [`JSON`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON) object and use it to display the full name of the chosen currency. Below is some code to get you started. + +You will need to import the `fs` module and use the `fs.readFile()` function which takes a callback! + +```javascript +fs.readFile('currencies.json', 'utf8', (err, contents) => { + if(err) console.log('you need to handle this properly') + console.log(contents) +}) +``` Even though the script is still simple you are probably already getting in a tangle! Imagine a more complex script with conditions, it would quickly get out of hand and become practically impossible to debug. @@ -155,6 +164,7 @@ A promise represents the result of an asynchronous operation. As such it can be Promises are created using the `new` keyword. This function is called immediately with two arguments. The first argument resolves the promise and the second one rejects it. Once the appropriate argument is called the promise state changes. ```javascript +const url = 'https://api.exchangeratesapi.io/latest?base=GBP' const getData = url => new Promise( (resolve, reject) => { request(url, (err, res, body) => { if (err) reject(new Error('invalid API call')) @@ -173,7 +183,7 @@ As you can see it it simple to wrap any async callbacks in promises but how are To use promises we need a mechanism that gets triggered as soon as a promise changes state. A promise includes a `then()` method which gets called if the state changes to _fulfilled_ and a `catch()` method that gets called if the state changes to _rejected_. ```javascript -const aPromise = getData('http://api.fixer.io/latest?base=GBP') +const aPromise = getData('https://api.exchangeratesapi.io/latest?base=GBP') aPromise.then( data => console.log(data)) @@ -185,7 +195,7 @@ If the state of the promise changes to _rejected_, the `catch()` method is calle This code can be written in a more concise way by _chaining_ the promise methods. ```javascript -getData('http://api.fixer.io/latest?base=GBP') +getData('https://api.exchangeratesapi.io/latest?base=GBP') .then( data => console.log(data)) .catch( err => console.error(`error: ${err.message}`)) ``` @@ -214,7 +224,7 @@ const exit = () => new Promise( () => { process.exit() }) -getData('http://api.fixer.io/latest?base=GBP') +getData('https://api.exchangeratesapi.io/latest?base=GBP') .then( data => printObject(data)) .then( () => exit()) .catch(err => console.error(`error: ${err.message}`)) @@ -229,7 +239,7 @@ Despite the code in the `printObject` promise being _synchronous_ it is better t If a promise only takes a single parameter and this matches the data passed back when the previous promise _fulfills_ there is a more concise way to write this. ```javascript -getData('http://api.fixer.io/latest?base=GBP') +getData('https://api.exchangeratesapi.io/latest?base=GBP') .then(printObject) .then(exit) .catch(err => console.error(`error: ${err.message}`)) @@ -240,7 +250,7 @@ getData('http://api.fixer.io/latest?base=GBP') There are some situations where you can't simply pass the output from one promise to the input of the next one. Sometimes you need to store data for use further down the promise chain. This can be achieved by storing the data in the `this` object. ```javascript -getData('http://api.fixer.io/latest?base=GBP') +getData('https://api.exchangeratesapi.io/latest?base=GBP') .then( data => this.jsonData = data) .then( () => printObject(this.jsonData)) .then(exit) @@ -256,9 +266,9 @@ Run the `promises.js` script. Its functionality should be familiar to the `curre Study the code carefully. Notice that it defines 5 promises and chains them together. You are going to extend the functionality by defining some additional promises and adding them to the promise chain. -1. modify the script to ask for the currency to convert to and display only the one conversion rate. -2. instead of printing the exchange rate, ask for the amount to be converted and them return the equivalent in the chosen currency -3. use the [OpenExchangeRates](https://openexchangerates.org/api/currencies.json) API to display the full name of the chosen currency +1. Modify the script to ask for the currency to convert to and display only the one conversion rate. +2. Instead of printing the exchange rate, ask for the amount to be converted and them return the equivalent in the chosen currency. +3. The `currencies.json` file contains a map between the currency code and the country name. Load this file into the script using the [`fs`](https://nodejs.org/api/fs.html) module, convert to a JavaScript object using the [`JSON`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON) object and use it to display the full name of the chosen currency. ### 6.5 Executing Code Concurrently @@ -281,7 +291,7 @@ const dataArray = ['USD', 'EUR'] const promiseArray = [] dataArray.forEach( curr => { promiseArray.push(new Promise( (resolve, reject) => { - const url = `http://api.fixer.io/latest?base=GBP&symbols=${curr}` + const url = `https://api.exchangeratesapi.io/latest?base=GBP&symbols=${curr}` request.get(url, (err, res, body) => { if (err) reject(new Error(`could not get conversion rate for ${curr}`)) resolve(body) @@ -347,7 +357,7 @@ const printObject = data => new Promise( resolve => { async function main() { try { - const data = await getData('http://api.fixer.io/latest?base=GBP') + const data = await getData('https://api.exchangeratesapi.io/latest?base=GBP') await printObject(data) process.exit() } catch (err) { @@ -386,8 +396,8 @@ both `printObjectPromise` and `printObjectAsync` behave in exactly the same mann Run the `asyncFunctions.js` script, located in the otherScripts folder. Note that it works in the same way as the previous ones. Open the script and study it carefully. -1. modify the script to ask for the currency to convert to and display only the one conversion rate. -2. instead of printing the exchange rate, ask for the amount to be converted and them return the equivalent in the chosen currency -3. use the [OpenExchangeRates](https://openexchangerates.org/api/currencies.json) API to display the full name of the chosen currency -4. rewrite the `printObject` promise as an _async function_. -5. rewrite another promise as an _async function_. +1. Modify the script to ask for the currency to convert to and display only the one conversion rate. +2. Instead of printing the exchange rate, ask for the amount to be converted and them return the equivalent in the chosen currency +3. The `currencies.json` file contains a map between the currency code and the country name. Load this file into the script using the [`fs`](https://nodejs.org/api/fs.html) module, convert to a JavaScript object using the [`JSON`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON) object and use it to display the full name of the chosen currency. +4. Rewrite the `printObject` promise as an _async function_. +5. Rewrite another promise as an _async function_. diff --git a/exercises/06_code_quality/asyncFunctions.js b/exercises/06_code_quality/asyncFunctions.js new file mode 100644 index 00000000..35f1e2b4 --- /dev/null +++ b/exercises/06_code_quality/asyncFunctions.js @@ -0,0 +1,57 @@ + +'use strict' + +const request = require('request') +const readline = require('readline') + +const baseURL = 'https://api.exchangeratesapi.io/latest' + +async function main() { + try { + const base = await getInput('enter base currency') + await checkValidCurrencyCode(base) + const data = await getData(`${baseURL}?base=${base}`) + await printObject(data) + const to = await getInput('convert to') + console.log(to) + process.exit() + } catch (err) { + console.log(`error: ${err.message}`) + } +} + +const getInput = prompt => new Promise(resolve => { + const read = readline.createInterface({ input: process.stdin, output: process.stdout }) + read.question(`${prompt}: `, value => { + console.log(`You entered ${value}`) + read.close() + resolve(value) + }) +}) + +const checkValidCurrencyCode = code => new Promise( (resolve, reject) => { + code = code.trim() + request(baseURL, (err, res, body) => { + if (err) reject(new Error('invalid API call')) + const rates = JSON.parse(body).rates + if (!rates.hasOwnProperty(code)) it.throw(new Error(`invalid currency code ${code}`)) + resolve() + }) +}) + +const getData = url => new Promise( (resolve, reject) => { + request(url, (err, res, body) => { + if (err) reject(new Error('invalid API call')) + resolve(body) + }) +}) + +const printObject = data => new Promise( (resolve) => { + const indent = 2 + data = JSON.parse(data) + const str = JSON.stringify(data, null, indent) + console.log(str) + resolve() +}) + +main() diff --git a/exercises/06_code_quality/currencies.json b/exercises/06_code_quality/currencies.json new file mode 100644 index 00000000..08407277 --- /dev/null +++ b/exercises/06_code_quality/currencies.json @@ -0,0 +1,173 @@ +{ + "AED": "United Arab Emirates Dirham", + "AFN": "Afghan Afghani", + "ALL": "Albanian Lek", + "AMD": "Armenian Dram", + "ANG": "Netherlands Antillean Guilder", + "AOA": "Angolan Kwanza", + "ARS": "Argentine Peso", + "AUD": "Australian Dollar", + "AWG": "Aruban Florin", + "AZN": "Azerbaijani Manat", + "BAM": "Bosnia-Herzegovina Convertible Mark", + "BBD": "Barbadian Dollar", + "BDT": "Bangladeshi Taka", + "BGN": "Bulgarian Lev", + "BHD": "Bahraini Dinar", + "BIF": "Burundian Franc", + "BMD": "Bermudan Dollar", + "BND": "Brunei Dollar", + "BOB": "Bolivian Boliviano", + "BRL": "Brazilian Real", + "BSD": "Bahamian Dollar", + "BTC": "Bitcoin", + "BTN": "Bhutanese Ngultrum", + "BWP": "Botswanan Pula", + "BYN": "Belarusian Ruble", + "BZD": "Belize Dollar", + "CAD": "Canadian Dollar", + "CDF": "Congolese Franc", + "CHF": "Swiss Franc", + "CLF": "Chilean Unit of Account (UF)", + "CLP": "Chilean Peso", + "CNH": "Chinese Yuan (Offshore)", + "CNY": "Chinese Yuan", + "COP": "Colombian Peso", + "CRC": "Costa Rican Colón", + "CUC": "Cuban Convertible Peso", + "CUP": "Cuban Peso", + "CVE": "Cape Verdean Escudo", + "CZK": "Czech Republic Koruna", + "DJF": "Djiboutian Franc", + "DKK": "Danish Krone", + "DOP": "Dominican Peso", + "DZD": "Algerian Dinar", + "EGP": "Egyptian Pound", + "ERN": "Eritrean Nakfa", + "ETB": "Ethiopian Birr", + "EUR": "Euro", + "FJD": "Fijian Dollar", + "FKP": "Falkland Islands Pound", + "GBP": "British Pound Sterling", + "GEL": "Georgian Lari", + "GGP": "Guernsey Pound", + "GHS": "Ghanaian Cedi", + "GIP": "Gibraltar Pound", + "GMD": "Gambian Dalasi", + "GNF": "Guinean Franc", + "GTQ": "Guatemalan Quetzal", + "GYD": "Guyanaese Dollar", + "HKD": "Hong Kong Dollar", + "HNL": "Honduran Lempira", + "HRK": "Croatian Kuna", + "HTG": "Haitian Gourde", + "HUF": "Hungarian Forint", + "IDR": "Indonesian Rupiah", + "ILS": "Israeli New Sheqel", + "IMP": "Manx pound", + "INR": "Indian Rupee", + "IQD": "Iraqi Dinar", + "IRR": "Iranian Rial", + "ISK": "Icelandic Króna", + "JEP": "Jersey Pound", + "JMD": "Jamaican Dollar", + "JOD": "Jordanian Dinar", + "JPY": "Japanese Yen", + "KES": "Kenyan Shilling", + "KGS": "Kyrgystani Som", + "KHR": "Cambodian Riel", + "KMF": "Comorian Franc", + "KPW": "North Korean Won", + "KRW": "South Korean Won", + "KWD": "Kuwaiti Dinar", + "KYD": "Cayman Islands Dollar", + "KZT": "Kazakhstani Tenge", + "LAK": "Laotian Kip", + "LBP": "Lebanese Pound", + "LKR": "Sri Lankan Rupee", + "LRD": "Liberian Dollar", + "LSL": "Lesotho Loti", + "LYD": "Libyan Dinar", + "MAD": "Moroccan Dirham", + "MDL": "Moldovan Leu", + "MGA": "Malagasy Ariary", + "MKD": "Macedonian Denar", + "MMK": "Myanma Kyat", + "MNT": "Mongolian Tugrik", + "MOP": "Macanese Pataca", + "MRO": "Mauritanian Ouguiya (pre-2018)", + "MRU": "Mauritanian Ouguiya", + "MUR": "Mauritian Rupee", + "MVR": "Maldivian Rufiyaa", + "MWK": "Malawian Kwacha", + "MXN": "Mexican Peso", + "MYR": "Malaysian Ringgit", + "MZN": "Mozambican Metical", + "NAD": "Namibian Dollar", + "NGN": "Nigerian Naira", + "NIO": "Nicaraguan Córdoba", + "NOK": "Norwegian Krone", + "NPR": "Nepalese Rupee", + "NZD": "New Zealand Dollar", + "OMR": "Omani Rial", + "PAB": "Panamanian Balboa", + "PEN": "Peruvian Nuevo Sol", + "PGK": "Papua New Guinean Kina", + "PHP": "Philippine Peso", + "PKR": "Pakistani Rupee", + "PLN": "Polish Zloty", + "PYG": "Paraguayan Guarani", + "QAR": "Qatari Rial", + "RON": "Romanian Leu", + "RSD": "Serbian Dinar", + "RUB": "Russian Ruble", + "RWF": "Rwandan Franc", + "SAR": "Saudi Riyal", + "SBD": "Solomon Islands Dollar", + "SCR": "Seychellois Rupee", + "SDG": "Sudanese Pound", + "SEK": "Swedish Krona", + "SGD": "Singapore Dollar", + "SHP": "Saint Helena Pound", + "SLL": "Sierra Leonean Leone", + "SOS": "Somali Shilling", + "SRD": "Surinamese Dollar", + "SSP": "South Sudanese Pound", + "STD": "São Tomé and Príncipe Dobra (pre-2018)", + "STN": "São Tomé and Príncipe Dobra", + "SVC": "Salvadoran Colón", + "SYP": "Syrian Pound", + "SZL": "Swazi Lilangeni", + "THB": "Thai Baht", + "TJS": "Tajikistani Somoni", + "TMT": "Turkmenistani Manat", + "TND": "Tunisian Dinar", + "TOP": "Tongan Pa'anga", + "TRY": "Turkish Lira", + "TTD": "Trinidad and Tobago Dollar", + "TWD": "New Taiwan Dollar", + "TZS": "Tanzanian Shilling", + "UAH": "Ukrainian Hryvnia", + "UGX": "Ugandan Shilling", + "USD": "United States Dollar", + "UYU": "Uruguayan Peso", + "UZS": "Uzbekistan Som", + "VEF": "Venezuelan Bolívar Fuerte (Old)", + "VES": "Venezuelan Bolívar Soberano", + "VND": "Vietnamese Dong", + "VUV": "Vanuatu Vatu", + "WST": "Samoan Tala", + "XAF": "CFA Franc BEAC", + "XAG": "Silver Ounce", + "XAU": "Gold Ounce", + "XCD": "East Caribbean Dollar", + "XDR": "Special Drawing Rights", + "XOF": "CFA Franc BCEAO", + "XPD": "Palladium Ounce", + "XPF": "CFP Franc", + "XPT": "Platinum Ounce", + "YER": "Yemeni Rial", + "ZAR": "South African Rand", + "ZMW": "Zambian Kwacha", + "ZWL": "Zimbabwean Dollar" +} diff --git a/exercises/06_code_quality/linter/package.json b/exercises/06_code_quality/linter/package.json index 6c5393de..6eb0a08d 100644 --- a/exercises/06_code_quality/linter/package.json +++ b/exercises/06_code_quality/linter/package.json @@ -34,6 +34,9 @@ "koa-static": "^5.0.0", "koa-views": "^6.1.5", "mime-types": "^2.1.22", + "readline": "^1.3.0", + "readline-sync": "^1.4.10", + "request": "^2.88.0", "sqlite-async": "^1.0.11" }, "devDependencies": { diff --git a/exercises/06_code_quality/list.js b/exercises/06_code_quality/list.js new file mode 100644 index 00000000..69912d5b --- /dev/null +++ b/exercises/06_code_quality/list.js @@ -0,0 +1,23 @@ +#!/usr/bin/env node + +'use strict' + +const readline = require('readline-sync') +const items = [] +let action + +while(true) { + action = String(readline.question('enter a command (add, list, quit): ')) + switch(action) { + case 'add': + const item = String(readline.question('item name: ')) + const qty = Number(readline.question(`now many items of type "${item}": `)) + items.push({item, qty}) + case 'list': + items.forEach( item => console.log(`${item.item}: ${item.qty}`)) + case 'quit': + process.exit() + default: + console.log('command not recognised') + } +} diff --git a/exercises/06_code_quality/nestedCallbacks.js b/exercises/06_code_quality/nestedCallbacks.js index cd3fcc12..6d89b729 100644 --- a/exercises/06_code_quality/nestedCallbacks.js +++ b/exercises/06_code_quality/nestedCallbacks.js @@ -14,7 +14,7 @@ getInput('enter base currency', (err, base) => { console.log(err.message) process.exit() } - getData(`http://api.fixer.io/latest?base=${base}`, (err, data) => { + getData(`https://api.exchangeratesapi.io/latest?base=${base}`, (err, data) => { if (err) { console.log(err.message) process.exit() @@ -39,7 +39,7 @@ function getInput(prompt, callback) { function checkValidCurrencyCode(code, callback) { code = code.trim() - request('http://api.fixer.io/latest', (err, res, body) => { + request('https://api.exchangeratesapi.io/latest', (err, res, body) => { if (err) callback(new Error('invalid API call')) const rates = JSON.parse(body).rates if (!rates.hasOwnProperty(code)) callback(new Error(`invalid currency code ${code}`)) diff --git a/exercises/06_code_quality/promises.js b/exercises/06_code_quality/promises.js index 74c13396..30397aea 100644 --- a/exercises/06_code_quality/promises.js +++ b/exercises/06_code_quality/promises.js @@ -2,17 +2,22 @@ 'use strict' const request = require('request') +const readline = require('readline') -const getInput = prompt => new Promise( (resolve) => { - process.stdin.resume() - process.stdin.setEncoding('utf8') - process.stdout.write(`${prompt}: `) - process.stdin.on('data', text => resolve(text)) +const baseURL = 'https://api.exchangeratesapi.io/latest' + +const getInput = prompt => new Promise(resolve => { + const read = readline.createInterface({ input: process.stdin, output: process.stdout }) + read.question(`${prompt}: `, value => { + console.log(`You entered ${value}`) + read.close() + resolve(value) + }) }) const checkValidCurrencyCode = code => new Promise( (resolve, reject) => { code = code.trim() - request('http://api.fixer.io/latest', (err, res, body) => { + request(baseURL, (err, res, body) => { if (err) reject(new Error('invalid API call')) const rates = JSON.parse(body).rates if (!rates.hasOwnProperty(code)) reject(new Error(`invalid currency code ${code}`)) @@ -21,7 +26,7 @@ const checkValidCurrencyCode = code => new Promise( (resolve, reject) => { }) const getData = code => new Promise( (resolve, reject) => { - request(`http://api.fixer.io/latest?base=${code}`, (err, res, body) => { + request(`${baseURL}?base=${code}`, (err, res, body) => { if (err) reject(new Error('invalid API call')) resolve(body) }) diff --git a/exercises/07_unit_testing/filesystem/modules/file.js b/exercises/07_unit_testing/filesystem/modules/file.js new file mode 100644 index 00000000..ec5e4202 --- /dev/null +++ b/exercises/07_unit_testing/filesystem/modules/file.js @@ -0,0 +1,35 @@ + +'use strict' + +const fs = require('fs') + +module.exports = class File { + + constructor(name) { + return async() => { + this.filename = name + } + } + + async add(item, qty) { + fs.readFile(this.filename, 'utf8', (err, data) => { + if (err) throw new Error('file not found') + data = JSON.parse(data) + data.append({ item, qty }) + data = JSON.stringify(data) + fs.writeFile(this.filename, data, 'utf8', (err) => { + if(err) throw err + } ) + }) + } + + get records() { + return async() => { + fs.readFile(this.filename, 'utf8', (err, data) => { + if (err) throw new Error('file not found') + return JSON.parse(data) + }) + } + } + +} diff --git a/exercises/07_unit_testing/filesystem/package.json b/exercises/07_unit_testing/filesystem/package.json new file mode 100644 index 00000000..2cc74c64 --- /dev/null +++ b/exercises/07_unit_testing/filesystem/package.json @@ -0,0 +1,14 @@ +{ + "name": "filesystem", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "mock-fs": "^4.10.1" + } +} diff --git a/exercises/07_unit_testing/filesystem/unit tests/file.spec.js b/exercises/07_unit_testing/filesystem/unit tests/file.spec.js new file mode 100644 index 00000000..8bdf4f84 --- /dev/null +++ b/exercises/07_unit_testing/filesystem/unit tests/file.spec.js @@ -0,0 +1,33 @@ + +'use strict' + +const File = require('../modules/file.js') +const mock = require('mock-fs') + +beforeAll( async() => { + mock({ + 'data': { + 'test.json': '["item": "bread", "qty": 42]' + } + }) +}) + +describe('xxx', () => { + + beforeEach( async() => { + const file = new File() + }) + afterEach( async() => { + // runs after each test completes + }) + test('xxx', async done => { + expect.assertions(1) + try { + // XXX + } catch(err) { + // XXX + } finally { + done() + } + }) +})