Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract out shared methods into base service and sync items #108

Merged
merged 11 commits into from Mar 14, 2023
Merged
2 changes: 0 additions & 2 deletions Gemfile
Expand Up @@ -68,6 +68,4 @@ end
group :test do
gem "faker" # https://github.com/faker-ruby/faker
gem "rspec"
gem "vcr"
gem "webmock"
end
32 changes: 7 additions & 25 deletions lib/asana/service.rb
@@ -1,19 +1,14 @@
# frozen_string_literal: true

require_relative "task"
require_relative "../base/service"

module Asana
# A service class to talk to the Asana API
class Service
prepend MemoWise
include Debug

attr_reader :options

def initialize(options)
@options = options
class Service < Base::Service
def initialize(options:)
super
@personal_access_token = ENV.fetch("ASANA_PERSONAL_ACCESS_TOKEN", nil)
@last_sync_data = options[:logger].sync_data_for(friendly_name)
end

def friendly_name
Expand Down Expand Up @@ -97,13 +92,13 @@ def sync_with_primary(primary_service)
def tasks_to_sync(*)
visible_project_gids = list_projects.map { |project| project["gid"] }
task_list = visible_project_gids.map { |project_gid| list_project_tasks(project_gid) }.flatten.uniq
tasks = task_list.map { |task| Task.new(task, options) }
tasks = task_list.map { |task| Task.new(asana_task: task, options:) }
tasks_with_subtasks = tasks.select { |task| task.subtask_count.positive? }
if tasks_with_subtasks.any?
tasks_with_subtasks.each do |parent_task|
subtask_hashes = list_task_subtasks(parent_task.id)
subtask_hashes.each do |subtask_hash|
subtask = Task.new(subtask_hash, options)
subtask = Task.new(asana_task: subtask_hash, options:)
parent_task.subtasks << subtask
# Remove the subtask from the main task list
# so we don't double sync them
Expand All @@ -130,7 +125,7 @@ def add_task(external_task, parent_task_gid = nil)
response = HTTParty.post("#{base_url}/#{endpoint}", authenticated_options.merge(request_body))
if response.success?
response_body = JSON.parse(response.body)
new_task = Task.new(response_body["data"], options)
new_task = Task.new(asana_task: response_body["data"], options:)
if (section = memberships_for_task(external_task)["section"])
request_body = { body: { data: { task: new_task.id } }.to_json }
response = HTTParty.post("#{base_url}/sections/#{section}/addTask", authenticated_options.merge(request_body))
Expand Down Expand Up @@ -184,19 +179,6 @@ def update_task(asana_task, external_task)
end
end

def should_sync?(task_updated_at = nil)
return true if options[:force]

time_since_last_sync = options[:logger].last_synced(friendly_name, interval: task_updated_at.nil?)
return true if time_since_last_sync.nil?

if task_updated_at.present?
time_since_last_sync < task_updated_at
else
time_since_last_sync > min_sync_interval
end
end

private

# the minimum time we should wait between syncing tasks
Expand Down
65 changes: 23 additions & 42 deletions lib/asana/task.rb
@@ -1,36 +1,36 @@
# frozen_string_literal: true

require_relative "../base/sync_item"

module Asana
# A representation of an Asana task
class Task
prepend MemoWise
include NoteParser

attr_reader :options, :id, :title, :url, :tags, :completed, :completed_at, :project, :section, :due_date, :due_at, :updated_at, :flagged, :notes, :type, :start_date, :start_at, :subtask_count, :subtasks, :assignee, :sync_id, :debug_data

def initialize(asana_task, options)
@options = options
@id = asana_task["gid"]
@title = asana_task["name"]
@url = asana_task["permalink_url"]
@tags = default_tags
@completed = asana_task["completed"]
@completed_at = Chronic.parse(asana_task["completed_at"])
class Task < Base::SyncItem
attr_reader :project, :section, :subtask_count, :subtasks, :assignee

def initialize(asana_task:, options:)
super(sync_item: asana_task, options:)

@project = project_from_memberships(asana_task)
@due_date = Chronic.parse(asana_task["due_on"])
@due_at = Chronic.parse(asana_task["due_at"])
@updated_at = Chronic.parse(asana_task["modified_at"])
@flagged = asana_task["hearted"]
@type = asana_task["resource_type"]
@start_date = Chronic.parse(asana_task["start_on"])
@start_at = Chronic.parse(asana_task["start_at"])
@subtask_count = asana_task.fetch("num_subtasks", 0).to_i
@subtasks = []
@assignee = asana_task.dig("assignee", "gid")
end

@sync_id, @notes = parsed_notes("sync_id", asana_task["notes"])
def attribute_map
{
id: "gid",
title: "name",
url: "permalink_url",
due_date: "due_on",
flagged: "hearted",
type: "resource_type",
start_date: "start_on",
updated_at: "modified_at"
}
end

@debug_data = asana_task if @options[:debug]
def chronic_attributes
%i[completed_at due_date due_at updated_at start_date start_at]
end

def provider
Expand All @@ -49,10 +49,6 @@ def open?
!completed?
end

def friendly_title
title.strip
end

# For now, default to true
def personal?
true
Expand All @@ -79,17 +75,6 @@ def to_json(*)
}.to_json
end

def to_s
"#{provider}::Task: (#{id})#{title}"
end

# Converts the task to a format required by the primary service
def to_primary
raise "Unsupported service" unless TaskBridge.task_services.include?(options[:primary])

