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