samovar 2.3.0 → 2.4.1

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.
@@ -1,12 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2016-2024, by Samuel Williams.
4
+ # Copyright, 2016-2025, by Samuel Williams.
5
5
 
6
- require_relative 'option'
6
+ require_relative "option"
7
7
 
8
8
  module Samovar
9
+ # Represents a collection of command-line options.
10
+ #
11
+ # Options provide a DSL for defining multiple option flags in a single block.
9
12
  class Options
13
+ # Parse and create an options collection from a block.
14
+ #
15
+ # @parameter arguments [Array] The arguments for the options collection.
16
+ # @parameter options [Hash] Additional options.
17
+ # @yields {|...| ...} A block that defines options using {#option}.
18
+ # @returns [Options] The frozen options collection.
10
19
  def self.parse(*arguments, **options, &block)
11
20
  options = self.new(*arguments, **options)
12
21
 
@@ -15,6 +24,10 @@ module Samovar
15
24
  return options.freeze
16
25
  end
17
26
 
27
+ # Initialize a new options collection.
28
+ #
29
+ # @parameter title [String] The title for this options group in usage output.
30
+ # @parameter key [Symbol] The key to use for storing parsed options.
18
31
  def initialize(title = "Options", key: :options)
19
32
  @title = title
20
33
  @ordered = []
@@ -27,6 +40,9 @@ module Samovar
27
40
  @defaults = {}
28
41
  end
29
42
 
43
+ # Initialize a duplicate of this options collection.
44
+ #
45
+ # @parameter source [Options] The source options to duplicate.
30
46
  def initialize_dup(source)
31
47
  super
32
48
 
@@ -35,12 +51,29 @@ module Samovar
35
51
  @defaults = @defaults.dup
36
52
  end
37
53
 
54
+ # The title for this options group in usage output.
55
+ #
56
+ # @attribute [String]
38
57
  attr :title
58
+
59
+ # The ordered list of options.
60
+ #
61
+ # @attribute [Array(Option)]
39
62
  attr :ordered
40
63
 
64
+ # The key to use for storing parsed options.
65
+ #
66
+ # @attribute [Symbol]
41
67
  attr :key
68
+
69
+ # The default values for options.
70
+ #
71
+ # @attribute [Hash]
42
72
  attr :defaults
43
73
 
74
+ # Freeze this options collection.
75
+ #
76
+ # @returns [Options] The frozen options collection.
44
77
  def freeze
45
78
  return self if frozen?
46
79
 
@@ -53,24 +86,41 @@ module Samovar
53
86
  super
54
87
  end
55
88
 
89
+ # Iterate over each option.
90
+ #
91
+ # @yields {|option| ...} Each option in the collection.
56
92
  def each(&block)
57
93
  @ordered.each(&block)
58
94
  end
59
95
 
96
+ # Check if this options collection is empty.
97
+ #
98
+ # @returns [Boolean] True if there are no options.
60
99
  def empty?
61
100
  @ordered.empty?
62
101
  end
63
102
 
103
+ # Define a new option in this collection.
104
+ #
105
+ # @parameter arguments [Array] The arguments for the option.
106
+ # @parameter options [Hash] Additional options.
107
+ # @yields {|value| ...} An optional block to transform the parsed value.
64
108
  def option(*arguments, **options, &block)
65
109
  self << Option.new(*arguments, **options, &block)
66
110
  end
67
111
 
112
+ # Merge another options collection into this one.
113
+ #
114
+ # @parameter options [Options] The options to merge.
68
115
  def merge!(options)
69
116
  options.each do |option|
70
117
  self << option
71
118
  end
72
119
  end
73
120
 
121
+ # Add an option to this collection.
122
+ #
123
+ # @parameter option [Option] The option to add.
74
124
  def << option
75
125
  @ordered << option
76
126
  option.flags.each do |flag|
@@ -86,24 +136,41 @@ module Samovar
86
136
  end
87
137
  end
88
138
 
139
+ # Parse options from the input.
140
+ #
141
+ # @parameter input [Array(String)] The command-line arguments.
142
+ # @parameter parent [Command | Nil] The parent command.
143
+ # @parameter default [Hash | Nil] Default values to use.
144
+ # @returns [Hash] The parsed option values.
89
145
  def parse(input, parent = nil, default = nil)
90
146
  values = (default || @defaults).dup
91
147
 
92
148
  while option = @keyed[input.first]
93
- prefix = input.first
149
+ # prefix = input.first
94
150
  result = option.parse(input)
