diff --git a/lib/travis/api/v3.rb b/lib/travis/api/v3.rb index 8e0d3909..221df59b 100644 --- a/lib/travis/api/v3.rb +++ b/lib/travis/api/v3.rb @@ -37,4 +37,3 @@ module Travis end end end - diff --git a/lib/travis/api/v3/access_control/anonymous.rb b/lib/travis/api/v3/access_control/anonymous.rb index c41df37f..08300b33 100644 --- a/lib/travis/api/v3/access_control/anonymous.rb +++ b/lib/travis/api/v3/access_control/anonymous.rb @@ -2,6 +2,10 @@ require 'travis/api/v3/access_control/generic' module Travis::API::V3 class AccessControl::Anonymous < AccessControl::Generic + def self.new + @instace ||= super + end + # use when Authorization header is not set auth_type(nil) diff --git a/lib/travis/api/v3/paginator.rb b/lib/travis/api/v3/paginator.rb new file mode 100644 index 00000000..26503c2d --- /dev/null +++ b/lib/travis/api/v3/paginator.rb @@ -0,0 +1,42 @@ +module Travis::API::V3 + class Paginator + attr_accessor :default_limit, :max_limit + + def initialize(default_limit: 25, max_limit: 100) + @default_limit = default_limit + @max_limit = max_limit + end + + def paginate(result, limit: nil, offset: nil, access_control: AccessControl::Anonymous.new) + limit &&= Integer(limit, :limit) + limit ||= default_limit + limit = default_limit if limit < 0 + + unless access_control.full_access? + limit = max_limit if limit > max_limit or limit < 1 + end + + offset &&= Integer(offset, :offset) + offset = 0 if offset.nil? or offset < 0 + + count = result.resource ? result.resource.count : 0 + result.resource &&= result.resource.limit(limit) unless limit == 0 + result.resource &&= result.resource.offset(offset) unless offset == 0 + + pagination_info = { + limit: limit, + offset: offset, + count: count, + } + + result.meta_data[:pagination] = pagination_info + result + end + + def Integer(value, key) + super(value) + rescue ArgumentError + raise WrongParams, "#{key} must be an integer" + end + end +end diff --git a/lib/travis/api/v3/paginator/url_generator.rb b/lib/travis/api/v3/paginator/url_generator.rb new file mode 100644 index 00000000..da29c34e --- /dev/null +++ b/lib/travis/api/v3/paginator/url_generator.rb @@ -0,0 +1,95 @@ +require "addressable/uri" + +module Travis::API::V3 + class Paginator + class URLGenerator + class FancyParser + def initialize(href) + @uri = Addressable::URI.parse(href) + end + + def generate(offset, limit) + uri = @uri.dup + uri.query_values = uri.query_values.merge("offset".freeze => offset, "limit".freeze => limit) + uri.to_s + end + end + + class FastParser + PATTERN = /\?(?:&?(?:limit|offset)=[^=]*)*\Z/ + + def self.can_handle?(href) + return true unless href.include? ??.freeze + href =~ PATTERN + end + + def initialize(href) + @path_info = href.split(??.freeze, 2).first + end + + def generate(offset, limit) + "#{@path_info}?limit=#{limit}&offset=#{offset}" + end + end + + def initialize(href, limit: 0, offset: 0, count: 0, **) + @parser = FastParser.can_handle?(href) ? FastParser.new(href) : FancyParser.new(href) + @href = href + @limit = limit + @offset = offset + @count = count + end + + def last? + @count <= @offset + @limit + end + + def first? + @offset == 0 + end + + def next_info + info(offset: @offset + @limit) unless last? + end + + def previous_info + return if @offset == 0 + @offset <= @limit ? info(offset: 0, limit: @offset) : info(offset: @offset - @limit, limit: @limit) + end + + def first_info + info(offset: 0) + end + + def last_info + offset = @count / @limit * @limit + offset -= @limit if offset == @count + info(offset: offset) + end + + def info(offset: @offset, limit: @limit) + { + :@href => uri_with(offset, limit), + :offset => offset, + :limit => limit + } + end + + def to_h + { + is_first: first?, + is_last: last?, + next: next_info, + prev: previous_info, + first: first_info, + last: last_info + } + end + + def uri_with(offset, limit) + return @href if offset == @offset and limit == @limit + @parser.generate(offset, limit) + end + end + end +end diff --git a/lib/travis/api/v3/queries/builds.rb b/lib/travis/api/v3/queries/builds.rb index 0877bac5..f45d3daf 100644 --- a/lib/travis/api/v3/queries/builds.rb +++ b/lib/travis/api/v3/queries/builds.rb @@ -1,7 +1,21 @@ module Travis::API::V3 class Queries::Builds < Query + params :state, :event_type, :previous_state, prefix: :build + params :name, prefix: :branch, method_name: :branch_name + def find(repository) - repository.builds + filter(repository.builds) + end + + def filter(list) + list = list.where(state: list(state)) if state + list = list.where(previous_state: list(previous_state)) if previous_state + list = list.where(event_type: list(event_type)) if event_type + list = list.where(branch: list(branch_name)) if branch_name + + list = list.includes(:commit).includes(branch: :last_build).includes(:repository) + list = list.includes(branch: { last_build: :commit }) if includes? 'build.commit'.freeze + list end end end diff --git a/lib/travis/api/v3/query.rb b/lib/travis/api/v3/query.rb index 5c89da27..84a31da9 100644 --- a/lib/travis/api/v3/query.rb +++ b/lib/travis/api/v3/query.rb @@ -4,24 +4,35 @@ module Travis::API::V3 # generate from eval to avoid additional string allocations on every params access @@params_accessor = <<-RUBY - attr_writer :%s + attr_writer :%s - def %s - return @%s if defined? @%s - return @%s = @params['%s.%s'.freeze] if @params.include? '%s.%s'.freeze - return @%s = @params['%s'.freeze]['%s'.freeze] if @params.include? '%s'.freeze and @params['%s'.freeze].is_a? Hash - return @%s = @params['%s'.freeze] if (@params['@type'.freeze] || @main_type) == '%s'.freeze - @%s = nil + def %s + return @%s if defined? @%s + return @%s = @params['%s.%s'.freeze] if @params.include? '%s.%s'.freeze + return @%s = @params['%s'.freeze]['%s'.freeze] if @params.include? '%s'.freeze and @params['%s'.freeze].is_a? Hash + return @%s = @params['%s'.freeze] if (@params['@type'.freeze] || @main_type) == '%s'.freeze + return @%s = @params['%s'.freeze] if %p and (@params['@type'.freeze] || @main_type) == '%s'.freeze + @%s = nil end - def %s! - %s or raise WrongParams, 'missing %s.%s'.freeze + def %s! + %s or raise WrongParams, 'missing %s.%s'.freeze end RUBY - def self.params(*list, prefix: nil) - prefix ||= name[/[^:]+$/].underscore - list.each { |e| class_eval(@@params_accessor % { name: e, prefix: prefix }) } + def self.params(*list, prefix: nil, method_name: nil) + type = name[/[^:]+$/].underscore + prefix ||= type.to_s + check_type = method_name.nil? and type != prefix + list.each do |entry| + class_eval(@@params_accessor % { + name: entry, + prefix: prefix, + type: type, + method_name: method_name || entry, + check_type: check_type + }) + end end attr_reader :params, :main_type @@ -51,6 +62,10 @@ module Travis::API::V3 !!value end + def list(value) + value.split(?,.freeze) + end + def user_condition(value) case value when String then { login: value } diff --git a/lib/travis/api/v3/renderer/collection_renderer.rb b/lib/travis/api/v3/renderer/collection_renderer.rb index 1be17af6..cd415343 100644 --- a/lib/travis/api/v3/renderer/collection_renderer.rb +++ b/lib/travis/api/v3/renderer/collection_renderer.rb @@ -17,19 +17,34 @@ module Travis::API::V3 available_attributes << value end - def initialize(list, href: nil, included: [], **options) - @href = href - @options = options - @list = list - @included = included + attr_reader :href, :options, :list, :included, :meta_data + + def initialize(list, href: nil, included: [], meta_data: {}, **options) + @href = href + @options = options + @list = list + @included = included + @meta_data = meta_data + end + + def fields + fields = { :"@type" => type } + fields[:@href] = href if href + fields[:@pagination] = pagination_info if meta_data.include? :pagination + fields + end + + def pagination_info + return meta_data[:pagination] unless href + generator = V3::Paginator::URLGenerator.new(href, **meta_data[:pagination]) + meta_data[:pagination].merge generator.to_h end def render - result = { :"@type" => type } - result[:@href] = @href if @href - included = @included.dup - result[collection_key] = @list.map do |entry| - rendered = render_entry(entry, included: included, mode: :standard, **@options) + result = fields + included = self.included.dup + result[collection_key] = list.map do |entry| + rendered = render_entry(entry, included: included, mode: :standard, **options) included << entry rendered end diff --git a/lib/travis/api/v3/result.rb b/lib/travis/api/v3/result.rb index 1cf5733b..c6875af1 100644 --- a/lib/travis/api/v3/result.rb +++ b/lib/travis/api/v3/result.rb @@ -1,15 +1,25 @@ module Travis::API::V3 class Result - attr_accessor :access_control, :type, :resource, :status, :href + attr_accessor :access_control, :type, :resource, :status, :href, :meta_data, :warnings - def initialize(access_control, type, resource = [], status: 200) - @access_control, @type, @resource, @status = access_control, type, resource, status + def initialize(access_control, type, resource = [], status: 200, **meta_data) + @warnings = [] + @access_control, @type, @resource, @status, @meta_data = access_control, type, resource, status, meta_data end def respond_to_missing?(method, *) super or method.to_sym == type.to_sym end + def warn(message, **info) + warnings << { :@type => 'warning'.freeze, :message => message, **info } + end + + def ignored_param(param, reason: nil, **info) + message = reason ? "query parameter #{param} #{reason}, ignored" : "query parameter #{param} ignored" + warn(message, warning_type: :ignored_parameter, parameter: param, **info) + end + def <<(value) resource << value self @@ -19,7 +29,20 @@ module Travis::API::V3 href = self.href href = V3.location(env) if href.nil? and env['REQUEST_METHOD'.freeze] == 'GET'.freeze include = params['include'.freeze].to_s.split(?,.freeze) - Renderer[type].render(resource, href: href, script_name: env['SCRIPT_NAME'.freeze], include: include, access_control: access_control) + add_info Renderer[type].render(resource, + href: href, + script_name: env['SCRIPT_NAME'.freeze], + include: include, + access_control: access_control, + meta_data: meta_data) + end + + def add_info(payload) + if warnings.any? + payload = { :@warnings => [] }.merge!(payload) unless payload.include? :@warnings + payload[:@warnings].concat(warnings) + end + payload end def method_missing(method, *args) diff --git a/lib/travis/api/v3/router.rb b/lib/travis/api/v3/router.rb index 1c89ffd4..8e39fc15 100644 --- a/lib/travis/api/v3/router.rb +++ b/lib/travis/api/v3/router.rb @@ -16,8 +16,11 @@ module Travis::API::V3 raise NotFound unless factory - service = factory.new(access_control, factory.filter_params(env_params).merge(params)) + filtered = factory.filter_params(env_params) + service = factory.new(access_control, filtered.merge(params)) result = service.run + + env_params.each_key { |key| result.ignored_param(key, reason: "not whitelisted".freeze) unless filtered.include?(key) } render(result, env_params, env) rescue Error => error result = Result.new(access_control, :error, error) diff --git a/lib/travis/api/v3/service.rb b/lib/travis/api/v3/service.rb index c8c90249..2a0011e0 100644 --- a/lib/travis/api/v3/service.rb +++ b/lib/travis/api/v3/service.rb @@ -24,6 +24,19 @@ module Travis::API::V3 @params end + def self.paginate(**options) + params("limit".freeze, "offset".freeze) + @paginator = Paginator.new(**options) + end + + def self.paginator + @paginator ||= nil + end + + def self.paginate? + !!@paginator if defined? @paginator + end + attr_accessor :access_control, :params def initialize(access_control, params) @@ -68,7 +81,15 @@ module Travis::API::V3 def run not_found unless result = run! result = result(result_type, result) unless result.is_a? Result - result + self.class.paginate? ? paginate(result) : result + end + + def paginate(result) + p params + self.class.paginator.paginate(result, + limit: params['limit'.freeze], + offset: params['offset'.freeze], + access_control: access_control) end def params_for?(prefix) diff --git a/lib/travis/api/v3/services/builds/find.rb b/lib/travis/api/v3/services/builds/find.rb index 48354927..77d928e1 100644 --- a/lib/travis/api/v3/services/builds/find.rb +++ b/lib/travis/api/v3/services/builds/find.rb @@ -1,5 +1,9 @@ module Travis::API::V3 class Services::Builds::Find < Service + params :state, :event_type, :previous_state, prefix: :build + params "branch.name" + paginate + def run! query.find(find(:repository)) end diff --git a/spec/support/coverage.rb b/spec/support/coverage.rb index f5b93400..ab12f3fa 100644 --- a/spec/support/coverage.rb +++ b/spec/support/coverage.rb @@ -1,7 +1,9 @@ -require 'simplecov' +unless ENV['SKIP_COVERAGE'] + require 'simplecov' -SimpleCov.start do - coverage_dir '.coverage' - add_filter "/spec/" - add_group "v3", "lib/travis/api/v3" + SimpleCov.start do + coverage_dir '.coverage' + add_filter "/spec/" + add_group "v3", "lib/travis/api/v3" + end end diff --git a/spec/v3/services/owner/find_spec.rb b/spec/v3/services/owner/find_spec.rb index 66d3feca..fd8fc6fa 100644 --- a/spec/v3/services/owner/find_spec.rb +++ b/spec/v3/services/owner/find_spec.rb @@ -130,14 +130,19 @@ describe Travis::API::V3::Services::Owner::Find do before { get("/v3/owner/example-org?organization.id=#{other.id}") } example { expect(last_response).to be_ok } example { expect(JSON.load(body)).to be == { - "@type" => "organization", - "@href" => "/v3/org/#{org.id}", - "@permissions" => { "read"=>true, "sync"=>false }, - "id" => org.id, - "login" => "example-org", - "name" => nil, - "github_id" => nil, - "avatar_url" => nil + "@type" => "organization", + "@href" => "/v3/org/#{org.id}", + "@permissions" => { "read"=>true, "sync"=>false }, + "id" => org.id, + "login" => "example-org", + "name" => nil, + "github_id" => nil, + "avatar_url" => nil, + "@warnings" => [{ + "@type" => "warning", + "message" => "query parameter organization.id not whitelisted, ignored", + "warning_type" => "ignored_parameter", + "parameter" => "organization.id"}] }} end end @@ -198,7 +203,12 @@ describe Travis::API::V3::Services::Owner::Find do "github_id" => nil, "avatar_url" => nil, "is_syncing" => nil, - "synced_at" => nil + "synced_at" => nil, + "@warnings" => [{ + "@type" => "warning", + "message" => "query parameter user.id not whitelisted, ignored", + "warning_type" => "ignored_parameter", + "parameter" => "user.id"}] }} end end