Skip to content

Commit

Permalink
Added challenge api
Browse files Browse the repository at this point in the history
  • Loading branch information
ktonon committed Jan 2, 2017
1 parent c8fccf7 commit b6c8536
Show file tree
Hide file tree
Showing 25 changed files with 586 additions and 96 deletions.
6 changes: 6 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
extends: airbnb
rules:
func-names: off
import/no-extraneous-dependencies: off
object-shorthand: off
require-yield: off
strict: off
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/.s3-website.json
/.serverless
/app/config.json
/elm-stuff/
/node_modules/
/npm-debug.log
/public/
/env.sh
7 changes: 7 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# package directories
elm-stuff
node_modules
jspm_packages

# Serverless directories
.serverless
102 changes: 102 additions & 0 deletions api/challenge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
'use strict';

const AWS = require('aws-sdk');
const base64 = require('base64'); // eslint-disable-line import/no-unresolved
const BPromise = require('bluebird');
const genId = require('./gen-id');
const router = require('koa-router');

AWS.config.setPromisesDependency(BPromise);
const dynamodb = new AWS.DynamoDB({ apiVersion: '2012-08-10' });
const api = module.exports = router({ prefix: '/challenge' });
const table = 'wordsnake_challenge';

const toChallenge = data => ({
id: data.Item.id.S,
board: data.Item.board.S,
shape: data.Item.shape.S,
results: data.Item.results.L.map(result => ({
player: result.M.player.S,
score: parseInt(result.M.score.N, 10),
words: result.M.words.SS,
})),
});

const getById = id => dynamodb.getItem({
TableName: table,
Key: { id: { S: id } },
}).promise().then(toChallenge);

api.get('/:id', function* () {
this.body = yield getById(this.params.id);
});

const validateResult = (context) => {
const player = context.request.body.player;
const score = context.request.body.score;
const encodedToken = context.request.body.token;
if (!player || !score || !encodedToken) {
context.throw(400, 'Missing parameter');
return null;
}
let token;
try {
token = JSON.parse(base64.atob(encodedToken));
} catch (err) {
context.throw(400, 'Invalid token encoding');
return null;
}
const board = token.b;
const shape = token.s;
const words = token.w;
if (!board || !shape || !words
|| !/^[A-Z]{9,}$/.test(board)
|| !/^(\d+)x(\d+)/.test(shape)
|| !/^(([A-Z]{3,})(,[A-Z]{3,})*)?$/.test(words)
) {
context.throw(400, 'Invalid token');
return null;
}
return {
board: board,
shape: shape,
result: {
M: {
player: { S: player.toString() },
score: { N: parseInt(score.toString(), 10).toString() },
words: { SS: words.split(',') },
},
},
};
};

api.post('/', function* () {
const id = genId();
const v = validateResult(this);
if (v) {
this.body = yield dynamodb.putItem({
TableName: table,
Item: {
id: { S: id },
board: { S: v.board },
shape: { S: v.shape },
results: { L: [v.result] },
},
}).promise().then(() => getById(id));
}
});

api.post('/:id', function* () {
const id = this.params.id;
const v = validateResult(this);
this.body = yield dynamodb.updateItem({
TableName: table,
Key: { id: { S: id } },
AttributeUpdates: {
results: {
Action: 'ADD',
Value: { L: [v.result] },
},
},
}).promise().then(() => getById(id));
});
9 changes: 9 additions & 0 deletions api/conf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';

const rc = require('rc');

const conf = rc('wordsnake', {
stage: 'dev',
});

module.exports = conf;
9 changes: 9 additions & 0 deletions api/gen-id.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const randomstring = require('randomstring');

// 57 ** 7 = 1,954,897,493,193
const genId = () => randomstring.generate({
length: 7,
charset: 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789',
});

module.exports = genId;
35 changes: 35 additions & 0 deletions api/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict';

const bodyParser = require('koa-bodyparser');
const koa = require('koa');
const cors = require('koa-cors');
const logger = require('./logger');
const logging = require('@microservice/koa-logging');
const middleware = require('./middleware');
const router = require('koa-router');
const serverless = require('serverless-http');
const challenge = require('./challenge');

const routes = router();
routes.use(middleware.error);
routes.use(logging(logger));
routes.use(bodyParser());

routes.use(challenge.routes());
routes.use(challenge.allowedMethods());

routes.get('/', function* () {
this.body = 'WordSnake API';
});

module.exports.routes = routes;

const app = koa();
app.use(cors({
origin: true,
credentials: true,
methods: ['GET', 'HEAD', 'OPTIONS'],
}));
app.use(routes.routes());
app.use(routes.allowedMethods());
module.exports.handler = serverless(app);
8 changes: 8 additions & 0 deletions api/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use strict';

