├── .dockerignore ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── bin └── github-trending-api ├── config └── logger.js ├── package.json ├── routes └── trending.js └── test ├── nock-fixtures.js └── trending.test.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directory 31 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 32 | node_modules 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | 40 | lcov 41 | coverage.html 42 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | services: 4 | - docker 5 | 6 | language: node_js 7 | 8 | node_js: 9 | - '5.2' 10 | - '4.1' 11 | - '0.12' 12 | - '0.10' 13 | 14 | after_script: "cat lcov | ./node_modules/coveralls/bin/coveralls.js" 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mhart/alpine-node:5 2 | 3 | ADD . /github-trending-api 4 | WORKDIR /github-trending-api 5 | RUN npm install --production 6 | 7 | CMD ./bin/github-trending-api 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Pedro Tacla Yamada 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # github-trending-api 2 | [![Build Status](https://travis-ci.org/yamadapc/github-trending-api.svg?branch=master)](https://travis-ci.org/yamadapc/github-trending-api) 3 | [![Docker Image Size](https://img.shields.io/imagelayers/image-size/yamadapc/github-trending-api/latest.svg)](https://hub.docker.com/r/yamadapc/github-trending-api/) 4 | [![Docker Image Pulls](https://img.shields.io/docker/pulls/yamadapc/github-trending-api.svg)](https://hub.docker.com/r/yamadapc/github-trending-api/) 5 | [![Coverage Status](https://coveralls.io/repos/yamadapc/github-trending-api/badge.svg?branch=master&service=github)](https://coveralls.io/github/yamadapc/github-trending-api?branch=master) 6 | 7 | - - - 8 | This module is an API for the `github-trending` NPM module. So it can be ran as 9 | an "ultra-micro-service". It has an in-memory cache for results; so _mostly_ you 10 | can use it from another point of the application and not worry about anything 11 | else. 12 | 13 | It exposes a standalone `express` app, which can be ran from the command-line 14 | with: 15 | ``` 16 | $ npm install github-trending-api 17 | $ github-trending-api 18 | ``` 19 | 20 | And an `express.Router` instance, which is what `require('github-trending-api')` 21 | returns. 22 | 23 | _A write-up on its stack is pending._ 24 | 25 | ## Docker 26 | This repository has automated image builds on hub.docker.com. So you can also 27 | run: 28 | ``` 29 | $ docker-machine start default 30 | $ eval $(docker-machine env default) 31 | $ docker run -it -p 3000:3000 yamadapc/github-trending-api 32 | $ curl `docker-machine ip default`:3000/repositories 33 | ``` 34 | 35 | ## Endpoints 36 | ### `GET /repositories` 37 | Responds with a list of trending repositories; accepts an optional `language` 38 | QueryString parameter. 39 | 40 | ### `GET /languages` 41 | Responds with the list of valid languages. 42 | 43 | ## License 44 | This code is licensed under MIT license. See [LICENSE](/LICENSE). 45 | -------------------------------------------------------------------------------- /bin/github-trending-api: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var log = require('../config/logger'); 3 | var trendingApi = require('..'); 4 | 5 | var PORT = process.env.PORT || 3000; 6 | 7 | if(!module.main) { 8 | var express = require('express'); 9 | var expressBunyanLogger = require('express-bunyan-logger'); 10 | var app = express(); 11 | 12 | trendingApi.enableLog = true; 13 | 14 | app.use(expressBunyanLogger()); 15 | app.use(trendingApi); 16 | 17 | app.listen(PORT, function() { 18 | log.info('Listening on port ' + PORT); 19 | }); 20 | } 21 | 22 | exports = module.exports = trendingApi; 23 | -------------------------------------------------------------------------------- /config/logger.js: -------------------------------------------------------------------------------- 1 | var bunyan = require('bunyan'); 2 | exports = module.exports = bunyan.createLogger({ 3 | name: 'github-trending-api', 4 | }); 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-trending-api", 3 | "version": "1.0.3", 4 | "description": "API for GitHub trending repositories", 5 | "bin": "bin/github-trending-api", 6 | "main": "routes/trending.js", 7 | "scripts": { 8 | "test": "mocha --require blanket -R mocha-spec-cov-alt", 9 | "test-html-cov": "mocha --require blanket -R html-cov > coverage.html" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+ssh://git@github.com/yamadapc/github-trending-api.git" 14 | }, 15 | "keywords": [ 16 | "github", 17 | "trending", 18 | "api", 19 | "express", 20 | "rest", 21 | "repositories", 22 | "social" 23 | ], 24 | "author": "Pedro Tacla Yamada", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/beijaflor-io/github-trending-api/issues" 28 | }, 29 | "homepage": "https://github.com/beijaflor-io/github-trending-api#readme", 30 | "dependencies": { 31 | "bluebird": "^3.0.6", 32 | "bunyan": "^1.5.1", 33 | "express": "^4.13.3", 34 | "express-bunyan-logger": "^1.2.0", 35 | "express-promise": "^0.4.0", 36 | "github-trending": "^1.3.1", 37 | "ttl-cache": "^1.0.2" 38 | }, 39 | "devDependencies": { 40 | "blanket": "^1.2.1", 41 | "coveralls": "^2.11.4", 42 | "mocha": "^2.1.0", 43 | "mocha-make-stub": "^2.3.2", 44 | "mocha-spec-cov-alt": "^1.1.0", 45 | "nock": "^3.4.0", 46 | "should": "^8.0.0", 47 | "supertest": "^1.1.0" 48 | }, 49 | "config": { 50 | "blanket": { 51 | "data-cover-never": [ 52 | "node_modules", 53 | "test" 54 | ], 55 | "pattern": [ 56 | "bin", 57 | "config", 58 | "routes" 59 | ], 60 | "spec-cov": { 61 | "threshold": 80, 62 | "localThreshold": 80, 63 | "lcovOutput": "lcov" 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /routes/trending.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'); 2 | var TtlCache = require('ttl-cache'); 3 | var express = require('express'); 4 | var expressPromise = require('express-promise'); 5 | var log = require('../config/logger'); 6 | var trending = require('github-trending'); 7 | 8 | Promise.promisifyAll(trending); 9 | var trendingAsync = Promise.promisify(trending); 10 | 11 | var router = express.Router(); 12 | router.use(expressPromise()); 13 | exports = module.exports = router; 14 | 15 | // TODO The users should never miss the cache. The application should manage 16 | // it's state in memory to do this. It'll end-up being implemented on another 17 | // place. 18 | 19 | // Cache for 8m 20 | router.cacheTtl = 60 * 8; 21 | // Update cache every 2m, so maximum cache time is 10m by default 22 | router.cacheInterval = 60 * 2; 23 | 24 | // Use memory cache for now; will add redis when necessary 25 | router.cache = new TtlCache({ 26 | ttl: router.cacheTtl, 27 | interval: router.cacheInterval, 28 | }); 29 | 30 | function getRepositoriesCached(language) { 31 | var cacheKey = 'repositories' + (language ? '.' + language : ''); 32 | var repositories = router.cache.get(cacheKey); 33 | 34 | if(repositories) return repositories; 35 | 36 | if(router.enableLog) { 37 | log.warn('Cache miss for `' + cacheKey + '`. Hitting GitHub.'); 38 | } 39 | 40 | return (language != null ? 41 | trendingAsync(language) : 42 | trendingAsync()).tap(function(repositories) { 43 | router.cache.set(cacheKey, repositories); 44 | }); 45 | } 46 | 47 | router.get('/repositories', function(req, res) { 48 | res.json(getRepositoriesCached(req.query.language)); 49 | }); 50 | 51 | function getLanguagesCached() { 52 | var languages = router.cache.get('languages'); 53 | if(languages) return languages; 54 | 55 | if(router.enableLog) { 56 | log.warn('Cache miss for `languages`. Hitting GitHub.'); 57 | } 58 | 59 | return trending.languagesAsync().tap(function(languages) { 60 | router.cache.set('languages', languages); 61 | return languages; 62 | }); 63 | } 64 | 65 | router.get('/languages', function(req, res) { 66 | res.json(getLanguagesCached()); 67 | }); 68 | -------------------------------------------------------------------------------- /test/trending.test.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'); 2 | var express = require('express'); 3 | var makeStub = require('mocha-make-stub'); 4 | var request = require('supertest'); 5 | var should = require('should'); 6 | var trending = require('github-trending'); 7 | 8 | var trendingApi = require('../routes/trending'); 9 | require('./nock-fixtures'); 10 | 11 | //var nock = require('nock'); 12 | //nock.recorder.rec(); 13 | 14 | Promise.promisifyAll(request.Test.prototype); 15 | 16 | describe('trending-api', function() { 17 | var app = express(); 18 | app.use(trendingApi); 19 | 20 | describe('GET /repositories', function() { 21 | it('responds with trending repositories', function() { 22 | return request(app) 23 | .get('/repositories') 24 | .endAsync() 25 | .then(function(res) { 26 | should.exist(res.body); 27 | res.body.map(function(o) { 28 | return o.title; 29 | }).should.containEql('hashcat'); 30 | }); 31 | }); 32 | 33 | it('is able to filter by language', function() { 34 | return request(app) 35 | .get('/repositories') 36 | .query({ 37 | language: 'haskell', 38 | }) 39 | .endAsync() 40 | .then(function(res) { 41 | should.exist(res.body); 42 | res.body.should.containEql({ 43 | title: 'elm-compiler', 44 | owner: 'elm-lang', 45 | description: 'Compiler for the Elm programming language. Elm aims to make web development more pleasant. Elm is a type inferred, functional reactive language that compiles to HTML, CSS, and JavaScript.', 46 | url: 'https://github.com/elm-lang/elm-compiler', 47 | language: 'Haskell', 48 | star: '10 stars today' 49 | }); 50 | }); 51 | }); 52 | }); 53 | 54 | describe('GET /languages', function() { 55 | it('responds with available languages', function() { 56 | return request(app) 57 | .get('/languages') 58 | .endAsync() 59 | .then(function(res) { 60 | should.exist(res.body); 61 | res.body.should.containEql('haskell'); 62 | }); 63 | }); 64 | 65 | describe('when hitting again', function() { 66 | makeStub(trending, 'languagesAsync', function() { 67 | return Promise.resolve(['haskell']); 68 | }); 69 | 70 | it('the second hit doesn\'t use the GH API', function() { 71 | var _this = this; 72 | return request(app) 73 | .get('/languages') 74 | .endAsync() 75 | .then(function(res) { 76 | res.body.should.containEql('haskell'); 77 | _this.languagesAsync.calledOnce.should.equal(false, 'Didn\'t hit the cache'); 78 | }); 79 | }); 80 | }); 81 | }); 82 | }); 83 | --------------------------------------------------------------------------------