rewrite all the things

This commit is contained in:
Konstantin Haase 2012-07-27 15:55:57 +02:00
parent f67e72602b
commit 7baf61054c
62 changed files with 1177 additions and 351 deletions

7
.travis.yml Normal file
View File

@ -0,0 +1,7 @@
language: ruby
rvm:
- 1.9.3
before_script:
- 'RAILS_ENV=test rake db:create db:schema:load --trace'
notifications:
irc: "irc.freenode.org#travis"

35
Gemfile
View File

@ -1,23 +1,34 @@
source :rubygems
ruby '1.9.3' rescue nil
gem 'travis-core', github: 'travis-ci/travis-core'
gem 'travis-support', github: 'travis-ci/travis-support'
gem 'travis-core', github: 'travis-ci/travis-core'
gem 'hubble', github: 'roidrage/hubble'
gem 'backports', '~> 2.5'
gem 'pg', '~> 0.13.2'
gem 'newrelic_rpm', '~> 3.3.0'
gem 'thin', '~> 1.4'
gem 'sinatra'
gem 'sinatra-contrib'
# gem 'sinatra-cross_origin', github: 'britg/sinatra-cross_origin'
gem 'rack-contrib', github: 'rack/rack-contrib', require: 'rack/contrib'
gem 'redcarpet'
gem 'rake', '~> 0.9.2.2'
gem 'versionist', '~> 0.2.0'
group :production do
gem 'rack-ssl'
end
# db
gem 'pg', '~> 0.13.2'
gem 'newrelic_rpm', '~> 3.3.0'
gem 'hubble', git: 'git://github.com/roidrage/hubble'
group :test do
gem 'rspec', '~> 2.11'
gem 'factory_girl', '~> 2.4.0'
end
# heroku
gem 'unicorn', '~> 4.1.1'
group :development do
gem 'yard-sinatra', github: 'rkh/yard-sinatra'
gem 'foreman'
gem 'rerun'
end
group :development, :test do
gem 'rake', '~> 0.9.2'
gem 'micro_migrations', git: 'git://gist.github.com/2087829.git'
end

View File

@ -1,12 +1,18 @@
GIT
remote: git://github.com/rack/rack-contrib.git
revision: b7e7c38fd02c3b5da91aa57af78b3f571c6ebcd0
remote: git://gist.github.com/2087829.git
revision: c766c06b0bdbda3bd96c3f4e376249cafcbbfaaa
specs:
rack-contrib (1.1.0)
rack (>= 0.9.1)
micro_migrations (0.0.1)
GIT
remote: git://github.com/roidrage/hubble
remote: git://github.com/rkh/yard-sinatra.git
revision: 3b1064eef407d2d288a5b96d258178a1e67b3b80
specs:
yard-sinatra (1.0.0)
yard (~> 0.7)
GIT
remote: git://github.com/roidrage/hubble.git
revision: 5220415d5542a2868d54f7be9f35fc1d66126b8e
specs:
hubble (0.1.2)
@ -63,25 +69,29 @@ GEM
activesupport (= 3.2.6)
arel (~> 3.0.2)
tzinfo (~> 0.3.29)
activeresource (3.2.6)
activemodel (= 3.2.6)
activesupport (= 3.2.6)
activesupport (3.2.6)
i18n (~> 0.6)
multi_json (~> 1.0)
addressable (2.2.8)
addressable (2.3.1)
arel (3.0.2)
atomic (1.0.1)
avl_tree (1.1.3)
backports (2.6.1)
backports (2.6.2)
builder (3.0.0)
daemons (1.1.8)
data_migrations (0.0.1)
activerecord
rake
diff-lcs (1.1.3)
erubis (2.7.0)
eventmachine (0.12.10)
factory_girl (2.4.2)
activesupport
faraday (0.8.1)
multipart-post (~> 1.1)
ffi (1.1.0)
foreman (0.53.0)
thor (>= 0.13.6)
gh (0.7.3)
addressable
backports (~> 2.3)
@ -95,7 +105,10 @@ GEM
i18n (0.6.0)
journey (1.0.4)
json (1.6.7)
kgio (2.7.4)
listen (0.4.7)
rb-fchange (~> 0.0.5)
rb-fsevent (~> 0.9.1)
rb-inotify (~> 0.8.8)
mail (2.4.4)
i18n (>= 0.4.0)
mime-types (~> 1.16)
@ -131,14 +144,6 @@ GEM
rack
rack-test (0.6.1)
rack (>= 1.0)
rails (3.2.6)
actionmailer (= 3.2.6)
actionpack (= 3.2.6)
activerecord (= 3.2.6)
activeresource (= 3.2.6)
activesupport (= 3.2.6)
bundler (~> 1.0)
railties (= 3.2.6)
railties (3.2.6)
actionpack (= 3.2.6)
activesupport (= 3.2.6)
@ -146,12 +151,27 @@ GEM
rake (>= 0.8.7)
rdoc (~> 3.4)
thor (>= 0.14.6, < 2.0)
raindrops (0.10.0)
rake (0.9.2.2)
rb-fchange (0.0.5)
ffi
rb-fsevent (0.9.1)
rb-inotify (0.8.8)
ffi (>= 0.5.0)
rdoc (3.12)
json (~> 1.4)
redcarpet (2.1.1)
redis (3.0.1)
rerun (0.7.1)
listen
rollout (1.1.0)
rspec (2.11.0)
rspec-core (~> 2.11.0)
rspec-expectations (~> 2.11.0)
rspec-mocks (~> 2.11.0)
rspec-core (2.11.1)
rspec-expectations (2.11.2)
diff-lcs (~> 1.1.3)
rspec-mocks (2.11.1)
signature (0.1.3)
simple_states (0.1.1)
activesupport
@ -171,33 +191,37 @@ GEM
hike (~> 1.2)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
thin (1.4.1)
daemons (>= 1.0.9)
eventmachine (>= 0.12.6)
rack (>= 1.0.0)
thor (0.14.6)
tilt (1.3.3)
treetop (1.4.10)
polyglot
polyglot (>= 0.3.1)
tzinfo (0.3.33)
unicorn (4.1.1)
kgio (~> 2.4)
rack
raindrops (~> 0.6)
versionist (0.2.3)
rails (~> 3.0)
yard (~> 0.7)
yard (0.8.2.1)
PLATFORMS
ruby
DEPENDENCIES
backports (~> 2.5)
factory_girl (~> 2.4.0)
foreman
hubble!
micro_migrations!
newrelic_rpm (~> 3.3.0)
pg (~> 0.13.2)
rack-contrib!
rake (~> 0.9.2.2)
rack-ssl
rake (~> 0.9.2)
redcarpet
rerun
rspec (~> 2.11)
sinatra
sinatra-contrib
thin (~> 1.4)
travis-core!
travis-support!
unicorn (~> 4.1.1)
versionist (~> 0.2.0)
yard-sinatra!

