asana 0.0.6 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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,49 @@
1
+ module Asana
2
+ module Resources
3
+ # An _event_ is an object representing a change to a resource that was
4
+ # observed by an event subscription.
5
+ #
6
+ # In general, requesting events on a resource is faster and subject to
7
+ # higher rate limits than requesting the resource itself. Additionally,
8
+ # change events bubble up - listening to events on a project would include
9
+ # when stories are added to tasks in the project, even on subtasks.
10
+ #
11
+ # Establish an initial sync token by making a request with no sync token.
12
+ # The response will be a `412` error - the same as if the sync token had
13
+ # expired.
14
+ #
15
+ # Subsequent requests should always provide the sync token from the
16
+ # immediately preceding call.
17
+ #
18
+ # Sync tokens may not be valid if you attempt to go 'backward' in the
19
+ # history by requesting previous tokens, though re-requesting the current
20
+ # sync token is generally safe, and will always return the same results.
21
+ #
22
+ # When you receive a `412 Precondition Failed` error, it means that the sync
23
+ # token is either invalid or expired. If you are attempting to keep a set of
24
+ # data in sync, this signals you may need to re-crawl the data.
25
+ #
26
+ # Sync tokens always expire after 24 hours, but may expire sooner, depending
27
+ # on load on the service.
28
+ class Event < Resource
29
+ attr_reader :type
30
+
31
+ class << self
32
+ # Returns the plural name of the resource.
33
+ def plural_name
34
+ 'events'
35
+ end
36
+
37
+ # Public: Returns an infinite collection of events on a particular
38
+ # resource.
39
+ #
40
+ # client - [Asana::Client] the client to perform the requests.
41
+ # id - [String] the id of the resource to get events from.
42
+ # wait - [Integer] the number of seconds to wait between each poll.
43
+ def for(client, id, wait: 1)
44
+ Events.new(resource: id, client: client, wait: wait)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,12 @@
1
+ module Asana
2
+ module Resources
3
+ # Public: Mixin to enable a resource with the ability to fetch events about
4
+ # itself.
5
+ module EventSubscription
6
+ # Public: Returns an infinite collection of events on the resource.
7
+ def events(wait: 1, options: {})
8
+ Events.new(resource: id, client: client, wait: wait, options: options)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,101 @@
1
+ module Asana
2
+ module Resources
3
+ # Public: An infinite collection of events.
4
+ #
5
+ # Since they are infinite, if you want to filter or do other collection
6
+ # operations without blocking indefinitely you should call #lazy on them to
7
+ # turn them into a lazy collection.
8
+ #
9
+ # Examples:
10
+ #
11
+ # # Subscribes to an event stream and blocks indefinitely, printing
12
+ # # information of every event as it comes in.
13
+ # events = Events.new(resource: 'someresourceID', client: client)
14
+ # events.each do |event|
15
+ # puts [event.type, event.action]
16
+ # end
17
+ #
18
+ # # Lazily filters events as they come in and prints them.
19
+ # events = Events.new(resource: 'someresourceID', client: client)
20
+ # events.lazy.select { |e| e.type == 'task' }.each do |event|
21
+ # puts [event.type, event.action]
22
+ # end
23
+ #
24
+ class Events
25
+ include Enumerable
26
+
27
+ # Public: Initializes a new Events instance, subscribed to a resource ID.
28
+ #
29
+ # resource - [String] a resource ID. Can be a task id or a workspace id.
30
+ # client - [Asana::Client] a client to perform the requests.
31
+ # wait - [Integer] the number of seconds to wait between each poll.
32
+ # options - [Hash] the request I/O options
33
+ def initialize(resource: required('resource'),
34
+ client: required('client'),
35
+ wait: 1, options: {})
36
+ @resource = resource
37
+ @client = client
38
+ @events = []
39
+ @wait = wait
40
+ @options = options
41
+ @sync = nil
42
+ @last_poll = nil
43
+ end
44
+
45
+ # Public: Iterates indefinitely over all events happening to a particular
46
+ # resource from the @sync timestamp or from now if it is nil.
47
+ def each(&block)
48
+ if block
49
+ loop do
50
+ poll if @events.empty?
51
+ event = @events.shift
52
+ yield event if event
53
+ end
54
+ else
55
+ to_enum
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ # Internal: Polls and fetches all events that have occurred since the sync
62
+ # token was created. Updates the sync token as it comes back from the
63
+ # response.
64
+ #
65
+ # If we polled less than @wait seconds ago, we don't do anything.
66
+ #
67
+ # Notes:
68
+ #
69
+ # On the first request, the sync token is not passed (because it is
70
+ # nil). The response will be the same as for an expired sync token, and
71
+ # will include a new valid sync token.
72
+ #
73
+ # If the sync token is too old (which may happen from time to time)
74
+ # the API will return a `412 Precondition Failed` error, and include
75
+ # a fresh `sync` token in the response.
76
+ def poll
77
+ rate_limiting do
78
+ body = @client.get('/events',
79
+ params: params,
80
+ options: @options).body
81
+ @sync = body['sync']
82
+ @events += body.fetch('data', []).map do |event_data|
83
+ Event.new(event_data, client: @client)
84
+ end
85
+ end
86
+ end
87
+
88
+ # Internal: Returns the formatted params for the poll request.
89
+ def params
90
+ { resource: @resource, sync: @sync }.reject { |_, v| v.nil? }
91
+ end
92
+
93
+ # Internal: Executes a block if at least @wait seconds have passed since
94
+ # @last_poll.
95
+ def rate_limiting(&block)
96
+ return if @last_poll && Time.now - @last_poll <= @wait
97
+ block.call.tap { @last_poll = Time.now }
98
+ end
99
+ end
100
+ end
101
+ end
@@ -1,28 +1,154 @@
1
+ ### WARNING: This file is auto-generated by the asana-api-meta repo. Do not
2
+ ### edit it manually.
3
+
1
4
  module Asana
