Module: Cliffy::Internal

Defined in:
lib/cliffy/internal/run.rb,
lib/cliffy/internal/wrapper.rb,
lib/cliffy/internal/validate.rb,
lib/cliffy/internal/generate_help_data.rb

Defined Under Namespace

Classes: Wrapper

Class Method Summary collapse

Class Method Details

.generate_help_data(command, command_name, executable_name) ⇒ Object



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/cliffy/internal/generate_help_data.rb', line 3

def self.generate_help_data command, command_name, executable_name
  usage_tokens = [executable_name, command_name]
  parameter_names_and_descriptions = {}
  options_and_descriptions = {}
  
  command.method(:run).parameters.each do |parameter|
    symbol = parameter[1]
    description = command.signature[symbol][:description]
    symbol_name = parameter[1].to_s.gsub '_', '-'
    case parameter.first
    when :req
      usage_tokens << "<#{symbol_name}>"
      parameter_names_and_descriptions[symbol_name] = description
    when :rest
      usage_tokens << "<#{symbol_name}...>"
      parameter_names_and_descriptions[symbol_name] = description
    when :key
      option = "--#{symbol_name}"
      type = command.signature[symbol][:type]
      case type
      when :boolean
        usage_tokens << "[#{option}]"
      when :integer, :float, :string
        usage_tokens << "[#{option} value]"
      when Hash
        sub_symbol_names = type.keys.map { |sub_symbol| sub_symbol.to_s.gsub '_', '-' }.join ' '
        usage_tokens << "[#{option} #{sub_symbol_names}]"
      end
      
      options_and_descriptions[option] = description
    end
  end

  usage = usage_tokens.join ' '
  titles_and_contents = {
    'Description' => command.description,
    'Usage' => usage
  }
  unless parameter_names_and_descriptions.empty?
    titles_and_contents.merge! 'Parameters' => parameter_names_and_descriptions
  end
  unless options_and_descriptions.empty?
    titles_and_contents.merge! 'Options' => options_and_descriptions
  end
  if command.respond_to? :notes
    titles_and_contents.merge! 'Notes' => command.notes
  end
  titles_and_contents
end

.run(command, arguments, command_name, executable_name) ⇒ Object



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/cliffy/internal/run.rb', line 5

def self.run command, arguments, command_name, executable_name
  unless command.respond_to? :signature
    command.run
    return
  end

  true_values = ['yes', 'true', '1']
  false_values = ['no', 'false', '0']
  invalid_arguments_message = "Invalid arguments. Run `#{executable_name} help #{command_name}` for help."
  run_method = command.method :run
  signature = command.signature
  remaining_arguments = arguments.dup
  keyword_parameters = {}

  optional_symbols_and_data = signature.filter { |symbol, data| data[:kind] == :optional }
  unless optional_symbols_and_data.empty?
    options_and_symbols = optional_symbols_and_data.to_h do |symbol, data|
      option_name = symbol.to_s.gsub '_', '-'
      option = "--#{option_name}"
      [option, symbol]
    end
    remaining_options = options_and_symbols.keys.to_set
    options = optional_symbols_and_data.keys.map { |key| "--#{key.to_s.gsub '_', '-'}" }.to_set
    queued_arguments = []
    while ! remaining_arguments.empty?
      argument = remaining_arguments.pop
      if argument.start_with?('--') && remaining_options.include?(argument)
        symbol = options_and_symbols[argument]
        data_type = optional_symbols_and_data[symbol][:type]
        value = nil
        case data_type

        when :boolean
          if queued_arguments.empty?
            value = true
          end

        when :integer, :float, :string
          if queued_arguments.count == 1
            queued_argument = queued_arguments.first
            case data_type
            when :integer
              value = Integer queued_argument, exception: false
            when :float
              value = Float queued_argument, exception: false
            when :string
              value = queued_argument
            end
          end
  
        when Hash
          if queued_arguments.count == data_type.count
            sub_symbols_and_values = {}
            queued_arguments.zip(data_type).each do |queued_argument, sub_type|
              sub_value = nil
              case sub_type[1]
              when :boolean
                sub_value = true if true_values.include? argument
                sub_value = false if false_values.include? argument
              when :integer
                sub_value = Integer queued_argument, exception: false
              when :float
                sub_value = Float queued_argument, exception: false
              when :string
                sub_value = queued_argument
              end
              raise invalid_arguments_message if sub_value == nil
              sub_symbols_and_values[sub_type.first] = sub_value
            end
            value = sub_symbols_and_values
          end
  
        end
        raise invalid_arguments_message if value === nil
        keyword_parameters[symbol] = value
        remaining_options.delete argument
        queued_arguments = []
      else
        queued_arguments.unshift argument
      end
    end
    remaining_arguments = queued_arguments
  end

  strict_parsing = true
  if command.respond_to?(:configuration) && command.configuration.include?(:strict_parsing)
    strict_parsing = command.configuration[:strict_parsing]
  end
  if strict_parsing
    if remaining_arguments.any? { |argument| argument.start_with? '--' }
      raise invalid_arguments_message
    end
  end

  required_parameters = []
  run_method.parameters.each do |parameter|
    next unless parameter.first == :req
    data = signature[parameter[1]]
    argument = remaining_arguments.shift
    raise invalid_arguments_message if argument == nil
    value = nil
    case data[:type]
    when :boolean
      value = true if true_values.include? argument
      value = false if false_values.include? argument
    when :integer
      value = Integer argument, exception: false
    when :float
      value = Float argument, exception: false
    when :string
      value = argument
    end
    raise invalid_arguments_message if value == nil
    required_parameters << value
  end

  variadic_parameters = []
  variadic_data = signature.values.find { |data| data[:kind] == :variadic }
  if variadic_data
    data_type = variadic_data[:type]
    while ! remaining_arguments.empty?
      argument = remaining_arguments.shift
      value = nil
      case data_type
      when :boolean
        value = true if true_values.include? argument
        value = false if false_values.include? argument
      when :integer
        value = Integer argument, exception: false
      when :float
        value = Float argument, exception: false
      when :string
        value = argument
      end
      raise invalid_arguments_message if value == nil
      variadic_parameters << value
    end
    variadic_count = variadic_parameters.count
    minimum = variadic_data[:minimum]
    raise invalid_arguments_message if minimum && variadic_parameters.count < minimum
    maximum = variadic_data[:maximum]
    raise invalid_arguments_message if maximum && variadic_parameters.count > maximum
  else
    raise invalid_arguments_message unless remaining_arguments.empty?
  end

  positional_parameters = required_parameters + variadic_parameters
  command.run *positional_parameters, **keyword_parameters