1
Procfile Normal file
View File

@ -0,0 +1 @@
web: bundle exec ./script/server

60
README.md Normal file
View File

@ -0,0 +1,60 @@
# The public Travis API
This is the app (eventually) running on https://api.travis-ci.org/
## Installation
Setup:
$ bundle install
Run tests:
$ RAILS_ENV=test rake db:create db:schema:load
$ rake spec
Run the server:
$ rake db:create db:schema:load
$ foreman start
## Contributing
1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create new Pull Request
### API documentation
We use source code comments to add documentation. If the server is running, you
can browse an HTML documenation at [`/docs`](http://localhost:5000/docs).
### Project architecture
lib
`-- travis
`-- api
`-- app
|-- endpoint # API endpoints
|-- extensions # Sinatra extensions
|-- helpers # Sinatra helpers
`-- middleware # Rack middleware
Classes inheriting from `Endpoint` or `Middleware`, they will automatically be
set up properly.
Each endpoint class gets mapped to a prefix, which defaults to the snake-case
class name (i.e. `Travis::Api::App::Profile` will map to `/profile`).
It can be overridden by setting `:prefix`:
``` ruby
require 'travis/api/app'
class Travis::Api::App
class MyRouts < Endpoint
set :prefix, '/awesome'
end
end
```

13
Rakefile Normal file
View File

@ -0,0 +1,13 @@
require 'bundler/setup'
ENV['SCHEMA'] = "#{Gem.loaded_specs['travis-core'].full_gem_path}/db/schema.rb"
require 'micro_migrations'
require 'travis'
begin
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new
task default: :spec
rescue LoadError
warn "could not load rspec"
end

View File

@ -1,5 +1,2 @@
$:.unshift 'lib'
require 'travis/api/app'
run Travis::Api::App
run Travis::Api::App.new

View File

@ -1,105 +1,64 @@
require 'sinatra'
require 'sinatra/reloader'
require 'travis/api/cors'
require 'json'
# Make sure we set that before everything
ENV['RACK_ENV'] ||= ENV['RAILS_ENV'] || ENV['ENV']
ENV['RAILS_ENV'] = ENV['RACK_ENV']
require 'travis'
require 'backports'
require 'rack'
require 'rack/protection'
require 'active_record'
Travis::Database.connect
# Rack class implementing the HTTP API.
# Instances respond to #call.
#
# run Travis::Api::App.new
#
# Requires TLS in production.
class Travis::Api::App
autoload :Responder, 'travis/api/app/responder'
autoload :Endpoint, 'travis/api/app/endpoint'
autoload :Extensions, 'travis/api/app/extensions'
autoload :Helpers, 'travis/api/app/helpers'
autoload :Middleware, 'travis/api/app/middleware'
module Travis
module Api
class App < Sinatra::Application
autoload :Service, 'travis/api/app/service'
disable :protection
Rack.autoload :SSL, 'rack/ssl'
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use Travis::API::CORS
# Used to track if setup already ran.
def self.setup?
@setup ||= false
end
error ActiveRecord::RecordNotFound do
not_found
end
# Loads all endpoints and middleware and hooks them up properly.
# Calls #setup on any middleware and endpoint.
#
# This method is not threadsafe, but called when loading
# the environment, so no biggy.
def self.setup(options = {})
return if setup?
Travis::Database.connect
configure :development do
register Sinatra::Reloader
set :show_exceptions, :after_handler
end
Responder.set(options) if options
Backports.require_relative_dir 'app/middleware'
Backports.require_relative_dir 'app/endpoint'
Responder.subclasses.each(&:setup)
before do
content_type :json
end
@setup = true
end
get '/repositories' do
respond_with Service::Repos.new(params).collection
end
attr_accessor :app
get '/repositories/:id' do
respond_with Service::Repos.new(params).item
# raise if not params[:format] == 'png'
end
get '/builds' do
respond_with Service::Builds.new(params).collection
end
get '/builds/:id' do
respond_with Service::Builds.new(params).item
end
get '/branches' do
# respond_with Service::Repos.new(params).item, :type => :branches
{ branches: [] }.to_json
end
get '/jobs' do
respond_with Service::Jobs.new(params).collection, :type => 'jobs'
end
get '/jobs/:id' do
respond_with Service::Jobs.new(params).item, :type => 'job'
end
get '/artifacts/:id' do
respond_with Service::Artifacts.new(params).item
end
get '/workers' do
respond_with Service::Workers.new(params).collection
end
get '/hooks' do
authenticate_user!
respond_with Service::Hooks.new(user, params).item
# rescue_from ActiveRecord::RecordInvalid, :with => Proc.new { head :not_acceptable }
end
put '/hooks/:id' do
authenticate_user!
respond_with Service::Hooks.new(user, params).update
end
get '/profile' do
authenticate_user!
respond_with Service::Profile.new(user).update
end
post '/profile/sync' do
authenticate_user!
respond_with Service::Profile.new(user).sync
end
private
def authenticate_user!
@user = User.find_by_login('svenfuchs')
end
def respond_with(resource, options = {})
Travis::Api.data(resource, { :params => params, :version => version }.merge(options)).to_json
end
def version
'v2'
end
def initialize
Travis::Api::App.setup
@app = Rack::Builder.app do
use Rack::Protection::PathTraversal
use Rack::SSL if Endpoint.production?
Middleware.subclasses.each { |m| use(m) }
Endpoint.subclasses.each { |e| map(e.prefix) { run(e) } }
end
end
# Rack protocol
def call(env)
app.call(env)
end
end

View File

@ -0,0 +1,16 @@
require 'travis/api/app'
class Travis::Api::App
# Superclass for HTTP endpoints. Takes care of prefixing.
class Endpoint < Responder
set(:prefix) { "/" << name[/[^:]+$/].underscore }
before { content_type :json }
error(ActiveRecord::RecordNotFound, Sinatra::NotFound) { not_found }
not_found { content_type =~ /json/ ? { 'file' => 'not found' } : 'file not found' }
# TODO: Dummy method.
def self.scope(name)
end
end
end

View File

@ -0,0 +1,11 @@
require 'travis/api/app'
class Travis::Api::App
class Endpoint
# TODO: Add documentation.
class Artifacts < Endpoint
# TODO: Add documentation.
get('/:id') { |id| Artifact.find(id) }
end
end
end

View File

@ -0,0 +1,11 @@
require 'travis/api/app'
class Travis::Api::App
class Endpoint
# TODO: Add documentation.
class Branches < Endpoint
# TODO: Add better implementation and documentation.
get('/') {{ branches: [] }}
end
end
end

View File

@ -0,0 +1,28 @@
require 'travis/api/app'
class Travis::Api::App
class Endpoint
# TODO: Add documentation.
class Builds < Endpoint
# TODO: Add documentation.
get '/' do
scope = repository.builds.by_event_type(params[:event_type] || 'push')
scope = params[:after] ? scope.older_than(params[:after]) : scope.recent
scope
end
# TODO: Add documentation.
get '/:id' do
one = params[:repository_id] ? repository.builds : Build
one.includes(:commit, :matrix => [:commit, :log]).find(params[:id])
end
private
def repository
pass if params.empty?
Repository.find_by(params) || not_found
end
end
end
end

View File

@ -0,0 +1,185 @@
require 'travis/api/app'
class Travis::Api::App
class Endpoint
# Generated API documentation.
class Documentation < Endpoint
set prefix: '/docs'
enable :inline_templates
# HTML view for [/endpoints](#/endpoints/).
get '/' do
content_type :html
endpoints = Endpoints.endpoints
erb :index, {}, :endpoints => endpoints.keys.sort.map { |k| endpoints[k] }
end
helpers do
def icon_for(verb)
# GET, POST, PATCH, PUT, DELETE"
case verb
when 'GET' then 'file'
when 'POST' then 'edit'
when 'PATCH' then 'wrench'
when 'PUT' then 'share'
when 'DELETE' then 'trash'
else 'question-sign'
end
end
def slug_for(route)
return route['uri'] if route['verb'] == 'GET'
route['verb'] + " " + route['uri']
end
def docs_for(entry)
markdown(entry['doc']).
gsub('<pre', '<pre class="prettyprint linenums lang-js pre-scrollable"').
gsub(/<\/?code>/, '').
gsub(/TODO:?/, '<span class="label label-warning">TODO</span>')
end
end
end
end
end
__END__
@@ index
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Travis API documentation</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- we might wanna change this -->
<link href="http://twitter.github.com/bootstrap/assets/css/bootstrap.css" rel="stylesheet" />
<link href="http://twitter.github.com/bootstrap/assets/css/bootstrap-responsive.css" rel="stylesheet" />
<link href="http://twitter.github.com/bootstrap/assets/js/google-code-prettify/prettify.css" rel="stylesheet" />
<script src="http://twitter.github.com/bootstrap/assets/js/jquery.js"></script>
<script src="http://twitter.github.com/bootstrap/assets/js/google-code-prettify/prettify.js"></script>
<script src="http://twitter.github.com/bootstrap/assets/js/bootstrap.min.js"></script>
<style type="text/css">
header {
position: relative;
text-align: center;
margin-top: 36px;
}
header h1 {
text-shadow: 2px 2px 5px #000;
margin-bottom: 9px;
font-size: 81px;
font-weight: bold;
letter-spacing: -1px;
line-height: 1;
}
header p {
margin-bottom: 18px;
font-weight: 300;
font-size: 18px;
}
.page-header {
margin-top: 90px;
}
.route {
margin-bottom: 36px;
}
.page-header a {
color: black;
}
.nav-list a {
color: inherit !important;
}
</style>
</head>
<body onload="prettyPrint()">
<div class="container-fluid">
<div class="row-fluid">
<header class="span12">
<h1>The Travis API</h1>
<p>All the routes, just waiting for you to build something awesome.</p>
</header>
</div>
<div class="row-fluid">
<aside class="span3">
<div class="page-header">
<h1>Navigation</h1>
</div>
<div class="well" style="padding: 8px 0;">
<ul class="nav nav-list">
<% endpoints.each do |endpoint| %>
<li class="nav-header"><a href="#<%= endpoint['name'] %>"><%= endpoint['name'] %></a></li>
<% endpoint['routes'].each do |route| %>
<li>
<a href="#<%= slug_for(route) %>">
<i class="icon-<%= icon_for route['verb'] %>"></i>
<tt><%= route['uri'] %></tt>
</a>
</li>
<% end %>
<% end %>
<li class="divider"></li>
<li class="nav-header">
External Links
</li>
<li>
<a href="https://travis-ci.org">
<i class="icon-globe"></i>
Travis CI
</a>
</li>
<li>
<a href="https://github.com/travis-ci/travis-api">
<i class="icon-cog"></i>
Source Code
</a>
</li>
<li>
<a href="https://github.com/travis-ci/travis-api/issues">
<i class="icon-list-alt"></i>
API issues
</a>
</li>
<li>
<a href="https://github.com/travis-ci/travis-ember">
<i class="icon-play-circle"></i>
Example Client
</a>
</li>
</ul>
</div>
</aside>
<section class="span9">
<% endpoints.each do |endpoint| %>
<div id="<%= endpoint['name'] %>">
<div class="page-header">
<h1>
<a href="#<%= endpoint['name'] %>"><%= endpoint['name'] %></a>
</h1>
</div>
<%= docs_for endpoint %>
<% endpoint['routes'].each do |route| %>
<div class="route" id="<%= slug_for(route) %>">
<pre><h3><%= route['verb'] %> <%= route['uri'] %></h3></pre>
<p>
<h5>Required autorization scope: <span class="label"><%= route['scope'] %></span></h5>
</p>
<%= docs_for route %>
</div>
<% end %>
</div>
<% end %>
</section>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,68 @@
require 'travis/api/app'
require 'yard/sinatra'
class Travis::Api::App
class Endpoint
# Documents all available API endpoints for the currently deployed version.
# Text is actually parsed from the source code upon server start.
class Endpoints < Endpoint
set :endpoints, {}
set :setup do
endpoint_files = Dir.glob(File.expand_path("../*.rb", __FILE__))
YARD::Registry.load(endpoint_files, true)
YARD::Sinatra.routes.each do |route|
namespace = route.namespace
controller = namespace.to_s.constantize
route_info = {
'uri' => (controller.prefix + route.http_path[1..-2]).gsub('//', '/'),
'verb' => route.http_verb,
'doc' => route.docstring,
'scope' => /scope\W+(\w+)/.match(route.source).try(:[], 1) || 'public'
}
endpoint = endpoints[controller.prefix] ||= {
'name' => namespace.name,
'doc' => namespace.docstring,
'prefix' => controller.prefix,
'routes' => []
}
endpoint['routes'] << route_info
end
set :json, endpoints.keys.sort.map { |k| endpoints[k] }.to_json
endpoints.each_value { |r| r[:json] = r.to_json if r.respond_to? :to_hash }
end
# Lists all available API endpoints by URI prefix.
#
# Values in the resulting array correspond to return values of
# [`/endpoints/:prefix`](#/endpoints/:prefix).
get '/' do
settings.json
end
# Infos about a specific controller.
#
# Example response:
#
# {
# name: "Endpoints",
# doc: "Documents all available API endpoints...",
# prefix: "/endpoints",
# routes: [
# {
# uri: "/endpoints/:prefix",
# verb: "GET",
# doc: "Infos about...",
# scope: "public"
# }
# ]
# }
get '/:prefix' do |prefix|
pass unless endpoint = settings.endpoints["/#{prefix}"]
endpoint[:json]
end
end
end
end

View File

@ -0,0 +1,15 @@
require 'travis/api/app'
class Travis::Api::App
class Endpoint
class Home < Endpoint
set(:prefix, '/')
# Landing point. Redirects web browsers to [API documentation](#/docs/).
get '/' do
redirect to('/docs/') if request.preferred_type('application/json', 'text/html') == 'text/html'
{ 'hello' => 'world' }
end
end
end
end

View File

@ -0,0 +1,14 @@
require 'travis/api/app'
class Travis::Api::App
class Endpoint
# TODO: Add documentation.
class Hooks < Endpoint
# TODO: Add implementation and documentation.
get('/', scope: :private) { raise NotImplementedError }
# TODO: Add implementation and documentation.
put('/:id', scope: :admin) { raise NotImplementedError }
end
end
end

View File

@ -0,0 +1,14 @@
require 'travis/api/app'
class Travis::Api::App
class Endpoint
# TODO: Add documentation.
class Jobs < Endpoint
# TODO: Add implementation and documentation.
get('/') { raise NotImplementedError }
# TODO: Add implementation and documentation.
get('/:id') { raise NotImplementedError }
end
end
end

View File

@ -0,0 +1,14 @@
require 'travis/api/app'
class Travis::Api::App
class Endpoint
# TODO: Add documentation.
class Profile < Endpoint
# TODO: Add implementation and documentation.
get('/', scope: :private) { raise NotImplementedError }
# TODO: Add implementation and documentation.
post('/sync', scope: :private) { raise NotImplementedError }
end
end
end

View File

@ -0,0 +1,20 @@
require 'travis/api/app'
class Travis::Api::App
class Endpoint
# TODO: Add documentation.
class Repositories < Endpoint
# TODO: Add documentation.
get '/' do
scope = Repository.timeline.recent
scope = scope.by_owner_name(params[:owner_name]) if params[:owner_name]
scope = scope.by_slug(params[:slug]) if params[:slug]
scope = scope.search(params[:search]) if params[:search].present?
scope
end
# TODO: Add documentation.
get('/:id') { Repository.find_by(params) }
end
end
end

View File

@ -0,0 +1,11 @@
require 'travis/api/app'
class Travis::Api::App
class Endpoint
# TODO: Add documentation.
class Workers < Endpoint
# TODO: Add implementation and documentation.
get('/') { Worker.order(:host, :name) }
end
end
end

View File

@ -0,0 +1,8 @@
require 'travis/api/app'
class Travis::Api::App
# Namespace for Sinatra extensions.
module Extensions
Backports.require_relative_dir 'extensions'
end
end

View File

@ -0,0 +1,28 @@
require 'travis/api/app'
class Travis::Api::App
module Extensions
# Allows writing
#
# helpers :some_helper
#
# Instead of
#
# helpers Travis::Api::App::Helpers::SomeHelper
module SmartConstants
def helpers(*list, &block)
super(*resolve_constants(list, Helpers), &block)
end
def register(*list, &block)
super(*resolve_constants(list, Extensions), &block)
end
private
def resolve_constants(list, namespace)
list.map { |e| Symbol === e ? namespace.const_get(e.to_s.camelize) : e }
end
end
end
end

View File

@ -0,0 +1,25 @@
require 'travis/api/app'
class Travis::Api::App
module Extensions
# Keeps track of subclasses. Used for endpoint and middleware detection.
# This will prevent garbage collection of subclasses.
module SubclassTracker
def direct_subclasses
@direct_subclasses ||= []
end
# List of "leaf" subclasses (ie subclasses without subclasses).
def subclasses
return [self] if direct_subclasses.empty?
direct_subclasses.map(&:subclasses).flatten.uniq
end
def inherited(subclass)
super
subclass.set app_file: caller_files.first
direct_subclasses << subclass
end
end
end
end

View File

@ -0,0 +1,8 @@
require 'travis/api/app'
class Travis::Api::App
# Namespace for helpers.
module Helpers
Backports.require_relative_dir 'helpers'
end
end

View File

@ -0,0 +1,32 @@
require 'travis/api/app'
class Travis::Api::App
module Helpers
# Allows routes to return either hashes or anything Travis::API.data can
# convert (in addition to the return values supported by Sinatra, of
# course). These values will be encoded in JSON.
module JsonRenderer
def respond_with(resource, options = {})
halt render_json(resource, options)
end
def body(value = nil, &block)
value = render_json(value) if value
super(value, &block)
end
private
def render_json(resource, options = {})
options[:version] ||= 'v2' # TODO: Content negotiation
options[:params] ||= params
builder = Travis::Api.builder(resource, options)
resource = builder.new(resource, options[:params]).data.to_json if builder
resource = resource.to_json if resource.is_a? Hash
resource
end
end
end
end

View File

@ -0,0 +1,7 @@
require 'travis/api/app'
class Travis::Api::App
# Superclass for all middleware.
class Middleware < Responder
end
end

View File

@ -0,0 +1,9 @@
require 'travis/api/app'
class Travis::Api::App
class Middleware
# Checks access tokens and sets appropriate scopes.
class AccessToken < Middleware
end
end
end

View File

@ -1,10 +1,12 @@
require 'sinatra/base'
module Travis
module API
class CORS < Sinatra::Base
disable :protection
require 'travis/api/app'
class Travis::Api::App
class Middleware
# Implements Cross-Origin Resource Sharing. Supported by all major browsers.
# See http://www.w3.org/TR/cors/
#
# TODO: Be smarter about origin.
class Cors < Middleware
before do
headers['Access-Control-Allow-Origin'] = "*"
headers['Access-Control-Allow-Credentials'] = "true"
@ -13,7 +15,7 @@ module Travis
options // do
headers['Access-Control-Allow-Methods'] = "GET, POST, PATCH, PUT, DELETE"
headers['Access-Control-Allow-Headers'] = "Content-Type, Authorization"
headers['Access-Control-Allow-Headers'] = "Content-Type, Authorization, Accept"
end
end
end

View File

@ -0,0 +1,15 @@
require 'travis/api/app'
class Travis::Api::App
class Middleware
# Makes sure we use Travis.logger everywhere.
class Logging < Middleware
set(:setup) { ActiveRecord::Base.logger = Travis.logger }
before do
env['rack.logger'] = Travis.logger
env['rack.errors'] = Travis.logger.instance_variable_get(:@logdev).dev rescue nil
end
end
end
end

View File

@ -0,0 +1,27 @@
require 'travis/api/app'
require 'sinatra/base'
class Travis::Api::App
# Superclass for any endpoint and middleware.
# Pulls in relevant helpers and extensions.
class Responder < Sinatra::Base
register Extensions::SmartConstants
configure do
# We pull in certain protection middleware in App.
# Being token based makes us invulnerable to common
# CSRF attack.
#
# Logging is set up by custom middleware
disable :protection, :logging, :setup
register :subclass_tracker
helpers :json_renderer
end
configure :development do
# We want error pages in development, but only
# when we don't have an error handler specified
set :show_exceptions, :after_handler
end
end
end

View File

@ -1,20 +0,0 @@
module Travis
module Api
class App
class Service
autoload :Artifacts, 'travis/api/app/service/artifacts'
autoload :Builds, 'travis/api/app/service/builds'
autoload :Hooks, 'travis/api/app/service/hooks'
autoload :Jobs, 'travis/api/app/service/jobs'
autoload :Repos, 'travis/api/app/service/repos'
autoload :Workers, 'travis/api/app/service/workers'
attr_reader :params
def initialize(params)
@params = params
end
end
end
end
end

View File

@ -1,14 +0,0 @@
module Travis
module Api
class App
class Service
class Artifacts < Service
def item
Artifact.find(params[:id])
end
end
end
end
end
end

View File

@ -1,27 +0,0 @@
module Travis
module Api
class App
class Service
class Builds < Service
def collection
scope = repository.builds.by_event_type(params[:event_type] || 'push')
scope = params[:after] ? scope.older_than(params[:after]) : scope.recent
scope
end
def item
one = params[:repository_id] ? repository.builds : Build
one.includes(:commit, :matrix => [:commit, :log]).find(params[:id])
end
private
def repository
Repository.find_by(params) || not_found # TODO needs to return nil if params are empty
end
end
end
end
end
end

View File

@ -1,39 +0,0 @@
module Travis
module Api
class App
class Service
class Hooks < Service
attr_reader :user
def initialize(user, params)
super(params)
@user = user
end
def collection
user.service_hooks
end
def update
hook.set(payload[:active], user)
hook
end
private
def hook
repository.service_hook
end
def repository
Repository.find_or_create_by_owner_name_and_name(params[:owner_name], params[:name])
end
def payload
params[:service_hook] || {}
end
end
end
end
end
end

View File

@ -1,23 +0,0 @@
module Travis
module Api
class App
class Service
class Jobs < Service
def collection
if params[:ids]
Job::Test.where(:id => params[:ids]).includes(:commit, :log)
else
jobs = Job::Test.queued.includes(:commit, :log)
jobs = jobs.where(:queue => params[:queue]) if params[:queue]
jobs
end
end
def item
Job::Test.find(params[:id])
end
end
end
end
end
end

View File

@ -1,32 +0,0 @@
module Travis
module Api
class App
class Service
class Profile < Service
attr_reader :user
def initialize(user)
@user = user
end
def item
user
end
def sync
unless user.is_syncing?
publisher.publish({ user_id: user.id }, type: 'sync')
user.update_attribute(:is_syncing, true)
end
end
private
def publisher
Travis::Amqp::Publisher.new('sync.user')
end
end
end
end
end
end

View File

@ -1,27 +0,0 @@
module Travis
module Api
class App
class Service
class Repos
attr_reader :params
def initialize(params)
@params = params
end
def collection
scope = Repository.timeline.recent
scope = scope.by_owner_name(params[:owner_name]) if params[:owner_name]
scope = scope.by_slug(params[:slug]) if params[:slug]
scope = scope.search(params[:search]) if params[:search].present?
scope
end
def item
Repository.find_by(params)
end
end
end
end
end
end

View File

@ -1,14 +0,0 @@
module Travis
module Api
class App
class Service
class Workers < Service
def collection
Worker.order(:host, :name)
end
end
end
end
end
end

8
script/server Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
cd "$(dirname "$0")/.."
[ $PORT ] || PORT=5000
[ $RACK_ENV ] || RACK_ENV=development
cmd="ruby -I lib -S bundle exec ruby -I lib -S thin start -p $PORT -e $RACK_ENV --threaded"
[[ $RACK_ENV == "development" ]] && exec rerun "$cmd -a 127.0.0.1"
exec $cmd

10
spec/app_spec.rb Normal file
View File

@ -0,0 +1,10 @@
require 'spec_helper'
describe Travis::Api::App do
describe :setup? do
it 'indicates if #setup has been called' do
Travis::Api::App.setup
Travis::Api::App.should be_setup
end
end
end

15
spec/default_spec.rb Normal file
View File

@ -0,0 +1,15 @@
require 'spec_helper'
describe Travis::Api::App::Endpoint::Home do
describe 'GET /' do
it 'replies with a json response by default' do
get('/')["Content-Type"].should include("json")
end
it 'redirects HTML requests to /docs' do
get '/', {}, 'HTTP_ACCEPT' => 'text/html'
status.should == 302
headers['Location'].should end_with('/docs/')
end
end
end

View File

@ -0,0 +1,12 @@
require 'spec_helper'
describe Travis::Api::App::Endpoint::Artifacts do
let(:artifact) { Factory(:log) }
let(:id) { artifact.id }
describe 'GET /artifacts/:id' do
it 'loads the artifact' do
get("/artifacts/#{id}").should be_ok
end
end
end

View File

@ -0,0 +1,5 @@
require 'spec_helper'
describe Travis::Api::App::Endpoint::Branches do
it 'has to be implemented'
end

View File

@ -0,0 +1,5 @@
require 'spec_helper'
describe Travis::Api::App::Endpoint::Builds do
it 'has to be tested'
end

View File

@ -0,0 +1,5 @@
require 'spec_helper'
describe Travis::Api::App::Endpoint::Documentation do
it 'has to be tested'
end

View File

@ -0,0 +1,5 @@
require 'spec_helper'
describe Travis::Api::App::Endpoint::Endpoints do
it 'has to be tested'
end

View File

@ -0,0 +1,5 @@
require 'spec_helper'
describe Travis::Api::App::Endpoint::Hooks do
it 'has to be tested'
end

View File

@ -0,0 +1,5 @@
require 'spec_helper'
describe Travis::Api::App::Endpoint::Jobs do
it 'has to be tested'
end

View File

@ -0,0 +1,5 @@
require 'spec_helper'
describe Travis::Api::App::Endpoint::Profile do
it 'has to be tested'
end

View File

@ -0,0 +1,5 @@
require 'spec_helper'
describe Travis::Api::App::Endpoint::Repositories do
it 'has to be tested'
end

View File

@ -0,0 +1,5 @@
require 'spec_helper'
describe Travis::Api::App::Endpoint::Workers do
it 'has to be tested'
end

18
spec/endpoint_spec.rb Normal file
View File

@ -0,0 +1,18 @@
require 'spec_helper'
describe Travis::Api::App::Endpoint do
class MyEndpoint < Travis::Api::App::Endpoint
set :prefix, '/my_endpoint'
get('/') { 'ok' }
end
it 'sets up endpoints automatically under given prefix' do
get('/my_endpoint/').should be_ok
body.should == "ok"
end
it 'does not require a trailing slash' do
get('/my_endpoint').should be_ok
body.should == "ok"
end
end

View File

@ -0,0 +1,22 @@
require 'spec_helper'
describe Travis::Api::App::Extensions::SmartConstants do
let(:some_app) do
Sinatra.new { register Travis::Api::App::Extensions::SmartConstants }
end
describe :helpers do
it 'works' do # :)
some_app.helpers :json_renderer
some_app.ancestors.should include(Travis::Api::App::Helpers::JsonRenderer)
end
end
describe :register do
it 'works' do # :)
some_app.register :subclass_tracker
some_app.should be_a(Travis::Api::App::Extensions::SubclassTracker)
end
end
end

View File

@ -0,0 +1,31 @@
require 'spec_helper'
describe Travis::Api::App::Extensions::SubclassTracker do
let!(:root) { Sinatra.new { register Travis::Api::App::Extensions::SubclassTracker } }
let!(:left) { Class.new(root) }
let!(:right) { Class.new(root) }
let!(:sub1) { Class.new(right) }
let!(:sub2) { Class.new(right) }
it 'tracks direct subclasses' do
classes = root.direct_subclasses
classes.size.should == 2
classes.should include(left)
classes.should include(right)
end
it 'tracks leaf subclasses' do
classes = root.subclasses
classes.size.should == 3
classes.should include(left)
classes.should include(sub1)
classes.should include(sub2)
end
it 'tracks subclasses of subclasses properly' do
classes = right.subclasses
classes.size.should == 2
classes.should include(sub1)
classes.should include(sub2)
end
end

View File

@ -0,0 +1,16 @@
require 'spec_helper'
require 'json'
describe Travis::Api::App::Helpers::JsonRenderer do
before do
mock_app do
helpers Travis::Api::App::Helpers::JsonRenderer
get('/') { {'foo' => 'bar'} }
end
end
it 'renders body as json' do
get('/').should be_ok
JSON.load(body).should == {'foo' => 'bar'}
end
end

View File

@ -0,0 +1,16 @@
require 'spec_helper'
describe Travis::Api::App::Middleware::AccessToken do
before do
mock_app do
use Travis::Api::App::Middleware::AccessToken
get('/check_cors') { 'ok' }
end
end
it 'sets associated scope properly'
it 'lets through requests without a token'
it 'reject requests with an invalide token'
it 'rejects expired tokens'
it 'checks that the token corresponds to Origin'
end

View File

@ -0,0 +1,50 @@
require 'spec_helper'
describe Travis::Api::App::Middleware::Cors do
before do
mock_app do
use Travis::Api::App::Middleware::Cors
get('/check_cors') { 'ok' }
end
end
describe 'normal request' do
before { get('/check_cors').should be_ok }
it 'sets Access-Control-Allow-Origin' do
headers['Access-Control-Allow-Origin'].should == "*"
end
it 'sets Access-Control-Allow-Credentials' do
headers['Access-Control-Allow-Credentials'].should == "true"
end
it 'sets Access-Control-Expose-Headers' do
headers['Access-Control-Expose-Headers'].should == "Content-Type"
end
end
describe 'OPTIONS requests' do
before { options('/').should be_ok }
it 'sets Access-Control-Allow-Origin' do
headers['Access-Control-Allow-Origin'].should == "*"
end
it 'sets Access-Control-Allow-Credentials' do
headers['Access-Control-Allow-Credentials'].should == "true"
end
it 'sets Access-Control-Expose-Headers' do
headers['Access-Control-Expose-Headers'].should == "Content-Type"
end
it 'sets Access-Control-Allow-Methods' do
headers['Access-Control-Allow-Methods'].should == "GET, POST, PATCH, PUT, DELETE"
end
it 'sets Access-Control-Allow-Headers' do
headers['Access-Control-Allow-Headers'].should == "Content-Type, Authorization, Accept"
end
end
end

View File

@ -0,0 +1,19 @@
require 'spec_helper'
describe Travis::Api::App::Middleware::Logging do
it 'configures ActiveRecord' do
ActiveRecord::Base.logger.should == Travis.logger
end
it 'sets the logger' do
mock_app do
use Travis::Api::App::Middleware::Logging
get '/check_logger' do
logger.should == Travis.logger
'ok'
end
end
get('/check_logger').should be_ok
end
end

12
spec/middleware_spec.rb Normal file
View File

@ -0,0 +1,12 @@
require 'spec_helper'
describe Travis::Api::App::Middleware do
class MyMiddleware < Travis::Api::App::Middleware
get('/my_middleware') { 'ok' }
end
it 'sets up middleware automatically' do
get('/my_middleware').should be_ok
body.should == "ok"
end
end

17
spec/spec_helper.rb Normal file
View File

@ -0,0 +1,17 @@
ENV['RACK_ENV'] = ENV['RAILS_ENV'] = ENV['ENV'] = 'test'
require 'rspec'
require 'travis/api/app'
require 'sinatra/test_helpers'
require 'logger'
Travis.logger = Logger.new(StringIO.new)
Travis::Api::App.setup
Backports.require_relative_dir 'support'
RSpec.configure do |config|
config.expect_with :rspec, :stdlib
config.include Sinatra::TestHelpers
config.before(:each) { set_app Travis::Api::App.new }
end

99
spec/support/factories.rb Normal file
View File

@ -0,0 +1,99 @@
require 'factory_girl'
FactoryGirl.define do
factory :build do
repository { Repository.first || Factory(:repository) }
association :request
association :commit
end
factory :commit do
repository { Repository.first || Factory(:repository) }
commit '62aae5f70ceee39123ef'
branch 'master'
message 'the commit message'
committed_at '2011-11-11T11:11:11Z'
committer_name 'Sven Fuchs'
committer_email 'svenfuchs@artweb-design.de'
author_name 'Sven Fuchs'
author_email 'svenfuchs@artweb-design.de'
compare_url 'https://github.com/svenfuchs/minimal/compare/master...develop'
end
factory :test, :class => 'Job::Test' do
repository { Repository.first || Factory(:repository) }
commit { Factory(:commit) }
source { Factory(:build) }
log { Factory(:log) }
queue "ruby"
end
factory :log, :class => 'Artifact::Log' do
content '$ bundle install --pa'
end
factory :request do
repository { Repository.first || Factory(:repository) }
association :commit
token 'the-token'
end
factory :repository do
name 'minimal'
owner_name 'svenfuchs'
owner_email 'svenfuchs@artweb-design.de'
url { |r| "http://github.com/#{r.owner_name}/#{r.name}" }
last_duration 60
created_at { |r| Time.utc(2011, 01, 30, 5, 25) }
updated_at { |r| r.created_at + 5.minutes }
end
factory :minimal, :parent => :repository do
end
factory :enginex, :class => Repository do
name 'enginex'
owner_name 'josevalim'
last_duration 30
end
factory :running_build, :parent => :build do
repository { Factory(:repository, :name => 'running_build') }
state 'started'
end
factory :successful_build, :parent => :build do
repository { Factory(:repository, :name => 'successful_build', :last_build_result => 0) }
result 0
state 'finished'
started_at { Time.now.utc }
finished_at { Time.now.utc }
end
factory :broken_build, :parent => :build do
repository { Factory(:repository, :name => 'broken_build', :last_build_result => 1) }
result 1
state 'finished'
started_at { Time.now.utc }
finished_at { Time.now.utc }
end
factory :user do
name 'Sven Fuchs'
login 'svenfuchs'
email 'sven@fuchs.com'
tokens { [Token.new] }
end
factory :worker do
name 'worker-1'
host 'ruby-1.workers.travis-ci.org'
state :working
last_seen_at { Time.now.utc }
end
factory :ssl_key do
private_key "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQDGed1uxl9szL0PVE/B6v9PDso+xRHs9e9YDB8Dm+QYFDyddud1\nn1134ZY39Dxg6zNhXDGKYilHP4E9boIuvgfSADN12eD1clogX46M4oBGgUAhtr5Q\nvGLn9TEW4IbeI+nDshMJLTLethCmB6Hwm5Ld9QnRVT6U/AztOTv9eJ/xKQIDAQAB\nAoGABQ3zcq/AnF+2bN6DzXdzmwrQYbrZEwTMXJyqaYgdzfMt/ACcMmWllrj6/1/L\n7dfvjgowBMstK/BVFUBsNk6GmmoCDHFAU+BgeyyqUxyeb63+0dIDwVYx9LHTL4dr\n9a8cVyeefqc3mqB13B9NUlS40Ij4kuK6EOGP3DZwC1FQVwECQQDtBQFqgRuNdfbV\naGIcXnuMnD4BGrnFHm0IBdLYsK4ULL85gFbhEew6DTYGYlGqX1dXbXYue8F18D8i\nzqL6HOBhAkEA1l6zvLdC2t3J9UnwpkwU0jSPX4BpHH7IkrCoGRggjwtbSxJFcCKB\nRrbPFDNAwchsa2/ldXSBrFg6Y7GlwF3lyQJAaJk+6LuVZzZZ+hAYzCA+Me15x479\n0Kn+v/2h8RL3n9ungD7NGIKKV4wg/WxCUgfFScX608S1udCObFP4xJwdwQJBALtl\nwEQqBGSmXCV0xM3rVoxH7En1TG3fm2E400pUoCnMKLugtlkHoPF7X91tzJ9aoQTu\npa2e8rkBy9FY++gFbZkCQAJ46lGEXZJqcACvLX0t/+RrvmqWMxCydLFG50kOnD8b\nVNILVyUn1lYasTs4aMYr6BRtVZoCxqV5/+rkMhb1eOM=\n-----END RSA PRIVATE KEY-----\n"
public_key "-----BEGIN RSA PUBLIC KEY-----\nMIGJAoGBAMZ53W7GX2zMvQ9UT8Hq/08Oyj7FEez171gMHwOb5BgUPJ1253WfXXfh\nljf0PGDrM2FcMYpiKUc/gT1ugi6+B9IAM3XZ4PVyWiBfjozigEaBQCG2vlC8Yuf1\nMRbght4j6cOyEwktMt62EKYHofCbkt31CdFVPpT8DO05O/14n/EpAgMBAAE=\n-----END RSA PUBLIC KEY-----\n"
end
end

View File

@ -1,8 +0,0 @@
# encoding: utf-8
Gem::Specification.new do |s|
s.name = 'travis-api'
s.version = '0.0.1'
s.require_path = 'lib'
end