95
151
  if result != nil
96
152
  values[option.key] = result
97
153
  end
98
154
  end
99
155
 
156
+ # Validate required options
157
+ @ordered.each do |option|
158
+ if option.required && !values.key?(option.key)
159
+ raise MissingValueError.new(parent, option.key)
160
+ end
161
+ end
162
+
100
163
  return values
101
- end
102
-
164
+ end # Generate a string representation for usage output.
165
+ #
166
+ # @returns [String] The usage string.
103
167
  def to_s
104
- @ordered.collect(&:to_s).join(' ')
168
+ @ordered.collect(&:to_s).join(" ")
105
169
  end
106
170
 
171
+ # Generate usage information for this options collection.
172
+ #
173
+ # @parameter rows [Output::Rows] The rows to append usage information to.
107
174
  def usage(rows)
108
175
  @ordered.each do |option|
109
176
  rows << option
@@ -4,15 +4,34 @@
4
4
  # Copyright, 2016-2023, by Samuel Williams.
5
5
 
6
6
  module Samovar
7
+ # Namespace for output formatting classes.
7
8
  module Output
9
+ end
10
+ end
11
+
12
+ module Samovar
13
+ module Output
14
+ # Represents column widths for aligned output formatting.
15
+ #
16
+ # Calculates the maximum width of each column across all rows for proper text alignment.
8
17
  class Columns
18
+ # Initialize column width calculator.
19
+ #
20
+ # @parameter rows [Array(Array)] The rows to calculate column widths from.
9
21
  def initialize(rows)
10
22
  @rows = rows
11
23
  @widths = calculate_widths(rows)
12
24
  end
13
25
 
26
+ # The calculated column widths.
27
+ #
28
+ # @attribute [Array(Integer)]
14
29
  attr :widths
15
30
 
31
+ # Calculate the maximum width for each column.
32
+ #
33
+ # @parameter rows [Array(Array)] The rows to analyze.
34
+ # @returns [Array(Integer)] The maximum width of each column.
16
35
  def calculate_widths(rows)
17
36
  widths = []
18
37
 
@@ -5,15 +5,33 @@
5
5
 
6
6
  module Samovar
7
7
  module Output
8
+ # Represents a header row in usage output.
9
+ #
10
+ # Headers display command names and their descriptions.
8
11
  class Header
12
+ # Initialize a new header.
13
+ #
14
+ # @parameter name [String] The command name.
15
+ # @parameter object [Command] The command class.
9
16
  def initialize(name, object)
10
17
  @name = name
11
18
  @object = object
12
19
  end
13
20
 
21
+ # The command name.
22
+ #
23
+ # @attribute [String]
14
24
  attr :name
25
+
26
+ # The command class.
27
+ #
28
+ # @attribute [Command]
15
29
  attr :object
16
30
 
31
+ # Generate an aligned header string.
32
+ #
33
+ # @parameter columns [Columns] The columns for alignment (unused for headers).
34
+ # @returns [String] The command line usage string.
17
35
  def align(columns)
18
36
  @object.command_line(@name)
19
37
  end
@@ -1,22 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2023, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
 
6
6
  module Samovar
7
7
  module Output
8
+ # Represents a row in usage output.
9
+ #
10
+ # Rows display formatted option or argument information with proper column alignment.
8
11
  class Row < Array
12
+ # Initialize a new row.
13
+ #
14
+ # @parameter object [Object] The object to convert to a row (must respond to `to_a`).
9
15
  def initialize(object)
10
16
  @object = object
11
17
  super object.to_a.collect(&:to_s)
12
18
  end
13
19
 
20
+ # The source object for this row.
21
+ #
22
+ # @attribute [Object]
14
23
  attr :object
15
24
 
25
+ # Generate an aligned row string.
26
+ #
27
+ # @parameter columns [Columns] The columns for alignment.
28
+ # @returns [String] The aligned row string.
16
29
  def align(columns)
17
30
  self.collect.with_index do |value, index|
18
31
  value.ljust(columns.widths[index])
19
- end.join(' ')
32
+ end.join(" ")
20
33
  end
21
34
  end
22
35
  end
@@ -1,40 +1,65 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2023, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
 
6
- require_relative 'header'
7
- require_relative 'columns'
8
- require_relative 'row'
6
+ require_relative "header"
7
+ require_relative "columns"
8
+ require_relative "row"
9
9
 
10
10
  module Samovar
11
11
  module Output
12
+ # Represents a collection of rows for usage output.
13
+ #
14
+ # Manages hierarchical usage information with support for nesting and formatting.
12
15
  class Rows
13
16
  include Enumerable
14
17
 
18
+ # Initialize a new rows collection.
19
+ #
20
+ # @parameter level [Integer] The indentation level for this collection.
15
21
  def initialize(level = 0)
16
22
  @level = level
17
23
  @rows = []
18
24
  end
19
25
 
26
+ # The indentation level.
27
+ #
28
+ # @attribute [Integer]
20
29
  attr :level
21
30
 
31
+ # Check if this collection is empty.
32
+ #
33
+ # @returns [Boolean] True if there are no rows.
22
34
  def empty?
23
35
  @rows.empty?
24
36
  end
25
37
 
38
+ # Get the first row.
39
+ #
40
+ # @returns [Object | Nil] The first row.
26
41
  def first
27
42
  @rows.first
28
43
  end
29
44
 
45
+ # Get the last row.
46
+ #
47
+ # @returns [Object | Nil] The last row.
30
48
  def last
31
49
  @rows.last
32
50
  end
33
51
 
52
+ # Get the indentation string for this level.
53
+ #
54
+ # @returns [String] The indentation string.
34
55
  def indentation
35
56
  @indentation ||= "\t" * @level
36
57
  end
37
58
 
59
+ # Iterate over each row.
60
+ #
61
+ # @parameter ignore_nested [Boolean] Whether to skip nested rows.
62
+ # @yields {|row, rows| ...} Each row with its parent collection.
38
63
  def each(ignore_nested: false, &block)
39
64
  return to_enum(:each, ignore_nested: ignore_nested) unless block_given?
40
65
 
@@ -47,16 +72,27 @@ module Samovar
47
72
  end
48
73
  end
49
74
 
75
+ # Add a row to this collection.
76
+ #
77
+ # @parameter object [Object] The object to add as a row.
78
+ # @returns [Rows] Self.
50
79
  def << object
51
80
  @rows << Row.new(object)
52
81
 
53
82
  return self
54
83
  end
55
84
 
85
+ # Get the columns for alignment.
86
+ #
87
+ # @returns [Columns] The columns calculator.
56
88
  def columns
57
89
  @columns ||= Columns.new(@rows.select{|row| row.is_a? Array})
58
90
  end
59
91
 
92
+ # Create a nested section in the output.
93
+ #
94
+ # @parameter arguments [Array] Arguments for the header.
95
+ # @yields {|rows| ...} A block that populates the nested rows.
60
96
  def nested(*arguments)
61
97
  @rows << Header.new(*arguments)
62
98
 
@@ -1,33 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2023, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
 
6
- require 'mapping/model'
7
- require 'console/terminal'
6
+ require "mapping/model"
7
+ require "console/terminal"
8
8
 
9
- require_relative '../error'
9
+ require_relative "../error"
10
10
 
11
- require_relative 'header'
11
+ require_relative "header"
12
12
 
13
- require_relative 'row'
14
- require_relative 'rows'
13
+ require_relative "row"
14
+ require_relative "rows"
15
15
 
16
16
  module Samovar
17
17
  module Output
18
+ # Formats and prints usage information to a terminal.
19
+ #
20
+ # Uses the `mapping` gem to handle different output object types with custom formatting rules.
18
21
  class UsageFormatter < Mapping::Model
22
+ # Print usage information to the output.
23
+ #
24
+ # @parameter rows [Rows] The rows to format and print.
25
+ # @parameter output [IO] The output stream to print to.
26
+ # @yields {|formatter| ...} Optional block to customize the formatter.
19
27
  def self.print(rows, output)
20
- formatter = self.new(rows, output)
28
+ formatter = self.new(output)
21
29
 
22
30
  yield formatter if block_given?
23
31
 
24
- formatter.print
32
+ formatter.print(rows)
25
33
  end
26
34
 
27
- def initialize(rows, output)
28
- @rows = rows
35
+ # Initialize a new usage formatter.
36
+ #
37
+ # @parameter rows [Rows] The rows to format.
38
+ # @parameter output [IO] The output stream to print to.
39
+ def initialize(output)
29
40
  @output = output
30
41
  @width = 80
42
+ @first = true
31
43
 
32
44
  @terminal = Console::Terminal.for(@output)
33
45
  @terminal[:header] = @terminal.style(nil, nil, :bright)
@@ -45,7 +57,11 @@ module Samovar
45
57
  end
46
58
 
47
59
  map(Header) do |header, rows|
48
- @terminal.puts unless header == @rows.first
60
+ if @first
61
+ @first = false
62
+ else
63
+ @terminal.puts
64
+ end
49
65
 
50
66
  command_line = header.object.command_line(header.name)
51
67
  @terminal.puts "#{rows.indentation}#{command_line}", style: :header
@@ -64,8 +80,10 @@ module Samovar
64
80
  items.collect{|row, rows| map(row, rows)}
65
81
  end
66
82
 
67
- def print
68
- map(@rows)
83
+ # Print the formatted usage output.
84
+ def print(rows, first: @first)
85
+ @first = first
86
+ map(rows)
69
87
  end
70
88
  end
71
89
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2016-2023, by Samuel Williams.
4
+ # Copyright, 2016-2025, by Samuel Williams.
5
5
 
6
- require_relative 'output/usage_formatter'
6
+ require_relative "output/usage_formatter"
data/lib/samovar/split.rb CHANGED
@@ -1,11 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2016-2023, by Samuel Williams.
4
+ # Copyright, 2016-2025, by Samuel Williams.
5
5
 
6
6
  module Samovar
7
+ # Represents a split point in the command-line arguments.
8
+ #
9
+ # A `Split` parser divides the argument list at a marker (typically `--`), allowing you to separate arguments meant for your command from those passed to another tool.
7
10
  class Split
8
- def initialize(key, description, marker: '--', default: nil, required: false)
11
+ # Initialize a new split parser.
12
+ #
13
+ # @parameter key [Symbol] The name of the attribute to store the values after the split.
14
+ # @parameter description [String] A description of the split for help output.
15
+ # @parameter marker [String] The marker that indicates the split point.
16
+ # @parameter default [Object] The default value if no split is present.
17
+ # @parameter required [Boolean] Whether the split is required.
18
+ def initialize(key, description, marker: "--", default: nil, required: false)
9
19
  @key = key
10
20
  @description = description
11
21
  @marker = marker
@@ -13,16 +23,41 @@ module Samovar
13
23
  @required = required
14
24
  end
15
25
 
26
+ # The name of the attribute to store the values after the split.
27
+ #
28
+ # @attribute [Symbol]
16
29
  attr :key
30
+
31
+ # A description of the split for help output.
32
+ #
33
+ # @attribute [String]
17
34
  attr :description
35
+
36
+ # The marker that indicates the split point.
37
+ #
38
+ # @attribute [String]
18
39
  attr :marker
40
+
41
+ # The default value if no split is present.
42
+ #
43
+ # @attribute [Object]
19
44
  attr :default
45
+
46
+ # Whether the split is required.
47
+ #
48
+ # @attribute [Boolean]
20
49
  attr :required
21
50
 
51
+ # Generate a string representation for usage output.
52
+ #
53
+ # @returns [String] The usage string.
22
54
  def to_s
23
55
  "#{@marker} <#{@key}...>"
24
56
  end
25
57
 
58
+ # Generate an array representation for usage output.
59
+ #
60
+ # @returns [Array] The usage array.
26
61
  def to_a
27
62
  usage = [to_s, @description]
28
63
 
@@ -35,13 +70,19 @@ module Samovar
35
70
  return usage
36
71
  end
37
72
 
73
+ # Parse arguments after the split marker.
74
+ #
75
+ # @parameter input [Array(String)] The command-line arguments.
76
+ # @parameter parent [Command | Nil] The parent command.
77
+ # @parameter default [Object | Nil] An override for the default value.
78
+ # @returns [Array(String) | Object | Nil] The arguments after the split, or the default if no split.
38
79
  def parse(input, parent = nil, default = nil)
39
80
  if offset = input.index(@marker)
40
81
  input.pop(input.size - offset).tap(&:shift)
41
82
  elsif default ||= @default
42
83
  return default
43
84
  elsif @required
44
- raise MissingValueError.new(parent, self)
85
+ raise MissingValueError.new(parent, @key)
45
86
  end
46
87
  end
47
88
  end
data/lib/samovar/table.rb CHANGED
@@ -1,10 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2016-2024, by Samuel Williams.
4
+ # Copyright, 2016-2025, by Samuel Williams.
5
5
 
6
6
  module Samovar
7
+ # Represents a table of parsing rows for a command.
8
+ #
9
+ # A table manages the collection of options, arguments, and nested commands that define how to parse a command line.
7
10
  class Table
