thor_enhance 0.3.0 → 0.5.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.
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThorEnhance
4
+ module Autogenerate
5
+ class Configuration
6
+ attr_reader :question_headers, :custom_headers, :configuration, :readme_empty_group, :readme_skip_key, :readme_enums
7
+
8
+ DEFAULT_SKIP_KEY = :skip
9
+
10
+ def initialize(required: false)
11
+ @required = required
12
+ @configuration = { add_option_enhance: {}, add_command_method_enhance: {} }
13
+ @readme_enums = []
14
+ @custom_headers = []
15
+ @question_headers = []
16
+ end
17
+
18
+ def set_default_required(value)
19
+ @required = value
20
+ end
21
+
22
+ def default
23
+ ThorEnhance::Configuration.allow_changes?
24
+
25
+ example
26
+ header
27
+ title
28
+ readme
29
+ end
30
+
31
+ def title(required: false)
32
+ ThorEnhance::Configuration.allow_changes?
33
+
34
+ required = required.nil? ? @required : required
35
+ configuration[:add_command_method_enhance][:title] = { repeatable: false, required: required }
36
+ end
37
+
38
+ def example(required: nil, repeatable: true)
39
+ ThorEnhance::Configuration.allow_changes?
40
+
41
+ required = required.nil? ? @required : required
42
+ configuration[:add_command_method_enhance][:example] = { repeatable: repeatable, required: required, required_kwargs: [:desc] }
43
+ end
44
+
45
+ def header
46
+ ThorEnhance::Configuration.allow_changes?
47
+
48
+ configuration[:add_command_method_enhance][:header] = { repeatable: true, required: false, required_kwargs: [:name, :desc], optional_kwargs: [:tag] }
49
+ end
50
+
51
+ def custom_header(name, question: false, repeatable: false, required: false)
52
+ ThorEnhance::Configuration.allow_changes?
53
+
54
+ raise ArgumentError, "Custom Header name must be unique. #{name} is already defined as a custom header. " if custom_headers.include?(name.to_sym)
55
+
56
+ custom_headers << name.to_sym
57
+ question_headers << name.to_sym if question
58
+ configuration[:add_command_method_enhance][name.to_sym] = { repeatable: repeatable, required: required, optional_kwargs: [:tag] }
59
+ end
60
+
61
+ def readme(required: nil, empty_group: :unassigned, skip_key: DEFAULT_SKIP_KEY, enums: [:important, :advanced, skip_key.to_sym].compact)
62
+ ThorEnhance::Configuration.allow_changes?
63
+
64
+ @readme_empty_group = empty_group
65
+ @readme_skip_key = skip_key
66
+ @readme_enums = enums.map(&:to_sym)
67
+ required = required.nil? ? @required : required
68
+ configuration[:add_option_enhance][:readme] = { enums: enums, required: required }
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module ThorEnhance
6
+ module Autogenerate
7
+ class Option
8
+ TEMPLATE_ERB = "#{File.dirname(__FILE__)}/templates/option.rb.erb"
9
+ OPTION_TEMPLATE = ERB.new(File.read(TEMPLATE_ERB))
10
+
11
+ attr_reader :name, :option
12
+
13
+ def initialize(name:, option:)
14
+ @name = name
15
+ @option = option
16
+ end
17
+
18
+ def template_text
19
+ text = []
20
+ text << "# What: #{option.description}"
21
+ text << "# Type: #{option.type}"
22
+ text << "# Required: #{option.required}"
23
+ text << "# Allowed Inputs: #{option.enum}" if option.enum
24
+ text << invocations.map { "#{_1}"}.join(" | ")
25
+
26
+ text.join("\n")
27
+ end
28
+
29
+ def invocations
30
+ base = [option.switch_name] + option.aliases
31
+ if option.type == :boolean
32
+ counter = option.switch_name.sub("--", "--no-")
33
+ base << counter
34
+ end
35
+
36
+ base
37
+ end
38
+
39
+ def readme_type
40
+ option.readme || ThorEnhance.configuration.autogenerated_config.readme_empty_group
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,12 @@
1
+ <% ThorEnhance.configuration.autogenerated_config.readme_enums.each_with_index do |group_name, index| %>
2
+ <% next if method_options[group_name].nil? %>
3
+ <details <%= "open" if index == 0 %>>
4
+ <summary> <h3> <%= group_name.to_s.titlecase %> options </h3> </summary>
5
+
6
+ ```bash
7
+ <%= method_options[group_name].map { _1.template_text }.join("\n") %>
8
+ ```
9
+
10
+ </details>
11
+
12
+ <% end %>
@@ -0,0 +1,49 @@
1
+ # <%= title.to_s.titlecase %>
2
+
3
+ ## Description
4
+ <%= command.long_description || command.description %>
5
+
6
+ ```bash
7
+ # Base command for `<%= command.usage %>`
8
+ <%= basename_string %>
9
+ ```
10
+
11
+ <%= custom_headers %>
12
+
13
+ <% headers.each do |header| %>
14
+ ## <%= header[:name] %>
15
+ <%= header[:desc]%>
16
+ <% end %>
17
+ <% if drawn_out_examples %>
18
+ ---
19
+
20
+ ## Examples
21
+ <% drawn_out_examples.each do |ex|%>
22
+ ```bash
23
+ <%= ex %>
24
+ ```
25
+ <% end %>
26
+ <% end %>
27
+ <% if !method_options_erb.strip.empty? %>
28
+ ---
29
+
30
+ <% if children_descriptors.empty? %>
31
+ ## Method Options
32
+
33
+ <%= method_options_erb %>
34
+ <% end %>
35
+ <% else %>
36
+ <% children_descriptors.each do |child| %>
37
+ ### [<%= child[:title]%>](<%= child[:link] %>)
38
+
39
+ <%= child[:description] %>
40
+
41
+ ```bash
42
+ <%= child[:basename_string]%> <options>
43
+ <%= child[:examples].map { _1 }.join("\n") %>
44
+ ```
45
+ <% end %>
46
+ <% end %>
47
+ ---
48
+
49
+ <%= footer_erb %>
@@ -0,0 +1,3 @@
1
+ > AutoGenerateted by [ThorEnhance](https://github.com/matt-taylor/thor_enhance) <br>
2
+ > Regenerate file with: <%= regenerate_single_command %> <br>
3
+ > Regenerate all files with: <%= regenerate_thor_command %>
@@ -0,0 +1,14 @@
1
+ # What: <%= option.description %>
2
+ # Type: <%= option.type %>
3
+ # Required: <%= option.required %>
4
+ # <%= "**Allowed inputs:** #{option.enum}" if option.enum %>
5
+ # <%= name.to_s.titlecase %>
6
+ <%= invocations.map { "`#{_1}`"}.join(" | ") %>
7
+
8
+ **What:** <%= option.description %><br>
9
+ **Invocation:** <br>
10
+ **Type:** <%= option.type %><br>
11
+ **Default:** <%= option.default || "none" %><br>
12
+ **Required:** <%= option.required %><br>
13
+ <%= "**Allowed inputs:** #{option.enum}" if option.enum %>
14
+
@@ -0,0 +1,9 @@
1
+ ## [<%= root_child[:title]%>](<%= root_child[:link] %>)
2
+ <%= root_child[:description] %>
3
+
4
+ ```bash
5
+ # Help Command
6
+ <%= root_child[:basename_string]%> <options>
7
+
8
+ <%= root_child[:examples].map { _1 }.join("\n") %>
9
+ ```
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThorEnhance
4
+ module Autogenerate
5
+ module Validate
6
+ module_function
7
+
8
+ def validate(options:, root: nil)
9
+ root_result = validate_root(options: options, root: root)
10
+ return root_result if root_result[:status] != :pass
11
+
12
+ trunk = root_result[:trunk]
13
+ constant = root_result[:constant]
14
+
15
+ subcommand_result = validate_subcommand(options: options, trunk: trunk)
16
+ return subcommand_result if subcommand_result[:status] != :pass
17
+
18
+ trunk = subcommand_result[:trunk]
19
+ command_result = validate_command(options: options, trunk: trunk, constant: constant)
20
+ return command_result if command_result[:status] != :pass
21
+ command = command_result[:command]
22
+ { command: command, trunk: trunk, constant: constant, status: :pass }
23
+ end
24
+
25
+ def validate_root(options:, root:)
26
+ begin
27
+ constant = root || Object.const_get(options.root.to_s)
28
+ rescue NameError => e
29
+ msg_array = [
30
+ "Unable to load provided --root|-r option `#{options.root}`",
31
+ "Please check the spelling and ensure the klass has loaded"
32
+ ]
33
+ return { error: e, msg_array: msg_array, status: :fail }
34
+ end
35
+
36
+ begin
37
+ trunk = ThorEnhance::Tree.tree(base: constant)
38
+ rescue TreeFailure => e
39
+ msg_array = [
40
+ "--root|-r option is not a Thor klass.",
41
+ "Please ensure that the provided klass is a child of Thor"
42
+ ]
43
+ return { error: e, msg_array: msg_array, status: :fail }
44
+ end
45
+
46
+ { trunk: trunk, constant: constant, status: :pass }
47
+ end
48
+
49
+ def validate_command(options:, trunk:, constant:)
50
+ # Return early when command is not present in the options object
51
+ command = options.command
52
+ return { status: :pass, trunk: trunk, command: nil } if command.nil?
53
+
54
+ # Return early when command is found in the tree trunk
55
+ command = trunk.children[options.command] rescue trunk[options.command]
56
+ return { status: :pass, trunk: trunk, command: command } if command
57
+
58
+ # Command option was available but command was not found in the trunk
59
+ msg_array = ["Failed to find --command|-c `#{options.command}`"]
60
+ msg_array << "Provided root command `#{constant}`"
61
+ msg_array << "With Provided subcommand `#{options.subcommand}`" if options.subcommand
62
+ msg_array << "does not have command `#{options.command}` as a child" if options.subcommand
63
+
64
+ { msg_array: msg_array, status: :fail }
65
+ end
66
+
67
+ def validate_subcommand(options:, trunk:)
68
+ subcommands = options.subcommand
69
+ return { trunk: trunk, status: :pass } if subcommands.nil?
70
+
71
+ subcommands = subcommands.dup
72
+ subcommand = subcommands.shift
73
+ temp_trunk = trunk[subcommand]
74
+ while subcommand != nil
75
+ if temp_trunk.nil? || !temp_trunk.children?
76
+ msg_array = [
77
+ "Order is important with --subcommands|-s options",
78
+ "Provided with: #{options.subcommand}",
79
+ "Subcommand `#{subcommand}` does not have any child commands",
80
+ "Every provided subcommand must have children",
81
+ "If the subcommand `#{subcommand}` is meant to be a command",
82
+ "Pass `#{subcommand}` in as `--command|-c #{subcommand}` instead",
83
+ ]
84
+ return { msg_array: msg_array, status: :fail }
85
+ end
86
+ subcommand = subcommands.shift
87
+ # Will always be in the child hash at this point if subcommand exists
88
+ temp_trunk = temp_trunk.children[subcommand] if subcommand
89
+ end
90
+
91
+ { trunk: temp_trunk, subcommands: options.subcommand, status: :pass }
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor_enhance/autogenerate/configuration"
4
+ require "thor_enhance/autogenerate/validate"
5
+ require "thor_enhance/autogenerate/option"
6
+ require "thor_enhance/autogenerate/command"
7
+
8
+ module ThorEnhance
9
+ module Autogenerate
10
+ module_function
11
+
12
+ ROOT_ERB = "#{File.dirname(__FILE__)}/autogenerate/templates/root.rb.erb"
13
+ ROOT_TEMPLATE = ERB.new(File.read(ROOT_ERB))
14
+
15
+ def execute!(options:, basename: File.basename($0), root: nil)
16
+ validate_result = Validate.validate(options: options, root: root)
17
+ return validate_result if validate_result[:status] != :pass
18
+
19
+ command = validate_result[:command]
20
+ trunk = validate_result[:trunk]
21
+ leaves =
22
+ if command
23
+ { options.command => command }
24
+ elsif Hash === trunk
25
+ # Parent trunk is a hash -- structure needs to change
26
+ trunk
27
+ else
28
+ # if not parent, grab the children
29
+ trunk.children
30
+ end
31
+
32
+ command_structure = leaves.map do |name, leaf|
33
+ parent = Command.new(name: name, leaf: leaf, basename: basename)
34
+ end
35
+
36
+ # flatten_children returns all kids, grandkids, great grandkids etc of the commands returned from the above mapping
37
+ youthful_kids = command_structure.map(&:flatten_children).flatten
38
+
39
+ # this is a flat map of the entire family tree. Each node knows where it is so we can flatten it
40
+ family_tree = command_structure + youthful_kids
41
+
42
+ save_generated_readmes!(commands: family_tree, generated_root: options.generated_root, apply: options.apply)
43
+ end
44
+
45
+ def save_generated_readmes!(commands:, generated_root:, apply:)
46
+ parent = generated_root || ENV["THOR_ENHANCE_GENERATED_ROOT_PATH"]
47
+ full_root = "#{parent}/commands"
48
+ saved_status = commands.map do |command|
49
+ command.save_self!(root: full_root, apply: apply)
50
+ end
51
+ self_for_roots = saved_status.collect { _1[:self_for_root] }
52
+ saved_status << root_savior!(apply: apply, full_root: full_root, self_for_roots: self_for_roots)
53
+
54
+ { status: :pass, saved_status: saved_status }
55
+ end
56
+
57
+ def root_savior!(full_root:, self_for_roots:, apply:)
58
+ full_path = "#{full_root}/Readme.md"
59
+ root_erb_result = self_for_roots.map do |root_child|
60
+ ROOT_TEMPLATE.result_with_hash({ root_child: root_child })
61
+ end.join("\n")
62
+
63
+ FileUtils.mkdir_p(full_root)
64
+ if File.exist?(full_path)
65
+ content = File.read(full_path)
66
+ diff = root_erb_result == content ? :same : :overwite
67
+ else
68
+ diff = :new
69
+ end
70
+
71
+ if apply
72
+ File.write(full_path, root_erb_result)
73
+ end
74
+
75
+ { path: full_path, diff: diff, apply: apply }
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module ThorEnhance
6
+ module Base
7
+ module BuildOption
8
+ def self.included(base)
9
+ base.extend(ClassMethods)
10
+ end
11
+
12
+ module ClassMethods
13
+ def build_option(name, options, scope)
14
+ # Method is monkey patched in order to make options aware of the klass that creates it if available
15
+ # This allows us to enable and disable required options based on the class
16
+ scope[name] = Thor::Option.new(name, {check_default_type: check_default_type}.merge!(options), self)
17
+ end
18
+ end
19
+ end
20
+
21
+ module AllowedKlass
22
+ def self.included(base)
23
+ base.extend(ClassMethods)
24
+ end
25
+
26
+ module ClassMethods
27
+ def thor_enhance_allow!
28
+ ThorEnhance.configuration.allowed_klasses << self
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+
@@ -12,11 +12,6 @@ module ThorEnhance
12
12
  define_method(name) { instance_variable_get("@#{name}") }
13
13
  define_method("#{name}=") { instance_variable_set("@#{name}", _1) }
14
14
  end
15
-
16
- # Prepend it so we can call this run method first
17
- ::Thor::Command.prepend ThorEnhance::CommandHook
18
-
19
- ::Thor::Command.include ThorEnhance::Command
20
15
  end
21
16
  end
22
17
  end
@@ -8,29 +8,52 @@ module ThorEnhance
8
8
  def self.thor_enhance_injection!
9
9
  return false unless ThorEnhance::Configuration.allow_changes?
10
10
 
11
- # This will dynamically define a class on the Thor module
11
+ # This will dynamically define class metohds on the Thor Base class
12
12
  # This allows us to add convenience helpers per method
13
+ # Allows us to add inline helper class functions for each desc task
13
14
  ThorEnhance.configuration.command_method_enhance.each do |name, object|
14
- # This is how thor works -- at the class level using memoization
15
- # Interesting approach and it works because thor should boot before everything else -- and only boots once
16
- ClassMethods.define_method("#{name}") do |input|
17
- value = instance_variable_get("@#{name}")
18
- value ||= {}
15
+ # Define a new method based on the name of each enhanced command method
16
+ # Method takes care all validation except requirment -- Requirement is done during command initialization
17
+ ClassMethods.define_method("#{name}") do |input = nil, *args, **kwargs|
18
+ return nil unless ThorEnhance.configuration.allowed?(self)
19
+
20
+ # Usage is nil when the `desc` has not been defined yet -- Under normal circumstance this will never happen
19
21
  if @usage.nil?
20
22
  raise ArgumentError, "Usage is not set. Please ensure `#{name}` is defined after usage is set"
21
23
  end
24
+
25
+ __thor_enhance_validate_arguments!(object, input, args, kwargs)
26
+ value = instance_variable_get("@#{name}")
27
+ value ||= {}
28
+
29
+ # Required check gets done on command initialization (defined below in ClassMethods)
30
+ if input.nil?
31
+ # no op when it is nil
32
+ elsif !object[:enums].nil?
33
+ unless object[:enums].include?(input)
34
+ raise ValidationFailed, "#{@usage} recieved command method `#{name}` with incorrect enum. Received: [#{input}]. Expected: [#{object[:enums]}]"
35
+ end
36
+ elsif !object[:allowed_klasses].nil?
37
+ unless object[:allowed_klasses].include?(value.class)
38
+ raise ValidationFailed, "#{@usage} recieved command method `#{name}` with incorrect class type. Received: [#{input.class}]. Expected: #{object[:allowed_klasses]}"
39
+ end
40
+ end
41
+
22
42
  if object[:repeatable]
23
43
  value[@usage] ||= []
24
- value[@usage] << input
44
+ value[@usage] << { input: input, arguments: { kwargs: kwargs, positional: args } }
25
45
  else
26
- value[@usage] = input
46
+ if value[@usage]
47
+ raise ValidationFailed, "#{@usage} recieved command method `#{name}` with repeated invocations of " \
48
+ "`#{name}`. Please remove the secondary invocation. Or set `#{name}` as a repeatable command method"
49
+ end
50
+
51
+ value[@usage] = { input: input, arguments: { kwargs: kwargs, positional: args } }
27
52
  end
28
53
 
29
54
  instance_variable_set("@#{name}", value)
30
55
  end
31
56
  end
32
-
33
- ::Thor.include ThorEnhance::CommandMethod
34
57
  end
35
58
 
36
59
  def self.included(base)
@@ -38,20 +61,119 @@ module ThorEnhance
38
61
  end
39
62
 
40
63
  module ClassMethods
64
+ THOR_ENHANCE_ENABLE = :enable
65
+ THOR_ENHANCE_DISABLE = :disable
66
+
67
+ def disable_thor_enhance!(&block)
68
+ __thor_enhance_access(type: THOR_ENHANCE_DISABLE, &block)
69
+ end
70
+
71
+ def enable_thor_enhance!(&block)
72
+ __thor_enhance_access(type: THOR_ENHANCE_ENABLE, &block)
73
+ end
41
74
 
42
75
  # Call all things super for it (super in thor also calls super as well)
43
- # If the command exists, then add the initi
76
+ # If the command exists, then set the instance variable
44
77
  def method_added(meth)
45
- super(meth)
78
+ value = super(meth)
46
79
 
47
- # Skip if the we should not be creating the command
80
+ # Skip if the command does not exist -- Super creates the command
48
81
  if command = all_commands[meth.to_s]
49
- ThorEnhance.configuration.command_method_enhance.each do |name, object|
50
- # When value is not required and not present, it will not exist. Rescue and return nil
51
- value = instance_variable_get("@#{name}")[meth.to_s] rescue nil
52
- command.send("#{name}=", value)
82
+
83
+ if ThorEnhance.configuration.allowed?(self)
84
+ ThorEnhance.configuration.command_method_enhance.each do |name, object|
85
+
86
+ instance_variable = instance_variable_get("@#{name}")
87
+ # instance variable was correctly assigned and exists as a hash
88
+ if Hash === instance_variable
89
+ # Expected key exists in the hash
90
+ # This key already passed validation for type and enum
91
+ # Set it and move on
92
+ if instance_variable.key?(meth.to_s)
93
+ value = instance_variable[meth.to_s]
94
+ command.send("#{name}=", value)
95
+ next
96
+ end
97
+ end
98
+
99
+ # At this point, the key command method was never invoked on for the `name` thor task
100
+ # The value is nil/unset
101
+
102
+ # If we have disabled required operations, go ahead and skip this
103
+ next if ::Thor.__thor_enhance_definition == ThorEnhance::CommandMethod::ClassMethods::THOR_ENHANCE_DISABLE
104
+
105
+ # Skip if the expected command method was not required
106
+ next unless object[:required]
107
+
108
+ # Skip if the method is part of the ignore list
109
+ next if ThorEnhance::Tree.ignore_commands.include?(meth.to_s)
110
+
111
+ # subcommands/subtasks need not require things that regular commands need
112
+ # If user wants them on the sucommand, thats cool, but we will never enforce it
113
+ next if subcommands.map(&:to_s).include?(meth.to_s)
114
+
115
+ # At this point, the command method is missing, we are not in disable mode, and the command method was required
116
+ # raise all hell
117
+ raise ThorEnhance::RequiredOption, "`#{meth}` does not have required command method #{name} invoked. " \
118
+ "Ensure it is added after the `desc` task is invoked"
119
+ end
53
120
  end
54
121
  end
122
+
123
+ value
124
+ end
125
+
126
+ def __thor_enhance_definition
127
+ @__thor_enhance_definition
128
+ end
129
+
130
+ def __thor_enhance_definition=(value)
131
+ @__thor_enhance_definition = value
132
+ end
133
+
134
+ def __thor_enhance_definition_stack
135
+ @__thor_enhance_definition_stack ||= []
136
+ end
137
+
138
+ private
139
+
140
+
141
+ def __thor_enhance_validate_arguments!(object, input, args, kwargs)
142
+ expected_arity = object.dig(:arity)
143
+ if args.length != expected_arity
144
+ raise ArgumentError, "Excluding #{input}, the expected arity command method `#{name}` for #{@usage} is #{expected_arity}. Provided #{args.length}"
145
+ end
146
+
147
+ # checks if there are extra kwargs present that are not expected
148
+ available_kwargs = object.dig(:kwargs).keys
149
+ extra_keys = kwargs.keys - available_kwargs
150
+ unless extra_keys.empty?
151
+ raise ArgumentError, "#{@usage} received command method `#{name}` with unknown KWargs #{extra_keys}"
152
+ end
153
+
154
+ # Checks if all the required kwargs are present
155
+ req_kwargs = object.dig(:kwargs).select { _2 }.keys
156
+ missing_required_kwargs = req_kwargs - kwargs.keys
157
+
158
+ # binding.pry if expected_arity > 0 || available_kwargs.length > 0
159
+ return if missing_required_kwargs.empty?
160
+
161
+ raise ArgumentError, "#{@usage} received command method `#{name}` with missing KWargs #{missing_required_kwargs}"
162
+ end
163
+
164
+ def __thor_enhance_access(type:, &block)
165
+ raise ArgumentError, "Expected to receive block. No block given" unless block_given?
166
+
167
+ # capture original value. This allows us to do nested enable/disables
168
+ ::Thor.__thor_enhance_definition_stack << ::Thor.__thor_enhance_definition.dup
169
+ ::Thor.__thor_enhance_definition = type
170
+
171
+ yield
172
+
173
+ nil
174
+ ensure
175
+ # Return the state to the most recently set stack
176
+ ::Thor.__thor_enhance_definition = ::Thor.__thor_enhance_definition_stack.pop
55
177
  end
56
178
  end
57
179
  end