abt-cli 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +3 -1
  3. data/lib/abt.rb +11 -0
  4. data/lib/abt/cli.rb +25 -29
  5. data/lib/abt/cli/dialogs.rb +2 -2
  6. data/lib/abt/cli/io.rb +21 -0
  7. data/lib/abt/{help.rb → docs.rb} +8 -14
  8. data/lib/abt/{help → docs}/cli.rb +3 -3
  9. data/lib/abt/{help → docs}/markdown.rb +3 -3
  10. data/lib/abt/helpers.rb +16 -0
  11. data/lib/abt/providers/asana.rb +9 -50
  12. data/lib/abt/providers/asana/api.rb +57 -0
  13. data/lib/abt/providers/asana/base_command.rb +5 -12
  14. data/lib/abt/providers/asana/commands/clear.rb +24 -0
  15. data/lib/abt/providers/asana/commands/clear_global.rb +24 -0
  16. data/lib/abt/providers/asana/commands/current.rb +71 -0
  17. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +50 -0
  18. data/lib/abt/providers/asana/commands/init.rb +63 -0
  19. data/lib/abt/providers/asana/commands/move.rb +56 -0
  20. data/lib/abt/providers/asana/commands/pick_task.rb +48 -0
  21. data/lib/abt/providers/asana/commands/projects.rb +32 -0
  22. data/lib/abt/providers/asana/commands/start.rb +60 -0
  23. data/lib/abt/providers/asana/commands/tasks.rb +37 -0
  24. data/lib/abt/providers/asana/configuration.rb +105 -0
  25. data/lib/abt/providers/harvest.rb +9 -0
  26. data/lib/abt/version.rb +1 -1
  27. metadata +18 -15
  28. data/lib/abt/asana_client.rb +0 -53
  29. data/lib/abt/providers/asana/clear.rb +0 -24
  30. data/lib/abt/providers/asana/clear_global.rb +0 -24
  31. data/lib/abt/providers/asana/current.rb +0 -69
  32. data/lib/abt/providers/asana/harvest_link_entry_data.rb +0 -48
  33. data/lib/abt/providers/asana/init.rb +0 -62
  34. data/lib/abt/providers/asana/move.rb +0 -54
  35. data/lib/abt/providers/asana/pick_task.rb +0 -46
  36. data/lib/abt/providers/asana/projects.rb +0 -30
  37. data/lib/abt/providers/asana/start.rb +0 -22
  38. data/lib/abt/providers/asana/tasks.rb +0 -35
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 929ab52c8ebd307b90ddb7daa14f2f63eeeebda8ed9905490a13dcd2f67770da
4
- data.tar.gz: e2582299b29e39faa8d4baccaba42954e7140a447d30c3c56a92507ac00f4a09
3
+ metadata.gz: a08875790c31e73415b7e1317db284d1fe5b62ff9d2c3e66449c457c757bfe56
4
+ data.tar.gz: cb9857e53d577777371b669f5b253fe48ded2530976fc7c76982d6fef3e660e3
5
5
  SHA512:
6
- metadata.gz: dec30158debfe3e97c4969d8724d5f56d04725f0cab1aedf79840bb01ef81d9f17c5245cc538c559e47bfa245f36c5aeed8d02dc6ff21fcbae46b30f79e88c5b
7
- data.tar.gz: c74d6a7ae33ff5843385796cf21d764de61d0981501b5ac7e24691a7c61c85ae6f646abacd1f1b74417d29559ae022881969dd71598acf1190487c6b64c0af02
6
+ metadata.gz: e24c4f96b42d0eab847c1c5bdef2b79b3847c51db8b862c236f82d61217b295fcfc754933f4f998510b0377b0847f754cf3a6e2d4f937c73b95a8992ad7962f1
7
+ data.tar.gz: 2a41173f1fddbd52084dbc8943b6ebcb893a81a47ffd5a07e1194c4b518d042d6c2407c6babc8774c69822d3093b6d0d306ad864edc06302797002594bad3715
data/bin/abt CHANGED
@@ -8,7 +8,9 @@ require 'oj'
8
8
  require_relative '../lib/abt.rb'
9
9
 
10
10
  begin
11
- Abt::Cli.new(ARGV).perform
11
+ Abt::Cli.new.perform
12
+ rescue Abt::Cli::AbortError => e
13
+ abort e.message
12
14
  rescue Interrupt
13
15
  abort 'Aborted'
14
16
  end
data/lib/abt.rb CHANGED
@@ -3,3 +3,14 @@
3
3
  Dir.glob("#{File.dirname(File.absolute_path(__FILE__))}/abt/*.rb").sort.each do |file|
4
4
  require file
5
5
  end
