first actually working version of v3
This commit is contained in:
parent
afbf30f1c0
commit
4bc211a2e7
1
Gemfile
1
Gemfile
|
@ -6,6 +6,7 @@ gem 'travis-support', github: 'travis-ci/travis-support'
|
|||
gem 'travis-config', '~> 0.1.0'
|
||||
gem 'travis-sidekiqs', github: 'travis-ci/travis-sidekiqs', require: nil, ref: 'cde9741'
|
||||
gem 'travis-yaml', github: 'travis-ci/travis-yaml'
|
||||
gem 'mustermann', github: 'rkh/mustermann'
|
||||
gem 'sinatra'
|
||||
gem 'sinatra-contrib', require: nil #github: 'sinatra/sinatra-contrib', require: nil
|
||||
|
||||
|
|
10
Gemfile.lock
10
Gemfile.lock
|
@ -20,6 +20,13 @@ GIT
|
|||
rack-contrib (1.2.0)
|
||||
rack (>= 0.9.1)
|
||||
|
||||
GIT
|
||||
remote: git://github.com/rkh/mustermann.git
|
||||
revision: 01df7fe671838d6caadac278d9704b5cd3c7b999
|
||||
specs:
|
||||
mustermann (0.4.0)
|
||||
tool (~> 0.2)
|
||||
|
||||
GIT
|
||||
remote: git://github.com/rkh/yard-sinatra.git
|
||||
revision: 00774d355123617ff0faa7e0ebd54c4cdcfcdf93
|
||||
|
@ -215,8 +222,6 @@ GEM
|
|||
metaclass (~> 0.0.1)
|
||||
multi_json (1.10.1)
|
||||
multipart-post (2.0.0)
|
||||
mustermann (0.4.0)
|
||||
tool (~> 0.2)
|
||||
net-http-persistent (2.9.4)
|
||||
net-http-pipeline (1.0.1)
|
||||
pg (0.13.2)
|
||||
|
@ -350,6 +355,7 @@ DEPENDENCIES
|
|||
metriks-librato_metrics!
|
||||
micro_migrations
|
||||
mocha (~> 0.12)
|
||||
mustermann!
|
||||
pry
|
||||
rack-attack
|
||||
rack-cache!
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
module Travis
|
||||
module API
|
||||
module V3
|
||||
V3 = self
|
||||
|
||||
def load_dir(dir, recursive: true)
|
||||
Dir.glob("#{dir}/*.rb").each { |f| require f[%r[(?<=lib/).+(?=\.rb$)]] }
|
||||
Dir.glob("#{dir}/*").each { |dir| load_dir(dir) } if recursive
|
||||
end
|
||||
|
||||
def response(payload, headers = {}, content_type: 'application/json'.freeze, status: 200)
|
||||
payload = JSON.pretty_generate(payload) unless payload.is_a? String
|
||||
headers = { 'Content-Type'.freeze => content_type, 'Content-Length'.freeze => payload.bytesize.to_s }.merge!(headers)
|
||||
[200, headers, [payload] ]
|
||||
end
|
||||
|
||||
extend self
|
||||
load_dir("#{__dir__}/v3")
|
||||
end
|
||||
|
|
|
@ -31,7 +31,7 @@ module Travis::API::V3
|
|||
end
|
||||
|
||||
def public_api?
|
||||
Travis.config.public_api
|
||||
!Travis.config.private_api
|
||||
end
|
||||
|
||||
def unrestricted_api?
|
||||
|
@ -42,7 +42,7 @@ module Travis::API::V3
|
|||
|
||||
def dispatch(object, method = caller_locations.first.base_label)
|
||||
method = object.class.name.underscore + ?_.freeze + method
|
||||
public_send(method) if respond_to?(method)
|
||||
send(method, object) if respond_to?(method, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@ module Travis::API::V3
|
|||
class OptIn
|
||||
attr_reader :legacy_stack, :prefix, :router, :accept, :version_header
|
||||
|
||||
def initialize(legacy_stack, prefix: '/v3/', router: Router.new, accept: 'application/vnd.travis-ci.3+', version_header: 'Travis-API-Version')
|
||||
def initialize(legacy_stack, prefix: '/v3', router: Router.new, accept: 'application/vnd.travis-ci.3+', version_header: 'Travis-API-Version')
|
||||
@legacy_stack = legacy_stack
|
||||
@prefix = prefix
|
||||
@router = router
|
||||
|
@ -29,11 +29,11 @@ module Travis::API::V3
|
|||
end
|
||||
|
||||
def redirect?(env)
|
||||
env['PATH_INFO'.freeze] + ?/.freeze == prefix
|
||||
env['PATH_INFO'.freeze] == prefix
|
||||
end
|
||||
|
||||
def redirect(env)
|
||||
[307, {'Location'.freeze => env['SCRIPT_NAME'.freeze] + prefix, 'Conent-Type'.freeze => 'text/plain'.freeze}, []]
|
||||
[307, {'Location'.freeze => env['SCRIPT_NAME'.freeze] + prefix + ?/.freeze, 'Conent-Type'.freeze => 'text/plain'.freeze}, []]
|
||||
end
|
||||
|
||||
def cascade?(status, headers, body)
|
||||
|
@ -46,7 +46,7 @@ module Travis::API::V3
|
|||
end
|
||||
|
||||
def from_prefix(env)
|
||||
return unless prefix and env['PATH_INFO'.freeze].start_with?(prefix)
|
||||
return unless prefix and env['PATH_INFO'.freeze].start_with?(prefix + ?/.freeze)
|
||||
env.merge({
|
||||
'SCRIPT_NAME'.freeze => env['SCRIPT_NAME'.freeze] + prefix,
|
||||
'PATH_INFO'.freeze => env['PATH_INFO'.freeze][prefix.size..-1]
|
||||
|
|
14
lib/travis/api/v3/renderer.rb
Normal file
14
lib/travis/api/v3/renderer.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
module Travis::API::V3
|
||||
module Renderer
|
||||
extend self
|
||||
|
||||
def [](key)
|
||||
return key if key.respond_to? :render
|
||||
const_get(key.to_s.camelize)
|
||||
end
|
||||
|
||||
def format_date(date)
|
||||
date && date.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
end
|
||||
end
|
||||
end
|
33
lib/travis/api/v3/renderer/repository.rb
Normal file
33
lib/travis/api/v3/renderer/repository.rb
Normal file
|
@ -0,0 +1,33 @@
|
|||
module Travis::API::V3
|
||||
module Renderer::Repository
|
||||
DIRECT_ATTRIBUTES = %i[id name slug description github_language private]
|
||||
extend self
|
||||
|
||||
def render(repository)
|
||||
{ :@type => 'repository'.freeze, **direct_attributes(repository), **nested_resources(repository) }
|
||||
end
|
||||
|
||||
def direct_attributes(repository)
|
||||
DIRECT_ATTRIBUTES.map { |a| [a, repository.public_send(a)] }.to_h
|
||||
end
|
||||
|
||||
def nested_resources(repository)
|
||||
{
|
||||
owner: {
|
||||
:@type => repository.owner_type.downcase,
|
||||
:id => repository.owner_id,
|
||||
:login => repository.owner_name
|
||||
},
|
||||
last_build: {
|
||||
:@type => 'build'.freeze,
|
||||
:id => repository.last_build_id,
|
||||
:number => repository.last_build_number,
|
||||
:state => repository.last_build_state.to_s,
|
||||
:duration => repository.last_build_duration,
|
||||
:started_at => Renderer.format_date(repository.last_build_started_at),
|
||||
:finished_at => Renderer.format_date(repository.last_build_finished_at),
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -15,6 +15,10 @@ module Travis::API::V3
|
|||
self
|
||||
end
|
||||
|
||||
def render
|
||||
Renderer[type].render(resource)
|
||||
end
|
||||
|
||||
def method_missing(method, *args)
|
||||
return super unless method.to_sym == type.to_sym
|
||||
raise ArgumentError, 'wrong number of arguments (1 for 0)'.freeze if args.any?
|
||||
|
|
|
@ -1,29 +1,41 @@
|
|||
module Travis::API::V3
|
||||
class Router
|
||||
not_found = '{"error":{"message":"not found"}}'.freeze
|
||||
headers = { 'Content-Type'.freeze => 'application/json'.freeze, 'X-Cascade'.freeze => 'pass'.freeze, 'Content-Length'.freeze => not_found.bytesize }
|
||||
NOT_FOUND = [ 404, headers, not_found ]
|
||||
include Travis::API::V3
|
||||
|
||||
attr_accessor :routs, :not_found
|
||||
not_found = '{"error":{"message":"not found"}}'.freeze
|
||||
headers = { 'Content-Type'.freeze => 'application/json'.freeze, 'X-Cascade'.freeze => 'pass'.freeze, 'Content-Length'.freeze => not_found.bytesize.to_s }
|
||||
NOT_FOUND = [ 404, headers, [not_found] ]
|
||||
|
||||
attr_accessor :routes, :not_found
|
||||
|
||||
def initialize(routes = Routes, not_found: NOT_FOUND)
|
||||
@routes = routes
|
||||
@not_found = not_found
|
||||
routes.draw_routes
|
||||
end
|
||||
|
||||
def call(env)
|
||||
return service_index(env) if env['PATH_INFO'.freeze] == ?/.freeze
|
||||
access_control = AccessControl.new(env)
|
||||
factory, params = @routes.factory_for(env['REQUEST_METHOD'.freeze], env['PATH_INFO'.freeze])
|
||||
factory, params = routes.factory_for(env['REQUEST_METHOD'.freeze], env['PATH_INFO'.freeze])
|
||||
env_params = params(env)
|
||||
if factory
|
||||
service = factory.new(access_control, env_params.merge(params))
|
||||
result = service.run
|
||||
render(result, env_params)
|
||||
else
|
||||
not_found
|
||||
NOT_FOUND
|
||||
end
|
||||
end
|
||||
|
||||
def render(result, env_params)
|
||||
V3.response(result.render)
|
||||
end
|
||||
|
||||
def service_index(env)
|
||||
ServiceIndex.for(env, routes).render(env)
|
||||
end
|
||||
|
||||
def params(env)
|
||||
request = Rack::Request.new(env)
|
||||
params = request.params
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
require 'travis/api/v3/routes/resource'
|
||||
require 'mustermann'
|
||||
|
||||
module Travis::API::V3
|
||||
module Routes::DSL
|
||||
def routes
|
||||
@routes ||= {}
|
||||
end
|
||||
|
||||
def resources
|
||||
@resources ||= []
|
||||
end
|
||||
|
@ -25,14 +28,36 @@ module Travis::API::V3
|
|||
end
|
||||
|
||||
def route(value)
|
||||
current_resource.route = Mustermann.new(value)
|
||||
current_resource.route = value
|
||||
end
|
||||
|
||||
def get(*args)
|
||||
current_resource.add_service('GET'.freeze, *args)
|
||||
end
|
||||
|
||||
def factory_for(method, path)
|
||||
def post(*args)
|
||||
current_resource.add_service('POST'.freeze, *args)
|
||||
end
|
||||
|
||||
def draw_routes
|
||||
resources.each do |resource|
|
||||
prefix = resource.route
|
||||
resource.services.each do |(request_method, sub_route), service|
|
||||
route = sub_route ? prefix + sub_route : prefix
|
||||
routes[route] ||= {}
|
||||
routes[route][request_method] = Services[service]
|
||||
end
|
||||
end
|
||||
self.routes.replace(routes)
|
||||
end
|
||||
|
||||
def factory_for(request_method, path)
|
||||
routes.each do |route, method_map|
|
||||
next unless params = route.params(path)
|
||||
raise MethodNotAllowed unless factory = method_map[request_method]
|
||||
return [factory, params]
|
||||
end
|
||||
nil # nothing matched
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
require 'mustermann'
|
||||
|
||||
module Travis::API::V3
|
||||
class Routes::Resource
|
||||
attr_accessor :identifier, :route, :services
|
||||
|
@ -7,9 +9,13 @@ module Travis::API::V3
|
|||
@services = {}
|
||||
end
|
||||
|
||||
def add_service(request_method, service, sub_route = '')
|
||||
services[request_method] ||= {}
|
||||
services[request_method][sub_route] = service
|
||||
def add_service(request_method, service, sub_route = nil)
|
||||
sub_route &&= Mustermann.new(sub_route)
|
||||
services[[request_method, sub_route]] = service
|
||||
end
|
||||
|
||||
def route=(value)
|
||||
@route = value ? Mustermann.new(value) : value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,33 @@
|
|||
module Travis::API::V3
|
||||
class Service
|
||||
def self.params(*)
|
||||
def self.required_params
|
||||
@required_params ||= []
|
||||
end
|
||||
|
||||
def self.params(*list, optional: false)
|
||||
@params ||= []
|
||||
list.each do |param|
|
||||
param = param.to_s
|
||||
define_method(param) { params[param] }
|
||||
required_params << param unless optional
|
||||
@params << param
|
||||
end
|
||||
@params
|
||||
end
|
||||
|
||||
attr_accessor :access_control, :params
|
||||
|
||||
def initialize(access_control, params)
|
||||
@access_control = access_control
|
||||
@params = params
|
||||
end
|
||||
|
||||
def required_params?
|
||||
required_params.all? { |param| params.include? param }
|
||||
end
|
||||
|
||||
def required_params
|
||||
self.class.required_params
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
90
lib/travis/api/v3/service_index.rb
Normal file
90
lib/travis/api/v3/service_index.rb
Normal file
|
@ -0,0 +1,90 @@
|
|||
module Travis::API::V3
|
||||
class ServiceIndex
|
||||
ALLOW_POST = ['application/json', 'application/x-www-form-urlencoded', 'multipart/form-data']
|
||||
@index_cache = {}
|
||||
|
||||
def self.for(env, routes)
|
||||
access_factory = AccessControl.new(env).class
|
||||
prefix = env['SCRIPT_NAME'.freeze]
|
||||
@index_cache[[access_factory, routes, prefix]] ||= new(access_factory, routes, prefix)
|
||||
end
|
||||
|
||||
attr_reader :access_factory, :routes, :json_home_response, :json_response, :prefix
|
||||
|
||||
def initialize(access_factory, routes, prefix)
|
||||
@prefix = prefix || ''
|
||||
@access_factory, @routes = access_factory, routes
|
||||
@json_response = V3.response(render_json, content_type: 'application/json'.freeze)
|
||||
@json_home_response = V3.response(render_json_home, content_type: 'application/json-home'.freeze)
|
||||
end
|
||||
|
||||
def render(env)
|
||||
json_home?(env) ? json_home_response : json_response
|
||||
end
|
||||
|
||||
def render_json
|
||||
resources = { }
|
||||
routes.resources.each do |resource|
|
||||
resources[resource.identifier] ||= {}
|
||||
resource.services.each do |(request_method, sub_route), service|
|
||||
service &&= service.to_s.sub(/_#{resource.identifier}$/, ''.freeze)
|
||||
list = resources[resource.identifier][service] ||= []
|
||||
pattern = sub_route ? resource.route + sub_route : resource.route
|
||||
pattern.to_templates.each do |template|
|
||||
list << { 'request-method'.freeze => request_method, 'uri-template'.freeze => prefix + template }
|
||||
end
|
||||
end
|
||||
end
|
||||
{ resources: resources }
|
||||
end
|
||||
|
||||
def render_json_home
|
||||
relations = {}
|
||||
|
||||
routes.resources.each do |resource|
|
||||
resource.services.each do |(request_method, sub_route), service|
|
||||
service &&= service.to_s.sub(/_#{resource.identifier}$/, ''.freeze)
|
||||
pattern = sub_route ? resource.route + sub_route : resource.route
|
||||
relation = "http://schema.travis-ci.com/rel/#{resource.identifier}/#{service}"
|
||||
pattern.to_templates.each do |template|
|
||||
relations[relation] ||= {}
|
||||
relations[relation][template] ||= { allow: [], vars: template.scan(/{\+?([^}]+)}/).flatten }
|
||||
relations[relation][template][:allow] << request_method
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
nested_relations = {}
|
||||
relations.delete_if do |relation, request_map|
|
||||
next if request_map.size < 2
|
||||
common_vars = request_map.values.map { |e| e[:vars] }.inject(:&)
|
||||
request_map.each do |template, payload|
|
||||
special_vars = payload[:vars] - common_vars
|
||||
schema = special_vars.any? ? "#{relation}/by_#{special_vars.join(?_)}" : relation
|
||||
nested_relations[schema] = { template => payload }
|
||||
end
|
||||
end
|
||||
relations.merge! nested_relations
|
||||
|
||||
resources = relations.map do |relation, payload|
|
||||
template, payload = payload.first
|
||||
hints = { 'allow' => payload[:allow] }
|
||||
hints['accept-post'] = ALLOW_POST if payload[:allow].include? 'POST'
|
||||
hints['accept-patch'] = ALLOW_POST if payload[:allow].include? 'PATCH'
|
||||
hints['accept-put'] = ALLOW_POST if payload[:allow].include? 'PUT'
|
||||
hints['representations'] = ['application/json', 'application/vnd.travis-ci.3+json']
|
||||
[relation, {
|
||||
'href-template' => prefix + template,
|
||||
'href-vars' => Hash[payload[:vars].map { |var| [var, "http://schema.travis-ci.com/param/#{var}"] }],
|
||||
'hints' => hints
|
||||
}]
|
||||
end
|
||||
|
||||
{ resources: Hash[resources] }
|
||||
end
|
||||
|
||||
def json_home?(env)
|
||||
env['HTTP_ACCEPT'.freeze] == 'application/json-home'.freeze
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,4 +1,8 @@
|
|||
module Travis::API::V3
|
||||
module Services
|
||||
def self.[](key)
|
||||
return key if key.respond_to? :new
|
||||
const_get(key.to_s.camelize)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue
Block a user