2
- class Project < Asana::Resource
5
+ module Resources
6
+ # A _project_ represents a prioritized list of tasks in Asana. It exists in a
7
+ # single workspace or organization and is accessible to a subset of users in
8
+ # that workspace or organization, depending on its permissions.
9
+ #
10
+ # Projects in organizations are shared with a single team. You cannot currently
11
+ # change the team of a project via the API. Non-organization workspaces do not
12
+ # have teams and so you should not specify the team of project in a
13
+ # regular workspace.
14
+ class Project < Resource
3
15
 
4
- alias :create :method_not_allowed
5
- alias :destroy :method_not_allowed
16
+ include EventSubscription
6
17
 
7
- def self.all_by_task(*args)
8
- parent_resources :task
9
- all(*args)
10
- end
11
18
 
12
- def self.all_by_workspace(*args)
13
- parent_resources :workspace
14
- all(*args)
15
- end
19
+ attr_reader :id
16
20
 
17
- def tasks
18
- Task.all_by_project(:params => { :project_id => self.id })
19
- end
21
+ class << self
22
+ # Returns the plural name of the resource.
23
+ def plural_name
24
+ 'projects'
25
+ end
20
26
 
21
- def modify(modified_fields)
22
- resource = Resource.new(modified_fields)
23
- response = Project.put(self.id, nil, resource.to_json)
24
- Project.new(connection.format.decode(response.body))
25
- end
27
+ # Creates a new project in a workspace or team.
28
+ #
29
+ # Every project is required to be created in a specific workspace or
30
+ # organization, and this cannot be changed once set. Note that you can use
31
+ # the `workspace` parameter regardless of whether or not it is an
32
+ # organization.
33
+ #
34
+ # If the workspace for your project _is_ an organization, you must also
35
+ # supply a `team` to share the project with.
36
+ #
37
+ # Returns the full record of the newly created project.
38
+ #
39
+ # workspace - [Id] The workspace or organization to create the project in.
40
+ # team - [Id] If creating in an organization, the specific team to create the
41
+ # project in.
42
+ #
43
+ # options - [Hash] the request I/O options.
44
+ # data - [Hash] the attributes to post.
45
+ def create(client, workspace: required("workspace"), team: nil, options: {}, **data)
46
+ with_params = data.merge(workspace: workspace, team: team).reject { |_,v| v.nil? || Array(v).empty? }
47
+ self.new(parse(client.post("/projects", body: with_params, options: options)).first, client: client)
48
+ end
49
+
50
+ # If the workspace for your project _is_ an organization, you must also
51
+ # supply a `team` to share the project with.
52
+ #
53
+ # Returns the full record of the newly created project.
54
+ #
55
+ # workspace - [Id] The workspace or organization to create the project in.
56
+ # options - [Hash] the request I/O options.
57
+ # data - [Hash] the attributes to post.
58
+ def create_in_workspace(client, workspace: required("workspace"), options: {}, **data)
59
+
60
+ self.new(parse(client.post("/workspaces/#{workspace}/projects", body: data, options: options)).first, client: client)
61
+ end
62
+
63
+ # Creates a project shared with the given team.
64
+ #
65
+ # Returns the full record of the newly created project.
66
+ #
67
+ # team - [Id] The team to create the project in.
68
+ # options - [Hash] the request I/O options.
69
+ # data - [Hash] the attributes to post.
70
+ def create_in_team(client, team: required("team"), options: {}, **data)
71
+
72
+ self.new(parse(client.post("/teams/#{team}/projects", body: data, options: options)).first, client: client)
73
+ end
74
+
75
+ # Returns the complete project record for a single project.
76
+ #
77
+ # id - [Id] The project to get.
78
+ # options - [Hash] the request I/O options.
79
+ def find_by_id(client, id, options: {})
26
80
 
