ruby-jira 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jira
4
+ class Client
5
+ # Defines methods related to project categories.
6
+ #
7
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-project-categories/
8
+ module ProjectCategories
9
+ # Returns all project categories
10
+ #
11
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-project-categories/#api-rest-api-3-projectcategory-get
12
+ #
13
+ # @return [Array<Hash>]
14
+ def project_categories
15
+ get("/projectCategory")
16
+ end
17
+
18
+ # Creates a project category
19
+ #
20
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-project-categories/#api-rest-api-3-projectcategory-post
21
+ #
22
+ # @param payload [Hash] Project category payload
23
+ # @return [Hash]
24
+ def create_project_category(payload = {})
25
+ post("/projectCategory", body: payload)
26
+ end
27
+
28
+ # Returns a project category by ID
29
+ #
30
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-project-categories/#api-rest-api-3-projectcategory-id-get
31
+ #
32
+ # @param id [Integer, String] The ID of the project category
33
+ # @return [Hash]
34
+ def project_category(id)
35
+ get("/projectCategory/#{url_encode(id)}")
36
+ end
37
+
38
+ # Updates a project category
39
+ #
40
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-project-categories/#api-rest-api-3-projectcategory-id-put
41
+ #
42
+ # @param id [Integer, String] The ID of the project category
43
+ # @param payload [Hash] Project category payload
44
+ # @return [Hash]
45
+ def update_project_category(id, payload = {})
46
+ put("/projectCategory/#{url_encode(id)}", body: payload)
47
+ end
48
+
49
+ # Deletes a project category
50
+ #
51
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-project-categories/#api-rest-api-3-projectcategory-id-delete
52
+ #
53
+ # @param id [Integer, String] The ID of the project category
54
+ # @return [nil]
55
+ def delete_project_category(id)
56
+ delete("/projectCategory/#{url_encode(id)}")
57
+ end
58
+ end
59
+ end
60
+ end
@@ -3,9 +3,13 @@
3
3
  module Jira
4
4
  class Client
5
5
  # Defines methods related to project permission schemes.
6
+ #
7
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-project-permission-schemes/
6
8
  module ProjectPermissionSchemes
7
9
  # Gets assigned issue security level scheme for a project
8
10
  #
11
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-project-permission-schemes/#api-rest-api-3-project-projectkeyorid-issuesecuritylevelscheme-get
12
+ #
9
13
  # @param project_key_or_id [Integer, String] Project ID or key
10
14
  # @param options [Hash] Query parameters
11
15
  # @return [Hash]
@@ -15,6 +19,8 @@ module Jira
15
19
 
16
20
  # Gets assigned permission scheme for a project
17
21
  #
22
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-project-permission-schemes/#api-rest-api-3-project-projectkeyorid-permissionscheme-get
23
+ #
18
24
  # @param project_key_or_id [Integer, String] Project ID or key
19
25
  # @param options [Hash] Query parameters
20
26
  # @return [Hash]
@@ -22,8 +28,20 @@ module Jira
22
28
  get("/project/#{url_encode(project_key_or_id)}/permissionscheme", query: options)
23
29
  end
24
30
 
31
+ # Returns security levels for a project
32
+ #
33
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-project-permission-schemes/#api-rest-api-3-project-projectkeyorid-securitylevel-get
34
+ #
35
+ # @param project_key_or_id [Integer, String] Project ID or key
36
+ # @return [Hash]
37
+ def project_security_levels(project_key_or_id)
38
+ get("/project/#{url_encode(project_key_or_id)}/securitylevel")
39
+ end
40
+
25
41
  # Assigns permission scheme to a project
26
42
  #
43
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-project-permission-schemes/#api-rest-api-3-project-projectkeyorid-permissionscheme-put
44
+ #
27
45
  # @param project_key_or_id [Integer, String] Project ID or key
28
46
  # @param scheme_id [Integer] Permission scheme ID
