samovar 1.10.0 → 2.0.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,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