Commit d5785b1b authored by Rémy Coutable's avatar Rémy Coutable

Merge branch '22600-related-resources-uris-using-grape-source-helpers-ee' into 'master'

Backports gitlab-ce!10491

See merge request !2501
parents 3a1b9c6a 450ff742
......@@ -16,6 +16,7 @@ gem 'mysql2', '~> 0.4.5', group: :mysql
gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.25.1.1'
gem 'grape-route-helpers', '~> 2.0.0'
gem 'faraday', '~> 0.12'
......
......@@ -370,6 +370,10 @@ GEM
grape-entity (0.6.0)
activesupport
multi_json (>= 1.3.2)
grape-route-helpers (2.0.0)
activesupport
grape (~> 0.16, >= 0.16.0)
rake
grpc (1.4.0)
google-protobuf (~> 3.1)
googleauth (~> 0.5.1)
......@@ -1017,6 +1021,7 @@ DEPENDENCIES
grape (~> 0.19.2)
grape-entity (~> 0.6.0)
gssapi
grape-route-helpers (~> 2.0.0)
haml_lint (~> 0.21.0)
hamlit (~> 2.6.1)
hashie-forbidden_attributes
......
---
title: Declare related resources into V4 API entities
merge_request:
author:
if defined?(GrapeRouteHelpers)
module GrapeRouteHelpers
class DecoratedRoute
# GrapeRouteHelpers gem tries to parse the versions
# from a string, not supporting Grape `version` array definition.
#
# Without the following fix, we get this on route helpers generation:
#
# => undefined method `scan' for ["v3", "v4"]
#
# 2.0.0 implementation of this method:
#
# ```
# def route_versions
# version_pattern = /[^\[",\]\s]+/
# if route_version
# route_version.scan(version_pattern)
# else
# [nil]
# end
# end
# ```
def route_versions
return [nil] if route_version.nil? || route_version.empty?
if route_version.is_a?(String)
version_pattern = /[^\[",\]\s]+/
route_version.scan(version_pattern)
else
route_version
end
end
end
end
end
API::API.logger Rails.logger
mount API::API => '/api'
mount API::API => '/'
......@@ -3,6 +3,7 @@ module API
include APIGuard
allow_access_with_scope :api
prefix :api
version %w(v3 v4), using: :path
......
......@@ -92,6 +92,38 @@ module API
end
class Project < Grape::Entity
include ::API::Helpers::RelatedResourcesHelpers
expose :_links do
expose :self do |project|
expose_url(api_v4_projects_path(id: project.id))
end
expose :issues, if: -> (*args) { issues_available?(*args) } do |project|
expose_url(api_v4_projects_issues_path(id: project.id))
end
expose :merge_requests, if: -> (*args) { mrs_available?(*args) } do |project|
expose_url(api_v4_projects_merge_requests_path(id: project.id))
end
expose :repo_branches do |project|
expose_url(api_v4_projects_repository_branches_path(id: project.id))
end
expose :labels do |project|
expose_url(api_v4_projects_labels_path(id: project.id))
end
expose :events do |project|
expose_url(api_v4_projects_events_path(id: project.id))
end
expose :members do |project|
expose_url(api_v4_projects_members_path(id: project.id))
end
end
expose :id, :description, :default_branch, :tag_list
expose :archived?, as: :archived
expose :visibility, :ssh_url_to_repo, :http_url_to_repo, :web_url
......@@ -327,6 +359,26 @@ module API
end
class Issue < IssueBasic
include ::API::Helpers::RelatedResourcesHelpers
expose :_links do
expose :self do |issue|
expose_url(api_v4_project_issue_path(id: issue.project_id, issue_iid: issue.iid))
end
expose :notes do |issue|
expose_url(api_v4_projects_issues_notes_path(id: issue.project_id, noteable_id: issue.iid))
end
expose :award_emoji do |issue|
expose_url(api_v4_projects_issues_award_emoji_path(id: issue.project_id, issue_iid: issue.iid))
end
expose :project do |issue|
expose_url(api_v4_projects_path(id: issue.project_id))
end
end
expose :subscribed do |issue, options|
issue.subscribed?(options[:current_user], options[:project] || issue.project)
end
......
module API
module Helpers
module RelatedResourcesHelpers
include GrapeRouteHelpers::NamedRouteMatcher
def issues_available?(project, options)
available?(:issues, project, options[:current_user])
end
def mrs_available?(project, options)
available?(:merge_requests, project, options[:current_user])
end
def expose_url(path)
url_options = Rails.application.routes.default_url_options
host, protocol, port = url_options.slice(:host, :protocol, :port).values
URI::HTTP.build(scheme: protocol, host: host, port: port, path: path).to_s
end
private
def available?(feature, project, current_user)
project.feature_available?(feature, current_user)
end
end
end
end
......@@ -117,7 +117,7 @@ module API
params do
requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
end
get ":id/issues/:issue_iid" do
get ":id/issues/:issue_iid", as: :api_v4_project_issue do
issue = find_project_issue(params[:issue_iid])
present issue, with: Entities::Issue, current_user: current_user, project: user_project
end
......
......@@ -274,11 +274,41 @@ module API
expose :job_events, as: :build_events
end
class Issue < ::API::Entities::Issue
class ProjectEntity < Grape::Entity
expose :id, :iid
expose(:project_id) { |entity| entity&.project.try(:id) }
expose :title, :description
expose :state, :created_at, :updated_at
end
class IssueBasic < ProjectEntity
expose :label_names, as: :labels
expose :milestone, using: ::API::Entities::Milestone
expose :assignees, :author, using: ::API::Entities::UserBasic
expose :assignee, using: ::API::Entities::UserBasic do |issue, options|
issue.assignees.first
end
expose :user_notes_count
expose :upvotes, :downvotes
expose :due_date
expose :confidential
expose :weight, if: ->(issue, _) { issue.supports_weight? }
expose :web_url do |issue, options|
Gitlab::UrlBuilder.build(issue)
end
end
class Issue < IssueBasic
unexpose :assignees
expose :assignee do |issue, options|
::API::Entities::UserBasic.represent(issue.assignees.first, options)
end
expose :subscribed do |issue, options|
issue.subscribed?(options[:current_user], options[:project] || issue.project)
end
end
end
end
......
......@@ -697,6 +697,19 @@ describe API::Issues do
expect(json_response['confidential']).to be_falsy
end
context 'links exposure' do
it 'exposes related resources full URIs' do
get api("/projects/#{project.id}/issues/#{issue.iid}", user)
links = json_response['_links']
expect(links['self']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}")
expect(links['notes']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}/notes")
expect(links['award_emoji']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}/award_emoji")
expect(links['project']).to end_with("/api/v4/projects/#{project.id}")
end
end
it "returns a project issue by internal id" do
get api("/projects/#{project.id}/issues/#{issue.iid}", user)
......
......@@ -866,6 +866,38 @@ describe API::Projects do
expect(json_response).not_to include("import_error")
end
context 'links exposure' do
it 'exposes related resources full URIs' do
get api("/projects/#{project.id}", user)
links = json_response['_links']
expect(links['self']).to end_with("/api/v4/projects/#{project.id}")
expect(links['issues']).to end_with("/api/v4/projects/#{project.id}/issues")
expect(links['merge_requests']).to end_with("/api/v4/projects/#{project.id}/merge_requests")
expect(links['repo_branches']).to end_with("/api/v4/projects/#{project.id}/repository/branches")
expect(links['labels']).to end_with("/api/v4/projects/#{project.id}/labels")
expect(links['events']).to end_with("/api/v4/projects/#{project.id}/events")
expect(links['members']).to end_with("/api/v4/projects/#{project.id}/members")
end
it 'filters related URIs when their feature is not enabled' do
project = create(:empty_project, :public,
:merge_requests_disabled,
:issues_disabled,
creator_id: user.id,
namespace: user.namespace)
get api("/projects/#{project.id}", user)
links = json_response['_links']
expect(links.has_key?('merge_requests')).to be_falsy
expect(links.has_key?('issues')).to be_falsy
expect(links['self']).to end_with("/api/v4/projects/#{project.id}")
end
end
describe 'permissions' do
context 'all projects' do
before do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment