thor_enhance 0.3.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,15 +4,13 @@ require "thor"
4
4
 
5
5
  module ThorEnhance
6
6
  class Configuration
7
-
8
- # Order is important -- Ensure deoreacte is first
9
7
  HOOKERS = [DEPRECATE = :deprecate, HOOK = :hook]
8
+ ALLOWED_VALUES = [DEFAULT_ALLOWED = nil, ALLOW_ALL = :all]
10
9
 
11
10
  class << self
12
- attr_accessor :allow_changes
13
-
14
11
  def allow_changes?(raise_error: true)
15
- return true if allow_changes.nil?
12
+ return true unless defined?(@@allow_changes)
13
+ return true if @@allow_changes.nil?
16
14
 
17
15
  if raise_error
18
16
  raise BaseError, "Configuration changes are halted. Unable to change ThorEnhancements"
@@ -22,7 +20,7 @@ module ThorEnhance
22
20
  end
23
21
 
24
22
  def disallow_changes!
25
- allow_changes = true
23
+ @@allow_changes = :prevent
26
24
  end
27
25
  end
28
26
 
@@ -32,13 +30,92 @@ module ThorEnhance
32
30
  ThorEnhance::Option.thor_enhance_injection!
33
31
  ThorEnhance::Command.thor_enhance_injection!
34
32
  ThorEnhance::CommandMethod.thor_enhance_injection!
33
+
34
+ ############
35
+ # Commands #
36
+ ############
37
+ # Prepend it so we can call our run method first
38
+ ::Thor::Command.prepend ThorEnhance::CommandHook
39
+ ::Thor::Command.include ThorEnhance::Command
40
+
41
+ ##################
42
+ # Command Method #
43
+ ##################
44
+ ::Thor.include ThorEnhance::CommandMethod
45
+
46
+ ###########
47
+ # Options #
48
+ ###########
49
+ # Must be prepended because we change the arity of the initializer here
50
+ ::Thor::Option.prepend ThorEnhance::Option
51
+ ::Thor.include ThorEnhance::Base::BuildOption
52
+
53
+ ####################
54
+ # Other Injections #
55
+ ####################
56
+ ::Thor.include ThorEnhance::Base::AllowedKlass
57
+
35
58
  self.class.disallow_changes!
36
59
  end
37
60
 
61
+ def readme_enhance!(required: false, &block)
62
+ # only inject readme things if it is required client side
63
+ require "thor_enhance/thor_auto_generate_inject"
64
+ require "thor_enhance/autogenerate"
65
+
66
+ if defined?(@autogenerated_config)
67
+ raise ValidationFailed, "ReadMe Enhance has already been initialized."
68
+ end
69
+
70
+ autogenerated_config.set_default_required(required)
71
+ autogenerated_config.default
72
+ yield(autogenerated_config) if block_given?
73
+
74
+ autogenerated_config.configuration.each do |meth, object|
75
+ object.each do |name, args|
76
+ public_send(meth, name, **args)
77
+ end
78
+ end
79
+ end
80
+
81
+ def autogenerated_config
82
+ @autogenerated_config ||= Autogenerate::Configuration.new
83
+ end
84
+
38
85
  def command_method_enhance
39
86
  @command_method_enhance ||= {}
40
87
  end
41
88
 
89
+ def allowed_klasses
90
+ @klass_procs ||= []
91
+ end
92
+
93
+ def basename
94
+ @basename
95
+ end
96
+
97
+ def basename=(name)
98
+ @basename = name
99
+ end
100
+
101
+ def allowed
102
+ @allowed ||= DEFAULT_ALLOWED
103
+ end
104
+
105
+ def allowed=(value)
106
+ raise ArgumentError, "Unexpected value for `allowed =`. Given: #{value}. Expected one of #{ALLOWED_VALUES}" unless ALLOWED_VALUES.include?(value)
107
+
108
+ @allowed = value
109
+ end
110
+
111
+ def allowed?(klass)
112
+ return true if allowed == ALLOW_ALL
113
+ return true if allowed_klasses.include?(klass)
114
+
115
+ # At this point, allowed is false and not an includable klass -- dont allow
116
+ false
117
+ end
118
+
42
119
  def option_enhance
