samovar 1.10.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,87 @@
1
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'mapping/model'
22
+ require 'event/terminal'
23
+
24
+ require_relative '../error'
25
+
26
+ require_relative 'header'
27
+
28
+ require_relative 'row'
29
+ require_relative 'rows'
30
+
31
+ module Samovar
32
+ module Output
33
+ class UsageFormatter < Mapping::Model
34
+ def self.print(rows, output)
35
+ formatter = self.new(rows, output)
36
+
37
+ yield formatter if block_given?
38
+
39
+ formatter.print
40
+ end
41
+
42
+ def initialize(rows, output)
43
+ @rows = rows
44
+ @output = output
45
+ @width = 80
46
+
47
+ @terminal = Event::Terminal.for(@output)
48
+ @terminal[:header] = @terminal.style(nil, nil, :bright)
49
+ @terminal[:description] = @terminal.style(:blue)
50
+ @terminal[:error] = @terminal.style(:red)
51
+ end
52
+
53
+ map(InvalidInputError) do |error|
54
+ # This is a little hack which avoids printing out "--help" if it was part of an incomplete parse. In the future I'd prefer if this was handled explicitly.
55
+ @terminal.puts("#{error.message} in:", style: :error) unless error.help?
56
+ end
57
+
58
+ map(MissingValueError) do |error|
59
+ @terminal.puts("#{error.message} in:", style: :error)
60
+ end
61
+
62
+ map(Header) do |header, rows|
63
+ @terminal.puts unless header == @rows.first
64
+
65
+ command_line = header.object.command_line(header.name)
66
+ @terminal.puts "#{rows.indentation}#{command_line}", style: :header
67
+
68
+ if description = header.object.description
69
+ @terminal.puts "#{rows.indentation}\t#{description}", style: :description
70
+ @terminal.puts
71
+ end
72
+ end
73
+
74
+ map(Row) do |row, rows|
75
+ @terminal.puts "#{rows.indentation}#{row.align(rows.columns)}"
76
+ end
77
+
78
+ map(Rows) do |items|
79
+ items.collect{|row, rows| map(row, rows)}
80
+ end
81
+
82
+ def print
83
+ map(@rows)
84
+ end
85
+ end
86
+ end
87
+ end
@@ -20,14 +20,19 @@
20
20
 
21
21
  module Samovar
22
22
  class Split
23
- def initialize(key, description, marker: '--', default: nil)
23
+ def initialize(key, description, marker: '--', default: nil, required: false)
24
24
  @key = key
25
25
  @description = description
26
26
  @marker = marker
27
27
  @default = default
28
+ @required = required
28
29
  end
29
30
 
30
31
  attr :key
32
+ attr :description
33
+ attr :marker
34
+ attr :default
35
+ attr :required
31
36
 
32
37
  def to_s
33
38
  "#{@marker} <#{@key}...>"
@@ -37,16 +42,22 @@ module Samovar
37
42
  usage = [to_s, @description]
38
43
 
39
44
  if @default
40
- usage << "Default: #{@default.inspect}"
45
+ usage << "(default: #{@default.inspect})"
46
+ elsif @required
47
+ usage << "(required)"
41
48
  end
42
49
 
43
50
  return usage
44
51
  end
45
52
 
46
- def parse(input, default = @default)
53
+ def parse(input, parent = nil, default = nil)
47
54
  if offset = input.index(@marker)
48
55
  input.pop(input.size - offset).tap(&:shift)
49
- end || default
56
+ elsif default ||= @default
57
+ return default
58
+ elsif @required
59
+ raise MissingValueError.new(parent, self)
60
+ end
50
61
  end
51
62
  end
52
63
  end
@@ -20,31 +20,79 @@
20
20
 
21
21
  module Samovar
22
22
  class Table
23
- def initialize
24
- @rows = []
25
- @parser = []
23
+ def self.nested(klass, parent = nil)
24
+ if klass.superclass.respond_to?(:table)
25
+ parent = klass.superclass.table
26
+ end
27
+
28
+ self.new(parent, name: klass.name)
29
+ end
30
+
31
+ def initialize(parent = nil, name: nil)
32
+ @parent = parent
33
+ @name = name
34
+ @rows = {}
26
35
  end
