asana 0.0.6 → 0.1.1

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 (67) hide show
  1. checksums.yaml +9 -9
  2. data/.codeclimate.yml +4 -0
  3. data/.gitignore +12 -20
  4. data/.rspec +4 -0
  5. data/.rubocop.yml +18 -0
  6. data/.travis.yml +12 -0
  7. data/.yardopts +5 -0
  8. data/CODE_OF_CONDUCT.md +13 -0
  9. data/Gemfile +17 -0
  10. data/Guardfile +85 -4
  11. data/LICENSE.txt +21 -0
  12. data/README.md +264 -135
  13. data/Rakefile +62 -7
  14. data/asana.gemspec +27 -21
  15. data/examples/Gemfile +6 -0
  16. data/examples/Gemfile.lock +56 -0
  17. data/examples/api_token.rb +21 -0
  18. data/examples/cli_app.rb +25 -0
  19. data/examples/events.rb +38 -0
  20. data/examples/omniauth_integration.rb +54 -0
  21. data/lib/asana.rb +8 -11
  22. data/lib/asana/authentication.rb +8 -0
  23. data/lib/asana/authentication/oauth2.rb +42 -0
  24. data/lib/asana/authentication/oauth2/access_token_authentication.rb +51 -0
  25. data/lib/asana/authentication/oauth2/bearer_token_authentication.rb +32 -0
  26. data/lib/asana/authentication/oauth2/client.rb +50 -0
  27. data/lib/asana/authentication/token_authentication.rb +20 -0
  28. data/lib/asana/client.rb +124 -0
  29. data/lib/asana/client/configuration.rb +165 -0
  30. data/lib/asana/errors.rb +90 -0
  31. data/lib/asana/http_client.rb +155 -0
  32. data/lib/asana/http_client/environment_info.rb +53 -0
  33. data/lib/asana/http_client/error_handling.rb +103 -0
  34. data/lib/asana/http_client/response.rb +32 -0
  35. data/lib/asana/resources.rb +11 -0
  36. data/lib/asana/resources/attachment.rb +44 -0
  37. data/lib/asana/resources/attachment_uploading.rb +33 -0
  38. data/lib/asana/resources/collection.rb +68 -0
  39. data/lib/asana/resources/event.rb +49 -0
  40. data/lib/asana/resources/event_subscription.rb +12 -0
  41. data/lib/asana/resources/events.rb +101 -0
  42. data/lib/asana/resources/project.rb +145 -19
  43. data/lib/asana/resources/registry.rb +62 -0
  44. data/lib/asana/resources/resource.rb +103 -0
  45. data/lib/asana/resources/response_helper.rb +14 -0
  46. data/lib/asana/resources/story.rb +58 -7
  47. data/lib/asana/resources/tag.rb +111 -19
  48. data/lib/asana/resources/task.rb +284 -57
  49. data/lib/asana/resources/team.rb +55 -0
  50. data/lib/asana/resources/user.rb +65 -10
  51. data/lib/asana/resources/workspace.rb +79 -34
  52. data/lib/asana/ruby2_0_0_compatibility.rb +3 -0
  53. data/lib/asana/version.rb +3 -1
  54. data/lib/templates/index.js +8 -0
  55. data/lib/templates/resource.ejs +225 -0
  56. data/package.json +7 -0
  57. metadata +91 -51
  58. data/LICENSE +0 -22
  59. data/lib/asana/config.rb +0 -23
  60. data/lib/asana/resource.rb +0 -52
  61. data/spec/asana/resources/project_spec.rb +0 -63
  62. data/spec/asana/resources/story_spec.rb +0 -39
  63. data/spec/asana/resources/tag_spec.rb +0 -63
  64. data/spec/asana/resources/task_spec.rb +0 -95
  65. data/spec/asana/resources/user_spec.rb +0 -64
  66. data/spec/asana/resources/workspace_spec.rb +0 -108
  67. data/spec/spec_helper.rb +0 -9