43
120
  @option_enhance ||= {
44
121
  DEPRECATE => { allowed_klasses: [Proc], behavior: :request, required: false },
@@ -47,14 +124,21 @@ module ThorEnhance
47
124
  end
48
125
 
49
126
  # Adding a new method to enhance the overall command
50
- def add_command_method_enhance(name, allowed_klasses: nil, enums: nil, required: false, repeatable: false)
127
+ def add_command_method_enhance(name, allowed_klasses: nil, enums: nil, required: false, repeatable: false, arity: 0, required_kwargs: [], optional_kwargs: [])
128
+ return if ENV["SKIP_TESTING_METHOD"]
129
+
51
130
  self.class.allow_changes?
52
131
 
53
- add_to_variable(command_method_enhance, ::Thor::Command.instance_methods, name, allowed_klasses, enums, required, repeatable)
132
+ kwargs = {}
133
+ required_kwargs.each { kwargs[_1.to_sym] = true }
134
+ optional_kwargs.each { kwargs[_1.to_sym] = false }
135
+ add_to_variable(command_method_enhance, ::Thor::Command.instance_methods, name, allowed_klasses, enums, required, repeatable, arity, kwargs)
54
136
  end
55
137
 
56
138
  # add a new flag on the command option
57
139
  def add_option_enhance(name, allowed_klasses: nil, enums: nil, required: false)
140
+ return if ENV["SKIP_TESTING_METHOD"]
141
+
58
142
  self.class.allow_changes?
59
143
 
60
144
  add_to_variable(option_enhance, ::Thor::Option.instance_methods, name, allowed_klasses, enums, required)
@@ -62,7 +146,7 @@ module ThorEnhance
62
146
 
63
147
  private
64
148
 
65
- def add_to_variable(storage, methods, name, allowed_klasses, enums, required, repeatable = false)
149
+ def add_to_variable(storage, methods, name, allowed_klasses, enums, required, repeatable = false, arity = 0, kwargs = false)
66
150
  # Reject if the name is not a Symbol or a string
67
151
  if [String, Symbol].none? { _1 === name }
68
152
  raise ArgumentError, "Invalid name type received. Received [#{name}] of type [#{name.class}]. Expected to be of type String or Symbol"
@@ -83,10 +167,19 @@ module ThorEnhance
83
167
 
84
168
  # if enums is present and not an array
85
169
  if !enums.nil? && !enums.is_a?(Array)
86
- raise ArgumentError, "Recieved enum of #{enums}. When present, it is expected to be an Array"
170
+ raise ArgumentError, "Recieved enums with #{enums}. When present, it is expected to be an Array"
171
+ end
172
+
173
+ # if allowed_klasses is present and not an array
174
+ if !allowed_klasses.nil? && !allowed_klasses.is_a?(Array)
175
+ raise ArgumentError, "Recieved allowed_klasses with #{allowed_klasses}. When present, it is expected to be an Array"
176
+ end
177
+
178
+ if arity < 0
179
+ raise ArgumentError, "Recieved arity with #{arity}. When present, it is expected to be greater than 0"
87
180
  end
88
181
 
89
- storage[name.to_sym] = { allowed_klasses: allowed_klasses, enums: enums, required: required, repeatable: repeatable }
182
+ storage[name.to_sym] = { allowed_klasses: allowed_klasses, enums: enums, required: required, repeatable: repeatable, kwargs: kwargs, arity: arity }
90
183
  end
91
184
  end
92
185
  end
@@ -5,30 +5,34 @@ require "thor"
5
5
  module ThorEnhance
6
6
  module Option
7
7
  def self.thor_enhance_injection!
8
- return false unless ThorEnhance::Configuration.allow_changes?
9
-
10
8
  # Create getter method for the enhance instance variable
11
9
  ThorEnhance.configuration.option_enhance.each do |name, object|
12
10
  define_method(name) { instance_variable_get("@#{name}") }
13
11
  end
14
-
15
- ::Thor::Option.include ThorEnhance::Option
16
12
  end
17
13
 
18
- def initialize(name, options = {})
19
- super
14
+ # Monkey patched initializer
15
+ # Thor Option initializer only takes (name, options) as arguments
16
+ # Thor::Option.new is only called from `build_option` which gets monkey patched in thor_enhance/base
17
+ def initialize(name, options = {}, klass = nil)
18
+ super(name, options)
20
19
 
21
- thor_enhance_definitions(options)
20
+ thor_enhance_definitions(options, klass)
22
21
  end
23
22
 
24
- def thor_enhance_definitions(options)
23
+ def thor_enhance_definitions(options, klass)
24
+ return nil unless ThorEnhance.configuration.allowed?(klass)
25
+
25
26
  ThorEnhance.configuration.option_enhance.each do |name, object|
26
- if options[name.to_sym].nil? && object[:required]
27
- raise RequiredOption, "#{@name} does not have required option #{name}. Please add it to the option"
27
+ # When disabled, we do not do the required check, if present, it is still required to be a valid option otherwise
28
+ unless ::Thor.__thor_enhance_definition == ThorEnhance::CommandMethod::ClassMethods::THOR_ENHANCE_DISABLE
29
+ if options[name.to_sym].nil? && object[:required]
30
+ raise RequiredOption, "#{@name} does not have required option #{name}. Please add it to the option"
31
+ end
28
32
  end
29
33
 
30
34
  value = options[name.to_sym]
31
- if value.nil? && object[:required] == false
35
+ if value.nil? # This can be nil here because we have already done a required check
32
36
  # no op when it is nil and not required
33
37
  elsif !object[:enums].nil?
34
38
  unless object[:enums].include?(value)
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor_enhance"
4
+
5
+ module ThorEnhance
6
+ class Sample < Thor
7
+ thor_enhance_allow!
8
+
9
+ class SubCommand < Thor
10
+ thor_enhance_allow!
11
+
12
+ desc "innard", "Validate that a subcommand works. This description can be as long as you want it to be."
13
+ long_desc "Wow, This longer description will take precedence over the desc above. This is what will be shown in the readme autogenerated page. Try me out!"
14
+ example "innard --count 5", desc: "Innard sub command with a count of 5"
15
+ example "innard --count 35", desc: "Innard sub command with a count of 35"
16
+ header name: "Deprecation warning", desc: "This command will get deprecated in the next major version"
17
+ when_should_i_use_this "Use sub command task to validate that subocommands work as expected"
18
+ method_option :count, type: :numeric, readme: :skip
19
+ def innard;end;
20
+ end
21
+
22
+ desc "sub", "Thor sub command validation for thor enhance"
23
+ subcommand "sub", SubCommand
24
+
25
+ desc "sample", "This Sample command does a lot of nothing"
26
+ when_should_i_use_this <<~README
27
+ Have you ever wanted your code to be useless?
28
+ Well, this command does absolutely nothing.
29
+ This output is to say that this command does absolutely nothing
30
+ README
31
+ how_does_this_help "Honestly, this does not help at all", tag: "h4"
32
+ how_does_this_help "But its cool because it is a repatable command", tag: "h1"
33
+ example "sample", desc: "yo yo ma"
34
+ example "sample --boolean", desc: "yo yo ma"
35
+ method_option :boolean, aliases: "-b", type: :boolean, desc: "Just a normal boolean", readme: :important
36
+ def sample
37
+ Kernel.puts "Executed Sample method"
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ ####################
4
+ #
5
+ # Injects a method directly into the thor base class
6
+ # This allows the developer to have this convenience method
7
+ # Just by utilizing ThorEnhance
8
+ #
9
+ ####################
10
+ require "thor"
11
+
12
+ class Thor
13
+ desc "thor_enhance_autogenerate", "Auto Generate ReadMe material for your Thor commands"
14
+ method_option :subcommand, aliases: "-s", type: :string, repeatable: true, desc: "When provided, autogeneration will execute on the subcommand"
15
+ method_option :command, aliases: "-c", type: :string, desc: "When provided, autogeneration will occur only on this method. Note: When used with subcommand, method must exist on subcommand"
16
+ method_option :basename, aliases: "-b", type: :string, desc: "The name of the file that executes the Thor script"
17
+ method_option :generated_root, aliases: "-r", type: :string, default: File.expand_path("generated_readme"), desc: "The root location to store autogenerated files"
18
+ method_option :apply, aliases: "-a", type: :boolean, desc: "When comfortable with the changes made, enabling apply will save changes to generated files"
19
+
20
+ def thor_enhance_autogenerate
21
+ require "thor_enhance/autogenerate"
22
+ basename = options.basename || ThorEnhance.basename || File.basename($0)
23
+
24
+ result = ThorEnhance::Autogenerate.execute!(options: options, root: self.class, basename: basename)
25
+
26
+ if result[:status] == :pass
27
+ __auto_generate_success!(result[:saved_status])
28
+ else
29
+ __auto_generate_fail!(result[:msg_array])
30
+ end
31
+ end
32
+
33
+ no_tasks do
34
+ def __auto_generate_success!(statuses)
35
+ if statuses.all? { _1[:apply] == true }
36
+ say "Readme changes are enabled", [:green, :bold], true
37
+ else
38
+ say "Readme changes are not enabled. To apply changes, add `--apply` to the command", [:on_white, :black], true
39
+ end
40
+ statuses.each do |status|
41
+ case status[:diff]
42
+ when :new
43
+ say " Added : #{status[:path]}", [:green, :bold], true
44
+ when :same
45
+ say " No Change: #{status[:path]}", [:yellow, :bold], true
46
+ when :overwite
47
+ say " Changes : #{status[:path]}", [:cyan, :bold], true
48
+ else
49
+ say " : #{status[:path]}", [:bold], true
50
+ end
51
+ end
52
+ end
53
+
54
+ def __auto_generate_fail!(msg_array)
55
+ say_error set_color("*********************** FAILED OPERATION ***********************", :red, :bold)
56
+ say_error set_color("FAIL: Unable to continue", :red, :bold)
57
+ msg_array.each do |line|
58
+ say_error set_color("FAIL: #{line}", :red, :bold)
59
+ end
60
+ say_error set_color("*********************** FAILED OPERATION ***********************", :red, :bold)
61
+ exit 1
62
+ end
63
+ end
64
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module ThorEnhance
4
4
  class Tree
5
- DEFAULT_IGNORE_COMMANDS = ["help"]
5
+ DEFAULT_IGNORE_COMMANDS = ["help", "thor_enhance_autogenerate"]
6
6
 
7
7
  def self.add_ignore_commands(command)
8
8
  return false if ignore_commands.include?(command)
@@ -20,6 +20,8 @@ module ThorEnhance
20
20
  end
21
21
 
22
22
  def self.tree(base:, parent: nil)
23
+ raise TreeFailure, "#{base} does not respond to all_commands. Unable to continue" unless base.respond_to?(:all_commands)
24
+
23
25
  base.all_commands.map do |k, command|
24
26
  next if ignore_commands.include?(k)
25
27
 
@@ -36,7 +38,7 @@ module ThorEnhance
36
38
  @parent = parent
37
39
  @base = base
38
40
  @command = command
39
- @children = []
41
+ @children = {}
40
42
 
41
43
  if !base.subcommand_classes.nil? && base.subcommand_classes[command.name]
42
44
  @children = self.class.tree(parent: self, base: base.subcommand_classes[command.name])
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ThorEnhance
4
- VERSION = "0.3.0"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/thor_enhance.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support"
4
+
5
+ require "thor_enhance/base"
3
6
  require "thor_enhance/command"
4
7
  require "thor_enhance/command_hook"
5
8
  require "thor_enhance/command_method"
@@ -9,10 +12,16 @@ require "thor_enhance/tree"
9
12
 
10
13
  module ThorEnhance
11
14
  class BaseError < StandardError; end
12
- class OptionNotAllowed < StandardError; end
13
- class ValidationFailed < StandardError; end
14
- class RequiredOption < StandardError; end
15
- class OptionDeprecated < StandardError; end
15
+ class OptionNotAllowed < BaseError; end
16
+ class ValidationFailed < BaseError; end
17
+ class RequiredOption < BaseError; end
18
+ class OptionDeprecated < BaseError; end
19
+ class TreeFailure < BaseError; end
20
+ class AutoGenerateFailure < BaseError; end
21
+
22
+ def self.basename
23
+ configuration.basename
24
+ end
16
25
 
17
26
  def self.configure
18
27
  yield configuration if block_given?
data/thor_enhance.gemspec CHANGED
@@ -30,9 +30,5 @@ Gem::Specification.new do |spec|
30
30
  spec.require_paths = ["lib"]
31
31
 
32
32
  spec.add_dependency "thor", "~> 1.3"
33
-
34
- spec.add_development_dependency "pry-byebug"
35
- spec.add_development_dependency "rake", "~> 12.0"
36
- spec.add_development_dependency "rspec", "~> 3.0"
37
- spec.add_development_dependency "simplecov", "~> 0.17.0"
33
+ spec.add_dependency "activesupport", ">=6"
38
34
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: thor_enhance
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Taylor
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-11-22 00:00:00.000000000 Z
11
+ date: 2023-12-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -25,61 +25,19 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.3'
27
27
  - !ruby/object:Gem::Dependency
28
- name: pry-byebug
28
+ name: activesupport
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :development
33
+ version: '6'
34
+ type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: rake
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '12.0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '12.0'
55
- - !ruby/object:Gem::Dependency
56
- name: rspec
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: '3.0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: '3.0'
69
- - !ruby/object:Gem::Dependency
70
- name: simplecov
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: 0.17.0
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: 0.17.0
40
+ version: '6'
83
41
  description: Have you ever wanted your thor commands to tell a story of what they
84
42
  are? Or have you ever wanted to deprecate an option over time easily? ThorEnhance
85
43
  allows to to annote methods and commands in a human readable way
@@ -101,20 +59,36 @@ files:
101
59
  - README.md
102
60
  - bin/console
103
61
  - bin/setup
62
+ - bin/thor_enhance
104
63
  - docker-compose.yml
64
+ - docs/autogenerate/Readme.md
105
65
  - docs/command.md
106
66
  - docs/hooks.md
67
+ - docs/initialization.md
107
68
  - docs/method_option.md
108
69
  - docs/tree.md
109
70
  - examples/basic_example.md
110
71
  - examples/example_with_subcommand.md
111
72
  - examples/hooks.md
112
73
  - lib/thor_enhance.rb
74
+ - lib/thor_enhance/autogenerate.rb
75
+ - lib/thor_enhance/autogenerate/command.rb
76
+ - lib/thor_enhance/autogenerate/configuration.rb
77
+ - lib/thor_enhance/autogenerate/option.rb
78
+ - lib/thor_enhance/autogenerate/templates/aggregate_options.rb.erb
79
+ - lib/thor_enhance/autogenerate/templates/command.rb.erb
80
+ - lib/thor_enhance/autogenerate/templates/footer.rb.erb
81
+ - lib/thor_enhance/autogenerate/templates/option.rb.erb
82
+ - lib/thor_enhance/autogenerate/templates/root.rb.erb
83
+ - lib/thor_enhance/autogenerate/validate.rb
84
+ - lib/thor_enhance/base.rb
113
85
  - lib/thor_enhance/command.rb
114
86
  - lib/thor_enhance/command_hook.rb
115
87
  - lib/thor_enhance/command_method.rb
116
88
  - lib/thor_enhance/configuration.rb
117
89
  - lib/thor_enhance/option.rb
90
+ - lib/thor_enhance/sample.rb
91
+ - lib/thor_enhance/thor_auto_generate_inject.rb
118
92
  - lib/thor_enhance/tree.rb
119
93
  - lib/thor_enhance/version.rb
120
94
  - thor_enhance.gemspec