11
+ # Create a nested table that inherits from the parent class's table.
12
+ #
13
+ # @parameter klass [Class] The command class to create a table for.
14
+ # @parameter parent [Table | Nil] The parent table to inherit from.
15
+ # @returns [Table] The new table.
8
16
  def self.nested(klass, parent = nil)
9
17
  if klass.superclass.respond_to?(:table)
10
18
  parent = klass.superclass.table
@@ -13,12 +21,19 @@ module Samovar
13
21
  self.new(parent, name: klass.name)
14
22
  end
15
23
 
24
+ # Initialize a new table.
25
+ #
26
+ # @parameter parent [Table | Nil] The parent table to inherit from.
27
+ # @parameter name [String | Nil] The name of the command this table belongs to.
16
28
  def initialize(parent = nil, name: nil)
17
29
  @parent = parent
18
30
  @name = name
19
31
  @rows = {}
20
32
  end
21
33
 
34
+ # Freeze this table.
35
+ #
36
+ # @returns [Table] The frozen table.
22
37
  def freeze
23
38
  return self if frozen?
24
39
 
@@ -27,14 +42,24 @@ module Samovar
27
42
  super
28
43
  end
29
44
 
45
+ # Get a row by key.
46
+ #
47
+ # @parameter key [Symbol] The key to look up.
48
+ # @returns [Object | Nil] The row with the given key.
30
49
  def [] key
31
50
  @rows[key]
32
51
  end
33
52
 
53
+ # Iterate over each row.
54
+ #
55
+ # @yields {|row| ...} Each row in the table.
34
56
  def each(&block)
35
57
  @rows.each_value(&block)
36
58
  end
37
59
 
60
+ # Add a row to the table.
61
+ #
62
+ # @parameter row The row to add.
38
63
  def << row
39
64
  if existing_row = @rows[row.key] and existing_row.respond_to?(:merge!)
40
65
  existing_row.merge!(row)
@@ -44,10 +69,17 @@ module Samovar
44
69
  end
45
70
  end
46
71
 
72
+ # Check if this table is empty.
73
+ #
74
+ # @returns [Boolean] True if this table and its parent are empty.
47
75
  def empty?
48
76
  @rows.empty? && @parent&.empty?
49
77
  end
50
78
 
79
+ # Merge this table's rows into another table.
80
+ #
81
+ # @parameter table [Table] The table to merge into.
82
+ # @returns [Table] The merged table.
51
83
  def merge_into(table)
52
84
  @parent&.merge_into(table)
53
85
 
@@ -58,6 +90,9 @@ module Samovar
58
90
  return table
59
91
  end
60
92
 
93
+ # Get a merged table that includes parent rows.
94
+ #
95
+ # @returns [Table] The merged table.
61
96
  def merged
62
97
  if @parent.nil? or @parent.empty?
63
98
  return self
@@ -66,10 +101,17 @@ module Samovar
66
101
  end
67
102
  end
68
103
 
104
+ # Generate a usage string from all rows.
105
+ #
106
+ # @returns [String] The usage string.
69
107
  def usage
70
- @rows.each_value.collect(&:to_s).reject(&:empty?).join(' ')
108
+ @rows.each_value.collect(&:to_s).reject(&:empty?).join(" ")
71
109
  end
72
110
 
111
+ # Parse the input according to the rows in this table.
112
+ #
113
+ # @parameter input [Array(String)] The command-line arguments.
114
+ # @parameter parent [Command] The parent command to store results in.
73
115
  def parse(input, parent)
74
116
  @rows.each do |key, row|
75
117
  next unless row.respond_to?(:parse)
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2016-2023, by Samuel Williams.
4
+ # Copyright, 2016-2025, by Samuel Williams.
5
5
  # Copyright, 2018, by Gabriel Mazetto.
6
6
 
7
+ # @namespace
7
8
  module Samovar
8
- VERSION = "2.3.0"
9
+ VERSION = "2.4.1"
9
10
  end
data/lib/samovar.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2016-2023, by Samuel Williams.
4
+ # Copyright, 2016-2025, by Samuel Williams.
5
5
 
6
- require_relative 'samovar/version'
7
- require_relative 'samovar/command'
6
+ require_relative "samovar/version"
7
+ require_relative "samovar/command"
data/license.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # MIT License
2
2
 
3
- Copyright, 2016-2024, by Samuel Williams.
3
+ Copyright, 2016-2025, by Samuel Williams.
4
4
  Copyright, 2018, by Gabriel Mazetto.
5
5
 
6
6
  Permission is hereby granted, free of charge, to any person obtaining a copy