send("to_#{options[:primary]}".downcase.to_sym)
end

# #####
# # # ###### ##### # # # #### ###### ####
# # # # # # # # # # # #
Expand Down Expand Up @@ -130,10 +115,6 @@ def to_google(with_due: false, skip_reclaim: false)

private

def default_tags
options[:tags] + ["Asana"]
end

# try to read the project and sections from the memberships array
# If there isn't anything there, use the projects array
def project_from_memberships(asana_task)
Expand Down
37 changes: 37 additions & 0 deletions lib/base/service.rb
@@ -0,0 +1,37 @@
# frozen_string_literal: true

module Base
class Service
prepend MemoWise
include Debug

attr_reader :options

def initialize(options:)
@options = options
@last_sync_data = options[:logger].sync_data_for(friendly_name)
end

def friendly_name
raise "not implemented"
end

def should_sync?(item_updated_at = nil)
time_since_last_sync = options[:logger].last_synced(friendly_name, interval: item_updated_at.nil?)
return true if time_since_last_sync.nil?

if item_updated_at.present?
time_since_last_sync < item_updated_at
else
time_since_last_sync > min_sync_interval
end
end

private

# the default minimum time we should wait between syncing items
def min_sync_interval
raise "not implemented"
end
end
end
98 changes: 98 additions & 0 deletions lib/base/sync_item.rb
@@ -0,0 +1,98 @@
# frozen_string_literal: true

module Base
class SyncItem
prepend MemoWise
include NoteParser

attr_reader :options, :tags, :sync_id, :sync_url, :notes, :debug_data

def initialize(sync_item:, options:)
@options = options
@debug_data = sync_item if @options[:debug]
@tags = default_tags
attributes = standard_attribute_map.merge(attribute_map).compact
attributes.each do |attribute_key, attribute_value|
value = read_attribute(sync_item, attribute_value)
value = Chronic.parse(value) if chronic_attributes.include?(attribute_key)
instance_variable_set("@#{attribute_key}", value)
define_singleton_method(attribute_key.to_sym) { instance_variable_get("@#{attribute_key}") }
end

@sync_id, @sync_url, @notes = parsed_notes(keys: %w[sync_id sync_url], notes: read_attribute(sync_item, attributes[:notes]))
end

def attribute_map
raise "not implemented"
end

def chronic_attributes
[]
end

def provider
raise "not implemented"
end

def friendly_title
title.strip
end

def sync_notes
notes_with_values(notes, sync_id:, sync_url:)
end

def to_s
"#{provider}::#{self.class.name}: (#{id})#{friendly_title}"
end

# Converts the task to a format required by the primary service
def to_primary
raise "Unsupported service" unless TaskBridge.task_services.include?(options[:primary])

send("to_#{options[:primary]}".downcase.to_sym)
end

private

def default_tags
options[:tags] + [provider]
end

# Subclasses should override this
def attribute_map
raise "Not implemented"
end

def standard_attribute_map
{
id: "id",
completed_at: "completed_at",
completed: "completed",
created_at: "created_at",
due_at: "due_at",
due_date: "due_date",
flagged: "flagged",
notes: "notes",
start_at: "start_at",
start_date: "start_date",
status: "status",
title: "title",
type: "type",
updated_at: "updated_at",
url: "url"
}
end

# read attributes using applescript
def read_attribute(sync_item, attribute)
value = if sync_item.is_a? Hash
sync_item.fetch(attribute, nil)
elsif sync_item.respond_to?(attribute.to_sym)
sync_item.send(attribute.to_sym)
end
value = value.get if value.respond_to?(:get)
value == :missing_value ? nil : value
end
end
end
35 changes: 17 additions & 18 deletions lib/github/issue.rb
@@ -1,23 +1,30 @@
# frozen_string_literal: true

require_relative "../base/sync_item"

module Github
# A representation of a Github issue
class Issue
attr_reader :options, :id, :title, :url, :number, :tags, :status, :project, :is_pr, :updated_at, :debug_data
class Issue < Base::SyncItem
attr_reader :number, :tags, :project, :is_pr, :updated_at

def initialize(github_issue:, options:)
super(sync_item: github_issue, options:)

def initialize(github_issue, options)
@options = options
@url = github_issue["html_url"]
@id = github_issue["id"]
@number = github_issue["number"]
@title = github_issue["title"]
@number = read_attribute(github_issue, "number")
# Add "Github" to the labels
@tags = (default_tags + github_issue["labels"].map { |label| label["name"] }).uniq
@status = github_issue["state"]
@project = github_issue["project"] || short_repo_name(github_issue)
@is_pr = (github_issue["pull_request"] && !github_issue["pull_request"]["diff_url"].nil?) || false
@updated_at = Chronic.parse(github_issue["updated_at"])&.getlocal
@debug_data = github_issue if @options[:debug]
end

def attribute_map
{
status: "state",
tags: nil,
url: "html_url",
updated_at: nil
}
end

def provider
Expand All @@ -36,10 +43,6 @@ def friendly_title
"#{project}-##{number}: #{is_pr ? '[PR] ' : ''}#{title.strip}"
end

def to_s
"#{provider}::Issue:(#{id})#{friendly_title}"
end

# #####
# # # ###### ##### # # # #### ###### ####
# # # # # # # # # # # #
Expand All @@ -57,10 +60,6 @@ def to_omnifocus

private

def default_tags
options[:tags] + ["Github"]
end

def short_repo_name(github_issue)
github_issue["repository_url"].split("/").last
end
Expand Down