eco-helpers 0.6.4 → 0.6.5

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,119 @@
1
+ require 'thor'
2
+
3
+ module Eco
4
+ module Common
5
+ class BaseCLI < MethaThor
6
+ PIPE = "!"
7
+ CUSTOM_HELP_MAPPINGS = ["?", "help"]
8
+ ALL_HELP_MAPPINGS = Thor::HELP_MAPPINGS + CUSTOM_HELP_MAPPINGS
9
+
10
+ class_option :simulate, type: :boolean, aliases: :s, desc: "do not launch updates or rewrite files"
11
+ class_option :verbosity, type: :numeric, default: 0, desc: "defines the level of verbosity to show feedback"
12
+
13
+ class_option :input, type: :hash, default: {json: nil}, required: false, desc: "--input=json:STRING"
14
+ class_option :pipe, type: :boolean, default: false, required: false, desc: "--pipe subcommand"
15
+
16
+ def self.start(given_args = ARGV, config = {})
17
+ #given_args = splat_namespaces(given_args)
18
+ given_args = BaseCLI.parse_help(given_args)
19
+ super(given_args, config)
20
+ end
21
+
22
+ # incompatible with options / arguments of Hash type
23
+ # see: parse_hash here: https://github.com/erikhuda/thor/blob/master/lib/thor/parser/arguments.rb
24
+ def self.splat_namespaces(args)
25
+ args.reduce([]) do |done, arg|
26
+ done.concat(arg.split(":"))
27
+ end
28
+ end
29
+
30
+ def self.parse_help(args)
31
+ # Help enhancement. Adapted from: https://stackoverflow.com/a/49044225/4352306
32
+ last = args.last
33
+ if args.length > 1 && help?(last)
34
+ # switch last and second last position
35
+ last = "help" if custom_help?(last)
36
+ args.insert(args.length - 2, last).pop
37
+ puts "> #{args.join(" ")}"
38
+ end
39
+ args
40
+ end
41
+
42
+ def self.help?(value)
43
+ case value
44
+ when String
45
+ ALL_HELP_MAPPINGS.include?(value)
46
+ when Symbol
47
+ help?(value.to_s)
48
+ when Array
49
+ value.any? { |v| help?(v) }
50
+ when Hash
51
+ value.keys.any { |k| help?(v) }
52
+ end
53
+ end
54
+
55
+ def self.custom_help?(value)
56
+ CUSTOM_HELP_MAPPINGS.include?(value)
57
+ end
58
+
59
+ protected
60
+
61
+ no_commands do
62
+ def input?(options)
63
+ if options
64
+ options.key?("input") || options.key?(:input) ||
65
+ options.key?("json") || options.key?(:json)
66
+ end
67
+
68
+ def input_object(input)
69
+ Eco::CLI::Input.new(input)
70
+ end
71
+
72
+ def input(value)
73
+ case
74
+ when value.is_a?(Eco::CLI::Input)
75
+ value.value
76
+ when value.is_a?(Eco::CLI::MultiInput)
77
+ value.to_h
78
+ when value.is_a?(String)
79
+ input(JSON.parse(value))
80
+ when value&.key?("input")
81
+ input(value["input"])
82
+ when value&.key?(:input)
83
+ input(value[:input])
84
+ when value&.key?("json")
85
+ JSON.parse(value["json"])
86
+ when value&.key?(:json)
87
+ JSON.parse(value[:json])
88
+ end
89
+ end
90
+
91
+ def pipe(input, args)
92
+ command, args, piped = parse_pipe(args)
93
+ invoke command, args, {input: input_object(input)} if piped
94
+ piped
95
+ end
96
+
97
+ def parse_pipe(args)
98
+ args.shift if piped = (args.first == PIPE)
99
+ subcommand = ""
100
+ if piped
101
+ raise "Error: bad usage of pipe ('#{PIPE}'). Expected: '#{PIPE} command' " unless subcommand = args.slice!(0)
102
+ if help?(subcommand)
103
+ # Idea adapted from: https://stackoverflow.com/a/46167300/4352306
104
+ self.class.command_help(Thor::Base.shell.new, args.first)
105
+ exit
106
+ end
107
+ end
108
+ [subcommand, args, piped]
109
+ end
110
+
111
+ def help?(value)
112
+ BaseCLI.help?(value)
113
+ end
114
+
115
+ end
116
+
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,111 @@
1
+ require 'thor'
2
+
3
+ module Eco
4
+ module Common
5
+ class MetaThor < Thor
6
+ INPUT_OPTION = "input"
7
+ INPUT_SYM = :input
8
+ INPUT_KEY = "json"
9
+ CUSTOM_HELP_MAPPINGS = ["?", "help"]
10
+ ALL_HELP_MAPPINGS = ::Thor::HELP_MAPPINGS + CUSTOM_HELP_MAPPINGS
11
+
12
+ class_option :input, type: :hash, default: {json: nil}, required: false, desc: "--input=json:STRING"
13
+ class_option :pipe, type: :boolean, default: false, required: false, desc: "--pipe subcommand"
14
+
15
+ class << self
16
+ def start(given_args = ARGV, config = {})
17
+ config[:shell] ||= Thor::Base.shell.new
18
+ CommandGroup.new(given_args).map do |unit|
19
+ args = unit.args {|args| parse_custom_help(args)}
20
+ input = unit.input&.as_option
21
+ result = dispatch(nil, args.dup, input, config)
22
+ exit if help?(args)
23
+ unit.output = Value.new(result)
24
+ end
25
+ end
26
+
27
+ # @note: it allows to use `help` at the end (alias: `?`)
28
+ # If there is custom help at the end of the current command,
29
+ # normalize custom help to native `help` and switch last
30
+ # and second last arguments
31
+ def parse_custom_help(args)
32
+ return args unless custom_help?(args&.last)
33
+ # move from custom to native help
34
+ if args.length > 1
35
+ # switch args last and second last position
36
+ args.insert(args.length - 2, "help").pop
37
+ else
38
+ args[0] = "help"
39
+ end
40
+ args
41
+ end
42
+
43
+ def custom_help?(value)
44
+ CUSTOM_HELP_MAPPINGS.include?(value)
45
+ end
46
+
47
+ def help?(value)
48
+ case value
49
+ when String
50
+ ALL_HELP_MAPPINGS.include?(value)
51
+ when Symbol
52
+ help?(value.to_s)
53
+ when Array
54
+ value.any? { |v| help?(v) }
55
+ when Hash
56
+ value.keys.any? { |k| help?(v) }
57
+ else
58
+ "what? #{value}"
59
+ end
60
+ end
61
+
62
+ end
63
+
64
+ protected
65
+
66
+ no_commands do
67
+ def input?(options)
68
+ options &&
69
+ (options.key?(INPUT_OPTION) || options.key?(INPUT_KEY) ||
70
+ options.key?(Value::KEY_OPTION) )
71
+ end
72
+
73
+ def input(value)
74
+
75
+ case
76
+ when value.is_a?(Value)
77
+ value.value
78
+ when value.is_a?(String)
79
+ input(JSON.parse(value))
80
+ when value.is_a?(Hash)
81
+ case
82
+ when value.key?(Value::KEY_OPTION)
83
+ Value.to_value(value)
84
+ when value.key?(INPUT_OPTION)
85
+ input(value[INPUT_OPTION])
86
+ when value.key?(INPUT_SYM)
87
+ input(value[INPUT_SYM])
88
+ when value.key?(INPUT_KEY)
89
+ val = value[INPUT_KEY]
90
+ JSON.parse(val) unless val.is_a?(Hash)
91
+ else
92
+ value
93
+ end
94
+ else
95
+ value
96
+ end
97
+ end
98
+
99
+ end
100
+
101
+ end
102
+ end
103
+ end
104
+
105
+ require_relative 'meta_thor/pipe'
106
+ require_relative 'meta_thor/command_unit'
107
+ require_relative 'meta_thor/command_group'
108
+ require_relative 'meta_thor/value'
109
+ require_relative 'meta_thor/thor'
110
+ #require_relative 'metha_thor/input'
111
+ #require_relative 'metha_thor/input_multi'
@@ -0,0 +1,36 @@
1
+ module Eco
2
+ module Common
3
+ class MetaThor
4
+ class CommandGroup
5
+
6
+ attr_reader :units
7
+
8
+ def initialize(args)
9
+ args = (args && [args].flatten) || []
10
+ #pp "CommandGroup split args: #{Pipe.split(args)}"
11
+ @units = Pipe.split(args).each_with_index.map do |arguments, i|
12
+ CommandUnit.new(args: arguments, group: self, index: i)
13
+ end
14
+ @units = units
15
+ end
16
+
17
+ def print
18
+ @units.map do |command|
19
+ pp command.args
20
+ end
21
+ end
22
+
23
+ def [](value)
24
+ @units[value]
25
+ end
26
+
27
+ def map
28
+ @units.map do |unit|
29
+ yield(unit)
30
+ end
31
+ end
32
+
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,48 @@
1
+ module Eco
2
+ module Common
3
+ class MetaThor
4
+ class CommandUnit
5
+ class << self
6
+
7
+ end
8
+
9
+ attr_reader :group, :index
10
+ attr_reader :pipe
11
+ attr_reader :args, :source_args
12
+ attr_accessor :output
13
+
14
+ def initialize(args:, group:, index:)
15
+ @source_args = args.dup || []
16
+ @args = @source_args
17
+ @args = @args.slice(1..-1) if Pipe.piped?(@source_args)
18
+ @pipe = Pipe.new(command: self) if Pipe.piped?(@source_args)
19
+
20
+ @group = group
21
+ @index = index
22
+ end
23
+
24
+ def piped?
25
+ !!@pipe
26
+ end
27
+
28
+ def input
29
+ pipe&.input
30
+ end
31
+
32
+ def args
33
+ arguments = @args.dup
34
+ arguments = yield(arguments) if block_given?
35
+ arguments
36
+ end
37
+
38
+ def piped_args
39
+ args.tap do |arguments|
40
+ arguments = yield(arguments) if block_given?
41
+ arguments.push(pipe.input.as_input_option) if piped?
42
+ end
43
+ end
44
+
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,111 @@
1
+ module Eco
2
+ module Common
3
+ class MethaThor
4
+ class Input
5
+ MODES = [:both, :json, :value]
6
+
7
+ attr_reader :mode
8
+
9
+ def initialize(value, mode = :both)
10
+ @mode = mode
11
+ @mode = :both unless MODES.include?(mode)
12
+
13
+ @doc = {}
14
+ @doc['value'] = {} if self.value?
15
+ @doc['json'] = {} if self.json?
16
+
17
+ self.value = value
18
+ end
19
+
20
+ def to_h
21
+ @doc
22
+ end
23
+
24
+ def json?
25
+ @mode == :json || @mode = :both
26
+ end
27
+
28
+ def value?
29
+ @mode == :value || @mode = :both
30
+ end
31
+
32
+ def json
33
+ @doc['json'] if self.json?
34
+ end
35
+
36
+ def parse
37
+ JSON.parse(self.json) if self.json?
38
+ end
39
+
40
+ def json=(value)
41
+ @doc['value'] = value if self.value?
42
+ @doc['json'] = to_json(value) if self.json?
43
+ end
44
+
45
+ def value
46
+ return @doc['value'] if self.value?
47
+ self.parse
48
+ end
49
+
50
+ def value=(value)
51
+ @doc['value'] = value if self.value?
52
+ @doc['json'] = to_json(value) if self.json?
53
+ end
54
+
55
+ def present?(key)
56
+ self[key]&.present?
57
+ end
58
+
59
+ def empty?
60
+ return @doc['value'].keys.length == 0 if self.value?
61
+ @doc['json'].keys.length == 0
62
+ end
63
+
64
+ # merging implies that @mode is equalized to the lesser
65
+ # => :both.merge(:json) == :json ; :value.merge(:both) == :value mode
66
+ # => :json.merge(:value) will raise an exception
67
+ def merge(value)
68
+ merge_hash(hash(value))
69
+ self
70
+ end
71
+
72
+ private
73
+
74
+ def to_json(value)
75
+ if value.respond_to?(:as_json)
76
+ input.as_json
77
+ elsif value.respond_to?(:to_json)
78
+ value.to_json
79
+ end
80
+ end
81
+
82
+ def doc_mode(doc)
83
+ case doc
84
+ when String
85
+ doc = JSON.parse(doc)
86
+ return nil unless doc.is_a?(Hash)
87
+ doc_mode(doc)
88
+ when CliInput
89
+ doc.mode
90
+ when Hash
91
+ mod_json = doc.key?('json')
92
+ mod_value = doc.key?('value')
93
+ mod_json ? (mod_value ? :both : :json) : (mod_value ? :value : nil)
94
+ end
95
+ end
96
+
97
+ def hash(value)
98
+ case value
99
+ when String
100
+ JSON.parse(value)
101
+ when CliInput
102
+ value.to_h
103
+ when Hash
104
+ value
105
+ end
106
+ end
107
+
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,139 @@
1
+ module Eco
2
+ module Common
3
+ class MethaThor
4
+ class MultiInput
5
+ MODES = [:both, :json, :value]
6
+
7
+ attr_reader :mode
8
+
9
+ def initialize(doc = {}, mode = :both)
10
+ @mode = doc_mode(doc) || mode
11
+ @mode = :both unless MODES.include?(@mode)
12
+
13
+ @doc = {}
14
+ @doc['value'] = {} if self.value?
15
+ @doc['json'] = {} if self.json?
16
+
17
+ self.merge(doc) unless doc.empty?
18
+ end
19
+
20
+ def to_h
21
+ @doc
22
+ end
23
+
24
+ def json?
25
+ @mode == :json || @mode = :both
26
+ end
27
+
28
+ def value?
29
+ @mode == :value || @mode = :both
30
+ end
31
+
32
+ def json(key)
33
+ @doc.dig('json', key)
34
+ end
35
+
36
+ def [](key)
37
+ return @doc.dig('value', key) if self.value?
38
+ JSON.parse(@doc.dig('json', key))
39
+ end
40
+
41
+ def []=(key, value)
42
+ @doc['value'][key] = value if self.value?
43
+ @doc['json'][key] = to_json(value) if self.json?
44
+ end
45
+
46
+ def delete(key)
47
+ if self.key?(key)
48
+ value = @doc['value'].delete(key) if self.value?
49
+ value2 = JSON.parse(@doc['json'].delete(key)) if self.json?
50
+ return value if self.value?
51
+ value2
52
+ end
53
+ end
54
+
55
+ def key?(key)
56
+ return @doc['value'].key?(key) if self.value?
57
+ @doc['json'].key?(key)
58
+ end
59
+
60
+ def present?(key)
61
+ self[key]&.present?
62
+ end
63
+
64
+ def empty?
65
+ return @doc['value'].keys.length == 0 if self.value?
66
+ @doc['json'].keys.length == 0
67
+ end
68
+
69
+ # merging implies that @mode is equalized to the lesser
70
+ # => :both.merge(:json) == :json ; :value.merge(:both) == :value mode
71
+ # => :json.merge(:value) will raise an exception
72
+ def merge(value)
73
+ merge_hash(hash(value))
74
+ self
75
+ end
76
+
77
+ private
78
+
79
+ def to_json(value)
80
+ if value.respond_to?(:as_json)
81
+ input.as_json
82
+ elsif value.respond_to?(:to_json)
83
+ value.to_json
84
+ end
85
+ end
86
+
87
+ def doc_mode(doc)
88
+ case doc
89
+ when String
90
+ doc = JSON.parse(doc)
91
+ return nil unless doc.is_a?(Hash)
92
+ doc_mode(doc)
93
+ when CliInput
94
+ doc.mode
95
+ when Hash
96
+ mod_json = doc.key?('json')
97
+ mod_value = doc.key?('value')
98
+ mod_json ? (mod_value ? :both : :json) : (mod_value ? :value : nil)
99
+ end
100
+ end
101
+
102
+ def hash(value)
103
+ case value
104
+ when String
105
+ JSON.parse(value)
106
+ when CliInput
107
+ value.to_h
108
+ when Hash
109
+ value
110
+ end
111
+ end
112
+
113
+ def merge_hash(value)
114
+ err_input = "Error: merge requires a correct Hashable value or a #{self.class.name} instance."
115
+ err_incompatible = "Error: can't merge value mode with json mode."
116
+ raise err_input unless value.is_a?(Hash)
117
+ return unless !value.empty?
118
+
119
+ raise err_input if ! (mode = doc_mode(value))
120
+ raise err_incompatible if ! (new_mode = resolve_mode(mode))
121
+ @mode = new_mode
122
+
123
+ @doc.delete('json') unless self.json?
124
+ @doc.delete('value') unless self.value?
125
+
126
+ @doc['json'].merge(value['json']) if self.json?
127
+ @doc['value'].merge(value['value']) if self.value?
128
+ end
129
+
130
+ def resolve_mode(mode)
131
+ return mode if @mode == :both
132
+ return @mode if mode == :both
133
+ return mode if @mode == mode
134
+ end
135
+
136
+ end
137
+ end
138
+ end
139
+ end