Last active
November 30, 2015 19:58
-
-
Save kylebakerio/e1b1227d285b78f67b56 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var express = require('express'); | |
var app = express(); | |
var bodyParser = require('body-parser'); | |
var morgan = require('morgan'); | |
var app = express(); | |
app.use(express.static(__dirname + '/client')); | |
app.use('/bower_components', express.static(__dirname + '/bower_components')); | |
app.use(morgan('dev')); | |
var games = []; | |
app.get('/', function (req, res) { | |
res.send("This should be a frontend interface for the bowling API, yo."); | |
}); | |
// takes in an ID and returns | |
app.get('/games/:id', function (req,res) { | |
// return full game object | |
if (typeof req.gameNum !== "number") res.send(/*code for invalid request*/); | |
else if (typeof games[req.gameNum]) res.send(/*code for resource doesn't exist*/); | |
else res.send(plainScores(req.gameNum)); | |
}) | |
// idempotent; takes in data on individual rolls that are explicitly defined, returns the round that has been 'patched'. Can create or update. | |
app.patch('/score', function (req, res) { | |
// example expected input: req.update === {gameNum: 13, player: "John", round: 0, roll: 0, pins: 12} | |
if (typeof req.update !== "object" || typeof req.update.gameNum !== "number") res.send(/*code for invalid request*/); | |
else if (typeof games[req.update.gameNum]) res.send(/*code for resource doesn't exist*/); | |
else { | |
updateScore(req.update.gameNum, req.update.player, req.update.round, req.update.roll, req.update.pins); | |
res.send(games[req.update.gameNum][req.update.player][req.update.round]); | |
} | |
}); | |
// takes in only the game's ID and the pins of the last roll, and returns... The whole game? | |
app.patch('/score', function (req, res) { | |
// example expected input: req === {names: ["John", "Sarah"], timeout: 21600000} | |
// have the client indicate time to keep game alive. | |
if ( !Array.isArray(req.names) || req.names.length < 1 ) res.send(/*code for invalid request*/); | |
else { | |
var gameNum = newGame(req.names); | |
res.send({gameID: gameNum}); | |
// setTimeout(games.splice(gameNum, 1), req.timeout); // prevents games from being saved forever. | |
} | |
}); | |
// takes an array of names, creates a server side game object, and returns the ID of that object. | |
app.post('/new-game', function (req, res) { | |
// example expected input: req === {names: ["John", "Sarah"], timeout: 21600000} | |
// have the client indicate time to keep game alive. | |
if ( !Array.isArray(req.names) || req.names.length < 1 ) res.send(/*code for invalid request*/); | |
else { | |
var gameNum = newGame(req.names); | |
res.status(201).send({gameID: gameNum}); | |
// setTimeout(games.splice(gameNum, 1), req.timeout); // prevents games from being saved forever. | |
} | |
}); | |
app.delete('/game', function (req,res) { | |
// game is finished, delete. you will need the timeoutID as well, which needs to be saved to the gameNum instance (global.clearTimeout(games[gameNum].timeoutID)) | |
if (typeof req.gameNum !== "number") res.send(/*code for invalid request*/); | |
else if (typeof games[req.gameNum]) res.send(/*code for resource doesn't exist*/); | |
res.send(games.splice([req.gameNum], 1)); // status code? proper to return finished game according to REST? splice works correctly? | |
}); | |
var server = app.listen(3017, function () { | |
var port = server.address().port; | |
console.log('Example app listening at http://%s:%s', /*host,*/ port); | |
}); | |
// | |
// bowling functions | |
// | |
function newGame (names) { | |
var game = {}; // create new game object | |
names.forEach( (name) => {game[name] = []} ); // every player gets an empty array to store their rounds in | |
games.push(game); // store newly created game locally on server | |
return games.length-1; // return the index of the just-created game for client use | |
} | |
function updateScore (gameNum, player, round, roll, pins) { | |
// example expected input: req.update === {gameNum: 13, player: 0, round: 0, roll: 0, pins: 12} | |
// if this is a new round, and there aren't existing scores for this round | |
if (roll === 0 && typeof games[gameNum][player].rounds[round] === "undefined") { | |
// create an array for this round | |
games[gameNum][player].rounds[round] = []; | |
} | |
// within the array for this round, assign the index that corresponds to this 'roll' to a function that returns the face value when called | |
games[gameNum][player].rounds[round][roll] = () => {return pins}; | |
// if this is a strike | |
if (roll === 0 && pins === 12) { | |
// push function into this round that returns bonus ball 1 OR zero if not yet played | |
games[gameNum][player].rounds[round].push( | |
() => { | |
return typeof games[gameNum][player].rounds[round+1] !== "undefined" ? games[gameNum][player].rounds[round+1][0]() : 0; | |
} | |
); | |
// push function into this round that returns bonus ball 2 OR zero if not yet played | |
games[gameNum][player].rounds[round].push( | |
() => { | |
return typeof games[gameNum][player].rounds[round+1] !== "undefined" && | |
typeof games[gameNum][player].rounds[round+1][1] !== "undefined" ? | |
games[gameNum][player].rounds[round+1][1]() : 0; | |
} | |
); | |
} | |
// if this is a spare | |
else if (roll === 1 && games[game][player].rounds[round][0]() + games[game][player].rounds[round][1]() === 12) { | |
// push function into this round that returns bonus ball 1 OR zero if not yet played | |
games[game][player].rounds[round].push( | |
() => { | |
return typeof games[game][player].rounds[round+1] !== "undefined" ? games[game][player].rounds[round+1][0]() : 0; | |
} | |
); | |
} | |
} | |
// returns the same game object, with the subarrays containing rolls populated with | |
// corresponding integers instead of functions. | |
function plainScores (gameIndex) { | |
var scores = {}; | |
for (player in games[gameIndex]) { | |
scores[player].rounds = []; | |
for (var i = 0; i < games[gameIndex][player].rounds.length; i++){ // for each round | |
scores[player].rounds[i] = games[gameIndex][player].rounds[i].map((func) => { return func()}); | |
} | |
} | |
return scores; | |
} | |
// Example reference game structure as locally stored: | |
/* | |
game[0] = [ // a game with one or many players, in an array of games | |
{ // a player | |
name: "John", | |
rounds: [ | |
[ // round 1 | |
() => {return 11}, // an individual roll | |
() => {return 1}, // another individual roll (and so on) | |
() => {return games[0 <--this game ][0 <--this player ].rounds[1 <--the next round ][0 <--the first roll of that round] (unless undefined, in which case return 0)}, | |
], | |
[ // round 2 | |
() => {return 12}, | |
() => {return games[0][0].rounds[2][0] (unless undefined, in which case return 0)}, | |
() => {return games[0][0].rounds[2][1] (unless undefined, in which case return 0)} | |
] | |
] | |
}, | |
{ // another player | |
name: "Sally", | |
rounds: [ | |
[ | |
() => {return 11}, | |
() => {return 0} | |
], | |
[ | |
() => {return 3}, | |
() => {return 9}, | |
() => {return games[0][1].rounds[2][0] (unless undefined, in which case return 0)} | |
] | |
] | |
} | |
] | |
*/ |
dillema: if the order of the players matters, I can't use an object. I could use a ES6 map, or an array. Bummer.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
REST is kind of irritating. It's so rigid. It's nice having things standardized, I guess, but it is inherently inefficient.
Problem: I think having an API endpoint that allows sequential updates the way I've envisioned is inherently not RESTful. Neither POST (creating a new resource), PUT (updating or creating a resource by submitting the entire resource), nor PATCH (specify a part of the resource and an action to update it) seem to be quite a fit.
I also feel like the REST paradigm doesn't correspond well to an object oriented paradigm. There seems to be a distinction people reference frequently between 'resources' and 'sub-resources', but in reality all resources are sub-resources, and all sub-resources are resources. Those boundaries are subjective, and it seems silly to not consider anything I can navigate to with precision as a resource in its own right.
Do these sound like valid thoughts?
I guess I'm going to use PATCH, because it is a partial addition/update, and the part I'm 'specifying' is 'whatever is next'...
...but all this reading, and thinking, and now I have to make note to myself that doing it that way does make it non-idempotent. If there's a connection problem, or a refresh, or a bug in the browser, or the javascript frontend framework, or a user's frontend code, whatever, that causes the score to be sent twice, it will seriously screw up the game. Doesn't that make an explicit, idempotent interface better for the API call?
Perhaps the best solution is to actually send the user the suggested 'next score roll' info that they'll store and use to send their next score--client doesn't have to keep track of where it is, and it can make a relatively safe/idempotent call to the server.
Does that sound reasonable?