asana-0.4.0/0000755000175600017570000000000012636335256011652 5ustar pravipraviasana-0.4.0/.codeclimate.yml0000644000175600017570000000010112636335255014713 0ustar pravipravilanguages: Ruby: true exclude_paths: - "lib/asana/resources/*" asana-0.4.0/CODE_OF_CONDUCT.md0000644000175600017570000000261512636335255014454 0ustar pravipravi# Contributor Code of Conduct As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) asana-0.4.0/Gemfile0000644000175600017570000000046612636335255013152 0ustar pravipravisource 'https://rubygems.org' # Specify your gem's dependencies in asana.gemspec gemspec group :tools do gem 'rubocop' gem 'rubocop-rspec' gem 'guard' gem 'guard-rspec' gem 'guard-rubocop' gem 'guard-yard' gem 'yard' gem 'yard-tomdoc' gem 'byebug' gem 'simplecov', require: false end asana-0.4.0/LICENSE.txt0000644000175600017570000000206612636335255013500 0ustar pravipraviThe MIT License (MIT) Copyright (c) 2015 Asana, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. asana-0.4.0/asana.gemspec0000644000175600017570000000223512636335255014303 0ustar pravipravi# coding: utf-8 lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'asana/version' Gem::Specification.new do |spec| spec.name = "asana" spec.version = Asana::VERSION spec.authors = ["Txus"] spec.email = ["me@txus.io"] spec.summary = %q{Official Ruby client for the Asana API} spec.description = %q{Official Ruby client for the Asana API} spec.homepage = "https://github.com/asana/ruby-asana" spec.license = "MIT" spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] spec.required_ruby_version = '~> 2.0' spec.add_dependency "oauth2", "~> 1.0" spec.add_dependency "faraday", "~> 0.9" spec.add_dependency "faraday_middleware", "~> 0.9" spec.add_dependency "faraday_middleware-multi_json", "~> 0.0" spec.add_development_dependency "bundler", "~> 1.7" spec.add_development_dependency "rake", "~> 10.0" spec.add_development_dependency "rspec", "~> 3.2" end asana-0.4.0/lib/0000755000175600017570000000000012636335255012417 5ustar pravipraviasana-0.4.0/lib/asana.rb0000644000175600017570000000045412636335255014032 0ustar pravipravirequire 'asana/ruby2_0_0_compatibility' require 'asana/authentication' require 'asana/resources' require 'asana/client' require 'asana/errors' require 'asana/http_client' require 'asana/version' # Public: Top-level namespace of the Asana API Ruby client. module Asana include Asana::Resources end asana-0.4.0/lib/templates/0000755000175600017570000000000012636335255014415 5ustar pravipraviasana-0.4.0/lib/templates/index.js0000644000175600017570000000023412636335255016061 0ustar pravipravimodule.exports = { resource: { template: 'resource.ejs', filename: function(resource, helpers) { return resource.name + '.rb'; } } }; asana-0.4.0/lib/templates/resource.ejs0000644000175600017570000002167612636335255016763 0ustar pravipravi<% var singularName = resource.name, pluralName = plural(singularName), mixins = { task: ["AttachmentUploading", "EventSubscription"], project: ["EventSubscription"] }, skip = { attachment: ["createOnTask"] }, formatComment = function formatComment(text, indentation) { var indent = Array(indentation + 1).join(" ") return text.trim().split("\n").map(function(line) { return indent + (line.length > 0 ? "# " : "#") + line }).join("\n") } function Action(action) { var that = this this.action = action this.collection = action.collection == true this.requiresData = action.method == "POST" || action.method == "PUT" this.isInstanceAction = (action.method == "PUT" || action.method == "DELETE") this.method = action.method this.methodName = snake(action.name) this.clientMethod = action.method.toLowerCase() this.returnsUpdatedRecord = action.method == 'PUT' || action.comment.match(/Returns[a-z\W]+updated/) !== null this.returnsNothing = action.comment.match(/Returns an empty/) !== null // Params and idParams var params = action.params || [] this.idParams = _.filter(params, function(p) { return p.type == "Id" }) // If it looks like an instance action but it's not, make it one if (!this.isInstanceAction) { var mainIdParam = _.find(this.idParams, function(p) { return p.name == singularName }) if (mainIdParam !== undefined && !action.name.match(/Id/)) { this.isInstanceAction = true this.mainIdParam = mainIdParam } } if (this.idParams.length == 1 && action.path.match(/%d/) && (action.name.match(/Id/) || (this.isInstanceAction && this.mainIdParam == undefined))) { var mainIdParam = this.idParams[0] this.mainIdParam = mainIdParam this.inferredReturnType = this.isInstanceAction ? 'self.class' : 'self' } if (mainIdParam !== undefined) { this.params = _.reject(params, function(p) { return p.name == mainIdParam.name }) } else { this.params = params } if (!this.inferredReturnType) { // Infer return type var name = action.path.match(/\/([a-zA-Z]+)$/) if (name !== null) { name = name[1] // Desugarize 'addProject' to 'project' var camelCaseTail = name.match(/.*([A-Z][a-z]+)$/) if (camelCaseTail !== null) { name = decap(camelCaseTail[1]) } name = single(name) var explicit = _.find(resources, function(p) { return p == name }) if (name == singularName || name == 'parent' || name == 'children' || name.match(/^sub/) !== null) { this.inferredReturnType = this.isInstanceAction ? 'self.class' : 'self' } else if (explicit !== undefined) { this.inferredReturnType = cap(explicit) } else { this.inferredReturnType = 'Resource' } } else { this.inferredReturnType = 'Resource' } } // Endpoint path this.path = _.reduce(this.idParams, function(acc, id) { var localName = that.mainIdParam == id ? "id" : id.name return acc.replace("\%d", "#{" + localName + "}") }, action.path) // Extra params (not in the URL) to be passed in the body of the call this.extraParams = _.reject(this.params, function(p) { return that.path.match(new RegExp("#{" + p.name + "}")) }) // Params processing var paramsLocal = "data" if (this.collection) { this.extraParams.push({ name: "per_page", apiParamName: "limit" }) } if (this.extraParams.length > 0) { var paramNames = _.map(this.extraParams, function(p) { return (p.apiParamName || p.name) + ": " + p.name }) if (this.requiresData) { var paramsProcessing = "with_params = data.merge(" + paramNames.join(", ") + ")" paramsLocal = "with_params" } else { var paramsProcessing = "params = { " + paramNames.join(", ") + " }" paramsLocal = "params" } paramsProcessing += ".reject { |_,v| v.nil? || Array(v).empty? }" } this.paramsProcessing = paramsProcessing this.paramsLocal = paramsLocal // Method argument names var argumentNames = Array() if (!this.isInstanceAction) { argumentNames.push("client") } if (this.mainIdParam !== undefined && !this.isInstanceAction) { argumentNames.push("id") } _.forEach(this.params, function(param) { argumentNames.push(param.name + ":" + (param.required ? " required(\"" + param.name + "\")" : " nil")) }) if (this.collection) { argumentNames.push("per_page: 20") } if (this.method != 'DELETE') { argumentNames.push("options: {}") } if (this.requiresData) { argumentNames.push("**data") } this.argumentNames = argumentNames // API request params var requestParams = Array() requestParams.push('"' + this.path + '"') if (this.paramsProcessing || this.argumentNames.indexOf("**data") != -1) { var argument = this.requiresData ? "body" : "params" requestParams.push(argument + ": " + paramsLocal) } if (this.method != 'DELETE') { requestParams.push("options: options") } this.requestParams = requestParams this.documentation = this.renderDocumentation() // Constructor this.constructor = function(body) { var pre = '', post = '' var wrapWithParsing = function(body) { var pre = '', post = '' if (!that.returnsNothing) { pre = 'parse(' post = ')' + (that.collection ? '' : '.first') } return pre + body + post } if (!that.returnsNothing) { if (that.isInstanceAction && that.returnsUpdatedRecord) { pre = "refresh_with(" post = ')' } else { pre = that.collection ? "Collection.new(" : that.inferredReturnType + ".new(" post = (that.collection ? ', type: ' + that.inferredReturnType : '') + ', client: client)' } } else { post = ' && true' } return pre + wrapWithParsing(body) + post } this.request = this.constructor("client." + this.clientMethod + "(" + this.requestParams.join(", ") + ")") } Action.prototype.renderDocumentation = function () { var formatParamNotes = function(params) { var trimmed = _.flatten(_.map(params, function(p) { return _.map(p.notes, function(note) { return note.trim() }) })) return (trimmed.length > 0 ? "\nNotes:\n\n" + trimmed.join("\n\n") : "") } var formatParam = function(p, name) { return (name !== undefined ? name : p.name) + " - [" + p.type + "] " + p.comment } var lines = _.map(this.params, function(p) { return formatParam(p) }) if (this.mainIdParam !== undefined && !this.isInstanceAction) { lines.unshift(formatParam(this.mainIdParam, "id")) } if (this.collection) { lines.push("per_page - [Integer] the number of records to fetch per page.") } if (this.method != 'DELETE') { lines.push("options - [Hash] the request I/O options.") } if (this.requiresData) { lines.push("data - [Hash] the attributes to post.") } return this.action.comment + "\n" + lines.join("\n") + formatParamNotes(this.params) } var actionsToSkip = skip[resource.name] || [] var actionsToGen = _.reject(resource.actions, function(action) { return actionsToSkip.indexOf(action.name) != -1 }) var allActions = _.map(actionsToGen, function(action) { return new Action(action) }), instanceActions = _.filter(allActions, function(action) { return action.isInstanceAction }), classActions = _.reject(allActions, function(action) { return action.isInstanceAction }) var mixinsToInclude = mixins[resource.name] || [] %>### WARNING: This file is auto-generated by the asana-api-meta repo. Do not ### edit it manually. module Asana module Resources <%= formatComment(resource.comment, 4) %> class <%= cap(singularName) %> < Resource <% _.forEach(mixinsToInclude, function(mixin) { %> include <%= mixin %> <% }) %> <% _.forEach(resource.properties, function(property) { %> attr_reader :<%= property.name %> <% }) %> class << self # Returns the plural name of the resource. def plural_name '<%= pluralName %>' end <% _.forEach(classActions, function(action) { %> <%= formatComment(action.documentation, 8) %> def <%= action.methodName %>(<%= action.argumentNames.join(", ") %>) <% if (action.paramsProcessing) { %> <%= action.paramsProcessing %><% } %> <%= action.request %> end <% }) %> end <% _.forEach(instanceActions, function(action) { %> <%= formatComment(action.documentation, 6) %> def <%= action.methodName %>(<%= action.argumentNames.join(", ") %>) <% if (action.paramsProcessing) { %> <%= action.paramsProcessing %><% } %> <%= action.request %> end <% }) %> end end end asana-0.4.0/lib/asana/0000755000175600017570000000000012636335255013502 5ustar pravipraviasana-0.4.0/lib/asana/version.rb0000644000175600017570000000011612636335255015512 0ustar pravipravi#:nodoc: module Asana # Public: Version of the gem. VERSION = '0.4.0' end asana-0.4.0/lib/asana/resources/0000755000175600017570000000000012636335255015514 5ustar pravipraviasana-0.4.0/lib/asana/resources/tag.rb0000644000175600017570000001334012636335255016615 0ustar pravipravi### WARNING: This file is auto-generated by the asana-api-meta repo. Do not ### edit it manually. module Asana module Resources # A _tag_ is a label that can be attached to any task in Asana. It exists in a # single workspace or organization. # # Tags have some metadata associated with them, but it is possible that we will # simplify them in the future so it is not encouraged to rely too heavily on it. # Unlike projects, tags do not provide any ordering on the tasks they # are associated with. class Tag < Resource attr_reader :id attr_reader :created_at attr_reader :followers attr_reader :name attr_reader :color attr_reader :notes attr_reader :workspace class << self # Returns the plural name of the resource. def plural_name 'tags' end # Creates a new tag in a workspace or organization. # # Every tag is required to be created in a specific workspace or # organization, and this cannot be changed once set. Note that you can use # the `workspace` parameter regardless of whether or not it is an # organization. # # Returns the full record of the newly created tag. # # workspace - [Id] The workspace or organization to create the tag in. # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def create(client, workspace: required("workspace"), options: {}, **data) with_params = data.merge(workspace: workspace).reject { |_,v| v.nil? || Array(v).empty? } self.new(parse(client.post("/tags", body: with_params, options: options)).first, client: client) end # Creates a new tag in a workspace or organization. # # Every tag is required to be created in a specific workspace or # organization, and this cannot be changed once set. Note that you can use # the `workspace` parameter regardless of whether or not it is an # organization. # # Returns the full record of the newly created tag. # # workspace - [Id] The workspace or organization to create the tag in. # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def create_in_workspace(client, workspace: required("workspace"), options: {}, **data) self.new(parse(client.post("/workspaces/#{workspace}/tags", body: data, options: options)).first, client: client) end # Returns the complete tag record for a single tag. # # id - [Id] The tag to get. # options - [Hash] the request I/O options. def find_by_id(client, id, options: {}) self.new(parse(client.get("/tags/#{id}", options: options)).first, client: client) end # Returns the compact tag records for some filtered set of tags. # Use one or more of the parameters provided to filter the tags returned. # # workspace - [Id] The workspace or organization to filter tags on. # team - [Id] The team to filter tags on. # archived - [Boolean] Only return tags whose `archived` field takes on the value of # this parameter. # # per_page - [Integer] the number of records to fetch per page. # options - [Hash] the request I/O options. def find_all(client, workspace: nil, team: nil, archived: nil, per_page: 20, options: {}) params = { workspace: workspace, team: team, archived: archived, limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? } Collection.new(parse(client.get("/tags", params: params, options: options)), type: self, client: client) end # Returns the compact tag records for all tags in the workspace. # # workspace - [Id] The workspace or organization to find tags in. # per_page - [Integer] the number of records to fetch per page. # options - [Hash] the request I/O options. def find_by_workspace(client, workspace: required("workspace"), per_page: 20, options: {}) params = { limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? } Collection.new(parse(client.get("/workspaces/#{workspace}/tags", params: params, options: options)), type: self, client: client) end end # Updates the properties of a tag. Only the fields provided in the `data` # block will be updated; any unspecified fields will remain unchanged. # # When using this method, it is best to specify only those fields you wish # to change, or else you may overwrite changes made by another user since # you last retrieved the task. # # Returns the complete updated tag record. # # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def update(options: {}, **data) refresh_with(parse(client.put("/tags/#{id}", body: data, options: options)).first) end # A specific, existing tag can be deleted by making a DELETE request # on the URL for that tag. # # Returns an empty data record. def delete() client.delete("/tags/#{id}") && true end # Returns the compact task records for all tasks with the given tag. # Tasks can have more than one tag at a time. # # per_page - [Integer] the number of records to fetch per page. # options - [Hash] the request I/O options. def get_tasks_with_tag(per_page: 20, options: {}) params = { limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? } Collection.new(parse(client.get("/tags/#{id}/tasks", params: params, options: options)), type: Task, client: client) end end end endasana-0.4.0/lib/asana/resources/project.rb0000644000175600017570000002537712636335255017525 0ustar pravipravi### WARNING: This file is auto-generated by the asana-api-meta repo. Do not ### edit it manually. module Asana module Resources # A _project_ represents a prioritized list of tasks in Asana. It exists in a # single workspace or organization and is accessible to a subset of users in # that workspace or organization, depending on its permissions. # # Projects in organizations are shared with a single team. You cannot currently # change the team of a project via the API. Non-organization workspaces do not # have teams and so you should not specify the team of project in a # regular workspace. class Project < Resource include EventSubscription attr_reader :name attr_reader :id attr_reader :owner attr_reader :current_status attr_reader :due_date attr_reader :created_at attr_reader :modified_at attr_reader :archived attr_reader :public attr_reader :members attr_reader :followers attr_reader :color attr_reader :notes attr_reader :workspace attr_reader :team class << self # Returns the plural name of the resource. def plural_name 'projects' end # Creates a new project in a workspace or team. # # Every project is required to be created in a specific workspace or # organization, and this cannot be changed once set. Note that you can use # the `workspace` parameter regardless of whether or not it is an # organization. # # If the workspace for your project _is_ an organization, you must also # supply a `team` to share the project with. # # Returns the full record of the newly created project. # # workspace - [Id] The workspace or organization to create the project in. # team - [Id] If creating in an organization, the specific team to create the # project in. # # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def create(client, workspace: required("workspace"), team: nil, options: {}, **data) with_params = data.merge(workspace: workspace, team: team).reject { |_,v| v.nil? || Array(v).empty? } self.new(parse(client.post("/projects", body: with_params, options: options)).first, client: client) end # If the workspace for your project _is_ an organization, you must also # supply a `team` to share the project with. # # Returns the full record of the newly created project. # # workspace - [Id] The workspace or organization to create the project in. # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def create_in_workspace(client, workspace: required("workspace"), options: {}, **data) self.new(parse(client.post("/workspaces/#{workspace}/projects", body: data, options: options)).first, client: client) end # Creates a project shared with the given team. # # Returns the full record of the newly created project. # # team - [Id] The team to create the project in. # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def create_in_team(client, team: required("team"), options: {}, **data) self.new(parse(client.post("/teams/#{team}/projects", body: data, options: options)).first, client: client) end # Returns the complete project record for a single project. # # id - [Id] The project to get. # options - [Hash] the request I/O options. def find_by_id(client, id, options: {}) self.new(parse(client.get("/projects/#{id}", options: options)).first, client: client) end # Returns the compact project records for some filtered set of projects. # Use one or more of the parameters provided to filter the projects returned. # # workspace - [Id] The workspace or organization to filter projects on. # team - [Id] The team to filter projects on. # archived - [Boolean] Only return projects whose `archived` field takes on the value of # this parameter. # # per_page - [Integer] the number of records to fetch per page. # options - [Hash] the request I/O options. def find_all(client, workspace: nil, team: nil, archived: nil, per_page: 20, options: {}) params = { workspace: workspace, team: team, archived: archived, limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? } Collection.new(parse(client.get("/projects", params: params, options: options)), type: self, client: client) end # Returns the compact project records for all projects in the workspace. # # workspace - [Id] The workspace or organization to find projects in. # archived - [Boolean] Only return projects whose `archived` field takes on the value of # this parameter. # # per_page - [Integer] the number of records to fetch per page. # options - [Hash] the request I/O options. def find_by_workspace(client, workspace: required("workspace"), archived: nil, per_page: 20, options: {}) params = { archived: archived, limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? } Collection.new(parse(client.get("/workspaces/#{workspace}/projects", params: params, options: options)), type: self, client: client) end # Returns the compact project records for all projects in the team. # # team - [Id] The team to find projects in. # archived - [Boolean] Only return projects whose `archived` field takes on the value of # this parameter. # # per_page - [Integer] the number of records to fetch per page. # options - [Hash] the request I/O options. def find_by_team(client, team: required("team"), archived: nil, per_page: 20, options: {}) params = { archived: archived, limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? } Collection.new(parse(client.get("/teams/#{team}/projects", params: params, options: options)), type: self, client: client) end end # A specific, existing project can be updated by making a PUT request on the # URL for that project. Only the fields provided in the `data` block will be # updated; any unspecified fields will remain unchanged. # # When using this method, it is best to specify only those fields you wish # to change, or else you may overwrite changes made by another user since # you last retrieved the task. # # Returns the complete updated project record. # # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def update(options: {}, **data) refresh_with(parse(client.put("/projects/#{id}", body: data, options: options)).first) end # A specific, existing project can be deleted by making a DELETE request # on the URL for that project. # # Returns an empty data record. def delete() client.delete("/projects/#{id}") && true end # Returns compact records for all sections in the specified project. # # per_page - [Integer] the number of records to fetch per page. # options - [Hash] the request I/O options. def sections(per_page: 20, options: {}) params = { limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? } Collection.new(parse(client.get("/projects/#{id}/sections", params: params, options: options)), type: Resource, client: client) end # Returns the compact task records for all tasks within the given project, # ordered by their priority within the project. Tasks can exist in more than one project at a time. # # per_page - [Integer] the number of records to fetch per page. # options - [Hash] the request I/O options. def tasks(per_page: 20, options: {}) params = { limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? } Collection.new(parse(client.get("/projects/#{id}/tasks", params: params, options: options)), type: Task, client: client) end # Adds the specified list of users as followers to the project. Followers are a subset of members, therefore if # the users are not already members of the project they will also become members as a result of this operation. # Returns the updated project record. # # followers - [Array] An array of followers to add to the project. # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def add_followers(followers: required("followers"), options: {}, **data) with_params = data.merge(followers: followers).reject { |_,v| v.nil? || Array(v).empty? } refresh_with(parse(client.post("/projects/#{id}/addFollowers", body: with_params, options: options)).first) end # Removes the specified list of users from following the project, this will not affect project membership status. # Returns the updated project record. # # followers - [Array] An array of followers to remove from the project. # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def remove_followers(followers: required("followers"), options: {}, **data) with_params = data.merge(followers: followers).reject { |_,v| v.nil? || Array(v).empty? } refresh_with(parse(client.post("/projects/#{id}/removeFollowers", body: with_params, options: options)).first) end # Adds the specified list of users as members of the project. Returns the updated project record. # # members - [Array] An array of members to add to the project. # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def add_members(members: required("members"), options: {}, **data) with_params = data.merge(members: members).reject { |_,v| v.nil? || Array(v).empty? } refresh_with(parse(client.post("/projects/#{id}/addMembers", body: with_params, options: options)).first) end # Removes the specified list of members from the project. Returns the updated project record. # # members - [Array] An array of members to remove from the project. # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def remove_members(members: required("members"), options: {}, **data) with_params = data.merge(members: members).reject { |_,v| v.nil? || Array(v).empty? } refresh_with(parse(client.post("/projects/#{id}/removeMembers", body: with_params, options: options)).first) end end end endasana-0.4.0/lib/asana/resources/attachment.rb0000644000175600017570000000320312636335255020167 0ustar pravipravi### WARNING: This file is auto-generated by the asana-api-meta repo. Do not ### edit it manually. module Asana module Resources # An _attachment_ object represents any file attached to a task in Asana, # whether it's an uploaded file or one associated via a third-party service # such as Dropbox or Google Drive. class Attachment < Resource attr_reader :id attr_reader :created_at attr_reader :download_url attr_reader :host attr_reader :name attr_reader :parent attr_reader :view_url class << self # Returns the plural name of the resource. def plural_name 'attachments' end # Returns the full record for a single attachment. # # id - [Id] Globally unique identifier for the attachment. # # options - [Hash] the request I/O options. def find_by_id(client, id, options: {}) self.new(parse(client.get("/attachments/#{id}", options: options)).first, client: client) end # Returns the compact records for all attachments on the task. # # task - [Id] Globally unique identifier for the task. # # per_page - [Integer] the number of records to fetch per page. # options - [Hash] the request I/O options. def find_by_task(client, task: required("task"), per_page: 20, options: {}) params = { limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? } Collection.new(parse(client.get("/tasks/#{task}/attachments", params: params, options: options)), type: self, client: client) end end end end endasana-0.4.0/lib/asana/resources/team.rb0000644000175600017570000000723312636335255016774 0ustar pravipravi### WARNING: This file is auto-generated by the asana-api-meta repo. Do not ### edit it manually. module Asana module Resources # A _team_ is used to group related projects and people together within an # organization. Each project in an organization is associated with a team. class Team < Resource attr_reader :id attr_reader :name class << self # Returns the plural name of the resource. def plural_name 'teams' end # Returns the full record for a single team. # # id - [Id] Globally unique identifier for the team. # # options - [Hash] the request I/O options. def find_by_id(client, id, options: {}) self.new(parse(client.get("/teams/#{id}", options: options)).first, client: client) end # Returns the compact records for all teams in the organization visible to # the authorized user. # # organization - [Id] Globally unique identifier for the workspace or organization. # # per_page - [Integer] the number of records to fetch per page. # options - [Hash] the request I/O options. def find_by_organization(client, organization: required("organization"), per_page: 20, options: {}) params = { limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? } Collection.new(parse(client.get("/organizations/#{organization}/teams", params: params, options: options)), type: self, client: client) end end # Returns the compact records for all users that are members of the team. # # per_page - [Integer] the number of records to fetch per page. # options - [Hash] the request I/O options. def users(per_page: 20, options: {}) params = { limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? } Collection.new(parse(client.get("/teams/#{id}/users", params: params, options: options)), type: User, client: client) end # The user making this call must be a member of the team in order to add others. # The user to add must exist in the same organization as the team in order to be added. # The user to add can be referenced by their globally unique user ID or their email address. # Returns the full user record for the added user. # # user - [String] An identifier for the user. Can be one of an email address, # the globally unique identifier for the user, or the keyword `me` # to indicate the current user making the request. # # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def add_user(user: required("user"), options: {}, **data) with_params = data.merge(user: user).reject { |_,v| v.nil? || Array(v).empty? } User.new(parse(client.post("/teams/#{id}/addUser", body: with_params, options: options)).first, client: client) end # The user to remove can be referenced by their globally unique user ID or their email address. # Removes the user from the specified team. Returns an empty data record. # # user - [String] An identifier for the user. Can be one of an email address, # the globally unique identifier for the user, or the keyword `me` # to indicate the current user making the request. # # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def remove_user(user: required("user"), options: {}, **data) with_params = data.merge(user: user).reject { |_,v| v.nil? || Array(v).empty? } client.post("/teams/#{id}/removeUser", body: with_params, options: options) && true end end end endasana-0.4.0/lib/asana/resources/workspace.rb0000644000175600017570000001324712636335255020046 0ustar pravipravi### WARNING: This file is auto-generated by the asana-api-meta repo. Do not ### edit it manually. module Asana module Resources # A _workspace_ is the highest-level organizational unit in Asana. All projects # and tasks have an associated workspace. # # An _organization_ is a special kind of workspace that represents a company. # In an organization, you can group your projects into teams. You can read # more about how organizations work on the Asana Guide. # To tell if your workspace is an organization or not, check its # `is_organization` property. # # Over time, we intend to migrate most workspaces into organizations and to # release more organization-specific functionality. We may eventually deprecate # using workspace-based APIs for organizations. Currently, and until after # some reasonable grace period following any further announcements, you can # still reference organizations in any `workspace` parameter. class Workspace < Resource attr_reader :id attr_reader :name attr_reader :is_organization class << self # Returns the plural name of the resource. def plural_name 'workspaces' end # Returns the full workspace record for a single workspace. # # id - [Id] Globally unique identifier for the workspace or organization. # # options - [Hash] the request I/O options. def find_by_id(client, id, options: {}) self.new(parse(client.get("/workspaces/#{id}", options: options)).first, client: client) end # Returns the compact records for all workspaces visible to the authorized user. # # per_page - [Integer] the number of records to fetch per page. # options - [Hash] the request I/O options. def find_all(client, per_page: 20, options: {}) params = { limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? } Collection.new(parse(client.get("/workspaces", params: params, options: options)), type: self, client: client) end end # A specific, existing workspace can be updated by making a PUT request on # the URL for that workspace. Only the fields provided in the data block # will be updated; any unspecified fields will remain unchanged. # # Currently the only field that can be modified for a workspace is its `name`. # # Returns the complete, updated workspace record. # # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def update(options: {}, **data) refresh_with(parse(client.put("/workspaces/#{id}", body: data, options: options)).first) end # Retrieves objects in the workspace based on an auto-completion/typeahead # search algorithm. This feature is meant to provide results quickly, so do # not rely on this API to provide extremely accurate search results. The # result set is limited to a single page of results with a maximum size, # so you won't be able to fetch large numbers of results. # # type - [Enum] The type of values the typeahead should return. # Note that unlike in the names of endpoints, the types listed here are # in singular form (e.g. `task`). Using multiple types is not yet supported. # # query - [String] The string that will be used to search for relevant objects. If an # empty string is passed in, the API will currently return an empty # result set. # # count - [Number] The number of results to return. The default is `20` if this # parameter is omitted, with a minimum of `1` and a maximum of `100`. # If there are fewer results found than requested, all will be returned. # # per_page - [Integer] the number of records to fetch per page. # options - [Hash] the request I/O options. def typeahead(type: required("type"), query: nil, count: nil, per_page: 20, options: {}) params = { type: type, query: query, count: count, limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? } Collection.new(parse(client.get("/workspaces/#{id}/typeahead", params: params, options: options)), type: Resource, client: client) end # The user can be referenced by their globally unique user ID or their email address. # Returns the full user record for the invited user. # # user - [String] An identifier for the user. Can be one of an email address, # the globally unique identifier for the user, or the keyword `me` # to indicate the current user making the request. # # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def add_user(user: required("user"), options: {}, **data) with_params = data.merge(user: user).reject { |_,v| v.nil? || Array(v).empty? } User.new(parse(client.post("/workspaces/#{id}/addUser", body: with_params, options: options)).first, client: client) end # The user making this call must be an admin in the workspace. # Returns an empty data record. # # user - [String] An identifier for the user. Can be one of an email address, # the globally unique identifier for the user, or the keyword `me` # to indicate the current user making the request. # # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def remove_user(user: required("user"), options: {}, **data) with_params = data.merge(user: user).reject { |_,v| v.nil? || Array(v).empty? } client.post("/workspaces/#{id}/removeUser", body: with_params, options: options) && true end end end endasana-0.4.0/lib/asana/resources/task.rb0000644000175600017570000003601212636335255017005 0ustar pravipravi### WARNING: This file is auto-generated by the asana-api-meta repo. Do not ### edit it manually. module Asana module Resources # The _task_ is the basic object around which many operations in Asana are # centered. In the Asana application, multiple tasks populate the middle pane # according to some view parameters, and the set of selected tasks determines # the more detailed information presented in the details pane. class Task < Resource include AttachmentUploading include EventSubscription attr_reader :id attr_reader :assignee attr_reader :assignee_status attr_reader :created_at attr_reader :completed attr_reader :completed_at attr_reader :due_on attr_reader :due_at attr_reader :external attr_reader :followers attr_reader :hearted attr_reader :hearts attr_reader :modified_at attr_reader :name attr_reader :notes attr_reader :num_hearts attr_reader :projects attr_reader :parent attr_reader :workspace attr_reader :memberships attr_reader :tags class << self # Returns the plural name of the resource. def plural_name 'tasks' end # Creating a new task is as easy as POSTing to the `/tasks` endpoint # with a data block containing the fields you'd like to set on the task. # Any unspecified fields will take on default values. # # Every task is required to be created in a specific workspace, and this # workspace cannot be changed once set. The workspace need not be set # explicitly if you specify a `project` or a `parent` task instead. # # workspace - [Id] The workspace to create a task in. # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def create(client, workspace: nil, options: {}, **data) with_params = data.merge(workspace: workspace).reject { |_,v| v.nil? || Array(v).empty? } self.new(parse(client.post("/tasks", body: with_params, options: options)).first, client: client) end # Creating a new task is as easy as POSTing to the `/tasks` endpoint # with a data block containing the fields you'd like to set on the task. # Any unspecified fields will take on default values. # # Every task is required to be created in a specific workspace, and this # workspace cannot be changed once set. The workspace need not be set # explicitly if you specify a `project` or a `parent` task instead. # # workspace - [Id] The workspace to create a task in. # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def create_in_workspace(client, workspace: required("workspace"), options: {}, **data) self.new(parse(client.post("/workspaces/#{workspace}/tasks", body: data, options: options)).first, client: client) end # Returns the complete task record for a single task. # # id - [Id] The task to get. # options - [Hash] the request I/O options. def find_by_id(client, id, options: {}) self.new(parse(client.get("/tasks/#{id}", options: options)).first, client: client) end # Returns the compact task records for all tasks within the given project, # ordered by their priority within the project. # # projectId - [Id] The project in which to search for tasks. # per_page - [Integer] the number of records to fetch per page. # options - [Hash] the request I/O options. def find_by_project(client, projectId: required("projectId"), per_page: 20, options: {}) params = { limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? } Collection.new(parse(client.get("/projects/#{projectId}/tasks", params: params, options: options)), type: self, client: client) end # Returns the compact task records for all tasks with the given tag. # # tag - [Id] The tag in which to search for tasks. # per_page - [Integer] the number of records to fetch per page. # options - [Hash] the request I/O options. def find_by_tag(client, tag: required("tag"), per_page: 20, options: {}) params = { limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? } Collection.new(parse(client.get("/tags/#{tag}/tasks", params: params, options: options)), type: self, client: client) end # Returns the compact task records for some filtered set of tasks. Use one # or more of the parameters provided to filter the tasks returned. # # assignee - [String] The assignee to filter tasks on. # workspace - [Id] The workspace or organization to filter tasks on. # completed_since - [String] Only return tasks that are either incomplete or that have been # completed since this time. # # modified_since - [String] Only return tasks that have been modified since the given time. # # per_page - [Integer] the number of records to fetch per page. # options - [Hash] the request I/O options. # Notes: # # If you specify `assignee`, you must also specify the `workspace` to filter on. # # If you specify `workspace`, you must also specify the `assignee` to filter on. # # A task is considered "modified" if any of its properties change, # or associations between it and other objects are modified (e.g. # a task being added to a project). A task is not considered modified # just because another object it is associated with (e.g. a subtask) # is modified. Actions that count as modifying the task include # assigning, renaming, completing, and adding stories. def find_all(client, assignee: nil, workspace: nil, completed_since: nil, modified_since: nil, per_page: 20, options: {}) params = { assignee: assignee, workspace: workspace, completed_since: completed_since, modified_since: modified_since, limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? } Collection.new(parse(client.get("/tasks", params: params, options: options)), type: self, client: client) end end # A specific, existing task can be updated by making a PUT request on the # URL for that task. Only the fields provided in the `data` block will be # updated; any unspecified fields will remain unchanged. # # When using this method, it is best to specify only those fields you wish # to change, or else you may overwrite changes made by another user since # you last retrieved the task. # # Returns the complete updated task record. # # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def update(options: {}, **data) refresh_with(parse(client.put("/tasks/#{id}", body: data, options: options)).first) end # A specific, existing task can be deleted by making a DELETE request on the # URL for that task. Deleted tasks go into the "trash" of the user making # the delete request. Tasks can be recovered from the trash within a period # of 30 days; afterward they are completely removed from the system. # # Returns an empty data record. def delete() client.delete("/tasks/#{id}") && true end # Adds each of the specified followers to the task, if they are not already # following. Returns the complete, updated record for the affected task. # # followers - [Array] An array of followers to add to the task. # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def add_followers(followers: required("followers"), options: {}, **data) with_params = data.merge(followers: followers).reject { |_,v| v.nil? || Array(v).empty? } refresh_with(parse(client.post("/tasks/#{id}/addFollowers", body: with_params, options: options)).first) end # Removes each of the specified followers from the task if they are # following. Returns the complete, updated record for the affected task. # # followers - [Array] An array of followers to remove from the task. # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def remove_followers(followers: required("followers"), options: {}, **data) with_params = data.merge(followers: followers).reject { |_,v| v.nil? || Array(v).empty? } refresh_with(parse(client.post("/tasks/#{id}/removeFollowers", body: with_params, options: options)).first) end # Returns a compact representation of all of the projects the task is in. # # per_page - [Integer] the number of records to fetch per page. # options - [Hash] the request I/O options. def projects(per_page: 20, options: {}) params = { limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? } Collection.new(parse(client.get("/tasks/#{id}/projects", params: params, options: options)), type: Project, client: client) end # Adds the task to the specified project, in the optional location # specified. If no location arguments are given, the task will be added to # the beginning of the project. # # `addProject` can also be used to reorder a task within a project that # already contains it. # # Returns an empty data block. # # project - [Id] The project to add the task to. # insertAfter - [Id] A task in the project to insert the task after, or `null` to # insert at the beginning of the list. # # insertBefore - [Id] A task in the project to insert the task before, or `null` to # insert at the end of the list. # # section - [Id] A section in the project to insert the task into. The task will be # inserted at the top of the section. # # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def add_project(project: required("project"), insertAfter: nil, insertBefore: nil, section: nil, options: {}, **data) with_params = data.merge(project: project, insertAfter: insertAfter, insertBefore: insertBefore, section: section).reject { |_,v| v.nil? || Array(v).empty? } client.post("/tasks/#{id}/addProject", body: with_params, options: options) && true end # Removes the task from the specified project. The task will still exist # in the system, but it will not be in the project anymore. # # Returns an empty data block. # # project - [Id] The project to remove the task from. # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def remove_project(project: required("project"), options: {}, **data) with_params = data.merge(project: project).reject { |_,v| v.nil? || Array(v).empty? } client.post("/tasks/#{id}/removeProject", body: with_params, options: options) && true end # Returns a compact representation of all of the tags the task has. # # per_page - [Integer] the number of records to fetch per page. # options - [Hash] the request I/O options. def tags(per_page: 20, options: {}) params = { limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? } Collection.new(parse(client.get("/tasks/#{id}/tags", params: params, options: options)), type: Tag, client: client) end # Adds a tag to a task. Returns an empty data block. # # tag - [Id] The tag to add to the task. # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def add_tag(tag: required("tag"), options: {}, **data) with_params = data.merge(tag: tag).reject { |_,v| v.nil? || Array(v).empty? } client.post("/tasks/#{id}/addTag", body: with_params, options: options) && true end # Removes a tag from the task. Returns an empty data block. # # tag - [Id] The tag to remove from the task. # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def remove_tag(tag: required("tag"), options: {}, **data) with_params = data.merge(tag: tag).reject { |_,v| v.nil? || Array(v).empty? } client.post("/tasks/#{id}/removeTag", body: with_params, options: options) && true end # Returns a compact representation of all of the subtasks of a task. # # per_page - [Integer] the number of records to fetch per page. # options - [Hash] the request I/O options. def subtasks(per_page: 20, options: {}) params = { limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? } Collection.new(parse(client.get("/tasks/#{id}/subtasks", params: params, options: options)), type: self.class, client: client) end # Creates a new subtask and adds it to the parent task. Returns the full record # for the newly created subtask. # # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def add_subtask(options: {}, **data) self.class.new(parse(client.post("/tasks/#{id}/subtasks", body: data, options: options)).first, client: client) end # Changes the parent of a task. Each task may only be a subtask of a single # parent, or no parent task at all. Returns an empty data block. # # parent - [Id] The new parent of the task, or `null` for no parent. # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def set_parent(parent: required("parent"), options: {}, **data) with_params = data.merge(parent: parent).reject { |_,v| v.nil? || Array(v).empty? } client.post("/tasks/#{id}/setParent", body: with_params, options: options) && true end # Returns a compact representation of all of the stories on the task. # # per_page - [Integer] the number of records to fetch per page. # options - [Hash] the request I/O options. def stories(per_page: 20, options: {}) params = { limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? } Collection.new(parse(client.get("/tasks/#{id}/stories", params: params, options: options)), type: Story, client: client) end # Adds a comment to a task. The comment will be authored by the # currently authenticated user, and timestamped when the server receives # the request. # # Returns the full record for the new story added to the task. # # text - [String] The plain text of the comment to add. # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def add_comment(text: required("text"), options: {}, **data) with_params = data.merge(text: text).reject { |_,v| v.nil? || Array(v).empty? } Story.new(parse(client.post("/tasks/#{id}/stories", body: with_params, options: options)).first, client: client) end end end endasana-0.4.0/lib/asana/resources/story.rb0000644000175600017570000000541212636335255017223 0ustar pravipravi### WARNING: This file is auto-generated by the asana-api-meta repo. Do not ### edit it manually. module Asana module Resources # A _story_ represents an activity associated with an object in the Asana # system. Stories are generated by the system whenever users take actions such # as creating or assigning tasks, or moving tasks between projects. _Comments_ # are also a form of user-generated story. # # Stories are a form of history in the system, and as such they are read-only. # Once generated, it is not possible to modify a story. class Story < Resource attr_reader :id attr_reader :created_at attr_reader :created_by attr_reader :hearted attr_reader :hearts attr_reader :num_hearts attr_reader :text attr_reader :html_text attr_reader :target attr_reader :source attr_reader :type class << self # Returns the plural name of the resource. def plural_name 'stories' end # Returns the compact records for all stories on the task. # # task - [Id] Globally unique identifier for the task. # # per_page - [Integer] the number of records to fetch per page. # options - [Hash] the request I/O options. def find_by_task(client, task: required("task"), per_page: 20, options: {}) params = { limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? } Collection.new(parse(client.get("/tasks/#{task}/stories", params: params, options: options)), type: self, client: client) end # Returns the full record for a single story. # # id - [Id] Globally unique identifier for the story. # # options - [Hash] the request I/O options. def find_by_id(client, id, options: {}) self.new(parse(client.get("/stories/#{id}", options: options)).first, client: client) end # Adds a comment to a task. The comment will be authored by the # currently authenticated user, and timestamped when the server receives # the request. # # Returns the full record for the new story added to the task. # # task - [Id] Globally unique identifier for the task. # # text - [String] The plain text of the comment to add. # options - [Hash] the request I/O options. # data - [Hash] the attributes to post. def create_on_task(client, task: required("task"), text: required("text"), options: {}, **data) with_params = data.merge(text: text).reject { |_,v| v.nil? || Array(v).empty? } self.new(parse(client.post("/tasks/#{task}/stories", body: with_params, options: options)).first, client: client) end end end end endasana-0.4.0/lib/asana/resources/user.rb0000644000175600017570000000577112636335255017031 0ustar pravipravi### WARNING: This file is auto-generated by the asana-api-meta repo. Do not ### edit it manually. module Asana module Resources # A _user_ object represents an account in Asana that can be given access to # various workspaces, projects, and tasks. # # Like other objects in the system, users are referred to by numerical IDs. # However, the special string identifier `me` can be used anywhere # a user ID is accepted, to refer to the current authenticated user. class User < Resource attr_reader :id attr_reader :name attr_reader :email attr_reader :photo attr_reader :workspaces class << self # Returns the plural name of the resource. def plural_name 'users' end # Returns the full user record for the currently authenticated user. # # options - [Hash] the request I/O options. def me(client, options: {}) Resource.new(parse(client.get("/users/me", options: options)).first, client: client) end # Returns the full user record for the single user with the provided ID. # # user - [String] An identifier for the user. Can be one of an email address, # the globally unique identifier for the user, or the keyword `me` # to indicate the current user making the request. # # options - [Hash] the request I/O options. def find_by_id(client, user: required("user"), options: {}) params = { user: user }.reject { |_,v| v.nil? || Array(v).empty? } Resource.new(parse(client.get("/users/%s", params: params, options: options)).first, client: client) end # Returns the user records for all users in the specified workspace or # organization. # # workspace - [Id] The workspace in which to get users. # per_page - [Integer] the number of records to fetch per page. # options - [Hash] the request I/O options. def find_by_workspace(client, workspace: required("workspace"), per_page: 20, options: {}) params = { limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? } Collection.new(parse(client.get("/workspaces/#{workspace}/users", params: params, options: options)), type: self, client: client) end # Returns the user records for all users in all workspaces and organizations # accessible to the authenticated user. Accepts an optional workspace ID # parameter. # # workspace - [Id] The workspace or organization to filter users on. # per_page - [Integer] the number of records to fetch per page. # options - [Hash] the request I/O options. def find_all(client, workspace: nil, per_page: 20, options: {}) params = { workspace: workspace, limit: per_page }.reject { |_,v| v.nil? || Array(v).empty? } Collection.new(parse(client.get("/users", params: params, options: options)), type: self, client: client) end end end end endasana-0.4.0/lib/asana/errors.rb0000644000175600017570000000571412636335255015352 0ustar pravipravimodule Asana # Public: Defines the different errors that the Asana API may throw, which the # client code may want to catch. module Errors # Public: A generic, catch-all API error. It contains the whole response # object for debugging purposes. # # Note: This exception should never be raised when there exists a more # specific subclass. APIError = Class.new(StandardError) do attr_accessor :response def to_s 'An unknown API error ocurred.' end end # Public: A 401 error. Raised when the credentials used are invalid and the # user could not be authenticated. NotAuthorized = Class.new(APIError) do def to_s 'A valid API key was not provided with the request, so the API could '\ 'not associate a user with the request.' end end # Public: A 403 error. Raised when the user doesn't have permission to # access the requested resource or to perform the requested action on it. Forbidden = Class.new(APIError) do def to_s 'The API key and request syntax was valid but the server is refusing '\ 'to complete the request. This can happen if you try to read or write '\ 'to objects or properties that the user does not have access to.' end end # Public: A 404 error. Raised when the requested resource doesn't exist. NotFound = Class.new(APIError) do def to_s 'Either the request method and path supplied do not specify a known '\ 'action in the API, or the object specified by the request does not '\ 'exist.' end end # Public: A 500 error. Raised when there is a problem in the Asana API # server. It contains a unique phrase that can be used to identify the # problem when contacting developer support. ServerError = Class.new(APIError) do attr_accessor :phrase def initialize(phrase) @phrase = phrase end def to_s "There has been an error on Asana's end. Use this unique phrase to "\ 'identify the problem when contacting developer support: ' + %("#{@phrase}") end end # Public: A 400 error. Raised when the request was malformed or missing some # parameters. It contains a list of errors indicating the specific problems. InvalidRequest = Class.new(APIError) do attr_accessor :errors def initialize(errors) @errors = errors end def to_s errors.join(', ') end end # Public: A 429 error. Raised when the Asana API enforces rate-limiting on # the client to avoid overload. It contains the number of seconds to wait # before retrying the operation. RateLimitEnforced = Class.new(APIError) do attr_accessor :retry_after_seconds def initialize(retry_after_seconds) @retry_after_seconds = retry_after_seconds end def to_s "Retry your request after #{@retry_after_seconds} seconds." end end end end asana-0.4.0/lib/asana/resource_includes/0000755000175600017570000000000012636335255017217 5ustar pravipraviasana-0.4.0/lib/asana/resource_includes/event_subscription.rb0000644000175600017570000000062212636335255023471 0ustar pravipravirequire_relative 'events' module Asana module Resources # Public: Mixin to enable a resource with the ability to fetch events about # itself. module EventSubscription # Public: Returns an infinite collection of events on the resource. def events(wait: 1, options: {}) Events.new(resource: id, client: client, wait: wait, options: options) end end end end asana-0.4.0/lib/asana/resource_includes/registry.rb0000644000175600017570000000350412636335255021416 0ustar pravipravirequire_relative 'resource' require 'set' module Asana module Resources # Internal: Global registry of Resource subclasses. It provides lookup from # singular and plural names to the actual class objects. # # Examples # # class Unicorn < Asana::Resources::Resource # path '/unicorns' # end # # Registry.lookup(:unicorn) # => Unicorn # Registry.lookup_many(:unicorns) # => Unicorn # module Registry class << self # Public: Registers a new resource class. # # resource_klass - [Class] the resource class. # # Returns nothing. def register(resource_klass) resources << resource_klass end # Public: Looks up a resource class by its singular name. # # singular_name - [#to_s] the name of the resource, e.g :unicorn. # # Returns the resource class or {Asana::Resources::Resource}. def lookup(singular_name) resources.detect do |klass| klass.singular_name.to_s == singular_name.to_s end || Resource end # Public: Looks up a resource class by its plural name. # # plural_name - [#to_s] the plural name of the resource, e.g :unicorns. # # Returns the resource class or {Asana::Resources::Resource}. def lookup_many(plural_name) resources.detect do |klass| klass.plural_name.to_s == plural_name.to_s end || Resource end # Internal: A set of Resource classes. # # Returns the Set, defaulting to the empty set. # # Note: this object is a mutable singleton, so it should not be accessed # from multiple threads. def resources @resources ||= Set.new end end end end end asana-0.4.0/lib/asana/resource_includes/event.rb0000644000175600017570000000372312636335255020672 0ustar pravipravirequire_relative 'events' module Asana module Resources # An _event_ is an object representing a change to a resource that was # observed by an event subscription. # # In general, requesting events on a resource is faster and subject to # higher rate limits than requesting the resource itself. Additionally, # change events bubble up - listening to events on a project would include # when stories are added to tasks in the project, even on subtasks. # # Establish an initial sync token by making a request with no sync token. # The response will be a `412` error - the same as if the sync token had # expired. # # Subsequent requests should always provide the sync token from the # immediately preceding call. # # Sync tokens may not be valid if you attempt to go 'backward' in the # history by requesting previous tokens, though re-requesting the current # sync token is generally safe, and will always return the same results. # # When you receive a `412 Precondition Failed` error, it means that the sync # token is either invalid or expired. If you are attempting to keep a set of # data in sync, this signals you may need to re-crawl the data. # # Sync tokens always expire after 24 hours, but may expire sooner, depending # on load on the service. class Event < Resource attr_reader :type class << self # Returns the plural name of the resource. def plural_name 'events' end # Public: Returns an infinite collection of events on a particular # resource. # # client - [Asana::Client] the client to perform the requests. # id - [String] the id of the resource to get events from. # wait - [Integer] the number of seconds to wait between each poll. def for(client, id, wait: 1) Events.new(resource: id, client: client, wait: wait) end end end end end asana-0.4.0/lib/asana/resource_includes/collection.rb0000644000175600017570000000415012636335255021677 0ustar pravipravirequire_relative 'response_helper' module Asana module Resources # Public: Represents a paginated collection of Asana resources. class Collection include Enumerable include ResponseHelper attr_reader :elements # Public: Initializes a collection representing a page of resources of a # given type. # # (elements, extra) - [Array] an (String, Hash) tuple coming from the # response parser. # type - [Class] the type of resource that the collection # contains. Defaults to the generic Resource. # client - [Asana::Client] the client to perform requests. def initialize((elements, extra), type: Resource, client: required('client')) @elements = elements.map { |elem| type.new(elem, client: client) } @type = type @next_page_data = extra['next_page'] @client = client end # Public: Iterates over the elements of the collection. def each(&block) if block @elements.each(&block) (next_page || []).each(&block) else to_enum end end # Public: Returns the size of the collection. def size to_a.size end alias_method :length, :size # Public: Returns a String representation of the collection. def to_s "# " \ "[#{@elements.map(&:inspect).join(', ')}" + (@next_page_data ? ', ...' : '') + ']>' end alias_method :inspect, :to_s # Public: Returns a new Asana::Resources::Collection with the next page # or nil if there are no more pages. Caches the result. def next_page if defined?(@next_page) @next_page else @next_page = if @next_page_data response = parse(@client.get(@next_page_data['path'])) self.class.new(response, type: @type, client: @client) end end end end end end asana-0.4.0/lib/asana/resource_includes/attachment_uploading.rb0000644000175600017570000000237412636335255023744 0ustar pravipravimodule Asana module Resources # Internal: Mixin to add the ability to upload an attachment to a specific # Asana resource (a Task, really). module AttachmentUploading # Uploads a new attachment to the resource. # # filename - [String] the absolute path of the file to upload. # mime - [String] the MIME type of the file # options - [Hash] the request I/O options # data - [Hash] extra attributes to post # # rubocop:disable Metrics/AbcSize # rubocop:disable Metrics/MethodLength def attach(filename: required('filename'), mime: required('mime'), options: {}, **data) path = File.expand_path(filename) unless File.exist?(path) fail ArgumentError, "file #{filename} doesn't exist" end upload = Faraday::UploadIO.new(path, mime) response = client.post("/#{self.class.plural_name}/#{id}/attachments", body: data, upload: upload, options: options) Attachment.new(parse(response).first, client: client) end # rubocop:enable Metrics/MethodLength # rubocop:enable Metrics/AbcSize end end end asana-0.4.0/lib/asana/resource_includes/events.rb0000644000175600017570000000667512636335255021066 0ustar pravipravirequire_relative 'event' module Asana module Resources # Public: An infinite collection of events. # # Since they are infinite, if you want to filter or do other collection # operations without blocking indefinitely you should call #lazy on them to # turn them into a lazy collection. # # Examples: # # # Subscribes to an event stream and blocks indefinitely, printing # # information of every event as it comes in. # events = Events.new(resource: 'someresourceID', client: client) # events.each do |event| # puts [event.type, event.action] # end # # # Lazily filters events as they come in and prints them. # events = Events.new(resource: 'someresourceID', client: client) # events.lazy.select { |e| e.type == 'task' }.each do |event| # puts [event.type, event.action] # end # class Events include Enumerable # Public: Initializes a new Events instance, subscribed to a resource ID. # # resource - [String] a resource ID. Can be a task id or a workspace id. # client - [Asana::Client] a client to perform the requests. # wait - [Integer] the number of seconds to wait between each poll. # options - [Hash] the request I/O options def initialize(resource: required('resource'), client: required('client'), wait: 1, options: {}) @resource = resource @client = client @events = [] @wait = wait @options = options @sync = nil @last_poll = nil end # Public: Iterates indefinitely over all events happening to a particular # resource from the @sync timestamp or from now if it is nil. def each(&block) if block loop do poll if @events.empty? event = @events.shift yield event if event end else to_enum end end private # Internal: Polls and fetches all events that have occurred since the sync # token was created. Updates the sync token as it comes back from the # response. # # If we polled less than @wait seconds ago, we don't do anything. # # Notes: # # On the first request, the sync token is not passed (because it is # nil). The response will be the same as for an expired sync token, and # will include a new valid sync token. # # If the sync token is too old (which may happen from time to time) # the API will return a `412 Precondition Failed` error, and include # a fresh `sync` token in the response. def poll rate_limiting do body = @client.get('/events', params: params, options: @options).body @sync = body['sync'] @events += body.fetch('data', []).map do |event_data| Event.new(event_data, client: @client) end end end # Internal: Returns the formatted params for the poll request. def params { resource: @resource, sync: @sync }.reject { |_, v| v.nil? } end # Internal: Executes a block if at least @wait seconds have passed since # @last_poll. def rate_limiting(&block) return if @last_poll && Time.now - @last_poll <= @wait block.call.tap { @last_poll = Time.now } end end end end asana-0.4.0/lib/asana/resource_includes/response_helper.rb0000644000175600017570000000057012636335255022743 0ustar pravipravimodule Asana module Resources # Internal: A helper to make response body parsing easier. module ResponseHelper def parse(response) data = response.body.fetch('data') do fail("Unexpected response body: #{response.body}") end extra = response.body.reject { |k, _| k == 'data' } [data, extra] end end end end asana-0.4.0/lib/asana/resource_includes/resource.rb0000644000175600017570000000551612636335255021402 0ustar pravipravirequire_relative 'registry' require_relative 'response_helper' module Asana module Resources # Public: The base resource class which provides some sugar over common # resource functionality. class Resource include ResponseHelper extend ResponseHelper def self.inherited(base) Registry.register(base) end def initialize(data, client: required('client')) @_client = client @_data = data data.each do |k, v| instance_variable_set(:"@#{k}", v) if respond_to?(k) end end # If it has findById, it implements #refresh def refresh if self.class.respond_to?(:find_by_id) self.class.find_by_id(client, id) else fail "#{self.class.name} does not respond to #find_by_id" end end # Internal: Proxies method calls to the data, wrapping it accordingly and # caching the result by defining a real reader method. # # Returns the value for the requested property. # # Raises a NoMethodError if the property doesn't exist. def method_missing(m, *args) super unless respond_to_missing?(m, *args) cache(m, wrapped(to_h[m.to_s])) end # Internal: Guard for the method_missing proxy. Checks if the resource # actually has a specific piece of data at all. # # Returns true if the resource has the property, false otherwise. def respond_to_missing?(m, *) to_h.key?(m.to_s) end # Public: # Returns the raw Hash representation of the data. def to_h @_data end def to_s attrs = to_h.map { |k, _| "#{k}: #{public_send(k).inspect}" }.join(', ') "#" end alias_method :inspect, :to_s private # Internal: The Asana::Client instance. def client @_client end # Internal: Caches a property and a value by defining a reader method for # it. # # property - [#to_s] the property # value - [Object] the corresponding value # # Returns the value. def cache(property, value) field = :"@#{property}" instance_variable_set(field, value) define_singleton_method(property) { instance_variable_get(field) } value end # Internal: Wraps a value in a more useful class if possible, namely a # Resource or a Collection. # # Returns the wrapped value or the plain value if it couldn't be wrapped. def wrapped(value) case value when Hash then Resource.new(value, client: client) when Array then value.map(&method(:wrapped)) else value end end def refresh_with(data) self.class.new(data, client: @client) end end end end asana-0.4.0/lib/asana/http_client/0000755000175600017570000000000012636335255016017 5ustar pravipraviasana-0.4.0/lib/asana/http_client/response.rb0000644000175600017570000000166012636335255020205 0ustar pravipravimodule Asana class HttpClient # Internal: Represents a response from the Asana API. class Response # Public: # Returns a [Faraday::Env] object for debugging. attr_reader :faraday_env # Public: # Returns the [Integer] status code of the response. attr_reader :status # Public: # Returns the [Hash] representing the parsed JSON body. attr_reader :body # Public: Wraps a Faraday response. # # faraday_response - [Faraday::Response] the Faraday response to wrap. def initialize(faraday_response) @faraday_env = faraday_response.env @status = faraday_env.status @body = faraday_env.body end # Public: # Returns a [String] representation of the response. def to_s "#" end alias_method :inspect, :to_s end end end asana-0.4.0/lib/asana/http_client/error_handling.rb0000644000175600017570000000672112636335255021347 0ustar pravipravirequire 'multi_json' require_relative '../errors' module Asana class HttpClient # Internal: Handles errors from the API and re-raises them as proper # exceptions. module ErrorHandling include Errors module_function # Public: Perform a request handling any API errors correspondingly. # # request - [Proc] a block that will execute the request. # # Returns a [Faraday::Response] object. # # Raises [Asana::Errors::InvalidRequest] for invalid requests. # Raises [Asana::Errors::NotAuthorized] for unauthorized requests. # Raises [Asana::Errors::Forbidden] for forbidden requests. # Raises [Asana::Errors::NotFound] when a resource can't be found. # Raises [Asana::Errors::RateLimitEnforced] when the API is throttling. # Raises [Asana::Errors::ServerError] when there's a server problem. # Raises [Asana::Errors::APIError] when the API returns an unknown error. # # rubocop:disable all def handle(&request) request.call rescue Faraday::ClientError => e raise e unless e.response case e.response[:status] when 400 then raise invalid_request(e.response) when 401 then raise not_authorized(e.response) when 403 then raise forbidden(e.response) when 404 then raise not_found(e.response) when 412 then recover_response(e.response) when 429 then raise rate_limit_enforced(e.response) when 500 then raise server_error(e.response) else raise api_error(e.response) end end # rubocop:enable all # Internal: Returns an InvalidRequest exception including a list of # errors. def invalid_request(response) errors = body(response).fetch('errors', []).map { |e| e['message'] } InvalidRequest.new(errors).tap do |exception| exception.response = response end end # Internal: Returns a NotAuthorized exception. def not_authorized(response) NotAuthorized.new.tap { |exception| exception.response = response } end # Internal: Returns a Forbidden exception. def forbidden(response) Forbidden.new.tap { |exception| exception.response = response } end # Internal: Returns a NotFound exception. def not_found(response) NotFound.new.tap { |exception| exception.response = response } end # Internal: Returns a RateLimitEnforced exception with a retry after # field. def rate_limit_enforced(response) retry_after_seconds = response[:headers]['Retry-After'] RateLimitEnforced.new(retry_after_seconds).tap do |exception| exception.response = response end end # Internal: Returns a ServerError exception with a unique phrase. def server_error(response) phrase = body(response).fetch('errors', []).first['phrase'] ServerError.new(phrase).tap do |exception| exception.response = response end end # Internal: Returns an APIError exception. def api_error(response) APIError.new.tap { |exception| exception.response = response } end # Internal: Parser a response body from JSON. def body(response) MultiJson.load(response[:body]) end def recover_response(response) r = response.dup.tap { |res| res[:body] = body(response) } Response.new(OpenStruct.new(env: OpenStruct.new(r))) end end end end asana-0.4.0/lib/asana/http_client/environment_info.rb0000644000175600017570000000265012636335255021726 0ustar pravipravirequire_relative '../version' require 'openssl' module Asana class HttpClient # Internal: Adds environment information to a Faraday request. class EnvironmentInfo # Internal: The default user agent to use in all requests to the API. USER_AGENT = "ruby-asana v#{Asana::VERSION}" def initialize(user_agent = nil) @user_agent = user_agent || USER_AGENT @openssl_version = OpenSSL::OPENSSL_VERSION @client_version = Asana::VERSION @os = os end # Public: Augments a Faraday connection with information about the # environment. def configure(builder) builder.headers[:user_agent] = @user_agent builder.headers[:"X-Asana-Client-Lib"] = header end private def header { os: @os, language: 'ruby', language_version: RUBY_VERSION, version: @client_version, openssl_version: @openssl_version } .map { |k, v| "#{k}=#{v}" }.join('&') end # rubocop:disable Metrics/MethodLength def os if RUBY_PLATFORM =~ /win32/ || RUBY_PLATFORM =~ /mingw/ 'windows' elsif RUBY_PLATFORM =~ /linux/ 'linux' elsif RUBY_PLATFORM =~ /darwin/ 'darwin' elsif RUBY_PLATFORM =~ /freebsd/ 'freebsd' else 'unknown' end end # rubocop:enable Metrics/MethodLength end end end asana-0.4.0/lib/asana/client/0000755000175600017570000000000012636335255014760 5ustar pravipraviasana-0.4.0/lib/asana/client/configuration.rb0000644000175600017570000001241712636335255020161 0ustar pravipravimodule Asana class Client # Internal: Represents a configuration DSL for an Asana::Client. # # Examples # # config = Configuration.new # config.authentication :access_token, 'personal_access_token' # config.adapter :typhoeus # config.configure_faraday { |conn| conn.use MyMiddleware } # config.to_h # # => { authentication: #, # faraday_adapter: :typhoeus, # faraday_configuration: # } # class Configuration # Public: Initializes an empty configuration object. def initialize @configuration = {} end # Public: Sets an authentication strategy. # # type - [:oauth2, :api_token] the kind of authentication strategy to use # value - [::OAuth2::AccessToken, String, Hash] the configuration for the # chosen authentication strategy. # # Returns nothing. # # Raises ArgumentError if the arguments are invalid. def authentication(type, value) auth = case type when :oauth2 then oauth2(value) when :access_token then from_bearer_token(value) else error "unsupported authentication type #{type}" end @configuration[:authentication] = auth end # Public: Sets a custom network adapter for Faraday. # # adapter - [Symbol, Proc] the adapter. # # Returns nothing. def faraday_adapter(adapter) @configuration[:faraday_adapter] = adapter end # Public: Sets a custom configuration block for the Faraday connection. # # config - [Proc] the configuration block. # # Returns nothing. def configure_faraday(&config) @configuration[:faraday_configuration] = config end # Public: Configures the client in debug mode, which will print verbose # information on STDERR. # # Returns nothing. def debug_mode @configuration[:debug_mode] = true end # Public: # Returns the configuration [Hash]. def to_h @configuration end private # Internal: Configures an OAuth2 authentication strategy from either an # OAuth2 access token object, or a plain refresh token, or a plain bearer # token. # # value - [::OAuth::AccessToken, String] the value to configure the # strategy from. # # Returns [Asana::Authentication::OAuth2::AccessTokenAuthentication, # Asana::Authentication::OAuth2::BearerTokenAuthentication] # the OAuth2 authentication strategy. # # Raises ArgumentError if the OAuth2 configuration arguments are invalid. # # rubocop:disable Metrics/MethodLength def oauth2(value) case value when ::OAuth2::AccessToken from_access_token(value) when -> v { v.is_a?(Hash) && v[:refresh_token] } from_refresh_token(value) when -> v { v.is_a?(Hash) && v[:bearer_token] } from_bearer_token(value[:bearer_token]) else error 'Invalid OAuth2 configuration: pass in either an ' \ '::OAuth2::AccessToken object of your own or a hash ' \ 'containing :refresh_token or :bearer_token.' end end # Internal: Configures an OAuth2 AccessTokenAuthentication strategy. # # access_token - [::OAuth2::AccessToken] the OAuth2 access token object # # Returns a [Authentication::OAuth2::AccessTokenAuthentication] strategy. def from_access_token(access_token) Authentication::OAuth2::AccessTokenAuthentication .new(access_token) end # Internal: Configures an OAuth2 AccessTokenAuthentication strategy. # # hash - The configuration hash: # :refresh_token - [String] the OAuth2 refresh token # :client_id - [String] the OAuth2 client id # :client_secret - [String] the OAuth2 client secret # :redirect_uri - [String] the OAuth2 redirect URI # # Returns a [Authentication::OAuth2::AccessTokenAuthentication] strategy. def from_refresh_token(hash) refresh_token, client_id, client_secret, redirect_uri = requiring(hash, :refresh_token, :client_id, :client_secret, :redirect_uri) Authentication::OAuth2::AccessTokenAuthentication .from_refresh_token(refresh_token, client_id: client_id, client_secret: client_secret, redirect_uri: redirect_uri) end # Internal: Configures an OAuth2 BearerTokenAuthentication strategy. # # bearer_token - [String] the plain OAuth2 bearer token # # Returns a [Authentication::OAuth2::BearerTokenAuthentication] strategy. def from_bearer_token(bearer_token) Authentication::OAuth2::BearerTokenAuthentication .new(bearer_token) end def requiring(hash, *keys) missing_keys = keys.select { |k| !hash.key?(k) } missing_keys.any? && error("Missing keys: #{missing_keys.join(', ')}") keys.map { |k| hash[k] } end def error(msg) fail ArgumentError, msg end end end end asana-0.4.0/lib/asana/client.rb0000644000175600017570000001007712636335255015312 0ustar pravipravirequire_relative 'authentication' require_relative 'client/configuration' require_relative 'resources' module Asana # Public: A client to interact with the Asana API. It exposes all the # available resources of the Asana API in idiomatic Ruby. # # Examples # # # Authentication with a personal access token # Asana::Client.new do |client| # client.authentication :access_token, '...' # end # # # OAuth2 with a plain bearer token (doesn't support auto-refresh) # Asana::Client.new do |client| # client.authentication :oauth2, bearer_token: '...' # end # # # OAuth2 with a plain refresh token and client credentials # Asana::Client.new do |client| # client.authentication :oauth2, # refresh_token: '...', # client_id: '...', # client_secret: '...', # redirect_uri: '...' # end # # # OAuth2 with an ::OAuth2::AccessToken object # Asana::Client.new do |client| # client.authentication :oauth2, my_oauth2_access_token_object # end # # # Use a custom Faraday network adapter # Asana::Client.new do |client| # client.authentication ... # client.adapter :typhoeus # end # # # Use a custom user agent string # Asana::Client.new do |client| # client.authentication ... # client.user_agent '...' # end # # # Pass in custom configuration to the Faraday connection # Asana::Client.new do |client| # client.authentication ... # client.configure_faraday { |conn| conn.use MyMiddleware } # end # class Client # Internal: Proxies Resource classes to implement a fluent API on the Client # instances. class ResourceProxy def initialize(client: required('client'), resource: required('resource')) @client = client @resource = resource end def method_missing(m, *args, &block) @resource.public_send(m, *([@client] + args), &block) end def respond_to_missing?(m, *) @resource.respond_to?(m) end end # Public: Initializes a new client. # # Yields a {Asana::Client::Configuration} object as a configuration # DSL. See {Asana::Client} for usage examples. def initialize config = Configuration.new.tap { |c| yield c }.to_h @http_client = HttpClient.new(authentication: config.fetch(:authentication), adapter: config[:faraday_adapter], user_agent: config[:user_agent], debug_mode: config[:debug_mode], &config[:faraday_config]) end # Public: Performs a GET request against an arbitrary Asana URL. Allows for # the user to interact with the API in ways that haven't been # reflected/foreseen in this library. def get(url, *args) @http_client.get(url, *args) end # Public: Performs a POST request against an arbitrary Asana URL. Allows for # the user to interact with the API in ways that haven't been # reflected/foreseen in this library. def post(url, *args) @http_client.post(url, *args) end # Public: Performs a PUT request against an arbitrary Asana URL. Allows for # the user to interact with the API in ways that haven't been # reflected/foreseen in this library. def put(url, *args) @http_client.put(url, *args) end # Public: Performs a DELETE request against an arbitrary Asana URL. Allows # for the user to interact with the API in ways that haven't been # reflected/foreseen in this library. def delete(url, *args) @http_client.delete(url, *args) end # Public: Exposes queries for all top-evel endpoints. # # E.g. #users will query /users and return a # Asana::Resources::Collection. Resources::Registry.resources.each do |resource_class| define_method(resource_class.plural_name) do ResourceProxy.new(client: @http_client, resource: resource_class) end end end end asana-0.4.0/lib/asana/http_client.rb0000644000175600017570000001262612636335255016353 0ustar pravipravirequire 'faraday' require 'faraday_middleware' require 'faraday_middleware/multi_json' require_relative 'http_client/error_handling' require_relative 'http_client/environment_info' require_relative 'http_client/response' module Asana # Internal: Wrapper over Faraday that abstracts authentication, request # parsing and common options. class HttpClient # Internal: The API base URI. BASE_URI = 'https://app.asana.com/api/1.0' # Public: Initializes an HttpClient to make requests to the Asana API. # # authentication - [Asana::Authentication] An authentication strategy. # adapter - [Symbol, Proc] A Faraday adapter, eiter a Symbol for # registered adapters or a Proc taking a builder for a # custom one. Defaults to Faraday.default_adapter. # user_agent - [String] The user agent. Defaults to "ruby-asana vX.Y.Z". # config - [Proc] An optional block that yields the Faraday builder # object for customization. def initialize(authentication: required('authentication'), adapter: nil, user_agent: nil, debug_mode: false, &config) @authentication = authentication @adapter = adapter || Faraday.default_adapter @environment_info = EnvironmentInfo.new(user_agent) @debug_mode = debug_mode @config = config end # Public: Performs a GET request against the API. # # resource_uri - [String] the resource URI relative to the base Asana API # URL, e.g "/users/me". # params - [Hash] the request parameters # options - [Hash] the request I/O options # # Returns an [Asana::HttpClient::Response] if everything went well. # Raises [Asana::Errors::APIError] if anything went wrong. def get(resource_uri, params: {}, options: {}) opts = options.reduce({}) do |acc, (k, v)| acc.tap do |hash| hash[:"opt_#{k}"] = v.is_a?(Array) ? v.join(',') : v end end perform_request(:get, resource_uri, params.merge(opts)) end # Public: Performs a PUT request against the API. # # resource_uri - [String] the resource URI relative to the base Asana API # URL, e.g "/users/me". # body - [Hash] the body to PUT. # options - [Hash] the request I/O options # # Returns an [Asana::HttpClient::Response] if everything went well. # Raises [Asana::Errors::APIError] if anything went wrong. def put(resource_uri, body: {}, options: {}) params = { data: body }.merge(options.empty? ? {} : { options: options }) perform_request(:put, resource_uri, params) end # Public: Performs a POST request against the API. # # resource_uri - [String] the resource URI relative to the base Asana API # URL, e.g "/tags". # body - [Hash] the body to POST. # upload - [Faraday::UploadIO] an upload object to post as multipart. # Defaults to nil. # options - [Hash] the request I/O options # # Returns an [Asana::HttpClient::Response] if everything went well. # Raises [Asana::Errors::APIError] if anything went wrong. def post(resource_uri, body: {}, upload: nil, options: {}) params = { data: body }.merge(options.empty? ? {} : { options: options }) if upload perform_request(:post, resource_uri, params.merge(file: upload)) do |c| c.request :multipart end else perform_request(:post, resource_uri, params) end end # Public: Performs a DELETE request against the API. # # resource_uri - [String] the resource URI relative to the base Asana API # URL, e.g "/tags". # # Returns an [Asana::HttpClient::Response] if everything went well. # Raises [Asana::Errors::APIError] if anything went wrong. def delete(resource_uri) perform_request(:delete, resource_uri) end private def connection(&request_config) Faraday.new do |builder| @authentication.configure(builder) @environment_info.configure(builder) request_config.call(builder) if request_config configure_format(builder) add_middleware(builder) @config.call(builder) if @config use_adapter(builder, @adapter) end end def perform_request(method, resource_uri, body = {}, &request_config) handling_errors do url = BASE_URI + resource_uri log_request(method, url, body) if @debug_mode Response.new(connection(&request_config).public_send(method, url, body)) end end def configure_format(builder) builder.request :multi_json builder.response :multi_json end def add_middleware(builder) builder.use Faraday::Response::RaiseError builder.use FaradayMiddleware::FollowRedirects end def use_adapter(builder, adapter) case adapter when Symbol builder.adapter(adapter) when Proc adapter.call(builder) end end def handling_errors(&request) ErrorHandling.handle(&request) end def log_request(method, url, body) STDERR.puts format('[%s] %s %s (%s)', self.class, method.to_s.upcase, url, body.inspect) end end end asana-0.4.0/lib/asana/authentication.rb0000644000175600017570000000031112636335255017041 0ustar pravipravirequire_relative 'authentication/oauth2' require_relative 'authentication/token_authentication' module Asana # Public: Authentication strategies for the Asana API. module Authentication end end asana-0.4.0/lib/asana/resources.rb0000644000175600017570000000064112636335255016042 0ustar pravipravirequire_relative 'resource_includes/resource' require_relative 'resource_includes/collection' Dir[File.join(File.dirname(__FILE__), 'resource_includes', '*.rb')] .each { |resource| require resource } Dir[File.join(File.dirname(__FILE__), 'resources', '*.rb')] .each { |resource| require resource } module Asana # Public: Contains all the resources that the Asana API can return. module Resources end end asana-0.4.0/lib/asana/ruby2_0_0_compatibility.rb0000644000175600017570000000012712636335255020461 0ustar pravipravidef required(name) fail(ArgumentError, "#{name} is a required keyword argument") end asana-0.4.0/lib/asana/authentication/0000755000175600017570000000000012636335255016521 5ustar pravipraviasana-0.4.0/lib/asana/authentication/token_authentication.rb0000644000175600017570000000076412636335255023274 0ustar pravipravimodule Asana module Authentication # Public: Represents an API token authentication mechanism. class TokenAuthentication def initialize(token) @token = token end # Public: Configures a Faraday connection injecting its token as # basic auth. # # builder - [Faraday::Connection] the Faraday connection instance. # # Returns nothing. def configure(connection) connection.basic_auth(@token, '') end end end end asana-0.4.0/lib/asana/authentication/oauth2/0000755000175600017570000000000012636335255017723 5ustar pravipraviasana-0.4.0/lib/asana/authentication/oauth2/bearer_token_authentication.rb0000644000175600017570000000212712636335255026011 0ustar pravipravimodule Asana module Authentication module OAuth2 # Public: A mechanism to authenticate with an OAuth2 bearer token obtained # somewhere, for instance through omniauth-asana. # # Note: This authentication mechanism doesn't support token refreshing. If # you'd like refreshing and you have a refresh token as well as a bearer # token, you can generate a proper access token with # {AccessTokenAuthentication.from_refresh_token}. class BearerTokenAuthentication # Public: Initializes a new BearerTokenAuthentication with a plain # bearer token. # # bearer_token - [String] a plain bearer token. def initialize(bearer_token) @token = bearer_token end # Public: Configures a Faraday connection injecting its token as an # OAuth2 bearer token. # # connection - [Faraday::Connection] the Faraday connection instance. # # Returns nothing. def configure(connection) connection.request :oauth2, @token end end end end end asana-0.4.0/lib/asana/authentication/oauth2/client.rb0000644000175600017570000000365412636335255021536 0ustar pravipravirequire 'oauth2' module Asana module Authentication module OAuth2 # Public: Deals with the details of obtaining an OAuth2 authorization URL # and obtaining access tokens from either authorization codes or refresh # tokens. class Client # Public: Initializes a new client with client credentials associated # with a registered Asana API application. # # client_id - [String] a client id from the registered application # client_secret - [String] a client secret from the registered # application # redirect_uri - [String] a redirect uri from the registered # application def initialize(client_id: required('client_id'), client_secret: required('client_secret'), redirect_uri: required('redirect_uri')) @client = ::OAuth2::Client.new(client_id, client_secret, site: 'https://app.asana.com', authorize_url: '/-/oauth_authorize', token_url: '/-/oauth_token') @redirect_uri = redirect_uri end # Public: # Returns the [String] OAuth2 authorize URL. def authorize_url @client.auth_code.authorize_url(redirect_uri: @redirect_uri) end # Public: Retrieves a token from an authorization code. # # Returns the [::OAuth2::AccessToken] token. def token_from_auth_code(auth_code) @client.auth_code.get_token(auth_code, redirect_uri: @redirect_uri) end # Public: Retrieves a token from a refresh token. # # Returns the refreshed [::OAuth2::AccessToken] token. def token_from_refresh_token(token) ::OAuth2::AccessToken.new(@client, '', refresh_token: token).refresh! end end end end end asana-0.4.0/lib/asana/authentication/oauth2/access_token_authentication.rb0000644000175600017570000000406712636335255026017 0ustar pravipravimodule Asana module Authentication module OAuth2 # Public: A mechanism to authenticate with an OAuth2 access token (a # bearer token and a refresh token) or just a refresh token. class AccessTokenAuthentication # Public: Builds an AccessTokenAuthentication from a refresh token and # client credentials, by refreshing into a new one. # # refresh_token - [String] a refresh token # client_id - [String] the client id of the registered Asana API # Application. # client_secret - [String] the client secret of the registered Asana API # Application. # redirect_uri - [String] the redirect uri of the registered Asana API # Application. # # Returns an [AccessTokenAuthentication] instance with a refreshed # access token. def self.from_refresh_token(refresh_token, client_id: required('client_id'), client_secret: required('client_secret'), redirect_uri: required('redirect_uri')) client = Client.new(client_id: client_id, client_secret: client_secret, redirect_uri: redirect_uri) new(client.token_from_refresh_token(refresh_token)) end # Public: Initializes a new AccessTokenAuthentication. # # access_token - [::OAuth2::AccessToken] An ::OAuth2::AccessToken # object. def initialize(access_token) @token = access_token end # Public: Configures a Faraday connection injecting a bearer token, # auto-refreshing it when needed. # # connection - [Faraday::Connection] the Faraday connection instance. # # Returns nothing. def configure(connection) @token = @token.refresh! if @token.expired? connection.request :oauth2, @token.token end end end end end asana-0.4.0/lib/asana/authentication/oauth2.rb0000644000175600017570000000341612636335255020254 0ustar pravipravirequire_relative 'oauth2/bearer_token_authentication' require_relative 'oauth2/access_token_authentication' require_relative 'oauth2/client' module Asana module Authentication # Public: Deals with OAuth2 authentication. Contains a function to get an # access token throught a browserless authentication flow, needed for some # applications such as CLI applications. module OAuth2 module_function # Public: Retrieves an access token through an offline authentication # flow. If your application can receive HTTP requests, you might want to # opt for a browser-based flow and use the omniauth-asana gem instead. # # Your registered application's redirect_uri should be exactly # "urn:ietf:wg:oauth:2.0:oob". # # client_id - [String] the client id of the registered Asana API # application. # client_secret - [String] the client secret of the registered Asana API # application. # # Returns an ::OAuth2::AccessToken object. # # Note: This function reads from STDIN and writes to STDOUT. It is meant # to be used only within the context of a CLI application. def offline_flow(client_id: required('client_id'), client_secret: required('client_secret')) client = Client.new(client_id: client_id, client_secret: client_secret, redirect_uri: 'urn:ietf:wg:oauth:2.0:oob') STDOUT.puts '1. Go to the following URL to authorize the ' \ " application: #{client.authorize_url}" STDOUT.puts '2. Paste the authorization code here: ' auth_code = STDIN.gets.chomp client.token_from_auth_code(auth_code) end end end end asana-0.4.0/.yardopts0000644000175600017570000000012412636335255013514 0ustar pravipravi--title "Asana API Ruby Client" --readme README.md --plugin tomdoc lib - LICENSE.txtasana-0.4.0/Guardfile0000644000175600017570000000527712636335255013511 0ustar pravipravi# A sample Guardfile # More info at https://github.com/guard/guard#readme ## Uncomment and set this to only include directories you want to watch # directories %w(app lib config test spec features) ## Uncomment to clear the screen before every task # clearing :on ## Guard internally checks for changes in the Guardfile and exits. ## If you want Guard to automatically start up again, run guard in a ## shell loop, e.g.: ## ## $ while bundle exec guard; do echo "Restarting Guard..."; done ## ## Note: if you are using the `directories` clause above and you are not ## watching the project directory ('.'), then you will want to move ## the Guardfile to a watched dir and symlink it back, e.g. # # $ mkdir config # $ mv Guardfile config/ # $ ln -s config/Guardfile . # # and, you'll have to watch "config/Guardfile" instead of "Guardfile" # Note: The cmd option is now required due to the increasing number of ways # rspec may be run, below are examples of the most common uses. # * bundler: 'bundle exec rspec' # * bundler binstubs: 'bin/rspec' # * spring: 'bin/rspec' (This will use spring if running and you have # installed the spring binstubs per the docs) # * zeus: 'zeus rspec' (requires the server to be started separately) # * 'just' rspec: 'rspec' guard :rspec, cmd: "bundle exec rspec" do require "guard/rspec/dsl" dsl = Guard::RSpec::Dsl.new(self) # Feel free to open issues for suggestions and improvements # RSpec files rspec = dsl.rspec watch(rspec.spec_helper) { rspec.spec_dir } watch(rspec.spec_support) { rspec.spec_dir } watch(rspec.spec_files) watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } # Rails files rails = dsl.rails(view_extensions: %w(erb haml slim)) dsl.watch_spec_files_for(rails.app_files) dsl.watch_spec_files_for(rails.views) watch(rails.controllers) do |m| [ rspec.spec.("routing/#{m[1]}_routing"), rspec.spec.("controllers/#{m[1]}_controller"), rspec.spec.("acceptance/#{m[1]}") ] end # Rails config changes watch(rails.spec_helper) { rspec.spec_dir } watch(rails.routes) { "#{rspec.spec_dir}/routing" } watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" } # Capybara features specs watch(rails.view_dirs) { |m| rspec.spec.("features/#{m[1]}") } # Turnip features and steps watch(%r{^spec/acceptance/(.+)\.feature$}) watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m| Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance" end end guard :rubocop do watch(%r{.+\.rb$}) watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) } end guard 'yard' do watch(%r{app/.+\.rb}) watch(%r{lib/.+\.rb}) watch(%r{ext/.+\.c}) end asana-0.4.0/.rspec0000644000175600017570000000007512636335255012770 0ustar pravipravi--color --require spec_helper --require asana --order random asana-0.4.0/package.json0000644000175600017570000000015012636335255014133 0ustar pravipravi{ "devDependencies": { "inflect": "^0.3.0", "js-yaml": "^3.2.5", "lodash": "^2.4.1" } } asana-0.4.0/.gitignore0000644000175600017570000000017212636335255013641 0ustar pravipravi.idea /.bundle/ /.yardoc /Gemfile.lock /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ /bin/ test.rb /node_modules/ asana-0.4.0/.rubocop.yml0000644000175600017570000000037112636335255014124 0ustar pravipraviAllCops: Include: - '**/Rakefile' Exclude: - 'bin/**/*' - 'examples/**/*' - 'lib/asana/resources/*' - 'spec/templates/unicorn.rb' - 'spec/templates/world.rb' - 'test.rb' LineLength: Max: 120 require: rubocop-rspec asana-0.4.0/examples/0000755000175600017570000000000012636335255013467 5ustar pravipraviasana-0.4.0/examples/personal_access_token.rb0000644000175600017570000000107412636335255020362 0ustar pravipravirequire 'bundler' Bundler.require require 'asana' access_token = ENV['ASANA_ACCESS_TOKEN'] unless access_token abort "Run this program with the env var ASANA_ACCESS_TOKEN.\n" \ "Go to http://app.asana.com/-/account_api to create a personal access token." end client = Asana::Client.new do |c| c.authentication :access_token, access_token end puts "My Workspaces:" client.workspaces.find_all.each do |workspace| puts "\t* #{workspace.name} - tags:" client.tags.find_by_workspace(workspace: workspace.id).each do |tag| puts "\t\t- #{tag.name}" end end asana-0.4.0/examples/Gemfile0000644000175600017570000000015212636335255014760 0ustar pravipravisource 'https://rubygems.org' gem 'omniauth' gem 'omniauth-asana' gem 'sinatra' gem 'asana', path: '../' asana-0.4.0/examples/Gemfile.lock0000644000175600017570000000221312636335255015707 0ustar pravipraviPATH remote: ../ specs: asana (0.1.1) faraday (~> 0.9) faraday_middleware (~> 0.9) faraday_middleware-multi_json (~> 0.0) oauth2 (~> 1.0) GEM remote: https://rubygems.org/ specs: faraday (0.9.1) multipart-post (>= 1.2, < 3) faraday_middleware (0.9.1) faraday (>= 0.7.4, < 0.10) faraday_middleware-multi_json (0.0.6) faraday_middleware multi_json hashie (3.4.1) jwt (1.5.0) multi_json (1.11.0) multi_xml (0.5.5) multipart-post (2.0.0) oauth2 (1.0.0) faraday (>= 0.8, < 0.10) jwt (~> 1.0) multi_json (~> 1.3) multi_xml (~> 0.5) rack (~> 1.2) omniauth (1.2.2) hashie (>= 1.2, < 4) rack (~> 1.0) omniauth-asana (0.0.1) omniauth (~> 1.0) omniauth-oauth2 (~> 1.1) omniauth-oauth2 (1.3.0) oauth2 (~> 1.0) omniauth (~> 1.2) rack (1.6.1) rack-protection (1.5.3) rack sinatra (1.4.6) rack (~> 1.4) rack-protection (~> 1.4) tilt (>= 1.3, < 3) tilt (2.0.1) PLATFORMS ruby DEPENDENCIES asana! omniauth omniauth-asana sinatra BUNDLED WITH 1.10.3 asana-0.4.0/examples/omniauth_integration.rb0000644000175600017570000000251612636335255020247 0ustar pravipravirequire 'bundler' Bundler.require require 'asana' class SinatraApp < Sinatra::Base id, secret = ENV['ASANA_CLIENT_ID'], ENV['ASANA_CLIENT_SECRET'] unless id && secret abort "Run this program with the env vars ASANA_CLIENT_ID and ASANA_CLIENT_SECRET.\n" \ "Refer to https://asana.com/developers/documentation/getting-started/authentication "\ "to get your credentials." end use OmniAuth::Strategies::Asana, id, secret enable :sessions get '/' do if $client 'My Workspaces' else 'sign in to asana' end end get '/workspaces' do if $client "

