openc-asana 0.1.2

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 (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