yandex_tracker 0.1.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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +22 -0
  4. data/CHANGELOG.md +3 -0
  5. data/Gemfile +17 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +122 -0
  8. data/Rakefile +12 -0
  9. data/lib/yandex_tracker/auth.rb +76 -0
  10. data/lib/yandex_tracker/client.rb +89 -0
  11. data/lib/yandex_tracker/collections/attachments.rb +43 -0
  12. data/lib/yandex_tracker/collections/base.rb +30 -0
  13. data/lib/yandex_tracker/collections/categories.rb +34 -0
  14. data/lib/yandex_tracker/collections/comments.rb +44 -0
  15. data/lib/yandex_tracker/collections/fields.rb +38 -0
  16. data/lib/yandex_tracker/collections/issues.rb +49 -0
  17. data/lib/yandex_tracker/collections/local_fields.rb +39 -0
  18. data/lib/yandex_tracker/collections/queues.rb +34 -0
  19. data/lib/yandex_tracker/collections/resolutions.rb +29 -0
  20. data/lib/yandex_tracker/collections/users.rb +34 -0
  21. data/lib/yandex_tracker/collections/workflows.rb +29 -0
  22. data/lib/yandex_tracker/configuration.rb +66 -0
  23. data/lib/yandex_tracker/errors.rb +30 -0
  24. data/lib/yandex_tracker/objects/attachment.rb +24 -0
  25. data/lib/yandex_tracker/objects/base.rb +81 -0
  26. data/lib/yandex_tracker/objects/category.rb +16 -0
  27. data/lib/yandex_tracker/objects/comment.rb +27 -0
  28. data/lib/yandex_tracker/objects/field.rb +16 -0
  29. data/lib/yandex_tracker/objects/issue.rb +37 -0
  30. data/lib/yandex_tracker/objects/local_field.rb +16 -0
  31. data/lib/yandex_tracker/objects/queue.rb +29 -0
  32. data/lib/yandex_tracker/objects/resolution.rb +16 -0
  33. data/lib/yandex_tracker/objects/user.rb +16 -0
  34. data/lib/yandex_tracker/objects/workflow.rb +16 -0
  35. data/lib/yandex_tracker/resources/attachment.rb +54 -0
  36. data/lib/yandex_tracker/resources/base.rb +72 -0
  37. data/lib/yandex_tracker/resources/category.rb +22 -0
  38. data/lib/yandex_tracker/resources/comment.rb +22 -0
  39. data/lib/yandex_tracker/resources/field.rb +22 -0
  40. data/lib/yandex_tracker/resources/issue.rb +42 -0
  41. data/lib/yandex_tracker/resources/local_field.rb +22 -0
  42. data/lib/yandex_tracker/resources/queue.rb +22 -0
  43. data/lib/yandex_tracker/resources/resolution.rb +18 -0
  44. data/lib/yandex_tracker/resources/user.rb +22 -0
  45. data/lib/yandex_tracker/resources/workflow.rb +18 -0
  46. data/lib/yandex_tracker/version.rb +5 -0
  47. data/lib/yandex_tracker.rb +63 -0
  48. data/sig/yandex_tracker.rbs +4 -0
  49. data/yandex_tracker.gemspec +40 -0
  50. metadata +150 -0
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YandexTracker
4
+ module Collections
5
+ #
6
+ # Collections::Resolutions
7
+ #
8
+ class Resolutions < Base
9
+ def initialize(client)
10
+ super
11
+ @resource = Resources::Resolution.new(client)
12
+ end
13
+
14
+ def find(id)
15
+ response = resource.find(id)
16
+ build_object(Objects::Resolution, response)
17
+ end
18
+
19
+ def list(**params)
20
+ response = resource.list(**params)
21
+ build_objects(Objects::Resolution, response)
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :resource
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YandexTracker
4
+ module Collections
5
+ #
6
+ # Collections::Users
7
+ #
8
+ class Users < Base
9
+ def initialize(client)
10
+ super
11
+ @resource = Resources::User.new(client)
12
+ end
13
+
14
+ def find(id)
15
+ response = resource.find(id)
16
+ build_object(Objects::User, response)
17
+ end
18
+
19
+ def myself(**params)
20
+ response = resource.myself(**params)
21
+ build_object(Objects::User, response)
22
+ end
23
+
24
+ def list(**params)
25
+ response = resource.list(**params)
26
+ build_objects(Objects::User, response)
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :resource
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YandexTracker
4
+ module Collections
5
+ #
6
+ # Collections::Workflows
7
+ #
8
+ class Workflows < Base
9
+ def initialize(client)
10
+ super
11
+ @resource = Resources::Workflow.new(client)
12
+ end
13
+
14
+ def find(id)
15
+ response = resource.find(id)
16
+ build_object(Objects::Workflow, response)
17
+ end
18
+
19
+ def list(**params)
20
+ response = resource.list(**params)
21
+ build_objects(Objects::Workflow, response)
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :resource
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YandexTracker
4
+ #
5
+ # Handles API client configuration and validation
6
+ #
7
+ class Configuration
8
+ attr_accessor :client_id, :client_secret,
9
+ :cloud_org_id, :org_id,
10
+ :access_token, :refresh_token
11
+
12
+ def validate!
13
+ validate_org_configuration!
14
+ validate_auth_configuration!
15
+ end
16
+
17
+ def update_tokens(access_token:, refresh_token:, expires_in:)
18
+ @access_token = access_token
19
+ @refresh_token = refresh_token
20
+ @token_expires_at = Time.now + expires_in
21
+ end
22
+
23
+ def token_expired?
24
+ return false unless @token_expires_at
25
+
26
+ Time.now >= (@token_expires_at - 300)
27
+ end
28
+
29
+ def can_refresh?
30
+ !@refresh_token.nil?
31
+ end
32
+
33
+ def additional_headers
34
+ headers = {}
35
+ headers["X-Cloud-Org-ID"] = cloud_org_id if cloud_org_id
36
+ headers["X-Org-Id"] = org_id if org_id && !cloud_org_id
37
+ headers
38
+ end
39
+
40
+ def can_perform_oauth?
41
+ valid_oauth_auth?
42
+ end
43
+
44
+ private
45
+
46
+ def validate_org_configuration!
47
+ return if cloud_org_id || org_id
48
+
49
+ raise Errors::ConfigurationError, "Required configuration missing: either cloud_org_id or org_id must be set"
50
+ end
51
+
52
+ def validate_auth_configuration!
53
+ return if valid_token_auth? || valid_oauth_auth?
54
+
55
+ raise Errors::ConfigurationError, "Either access_token or (client_id + client_secret) must be set"
56
+ end
57
+
58
+ def valid_token_auth?
59
+ access_token
60
+ end
61
+
62
+ def valid_oauth_auth?
63
+ client_id && client_secret
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YandexTracker
4
+ #
5
+ # Custom error classes and error formatting for the API client
6
+ #
7
+ module Errors
8
+ class ApiError < StandardError; end
9
+ class AuthError < ApiError; end
10
+ class Unauthorized < ApiError; end
11
+ class NotFound < ApiError; end
12
+ class TimeoutError < ApiError; end
13
+ class ConnectionError < ApiError; end
14
+ class ConfigurationError < StandardError; end
15
+ class Error < StandardError; end
16
+ class ArgumentError < Error; end
17
+ class ResourceError < Error; end
18
+ class ContextError < Error; end
19
+
20
+ module_function
21
+
22
+ def format_message(body)
23
+ return body.to_s unless body.is_a?(Hash)
24
+
25
+ # TODO: {"errors"=>{"type"=>"Требуется параметр.", "category"=>"Требуется параметр."}, \
26
+ # "errorsData"=>{}, "errorMessages"=>[], "statusCode"=>422}
27
+ body["errorMessages"]&.join(", ") || body["message"] || body.to_s
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YandexTracker
4
+ module Objects
5
+ #
6
+ # Objects::Attachment
7
+ #
8
+ class Attachment < Base
9
+ def download
10
+ resource.get(data["content"])
11
+ end
12
+
13
+ def thumbnail
14
+ resource.get(data["thumbnail"])
15
+ end
16
+
17
+ private
18
+
19
+ def resource
20
+ @resource ||= Resources::Attachment.new(client)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YandexTracker
4
+ module Objects
5
+ #
6
+ # Objects::Base
7
+ #
8
+ class Base
9
+ attr_reader :client, :data, :context
10
+
11
+ def initialize(client, data, context = {})
12
+ @client = client
13
+ @data = data
14
+ @context = context
15
+ refresh_from(data)
16
+ end
17
+
18
+ def id
19
+ data["id"]
20
+ end
21
+
22
+ def method_missing(name, *args)
23
+ key = name.to_s
24
+ return wrap_value(data[key]) if data.key?(key)
25
+
26
+ super
27
+ end
28
+
29
+ def respond_to_missing?(name, include_private = false)
30
+ data.key?(name.to_s) || super
31
+ end
32
+
33
+ # fetch full object from .self
34
+ def expand
35
+ return self unless data["self"]
36
+
37
+ response = client.conn.get(data["self"]).body
38
+ refresh_from(response)
39
+ end
40
+
41
+ protected
42
+
43
+ def refresh_from(new_data)
44
+ @data = new_data
45
+ self
46
+ end
47
+
48
+ private
49
+
50
+ def wrap_value(value)
51
+ case value
52
+ when Hash then wrap_hash(value)
53
+ when Array then value.map { |v| wrap_value(v) }
54
+ else value
55
+ end
56
+ end
57
+
58
+ def wrap_hash(hash)
59
+ return hash unless hash["self"]
60
+
61
+ resource_type = hash["self"].split("/").last(2).first
62
+ object_class = classify(resource_type)
63
+
64
+ return hash unless object_class
65
+
66
+ object_class.new(client, hash, context)
67
+ end
68
+
69
+ def classify(type)
70
+ class_name = type.split(/(?=[A-Z])/).map(&:capitalize).join.chomp("s")
71
+ Objects.const_get(class_name)
72
+ rescue NameError
73
+ nil
74
+ end
75
+
76
+ def build_objects(klass, data)
77
+ data.map { |item| klass.new(client, item, context) }
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YandexTracker
4
+ module Objects
5
+ #
6
+ # Objects::Category
7
+ #
8
+ class Category < Base
9
+ private
10
+
11
+ def resource
12
+ @resource ||= Resources::Category.new(client)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YandexTracker
4
+ module Objects
5
+ #
6
+ # Objects::Comment
7
+ #
8
+ class Comment < Base
9
+ def attachments
10
+ @attachments ||= Collections::Attachments.new(client, context[:issue_id], comment_id: id)
11
+ end
12
+
13
+ def update(**attributes)
14
+ raise ArgumentError, "issue_id is required" unless context[:issue_id]
15
+
16
+ response = resource.update(context[:issue_id], id, **attributes)
17
+ refresh_from(response)
18
+ end
19
+
20
+ private
21
+
22
+ def resource
23
+ @resource ||= Resources::Comment.new(client)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YandexTracker
4
+ module Objects
5
+ #
6
+ # Objects::Field
7
+ #
8
+ class Field < Base
9
+ private
10
+
11
+ def resource
12
+ @resource ||= Resources::Field.new(client)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YandexTracker
4
+ module Objects
5
+ #
6
+ # Objects::Issue
7
+ #
8
+ class Issue < Base
9
+ def comments
10
+ @comments ||= Collections::Comments.new(client, id)
11
+ end
12
+
13
+ def attachments
14
+ if data["attachments"]
15
+ build_objects(Objects::Attachment, data["attachments"])
16
+ else
17
+ Collections::Attachments.new(client, id)
18
+ end
19
+ end
20
+
21
+ def update(**attributes)
22
+ response = resource.update(id, attributes)
23
+ refresh_from(response)
24
+ end
25
+
26
+ def transition(transition_id, **attributes)
27
+ resource.transition(id, transition_id, **attributes)
28
+ end
29
+
30
+ private
31
+
32
+ def resource
33
+ @resource ||= Resources::Issue.new(client)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YandexTracker
4
+ module Objects
5
+ #
6
+ # Objects::LocalField
7
+ #
8
+ class LocalField < Base
9
+ private
10
+
11
+ def resource
12
+ @resource ||= Resources::LocalField.new(client)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YandexTracker
4
+ module Objects
5
+ #
6
+ # Objects::Queue
7
+ #
8
+ class Queue < Base
9
+ def issues
10
+ @issues ||= Collections::Issues.new(client, id)
11
+ end
12
+
13
+ def update(**attributes)
14
+ response = resource.update(id, attributes)
15
+ refresh_from(response)
16
+ end
17
+
18
+ def local_fields
19
+ @local_fields ||= Collections::LocalFields.new(client, id)
20
+ end
21
+
22
+ private
23
+
24
+ def resource
25
+ @resource ||= Resources::Queue.new(client)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YandexTracker
4
+ module Objects
5
+ #
6
+ # Objects::Resolution
7
+ #
8
+ class Resolution < Base
9
+ private
10
+
11
+ def resource
12
+ @resource ||= Resources::Resolution.new(client)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YandexTracker
4
+ module Objects
5
+ #
6
+ # Objects::User
7
+ #
8
+ class User < Base
9
+ private
10
+
11
+ def resource
12
+ @resource ||= Resources::User.new(client)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YandexTracker
4
+ module Objects
5
+ #
6
+ # Objects::Workflow
7
+ #
8
+ class Workflow < Base
9
+ private
10
+
11
+ def resource
12
+ @resource ||= Resources::Workflow.new(client)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YandexTracker
4
+ module Resources
5
+ #
6
+ # Resources::Attachment
7
+ #
8
+ class Attachment < Base
9
+ # Create unattached file
10
+ def create(file, **attributes)
11
+ upload("attachments", file, attributes)
12
+ end
13
+
14
+ # Upload file directly to issue
15
+ def create_for_issue(issue_id, file, **attributes)
16
+ upload("issues/#{issue_id}/attachments", file, attributes)
17
+ end
18
+
19
+ # Upload file directly to comment
20
+ def create_for_comment(issue_id, comment_id, file, **attributes)
21
+ upload("issues/#{issue_id}/comments/#{comment_id}/attachments", file, attributes)
22
+ end
23
+
24
+ def find(id)
25
+ get("attachments/#{id}")
26
+ end
27
+
28
+ def list(issue_id, **params)
29
+ get("issues/#{issue_id}/attachments", params)
30
+ end
31
+
32
+ private
33
+
34
+ def upload(path, file, attributes)
35
+ form = {
36
+ file: Faraday::Multipart::FilePart.new(
37
+ file.path,
38
+ mime_type(file),
39
+ File.basename(file)
40
+ )
41
+ }.merge(attributes)
42
+
43
+ handle_response client.multipart_conn.post(path, form)
44
+ end
45
+
46
+ def mime_type(file)
47
+ return file.content_type if file.respond_to?(:content_type)
48
+
49
+ require "mime/types"
50
+ MIME::Types.type_for(file.path).first&.content_type || "application/octet-stream"
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module YandexTracker
6
+ module Resources
7
+ #
8
+ # Resources::Base
9
+ #
10
+ class Base
11
+ attr_reader :client
12
+
13
+ def initialize(client)
14
+ @client = client
15
+ end
16
+
17
+ def get(path, params = {}, query_params = {})
18
+ handle_response client.conn.get(prepare_path(path, query_params), params)
19
+ end
20
+
21
+ def post(path, body = {}, query_params = {})
22
+ handle_response client.conn.post(prepare_path(path, query_params), body)
23
+ end
24
+
25
+ def put(path, body = {}, query_params = {})
26
+ handle_response client.conn.put(prepare_path(path, query_params), body)
27
+ end
28
+
29
+ def patch(path, body = {}, query_params = {})
30
+ handle_response client.conn.patch(prepare_path(path, query_params), body)
31
+ end
32
+
33
+ def delete(path, params = {}, query_params = {})
34
+ handle_response client.conn.delete(prepare_path(path, query_params), params)
35
+ end
36
+
37
+ private
38
+
39
+ def handle_response(response)
40
+ return response.body if response.success?
41
+
42
+ handle_error_response(response)
43
+ rescue Faraday::TimeoutError
44
+ raise Errors::TimeoutError, "Request timed out"
45
+ rescue Faraday::ConnectionFailed => e
46
+ raise Errors::ConnectionError, "Connection failed: #{e.message}"
47
+ end
48
+
49
+ def handle_error_response(response)
50
+ puts response.body
51
+ case response.status
52
+ when 401, 403 then raise Errors::Unauthorized, Errors.format_message(response.body)
53
+ when 404 then raise Errors::NotFound, Errors.format_message(response.body)
54
+ else raise Errors::ApiError, Errors.format_message(response.body)
55
+ end
56
+ end
57
+
58
+ def encode_path(path)
59
+ URI.encode_www_form_component(path.to_s)
60
+ rescue URI::InvalidURIError => e
61
+ raise Errors::ApiError, "Invalid path: #{e.message}"
62
+ end
63
+
64
+ def prepare_path(path, query_params = {})
65
+ segments = path.split("/").map { |segment| encode_path(segment) }
66
+ path = segments.join("/")
67
+ path = "#{path}?#{URI.encode_www_form(query_params)}" unless query_params.empty?
68
+ path
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YandexTracker
4
+ module Resources
5
+ #
6
+ # Resources::Category
7
+ #
8
+ class Category < Base
9
+ def list(**params)
10
+ get("fields/categories", params)
11
+ end
12
+
13
+ def create(**attributes)
14
+ post("fields/categories", attributes)
15
+ end
16
+
17
+ def find(id)
18
+ get("fields/categories/#{id}")
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YandexTracker
4
+ module Resources
5
+ #
6
+ # Resources::Comment
7
+ #
8
+ class Comment < Base
9
+ def create(issue_id, **attributes)
10
+ post("issues/#{issue_id}/comments", attributes)
11
+ end
12
+
13
+ def update(issue_id, comment_id, **attributes)
14
+ patch("issues/#{issue_id}/comments/#{comment_id}", attributes)
15
+ end
16
+
17
+ def list(issue_id, **params)
18
+ get("issues/#{issue_id}/comments", params)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YandexTracker
4
+ module Resources
5
+ #
6
+ # Resources::Field
7
+ #
8
+ class Field < Base
9
+ def list(**params)
10
+ get("fields", params)
11
+ end
12
+
13
+ def create(**attributes)
14
+ post("fields", attributes)
15
+ end
16
+
17
+ def find(id)
18
+ get("fields/#{id}")
19
+ end
20
+ end
21
+ end
22
+ end