Commit 47477157 authored by Sean McGivern's avatar Sean McGivern

Merge branch '9643-jira-api-serializer' into 'master'

Create serializer for Jira API JSON payload

See merge request gitlab-org/gitlab-ee!14560
parents 06a55642 56f0cd6e
......@@ -31,7 +31,7 @@ class JiraService < IssueTrackerService
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
def self.reference_pattern(only_long: true)
@reference_pattern ||= /(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)/
@reference_pattern ||= /(?<issue>\b#{Gitlab::Regex.jira_issue_key_regex})/
end
def initialize_properties
......
......@@ -8,7 +8,18 @@ module Atlassian
@shared_secret = shared_secret
end
def store_dev_info(dev_info_json)
def store_dev_info(project:, commits: nil, branches: nil, merge_requests: nil)
dev_info_json = {
repositories: [
Serializers::RepositoryEntity.represent(
project,
commits: commits,
branches: branches,
merge_requests: merge_requests
)
]
}.to_json
uri = URI.join(@base_uri, '/rest/devinfo/0.10/bulk')
headers = {
......
# frozen_string_literal: true
module Atlassian
module JiraConnect
module Serializers
class AuthorEntity < Grape::Entity
include Gitlab::Routing
expose :name
expose :email
with_options(unless: -> (user) { user.is_a?(CommitEntity::CommitAuthor) }) do
expose :username
expose :url do |user|
user_url(user)
end
expose :avatar do |user|
user.avatar_url(only_path: false)
end
end
end
end
end
end
# frozen_string_literal: true
module Atlassian
module JiraConnect
module Serializers
class BaseEntity < Grape::Entity
include Gitlab::Routing
include GitlabRoutingHelper
format_with(:string) { |value| value.to_s }
expose :monotonic_time, as: :updateSequenceId
private
def monotonic_time
Gitlab::Metrics::System.monotonic_time.to_i
end
end
end
end
end
# frozen_string_literal: true
module Atlassian
module JiraConnect
module Serializers
class BranchEntity < BaseEntity
expose :target, as: :id
expose :issueKeys do |branch|
JiraIssueKeyExtractor.new(branch.name).issue_keys
end
expose :name
expose :lastCommit, using: JiraConnect::Serializers::CommitEntity do |branch, options|
options[:project].commit(branch.dereferenced_target)
end
expose :url do |branch, options|
project_commits_url(options[:project], branch.name)
end
expose :createPullRequestUrl do |branch, options|
project_new_merge_request_url(
options[:project],
merge_request: {
source_branch: branch.name
}
)
end
end
end
end
end
# frozen_string_literal: true
module Atlassian
module JiraConnect
module Serializers
class CommitEntity < BaseEntity
CommitAuthor = Struct.new(:name, :email)
expose :id
expose :issueKeys do |commit|
JiraIssueKeyExtractor.new(commit.safe_message).issue_keys
end
expose :id, as: :hash
expose :short_id, as: :displayId
expose :safe_message, as: :message
expose :flags do |commit|
if commit.merge_commit?
['MERGE_COMMIT']
else
[]
end
end
expose :author, using: JiraConnect::Serializers::AuthorEntity
expose :fileCount do |commit|
commit.stats.total
end
expose :files do |commit, options|
files = commit.diffs(max_files: 10).diff_files
JiraConnect::Serializers::FileEntity.represent files, options.merge(commit: commit)
end
expose :created_at, as: :authorTimestamp
expose :url do |commit, options|
project_commits_url(options[:project], commit.id)
end
private
def author
object.author || CommitAuthor.new(object.author_name, object.author_email)
end
end
end
end
end
# frozen_string_literal: true
module Atlassian
module JiraConnect
module Serializers
class FileEntity < Grape::Entity
include Gitlab::Routing
expose :path do |file|
file.deleted_file? ? file.old_path : file.new_path
end
expose :changeType do |file|
if file.new_file?
'ADDED'
elsif file.deleted_file?
'DELETED'
elsif file.renamed_file?
'MOVED'
else
'MODIFIED'
end
end
expose :added_lines, as: :linesAdded
expose :removed_lines, as: :linesRemoved
expose :url do |file, options|
file_path = if file.deleted_file?
File.join(options[:commit].parent_id, file.old_path)
else
File.join(options[:commit].id, file.new_path)
end
project_blob_url(options[:project], file_path)
end
end
end
end
end
# frozen_string_literal: true
module Atlassian
module JiraConnect
module Serializers
class PullRequestEntity < BaseEntity
STATUS_MAPPING = {
'opened' => 'OPEN',
'locked' => 'OPEN',
'merged' => 'MERGED',
'closed' => 'DECLINED'
}.freeze
expose :id, format_with: :string
expose :issueKeys do |mr|
JiraIssueKeyExtractor.new(mr.title, mr.description).issue_keys
end
expose :displayId do |mr|
mr.to_reference(full: true)
end
expose :title
expose :author, using: JiraConnect::Serializers::AuthorEntity
expose :user_notes_count, as: :commentCount
expose :source_branch, as: :sourceBranch
expose :target_branch, as: :destinationBranch
expose :lastUpdate do |mr|
mr.last_edited_at || mr.created_at
end
expose :status do |mr|
STATUS_MAPPING[mr.state] || 'UNKNOWN'
end
expose :sourceBranchUrl do |mr|
project_commits_url(mr.project, mr.source_branch)
end
expose :url do |mr|
merge_request_url(mr)
end
end
end
end
end
# frozen_string_literal: true
module Atlassian
module JiraConnect
module Serializers
class RepositoryEntity < BaseEntity
expose :id, format_with: :string
expose :name
expose :description
expose :url do |project|
project_url(project)
end
expose :avatar do |project|
project.avatar_url(only_path: false)
end
expose :commits do |project, options|
JiraConnect::Serializers::CommitEntity.represent options[:commits], project: project
end
expose :branches do |project, options|
JiraConnect::Serializers::BranchEntity.represent options[:branches], project: project
end
expose :pullRequests do |project, options|
JiraConnect::Serializers::PullRequestEntity.represent options[:merge_requests], project: project
end
end
end
end
end
# frozen_string_literal: true
module Atlassian
class JiraIssueKeyExtractor
def initialize(*text)
@text = text.join(' ')
end
def issue_keys
@text.scan(Gitlab::Regex.jira_issue_key_regex).uniq
end
end
end
{
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string" },
"username": { "type": "string" },
"url": { "type": "uri" },
"avatar": { "type": "uri" }
},
"required": [ "name", "email" ],
"additionalProperties": false
}
{
"type": "object",
"properties": {
"id": { "type": "string" },
"issueKeys": { "type": "array" },
"name": { "type": "string" },
"lastCommit": {
"$ref": "./commit.json"
},
"url": { "type": "uri" },
"createPullRequestUrl": { "type": "uri" },
"updateSequenceId": { "type": "integer" }
},
"required": [
"id", "issueKeys", "name", "lastCommit",
"url", "createPullRequestUrl", "updateSequenceId"
],
"additionalProperties": false
}
{
"type": "object",
"properties": {
"id": { "type": "string" },
"issueKeys": { "type": "array" },
"hash": { "type": "string" },
"displayId": { "type": "string" },
"message": { "type": "string" },
"flags": { "type": "array" },
"author": {
"$ref": "./author.json"
},
"fileCount": { "type": "integer" },
"files": {
"type": "array",
"items": {
"$ref": "./file.json"
}
},
"authorTimestamp": { "type": "timestamp" },
"url": { "type": "uri" },
"updateSequenceId": { "type": "integer" }
},
"required": [
"id", "issueKeys", "hash", "displayId", "message", "flags", "author",
"fileCount", "files", "authorTimestamp", "url", "updateSequenceId"
],
"additionalProperties": false
}
{
"type": "object",
"properties": {
"path": { "type": "string" },
"changeType": { "type": "string" },
"linesAdded": { "type": "integer" },
"linesRemoved": { "type": "integer" },
"url": { "type": "uri" }
},
"required": [
"path", "changeType", "linesAdded", "linesRemoved", "url"
],
"additionalProperties": false
}
{
"type": "object",
"properties": {
"id": { "type": "string" },
"issueKeys": { "type": "array" },
"displayId": { "type": "string" },
"title": { "type": "string" },
"author": {
"$ref": "./author.json"
},
"commentCount": { "type": "integer" },
"sourceBranch": { "type": "string" },
"destinationBranch": { "type": "string" },
"lastUpdate": { "type": "timestamp" },
"status": { "type": "string" },
"sourceBranchUrl": { "type": "uri" },
"url": { "type": "uri" },
"updateSequenceId": { "type": "integer" }
},
"required": [
"id", "issueKeys", "displayId", "title", "author", "commentCount",
"sourceBranch", "destinationBranch", "lastUpdate", "status",
"sourceBranchUrl", "url", "updateSequenceId"
],
"additionalProperties": false
}
{
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"description": { "type": ["string", "null"] },
"url": { "type": "uri" },
"avatar": { "type": "uri" },
"commits": {
"type": "array",
"items": {
"$ref": "./commit.json"
}
},
"branches": {
"type": "array",
"items": {
"$ref": "./branch.json"
}
},
"pullRequests": {
"type": "array",
"items": {
"$ref": "./pull_request.json"
}
},
"updateSequenceId": { "type": "integer" }
},
"required": [
"id", "name", "description", "url", "avatar",
"commits", "branches", "pullRequests", "updateSequenceId"
],
"additionalProperties": false
}
......@@ -13,12 +13,6 @@ describe Atlassian::JiraConnect::Client do
describe '#store_dev_info' do
it "calls the API with auth headers" do
dev_info_json = {
repositories: [
name: 'atlassian-connect-jira-example'
]
}.to_json
expected_jwt = Atlassian::Jwt.encode(
Atlassian::Jwt.build_claims(
issuer: Atlassian::JiraConnect.app_key,
......@@ -30,14 +24,13 @@ describe Atlassian::JiraConnect::Client do
stub_full_request('https://gitlab-test.atlassian.net/rest/devinfo/0.10/bulk', method: :post)
.with(
body: dev_info_json,
headers: {
'Authorization' => "JWT #{expected_jwt}",
'Content-Type' => 'application/json'
}
)
subject.store_dev_info(dev_info_json)
subject.store_dev_info(project: create(:project))
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Atlassian::JiraConnect::Serializers::AuthorEntity do
subject { described_class.represent(user).as_json }
context 'when object is a User model' do
let(:user) { build_stubbed(:user) }
it 'exposes all fields' do
expect(subject.keys).to contain_exactly(:name, :email, :username, :url, :avatar)
end
end
context 'when object is a CommitAuthor struct from a commit' do
let(:user) { Atlassian::JiraConnect::Serializers::CommitEntity::CommitAuthor.new('Full Name', 'user@example.com') }
it 'exposes name and email only' do
expect(subject.keys).to contain_exactly(:name, :email)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Atlassian::JiraConnect::Serializers::RepositoryEntity do
subject do
project = create(:project, :repository)
commits = [project.commit]
branches = [project.repository.find_branch('master')]
merge_requests = [create(:merge_request, source_project: project, target_project: project)]
described_class.represent(
project,
commits: commits,
branches: branches,
merge_requests: merge_requests
).to_json
end
it { is_expected.to match_schema('jira_connect/repository', dir: 'ee') }
end
# frozen_string_literal: true
require 'fast_spec_helper'
describe Atlassian::JiraIssueKeyExtractor do
describe '#issue_keys' do
subject { described_class.new('TEST-01 Some A-100 issue title OTHER-02 ABC!-1 that mentions Jira issue').issue_keys }
it 'returns all valid Jira issue keys' do
is_expected.to contain_exactly('TEST-01', 'OTHER-02')
end
context 'when multiple strings are passed in' do
subject { described_class.new('TEST-01 Some A-100', 'issue title OTHER', '-02 ABC!-1 that mentions Jira issue').issue_keys }
it 'returns all valid Jira issue keys in any of those string' do
is_expected.to contain_exactly('TEST-01')
end
end
end
end
......@@ -94,6 +94,12 @@ module Gitlab
}mx
end
# Based on Jira's project key format
# https://confluence.atlassian.com/adminjiraserver073/changing-the-project-key-format-861253229.html
def jira_issue_key_regex
@jira_issue_key_regex ||= /[A-Z][A-Z_0-9]+-\d+/
end
def jira_transition_id_regex
@jira_transition_id_regex ||= /\d+/
end
......
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