openc-asana 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +4 -0
  3. data/.gitignore +13 -0
  4. data/.rspec +4 -0
  5. data/.rubocop.yml +11 -0
  6. data/.travis.yml +12 -0
  7. data/.yardopts +5 -0
  8. data/CODE_OF_CONDUCT.md +13 -0
  9. data/Gemfile +21 -0
  10. data/Guardfile +86 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +355 -0
  13. data/Rakefile +65 -0
  14. data/examples/Gemfile +6 -0
  15. data/examples/Gemfile.lock +59 -0
  16. data/examples/api_token.rb +21 -0
  17. data/examples/cli_app.rb +25 -0
  18. data/examples/events.rb +38 -0
  19. data/examples/omniauth_integration.rb +54 -0
  20. data/lib/asana.rb +12 -0
  21. data/lib/asana/authentication.rb +8 -0
  22. data/lib/asana/authentication/oauth2.rb +42 -0
  23. data/lib/asana/authentication/oauth2/access_token_authentication.rb +51 -0
  24. data/lib/asana/authentication/oauth2/bearer_token_authentication.rb +32 -0
  25. data/lib/asana/authentication/oauth2/client.rb +50 -0
  26. data/lib/asana/authentication/token_authentication.rb +20 -0
  27. data/lib/asana/client.rb +124 -0
  28. data/lib/asana/client/configuration.rb +165 -0
  29. data/lib/asana/errors.rb +92 -0
  30. data/lib/asana/http_client.rb +155 -0
  31. data/lib/asana/http_client/environment_info.rb +53 -0
  32. data/lib/asana/http_client/error_handling.rb +103 -0
  33. data/lib/asana/http_client/response.rb +32 -0
  34. data/lib/asana/resource_includes/attachment_uploading.rb +33 -0
  35. data/lib/asana/resource_includes/collection.rb +68 -0
  36. data/lib/asana/resource_includes/event.rb +51 -0
  37. data/lib/asana/resource_includes/event_subscription.rb +14 -0
  38. data/lib/asana/resource_includes/events.rb +103 -0
  39. data/lib/asana/resource_includes/registry.rb +63 -0
  40. data/lib/asana/resource_includes/resource.rb +103 -0
  41. data/lib/asana/resource_includes/response_helper.rb +14 -0
  42. data/lib/asana/resources.rb +14 -0
  43. data/lib/asana/resources/attachment.rb +44 -0
  44. data/lib/asana/resources/project.rb +154 -0
  45. data/lib/asana/resources/story.rb +64 -0
  46. data/lib/asana/resources/tag.rb +120 -0
  47. data/lib/asana/resources/task.rb +300 -0
  48. data/lib/asana/resources/team.rb +55 -0
  49. data/lib/asana/resources/user.rb +72 -0
  50. data/lib/asana/resources/workspace.rb +91 -0
  51. data/lib/asana/ruby2_0_0_compatibility.rb +3 -0
  52. data/lib/asana/version.rb +5 -0
  53. data/lib/templates/index.js +8 -0
  54. data/lib/templates/resource.ejs +225 -0
  55. data/openc-asana.gemspec +32 -0
  56. data/package.json +7 -0
  57. metadata +200 -0