end

.validate(command) ⇒ Object



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/cliffy/internal/validate.rb', line 3

def self.validate command
  prefix = "Error validating '#{command.class}' command:"
  raise "#{prefix} No run method." unless command.respond_to? :run
  raise "#{prefix} No description." unless command.respond_to? :description
  description = command.description
  unless description.is_a?(String) && ! description.empty? && description.lines.count == 1
    raise "#{prefix} Invalid description."
  end

  if command.respond_to? :notes
    notes = command.notes
    raise "#{prefix} Notes is not an array." unless notes.is_a?(Array)
    notes.each_with_index do |note, index|
      unless note.is_a?(String) && ! note.empty? && note.lines.count == 1
        raise "#{prefix} Invalid note at index #{index}."
      end
    end
  end

  parameters = command.method(:run).parameters
  return if parameters.empty?

  unless command.respond_to?(:signature) && command.signature.is_a?(Hash)
    raise "#{prefix} Signature missing or not defined correctly."
  end

  command.signature.each do |symbol, data|
    raise "#{prefix} signature key '#{symbol}' is not a symbol." unless symbol.is_a? Symbol
    data_prefix = "#{prefix} Signature data for '#{symbol}'"
    raise "#{data_prefix} is not a hash." unless data.is_a? Hash
    raise "#{data_prefix} does not have a kind." unless data.include? :kind
    raise "#{data_prefix} does not have a type." unless data.include? :type
    raise "#{data_prefix} does not have a description." unless data.include? :description

    data_kind = data[:kind]
    data_type = data[:type]
    data_description = data[:description]

    case data_kind
    when :required, :optional
      valid_data_keys = [:kind, :description, :type]
    when :variadic
      valid_data_keys = [:kind, :description, :type, :minimum, :maximum]
    else
      raise "#{data_prefix} has an invalid kind."
    end

    unless data.keys.to_set.subset? valid_data_keys.to_set
      raise "#{data_prefix} contains an unrecognized key."
    end
    unless data_description.is_a?(String) && ! data_description.empty? && data_description.lines.count == 1
      raise "#{data_prefix} does not have a valid description."
    end

    primitives = [:string, :integer, :float, :boolean]
    case data_kind
    when :required, :variadic
      unless primitives.include? data_type
        raise "#{data_prefix} is of kind '#{data_kind}' and must have a valid primitive type."
      end
    when :optional
      case data_type
      when :string, :integer, :float, :boolean
      when Hash
        unless data_type.keys.all? { |key| key.is_a? Symbol }
          raise "#{data_prefix} contains an invalid key in the value hash."
        end
        unless data_type.values.all? { |value| primitives.include? value }
          raise "#{data_prefix} contains an invalid value in the value hash."
        end
      else
        raise "#{data_prefix} is of kind 'optional' and must have a valid primitive or hash type."
      end
    end

    if data_kind == :variadic
      if data.include? :minimum
        minimum = data[:minimum]
        unless minimum.is_a?(Integer) && minimum >= 0
          raise "#{data_prefix} has an invalid minimum value."
        end
      else
        minimum = 0
      end
      if data.include? :maximum
        maximum = data[:maximum]
        unless maximum.is_a?(Integer) && maximum >= 0 && maximum > minimum
          raise "#{data_prefix} has an invalid maximum value."
        end
      end
    end
  end

  method_symbols = parameters.map { |parameter| parameter[1] }.to_set
  signature_symbols = command.signature.keys.to_set
  missing_method_symbols = signature_symbols - method_symbols
  missing_signature_symbols = method_symbols - signature_symbols
  unless missing_method_symbols.empty?
    raise "#{prefix} Symbols missing from run method that are in signature: #{missing_method_symbols.join ', '}"
  end
  unless missing_signature_symbols.empty?
    raise "#{prefix} Symbols missing from signature that are in run method: #{missing_signature_symbols.join ', '}"
  end

  found_req = false
  found_rest = false
  found_key = false
  parameters.each do |parameter|
    parameter_prefix = "#{prefix} '#{parameter[1]}'"
    case parameter.first
    when :req
      kind = :required
      found_req = true
      raise "#{prefix} Required positional after variadic." if found_rest
      raise "#{prefix} Required positional after required keyword." if found_key
    when :rest
      kind = :variadic
      found_rest = true
      raise "#{prefix} Variadic after required keyword." if found_key
    when :key
      kind = :optional
      found_key = true
    else
      raise "#{parameter_prefix} is not a supported kind of method parameter."
    end
    unless command.signature[parameter[1]][:kind] == kind
      raise "#{parameter_prefix} should have a kind of '#{kind}', in the signature."
    end
  end
end