27
36
 
28
- attr :rows
37
+ def freeze
38
+ return self if frozen?
39
+
40
+ @rows.freeze
41
+
42
+ super
43
+ end
44
+
45
+ def [] key
46
+ @rows[key]
47
+ end
48
+
49
+ def each(&block)
50
+ @rows.each_value(&block)
51
+ end
29
52
 
30
53
  def << row
31
- @rows << row
54
+ if existing_row = @rows[row.key] and existing_row.respond_to?(:merge!)
55
+ existing_row.merge!(row)
56
+ else
57
+ # In the above case where there is an existing row, but it doensn't support being merged, we overwrite it. This preserves order.
58
+ @rows[row.key] = row.dup
59
+ end
60
+ end
61
+
62
+ def empty?
63
+ @rows.empty? && @parent&.empty?
64
+ end
65
+
66
+ def merge_into(table)
67
+ @parent&.merge_into(table)
32
68
 
33
- if row.respond_to?(:parse)
34
- @parser << row
69
+ @rows.each_value do |row|
70
+ table << row
71
+ end
72
+
73
+ return table
74
+ end
75
+
76
+ def merged
77
+ if @parent.nil? or @parent.empty?
78
+ return self
79
+ else
80
+ merge_into(self.class.new)
35
81
  end
36
82
  end
37
83
 
38
84
  def usage
39
- @rows.collect(&:to_s).join(' ')
85
+ @rows.each_value.collect(&:to_s).reject(&:empty?).join(' ')
40
86
  end
41
87
 
42
- def parse(input, command)
43
- @parser.each do |row|
44
- current = command.send(row.key)
88
+ def parse(input, parent)
89
+ @rows.each do |key, row|
90
+ next unless row.respond_to?(:parse)
91
+
92
+ current = parent.send(key)
45
93
 
46
- if result = row.parse(input, current)
47
- command.send("#{row.key}=", result)
94
+ if result = row.parse(input, parent, current)
95
+ parent.send("#{row.key}=", result)
48
96
  end
49
97
  end
50
98
  end
@@ -19,5 +19,5 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  module Samovar
22
- VERSION = "1.10.0"
22
+ VERSION = "2.0.0"
23
23
  end
@@ -1,6 +1,24 @@
1
+ # Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
1
20
 
2
21
  require 'samovar'
3
- require 'stringio'
4
22
 
5
23
  module Samovar::CoerceSpec
6
24
  class Coerce < Samovar::Command
@@ -1,6 +1,24 @@
1
+ # Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
1
20
 
2
21
  require 'samovar'
3
- require 'stringio'
4
22
 
5
23
  module Samovar::CommandSpec
6
24
  class Bottom < Samovar::Command
@@ -22,7 +40,7 @@ module Samovar::CommandSpec
22
40
  option '-v/--version', "Print out the application version."
23
41
  end
24
42
 