@@ -0,0 +1,20 @@
1
+ module Asana
2
+ module Authentication
3
+ # Public: Represents an API token authentication mechanism.
4
+ class TokenAuthentication
5
+ def initialize(token)
6
+ @token = token
7
+ end
8
+
9
+ # Public: Configures a Faraday connection injecting its token as
10
+ # basic auth.
11
+ #
12
+ # builder - [Faraday::Connection] the Faraday connection instance.
13
+ #
14
+ # Returns nothing.
15
+ def configure(connection)
16
+ connection.basic_auth(@token, '')
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,124 @@
1
+ require_relative 'authentication'
2
+ require_relative 'client/configuration'
3
+ require_relative 'resources'
4
+
5
+ module Asana
6
+ # Public: A client to interact with the Asana API. It exposes all the
7
+ # available resources of the Asana API in idiomatic Ruby.
8
+ #
9
+ # Examples
10
+ #
11
+ # # Authentication with an API token
12
+ # Asana::Client.new do |client|
13
+ # client.authentication :api_token, '...'
14
+ # end
15
+ #
16
+ # # OAuth2 with a plain bearer token (doesn't support auto-refresh)
17
+ # Asana::Client.new do |client|
18
+ # client.authentication :oauth2, bearer_token: '...'
19
+ # end
20
+ #
21
+ # # OAuth2 with a plain refresh token and client credentials
22
+ # Asana::Client.new do |client|
23
+ # client.authentication :oauth2,
24
+ # refresh_token: '...',
25
+ # client_id: '...',
26
+ # client_secret: '...',
27
+ # redirect_uri: '...'
28
+ # end
29
+ #
30
+ # # OAuth2 with an ::OAuth2::AccessToken object
31
+ # Asana::Client.new do |client|
32
+ # client.authentication :oauth2, my_oauth2_access_token_object
33
+ # end
34
+ #
35
+ # # Use a custom Faraday network adapter
36
+ # Asana::Client.new do |client|
37
+ # client.authentication ...
38
+ # client.adapter :typhoeus
39
+ # end
40
+ #
41
+ # # Use a custom user agent string
42
+ # Asana::Client.new do |client|
43
+ # client.authentication ...
44
+ # client.user_agent '...'
45
+ # end
46
+ #
47
+ # # Pass in custom configuration to the Faraday connection
48
+ # Asana::Client.new do |client|
49
+ # client.authentication ...
50
+ # client.configure_faraday { |conn| conn.use MyMiddleware }
51
+ # end
52
+ #
53
+ class Client
54
+ # Internal: Proxies Resource classes to implement a fluent API on the Client
55
+ # instances.
56
+ class ResourceProxy
57
+ def initialize(client: required('client'), resource: required('resource'))
58
+ @client = client
59
+ @resource = resource
60
+ end
61
+
62
+ def method_missing(m, *args, &block)
63
+ @resource.public_send(m, *([@client] + args), &block)
64
+ end
65
+
66
+ def respond_to_missing?(m, *)
67
+ @resource.respond_to?(m)
68
+ end
69
+ end
70
+
71
+ # Public: Initializes a new client.
72
+ #
73
+ # Yields a {Asana::Client::Configuration} object as a configuration
74
+ # DSL. See {Asana::Client} for usage examples.
75
+ def initialize
76
+ config = Configuration.new.tap { |c| yield c }.to_h
77
+ @http_client =
78
+ HttpClient.new(authentication: config.fetch(:authentication),
79
+ adapter: config[:faraday_adapter],
80
+ user_agent: config[:user_agent],
81
+ debug_mode: config[:debug_mode],
82
+ &config[:faraday_config])
83
+ end
84
+
85
+ # Public: Performs a GET request against an arbitrary Asana URL. Allows for
86
+ # the user to interact with the API in ways that haven't been
87
+ # reflected/foreseen in this library.
88
+ def get(url, *args)
89
+ @http_client.get(url, *args)
90
+ end
91
+
92
+ # Public: Performs a POST request against an arbitrary Asana URL. Allows for
93
+ # the user to interact with the API in ways that haven't been
94
+ # reflected/foreseen in this library.
95
+ def post(url, *args)
96
+ @http_client.post(url, *args)
97
+ end
98
+
99
+ # Public: Performs a PUT request against an arbitrary Asana URL. Allows for
100
+ # the user to interact with the API in ways that haven't been
101
+ # reflected/foreseen in this library.
102
+ def put(url, *args)
103
+ @http_client.put(url, *args)
104
+ end
105
+
106
+ # Public: Performs a DELETE request against an arbitrary Asana URL. Allows
107
+ # for the user to interact with the API in ways that haven't been
108
+ # reflected/foreseen in this library.
109
+ def delete(url, *args)
110
+ @http_client.delete(url, *args)
111
+ end
112
+
113
+ # Public: Exposes queries for all top-evel endpoints.
114
+ #
115
+ # E.g. #users will query /users and return a
116
+ # Asana::Resources::Collection<User>.
117
+ Resources::Registry.resources.each do |resource_class|
118
+ define_method(resource_class.plural_name) do
119
+ ResourceProxy.new(client: @http_client,
120
+ resource: resource_class)
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,165 @@
1
+ module Asana
2
+ class Client
3
+ # Internal: Represents a configuration DSL for an Asana::Client.
4
+ #
5
+ # Examples
6
+ #
7
+ # config = Configuration.new
8
+ # config.authentication :api_token, 'my_api_token'
9
+ # config.adapter :typhoeus
10
+ # config.configure_faraday { |conn| conn.use MyMiddleware }
11
+ # config.to_h
12
+ # # => { authentication: #<Authentication::TokenAuthentication>,
13
+ # faraday_adapter: :typhoeus,
14
+ # faraday_configuration: #<Proc> }
15
+ #
16
+ class Configuration
17
+ # Public: Initializes an empty configuration object.
18
+ def initialize
19
+ @configuration = {}
20
+ end
21
+
22
+ # Public: Sets an authentication strategy.
23
+ #
24
+ # type - [:oauth2, :api_token] the kind of authentication strategy to use
25
+ # value - [::OAuth2::AccessToken, String, Hash] the configuration for the
26
+ # chosen authentication strategy.
27
+ #
28
+ # Returns nothing.
29
+ #
30
+ # Raises ArgumentError if the arguments are invalid.
31
+ def authentication(type, value)
32
+ auth = case type
33
+ when :oauth2 then oauth2(value)
34
+ when :api_token then api_token(value)
35
+ else error "unsupported authentication type #{type}"
36
+ end
37
+ @configuration[:authentication] = auth
38
+ end
39
+
40
+ # Public: Sets a custom network adapter for Faraday.
41
+ #
42
+ # adapter - [Symbol, Proc] the adapter.
43
+ #
44
+ # Returns nothing.
45
+ def faraday_adapter(adapter)
46
+ @configuration[:faraday_adapter] = adapter
47
+ end
48
+
49
+ # Public: Sets a custom configuration block for the Faraday connection.
50
+ #
51
+ # config - [Proc] the configuration block.
52
+ #
53
+ # Returns nothing.
54
+ def configure_faraday(&config)
55
+ @configuration[:faraday_configuration] = config
56
+ end
57
+
58
+ # Public: Configures the client in debug mode, which will print verbose
59
+ # information on STDERR.
60
+ #
61
+ # Returns nothing.
62
+ def debug_mode
63
+ @configuration[:debug_mode] = true
64
+ end
65
+
66
+ # Public:
67
+ # Returns the configuration [Hash].
68
+ def to_h
69
+ @configuration
70
+ end
71
+
72
+ private
73
+
74
+ # Internal: Configures an OAuth2 authentication strategy from either an
75
+ # OAuth2 access token object, or a plain refresh token, or a plain bearer
76
+ # token.
77
+ #
78
+ # value - [::OAuth::AccessToken, String] the value to configure the
79
+ # strategy from.
80
+ #
81
+ # Returns [Asana::Authentication::OAuth2::AccessTokenAuthentication,
82
+ # Asana::Authentication::OAuth2::BearerTokenAuthentication]
83
+ # the OAuth2 authentication strategy.
84
+ #
85
+ # Raises ArgumentError if the OAuth2 configuration arguments are invalid.
86
+ #
87
+ # rubocop:disable Metrics/MethodLength
88
+ def oauth2(value)
89
+ case value
90
+ when ::OAuth2::AccessToken
91
+ from_access_token(value)
92
+ when -> v { v.is_a?(Hash) && v[:refresh_token] }
93
+ from_refresh_token(value)
94
+ when -> v { v.is_a?(Hash) && v[:bearer_token] }
95
+ from_bearer_token(value[:bearer_token])
96
+ else
97
+ error 'Invalid OAuth2 configuration: pass in either an ' \
98
+ '::OAuth2::AccessToken object of your own or a hash ' \
99
+ 'containing :refresh_token or :bearer_token.'
100
+ end
101
+ end
102
+ # rubocop:enable Metrics/MethodLength
103
+
104
+ # Internal: Configures a TokenAuthentication strategy.
105
+ #
106
+ # token - [String] the API token
107
+ #
108
+ # Returns a [Authentication::TokenAuthentication] strategy.
109
+ def api_token(token)
110
+ Authentication::TokenAuthentication.new(token)
111
+ end
112
+
113
+ # Internal: Configures an OAuth2 AccessTokenAuthentication strategy.
114
+ #
115
+ # access_token - [::OAuth2::AccessToken] the OAuth2 access token object
116
+ #
117
+ # Returns a [Authentication::OAuth2::AccessTokenAuthentication] strategy.
118
+ def from_access_token(access_token)
119
+ Authentication::OAuth2::AccessTokenAuthentication
120
+ .new(access_token)
121
+ end
122
+
123
+ # Internal: Configures an OAuth2 AccessTokenAuthentication strategy.
124
+ #
125
+ # hash - The configuration hash:
126
+ # :refresh_token - [String] the OAuth2 refresh token
127
+ # :client_id - [String] the OAuth2 client id
128
+ # :client_secret - [String] the OAuth2 client secret
129
+ # :redirect_uri - [String] the OAuth2 redirect URI
130
+ #
131
+ # Returns a [Authentication::OAuth2::AccessTokenAuthentication] strategy.
132
+ def from_refresh_token(hash)
133
+ refresh_token, client_id, client_secret, redirect_uri =
134
+ requiring(hash, :refresh_token, :client_id,
135
+ :client_secret, :redirect_uri)
136
+
137
+ Authentication::OAuth2::AccessTokenAuthentication
138
+ .from_refresh_token(refresh_token,
139
+ client_id: client_id,
140
+ client_secret: client_secret,
141
+ redirect_uri: redirect_uri)
142
+ end
143
+
144
+ # Internal: Configures an OAuth2 BearerTokenAuthentication strategy.
145
+ #
146
+ # bearer_token - [String] the plain OAuth2 bearer token
147
+ #
148
+ # Returns a [Authentication::OAuth2::BearerTokenAuthentication] strategy.
149
+ def from_bearer_token(bearer_token)
150
+ Authentication::OAuth2::BearerTokenAuthentication
151
+ .new(bearer_token)
152
+ end
153
+
154
+ def requiring(hash, *keys)
155
+ missing_keys = keys.select { |k| !hash.key?(k) }
156
+ missing_keys.any? && error("Missing keys: #{missing_keys.join(', ')}")
157
+ keys.map { |k| hash[k] }
158
+ end
159
+
160
+ def error(msg)
161
+ fail ArgumentError, msg
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,92 @@
1
+ module Asana
2
+ # Public: Defines the different errors that the Asana API may throw, which the
3
+ # client code may want to catch.
4
+ module Errors
5
+ # Public: A generic, catch-all API error. It contains the whole response
6
+ # object for debugging purposes.
7
+ #
8
+ # Note: This exception should never be raised when there exists a more
9
+ # specific subclass.
10
+ APIError = Class.new(StandardError) do
11
+ attr_accessor :response
12
+
13
+ def to_s
14
+ 'An unknown API error ocurred.'
15
+ end
16
+ end
17
+
18
+ # Public: A 401 error. Raised when the credentials used are invalid and the
19
+ # user could not be authenticated.
20
+ NotAuthorized = Class.new(APIError) do
21
+ def to_s
22
+ 'A valid API key was not provided with the request, so the API could '\
23
+ 'not associate a user with the request.'
24
+ end
25
+ end
26
+
27
+ # Public: A 403 error. Raised when the user doesn't have permission to
28
+ # access the requested resource or to perform the requested action on it.
29
+ Forbidden = Class.new(APIError) do
30
+ def to_s
31
+ 'The API key and request syntax was valid but the server is refusing '\
32
+ 'to complete the request. This can happen if you try to read or write '\
33
+ 'to objects or properties that the user does not have access to.'
34
+ end
35
+ end
36
+
37
+ # Public: A 404 error. Raised when the requested resource doesn't exist.
38
+ NotFound = Class.new(APIError) do
39
+ def to_s
40
+ 'Either the request method and path supplied do not specify a known '\
41
+ 'action in the API, or the object specified by the request does not '\
42
+ 'exist.'
43
+ end
44
+ end
45
+
46
+ # Public: A 500 error. Raised when there is a problem in the Asana API
47
+ # server. It contains a unique phrase that can be used to identify the
48
+ # problem when contacting developer support.
49
+ ServerError = Class.new(APIError) do
50
+ attr_accessor :phrase
51
+
52
+ def initialize(phrase)
53
+ @phrase = phrase
54
+ end
55
+
56
+ def to_s
57
+ "There has been an error on Asana's end. Use this unique phrase to "\
58
+ 'identify the problem when contacting developer support: ' +
59
+ %("#{@phrase}")
60
+ end
61
+ end
62
+
63
+ # Public: A 400 error. Raised when the request was malformed or missing some
64
+ # parameters. It contains a list of errors indicating the specific problems.
65
+ InvalidRequest = Class.new(APIError) do
66
+ attr_accessor :errors
67
+
68
+ def initialize(errors)
69
+ @errors = errors
70
+ end
71
+
72
+ def to_s
73
+ errors.join(', ')
74
+ end
75
+ end
76
+
77
+ # Public: A 429 error. Raised when the Asana API enforces rate-limiting on
78
+ # the client to avoid overload. It contains the number of seconds to wait
79
+ # before retrying the operation.
80
+ RateLimitEnforced = Class.new(APIError) do
81
+ attr_accessor :retry_after_seconds
82
+
83
+ def initialize(retry_after_seconds)
84
+ @retry_after_seconds = retry_after_seconds
85
+ end
86
+
87
+ def to_s
88
+ "Retry your request after #{@retry_after_seconds} seconds."
89
+ end
90
+ end
91
+ end
92
+ end
@@ -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