29
47
  # @param options [Hash] Additional payload
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jira
4
+ class Client
5
+ # Defines methods related to project properties.
6
+ #
7
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-project-properties/
8
+ module ProjectProperties
9
+ # Returns all property keys for a project
10
+ #
11
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-project-properties/#api-rest-api-3-project-projectidorkey-properties-get
12
+ #
13
+ # @param project_id_or_key [Integer, String] The ID or key of the project
14
+ # @return [Hash]
15
+ def project_property_keys(project_id_or_key)
16
+ get("/project/#{url_encode(project_id_or_key)}/properties")
17
+ end
18
+
19
+ # Returns the value of a project property
20
+ #
21
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-project-properties/#api-rest-api-3-project-projectidorkey-properties-propertykey-get
22
+ #
23
+ # @param project_id_or_key [Integer, String] The ID or key of the project
24
+ # @param property_key [String] The key of the property
25
+ # @return [Hash]
26
+ def project_property(project_id_or_key, property_key)
27
+ get("/project/#{url_encode(project_id_or_key)}/properties/#{url_encode(property_key)}")
28
+ end
29
+
30
+ # Sets the value of a project property
31
+ #
32
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-project-properties/#api-rest-api-3-project-projectidorkey-properties-propertykey-put
33
+ #
34
+ # @param project_id_or_key [Integer, String] The ID or key of the project
35
+ # @param property_key [String] The key of the property
36
+ # @param payload [Hash] The value to set
37
+ # @return [nil]
38
+ def set_project_property(project_id_or_key, property_key, payload = {})
39
+ put("/project/#{url_encode(project_id_or_key)}/properties/#{url_encode(property_key)}", body: payload)
40
+ end
41
+
42
+ # Deletes a project property
43
+ #
44
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-project-properties/#api-rest-api-3-project-projectidorkey-properties-propertykey-delete
45
+ #
46
+ # @param project_id_or_key [Integer, String] The ID or key of the project
47
+ # @param property_key [String] The key of the property
48
+ # @return [nil]
49
+ def delete_project_property(project_id_or_key, property_key)
50
+ delete("/project/#{url_encode(project_id_or_key)}/properties/#{url_encode(property_key)}")
51
+ end
52
+ end
53
+ end
54
+ end
@@ -3,9 +3,13 @@
3
3
  module Jira
4
4
  class Client
5
5
  # Defines methods related to projects.
6
+ #
7
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-projects/
6
8
  module Projects
7
9
  # Search projects
8
10
  #
11
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-projects/#api-rest-api-3-project-search-get
12
+ #
9
13
  # @param options [Hash] Query parameters
10
14
  # @return [Jira::Request::PaginatedResponse]
11
15
  def projects(options = {})
@@ -14,6 +18,8 @@ module Jira
14
18
 
15
19
  # Gets a single project
16
20
  #
21
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-projects/#api-rest-api-3-project-projectidorkey-get
22
+ #
17
23
  # @param project_id_or_key [Integer, String] Project ID or key
18
24
  # @param options [Hash] Query parameters
19
25
  # @return [Hash]
@@ -23,11 +29,65 @@ module Jira
23
29
 
24
30
  # Archives a project
25
31
  #
32
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-projects/#api-rest-api-3-project-projectidorkey-archive-post
33
+ #
26
34
  # @param project_id_or_key [Integer, String] Project ID or key
27
35
  # @return [Hash]
28
36
  def archive_project(project_id_or_key)
29
37
  post("/project/#{url_encode(project_id_or_key)}/archive")
30
38
  end
39
+
40
+ # Updates a project
41
+ #
42
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-projects/#api-rest-api-3-project-projectidorkey-put
43
+ #
44
+ # @param project_id_or_key [Integer, String] Project ID or key
45
+ # @param payload [Hash] Fields to update
46
+ # @return [Hash]
47
+ def update_project(project_id_or_key, payload = {})
48
+ put("/project/#{url_encode(project_id_or_key)}", body: payload)
49
+ end
50
+
51
+ # Deletes a project
52
+ #
53
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-projects/#api-rest-api-3-project-projectidorkey-delete
54
+ #
55
+ # @param project_id_or_key [Integer, String] Project ID or key
56
+ # @return [Hash]
57
+ def delete_project(project_id_or_key)
58
+ delete("/project/#{url_encode(project_id_or_key)}")
59
+ end
60
+
61
+ # Gets all issue types with their statuses for a project
62
+ #
63
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-projects/#api-rest-api-3-project-projectidorkey-statuses-get
64
+ #
65
+ # @param project_id_or_key [Integer, String] Project ID or key
66
+ # @return [Array<Hash>]
67
+ def project_statuses(project_id_or_key)
68
+ get("/project/#{url_encode(project_id_or_key)}/statuses")
69
+ end
70
+
71
+ # Gets the issue type hierarchy for a project
72
+ #
73
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-projects/#api-rest-api-3-project-projectid-hierarchy-get
74
+ #
75
+ # @param project_id [Integer, String] Project ID
76
+ # @return [Hash]
77
+ def project_issue_type_hierarchy(project_id)
78
+ get("/project/#{url_encode(project_id)}/hierarchy")
79
+ end
80
+
81
+ # Gets the notification scheme for a project
82
+ #
83
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-projects/#api-rest-api-3-project-projectkeyorid-notificationscheme-get
84
+ #
85
+ # @param project_key_or_id [Integer, String] Project key or ID
86
+ # @param options [Hash] Query parameters (e.g. expand:)
87
+ # @return [Hash]
88
+ def project_notification_scheme(project_key_or_id, options = {})
89
+ get("/project/#{url_encode(project_key_or_id)}/notificationscheme", query: options)
90
+ end
31
91
  end