25
- nested '<command>', {
43
+ nested :command, {
26
44
  'bottom' => Bottom
27
45
  }
28
46
  end
@@ -55,17 +73,9 @@ module Samovar::CommandSpec
55
73
  it "should generate documentation" do
56
74
  top = Top[]
57
75
  buffer = StringIO.new
58
- top.print_usage('top', output: buffer)
76
+ top.print_usage(output: buffer)
59
77
 
60
78
  expect(buffer.string).to be_include(Top.description)
61
79
  end
62
-
63
- it "can run commands" do
64
- expect(subject.system("ls")).to be_truthy
65
- expect(subject.system!("ls")).to be_truthy
66
-
67
- expect(subject.system("fail")).to be_falsey
68
- expect{subject.system!("fail")}.to raise_error(Samovar::SystemError)
69
- end
70
80
  end
71
81
  end
@@ -0,0 +1,49 @@
1
+ # Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'samovar/many'
22
+
23
+ RSpec.describe Samovar::Many do
24
+ let(:default) {["1", "2", "3"]}
25
+ let(:input) {["2", "3", "--else"]}
26
+
27
+ subject{described_class.new(:items, "some items", default: default)}
28
+
29
+ it "has string representation" do
30
+ expect(subject.to_s).to be == "<items...>"
31
+ end
32
+
33
+ it "should have default" do
34
+ expect(subject.default).to be == default
35
+ end
36
+
37
+ it "should use default" do
38
+ expect(subject.parse([])).to be == default
39
+ end
40
+
41
+ it "should use specified default" do
42
+ expect(subject.parse([], nil, ["2"])).to be == ["2"]
43
+ end
44
+
45
+ it "should not use default if input specified" do
46
+ expect(subject.parse(input)).to be == ["2", "3"]
47
+ expect(input).to be == ["--else"]
48
+ end
49
+ end
@@ -1,39 +1,108 @@
1
+ # Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
1
20
 
2
21
  require 'samovar'
3
- require 'stringio'
22
+
23
+ RSpec.describe Samovar::Nested do
24
+ let(:commands) do
25
+ {
26
+ 'inner-a' => Class.new(Samovar::Command),
27
+ 'inner-b' => Class.new(Samovar::Command),
28
+ }
29
+ end
30
+
31
+ let(:default) {'inner-a'}
32
+ let(:input) {['inner-a']}
33
+ subject{described_class.new(:command, commands, default: default)}
34
+
35
+ it "has string representation" do
36
+ expect(subject.to_s).to be == "<command>"
37
+ end
38
+
39
+ it "should have default" do
40
+ expect(subject.default).to be == default
41
+ end
42
+
43
+ it "should use default" do
44
+ expect(subject.parse([])).to be_kind_of commands[default]
45
+ end
46
+
47
+ it "should use specified default" do
48
+ command = commands['inner-b'].new
49
+
50
+ expect(subject.parse([], nil, command)).to be command
51
+ end
52
+
53
+ it "should not use default if input specified" do
54
+ expect(subject.parse(input)).to be_kind_of commands['inner-a']
55
+ end
56
+ end
4
57
 
5
58
  module Samovar::NestedSpec
6
59
  class InnerA < Samovar::Command
60
+ options
7
61
  end
8
62
 
9
- class InnerB < Samovar::Command
63
+ class InnerB < InnerA
10
64
  options do
11
- option '--help'
65
+ option '--help', "Do you need it?"
66
+ end
67
+ end
68
+
69
+ class InnerC < InnerB
70
+ options do
71
+ option '--frobulate', "Zork is waiting for you."
12
72
  end
13
73
  end
14
74
 
15
75
  class Outer < Samovar::Command
16
76
  options do
17
- option '--help'
18
77
  end
19
-
20
- nested '<command>', {
78
+
79
+ nested :command, {
21
80
  'inner-a' => InnerA,
22
81
  'inner-b' => InnerB,
82
+ 'inner-c' => InnerC,
23
83
  }, default: 'inner-b'
24
84
  end
25
-
85
+
26
86
  RSpec.describe Samovar::Nested do
27
87
  it "should select default nested command" do
28
88
  outer = Outer[]
29
89
  expect(outer.command).to be_kind_of(InnerB)
90
+
91
+ outer.print_usage
30
92
  end
31
-
93
+
32
94
  it "should select explicitly named nested command" do
33
95
  outer = Outer['inner-a']
34
96
  expect(outer.command).to be_kind_of(InnerA)
35
97
  end
36
-
98
+
99
+ it "can parse derived options" do
100
+ outer = Outer['inner-c', '--help']
101
+ expect(outer.command).to be_kind_of(InnerC)
102
+ expect(outer.command.options).to include(help: true)
103
+ expect(outer.command.parent).to be outer
104
+ end
105
+
37
106
  xit "should parse help option at outer level" do
38
107
  outer = Outer['inner-a', '--help']
39
108
  expect(outer.options[:help]).to_be truthy