6
+
7
+ module Abt
8
+ def self.provider_names
9
+ Providers.constants.sort.map { |constant_name| Helpers.const_to_command(constant_name) }
10
+ end
11
+
12
+ def self.provider_module(name)
13
+ const_name = Helpers.command_to_const(name)
14
+ Providers.const_get(const_name) if Providers.const_defined?(const_name)
15
+ end
16
+ end
@@ -6,22 +6,29 @@ end
6
6
 
7
7
  module Abt
8
8
  class Cli
9
+ class AbortError < StandardError; end
10
+
9
11
  include Dialogs
12
+ include Io
10
13
 
11
- attr_reader :command, :args
14
+ attr_reader :command, :args, :input, :output, :err_output
12
15
 
13
- def initialize(argv)
16
+ def initialize(argv: ARGV, input: STDIN, output: STDOUT, err_output: STDERR)
14
17
  (@command, *@args) = argv
15
18
 
16
- @args += args_from_stdin unless STDIN.isatty # Add piped arguments
19
+ @input = input
20
+ @output = output
21
+ @err_output = err_output
22
+
23
+ @args += args_from_stdin unless input.isatty # Add piped arguments
17
24
  end
18
25
 
19
- def perform(command: @command, args: @args)
26
+ def perform
20
27
  handle_global_commands!
21
28
 
22
29
  abort('No provider arguments') if args.empty?
23
30
 
24
- process_providers(command: command, args: args)
31
+ process_providers
25
32
  end
26
33
 
27
34
  def print_provider_command(provider, arg_str, description)
@@ -34,13 +41,13 @@ module Abt
34
41
  case command
35
42
  when nil
36
43
  warn("No command specified\n\n")
37
- puts(Abt::Help::Cli.content)
44
+ puts(Abt::Docs::Cli.content)
38
45
  exit
39
46
  when '--help', '-h', 'help', 'commands'
40
- puts(Abt::Help::Cli.content)
47
+ puts(Abt::Docs::Cli.content)
41
48
  exit
42
49
  when 'help-md'
43
- puts(Abt::Help::Markdown.content)
50
+ puts(Abt::Docs::Markdown.content)
44
51
  exit
45
52
  when '--version', '-v', 'version'
46
53
  puts(Abt::VERSION)
@@ -58,7 +65,7 @@ module Abt
58
65
  end
59
66
  end
60
67
 
61
- def process_providers(command:, args:)
68
+ def process_providers
62
69
  used_providers = []
63
70
  args.each do |provider_args|
64
71
  (provider, arg_str) = provider_args.split(':')
@@ -71,33 +78,22 @@ module Abt
71
78
  used_providers << provider if process_provider_command(provider, command, arg_str)
72
79
  end
73
80
 
74
- warn 'No matching providers found for command' if used_providers.empty?
81
+ warn 'No matching providers found for command' if used_providers.empty? && output.isatty
75
82
  end
76
83
 
77
- def process_provider_command(provider, command, arg_str)
78
- command_class = class_for_provider_and_command(provider, command)
84
+ def process_provider_command(provider_name, command_name, arg_str)
85
+ provider = Abt.provider_module(provider_name)
86
+ return false if provider.nil?
79
87
 
80
- return false unless command_class
88
+ command = provider.command_class(command_name)
89
+ return false if command.nil?
81
90
 
82
- if STDOUT.isatty
83
- warn "===== #{command} #{provider}#{arg_str.nil? ? '' : ":#{arg_str}"} =====".upcase
91
+ if output.isatty
92
+ warn "===== #{command_name} #{provider_name}#{arg_str.nil? ? '' : ":#{arg_str}"} =====".upcase
84
93
  end
85
94
 
86
- command_class.new(arg_str: arg_str, cli: self).call
95
+ command.new(arg_str: arg_str, cli: self).call
87
96
  true
88
97
  end
89
-
90
- def class_for_provider_and_command(provider, command)
91
- inflector = Dry::Inflector.new
92
- provider_class_name = inflector.camelize(inflector.underscore(provider))
93
-
94
- return unless Abt::Providers.const_defined? provider_class_name
95
-
96
- provider_class = Abt::Providers.const_get provider_class_name
97
- command_class_name = inflector.camelize(inflector.underscore(command))
98
- return unless provider_class.const_defined? command_class_name
99
-
100
- provider_class.const_get command_class_name
101
- end
102
98
  end
103
99
  end
@@ -4,7 +4,7 @@ module Abt
4
4
  class Cli
5
5
  module Dialogs
6
6
  def prompt(question)
7
- STDERR.print "#{question}: "
7
+ print "#{question}: "
8
8
  read_user_input.strip
9
9
  end
10
10
 
@@ -44,7 +44,7 @@ module Abt
44
44
  end
45
45
 
46
46
  def read_option_number(options_length, allow_back_option)
