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.
@@ -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