32
92
  end
33
93
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jira
4
+ class Client
5
+ # Defines methods related to time tracking.
6
+ #
7
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-time-tracking/
8
+ module TimeTracking
9
+ # Returns the time tracking provider that is currently selected
10
+ #
11
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-time-tracking/#api-rest-api-3-configuration-timetracking-get
12
+ #
13
+ # @return [Hash]
14
+ def time_tracking_provider
15
+ get("/configuration/timetracking")
16
+ end
17
+
18
+ # Selects a time tracking provider
19
+ #
20
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-time-tracking/#api-rest-api-3-configuration-timetracking-put
21
+ #
22
+ # @param payload [Hash] Time tracking provider payload
23
+ # @return [nil]
24
+ def select_time_tracking_provider(payload = {})
25
+ put("/configuration/timetracking", body: payload)
26
+ end
27
+
28
+ # Returns all time tracking providers
29
+ #
30
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-time-tracking/#api-rest-api-3-configuration-timetracking-list-get
31
+ #
32
+ # @return [Array<Hash>]
33
+ def time_tracking_providers
34
+ get("/configuration/timetracking/list")
35
+ end
36
+
37
+ # Returns the time tracking settings
38
+ #
39
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-time-tracking/#api-rest-api-3-configuration-timetracking-options-get
40
+ #
41
+ # @return [Hash]
42
+ def time_tracking_settings
43
+ get("/configuration/timetracking/options")
44
+ end
45
+
46
+ # Sets the time tracking settings
47
+ #
48
+ # @url https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-time-tracking/#api-rest-api-3-configuration-timetracking-options-put
49
+ #
50
+ # @param payload [Hash] Time tracking settings payload
51
+ # @return [Hash]
52
+ def set_time_tracking_settings(payload = {})
53
+ put("/configuration/timetracking/options", body: payload)
54
+ end
55
+ end
56
+ end
57
+ end
data/lib/jira/client.rb CHANGED
@@ -4,9 +4,15 @@ module Jira
4
4
  class Client < API
5
5
  Dir[File.expand_path("client/*.rb", __dir__)].each { |file| require file }
6
6
 
7
+ include IssueComments
8
+ include IssueSearch
7
9
  include Issues
10
+ include IssueWorklogs
11
+ include ProjectCategories
8
12
  include ProjectPermissionSchemes
13
+ include ProjectProperties
9
14
  include Projects
15
+ include TimeTracking
10
16
 
11
17
  # Text representation of the client, masking auth secrets.
12
18
  #
@@ -21,6 +21,7 @@ module Jira
21
21
  ratelimit_retries
22
22
  ratelimit_base_delay
23
23
  ratelimit_max_delay
24
+ logger
24
25
  ].freeze
25
26
 
26
27
  DEFAULT_USER_AGENT = "Ruby Jira Gem #{Jira::VERSION}".freeze
@@ -63,6 +64,7 @@ module Jira
63
64
  self.ratelimit_max_delay = float_env("JIRA_RATELIMIT_MAX_DELAY", DEFAULT_RATELIMIT_MAX_DELAY)
64
65
  self.httparty = get_httparty_config(ENV.fetch("JIRA_HTTPARTY_OPTIONS", nil))
65
66
  self.user_agent = DEFAULT_USER_AGENT
67
+ self.logger = nil
66
68
  end
67
69
 
68
70
  private
data/lib/jira/error.rb CHANGED
@@ -11,6 +11,9 @@ module Jira
11
11
  # Raised when impossible to parse response body.
12
12
  class Parsing < Base; end
13
13
 
14
+ # Raised when pagination cannot continue safely.
15
+ class Pagination < Base; end
16
+
14
17
  # Custom error class for rescuing from HTTP response errors.
