From 55a50cd38d3f2a57f0fec2e674253a2e6628a467 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 15 Oct 2026 14:38:51 -0400 Subject: [PATCH 01/10] Add RBS for asana 0.10 Cover the asana client API as applications use it: client configuration, the resource proxies returned by Client#tasks, #projects and the rest, the resource classes and their attributes, paginated collections, and the API error classes. The proxies are method_missing forwarders to each resource class's singleton methods with the HTTP client prepended, so each one is an interface under Asana::Client declaring those methods without the client argument. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01HSbQkrSvmSooewdXEYTCBM --- gems/asana/.rubocop.yml | 19 ++ gems/asana/0.10/_test/metadata.yaml | 4 + gems/asana/0.10/_test/test.rb | 67 ++++ gems/asana/0.10/asana.rbs | 508 ++++++++++++++++++++++++++++ 4 files changed, 598 insertions(+) create mode 100644 gems/asana/.rubocop.yml create mode 100644 gems/asana/0.10/_test/metadata.yaml create mode 100644 gems/asana/0.10/_test/test.rb create mode 100644 gems/asana/0.10/asana.rbs diff --git a/gems/asana/.rubocop.yml b/gems/asana/.rubocop.yml new file mode 100644 index 000000000..7d0be8ff2 --- /dev/null +++ b/gems/asana/.rubocop.yml @@ -0,0 +1,19 @@ +# This configuration inherits from /.rubocop.yml. +# You can configure RBS style of this gem. +# This file is used on CI. It is configured to automatically +# make rubocop suggestions on pull requests for this gem. +# If you do not like the style enforcement, you should remove this file. +inherit_from: ../../.rubocop.yml + +## +# If you want to customize the style, please consult with the gem reviewers. +# You can see the list of cops at https://github.com/ksss/rubocop-on-rbs/blob/main/docs/modules/ROOT/pages/cops.adoc + +RBS/Layout: + Enabled: true + +RBS/Lint: + Enabled: true + +RBS/Style: + Enabled: true diff --git a/gems/asana/0.10/_test/metadata.yaml b/gems/asana/0.10/_test/metadata.yaml new file mode 100644 index 000000000..29015bc02 --- /dev/null +++ b/gems/asana/0.10/_test/metadata.yaml @@ -0,0 +1,4 @@ +# If the test needs gem's RBS files, declare them here. +# +# additional_gems: +# - GEM_NAME diff --git a/gems/asana/0.10/_test/test.rb b/gems/asana/0.10/_test/test.rb new file mode 100644 index 000000000..1da12a3b1 --- /dev/null +++ b/gems/asana/0.10/_test/test.rb @@ -0,0 +1,67 @@ +require "asana" + +client = Asana::Client.new do |c| + c.authentication :access_token, "personal-access-token" + c.default_headers "asana-enable" => "new_user_task_lists" +end + +workspace = client.workspaces.find_all.first +workspace_gid = workspace&.gid || "1" +workspace&.name&.upcase + +me = client.users.me +me.to_h["gid"] + +project = client.projects.find_by_id("2", options: { fields: ["name", "due_date"] }) +project.due_date&.length +client.projects.find_all(workspace: workspace_gid, archived: false).map(&:gid) + +sections = client.sections.get_sections_for_project(project_gid: project.gid) +sections.each do |section| + section.name&.strip + client.tasks.get_tasks(section: section.gid, options: { fields: ["name", "completed"] }).each do |task| + task.completed + end +end + +task = client.tasks.find_by_id("3") +task.name&.strip +task.assignee&.fetch("gid") +task.memberships&.each { |membership| membership["section"] } +task.custom_fields&.size +task.subtasks(per_page: 50).each { |subtask| subtask.gid } +task.dependencies.to_a.size +task.stories.each { |story| story.created_at } +task.update(completed: true).completed +task.attach(filename: "notes.txt", mime: "text/plain", io: StringIO.new("hello")).name + +new_task = Asana::Resources::Task.create(client, workspace: workspace_gid, name: "Write RBS") +client.tasks.update_task(task_gid: new_task.gid, name: "Write more RBS") +client.tasks.add_project_for_task(task_gid: new_task.gid, project: project.gid) +client.sections.add_task_for_section(section_gid: "4", task: new_task.gid) +client.tasks.get_subtasks_for_task(task_gid: new_task.gid).first&.gid +client.tasks.search_tasks_for_workspace(workspace_gid: workspace_gid, text: "RBS", completed: false).to_a + +tags = client.tags.get_tags_for_workspace(workspace_gid: workspace_gid) +tags.select { |tag| tag.name == "urgent" }.each { |tag| tag.get_tasks_with_tag.size } + +list = client.user_task_lists.get_user_task_list_for_user(user_gid: "me", workspace: workspace_gid) +client.tasks.find_all(user_task_list: list.gid, completed_since: "now").next_page + +portfolio = client.portfolios.find_by_id("5") +client.portfolios.get_items_for_portfolio(portfolio_gid: portfolio.gid).map(&:name) + +client.custom_fields.get_custom_fields_for_workspace(workspace_gid: workspace_gid).map(&:type) +client.attachments.find_by_task(task: new_task.gid).each { |attachment| attachment.view_url } +client.webhooks.get_all(workspace: workspace_gid).each { |webhook| webhook.active } + +response = client.get("/users/me") +response.body["data"] + +begin + client.tasks.find_by_id("missing") +rescue Asana::Errors::NotFound => e + e.response&.status +rescue Asana::Errors::RateLimitEnforced => e + e.retry_after_seconds + 1 +end diff --git a/gems/asana/0.10/asana.rbs b/gems/asana/0.10/asana.rbs new file mode 100644 index 000000000..64e61f13f --- /dev/null +++ b/gems/asana/0.10/asana.rbs @@ -0,0 +1,508 @@ +module Asana + VERSION: String + + # Asana::Client#tasks, #projects, etc. are defined with define_method over + # Resources::Registry and return a ResourceProxy, which forwards every call + # to the resource class's singleton method with the HTTP client prepended. + # Each interface below is that resource's singleton API minus the leading + # client argument. + class Client + class Configuration + def initialize: () -> void + def authentication: (:access_token, String value) -> void + | (:oauth2, untyped value) -> void + def faraday_adapter: (Symbol adapter) -> void + def configure_faraday: () { (untyped conn) -> void } -> void + def debug_mode: () -> void + def log_asana_change_warnings: (boolish value) -> void + def default_headers: (Hash[String, String] value) -> void + def to_h: () -> Hash[Symbol, untyped] + end + + class ResourceProxy + def initialize: (client: HttpClient, resource: singleton(Resources::Resource)) -> void + end + + interface _Attachments + def find_by_id: (String id, ?options: Resources::options) -> Resources::Attachment + def find_by_task: (task: String, ?per_page: Integer, ?options: Resources::options) -> Resources::Collection[Resources::Attachment] + end + + interface _CustomFields + def find_by_id: (String id, ?options: Resources::options) -> Resources::CustomField + def find_by_workspace: (workspace: String, ?per_page: Integer, ?options: Resources::options) -> Resources::Collection[Resources::Resource] + def get_custom_fields_for_workspace: (workspace_gid: String, ?options: Resources::options) -> Resources::Collection[Resources::CustomField] + end + + interface _Portfolios + def find_by_id: (String id, ?options: Resources::options) -> Resources::Portfolio + def find_all: (workspace: String, owner: String, ?per_page: Integer, ?options: Resources::options) -> Resources::Collection[Resources::Portfolio] + def get_items_for_portfolio: (portfolio_gid: String, ?options: Resources::options) -> Resources::Collection[Resources::Project] + end + + interface _Projects + def create: (workspace: String, ?team: String?, ?options: Resources::options, **untyped data) -> Resources::Project + def find_by_id: (String id, ?options: Resources::options) -> Resources::Project + def find_all: (?workspace: String?, ?team: String?, ?is_template: bool?, ?archived: bool?, ?per_page: Integer, ?options: Resources::options) -> Resources::Collection[Resources::Project] + def find_by_workspace: (workspace: String, ?is_template: bool?, ?archived: bool?, ?per_page: Integer, ?options: Resources::options) -> Resources::Collection[Resources::Project] + end + + interface _Sections + def find_by_id: (String id, ?options: Resources::options) -> Resources::Section + def get_sections_for_project: (project_gid: String, ?options: Resources::options) -> Resources::Collection[Resources::Section] + def add_task_for_section: (section_gid: String, ?options: Resources::options, **untyped data) -> untyped + end + + interface _Stories + def find_by_id: (String id, ?options: Resources::options) -> Resources::Story + def find_by_task: (task: String, ?per_page: Integer, ?options: Resources::options) -> Resources::Collection[Resources::Story] + end + + interface _Tags + def create: (workspace: String, ?options: Resources::options, **untyped data) -> Resources::Tag + def find_by_id: (String id, ?options: Resources::options) -> Resources::Tag + def find_all: (?workspace: String?, ?team: String?, ?archived: bool?, ?per_page: Integer, ?options: Resources::options) -> Resources::Collection[Resources::Tag] + def find_by_workspace: (workspace: String, ?per_page: Integer, ?options: Resources::options) -> Resources::Collection[Resources::Tag] + def get_tags_for_workspace: (workspace_gid: String, ?options: Resources::options) -> Resources::Collection[Resources::Tag] + end + + interface _Tasks + def create: (?workspace: String?, ?options: Resources::options, **untyped data) -> Resources::Task + def find_by_id: (String id, ?options: Resources::options) -> Resources::Task + def find_all: (?assignee: String?, ?workspace: String?, ?project: String?, ?section: String?, ?tag: String?, ?user_task_list: String?, ?completed_since: String?, ?modified_since: String?, ?per_page: Integer, ?options: Resources::options) -> Resources::Collection[Resources::Task] + def get_task: (task_gid: String, ?options: Resources::options) -> Resources::Task + def get_tasks: (?assignee: String?, ?project: String?, ?section: String?, ?workspace: String?, ?completed_since: String?, ?modified_since: String?, ?options: Resources::options) -> Resources::Collection[Resources::Task] + def get_subtasks_for_task: (task_gid: String, ?options: Resources::options) -> Resources::Collection[Resources::Task] + def update_task: (task_gid: String, ?options: Resources::options, **untyped data) -> Resources::Task + def add_project_for_task: (task_gid: String, ?options: Resources::options, **untyped data) -> untyped + def search_tasks_for_workspace: (workspace_gid: String, ?options: Resources::options, **untyped filters) -> Resources::Collection[Resources::Task] + end + + interface _UserTaskLists + def find_by_id: (String id, ?options: Resources::options) -> Resources::UserTaskList + def get_user_task_list_for_user: (user_gid: String, ?workspace: String?, ?options: Resources::options) -> Resources::UserTaskList + end + + interface _Users + def me: (?options: Resources::options) -> Resources::Resource + def find_by_id: (String id, ?options: Resources::options) -> Resources::User + def find_all: (?workspace: String?, ?per_page: Integer, ?options: Resources::options) -> Resources::Collection[Resources::User] + def find_by_workspace: (workspace: String, ?per_page: Integer, ?options: Resources::options) -> Resources::Collection[Resources::User] + end + + interface _Webhooks + def create: (resource: String, target: String, ?options: Resources::options, **untyped data) -> Resources::Webhook + def create_webhook: (?options: Resources::options, **untyped data) -> Resources::Webhook + def get_all: (workspace: String, ?resource: String?, ?per_page: Integer, ?options: Resources::options) -> Resources::Collection[Resources::Webhook] + end + + interface _Workspaces + def find_by_id: (String id, ?options: Resources::options) -> Resources::Workspace + def find_all: (?per_page: Integer, ?options: Resources::options) -> Resources::Collection[Resources::Workspace] + end + + def initialize: () { (Configuration config) -> void } -> void + + def get: (String url, **untyped args) -> HttpClient::Response + def post: (String url, **untyped args) -> HttpClient::Response + def put: (String url, **untyped args) -> HttpClient::Response + def delete: (String url, **untyped args) -> HttpClient::Response + + def attachments: () -> _Attachments + def custom_fields: () -> _CustomFields + def portfolios: () -> _Portfolios + def projects: () -> _Projects + def sections: () -> _Sections + def stories: () -> _Stories + def tags: () -> _Tags + def tasks: () -> _Tasks + def user_task_lists: () -> _UserTaskLists + def users: () -> _Users + def webhooks: () -> _Webhooks + def workspaces: () -> _Workspaces + end + + class HttpClient + class Response + attr_reader faraday_env: untyped + attr_reader status: Integer + attr_reader body: Hash[String, untyped] + attr_reader headers: Hash[String, String] + end + + def get: (String resource_uri, ?params: Hash[Symbol, untyped], ?options: Resources::options) -> Response + def put: (String resource_uri, ?body: Hash[Symbol, untyped], ?options: Resources::options) -> Response + def post: (String resource_uri, ?body: Hash[Symbol, untyped], ?upload: untyped, ?options: Resources::options) -> Response + def delete: (String resource_uri, ?params: Hash[Symbol, untyped], ?options: Resources::options) -> Response + end + + module Errors + class APIError < StandardError + attr_accessor response: HttpClient::Response? + end + + class NotAuthorized < APIError + end + + class PremiumOnly < APIError + end + + class Forbidden < APIError + end + + class NotFound < APIError + end + + class ServerError < APIError + attr_accessor phrase: String + def initialize: (String phrase) -> void + end + + class InvalidRequest < APIError + attr_accessor errors: Array[String] + def initialize: (Array[String] errors) -> void + end + + class RateLimitEnforced < APIError + attr_accessor retry_after_seconds: Integer + def initialize: (Integer retry_after_seconds) -> void + end + end + + module Resources + # The request I/O options every endpoint accepts, e.g. + # { fields: ['name'], expand: ['projects'], pretty: true }. + type options = Hash[Symbol, untyped] + + # Resource class methods take the client they send requests through: + # an Asana::HttpClient when reached through Asana::Client's proxies, or an + # Asana::Client when called directly (it answers the same verbs). + type client = Asana::Client | Asana::HttpClient + + module ResponseHelper + def parse: (HttpClient::Response response) -> [untyped, Hash[String, untyped]] + end + + class Resource + include ResponseHelper + extend ResponseHelper + + def initialize: (Hash[String, untyped] data, client: client) -> void + def refresh: () -> self + def to_h: () -> Hash[String, untyped] + + private + + def client: () -> client + def refresh_with: (Hash[String, untyped] data) -> self + end + + class Collection[T] + include Enumerable[T] + include ResponseHelper + + attr_reader elements: Array[T] + + def initialize: ([Array[Hash[String, untyped]], Hash[String, untyped]] response, ?type: singleton(Resource), client: client) -> void + def each: () { (T) -> void } -> void + | () -> Enumerator[T, void] + def last: () -> T? + def size: () -> Integer + alias length size + def next_page: () -> Collection[T]? + end + + module AttachmentUploading + # Anything Faraday::FilePart can read the upload from, e.g. a StringIO. + interface _Readable + def read: () -> String? + end + + def attach: (filename: String, mime: String, ?io: _Readable?, ?options: options, **untyped data) -> Attachment + end + + module EventSubscription + def events: (?wait: Integer, ?options: options) -> untyped + end + + # Generated endpoint classes. Each concrete resource below inherits its + # generated API endpoints from one of these. + + class AttachmentsBase < Resource + end + + class CustomFieldsBase < Resource + def self.get_custom_fields_for_workspace: (client client, workspace_gid: String, ?options: options) -> Collection[CustomField] + end + + class PortfoliosBase < Resource + def self.get_items_for_portfolio: (client client, portfolio_gid: String, ?options: options) -> Collection[Project] + end + + class ProjectsBase < Resource + end + + class SectionsBase < Resource + def self.get_sections_for_project: (client client, project_gid: String, ?options: options) -> Collection[Section] + def self.add_task_for_section: (client client, section_gid: String, ?options: options, **untyped data) -> untyped + end + + class StoriesBase < Resource + end + + class TagsBase < Resource + def self.get_tags_for_workspace: (client client, workspace_gid: String, ?options: options) -> Collection[Tag] + end + + class TasksBase < Resource + def self.get_task: (client client, task_gid: String, ?options: options) -> Task + def self.get_tasks: (client client, ?assignee: String?, ?project: String?, ?section: String?, ?workspace: String?, ?completed_since: String?, ?modified_since: String?, ?options: options) -> Collection[Task] + def self.get_subtasks_for_task: (client client, task_gid: String, ?options: options) -> Collection[Task] + def self.update_task: (client client, task_gid: String, ?options: options, **untyped data) -> Task + def self.add_project_for_task: (client client, task_gid: String, ?options: options, **untyped data) -> untyped + def self.search_tasks_for_workspace: (client client, workspace_gid: String, ?options: options, **untyped filters) -> Collection[Task] + end + + class UsersBase < Resource + end + + class UserTaskListsBase < Resource + def self.get_user_task_list_for_user: (client client, user_gid: String, ?workspace: String?, ?options: options) -> UserTaskList + end + + class WebhooksBase < Resource + def self.create_webhook: (client client, ?options: options, **untyped data) -> Webhook + end + + class WorkspacesBase < Resource + end + + class Attachment < AttachmentsBase + attr_reader gid: String + attr_reader resource_type: String + attr_reader created_at: String? + attr_reader download_url: String? + attr_reader host: String? + attr_reader name: String? + attr_reader parent: Hash[String, untyped]? + attr_reader view_url: String? + + def self.find_by_id: (client client, String id, ?options: options) -> Attachment + def self.find_by_task: (client client, task: String, ?per_page: Integer, ?options: options) -> Collection[Attachment] + end + + class CustomField < CustomFieldsBase + attr_reader gid: String + attr_reader resource_type: String + attr_reader resource_subtype: String? + attr_reader name: String? + attr_reader description: String? + attr_reader type: String? + attr_reader enum_options: Array[Hash[String, untyped]]? + attr_reader precision: Integer? + + def self.find_by_id: (client client, String id, ?options: options) -> CustomField + # Wraps its elements as plain Resources, not CustomFields. + def self.find_by_workspace: (client client, workspace: String, ?per_page: Integer, ?options: options) -> Collection[Resource] + def update: (?options: options, **untyped data) -> CustomField + end + + class Portfolio < PortfoliosBase + attr_reader gid: String + attr_reader resource_type: String + attr_reader name: String? + attr_reader owner: Hash[String, untyped]? + attr_reader created_at: String? + attr_reader created_by: Hash[String, untyped]? + attr_reader custom_field_settings: Array[Hash[String, untyped]]? + attr_reader color: String? + attr_reader workspace: Hash[String, untyped]? + attr_reader members: Array[Hash[String, untyped]]? + + def self.find_by_id: (client client, String id, ?options: options) -> Portfolio + def self.find_all: (client client, workspace: String, owner: String, ?per_page: Integer, ?options: options) -> Collection[Portfolio] + def update: (?options: options, **untyped data) -> Portfolio + def get_items: (?options: options) -> Collection[Resource] + end + + class Project < ProjectsBase + include EventSubscription + + attr_reader gid: String + attr_reader resource_type: String + attr_reader name: String? + attr_reader owner: Hash[String, untyped]? + attr_reader current_status: Hash[String, untyped]? + attr_reader due_date: String? + attr_reader due_on: String? + attr_reader start_on: String? + attr_reader created_at: String? + attr_reader modified_at: String? + attr_reader archived: bool? + attr_reader public: bool? + attr_reader members: Array[Hash[String, untyped]]? + attr_reader followers: Array[Hash[String, untyped]]? + attr_reader custom_fields: Array[Hash[String, untyped]]? + attr_reader custom_field_settings: Array[Hash[String, untyped]]? + attr_reader color: String? + attr_reader notes: String? + attr_reader html_notes: String? + attr_reader workspace: Hash[String, untyped]? + attr_reader team: Hash[String, untyped]? + attr_reader layout: String? + + def self.create: (client client, workspace: String, ?team: String?, ?options: options, **untyped data) -> Project + def self.find_by_id: (client client, String id, ?options: options) -> Project + def self.find_all: (client client, ?workspace: String?, ?team: String?, ?is_template: bool?, ?archived: bool?, ?per_page: Integer, ?options: options) -> Collection[Project] + def self.find_by_workspace: (client client, workspace: String, ?is_template: bool?, ?archived: bool?, ?per_page: Integer, ?options: options) -> Collection[Project] + def update: (?options: options, **untyped data) -> Project + end + + class Section < SectionsBase + attr_reader gid: String + attr_reader resource_type: String + attr_reader name: String? + attr_reader project: Hash[String, untyped]? + attr_reader created_at: String? + + def self.find_by_id: (client client, String id, ?options: options) -> Section + def update: (?options: options, **untyped data) -> Section + end + + class Story < StoriesBase + attr_reader gid: String + attr_reader resource_type: String + attr_reader resource_subtype: String? + attr_reader created_at: String? + attr_reader created_by: Hash[String, untyped]? + attr_reader liked: bool? + attr_reader likes: Array[Hash[String, untyped]]? + attr_reader num_likes: Integer? + attr_reader text: String? + attr_reader html_text: String? + attr_reader target: Hash[String, untyped]? + attr_reader is_pinned: bool? + attr_reader is_edited: bool? + attr_reader source: String? + attr_reader type: String? + + def self.find_by_id: (client client, String id, ?options: options) -> Story + def self.find_by_task: (client client, task: String, ?per_page: Integer, ?options: options) -> Collection[Story] + def update: (?text: String?, ?html_text: String?, ?is_pinned: bool?, ?options: options, **untyped data) -> Story + end + + class Tag < TagsBase + attr_reader gid: String + attr_reader resource_type: String + attr_reader created_at: String? + attr_reader followers: Array[Hash[String, untyped]]? + attr_reader name: String? + attr_reader color: String? + attr_reader workspace: Hash[String, untyped]? + + def self.create: (client client, workspace: String, ?options: options, **untyped data) -> Tag + def self.find_by_id: (client client, String id, ?options: options) -> Tag + def self.find_all: (client client, ?workspace: String?, ?team: String?, ?archived: bool?, ?per_page: Integer, ?options: options) -> Collection[Tag] + def self.find_by_workspace: (client client, workspace: String, ?per_page: Integer, ?options: options) -> Collection[Tag] + def update: (?options: options, **untyped data) -> Tag + def get_tasks_with_tag: (?per_page: Integer, ?options: options) -> Collection[Task] + end + + class Task < TasksBase + include AttachmentUploading + include EventSubscription + + attr_reader gid: String + attr_reader resource_type: String + attr_reader resource_subtype: String? + attr_reader assignee: Hash[String, untyped]? + attr_reader assignee_status: String? + attr_reader created_at: String? + attr_reader completed: bool? + attr_reader completed_at: String? + attr_reader custom_fields: Array[Hash[String, untyped]]? + attr_reader dependents: Array[Hash[String, untyped]]? + attr_reader due_on: String? + attr_reader due_at: String? + attr_reader external: Hash[String, untyped]? + attr_reader followers: Array[Hash[String, untyped]]? + attr_reader is_rendered_as_separator: bool? + attr_reader liked: bool? + attr_reader likes: Array[Hash[String, untyped]]? + attr_reader memberships: Array[Hash[String, untyped]]? + attr_reader modified_at: String? + attr_reader name: String? + attr_reader notes: String? + attr_reader html_notes: String? + attr_reader num_likes: Integer? + attr_reader num_subtasks: Integer? + attr_reader parent: Hash[String, untyped]? + attr_reader projects: Array[Hash[String, untyped]]? + attr_reader start_on: String? + attr_reader workspace: Hash[String, untyped]? + attr_reader tags: Array[Hash[String, untyped]]? + + def self.create: (client client, ?workspace: String?, ?options: options, **untyped data) -> Task + def self.find_by_id: (client client, String id, ?options: options) -> Task + def self.find_all: (client client, ?assignee: String?, ?workspace: String?, ?project: String?, ?section: String?, ?tag: String?, ?user_task_list: String?, ?completed_since: String?, ?modified_since: String?, ?per_page: Integer, ?options: options) -> Collection[Task] + + def update: (?options: options, **untyped data) -> Task + # The gem also declares `attr_reader :dependencies`; this method, + # defined later, replaces it and fetches the dependencies from the API. + def dependencies: (?options: options) -> Collection[Task] + def subtasks: (?per_page: Integer, ?options: options) -> Collection[Task] + def stories: (?per_page: Integer, ?options: options) -> Collection[Story] + end + + class User < UsersBase + attr_reader gid: String + attr_reader resource_type: String + attr_reader name: String? + attr_reader email: String? + attr_reader photo: Hash[String, String]? + attr_reader workspaces: Array[Hash[String, untyped]]? + + # Wraps the response as a plain Resource, not a User. + def self.me: (client client, ?options: options) -> Resource + def self.find_by_id: (client client, String id, ?options: options) -> User + def self.find_all: (client client, ?workspace: String?, ?per_page: Integer, ?options: options) -> Collection[User] + def self.find_by_workspace: (client client, workspace: String, ?per_page: Integer, ?options: options) -> Collection[User] + end + + class UserTaskList < UserTaskListsBase + attr_reader gid: String + attr_reader resource_type: String + attr_reader name: String? + attr_reader owner: Hash[String, untyped]? + attr_reader workspace: Hash[String, untyped]? + + def self.find_by_id: (client client, String id, ?options: options) -> UserTaskList + end + + class Webhook < WebhooksBase + attr_reader gid: String + attr_reader resource_type: String + attr_reader resource: Hash[String, untyped]? + attr_reader target: String? + attr_reader active: bool? + attr_reader created_at: String? + attr_reader last_success_at: String? + attr_reader last_failure_at: String? + attr_reader last_failure_content: String? + + def self.create: (client client, resource: String, target: String, ?options: options, **untyped data) -> Webhook + def self.get_all: (client client, workspace: String, ?resource: String?, ?per_page: Integer, ?options: options) -> Collection[Webhook] + end + + class Workspace < WorkspacesBase + attr_reader gid: String + attr_reader resource_type: String + attr_reader name: String? + attr_reader is_organization: bool? + + def self.find_by_id: (client client, String id, ?options: options) -> Workspace + def self.find_all: (client client, ?per_page: Integer, ?options: options) -> Collection[Workspace] + def update: (?options: options, **untyped data) -> Workspace + end + end +end From ee260b2534a926e3490ab86af49524b77a66c5ec Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 15 Oct 2026 14:55:39 -0400 Subject: [PATCH 02/10] Add apiology as asana RBS reviewer Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01HSbQkrSvmSooewdXEYTCBM --- gems/asana/_reviewers.yaml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 gems/asana/_reviewers.yaml diff --git a/gems/asana/_reviewers.yaml b/gems/asana/_reviewers.yaml new file mode 100644 index 000000000..8d710c457 --- /dev/null +++ b/gems/asana/_reviewers.yaml @@ -0,0 +1,2 @@ +reviewers: + - apiology From 69a39c3c3630c53657b5090410f2cbdab479c1c5 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 15 Oct 2026 14:58:50 -0400 Subject: [PATCH 03/10] Mark omitted-field asana attributes implicit nil Resource attributes are nil when the request's fields option leaves them out, which is the edge case implicitly-returns-nil describes. Attributes the API itself can return as null keep optional types. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01HSbQkrSvmSooewdXEYTCBM --- gems/asana/0.10/asana.rbs | 152 +++++++++++++++++++------------------- 1 file changed, 78 insertions(+), 74 deletions(-) diff --git a/gems/asana/0.10/asana.rbs b/gems/asana/0.10/asana.rbs index 64e61f13f..5c303b8bf 100644 --- a/gems/asana/0.10/asana.rbs +++ b/gems/asana/0.10/asana.rbs @@ -179,6 +179,10 @@ module Asana # Asana::Client when called directly (it answers the same verbs). type client = Asana::Client | Asana::HttpClient + # Resource attributes are nil when the request's `fields` option leaves + # them out. Those are declared `implicitly-returns-nil`; attributes the + # API itself can return as null (e.g. Task#due_on) are optional types. + module ResponseHelper def parse: (HttpClient::Response response) -> [untyped, Hash[String, untyped]] end @@ -280,10 +284,10 @@ module Asana class Attachment < AttachmentsBase attr_reader gid: String attr_reader resource_type: String - attr_reader created_at: String? + def created_at: %a{implicitly-returns-nil} () -> String attr_reader download_url: String? attr_reader host: String? - attr_reader name: String? + def name: %a{implicitly-returns-nil} () -> String attr_reader parent: Hash[String, untyped]? attr_reader view_url: String? @@ -294,10 +298,10 @@ module Asana class CustomField < CustomFieldsBase attr_reader gid: String attr_reader resource_type: String - attr_reader resource_subtype: String? - attr_reader name: String? + def resource_subtype: %a{implicitly-returns-nil} () -> String + def name: %a{implicitly-returns-nil} () -> String attr_reader description: String? - attr_reader type: String? + def type: %a{implicitly-returns-nil} () -> String attr_reader enum_options: Array[Hash[String, untyped]]? attr_reader precision: Integer? @@ -310,14 +314,14 @@ module Asana class Portfolio < PortfoliosBase attr_reader gid: String attr_reader resource_type: String - attr_reader name: String? - attr_reader owner: Hash[String, untyped]? - attr_reader created_at: String? + def name: %a{implicitly-returns-nil} () -> String + def owner: %a{implicitly-returns-nil} () -> Hash[String, untyped] + def created_at: %a{implicitly-returns-nil} () -> String attr_reader created_by: Hash[String, untyped]? - attr_reader custom_field_settings: Array[Hash[String, untyped]]? - attr_reader color: String? - attr_reader workspace: Hash[String, untyped]? - attr_reader members: Array[Hash[String, untyped]]? + def custom_field_settings: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] + def color: %a{implicitly-returns-nil} () -> String + def workspace: %a{implicitly-returns-nil} () -> Hash[String, untyped] + def members: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] def self.find_by_id: (client client, String id, ?options: options) -> Portfolio def self.find_all: (client client, workspace: String, owner: String, ?per_page: Integer, ?options: options) -> Collection[Portfolio] @@ -330,26 +334,26 @@ module Asana attr_reader gid: String attr_reader resource_type: String - attr_reader name: String? + def name: %a{implicitly-returns-nil} () -> String attr_reader owner: Hash[String, untyped]? attr_reader current_status: Hash[String, untyped]? attr_reader due_date: String? attr_reader due_on: String? attr_reader start_on: String? - attr_reader created_at: String? - attr_reader modified_at: String? - attr_reader archived: bool? - attr_reader public: bool? - attr_reader members: Array[Hash[String, untyped]]? - attr_reader followers: Array[Hash[String, untyped]]? - attr_reader custom_fields: Array[Hash[String, untyped]]? - attr_reader custom_field_settings: Array[Hash[String, untyped]]? + def created_at: %a{implicitly-returns-nil} () -> String + def modified_at: %a{implicitly-returns-nil} () -> String + def archived: %a{implicitly-returns-nil} () -> bool + def public: %a{implicitly-returns-nil} () -> bool + def members: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] + def followers: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] + def custom_fields: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] + def custom_field_settings: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] attr_reader color: String? - attr_reader notes: String? - attr_reader html_notes: String? - attr_reader workspace: Hash[String, untyped]? + def notes: %a{implicitly-returns-nil} () -> String + def html_notes: %a{implicitly-returns-nil} () -> String + def workspace: %a{implicitly-returns-nil} () -> Hash[String, untyped] attr_reader team: Hash[String, untyped]? - attr_reader layout: String? + def layout: %a{implicitly-returns-nil} () -> String def self.create: (client client, workspace: String, ?team: String?, ?options: options, **untyped data) -> Project def self.find_by_id: (client client, String id, ?options: options) -> Project @@ -361,9 +365,9 @@ module Asana class Section < SectionsBase attr_reader gid: String attr_reader resource_type: String - attr_reader name: String? - attr_reader project: Hash[String, untyped]? - attr_reader created_at: String? + def name: %a{implicitly-returns-nil} () -> String + def project: %a{implicitly-returns-nil} () -> Hash[String, untyped] + def created_at: %a{implicitly-returns-nil} () -> String def self.find_by_id: (client client, String id, ?options: options) -> Section def update: (?options: options, **untyped data) -> Section @@ -372,19 +376,19 @@ module Asana class Story < StoriesBase attr_reader gid: String attr_reader resource_type: String - attr_reader resource_subtype: String? - attr_reader created_at: String? + def resource_subtype: %a{implicitly-returns-nil} () -> String + def created_at: %a{implicitly-returns-nil} () -> String attr_reader created_by: Hash[String, untyped]? - attr_reader liked: bool? - attr_reader likes: Array[Hash[String, untyped]]? - attr_reader num_likes: Integer? - attr_reader text: String? - attr_reader html_text: String? - attr_reader target: Hash[String, untyped]? - attr_reader is_pinned: bool? - attr_reader is_edited: bool? - attr_reader source: String? - attr_reader type: String? + def liked: %a{implicitly-returns-nil} () -> bool + def likes: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] + def num_likes: %a{implicitly-returns-nil} () -> Integer + def text: %a{implicitly-returns-nil} () -> String + def html_text: %a{implicitly-returns-nil} () -> String + def target: %a{implicitly-returns-nil} () -> Hash[String, untyped] + def is_pinned: %a{implicitly-returns-nil} () -> bool + def is_edited: %a{implicitly-returns-nil} () -> bool + def source: %a{implicitly-returns-nil} () -> String + def type: %a{implicitly-returns-nil} () -> String def self.find_by_id: (client client, String id, ?options: options) -> Story def self.find_by_task: (client client, task: String, ?per_page: Integer, ?options: options) -> Collection[Story] @@ -394,11 +398,11 @@ module Asana class Tag < TagsBase attr_reader gid: String attr_reader resource_type: String - attr_reader created_at: String? - attr_reader followers: Array[Hash[String, untyped]]? - attr_reader name: String? + def created_at: %a{implicitly-returns-nil} () -> String + def followers: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] + def name: %a{implicitly-returns-nil} () -> String attr_reader color: String? - attr_reader workspace: Hash[String, untyped]? + def workspace: %a{implicitly-returns-nil} () -> Hash[String, untyped] def self.create: (client client, workspace: String, ?options: options, **untyped data) -> Tag def self.find_by_id: (client client, String id, ?options: options) -> Tag @@ -414,33 +418,33 @@ module Asana attr_reader gid: String attr_reader resource_type: String - attr_reader resource_subtype: String? + def resource_subtype: %a{implicitly-returns-nil} () -> String attr_reader assignee: Hash[String, untyped]? attr_reader assignee_status: String? - attr_reader created_at: String? - attr_reader completed: bool? + def created_at: %a{implicitly-returns-nil} () -> String + def completed: %a{implicitly-returns-nil} () -> bool attr_reader completed_at: String? - attr_reader custom_fields: Array[Hash[String, untyped]]? - attr_reader dependents: Array[Hash[String, untyped]]? + def custom_fields: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] + def dependents: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] attr_reader due_on: String? attr_reader due_at: String? attr_reader external: Hash[String, untyped]? - attr_reader followers: Array[Hash[String, untyped]]? - attr_reader is_rendered_as_separator: bool? - attr_reader liked: bool? - attr_reader likes: Array[Hash[String, untyped]]? - attr_reader memberships: Array[Hash[String, untyped]]? - attr_reader modified_at: String? - attr_reader name: String? - attr_reader notes: String? - attr_reader html_notes: String? - attr_reader num_likes: Integer? - attr_reader num_subtasks: Integer? + def followers: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] + def is_rendered_as_separator: %a{implicitly-returns-nil} () -> bool + def liked: %a{implicitly-returns-nil} () -> bool + def likes: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] + def memberships: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] + def modified_at: %a{implicitly-returns-nil} () -> String + def name: %a{implicitly-returns-nil} () -> String + def notes: %a{implicitly-returns-nil} () -> String + def html_notes: %a{implicitly-returns-nil} () -> String + def num_likes: %a{implicitly-returns-nil} () -> Integer + def num_subtasks: %a{implicitly-returns-nil} () -> Integer attr_reader parent: Hash[String, untyped]? - attr_reader projects: Array[Hash[String, untyped]]? + def projects: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] attr_reader start_on: String? - attr_reader workspace: Hash[String, untyped]? - attr_reader tags: Array[Hash[String, untyped]]? + def workspace: %a{implicitly-returns-nil} () -> Hash[String, untyped] + def tags: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] def self.create: (client client, ?workspace: String?, ?options: options, **untyped data) -> Task def self.find_by_id: (client client, String id, ?options: options) -> Task @@ -457,10 +461,10 @@ module Asana class User < UsersBase attr_reader gid: String attr_reader resource_type: String - attr_reader name: String? + def name: %a{implicitly-returns-nil} () -> String attr_reader email: String? attr_reader photo: Hash[String, String]? - attr_reader workspaces: Array[Hash[String, untyped]]? + def workspaces: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] # Wraps the response as a plain Resource, not a User. def self.me: (client client, ?options: options) -> Resource @@ -472,9 +476,9 @@ module Asana class UserTaskList < UserTaskListsBase attr_reader gid: String attr_reader resource_type: String - attr_reader name: String? - attr_reader owner: Hash[String, untyped]? - attr_reader workspace: Hash[String, untyped]? + def name: %a{implicitly-returns-nil} () -> String + def owner: %a{implicitly-returns-nil} () -> Hash[String, untyped] + def workspace: %a{implicitly-returns-nil} () -> Hash[String, untyped] def self.find_by_id: (client client, String id, ?options: options) -> UserTaskList end @@ -482,10 +486,10 @@ module Asana class Webhook < WebhooksBase attr_reader gid: String attr_reader resource_type: String - attr_reader resource: Hash[String, untyped]? - attr_reader target: String? - attr_reader active: bool? - attr_reader created_at: String? + def resource: %a{implicitly-returns-nil} () -> Hash[String, untyped] + def target: %a{implicitly-returns-nil} () -> String + def active: %a{implicitly-returns-nil} () -> bool + def created_at: %a{implicitly-returns-nil} () -> String attr_reader last_success_at: String? attr_reader last_failure_at: String? attr_reader last_failure_content: String? @@ -497,8 +501,8 @@ module Asana class Workspace < WorkspacesBase attr_reader gid: String attr_reader resource_type: String - attr_reader name: String? - attr_reader is_organization: bool? + def name: %a{implicitly-returns-nil} () -> String + def is_organization: %a{implicitly-returns-nil} () -> bool def self.find_by_id: (client client, String id, ?options: options) -> Workspace def self.find_all: (client client, ?per_page: Integer, ?options: options) -> Collection[Workspace] From 6536638f45b321944eba945c1ec8cb0a5f2ea28b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 15 Oct 2026 15:35:26 -0400 Subject: [PATCH 04/10] Declare gid on every asana Resource Nested objects and some endpoints (Users.me) come back as plain Resources, and every Asana object has a gid. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01HSbQkrSvmSooewdXEYTCBM --- gems/asana/0.10/_test/test.rb | 2 +- gems/asana/0.10/asana.rbs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/gems/asana/0.10/_test/test.rb b/gems/asana/0.10/_test/test.rb index 1da12a3b1..a4d32744d 100644 --- a/gems/asana/0.10/_test/test.rb +++ b/gems/asana/0.10/_test/test.rb @@ -10,7 +10,7 @@ workspace&.name&.upcase me = client.users.me -me.to_h["gid"] +me.gid.length project = client.projects.find_by_id("2", options: { fields: ["name", "due_date"] }) project.due_date&.length diff --git a/gems/asana/0.10/asana.rbs b/gems/asana/0.10/asana.rbs index 5c303b8bf..628eacecf 100644 --- a/gems/asana/0.10/asana.rbs +++ b/gems/asana/0.10/asana.rbs @@ -192,6 +192,9 @@ module Asana extend ResponseHelper def initialize: (Hash[String, untyped] data, client: client) -> void + # Every Asana object carries a gid, including the plain Resources that + # nested objects and some endpoints (e.g. Users.me) are wrapped in. + def gid: () -> String def refresh: () -> self def to_h: () -> Hash[String, untyped] From 2a90697c7515161e023305ad2ebcf401643ffa2f Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 15 Oct 2026 16:22:40 -0400 Subject: [PATCH 05/10] Type asana methods that replace attribute readers Task#dependents, #projects and #tags and Portfolio#custom_field_settings are declared with attr_reader and then redefined as methods that fetch a Collection from the API; the later def is what runs. Also add Project#tasks and the Task.find_by_tag/section/project class methods. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01HSbQkrSvmSooewdXEYTCBM --- gems/asana/0.10/_test/test.rb | 6 ++++++ gems/asana/0.10/asana.rbs | 25 +++++++++++++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/gems/asana/0.10/_test/test.rb b/gems/asana/0.10/_test/test.rb index a4d32744d..6955748e7 100644 --- a/gems/asana/0.10/_test/test.rb +++ b/gems/asana/0.10/_test/test.rb @@ -32,6 +32,12 @@ task.subtasks(per_page: 50).each { |subtask| subtask.gid } task.dependencies.to_a.size task.stories.each { |story| story.created_at } +task.tags.map(&:name) +task.projects(per_page: 10).first&.gid +task.dependents.size +project.tasks(per_page: 50, options: { fields: ["name"] }).first&.name +Asana::Resources::Task.find_by_tag(client, tag: "6").to_a +client.portfolios.find_by_id("5").custom_field_settings.map(&:is_important) task.update(completed: true).completed task.attach(filename: "notes.txt", mime: "text/plain", io: StringIO.new("hello")).name diff --git a/gems/asana/0.10/asana.rbs b/gems/asana/0.10/asana.rbs index 628eacecf..988fab001 100644 --- a/gems/asana/0.10/asana.rbs +++ b/gems/asana/0.10/asana.rbs @@ -238,6 +238,9 @@ module Asana class AttachmentsBase < Resource end + class CustomFieldSettingsBase < Resource + end + class CustomFieldsBase < Resource def self.get_custom_fields_for_workspace: (client client, workspace_gid: String, ?options: options) -> Collection[CustomField] end @@ -298,6 +301,16 @@ module Asana def self.find_by_task: (client client, task: String, ?per_page: Integer, ?options: options) -> Collection[Attachment] end + class CustomFieldSetting < CustomFieldSettingsBase + attr_reader gid: String + def resource_type: %a{implicitly-returns-nil} () -> String + def created_at: %a{implicitly-returns-nil} () -> String + def is_important: %a{implicitly-returns-nil} () -> bool + def parent: %a{implicitly-returns-nil} () -> Hash[String, untyped] + def project: %a{implicitly-returns-nil} () -> Hash[String, untyped] + def custom_field: %a{implicitly-returns-nil} () -> Hash[String, untyped] + end + class CustomField < CustomFieldsBase attr_reader gid: String attr_reader resource_type: String @@ -321,7 +334,7 @@ module Asana def owner: %a{implicitly-returns-nil} () -> Hash[String, untyped] def created_at: %a{implicitly-returns-nil} () -> String attr_reader created_by: Hash[String, untyped]? - def custom_field_settings: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] + def custom_field_settings: (?options: options) -> Collection[CustomFieldSetting] def color: %a{implicitly-returns-nil} () -> String def workspace: %a{implicitly-returns-nil} () -> Hash[String, untyped] def members: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] @@ -363,6 +376,7 @@ module Asana def self.find_all: (client client, ?workspace: String?, ?team: String?, ?is_template: bool?, ?archived: bool?, ?per_page: Integer, ?options: options) -> Collection[Project] def self.find_by_workspace: (client client, workspace: String, ?is_template: bool?, ?archived: bool?, ?per_page: Integer, ?options: options) -> Collection[Project] def update: (?options: options, **untyped data) -> Project + def tasks: (?per_page: Integer, ?options: options) -> Collection[Task] end class Section < SectionsBase @@ -428,7 +442,6 @@ module Asana def completed: %a{implicitly-returns-nil} () -> bool attr_reader completed_at: String? def custom_fields: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] - def dependents: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] attr_reader due_on: String? attr_reader due_at: String? attr_reader external: Hash[String, untyped]? @@ -444,14 +457,15 @@ module Asana def num_likes: %a{implicitly-returns-nil} () -> Integer def num_subtasks: %a{implicitly-returns-nil} () -> Integer attr_reader parent: Hash[String, untyped]? - def projects: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] attr_reader start_on: String? def workspace: %a{implicitly-returns-nil} () -> Hash[String, untyped] - def tags: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] def self.create: (client client, ?workspace: String?, ?options: options, **untyped data) -> Task def self.find_by_id: (client client, String id, ?options: options) -> Task def self.find_all: (client client, ?assignee: String?, ?workspace: String?, ?project: String?, ?section: String?, ?tag: String?, ?user_task_list: String?, ?completed_since: String?, ?modified_since: String?, ?per_page: Integer, ?options: options) -> Collection[Task] + def self.find_by_project: (client client, ?project: String?, ?projectId: String?, ?per_page: Integer, ?options: options) -> Collection[Task] + def self.find_by_tag: (client client, tag: String, ?per_page: Integer, ?options: options) -> Collection[Task] + def self.find_by_section: (client client, section: String, ?per_page: Integer, ?options: options) -> Collection[Task] def update: (?options: options, **untyped data) -> Task # The gem also declares `attr_reader :dependencies`; this method, @@ -459,6 +473,9 @@ module Asana def dependencies: (?options: options) -> Collection[Task] def subtasks: (?per_page: Integer, ?options: options) -> Collection[Task] def stories: (?per_page: Integer, ?options: options) -> Collection[Story] + def dependents: (?options: options) -> Collection[Task] + def projects: (?per_page: Integer, ?options: options) -> Collection[Project] + def tags: (?per_page: Integer, ?options: options) -> Collection[Tag] end class User < UsersBase From 51887749f43045616bb63ecaa4a3f7e83fb87ef4 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 15 Oct 2026 16:30:48 -0400 Subject: [PATCH 06/10] Type compact asana object references as String hashes Nested objects (project, assignee, memberships entries, ...) arrive in compact form with string values only; untyped values made every .fetch on them an unresolved call for strong type checkers. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01HSbQkrSvmSooewdXEYTCBM --- gems/asana/0.10/asana.rbs | 56 ++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/gems/asana/0.10/asana.rbs b/gems/asana/0.10/asana.rbs index 988fab001..604ab9b2a 100644 --- a/gems/asana/0.10/asana.rbs +++ b/gems/asana/0.10/asana.rbs @@ -182,6 +182,8 @@ module Asana # Resource attributes are nil when the request's `fields` option leaves # them out. Those are declared `implicitly-returns-nil`; attributes the # API itself can return as null (e.g. Task#due_on) are optional types. + # References to other objects arrive in compact form ({ "gid" =>, + # "name" =>, "resource_type" => }), so they are Hash[String, String]. module ResponseHelper def parse: (HttpClient::Response response) -> [untyped, Hash[String, untyped]] @@ -294,7 +296,7 @@ module Asana attr_reader download_url: String? attr_reader host: String? def name: %a{implicitly-returns-nil} () -> String - attr_reader parent: Hash[String, untyped]? + attr_reader parent: Hash[String, String]? attr_reader view_url: String? def self.find_by_id: (client client, String id, ?options: options) -> Attachment @@ -306,8 +308,8 @@ module Asana def resource_type: %a{implicitly-returns-nil} () -> String def created_at: %a{implicitly-returns-nil} () -> String def is_important: %a{implicitly-returns-nil} () -> bool - def parent: %a{implicitly-returns-nil} () -> Hash[String, untyped] - def project: %a{implicitly-returns-nil} () -> Hash[String, untyped] + def parent: %a{implicitly-returns-nil} () -> Hash[String, String] + def project: %a{implicitly-returns-nil} () -> Hash[String, String] def custom_field: %a{implicitly-returns-nil} () -> Hash[String, untyped] end @@ -331,13 +333,13 @@ module Asana attr_reader gid: String attr_reader resource_type: String def name: %a{implicitly-returns-nil} () -> String - def owner: %a{implicitly-returns-nil} () -> Hash[String, untyped] + def owner: %a{implicitly-returns-nil} () -> Hash[String, String] def created_at: %a{implicitly-returns-nil} () -> String - attr_reader created_by: Hash[String, untyped]? + attr_reader created_by: Hash[String, String]? def custom_field_settings: (?options: options) -> Collection[CustomFieldSetting] def color: %a{implicitly-returns-nil} () -> String - def workspace: %a{implicitly-returns-nil} () -> Hash[String, untyped] - def members: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] + def workspace: %a{implicitly-returns-nil} () -> Hash[String, String] + def members: %a{implicitly-returns-nil} () -> Array[Hash[String, String]] def self.find_by_id: (client client, String id, ?options: options) -> Portfolio def self.find_all: (client client, workspace: String, owner: String, ?per_page: Integer, ?options: options) -> Collection[Portfolio] @@ -351,7 +353,7 @@ module Asana attr_reader gid: String attr_reader resource_type: String def name: %a{implicitly-returns-nil} () -> String - attr_reader owner: Hash[String, untyped]? + attr_reader owner: Hash[String, String]? attr_reader current_status: Hash[String, untyped]? attr_reader due_date: String? attr_reader due_on: String? @@ -360,15 +362,15 @@ module Asana def modified_at: %a{implicitly-returns-nil} () -> String def archived: %a{implicitly-returns-nil} () -> bool def public: %a{implicitly-returns-nil} () -> bool - def members: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] - def followers: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] + def members: %a{implicitly-returns-nil} () -> Array[Hash[String, String]] + def followers: %a{implicitly-returns-nil} () -> Array[Hash[String, String]] def custom_fields: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] def custom_field_settings: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] attr_reader color: String? def notes: %a{implicitly-returns-nil} () -> String def html_notes: %a{implicitly-returns-nil} () -> String - def workspace: %a{implicitly-returns-nil} () -> Hash[String, untyped] - attr_reader team: Hash[String, untyped]? + def workspace: %a{implicitly-returns-nil} () -> Hash[String, String] + attr_reader team: Hash[String, String]? def layout: %a{implicitly-returns-nil} () -> String def self.create: (client client, workspace: String, ?team: String?, ?options: options, **untyped data) -> Project @@ -383,7 +385,7 @@ module Asana attr_reader gid: String attr_reader resource_type: String def name: %a{implicitly-returns-nil} () -> String - def project: %a{implicitly-returns-nil} () -> Hash[String, untyped] + def project: %a{implicitly-returns-nil} () -> Hash[String, String] def created_at: %a{implicitly-returns-nil} () -> String def self.find_by_id: (client client, String id, ?options: options) -> Section @@ -395,13 +397,13 @@ module Asana attr_reader resource_type: String def resource_subtype: %a{implicitly-returns-nil} () -> String def created_at: %a{implicitly-returns-nil} () -> String - attr_reader created_by: Hash[String, untyped]? + attr_reader created_by: Hash[String, String]? def liked: %a{implicitly-returns-nil} () -> bool def likes: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] def num_likes: %a{implicitly-returns-nil} () -> Integer def text: %a{implicitly-returns-nil} () -> String def html_text: %a{implicitly-returns-nil} () -> String - def target: %a{implicitly-returns-nil} () -> Hash[String, untyped] + def target: %a{implicitly-returns-nil} () -> Hash[String, String] def is_pinned: %a{implicitly-returns-nil} () -> bool def is_edited: %a{implicitly-returns-nil} () -> bool def source: %a{implicitly-returns-nil} () -> String @@ -416,10 +418,10 @@ module Asana attr_reader gid: String attr_reader resource_type: String def created_at: %a{implicitly-returns-nil} () -> String - def followers: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] + def followers: %a{implicitly-returns-nil} () -> Array[Hash[String, String]] def name: %a{implicitly-returns-nil} () -> String attr_reader color: String? - def workspace: %a{implicitly-returns-nil} () -> Hash[String, untyped] + def workspace: %a{implicitly-returns-nil} () -> Hash[String, String] def self.create: (client client, workspace: String, ?options: options, **untyped data) -> Tag def self.find_by_id: (client client, String id, ?options: options) -> Tag @@ -436,7 +438,7 @@ module Asana attr_reader gid: String attr_reader resource_type: String def resource_subtype: %a{implicitly-returns-nil} () -> String - attr_reader assignee: Hash[String, untyped]? + attr_reader assignee: Hash[String, String]? attr_reader assignee_status: String? def created_at: %a{implicitly-returns-nil} () -> String def completed: %a{implicitly-returns-nil} () -> bool @@ -444,21 +446,21 @@ module Asana def custom_fields: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] attr_reader due_on: String? attr_reader due_at: String? - attr_reader external: Hash[String, untyped]? - def followers: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] + attr_reader external: Hash[String, String]? + def followers: %a{implicitly-returns-nil} () -> Array[Hash[String, String]] def is_rendered_as_separator: %a{implicitly-returns-nil} () -> bool def liked: %a{implicitly-returns-nil} () -> bool def likes: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] - def memberships: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] + def memberships: %a{implicitly-returns-nil} () -> Array[Hash[String, Hash[String, String]]] def modified_at: %a{implicitly-returns-nil} () -> String def name: %a{implicitly-returns-nil} () -> String def notes: %a{implicitly-returns-nil} () -> String def html_notes: %a{implicitly-returns-nil} () -> String def num_likes: %a{implicitly-returns-nil} () -> Integer def num_subtasks: %a{implicitly-returns-nil} () -> Integer - attr_reader parent: Hash[String, untyped]? + attr_reader parent: Hash[String, String]? attr_reader start_on: String? - def workspace: %a{implicitly-returns-nil} () -> Hash[String, untyped] + def workspace: %a{implicitly-returns-nil} () -> Hash[String, String] def self.create: (client client, ?workspace: String?, ?options: options, **untyped data) -> Task def self.find_by_id: (client client, String id, ?options: options) -> Task @@ -484,7 +486,7 @@ module Asana def name: %a{implicitly-returns-nil} () -> String attr_reader email: String? attr_reader photo: Hash[String, String]? - def workspaces: %a{implicitly-returns-nil} () -> Array[Hash[String, untyped]] + def workspaces: %a{implicitly-returns-nil} () -> Array[Hash[String, String]] # Wraps the response as a plain Resource, not a User. def self.me: (client client, ?options: options) -> Resource @@ -497,8 +499,8 @@ module Asana attr_reader gid: String attr_reader resource_type: String def name: %a{implicitly-returns-nil} () -> String - def owner: %a{implicitly-returns-nil} () -> Hash[String, untyped] - def workspace: %a{implicitly-returns-nil} () -> Hash[String, untyped] + def owner: %a{implicitly-returns-nil} () -> Hash[String, String] + def workspace: %a{implicitly-returns-nil} () -> Hash[String, String] def self.find_by_id: (client client, String id, ?options: options) -> UserTaskList end @@ -506,7 +508,7 @@ module Asana class Webhook < WebhooksBase attr_reader gid: String attr_reader resource_type: String - def resource: %a{implicitly-returns-nil} () -> Hash[String, untyped] + def resource: %a{implicitly-returns-nil} () -> Hash[String, String] def target: %a{implicitly-returns-nil} () -> String def active: %a{implicitly-returns-nil} () -> bool def created_at: %a{implicitly-returns-nil} () -> String From b7066619cee60c7c4f8da9766564ae8700c9e9f8 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 15 Oct 2026 18:12:54 -0400 Subject: [PATCH 07/10] Declare asana fields answered through method_missing Asana API fields the gem has no attr_reader for still resolve, through Resource#method_missing: Task#start_at and #assignee_section, Project #completed, CustomField values, Story change fields and the rest of the fields Asana/openapi@dbe7d01f9f lists for each resource. Unlike the attr_readers, these raise NoMethodError when not requested and wrap nested objects in a plain Resource, so they are typed from the spec directly: nullable only where the spec says so. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01HSbQkrSvmSooewdXEYTCBM --- gems/asana/0.10/_test/test.rb | 4 + gems/asana/0.10/asana.rbs | 165 ++++++++++++++++++++++++++++++++++ 2 files changed, 169 insertions(+) diff --git a/gems/asana/0.10/_test/test.rb b/gems/asana/0.10/_test/test.rb index 6955748e7..895e6c5e6 100644 --- a/gems/asana/0.10/_test/test.rb +++ b/gems/asana/0.10/_test/test.rb @@ -35,6 +35,10 @@ task.tags.map(&:name) task.projects(per_page: 10).first&.gid task.dependents.size +task.start_at&.length +task.assignee_section&.gid +task.permalink_url.length +project.completed_by&.gid project.tasks(per_page: 50, options: { fields: ["name"] }).first&.name Asana::Resources::Task.find_by_tag(client, tag: "6").to_a client.portfolios.find_by_id("5").custom_field_settings.map(&:is_important) diff --git a/gems/asana/0.10/asana.rbs b/gems/asana/0.10/asana.rbs index 604ab9b2a..767582a89 100644 --- a/gems/asana/0.10/asana.rbs +++ b/gems/asana/0.10/asana.rbs @@ -299,6 +299,13 @@ module Asana attr_reader parent: Hash[String, String]? attr_reader view_url: String? + # Fields the gem answers only through Resource#method_missing, which + # wraps nested objects in a plain Resource and raises NoMethodError + # unless the request's fields option included the field. + def permanent_url: () -> String? + def size: () -> Integer + def connected_to_app: () -> bool + def self.find_by_id: (client client, String id, ?options: options) -> Attachment def self.find_by_task: (client client, task: String, ?per_page: Integer, ?options: options) -> Collection[Attachment] end @@ -323,6 +330,36 @@ module Asana attr_reader enum_options: Array[Hash[String, untyped]]? attr_reader precision: Integer? + # Fields the gem answers only through Resource#method_missing, which + # wraps nested objects in a plain Resource and raises NoMethodError + # unless the request's fields option included the field. + def enabled: () -> bool + def representation_type: () -> String + def id_prefix: () -> String? + def input_restrictions: () -> Array[String] + def is_formula_field: () -> bool + def is_lookup_field: () -> bool + def date_value: () -> Resource? + def enum_value: () -> Resource? + def multi_enum_values: () -> Array[Resource] + def number_value: () -> Numeric? + def text_value: () -> String? + def display_value: () -> String? + def format: () -> String + def currency_code: () -> String? + def custom_label: () -> String? + def custom_label_position: () -> String? + def is_global_to_workspace: () -> bool + def has_notifications_enabled: () -> bool + def asana_created_field: () -> String? + def is_value_read_only: () -> bool + def created_by: () -> Resource? + def people_value: () -> Array[Resource] + def reference_value: () -> Array[Resource] + def html_text_value: () -> String? + def privacy_setting: () -> String + def default_access_level: () -> String + def self.find_by_id: (client client, String id, ?options: options) -> CustomField # Wraps its elements as plain Resources, not CustomFields. def self.find_by_workspace: (client client, workspace: String, ?per_page: Integer, ?options: options) -> Collection[Resource] @@ -336,6 +373,23 @@ module Asana def owner: %a{implicitly-returns-nil} () -> Hash[String, String] def created_at: %a{implicitly-returns-nil} () -> String attr_reader created_by: Hash[String, String]? + + # Fields the gem answers only through Resource#method_missing, which + # wraps nested objects in a plain Resource and raises NoMethodError + # unless the request's fields option included the field. + def resource_subtype: () -> String + def archived: () -> bool + def start_on: () -> String? + def due_on: () -> String? + def default_access_level: () -> String + def public: () -> bool + def current_status_update: () -> Resource? + def custom_fields: () -> Array[Resource] + def permalink_url: () -> String + def privacy_setting: () -> String + def project_templates: () -> Array[Resource] + def custom_type: () -> Resource? + def custom_field_settings: (?options: options) -> Collection[CustomFieldSetting] def color: %a{implicitly-returns-nil} () -> String def workspace: %a{implicitly-returns-nil} () -> Hash[String, String] @@ -373,6 +427,25 @@ module Asana attr_reader team: Hash[String, String]? def layout: %a{implicitly-returns-nil} () -> String + # Fields the gem answers only through Resource#method_missing, which + # wraps nested objects in a plain Resource and raises NoMethodError + # unless the request's fields option included the field. + def resource_subtype: () -> String + def icon: () -> String? + def current_status_update: () -> Resource? + def default_view: () -> String + def privacy_setting: () -> String + def default_access_level: () -> String + def minimum_access_level_for_customization: () -> String + def minimum_access_level_for_sharing: () -> String + def completed: () -> bool + def completed_at: () -> String? + def completed_by: () -> Resource? + def permalink_url: () -> String + def project_brief: () -> Resource? + def created_from_template: () -> Resource? + def custom_type: () -> Resource? + def self.create: (client client, workspace: String, ?team: String?, ?options: options, **untyped data) -> Project def self.find_by_id: (client client, String id, ?options: options) -> Project def self.find_all: (client client, ?workspace: String?, ?team: String?, ?is_template: bool?, ?archived: bool?, ?per_page: Integer, ?options: options) -> Collection[Project] @@ -388,6 +461,11 @@ module Asana def project: %a{implicitly-returns-nil} () -> Hash[String, String] def created_at: %a{implicitly-returns-nil} () -> String + # Fields the gem answers only through Resource#method_missing, which + # wraps nested objects in a plain Resource and raises NoMethodError + # unless the request's fields option included the field. + def projects: () -> Array[Resource] + def self.find_by_id: (client client, String id, ?options: options) -> Section def update: (?options: options, **untyped data) -> Section end @@ -409,6 +487,49 @@ module Asana def source: %a{implicitly-returns-nil} () -> String def type: %a{implicitly-returns-nil} () -> String + # Fields the gem answers only through Resource#method_missing, which + # wraps nested objects in a plain Resource and raises NoMethodError + # unless the request's fields option included the field. + def sticker_name: () -> String + def is_editable: () -> bool + def hearted: () -> bool + def hearts: () -> Array[Resource] + def num_hearts: () -> Integer + def reaction_summary: () -> Array[Resource] + def previews: () -> Array[Resource] + def old_name: () -> String + def new_name: () -> String? + def old_dates: () -> Resource + def new_dates: () -> Resource + def old_resource_subtype: () -> String + def new_resource_subtype: () -> String + def story: () -> Resource + def assignee: () -> Resource + def follower: () -> Resource + def old_section: () -> Resource + def new_section: () -> Resource + def task: () -> Resource + def project: () -> Resource + def tag: () -> Resource + def custom_field: () -> Resource + def old_text_value: () -> String + def new_text_value: () -> String + def old_number_value: () -> Integer? + def new_number_value: () -> Integer + def old_enum_value: () -> Resource + def new_enum_value: () -> Resource + def old_date_value: () -> Resource + def new_date_value: () -> Resource + def old_people_value: () -> Array[Resource] + def new_people_value: () -> Array[Resource] + def old_multi_enum_values: () -> Array[Resource] + def new_multi_enum_values: () -> Array[Resource] + def new_approval_status: () -> String + def old_approval_status: () -> String + def duplicate_of: () -> Resource + def duplicated_from: () -> Resource + def dependency: () -> Resource + def self.find_by_id: (client client, String id, ?options: options) -> Story def self.find_by_task: (client client, task: String, ?per_page: Integer, ?options: options) -> Collection[Story] def update: (?text: String?, ?html_text: String?, ?is_pinned: bool?, ?options: options, **untyped data) -> Story @@ -423,6 +544,12 @@ module Asana attr_reader color: String? def workspace: %a{implicitly-returns-nil} () -> Hash[String, String] + # Fields the gem answers only through Resource#method_missing, which + # wraps nested objects in a plain Resource and raises NoMethodError + # unless the request's fields option included the field. + def notes: () -> String + def permalink_url: () -> String + def self.create: (client client, workspace: String, ?options: options, **untyped data) -> Tag def self.find_by_id: (client client, String id, ?options: options) -> Tag def self.find_all: (client client, ?workspace: String?, ?team: String?, ?archived: bool?, ?per_page: Integer, ?options: options) -> Collection[Tag] @@ -462,6 +589,25 @@ module Asana attr_reader start_on: String? def workspace: %a{implicitly-returns-nil} () -> Hash[String, String] + # Fields the gem answers only through Resource#method_missing, which + # wraps nested objects in a plain Resource and raises NoMethodError + # unless the request's fields option included the field. + def created_by: () -> Resource + def approval_status: () -> String + def assigned_by: () -> Resource? + def completed_by: () -> Resource? + def hearted: () -> bool + def hearts: () -> Array[Resource] + def has_subtasks_date_mismatch: () -> bool + def num_hearts: () -> Integer + def start_at: () -> String? + def actual_time_minutes: () -> Numeric? + def assignee_section: () -> Resource? + def custom_type: () -> Resource? + def custom_type_status_option: () -> Resource? + def effective_memberships: () -> Array[Resource] + def permalink_url: () -> String + def self.create: (client client, ?workspace: String?, ?options: options, **untyped data) -> Task def self.find_by_id: (client client, String id, ?options: options) -> Task def self.find_all: (client client, ?assignee: String?, ?workspace: String?, ?project: String?, ?section: String?, ?tag: String?, ?user_task_list: String?, ?completed_since: String?, ?modified_since: String?, ?per_page: Integer, ?options: options) -> Collection[Task] @@ -489,6 +635,12 @@ module Asana def workspaces: %a{implicitly-returns-nil} () -> Array[Hash[String, String]] # Wraps the response as a plain Resource, not a User. + + # Fields the gem answers only through Resource#method_missing, which + # wraps nested objects in a plain Resource and raises NoMethodError + # unless the request's fields option included the field. + def custom_fields: () -> Array[Resource] + def self.me: (client client, ?options: options) -> Resource def self.find_by_id: (client client, String id, ?options: options) -> User def self.find_all: (client client, ?workspace: String?, ?per_page: Integer, ?options: options) -> Collection[User] @@ -516,6 +668,14 @@ module Asana attr_reader last_failure_at: String? attr_reader last_failure_content: String? + # Fields the gem answers only through Resource#method_missing, which + # wraps nested objects in a plain Resource and raises NoMethodError + # unless the request's fields option included the field. + def delivery_retry_count: () -> Integer + def next_attempt_after: () -> String + def failure_deletion_timestamp: () -> String + def filters: () -> Array[Resource] + def self.create: (client client, resource: String, target: String, ?options: options, **untyped data) -> Webhook def self.get_all: (client client, workspace: String, ?resource: String?, ?per_page: Integer, ?options: options) -> Collection[Webhook] end @@ -526,6 +686,11 @@ module Asana def name: %a{implicitly-returns-nil} () -> String def is_organization: %a{implicitly-returns-nil} () -> bool + # Fields the gem answers only through Resource#method_missing, which + # wraps nested objects in a plain Resource and raises NoMethodError + # unless the request's fields option included the field. + def email_domains: () -> Array[String] + def self.find_by_id: (client client, String id, ?options: options) -> Workspace def self.find_all: (client client, ?per_page: Integer, ?options: options) -> Collection[Workspace] def update: (?options: options, **untyped data) -> Workspace From f42908fd932bce7693bb110994e05514574b153b Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 19 Oct 2026 12:55:31 -0400 Subject: [PATCH 08/10] Accept per_page in asana get_tasks TasksBase.get_tasks takes per_page: 20 and sends it as the limit param (lib/asana/resources/gen/tasks_base.rb), but the signature left it out, so callers passing per_page were flagged as using an unknown keyword. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Wo4FerFK9aaxrGrmVE9ncZ --- gems/asana/0.10/_test/test.rb | 2 +- gems/asana/0.10/asana.rbs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gems/asana/0.10/_test/test.rb b/gems/asana/0.10/_test/test.rb index 895e6c5e6..9e3810882 100644 --- a/gems/asana/0.10/_test/test.rb +++ b/gems/asana/0.10/_test/test.rb @@ -19,7 +19,7 @@ sections = client.sections.get_sections_for_project(project_gid: project.gid) sections.each do |section| section.name&.strip - client.tasks.get_tasks(section: section.gid, options: { fields: ["name", "completed"] }).each do |task| + client.tasks.get_tasks(section: section.gid, per_page: 100, options: { fields: ["name", "completed"] }).each do |task| task.completed end end diff --git a/gems/asana/0.10/asana.rbs b/gems/asana/0.10/asana.rbs index 767582a89..d1e287ee9 100644 --- a/gems/asana/0.10/asana.rbs +++ b/gems/asana/0.10/asana.rbs @@ -71,7 +71,7 @@ module Asana def find_by_id: (String id, ?options: Resources::options) -> Resources::Task def find_all: (?assignee: String?, ?workspace: String?, ?project: String?, ?section: String?, ?tag: String?, ?user_task_list: String?, ?completed_since: String?, ?modified_since: String?, ?per_page: Integer, ?options: Resources::options) -> Resources::Collection[Resources::Task] def get_task: (task_gid: String, ?options: Resources::options) -> Resources::Task - def get_tasks: (?assignee: String?, ?project: String?, ?section: String?, ?workspace: String?, ?completed_since: String?, ?modified_since: String?, ?options: Resources::options) -> Resources::Collection[Resources::Task] + def get_tasks: (?assignee: String?, ?project: String?, ?section: String?, ?workspace: String?, ?completed_since: String?, ?per_page: Integer, ?modified_since: String?, ?options: Resources::options) -> Resources::Collection[Resources::Task] def get_subtasks_for_task: (task_gid: String, ?options: Resources::options) -> Resources::Collection[Resources::Task] def update_task: (task_gid: String, ?options: Resources::options, **untyped data) -> Resources::Task def add_project_for_task: (task_gid: String, ?options: Resources::options, **untyped data) -> untyped @@ -268,7 +268,7 @@ module Asana class TasksBase < Resource def self.get_task: (client client, task_gid: String, ?options: options) -> Task - def self.get_tasks: (client client, ?assignee: String?, ?project: String?, ?section: String?, ?workspace: String?, ?completed_since: String?, ?modified_since: String?, ?options: options) -> Collection[Task] + def self.get_tasks: (client client, ?assignee: String?, ?project: String?, ?section: String?, ?workspace: String?, ?completed_since: String?, ?per_page: Integer, ?modified_since: String?, ?options: options) -> Collection[Task] def self.get_subtasks_for_task: (client client, task_gid: String, ?options: options) -> Collection[Task] def self.update_task: (client client, task_gid: String, ?options: options, **untyped data) -> Task def self.add_project_for_task: (client client, task_gid: String, ?options: options, **untyped data) -> untyped From 0cec644dd867958c4aee4445bb4d853f6aa19bcf Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 19 Oct 2026 14:43:48 -0400 Subject: [PATCH 09/10] Declare Task#add_project for asana Task#add_project (lib/asana/resources/task.rb) posts to /tasks/:gid/addProject and returns true, but the signature lacked it, so callers such as checkoff's CLI move command hit NoMethod. An explicit nil for insert_after or insert_before is sent as null, so those take String?. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Wo4FerFK9aaxrGrmVE9ncZ --- gems/asana/0.10/_test/test.rb | 1 + gems/asana/0.10/asana.rbs | 1 + 2 files changed, 2 insertions(+) diff --git a/gems/asana/0.10/_test/test.rb b/gems/asana/0.10/_test/test.rb index 9e3810882..8c4403d76 100644 --- a/gems/asana/0.10/_test/test.rb +++ b/gems/asana/0.10/_test/test.rb @@ -38,6 +38,7 @@ task.start_at&.length task.assignee_section&.gid task.permalink_url.length +task.add_project(project: project.gid, section: sections.first&.gid) project.completed_by&.gid project.tasks(per_page: 50, options: { fields: ["name"] }).first&.name Asana::Resources::Task.find_by_tag(client, tag: "6").to_a diff --git a/gems/asana/0.10/asana.rbs b/gems/asana/0.10/asana.rbs index d1e287ee9..693205cd8 100644 --- a/gems/asana/0.10/asana.rbs +++ b/gems/asana/0.10/asana.rbs @@ -624,6 +624,7 @@ module Asana def dependents: (?options: options) -> Collection[Task] def projects: (?per_page: Integer, ?options: options) -> Collection[Project] def tags: (?per_page: Integer, ?options: options) -> Collection[Tag] + def add_project: (project: String, ?insert_after: String?, ?insert_before: String?, ?section: String?, ?options: options, **untyped data) -> bool end class User < UsersBase From f13172e0d8e53426649d4d1d8022347719db0a28 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 19 Oct 2026 14:44:00 -0400 Subject: [PATCH 10/10] Type asana assignee_section as a compact section Task#assignee_section comes through Resource#method_missing, which wraps the compact section ({gid, name, resource_type}) in a plain Resource that answers name and resource_type by method_missing too. Typed as bare Resource, assignee_section&.name (checkoff's MyTasks#by_my_tasks_section) was a NoMethod error, so the result is now Resource intersected with a _CompactResource interface. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Wo4FerFK9aaxrGrmVE9ncZ --- gems/asana/0.10/_test/test.rb | 1 + gems/asana/0.10/asana.rbs | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/gems/asana/0.10/_test/test.rb b/gems/asana/0.10/_test/test.rb index 8c4403d76..ce3944e4a 100644 --- a/gems/asana/0.10/_test/test.rb +++ b/gems/asana/0.10/_test/test.rb @@ -37,6 +37,7 @@ task.dependents.size task.start_at&.length task.assignee_section&.gid +task.assignee_section&.name&.upcase task.permalink_url.length task.add_project(project: project.gid, section: sections.first&.gid) project.completed_by&.gid diff --git a/gems/asana/0.10/asana.rbs b/gems/asana/0.10/asana.rbs index 693205cd8..723df5803 100644 --- a/gems/asana/0.10/asana.rbs +++ b/gems/asana/0.10/asana.rbs @@ -179,6 +179,14 @@ module Asana # Asana::Client when called directly (it answers the same verbs). type client = Asana::Client | Asana::HttpClient + # A nested object in compact form that Resource#method_missing wraps in + # a plain Resource, which answers each of its fields by method_missing. + interface _CompactResource + def gid: () -> String + def name: () -> String + def resource_type: () -> String + end + # Resource attributes are nil when the request's `fields` option leaves # them out. Those are declared `implicitly-returns-nil`; attributes the # API itself can return as null (e.g. Task#due_on) are optional types. @@ -602,7 +610,7 @@ module Asana def num_hearts: () -> Integer def start_at: () -> String? def actual_time_minutes: () -> Numeric? - def assignee_section: () -> Resource? + def assignee_section: () -> (Resource & _CompactResource)? def custom_type: () -> Resource? def custom_type_status_option: () -> Resource? def effective_memberships: () -> Array[Resource]