47
- STDERR.print "(1-#{options_length}#{allow_back_option ? ', q: back' : ''}): "
47
+ print "(1-#{options_length}#{allow_back_option ? ', q: back' : ''}): "
48
48
 
49
49
  input = read_user_input
50
50
 
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ class Cli
5
+ module Io
6
+ %i[puts print].each do |method_name|
7
+ define_method(method_name) do |*args|
8
+ output.puts(*args)
9
+ end
10
+ end
11
+
12
+ def warn(*args)
13
+ err_output.puts(*args)
14
+ end
15
+
16
+ def abort(message)
17
+ raise AbortError, message
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Dir.glob("#{File.expand_path(__dir__)}/help/*.rb").sort.each do |file|
3
+ Dir.glob("#{File.expand_path(__dir__)}/docs/*.rb").sort.each do |file|
4
4
  require file
5
5
  end
6
6
 
7
7
  module Abt
8
- module Help
8
+ module Docs
9
9
  class << self
10
10
  def examples # rubocop:disable Metrics/MethodLength
11
11
  {
@@ -28,23 +28,17 @@ module Abt
28
28
 
29
29
  private
30
30
 
31
- def commandize(string)
32
- string = string.to_s
33
- string[0] = string[0].downcase
34
- string.gsub(/([A-Z])/, '-\1').downcase
35
- end
36
-
37
31
  def provider_definitions
38
- Abt::Providers.constants.sort.each_with_object({}) do |provider_name, definition|
39
- provider_class = Abt::Providers.const_get(provider_name)
32
+ Abt.provider_names.sort.each_with_object({}) do |name, definition|
33
+ provider_module = Abt.provider_module(name)
40
34
 
41
- definition[commandize(provider_name)] = command_definitions(provider_class)
35
+ definition[name] = command_definitions(provider_module)
42
36
  end
43
37
  end
44
38
 
45
- def command_definitions(provider_class)
46
- provider_class.constants.sort.each_with_object({}) do |command_name, definition|
47
- command_class = provider_class.const_get(command_name)
39
+ def command_definitions(provider_module)
40
+ provider_module.command_names.each_with_object({}) do |name, definition|
41
+ command_class = provider_module.command_class(name)
48
42
 
49
43
  if command_class.respond_to?(:command) && command_class.respond_to?(:description)
50
44
  definition[command_class.command] = command_class.description
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Abt
4
- module Help
4
+ module Docs
5
5
  module Cli
6
6
  class << self
7
7
  def content
@@ -20,7 +20,7 @@ module Abt
20
20
  def example_commands
21
21
  lines = []
22
22
 
23
- Help.examples.each_with_index do |(title, examples), index|
23
+ Docs.examples.each_with_index do |(title, examples), index|
24
24
  lines << '' unless index.zero?
25
25
  lines << title
26
26
 
@@ -36,7 +36,7 @@ module Abt
36
36
  def providers_commands
37
37
  lines = []
38
38
 
39
- Help.providers.each_with_index do |(provider_name, commands_definition), index|
39
+ Docs.providers.each_with_index do |(provider_name, commands_definition), index|
40
40
  lines << '' unless index.zero?
41
41
  lines << "#{inflector.humanize(provider_name)}:"
42
42
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Abt
4
- module Help
4
+ module Docs
5
5
  module Markdown
6
6
  class << self
7
7
  def content
@@ -24,7 +24,7 @@ module Abt
24
24
  def example_commands
25
25
  lines = []
26
26
 
27
- Help.examples.each_with_index do |(title, commands), index|
27
+ Docs.examples.each_with_index do |(title, commands), index|
28
28
  lines << '' unless index.zero?
29
29
  lines << title
30
30
 
@@ -40,7 +40,7 @@ module Abt
40
40
  def provider_commands # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
41
41
  lines = []
42
42
 
43
- Help.providers.each_with_index do |(provider_name, commands), index|
43
+ Docs.providers.each_with_index do |(provider_name, commands), index|
44
44
  lines << '' unless index.zero?
45
45
  lines << "### #{inflector.humanize(provider_name)}"
46
46
  lines << '| Command | Description |'
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Helpers
5
+ def self.const_to_command(string)
6
+ string = string.to_s
7
+ string[0] = string[0].downcase
8
+ string.gsub(/([A-Z])/, '-\1').downcase
9
+ end
10
+
11
+ def self.command_to_const(string)
12
+ inflector = Dry::Inflector.new
13
+ inflector.camelize(inflector.underscore(string))
14
+ end
15
+ end
16
+ end
@@ -1,59 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Dir.glob("#{File.expand_path(__dir__)}/asana/*.rb").sort.each do |file|
4
- require file
5
- end
3
+ Dir.glob("#{File.expand_path(__dir__)}/asana/*.rb").sort.each { |file| require file }
4
+ Dir.glob("#{File.expand_path(__dir__)}/asana/commands/*.rb").sort.each { |file| require file }
6
5
 
7
6
  module Abt
8
7
  module Providers
9
- class Asana
10
- class << self
11
- def workspace_gid
12
- @workspace_gid ||= begin
13
- current = Abt::GitConfig.global('abt.asana.workspaceGid')
14
- if current.nil?
15
- prompt_workspace['gid']
16
- else
17
- current
18
- end
19
- end
20
- end
21
-
22
- def clear
23
- Abt::GitConfig.unset_local('abt.asana.projectGid')
24
- Abt::GitConfig.unset_local('abt.asana.taskGid')
25
- end
26
-
27
- def clear_global
28
- Abt::GitConfig.unset_global('abt.asana.workspaceGid')
29
- Abt::GitConfig.unset_global('abt.asana.accessToken')
30
- end
31
-
32
- def client
33
- Abt::AsanaClient.new(access_token: access_token)
34
- end
35
-
36
- private
37
-
38
- def prompt_workspace
39
- workspaces = client.get_paged('workspaces')
40
- if workspaces.empty?
41
- abort 'Your asana access token does not have access to any workspaces'
42
- end
43
-
44
- # TODO: Handle if there are multiple workspaces
45
- workspace = workspaces.first
46
- Abt::GitConfig.global('abt.asana.workspaceGid', workspace['gid'])
47
- workspace
48
- end
8
+ module Asana
9
+ def self.command_names
10
+ Commands.constants.sort.map { |constant_name| Helpers.const_to_command(constant_name) }
11
+ end
49
12
 
50
- def access_token
51
- Abt::GitConfig.prompt_global(
52
- 'abt.asana.accessToken',
53
- 'Please enter your personal asana access_token',
54
- 'Create a personal access token here: https://app.asana.com/0/developer-console'
55
- )
56
- end
13
+ def self.command_class(name)
14
+ const_name = Helpers.command_to_const(name)
15
+ Commands.const_get(const_name) if Commands.const_defined?(const_name)
57
16
  end
58
17
  end
59
18
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ class Api
7
+ API_ENDPOINT = 'https://app.asana.com/api/1.0'
8
+ VERBS = %i[get post patch].freeze
9
+
10
+ attr_reader :access_token
11
+
12
+ def initialize(access_token:)
13
+ @access_token = access_token
14
+ end
15
+
16
+ VERBS.each do |verb|
17
+ define_method(verb) do |*args|
18
+ request(verb, *args)['data']
19
+ end
20
+ end
21
+
22
+ def get_paged(path, query = {})
23
+ records = []
24
+
25
+ loop do
26
+ result = request(:get, path, query.merge(limit: 100))
27
+ records += result['data']
28
+ break if result['next_page'].nil?
29
+
30
+ path = result['next_page']['path'][1..-1]
31
+ end
32
+
33
+ records
34
+ end
35
+
36
+ def request(*args)
37
+ response = connection.public_send(*args)
38
+
39
+ if response.success?
40
+ Oj.load(response.body)
41
+ else
42
+ error_class = Abt::HttpError.error_class_for_status(response.status)
43
+ encoded_response_body = response.body.force_encoding('utf-8')
44
+ raise error_class, "Code: #{response.status}, body: #{encoded_response_body}"
45
+ end
46
+ end
47
+
48
+ def connection
49
+ @connection ||= Faraday.new(API_ENDPOINT) do |connection|
50
+ connection.headers['Authorization'] = "Bearer #{access_token}"
51
+ connection.headers['Content-Type'] = 'application/json'
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -2,12 +2,13 @@
2
2
 
3
3
  module Abt
4
4
  module Providers
5
- class Asana
5
+ module Asana
6
6
  class BaseCommand
7
- attr_reader :arg_str, :project_gid, :task_gid, :cli
7
+ attr_reader :arg_str, :project_gid, :task_gid, :cli, :config
8
8
 
9
9
  def initialize(arg_str:, cli:)
10
10
  @arg_str = arg_str
11
+ @config = Configuration.new(cli: cli)
11
12
 
12
13
  if arg_str.nil?
13
14
  use_current_args
@@ -45,16 +46,8 @@ module Abt
45
46
  @task_gid = nil if @task_gid.empty?
46
47
  end
47
48
 
48
- def remember_project_gid(project_gid)
49
- Abt::GitConfig.local('abt.asana.projectGid', project_gid)
50
- end
51
-
52
- def remember_task_gid(task_gid)
53
- if task_gid.nil?
54
- Abt::GitConfig.unset_local('abt.asana.taskGid')
55
- else
56
- Abt::GitConfig.local('abt.asana.taskGid', task_gid)
57
- end
49
+ def api
50
+ Abt::Providers::Asana::Api.new(access_token: config.access_token)
58
51
  end
59
52
  end
60
53
  end