eco-helpers 0.6.4 → 0.6.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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