const conf = require('./conf');

module.exports = require('@microservice/logger')({
name: 'wordsnake',
environment: conf.stage,
});
13 changes: 13 additions & 0 deletions api/middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use strict';

const m = module.exports = {};

m.error = function* (next) {
try {
yield next;
} catch (err) {
this.status = err.status || err.statusCode || 500;
this.body = err.cause || (err.message ? { message: err.message } : err);
this.app.emit('error', err, this);
}
};
129 changes: 129 additions & 0 deletions app/Challenge/Challenge.elm
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
module Challenge.Challenge exposing (..)

import Http
import Json.Decode as D exposing (at)
import Json.Encode as J
import Regex exposing (regex, HowMany(..))
import Routing.Shape as Shape exposing (..)
import Task exposing (Task)
import Routing.Token as Token exposing (..)
import UrlParser exposing (..)


type alias Endpoint =
String


type alias Challenge =
{ id : Id
, board : String
, shape : Shape
, results : List GameResult
}


type alias Id =
String


type alias GameResult =
{ player : String
, score : Int
, words : List String
}


type Error
= HttpError Http.Error
| Error String


idParser : Parser (Id -> a) a
idParser =
UrlParser.custom "CHALLENGE_ID"
(\segment ->
case parseId segment of
Just id ->
Ok id

Nothing ->
Err ("Bad id: " ++ segment)
)


parseId : String -> Maybe String
parseId w =
w
|> Regex.find (AtMost 1) (regex "^([0-9A-Za-z]{7})$")
|> List.head
|> Maybe.andThen
(\m ->
m.submatches
|> List.head
|> Maybe.andThen (\id -> id)
)



-- API


get : Endpoint -> Id -> Task Error Challenge
get endpoint id =
Http.get (endpoint ++ "/challenge/" ++ id) challengeDecoder
|> Http.toTask
|> Task.mapError HttpError


create : Endpoint -> ( String, Int, Token ) -> Task Error Challenge
create endpoint gameResult =
Http.request
{ method = "POST"
, headers = []
, url = endpoint ++ "/challenge"
, body = Http.jsonBody (gameResult |> gameResultEncoder)
, expect = Http.expectJson challengeDecoder
, timeout = Nothing
, withCredentials = False
}
|> Http.toTask
|> Task.mapError HttpError


update : Endpoint -> Id -> ( String, Int, Token ) -> Task Error Challenge
update endpoint id gameResult =
Task.fail (Error "not implemented")



-- ENCODER


gameResultEncoder : ( String, Int, Token ) -> J.Value
gameResultEncoder ( player, score, token ) =
J.object
[ ( "player", J.string player )
, ( "score", J.int score )
, ( "token", token |> Token.toPathComponent |> J.string )
]



-- DECODER


challengeDecoder : D.Decoder Challenge
challengeDecoder =
D.map4 Challenge
(at [ "id" ] D.string)
(at [ "board" ] D.string)
(at [ "shape" ] Shape.decoder)
(at [ "results" ] (D.list gameResultDecoder))


gameResultDecoder : D.Decoder GameResult
gameResultDecoder =
D.map3 GameResult
(at [ "player" ] D.string)
(at [ "score" ] D.int)
(at [ "words" ] (D.list D.string))
25 changes: 18 additions & 7 deletions app/Config/Config.elm
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@ import Window
-- MODEL


type alias Endpoints =
{ api : String
, dictionaryApi : String
}


type alias Model =
{ apiEndpoint : String
{ endpoints : Endpoints
, isMobile : Bool
, language : Lang
, forkMe : Maybe String
Expand All @@ -19,16 +25,21 @@ type alias Model =

empty : Model
empty =
Model ""
Model (Endpoints "" "")
False
Lang.default
Nothing
(Window.Size 0 0)


isEmpty : Model -> Bool
isEmpty =
.apiEndpoint >> String.isEmpty
isEmpty model =
model.endpoints.api |> String.isEmpty


setEndpoints : Endpoints -> Model -> Model
setEndpoints endpoints model =
{ model | endpoints = endpoints }


setWindowSize : Window.Size -> Model -> Model
Expand All @@ -41,10 +52,10 @@ sizeDidChange size model =
size.width /= model.windowSize.width || size.height /= model.windowSize.height


decoder : D.Decoder Model
decoder =
decoder : Endpoints -> D.Decoder Model
decoder endpoints =
D.map5 Model
(at [ "apiEndpoint" ] D.string)
(D.succeed endpoints)
(at [ "isMobile" ] D.bool)
(at [ "language" ] Lang.decoder)
(D.maybe <| at [ "forkMe" ] D.string)
Expand Down
Loading

0 comments on commit b6c8536

Please sign in to comment.