My Workspaces

" \ "
    " + $client.workspaces.find_all.map { |w| "
  • #{w.name}
  • " }.join + "
" else redirect '/sign_in' end end get '/auth/:name/callback' do creds = request.env["omniauth.auth"]["credentials"].tap { |h| h.delete('expires') } strategy = request.env["omniauth.strategy"] access_token = OAuth2::AccessToken.from_hash(strategy.client, creds).refresh! $client = Asana::Client.new do |c| c.authentication :oauth2, access_token end redirect '/workspaces' end get '/sign_in' do redirect '/auth/asana' end get '/sign_out' do $client = nil redirect '/' end end SinatraApp.run! if __FILE__ == $0 asana-0.4.0/examples/events.rb0000644000175600017570000000214112636335255015316 0ustar pravipravi# -*- coding: utf-8 -*- require 'bundler' Bundler.require require 'asana' access_token = ENV['ASANA_ACCESS_TOKEN'] unless access_token abort "Run this program with the env var ASANA_ACCESS_TOKEN.\n" \ "Go to http://app.asana.com/-/account_api to create a personal access token." end client = Asana::Client.new do |c| c.authentication :access_token, access_token end workspace = client.workspaces.find_all.first task = client.tasks.find_all(assignee: "me", workspace: workspace.id).first unless task task = client.tasks.create(workspace: workspace.id, name: "Hello world!", assignee: "me") end Thread.abort_on_exception = true Thread.new do puts "Listening for 'changed' events on #{task} in one thread..." task.events(wait: 2).lazy.select { |event| event.action == 'changed' }.each do |event| puts "#{event.user.name} changed #{event.resource}" end end Thread.new do puts "Listening for non-'changed' events on #{task} in another thread..." task.events(wait: 1).lazy.reject { |event| event.action == 'changed' }.each do |event| puts "'#{event.action}' event: #{event}" end end sleep asana-0.4.0/examples/cli_app.rb0000644000175600017570000000157412636335255015432 0ustar pravipravirequire 'bundler' Bundler.require require 'asana' id, secret = ENV['ASANA_CLIENT_ID'], ENV['ASANA_CLIENT_SECRET'] unless id && secret abort "Run this program with the env vars ASANA_CLIENT_ID and ASANA_CLIENT_SECRET.\n" \ "Refer to https://asana.com/developers/documentation/getting-started/authentication "\ "to get your credentials." \ "The redirect URI for your application should be \"urn:ietf:wg:oauth:2.0:oob\"." end access_token = Asana::Authentication::OAuth2.offline_flow(client_id: id, client_secret: secret) client = Asana::Client.new do |c| c.authentication :oauth2, access_token end puts "My Workspaces:" client.workspaces.find_all.each do |workspace| puts "\t* #{workspace.name} - tags:" client.tags.find_by_workspace(workspace: workspace.id).each do |tag| puts "\t\t- #{tag.name}" end end asana-0.4.0/Rakefile0000644000175600017570000000300112636335255013310 0ustar pravipravirequire 'rspec/core/rake_task' require 'rubocop/rake_task' require 'yard' RSpec::Core::RakeTask.new(:spec) RuboCop::RakeTask.new YARD::Rake::YardocTask.new do |t| t.stats_options = ['--list-undoc'] end desc 'Generates a test resource from a YAML using the resource template.' task :codegen do `node spec/support/codegen.js` end namespace :bump do def read_version File.readlines('./lib/asana/version.rb') .detect { |l| l =~ /VERSION/ } .scan(/VERSION = '([^']+)/).flatten.first.split('.') .map { |n| Integer(n) } end # rubocop:disable Metrics/MethodLength def write_version(major, minor, patch) str = <<-EOS #:nodoc: module Asana # Public: Version of the gem. VERSION = '#{major}.#{minor}.#{patch}' end EOS File.open('./lib/asana/version.rb', 'w') do |f| f.write str end new_version = "#{major}.#{minor}.#{patch}" system('git add lib/asana/version.rb') system(%(git commit -m "Bumped to #{new_version}" && ) + %(git tag -a v#{new_version} -m "Version #{new_version}")) puts "\nRun git push --tags to release." end desc 'Bumps a patch version' task :patch do major, minor, patch = read_version write_version major, minor, patch + 1 end desc 'Bumps a minor version' task :minor do major, minor, = read_version write_version major, minor + 1, 0 end desc 'Bumps a major version' task :major do major, = read_version write_version major + 1, 0, 0 end end task default: [:codegen, :spec, :rubocop, :yard] asana-0.4.0/README.md0000644000175600017570000002514212636335255013134 0ustar pravipravi# Asana [![Gem Version](https://badge.fury.io/rb/asana.svg)](http://badge.fury.io/rb/asana) [![Build Status](https://travis-ci.org/Asana/ruby-asana.svg?branch=master)](https://travis-ci.org/Asana/ruby-asana) [![Code Climate](https://codeclimate.com/github/Asana/ruby-asana/badges/gpa.svg)](https://codeclimate.com/github/Asana/ruby-asana) [![Dependency Status](https://gemnasium.com/Asana/ruby-asana.svg)](https://gemnasium.com/Asana/ruby-asana) A Ruby client for the 1.0 version of the Asana API. Supported rubies: * MRI 2.0.0 up to 2.2.x stable ## Installation Add this line to your application's Gemfile: ```ruby gem 'asana' ``` And then execute: $ bundle Or install it yourself as: $ gem install asana ## Usage To do anything, you'll need always an instance of `Asana::Client` configured with your preferred authentication method (see the Authentication section below for more complex scenarios) and other options. The most minimal example would be as follows: ```ruby require 'asana' client = Asana::Client.new do |c| c.authentication :access_token, 'personal_access_token' end client.workspaces.find_all.first ``` A full-blown customized client using OAuth2 wih a previously obtained refresh token, Typhoeus as a Faraday adapter, a custom user agent and custom Faraday middleware: ```ruby require 'asana' client = Asana::Client.new do |c| c.authentication :oauth2, refresh_token: 'abc', client_id: 'bcd', client_secret: 'cde', redirect_uri: 'http://example.org/auth' c.faraday_adapter :typhoeus c.configure_faraday { |conn| conn.use SomeFaradayMiddleware } end workspace = client.workspaces.find_by_id(12) workspace.users # => # ...> client.tags.create_in_workspace(workspace: workspace.id, name: 'foo') # => # ``` All resources are exposed as methods on the `Asana::Client` instance. Check out the [documentation for each of them][docs]. ### Authentication This gem supports authenticating against the Asana API with either an API token or through OAuth2. #### Personal Access Token ```ruby Asana::Client.new do |c| c.authentication :access_token, 'personal_access_token' end ``` #### OAuth2 Authenticating through OAuth2 is preferred. There are many ways you can do this. ##### With a plain bearer token (doesn't support auto-refresh) If you have a plain bearer token obtained somewhere else and you don't mind not having your token auto-refresh, you can authenticate with it as follows: ```ruby Asana::Client.new do |c| c.authentication :oauth2, bearer_token: 'my_bearer_token' end ``` ##### With a refresh token and client credentials If you obtained a refresh token, you can use it together with your client credentials to authenticate: ```ruby Asana::Client.new do |c| c.authentication :oauth2, refresh_token: 'abc', client_id: 'bcd', client_secret: 'cde', redirect_uri: 'http://example.org/auth' end ``` ##### With an ::OAuth2::AccessToken object (from `omniauth-asana` for example) If you use `omniauth-asana` or a browser-based OAuth2 authentication strategy in general, possibly because your application is a web application, you can reuse those credentials to authenticate with this API client. Here's how to do it from the callback method: ```ruby # assuming we're using Sinatra and omniauth-asana get '/auth/:name/callback' do creds = request.env["omniauth.auth"]["credentials"].tap { |h| h.delete('expires') } strategy = request.env["omniauth.strategy"] # We need to refresh the omniauth OAuth2 token access_token = OAuth2::AccessToken.from_hash(strategy.client, creds).refresh! $client = Asana::Client.new do |c| c.authentication :oauth2, access_token end redirect '/' end ``` See `examples/omniauth_integration.rb` for a working example of this. ##### Using an OAuth2 offline authentication flow (for CLI applications) If your application can't receive HTTP requests and thus you can't use `omniauth-asana`, for example if it's a CLI application, you can authenticate as follows: ```ruby access_token = Asana::Authentication::OAuth2.offline_flow(client_id: ..., client_secret: ...) client = Asana::Client.new do |c| c.authentication :oauth2, access_token end client.tasks.find_by_id(12) ``` This will print an authorization URL on STDOUT, and block until you paste in the authorization code, which you can get by visiting that URL and granting the necessary permissions. ### Pagination Whenever you ask for a collection of resources, you can provide a number of results per page to fetch, between 1 and 100. If you don't provide any, it defaults to 20. ```ruby my_tasks = client.tasks.find_by_tag(tag: tag_id, per_page: 5) # => # ...> ``` An `Asana::Collection` is a paginated collection -- it holds the first `per_page` results, and a reference to the next page if any. When you iterate an `Asana::Collection`, it'll transparently keep fetching all the pages, and caching them along the way: ```ruby my_tasks.size # => 23, not 5 my_tasks.take(14) # => [#, #, ... until 14] ``` #### Manual pagination If you only want to deal with one page at a time and manually paginate, you can get the elements of the current page with `#elements` and ask for the next page with `#next_page`, which will return an `Asana::Collection` with the next page of elements: ```ruby my_tasks.elements # => [#, #, ... until 5] my_tasks.next_page # => # ``` #### Lazy pagination Because an `Asana::Collection` represents the entire collection, it is often handy to just take what you need from it, rather than let it fetch all its contents from the network. You can accomplish this by turning it into a lazy collection with `#lazy`: ```ruby # let my_tasks be an Asana::Collection of 10 pages of 100 elements each my_tasks.lazy.drop(120).take(15).to_a # Fetches only 2 pages, enough to get elements 120 to 135 # => [#, #, ...] ``` ### Error handling In any request against the Asana API, there a number of errors that could arise. Those are well documented in the [Asana API Documentation][apidocs], and are represented as exceptions under the namespace `Asana::Errors`. All errors are subclasses of `Asana::Errors::APIError`, so make sure to rescue instances of this class if you want to handle them yourself. ### I/O options All requests (except `DELETE`) accept extra I/O options [as documented in the API docs][io]. Just pass an extra `options` hash to any request: ```ruby client.tasks.find_by_id(12, options: { expand: ['workspace'] }) ``` ### Attachment uploading To attach a file to a task or a project, you just need its absolute path on your filesystem and its MIME type, and the file will be uploaded for you: ```ruby task = client.tasks.find_by_id(12) attachment = task.attach(filename: '/absolute/path/to/my/file.png', mime: 'image/png') attachment.name # => 'file.png' ``` ### Event streams To subscribe to an event stream of a task or a project, just call `#events` on it: ```ruby task = client.tasks.find_by_id(12) task.events # => # # You can do the same with only the task id: events = client.events.for(task.id) ``` An `Asana::Events` object is an infinite collection of `Asana::Event` instances. Be warned that if you call `#each` on it, it will block forever! Note that, by default, an event stream will wait at least 1 second between polls, but that's configurable with the `wait` parameter: ```ruby # wait at least 3 and a half seconds between each poll to the API task.events(wait: 3.5) # => # ``` There are some interesting things you can do with an event stream, as it is a normal Ruby Enumerable. Read below to get some ideas. #### Subscribe to the event stream with a callback, polling every 2 seconds ```ruby # Run this in another thread so that we don't block forever events = client.tasks.find_by_id(12).events(wait: 2) Thread.new do events.each do |event| notify_someone "New event arrived! #{event}" end end ``` #### Make the stream lazy and filter it by a specific pattern To do that we need to call `#lazy` on the `Events` instance, just like with any other `Enumerable`. ```ruby events = client.tasks.find_by_id(12).events only_change_events = events.lazy.select { |event| event.action == 'changed' } Thread.new do only_change_events.each do |event| notify_someone "New change event arrived! #{event}" end end ``` ## Development You'll need Ruby 2.1+ and Node v0.10.26+ / NPM 1.4.3+ installed. After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment. Run the build with `rake`. This is equivalent to: $ rake spec && rake rubocop && rake yard To install this gem onto your local machine, run `bundle exec rake install`. ## Releasing a new version To release a new version, run either of these commands: rake bump:patch rake bump:minor rake bump:major This will: update `lib/asana/version.rb`, commit and tag the commit. Then you just need to `push --tags` to let Travis build and release the new version to Rubygems: git push --tags ### Code generation The specific Asana resource classes (`Tag`, `Workspace`, `Task`, etc) are generated code, hence they shouldn't be modified by hand. The code that generates it lives in `lib/templates/resource.ejs`, and is tested by generating `spec/templates/unicorn.rb` and running `spec/templates/unicorn_spec.rb` as part of the build. If you wish to make changes on the code generation script: 1. Add/modify a spec on `spec/templates/unicorn_spec.rb` 2. Add your new feature or change to `lib/templates/resource.ejs` 3. Run `rake` or, more granularly, `rake codegen && rspec spec/templates/unicorn_spec.rb` Once you're sure your code works, submit a pull request and ask the maintainer to make a release, as they'll need to run a release script from the [asana-api-meta][meta] repository. ## Contributing 1. Fork it ( https://github.com/[my-github-username]/asana/fork ) 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create a new Pull Request [apidocs]: https://asana.com/developers [io]: https://asana.com/developers/documentation/getting-started/input-output-options [docs]: http://www.rubydoc.info/github/Asana/ruby-asana/master [meta]: https://github.com/asana/asana-api-meta asana-0.4.0/.travis.yml0000644000175600017570000000150212636335255013760 0ustar pravipravilanguage: ruby rvm: - 2.2.2 - 2.0.0 deploy: provider: rubygems api_key: secure: ZFzExKt6auBOcQyg8955GwlZW2OaS64Q+XHGSay2MzjALw1iiy5uuMdwkueCKrGWSv6/eBe9jjsmYBe3OfkUIpIBbUacnbEYeaC70AyucNjvFrrl0YVYHb7neojarJUmKz9bz9Pkju/jdxksaYaj58xfq5YPQDfjFtdmylvuNEYpujT6goPEbxG4U4PpIhhQOZRDRXXAPS+f7jHejTSK06kvJjiJw0d51VJtBbp+0TKNKL6BDKdOKjKeHuebuUmSw8crDyaYdnwYwmNg1cJrGOv2t76M08zoKkkIO2lwPMHisi1/+cbVcZfxM4SfdHJeU6cQuRdb0uCUbbj6GsGwT8vWP2mGUrLe4UV/GfZDmvK3MKeKIlkgig31a3Qny9yjn8EjSnKHYuHBbJvPQDPPpFUfgEneUxn2t4P6m+epkd1gldWqTWf8mhMR/6xAFT4s+BaxnMMJsTC3Ea+dZZ30EqCw/kx5B2Z1KVLgsxHeMN/Q+AeOcbOvlGDsFL0Mjk/PqDTW1AWKLs/D1ohcxjSmlNJGWR6JHa/Ei0GqjDE2+/ZGsKsRfcDD4kU5qnKdqdzDlbL3cL4tChzuWVcguYdrg1yZzqPrCPzmy+2D7Hphyaj9CPKEh7qwT+IQU5o/V2peOJUjKrMlJS4gFq6MvTDh5U59J88Kkg72DXhcEUcySkU= gem: asana on: tags: true repo: Asana/ruby-asana asana-0.4.0/metadata.yml0000644000175600017570000001177512636335256014170 0ustar pravipravi--- !ruby/object:Gem::Specification name: asana version: !ruby/object:Gem::Version version: 0.4.0 platform: ruby authors: - Txus autorequire: bindir: exe cert_chain: [] date: 2015-10-20 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: oauth2 requirement: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: '1.0' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: '1.0' - !ruby/object:Gem::Dependency name: faraday requirement: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: '0.9' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: '0.9' - !ruby/object:Gem::Dependency name: faraday_middleware requirement: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: '0.9' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: '0.9' - !ruby/object:Gem::Dependency name: faraday_middleware-multi_json requirement: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: '0.0' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: '0.0' - !ruby/object:Gem::Dependency name: bundler requirement: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: '1.7' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: '1.7' - !ruby/object:Gem::Dependency name: rake requirement: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: '10.0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: '10.0' - !ruby/object:Gem::Dependency name: rspec requirement: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: '3.2' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: '3.2' description: Official Ruby client for the Asana API email: - me@txus.io executables: [] extensions: [] extra_rdoc_files: [] files: - .codeclimate.yml - .gitignore - .rspec - .rubocop.yml - .travis.yml - .yardopts - CODE_OF_CONDUCT.md - Gemfile - Guardfile - LICENSE.txt - README.md - Rakefile - asana.gemspec - bin/console - bin/setup - examples/Gemfile - examples/Gemfile.lock - examples/cli_app.rb - examples/events.rb - examples/omniauth_integration.rb - examples/personal_access_token.rb - lib/asana.rb - lib/asana/authentication.rb - lib/asana/authentication/oauth2.rb - lib/asana/authentication/oauth2/access_token_authentication.rb - lib/asana/authentication/oauth2/bearer_token_authentication.rb - lib/asana/authentication/oauth2/client.rb - lib/asana/authentication/token_authentication.rb - lib/asana/client.rb - lib/asana/client/configuration.rb - lib/asana/errors.rb - lib/asana/http_client.rb - lib/asana/http_client/environment_info.rb - lib/asana/http_client/error_handling.rb - lib/asana/http_client/response.rb - lib/asana/resource_includes/attachment_uploading.rb - lib/asana/resource_includes/collection.rb - lib/asana/resource_includes/event.rb - lib/asana/resource_includes/event_subscription.rb - lib/asana/resource_includes/events.rb - lib/asana/resource_includes/registry.rb - lib/asana/resource_includes/resource.rb - lib/asana/resource_includes/response_helper.rb - lib/asana/resources.rb - lib/asana/resources/attachment.rb - lib/asana/resources/project.rb - lib/asana/resources/story.rb - lib/asana/resources/tag.rb - lib/asana/resources/task.rb - lib/asana/resources/team.rb - lib/asana/resources/user.rb - lib/asana/resources/workspace.rb - lib/asana/ruby2_0_0_compatibility.rb - lib/asana/version.rb - lib/templates/index.js - lib/templates/resource.ejs - package.json homepage: https://github.com/asana/ruby-asana licenses: - MIT metadata: {} post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: '2.0' required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: rubygems_version: 2.4.5 signing_key: specification_version: 4 summary: Official Ruby client for the Asana API test_files: []