15
18
  class ResponseError < Base
16
19
  POSSIBLE_MESSAGE_KEYS = %i[message error_description error].freeze
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jira
4
+ # Provides a #log helper that delegates to the configured logger.
5
+ # Mixed into Request and pagination classes to emit debug information.
6
+ #
7
+ # @example Enable logging
8
+ # require "logger"
9
+ # Jira.configure { |c| c.logger = Logger.new($stdout) }
10
+ module Logging
11
+ def log(message)
12
+ Jira.logger&.debug("[ruby-jira] #{message}")
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jira
4
+ module Pagination
5
+ # Shared behavior for paginated collection wrappers.
6
+ module CollectionBehavior
7
+ def inspect
8
+ @array.inspect
9
+ end
10
+
11
+ def method_missing(name, *, &)
12
+ return @array.send(name, *, &) if @array.respond_to?(name)
13
+
14
+ super
15
+ end
16
+
17
+ def respond_to_missing?(method_name, include_private = false)
18
+ super || @array.respond_to?(method_name, include_private)
19
+ end
20
+
21
+ def each_page # rubocop:disable Metrics/MethodLength
22
+ return enum_for(:each_page) unless block_given?
23
+
24
+ current = self
25
+ seen_markers = {}
26
+ loop do
27
+ yield current
28
+ break unless current.has_next_page?
29
+
30
+ marker = current.pagination_progress_marker
31
+ check_repeated_marker!(seen_markers, marker)
32
+ seen_markers[marker] = true if marker
33
+ current = next_page_with_progress_check(current, marker)
34
+ end
35
+ end
36
+
37
+ def lazy_paginate
38
+ to_enum(:each_page).lazy.flat_map(&:to_ary)
39
+ end
40
+
41
+ def auto_paginate(&block)
42
+ return lazy_paginate.to_a unless block
43
+
44
+ lazy_paginate.each(&block)
45
+ end
46
+
47
+ def paginate_with_limit(limit, &block)
48
+ return lazy_paginate.take(limit).to_a unless block
49
+
50
+ lazy_paginate.take(limit).each(&block)
51
+ end
52
+
53
+ private
54
+
55
+ def check_repeated_marker!(seen_markers, marker)
56
+ return unless marker
57
+ return unless seen_markers.key?(marker)
58
+
59
+ raise Error::Pagination, "pagination cursor repeated: #{marker.inspect}"
60
+ end
61
+
62
+ def next_page_with_progress_check(current, marker)
63
+ next_page = current.next_page
64
+ raise Error::Pagination, "pagination returned nil page" unless next_page
65
+ return next_page unless marker
66
+ return next_page unless next_page.pagination_progress_marker == marker
67
+
68
+ raise Error::Pagination, "pagination did not advance from #{marker.inspect}"
69
+ end
70
+ end
71
+ end
72
+ end
@@ -1,10 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "uri"
4
+
3
5
  module Jira
4
- # Wrapper for Jira cursor-paginated responses (nextPageToken style).
6
+ # Wrapper for Jira cursor-paginated responses.
5
7
  #
6
8
  # Endpoints like POST /search/jql return a body of:
7
9
  # { nextPageToken: "token", total: int, <items_key>: [...] }
10
+ # Some Jira APIs use alternative cursor fields:
11
+ # { nextPageCursor: "cursor", ... } or { cursor: "...", nextPageCursor: "...", last: false, ... }
12
+ # Other APIs use URL-based cursor fields:
13
+ # { nextPage: "https://...", lastPage: false, ... }
8
14
  #
9
15
  # The items array key varies by endpoint (e.g. "issues", "worklogs").
10
16
  # This class detects it automatically as the first non-metadata Array value.
@@ -12,77 +18,87 @@ module Jira
12
18
  # Pagination is driven by a +next_page_fetcher+ proc set by the Request layer,
13
19
  # which re-issues the original request with +nextPageToken+ injected.
14
20
  class CursorPaginatedResponse
15
- METADATA_KEYS = %i[nextPageToken total self].freeze
21
+ include Logging
22
+ include Pagination::CollectionBehavior
23
+
24
+ METADATA_KEYS = %i[
25
+ nextPageToken nextPageCursor cursor nextPage total self isLast last lastPage expand warningMessages maxResults
26
+ startAt size
27
+ ].freeze
16
28
 