@@ -0,0 +1,155 @@
1
+ require 'faraday'
2
+ require 'faraday_middleware'
3
+ require 'faraday_middleware/multi_json'
4
+
5
+ require_relative 'http_client/error_handling'
6
+ require_relative 'http_client/environment_info'
7
+ require_relative 'http_client/response'
8
+
9
+ module Asana
10
+ # Internal: Wrapper over Faraday that abstracts authentication, request
11
+ # parsing and common options.
12
+ class HttpClient
13
+ # Internal: The API base URI.
14
+ BASE_URI = 'https://app.asana.com/api/1.0'
15
+
16
+ # Public: Initializes an HttpClient to make requests to the Asana API.
17
+ #
18
+ # authentication - [Asana::Authentication] An authentication strategy.
19
+ # adapter - [Symbol, Proc] A Faraday adapter, eiter a Symbol for
20
+ # registered adapters or a Proc taking a builder for a
21
+ # custom one. Defaults to Faraday.default_adapter.
22
+ # user_agent - [String] The user agent. Defaults to "ruby-asana vX.Y.Z".
23
+ # config - [Proc] An optional block that yields the Faraday builder
24
+ # object for customization.
25
+ def initialize(authentication: required('authentication'),
26
+ adapter: nil,
27
+ user_agent: nil,
28
+ debug_mode: false,
29
+ &config)
30
+ @authentication = authentication
31
+ @adapter = adapter || Faraday.default_adapter
32
+ @environment_info = EnvironmentInfo.new(user_agent)
33
+ @debug_mode = debug_mode
34
+ @config = config
35
+ end
36
+
37
+ # Public: Performs a GET request against the API.
38
+ #
39
+ # resource_uri - [String] the resource URI relative to the base Asana API
40
+ # URL, e.g "/users/me".
41
+ # params - [Hash] the request parameters
42
+ # options - [Hash] the request I/O options
43
+ #
44
+ # Returns an [Asana::HttpClient::Response] if everything went well.
45
+ # Raises [Asana::Errors::APIError] if anything went wrong.
46
+ def get(resource_uri, params: {}, options: {})
47
+ opts = options.reduce({}) do |acc, (k, v)|
48
+ acc.tap do |hash|
49
+ hash[:"opt_#{k}"] = v.is_a?(Array) ? v.join(',') : v
50
+ end
51
+ end
52
+ perform_request(:get, resource_uri, params.merge(opts))
53
+ end
54
+
55
+ # Public: Performs a PUT request against the API.
56
+ #
57
+ # resource_uri - [String] the resource URI relative to the base Asana API
58
+ # URL, e.g "/users/me".
59
+ # body - [Hash] the body to PUT.
60
+ # options - [Hash] the request I/O options
61
+ #
62
+ # Returns an [Asana::HttpClient::Response] if everything went well.
63
+ # Raises [Asana::Errors::APIError] if anything went wrong.
64
+ def put(resource_uri, body: {}, options: {})
65
+ params = { data: body }.merge(options.empty? ? {} : { options: options })
66
+ perform_request(:put, resource_uri, params)
67
+ end
68
+
69
+ # Public: Performs a POST request against the API.
70
+ #
71
+ # resource_uri - [String] the resource URI relative to the base Asana API
72
+ # URL, e.g "/tags".
73
+ # body - [Hash] the body to POST.
74
+ # upload - [Faraday::UploadIO] an upload object to post as multipart.
75
+ # Defaults to nil.
76
+ # options - [Hash] the request I/O options
77
+ #
78
+ # Returns an [Asana::HttpClient::Response] if everything went well.
79
+ # Raises [Asana::Errors::APIError] if anything went wrong.
80
+ def post(resource_uri, body: {}, upload: nil, options: {})
81
+ params = { data: body }.merge(options.empty? ? {} : { options: options })
82
+ if upload
83
+ perform_request(:post, resource_uri, params.merge(file: upload)) do |c|
84
+ c.request :multipart
85
+ end
86
+ else
87
+ perform_request(:post, resource_uri, params)
88
+ end
89
+ end
90
+
91
+ # Public: Performs a DELETE request against the API.
92
+ #
93
+ # resource_uri - [String] the resource URI relative to the base Asana API
94
+ # URL, e.g "/tags".
95
+ #
96
+ # Returns an [Asana::HttpClient::Response] if everything went well.
97
+ # Raises [Asana::Errors::APIError] if anything went wrong.
98
+ def delete(resource_uri)
99
+ perform_request(:delete, resource_uri)
100
+ end
101
+
102
+ private
103
+
104
+ def connection(&request_config)
105
+ Faraday.new do |builder|
106
+ @authentication.configure(builder)
107
+ @environment_info.configure(builder)
108
+ request_config.call(builder) if request_config
109
+ configure_format(builder)
110
+ add_middleware(builder)
111
+ @config.call(builder) if @config
112
+ use_adapter(builder, @adapter)
113
+ end
114
+ end
115
+
116
+ def perform_request(method, resource_uri, body = {}, &request_config)
117
+ handling_errors do
118
+ url = BASE_URI + resource_uri
119
+ log_request(method, url, body) if @debug_mode
120
+ Response.new(connection(&request_config).public_send(method, url, body))
121
+ end
122
+ end
123
+
124
+ def configure_format(builder)
125
+ builder.request :multi_json
126
+ builder.response :multi_json
127
+ end
128
+
129
+ def add_middleware(builder)
130
+ builder.use Faraday::Response::RaiseError
131
+ builder.use FaradayMiddleware::FollowRedirects
132
+ end
133
+
134
+ def use_adapter(builder, adapter)
135
+ case adapter
136
+ when Symbol
137
+ builder.adapter(adapter)
138
+ when Proc
139
+ adapter.call(builder)
140
+ end
141
+ end
142
+
143
+ def handling_errors(&request)
144
+ ErrorHandling.handle(&request)
145
+ end
146
+
147
+ def log_request(method, url, body)
148
+ STDERR.puts format('[%s] %s %s (%s)',
149
+ self.class,
150
+ method.to_s.upcase,
151
+ url,
152
+ body.inspect)
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,53 @@
1
+ require_relative '../version'
2
+ require 'openssl'
3
+
4
+ module Asana
5
+ class HttpClient
6
+ # Internal: Adds environment information to a Faraday request.
7
+ class EnvironmentInfo
8
+ # Internal: The default user agent to use in all requests to the API.
9
+ USER_AGENT = "ruby-asana v#{Asana::VERSION}"
10
+
11
+ def initialize(user_agent = nil)
12
+ @user_agent = user_agent || USER_AGENT
13
+ @openssl_version = OpenSSL::OPENSSL_VERSION
14
+ @client_version = Asana::VERSION
15
+ @os = os
16
+ end
17
+
18
+ # Public: Augments a Faraday connection with information about the
19
+ # environment.
20
+ def configure(builder)
21
+ builder.headers[:user_agent] = @user_agent
22
+ builder.headers[:"X-Asana-Client-Lib"] = header
23
+ end
24
+
25
+ private
26
+
27
+ def header
28
+ { os: @os,
29
+ language: 'ruby',
30
+ language_version: RUBY_VERSION,
31
+ version: @client_version,
32
+ openssl_version: @openssl_version }
33
+ .map { |k, v| "#{k}=#{v}" }.join('&')
34
+ end
35
+
36
+ # rubocop:disable Metrics/MethodLength
37
+ def os
38
+ if RUBY_PLATFORM =~ /win32/ || RUBY_PLATFORM =~ /mingw/
39
+ 'windows'
40
+ elsif RUBY_PLATFORM =~ /linux/
41
+ 'linux'
42
+ elsif RUBY_PLATFORM =~ /darwin/
43
+ 'darwin'
44
+ elsif RUBY_PLATFORM =~ /freebsd/
45
+ 'freebsd'
46
+ else
47
+ 'unknown'
48
+ end
49
+ end
50
+ # rubocop:enable Metrics/MethodLength
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,103 @@
1
+ require 'multi_json'
2
+
3
+ require_relative '../errors'
4
+
5
+ module Asana
6
+ class HttpClient
7
+ # Internal: Handles errors from the API and re-raises them as proper
8
+ # exceptions.
9
+ module ErrorHandling
10
+ include Errors
11
+
12
+ module_function
13
+
14
+ # Public: Perform a request handling any API errors correspondingly.
15
+ #
16
+ # request - [Proc] a block that will execute the request.
17
+ #
18
+ # Returns a [Faraday::Response] object.
19
+ #
20
+ # Raises [Asana::Errors::InvalidRequest] for invalid requests.
21
+ # Raises [Asana::Errors::NotAuthorized] for unauthorized requests.
22
+ # Raises [Asana::Errors::Forbidden] for forbidden requests.
23
+ # Raises [Asana::Errors::NotFound] when a resource can't be found.
24
+ # Raises [Asana::Errors::RateLimitEnforced] when the API is throttling.
25
+ # Raises [Asana::Errors::ServerError] when there's a server problem.
26
+ # Raises [Asana::Errors::APIError] when the API returns an unknown error.
27
+ #
28
+ # rubocop:disable all
29
+ def handle(&request)
30
+ request.call
31
+ rescue Faraday::ClientError => e
32
+ raise e unless e.response
33
+ case e.response[:status]
34
+ when 400 then raise invalid_request(e.response)
35
+ when 401 then raise not_authorized(e.response)
36
+ when 403 then raise forbidden(e.response)
37
+ when 404 then raise not_found(e.response)
38
+ when 412 then recover_response(e.response)
39
+ when 429 then raise rate_limit_enforced(e.response)
40
+ when 500 then raise server_error(e.response)
41
+ else raise api_error(e.response)
42
+ end
43
+ end
44
+ # rubocop:enable all
45
+
46
+ # Internal: Returns an InvalidRequest exception including a list of
47
+ # errors.
48
+ def invalid_request(response)
49
+ errors = body(response).fetch('errors', []).map { |e| e['message'] }
50
+ InvalidRequest.new(errors).tap do |exception|
51
+ exception.response = response
52
+ end
53
+ end
54
+
55
+ # Internal: Returns a NotAuthorized exception.
56
+ def not_authorized(response)
57
+ NotAuthorized.new.tap { |exception| exception.response = response }
58
+ end
59
+
60
+ # Internal: Returns a Forbidden exception.
61
+ def forbidden(response)
62
+ Forbidden.new.tap { |exception| exception.response = response }
63
+ end
64
+
65
+ # Internal: Returns a NotFound exception.
66
+ def not_found(response)
67
+ NotFound.new.tap { |exception| exception.response = response }
68
+ end
69
+
70
+ # Internal: Returns a RateLimitEnforced exception with a retry after
71
+ # field.
72
+ def rate_limit_enforced(response)
73
+ retry_after_seconds = response[:headers]['Retry-After']
74
+ RateLimitEnforced.new(retry_after_seconds).tap do |exception|
75
+ exception.response = response
76
+ end
77
+ end
78
+
79
+ # Internal: Returns a ServerError exception with a unique phrase.
80
+ def server_error(response)
81
+ phrase = body(response).fetch('errors', []).first['phrase']
82
+ ServerError.new(phrase).tap do |exception|
83
+ exception.response = response
84
+ end
85
+ end
86
+
87
+ # Internal: Returns an APIError exception.
88
+ def api_error(response)
89
+ APIError.new.tap { |exception| exception.response = response }
90
+ end
91
+
92
+ # Internal: Parser a response body from JSON.
93
+ def body(response)
94
+ MultiJson.load(response[:body])
95
+ end
96
+
97
+ def recover_response(response)
98
+ r = response.dup.tap { |res| res[:body] = body(response) }
99
+ Response.new(OpenStruct.new(env: OpenStruct.new(r)))
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,32 @@
1
+ module Asana
2
+ class HttpClient
3
+ # Internal: Represents a response from the Asana API.
4
+ class Response
5
+ # Public:
6
+ # Returns a [Faraday::Env] object for debugging.
7
+ attr_reader :faraday_env
8
+ # Public:
9
+ # Returns the [Integer] status code of the response.
10
+ attr_reader :status
11
+ # Public:
12
+ # Returns the [Hash] representing the parsed JSON body.
13
+ attr_reader :body
14
+
15
+ # Public: Wraps a Faraday response.
16
+ #
17
+ # faraday_response - [Faraday::Response] the Faraday response to wrap.
18
+ def initialize(faraday_response)
19
+ @faraday_env = faraday_response.env
20
+ @status = faraday_env.status
21
+ @body = faraday_env.body
22
+ end
23
+
24
+ # Public:
25
+ # Returns a [String] representation of the response.
26
+ def to_s
27
+ "#<Asana::HttpClient::Response status=#{@status} body=#{@body}>"
28
+ end
29
+ alias_method :inspect, :to_s
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,11 @@
1
+ require_relative 'resources/resource'
2
+ require_relative 'resources/collection'
3
+
4
+ Dir[File.join(File.dirname(__FILE__), 'resources', '*.rb')]
5
+ .each { |resource| require resource }
6
+
7
+ module Asana
8
+ # Public: Contains all the resources that the Asana API can return.
9
+ module Resources
10
+ end
11
+ end
@@ -0,0 +1,44 @@
1
+ ### WARNING: This file is auto-generated by the asana-api-meta repo. Do not
2
+ ### edit it manually.
3
+
4
+ module Asana
5
+ module Resources
6
+ # An _attachment_ object represents any file attached to a task in Asana,
7
+ # whether it's an uploaded file or one associated via a third-party service
8
+ # such as Dropbox or Google Drive.
9
+ class Attachment < Resource
10
+
11
+
12
+ attr_reader :id
13
+
14
+ class << self
15
+ # Returns the plural name of the resource.
16
+ def plural_name
17
+ 'attachments'
18
+ end
19
+
20
+ # Returns the full record for a single attachment.
21
+ #
22
+ # id - [Id] Globally unique identifier for the attachment.
23
+ #
24
+ # options - [Hash] the request I/O options.
25
+ def find_by_id(client, id, options: {})
26
+
27
+ self.new(parse(client.get("/attachments/#{id}", options: options)).first, client: client)
28
+ end
29
+
30
+ # Returns the compact records for all attachments on the task.
31
+ #
32
+ # task - [Id] Globally unique identifier for the task.
33
+ #
34
+ # per_page - [Integer] the number of records to fetch per page.
35
+ # options - [Hash] the request I/O options.
36
+ def find_by_task(client, task: required("task"), per_page: 20, options: {})
37
+ params = { limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? }
38
+ Collection.new(parse(client.get("/tasks/#{task}/attachments", params: params, options: options)), type: self, client: client)
39
+ end
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,33 @@
1
+ module Asana
2
+ module Resources
3
+ # Internal: Mixin to add the ability to upload an attachment to a specific
4
+ # Asana resource (a Task, really).
5
+ module AttachmentUploading
6
+ # Uploads a new attachment to the resource.
7
+ #
8
+ # filename - [String] the absolute path of the file to upload.
9
+ # mime - [String] the MIME type of the file
10
+ # options - [Hash] the request I/O options
11
+ # data - [Hash] extra attributes to post
12
+ #
13
+ # rubocop:disable Metrics/AbcSize
14
+ # rubocop:disable Metrics/MethodLength
15
+ def attach(filename: required('filename'),
16
+ mime: required('mime'),
17
+ options: {}, **data)
18
+ path = File.expand_path(filename)
19
+ unless File.exist?(path)
20
+ fail ArgumentError, "file #{filename} doesn't exist"
21
+ end
22
+ upload = Faraday::UploadIO.new(path, mime)
23
+ response = client.post("/#{self.class.plural_name}/#{id}/attachments",
24
+ body: data,
25
+ upload: upload,
26
+ options: options)
27
+ Attachment.new(parse(response).first, client: client)
28
+ end
29
+ # rubocop:enable Metrics/MethodLength
30
+ # rubocop:enable Metrics/AbcSize
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,68 @@
1
+ require_relative 'response_helper'
2
+
3
+ module Asana
4
+ module Resources
5
+ # Public: Represents a paginated collection of Asana resources.
6
+ class Collection
7
+ include Enumerable
8
+ include ResponseHelper
9
+
10
+ attr_reader :elements
11
+
12
+ # Public: Initializes a collection representing a page of resources of a
13
+ # given type.
14
+ #
15
+ # (elements, extra) - [Array] an (String, Hash) tuple coming from the
16
+ # response parser.
17
+ # type - [Class] the type of resource that the collection
18
+ # contains. Defaults to the generic Resource.
19
+ # client - [Asana::Client] the client to perform requests.
20
+ def initialize((elements, extra),
21
+ type: Resource,
22
+ client: required('client'))
23
+ @elements = elements.map { |elem| type.new(elem, client: client) }
24
+ @type = type
25
+ @next_page_data = extra['next_page']
26
+ @client = client
27
+ end
28
+
29
+ # Public: Iterates over the elements of the collection.
30
+ def each(&block)
31
+ if block
32
+ @elements.each(&block)
33
+ (next_page || []).each(&block)
34
+ else
35
+ to_enum
36
+ end
37
+ end
38
+
39
+ # Public: Returns the size of the collection.
40
+ def size
41
+ to_a.size
42
+ end
43
+ alias_method :length, :size
44
+
45
+ # Public: Returns a String representation of the collection.
46
+ def to_s
47
+ "#<Asana::Collection<#{@type}> " \
48
+ "[#{@elements.map(&:inspect).join(', ')}" +
49
+ (@next_page_data ? ', ...' : '') + ']>'
50
+ end
51
+
52
+ alias_method :inspect, :to_s
53
+
54
+ # Public: Returns a new Asana::Resources::Collection with the next page
55
+ # or nil if there are no more pages. Caches the result.
56
+ def next_page
57
+ if defined?(@next_page)
58
+ @next_page
59
+ else
60
+ @next_page = if @next_page_data
61
+ response = parse(@client.get(@next_page_data['path']))
62
+ self.class.new(response, type: @type, client: @client)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end