escort 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +1 -0
  3. data/.irbrc +1 -0
  4. data/.rspec +3 -0
  5. data/.rvmrc +22 -0
  6. data/README.md +31 -56
  7. data/TODO.md +152 -0
  8. data/escort.gemspec +6 -2
  9. data/examples/1_1_basic.rb +15 -0
  10. data/examples/1_2_basic_requires_arguments.rb +15 -0
  11. data/examples/2_2_command.rb +18 -0
  12. data/examples/2_2_command_requires_arguments.rb +20 -0
  13. data/examples/2_3_nested_commands.rb +26 -0
  14. data/examples/3_validations.rb +31 -0
  15. data/examples/4_1_config_file.rb +42 -0
  16. data/examples/argument_handling/basic.rb +12 -0
  17. data/examples/argument_handling/basic_command.rb +18 -0
  18. data/examples/argument_handling/no_arguments.rb +14 -0
  19. data/examples/argument_handling/no_arguments_command.rb +20 -0
  20. data/examples/basic/app.rb +16 -0
  21. data/examples/command_aliases/app.rb +31 -0
  22. data/examples/config_file/.apprc2 +16 -0
  23. data/examples/config_file/app.rb +78 -0
  24. data/examples/config_file/sub_commands.rb +35 -0
  25. data/examples/default_command/app.rb +20 -0
  26. data/examples/sub_commands/app.rb +18 -0
  27. data/examples/validation_basic/app.rb +31 -0
  28. data/lib/escort.rb +51 -4
  29. data/lib/escort/action_command/base.rb +79 -0
  30. data/lib/escort/action_command/escort_utility_command.rb +53 -0
  31. data/lib/escort/app.rb +89 -36
  32. data/lib/escort/arguments.rb +20 -0
  33. data/lib/escort/auto_options.rb +71 -0
  34. data/lib/escort/error/error.rb +50 -0
  35. data/lib/escort/formatter/borderless_table.rb +102 -0
  36. data/lib/escort/formatter/common.rb +58 -0
  37. data/lib/escort/formatter/default_help_formatter.rb +106 -0
  38. data/lib/escort/formatter/options.rb +13 -0
  39. data/lib/escort/formatter/string_splitter.rb +30 -0
  40. data/lib/escort/formatter/terminal.rb +22 -0
  41. data/lib/escort/formatter/terminal_formatter.rb +52 -0
  42. data/lib/escort/global_pre_parser.rb +43 -0
  43. data/lib/escort/logger.rb +75 -0
  44. data/lib/escort/option_parser.rb +145 -0
  45. data/lib/escort/setup/configuration/generator.rb +75 -0
  46. data/lib/escort/setup/configuration/instance.rb +33 -0
  47. data/lib/escort/setup/configuration/loader.rb +37 -0
  48. data/lib/escort/setup/configuration/locator/base.rb +19 -0
  49. data/lib/escort/setup/configuration/locator/descending_to_home.rb +23 -0
  50. data/lib/escort/setup/configuration/merge_tool.rb +38 -0
  51. data/lib/escort/setup/configuration/reader.rb +36 -0
  52. data/lib/escort/setup/configuration/writer.rb +44 -0
  53. data/lib/escort/setup/dsl/action.rb +17 -0
  54. data/lib/escort/setup/dsl/command.rb +74 -0
  55. data/lib/escort/setup/dsl/config_file.rb +13 -0
  56. data/lib/escort/setup/dsl/global.rb +84 -0
  57. data/lib/escort/setup/dsl/options.rb +25 -0
  58. data/lib/escort/setup/dsl/validations.rb +25 -0
  59. data/lib/escort/setup_accessor.rb +194 -0
  60. data/lib/escort/trollop.rb +15 -4
  61. data/lib/escort/utils.rb +21 -0
  62. data/lib/escort/validator.rb +42 -0
  63. data/lib/escort/version.rb +1 -1
  64. data/spec/helpers/execute_action_matcher.rb +21 -0
  65. data/spec/helpers/exit_with_code_matcher.rb +21 -0
  66. data/spec/helpers/give_option_to_action_with_value_matcher.rb +22 -0
  67. data/spec/integration/basic_options_spec.rb +53 -0
  68. data/spec/integration/basic_spec.rb +24 -0
  69. data/spec/lib/escort/formatter/string_splitter_spec.rb +38 -0
  70. data/spec/lib/escort/setup_accessor_spec.rb +42 -0
  71. data/spec/lib/escort/utils_spec.rb +30 -0
  72. data/spec/spec_helper.rb +22 -0
  73. metadata +128 -16
  74. data/lib/escort/command.rb +0 -23
  75. data/lib/escort/dsl.rb +0 -20
  76. data/lib/escort/global_dsl.rb +0 -11