81
+ self.new(parse(client.get("/projects/#{id}", options: options)).first, client: client)
82
+ end
83
+
84
+ # Returns the compact project records for some filtered set of projects.
85
+ # Use one or more of the parameters provided to filter the projects returned.
86
+ #
87
+ # workspace - [Id] The workspace or organization to filter projects on.
88
+ # team - [Id] The team to filter projects on.
89
+ # archived - [Boolean] Only return projects whose `archived` field takes on the value of
90
+ # this parameter.
91
+ #
92
+ # per_page - [Integer] the number of records to fetch per page.
93
+ # options - [Hash] the request I/O options.
94
+ def find_all(client, workspace: nil, team: nil, archived: nil, per_page: 20, options: {})
95
+ params = { workspace: workspace, team: team, archived: archived, limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? }
96
+ Collection.new(parse(client.get("/projects", params: params, options: options)), type: self, client: client)
97
+ end
98
+
99
+ # Returns the compact project records for all projects in the workspace.
100
+ #
101
+ # workspace - [Id] The workspace or organization to find projects in.
102
+ # archived - [Boolean] Only return projects whose `archived` field takes on the value of
103
+ # this parameter.
104
+ #
105
+ # per_page - [Integer] the number of records to fetch per page.
106
+ # options - [Hash] the request I/O options.
107
+ def find_by_workspace(client, workspace: required("workspace"), archived: nil, per_page: 20, options: {})
108
+ params = { archived: archived, limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? }
109
+ Collection.new(parse(client.get("/workspaces/#{workspace}/projects", params: params, options: options)), type: self, client: client)
110
+ end
111
+
112
+ # Returns the compact project records for all projects in the team.
113
+ #
114
+ # team - [Id] The team to find projects in.
115
+ # archived - [Boolean] Only return projects whose `archived` field takes on the value of
116
+ # this parameter.
117
+ #
118
+ # per_page - [Integer] the number of records to fetch per page.
119
+ # options - [Hash] the request I/O options.
120
+ def find_by_team(client, team: required("team"), archived: nil, per_page: 20, options: {})
121
+ params = { archived: archived, limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? }
122
+ Collection.new(parse(client.get("/teams/#{team}/projects", params: params, options: options)), type: self, client: client)
123
+ end
124
+ end
125
+
126
+ # A specific, existing project can be updated by making a PUT request on the
127
+ # URL for that project. Only the fields provided in the `data` block will be
128
+ # updated; any unspecified fields will remain unchanged.
129
+ #
130
+ # When using this method, it is best to specify only those fields you wish
131
+ # to change, or else you may overwrite changes made by another user since
132
+ # you last retrieved the task.
133
+ #
134
+ # Returns the complete updated project record.
135
+ #
136
+ # options - [Hash] the request I/O options.
137
+ # data - [Hash] the attributes to post.
138
+ def update(options: {}, **data)
139
+
140
+ refresh_with(parse(client.put("/projects/#{id}", body: data, options: options)).first)
141
+ end
142
+
143
+ # A specific, existing project can be deleted by making a DELETE request
144
+ # on the URL for that project.
145
+ #
146
+ # Returns an empty data record.
147
+ def delete()
148
+
149
+ client.delete("/projects/#{id}") && true
150
+ end
151
+
152
+ end
27
153
  end
28
154
  end