17
29
  attr_accessor :client, :next_page_fetcher
18
- attr_reader :next_page_token, :total
30
+ attr_reader :next_page_token, :next_page_cursor, :next_page_url, :total
19
31
 
20
- def initialize(body)
21
- @body = body
32
+ def initialize(body) # rubocop:disable Metrics/AbcSize
22
33
  @next_page_token = body[:nextPageToken]
34
+ @next_page_cursor = body[:nextPageCursor] || body[:cursor]
35
+ @next_page_url = body[:nextPage]
36
+ @is_last = body[:isLast]
37
+ @last = body[:last]
38
+ @last_page = body[:lastPage]
23
39
  @total = body.fetch(:total, 0).to_i
24
- @array = wrap_items(detect_items_array(body))
40
+ items_key, items = detect_items_array(body)
41
+ log "CursorPaginatedResponse: items_key=#{items_key.inspect} count=#{items.length}"
42
+ @array = wrap_items(items)
25
43
  end
26
44
 
27
- def inspect
28
- @array.inspect
45
+ def next_page?
46
+ return false if @is_last == true || @last == true || @last_page == true
47
+
48
+ !next_page_locator.to_s.empty?
29
49
  end
50
+ alias has_next_page? next_page?
30
51
 
31
- def method_missing(name, *, &)
32
- return @array.send(name, *, &) if @array.respond_to?(name)
52
+ def next_page
53
+ return nil unless has_next_page?
54
+ return next_page_by_link unless @next_page_url.to_s.empty?
55
+ raise Error::MissingCredentials, "next_page_fetcher not set on CursorPaginatedResponse" unless @next_page_fetcher
33
56
 
34
- super
57
+ @next_page_fetcher.call(next_page_locator)
35
58
  end
36
59
 
37
- def respond_to_missing?(method_name, include_private = false)
38
- super || @array.respond_to?(method_name, include_private)
39
- end
60
+ def cursor_parameter_key
61
+ return :nextPageToken unless @next_page_token.to_s.empty?
62
+ return :cursor unless @next_page_cursor.to_s.empty?
40
63
 
41
- def each_page
42
- current = self
43
- yield current
44
- while current.has_next_page?
45
- current = current.next_page
46
- yield current
47
- end
64
+ nil
48
65
  end
49
66
 
50
- def lazy_paginate
51
- to_enum(:each_page).lazy.flat_map(&:to_ary)
67
+ def fetcher_based_pagination?
68
+ @next_page_url.to_s.empty?
52
69
  end
53
70
 
54
- def auto_paginate(&block)
55
- return lazy_paginate.to_a unless block
56
-
57
- lazy_paginate.each(&block)
71
+ def pagination_progress_marker
72
+ next_page_locator
58
73
  end
59
74
 
60
- def paginate_with_limit(limit, &block)
61
- return lazy_paginate.take(limit).to_a unless block
75
+ private
62
76
 
63
- lazy_paginate.take(limit).each(&block)
64
- end
77
+ def next_page_locator
78
+ return @next_page_token unless @next_page_token.to_s.empty?
79
+ return @next_page_cursor unless @next_page_cursor.to_s.empty?
80
+ return @next_page_url unless @next_page_url.to_s.empty?
65
81
 
66
- def next_page?
67
- !@next_page_token.to_s.empty?
82
+ nil
68
83
  end
69
- alias has_next_page? next_page?
70
84
 
71
- def next_page
72
- return nil unless has_next_page?
73
- raise Error::MissingCredentials, "next_page_fetcher not set on CursorPaginatedResponse" unless @next_page_fetcher
85
+ def next_page_by_link
86
+ raise Error::MissingCredentials, "client not set on CursorPaginatedResponse" unless @client
74
87
 
75
- @next_page_fetcher.call(@next_page_token)
88
+ @client.get(client_relative_path(@next_page_url))
76
89
  end
77
90
 
78
- private
91
+ def client_relative_path(link)
92
+ client_endpoint_path = @client.api_request_path
93
+ URI.parse(link).request_uri.sub(client_endpoint_path, "")
94
+ end
79
95
 
80
96
  def detect_items_array(body)
81
97
  body.each do |key, value|
82
98
  next if METADATA_KEYS.include?(key)
83
- return value if value.is_a?(Array)
99
+ return [key, value] if value.is_a?(Array)
84
100
  end
85
- []
101
+ [nil, []]
86
102
  end
87
103
 
88
104
  def wrap_items(items)