@@ -0,0 +1,75 @@
1
+ module Escort
2
+ module Setup
3
+ module Configuration
4
+ class Generator
5
+ attr_reader :setup
6
+
7
+ def initialize(setup)
8
+ @setup = setup
9
+ end
10
+
11
+ def default_data
12
+ config_hash = init_config_hash
13
+ options([], config_hash[:global][:options]) #global options
14
+ options_for_commands(setup.canonical_command_names_for([]), [], config_hash[:global][:commands])
15
+ config_hash
16
+ end
17
+
18
+ private
19
+
20
+ def options_for_commands(commands, context, options = {})
21
+ commands.each do |command_name|
22
+ command_name = command_name.to_sym
23
+ #next if command_name == :escort
24
+ options[command_name] = {}
25
+ options[command_name][:options] = {}
26
+ options[command_name][:commands] = {}
27
+ current_context = context.dup
28
+ current_context << command_name
29
+ options(current_context, options[command_name][:options]) #command_options
30
+ options_for_commands(setup.canonical_command_names_for(current_context), current_context, options[command_name][:commands])
31
+ end
32
+ end
33
+
34
+ def init_config_hash
35
+ {
36
+ :global => {
37
+ :options => {},
38
+ :commands => {}
39
+ },
40
+ :user => {}
41
+ }
42
+ end
43
+
44
+ def options(context = [], options = {})
45
+ command_names = setup.command_names_for(context)
46
+ parser = init_parser(command_names)
47
+ parser = add_setup_options_to(parser, context)
48
+ options.merge!(default_option_values(parser))
49
+ end
50
+
51
+ def init_parser(stop_words)
52
+ Trollop::Parser.new.tap do |parser|
53
+ parser.stop_on(stop_words) # make sure we halt parsing if we see a command
54
+ end
55
+ end
56
+
57
+ def add_setup_options_to(parser, context = [])
58
+ setup.options_for(context).each do |name, opts|
59
+ parser.opt name, opts[:desc] || "", opts.dup #have to make sure to dup here, otherwise opts might get stuff added to it and it
60
+ #may cause problems later, e.g. adds default value and when parsed again trollop barfs
61
+ end
62
+ parser
63
+ end
64
+
65
+ def default_option_values(parser)
66
+ hash = {}
67
+ parser.specs.each_pair do |key, data|
68
+ hash[key] = data[:default] || nil
69
+ end
70
+ hash
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,33 @@
1
+ module Escort
2
+ module Setup
3
+ module Configuration
4
+ class Instance
5
+ class << self
6
+ def blank
7
+ self.new(nil, {})
8
+ end
9
+ end
10
+
11
+ attr_reader :data, :path
12
+
13
+ def initialize(path, hash)
14
+ @data = hash
15
+ @path = path
16
+ end
17
+
18
+ def blank?
19
+ data.empty?
20
+ end
21
+ alias empty? blank?
22
+
23
+ def global
24
+ data[:global] || {}
25
+ end
26
+
27
+ def user
28
+ data[:user] || {}
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,37 @@
1
+ module Escort
2
+ module Setup
3
+ module Configuration
4
+ class Loader
5
+ attr_reader :setup, :auto_options
6
+
7
+ def initialize(setup, auto_options)
8
+ @setup = setup
9
+ @auto_options = auto_options
10
+ end
11
+
12
+ def configuration
13
+ if setup.has_config_file?
14
+ Writer.new(config_path, Generator.new(setup).default_data).write if setup.config_file_autocreatable?
15
+ Reader.new(config_path).read
16
+ else
17
+ Instance.blank
18
+ end
19
+ end
20
+
21
+ def default_config_path
22
+ @default_config_path ||= File.join(File.expand_path(ENV["HOME"]), config_filename)
23
+ end
24
+
25
+ private
26
+
27
+ def config_filename
28
+ @config_filename ||= setup.config_file
29
+ end
30
+
31
+ def config_path
32
+ @config_path ||= (auto_options.non_default_config_path || Locator::DescendingToHome.new(config_filename).locate || default_config_path)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,19 @@
1
+ module Escort
2
+ module Setup
3
+ module Configuration
4
+ module Locator
5
+ class Base
6
+ attr_reader :filename
7
+
8
+ def initialize(filename)
9
+ @filename = filename
10
+ end
11
+
12
+ def locate
13
+ raise "Must be defined in child class"
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,23 @@
1
+ require 'pathname'
2
+
3
+ module Escort
4
+ module Setup
5
+ module Configuration
6
+ module Locator
7
+ class DescendingToHome < Base
8
+ def locate
9
+ return nil unless filename
10
+ possible_configs = []
11
+ Pathname.new(Dir.pwd).descend do |path|
12
+ filepath = File.join(path, filename)
13
+ if File.exists?(filepath)
14
+ possible_configs << filepath
15
+ end
16
+ end
17
+ possible_configs.empty? ? nil : possible_configs.last
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,38 @@
1
+ module Escort
2
+ module Setup
3
+ module Configuration
4
+ class MergeTool
5
+ def initialize(new_config_hash, old_config_hash)
6
+ @new_config_hash = new_config_hash
7
+ @old_config_hash = old_config_hash
8
+ end
9
+
10
+ def config_hash
11
+ merge_config(@new_config_hash, @old_config_hash)
12
+ end
13
+
14
+ private
15
+
16
+ def merge_config(new_hash, old_hash)
17
+ new_hash.keys.each do |key|
18
+ new_config_value = new_hash[key]
19
+ old_config_value = old_hash[key]
20
+
21
+ if new_config_value.kind_of?(Hash) && old_config_value.kind_of?(Hash)
22
+ new_hash[key] = merge_config(new_config_value, old_config_value)
23
+ elsif old_config_value.nil?
24
+ new_hash[key] = new_config_value
25
+ else
26
+ new_hash[key] = old_config_value
27
+ end
28
+
29
+ if key == :user
30
+ new_hash[key] = old_config_value
31
+ end
32
+ end
33
+ new_hash
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,36 @@
1
+ require 'json'
2
+
3
+ module Escort
4
+ module Setup
5
+ module Configuration
6
+ class Reader
7
+ attr_reader :path
8
+
9
+ def initialize(path)
10
+ @path = path
11
+ end
12
+
13
+ def read
14
+ data = {}
15
+ data = load_config_at_path if path
16
+ Instance.new(path, data)
17
+ end
18
+
19
+ private
20
+
21
+ def load_config_at_path
22
+ data = {}
23
+ begin
24
+ json = File.read(path)
25
+ hash = JSON.parse(json)
26
+ data = Escort::Utils.symbolize_keys(hash)
27
+ rescue => e
28
+ error_logger.warn { "Found config at #{path}, but failed to load it, perhaps your JSON syntax is invalid. Attempting to continue without..." }
29
+ error_logger.error(e)
30
+ end
31
+ data
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,44 @@
1
+ require 'json'
2
+
3
+ module Escort
4
+ module Setup
5
+ module Configuration
6
+ class Writer
7
+ attr_reader :path, :data
8
+
9
+ def initialize(path, data)
10
+ @path = path
11
+ @data = data
12
+ end
13
+
14
+ def write
15
+ if path && !File.exists?(path)
16
+ save_to_file
17
+ Instance.new(path, data)
18
+ else
19
+ Instance.blank
20
+ end
21
+ end
22
+
23
+ def update
24
+ current_data = {}
25
+ if File.exists? path
26
+ current_data = Reader.new(path).read.data
27
+ end
28
+ @data = Escort::Setup::Configuration::MergeTool.new(data, current_data).config_hash
29
+ save_to_file
30
+ Instance.new(path, data)
31
+ end
32
+
33
+ private
34
+
35
+ def save_to_file
36
+ current_path = File.expand_path(path)
37
+ File.open(current_path,"w") do |f|
38
+ f.puts JSON.pretty_generate(data)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,17 @@
1
+ module Escort
2
+ module Setup
3
+ module Dsl
4
+ class Action
5
+ class << self
6
+ def action(command_name, instance, &block)
7
+ instance.instance_variable_set(:"@block", block)
8
+ end
9
+ end
10
+
11
+ def initialize(command_name = :global)
12
+ @command_name = command_name
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,74 @@
1
+ module Escort
2
+ module Setup
3
+ module Dsl
4
+ class Command
5
+ def initialize(name, options = {}, &block)
6
+ reset(name)
7
+ @name = name
8
+ @description = options[:description] || options[:desc] || ""
9
+ @aliases = [options[:aliases] || []].flatten
10
+ @requires_arguments ||= options[:requires_arguments]
11
+ block.call(self) if block_given?
12
+ rescue => e
13
+ raise Escort::ClientError.new("Problem with syntax of command '#{@name}' configuration", e)
14
+ end
15
+
16
+ def options(&block)
17
+ Options.options(@name, @options, &block)
18
+ end
19
+
20
+ def action(&block)
21
+ Action.action(@name, @action, &block)
22
+ end
23
+
24
+ def validations(&block)
25
+ Validations.validations(@name, @validations, &block)
26
+ end
27
+
28
+ def command(name, options = {}, &block)
29
+ options[:requires_arguments] = @requires_arguments
30
+ command = Command.new(name.to_sym, options, &block)
31
+ aliases = [options[:aliases] || []].flatten + [name]
32
+ aliases.each do |name|
33
+ @commands[name.to_sym] = command
34
+ end
35
+ end
36
+
37
+ def requires_arguments(boolean = true)
38
+ #TODO raise a client error if the value is anything besides true or false
39
+ @requires_arguments = boolean
40
+ @commands.each do |command|
41
+ command.requires_arguments(boolean)
42
+ end
43
+ end
44
+
45
+ def conflicting_options(*command_names)
46
+ @conflicts << command_names
47
+ end
48
+
49
+ def summary(summary)
50
+ @summary = summary
51
+ end
52
+
53
+ def description(description)
54
+ @description = description
55
+ end
56
+
57
+ private
58
+
59
+ def reset(name)
60
+ @requires_arguments = false
61
+ @commands = {}
62
+ @options = Options.new(name)
63
+ @action = Action.new(name)
64
+ @validations = Validations.new(name)
65
+ @name = nil
66
+ @description = nil
67
+ @aliases = []
68
+ @conflicts = []
69
+ @summary = nil
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,13 @@
1
+ module Escort
2
+ module Setup
3
+ module Dsl
4
+ class ConfigFile
5
+ def initialize(name, options = {})
6
+ @name = name
7
+ #TODO possibly raise error if autocreate is not a boolean
8
+ @autocreate = options[:autocreate] || false
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,84 @@
1
+ module Escort
2
+ module Setup
3
+ module Dsl
4
+ class Global
5
+ def initialize(&block)
6
+ reset
7
+ block.call(self)
8
+ rescue => e
9
+ raise Escort::ClientError.new("Problem with syntax of global configuration", e)
10
+ end
11
+
12
+ def options(&block)
13
+ Options.options(:global, @options, &block)
14
+ end
15
+
16
+ def action(&block)
17
+ Action.action(:global, @action, &block)
18
+ end
19
+
20
+ def validations(&block)
21
+ Validations.validations(:global, @validations, &block)
22
+ end
23
+
24
+ def command(name, options = {}, &block)
25
+ options[:requires_arguments] = @requires_arguments
26
+ command = Command.new(name.to_sym, options, &block)
27
+ aliases = [options[:aliases] || []].flatten + [name]
28
+ aliases.each do |name|
29
+ @commands[name.to_sym] = command
30
+ end
31
+ end
32
+
33
+ def requires_arguments(boolean = true)
34
+ #TODO raise a client error if the value is anything besides true or false
35
+ @requires_arguments = boolean
36
+ @commands.each do |command|
37
+ command.requires_arguments(boolean)
38
+ end
39
+ end
40
+
41
+ def config_file(name, options = {})
42
+ @config_file = ConfigFile.new(name, options)
43
+ end
44
+
45
+ def version(version)
46
+ @version = version
47
+ end
48
+
49
+ def summary(summary)
50
+ @summary = summary
51
+ end
52
+
53
+ def description(description)
54
+ @description = description
55
+ end
56
+
57
+
58
+ def conflicting_options(*command_names)
59
+ @conflicts << command_names
60
+ end
61
+
62
+ private
63
+
64
+ def reset
65
+ @version = nil
66
+ @summary = nil
67
+ @description = nil
68
+ @commands = {}
69
+ @requires_arguments = false
70
+ @options = Options.new
71
+ @action = Action.new
72
+ @validations = Validations.new
73
+ @config_file = nil
74
+ @conflicts = []
75
+ end
76
+
77
+ def set_instance_variable_on(instance, instance_variable, value)
78
+ instance_variable_symbol = :"@#{instance_variable.to_s}"
79
+ instance.instance_variable_set(instance_variable_symbol, value)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end