@@ -0,0 +1,62 @@
1
+ require 'set'
2
+
3
+ module Asana
4
+ module Resources
5
+ # Internal: Global registry of Resource subclasses. It provides lookup from
6
+ # singular and plural names to the actual class objects.
7
+ #
8
+ # Examples
9
+ #
10
+ # class Unicorn < Asana::Resources::Resource
11
+ # path '/unicorns'
12
+ # end
13
+ #
14
+ # Registry.lookup(:unicorn) # => Unicorn
15
+ # Registry.lookup_many(:unicorns) # => Unicorn
16
+ #
17
+ module Registry
18
+ class << self
19
+ # Public: Registers a new resource class.
20
+ #
21
+ # resource_klass - [Class] the resource class.
22
+ #
23
+ # Returns nothing.
24
+ def register(resource_klass)
25
+ resources << resource_klass
26
+ end
27
+
28
+ # Public: Looks up a resource class by its singular name.
29
+ #
30
+ # singular_name - [#to_s] the name of the resource, e.g :unicorn.
31
+ #
32
+ # Returns the resource class or {Asana::Resources::Resource}.
33
+ def lookup(singular_name)
34
+ resources.detect do |klass|
35
+ klass.singular_name.to_s == singular_name.to_s
36
+ end || Resource
37
+ end
38
+
39
+ # Public: Looks up a resource class by its plural name.
40
+ #
41
+ # plural_name - [#to_s] the plural name of the resource, e.g :unicorns.
42
+ #
43
+ # Returns the resource class or {Asana::Resources::Resource}.
44
+ def lookup_many(plural_name)
45
+ resources.detect do |klass|
46
+ klass.plural_name.to_s == plural_name.to_s
47
+ end || Resource
48
+ end
49
+
50
+ # Internal: A set of Resource classes.
51
+ #
52
+ # Returns the Set, defaulting to the empty set.
53
+ #
54
+ # Note: this object is a mutable singleton, so it should not be accessed
55
+ # from multiple threads.
56
+ def resources
57
+ @resources ||= Set.new
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,103 @@
1
+ require_relative 'registry'
2
+ require_relative 'response_helper'
3
+
4
+ module Asana
5
+ module Resources
6
+ # Public: The base resource class which provides some sugar over common
7
+ # resource functionality.
8
+ class Resource
9
+ include ResponseHelper
10
+ extend ResponseHelper
11
+
12
+ def self.inherited(base)
13
+ Registry.register(base)
14
+ end
15
+
16
+ def initialize(data, client: required('client'))
17
+ @_client = client
18
+ @_data = data
19
+ data.each do |k, v|
20
+ instance_variable_set(:"@#{k}", v) if respond_to?(k)
21
+ end
22
+ end
23
+
24
+ # If it has findById, it implements #refresh
25
+ def refresh
26
+ if self.class.respond_to?(:find_by_id)
27
+ self.class.find_by_id(client, id)
28
+ else
29
+ fail "#{self.class.name} does not respond to #find_by_id"
30
+ end
31
+ end
32
+
33
+ # Internal: Proxies method calls to the data, wrapping it accordingly and
34
+ # caching the result by defining a real reader method.
35
+ #
36
+ # Returns the value for the requested property.
37
+ #
38
+ # Raises a NoMethodError if the property doesn't exist.
39
+ def method_missing(m, *args)
40
+ super unless respond_to_missing?(m, *args)
41
+ cache(m, wrapped(to_h[m.to_s]))
42
+ end
43
+
44
+ # Internal: Guard for the method_missing proxy. Checks if the resource
45
+ # actually has a specific piece of data at all.
46
+ #
47
+ # Returns true if the resource has the property, false otherwise.
48
+ def respond_to_missing?(m, *)
49
+ to_h.key?(m.to_s)
50
+ end
51
+
52
+ # Public:
53
+ # Returns the raw Hash representation of the data.
54
+ def to_h
55
+ @_data
56
+ end
57
+
58
+ def to_s
59
+ attrs = to_h.map { |k, _| "#{k}: #{public_send(k).inspect}" }.join(', ')
60
+ "#<Asana::#{self.class.name.split('::').last} #{attrs}>"
61
+ end
62
+
63
+ alias_method :inspect, :to_s
64
+
65
+ private
66
+
67
+ # Internal: The Asana::Client instance.
68
+ def client # rubocop:disable Style/TrivialAccessors
69
+ @_client
70
+ end
71
+
72
+ # Internal: Caches a property and a value by defining a reader method for
73
+ # it.
74
+ #
75
+ # property - [#to_s] the property
76
+ # value - [Object] the corresponding value
77
+ #
78
+ # Returns the value.
79
+ def cache(property, value)
80
+ field = :"@#{property}"
81
+ instance_variable_set(field, value)
82
+ define_singleton_method(property) { instance_variable_get(field) }
83
+ value
84
+ end
85
+
86
+ # Internal: Wraps a value in a more useful class if possible, namely a
87
+ # Resource or a Collection.
88
+ #
89
+ # Returns the wrapped value or the plain value if it couldn't be wrapped.
90
+ def wrapped(value)
91
+ case value
92
+ when Hash then Resource.new(value, client: client)
93
+ when Array then value.map(&method(:wrapped))
94
+ else value
95
+ end
96
+ end
97
+
98
+ def refresh_with(data)
99
+ self.class.new(data, client: @client)
100
+ end
101
+ end
102
+ end
103
+ end