pe-razor-client 0.14.0 → 0.15.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/NEWS.md +25 -0
  3. data/bin/razor +9 -4
  4. data/lib/razor/cli.rb +10 -0
  5. data/lib/razor/cli/document.rb +59 -0
  6. data/lib/razor/cli/format.rb +81 -20
  7. data/lib/razor/cli/navigate.rb +121 -27
  8. data/lib/razor/cli/parse.rb +83 -10
  9. data/lib/razor/cli/transforms.rb +42 -0
  10. data/lib/razor/cli/version.rb +46 -0
  11. data/lib/razor/cli/views.rb +25 -0
  12. data/lib/razor/cli/views.yaml +196 -0
  13. data/spec/cli/format_spec.rb +99 -0
  14. data/spec/cli/navigate_spec.rb +111 -2
  15. data/spec/cli/parse_spec.rb +20 -0
  16. data/spec/fixtures/vcr/Razor_CLI_Navigate/argument_formatting/should_allow_spaces.yml +966 -0
  17. data/spec/fixtures/vcr/Razor_CLI_Navigate/for_command_help/should_provide_command_help_for_razor_--help_command_.yml +99 -0
  18. data/spec/fixtures/vcr/Razor_CLI_Navigate/for_command_help/should_provide_command_help_for_razor_-h_command_.yml +99 -0
  19. data/spec/fixtures/vcr/Razor_CLI_Navigate/for_command_help/should_provide_command_help_for_razor_command_--help_.yml +99 -0
  20. data/spec/fixtures/vcr/Razor_CLI_Navigate/for_command_help/should_provide_command_help_for_razor_command_-h_.yml +99 -0
  21. data/spec/fixtures/vcr/Razor_CLI_Navigate/for_command_help/should_provide_command_help_for_razor_command_help_.yml +99 -0
  22. data/spec/fixtures/vcr/Razor_CLI_Navigate/for_command_help/should_provide_command_help_for_razor_help_command_.yml +99 -0
  23. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_authentication/should_preserve_that_across_navigation.yml +9 -42
  24. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_authentication/should_supply_that_to_the_API_service.yml +5 -5
  25. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_invalid_parameter/should_fail_with_bad_JSON.yml +228 -0
  26. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_invalid_parameter/should_fail_with_malformed_argument.yml +120 -0
  27. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_multiple_arguments_with_same_name/combining_as_an_array/should_merge_an_array_into_an_existing_array.yml +2006 -0
  28. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_multiple_arguments_with_same_name/combining_as_an_array/should_merge_the_arguments_as_an_array.yml +2006 -0
  29. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_multiple_arguments_with_same_name/combining_as_an_array/should_merge_the_arguments_into_an_existing_array.yml +2006 -0
  30. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_multiple_arguments_with_same_name/combining_as_an_object/should_construct_a_json_object.yml +234 -0
  31. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_multiple_arguments_with_same_name/combining_as_an_object/should_construct_a_json_object_with_unicode.yml +412 -0
  32. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_multiple_arguments_with_same_name/combining_as_an_object/should_fail_with_mixed_types_array_then_hash_.yml +228 -0
  33. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_multiple_arguments_with_same_name/combining_as_an_object/should_fail_with_mixed_types_hash_then_array_.yml +164 -0
  34. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_no_parameters/should_fail_with_bad_JSON.yml +36 -0
  35. data/spec/fixtures/vcr/Razor_CLI_Parse/_new/_help/should_print_a_list_of_known_endpoints.yml +5 -5
  36. data/spec/spec_helper.rb +12 -1
  37. data/spec/testing.md +16 -0
  38. data/spec/version_spec.rb +8 -0
  39. metadata +67 -60
  40. data/.gitignore +0 -7
  41. data/.yardopts +0 -2
  42. data/Gemfile +0 -35
  43. data/Gemfile.lock +0 -53
  44. data/Rakefile +0 -37
  45. data/lib/razor/cli/navigate.yaml +0 -28
  46. data/pe-razor-client.gemspec +0 -32
  47. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_a_single_item_path/.yml +0 -69
  48. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_an_invalid_path/.yml +0 -36
  49. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_no_path/.yml +0 -36
  50. data/spec/fixtures/vcr/Razor_CLI_Parse/_new/_help/.yml +0 -36
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 56d3c2b6b7151fc0b1e41bc7b6761b9e7bc46b04
4
- data.tar.gz: 28c812344e73fefc6a6634c9688e302afefaa716
3
+ metadata.gz: ec9ee14e06b62d0312820a7ca7e00b614f00c1ee
4
+ data.tar.gz: a0a3bb05a69607f877708e427ef8e25d13a743f5
5
5
  SHA512:
6
- metadata.gz: 8261b52410c1685770bb406d68698d541c1e986c6b249b21d9cbda8255763a130de0808f21e021228388978482cf9b3c1da6fdf6c226edaf5ce2b4697e49ab34
7
- data.tar.gz: 37f1d1535ddcb8d12a405548912dbfcec7a1cdea73b39d39e1df0310ee181ef706e4b08420f4159c1d5f7fa516514d54514c92bc753b7c2d72fd1aff2558b20c
6
+ metadata.gz: 0780b03661a1692be351db757611ab91b2592d6d4f54d400634558944dea6745e28a8b0c9a28c330545a4a28af20b58c6ec62d87c2f90c35e639a5108249d67a
7
+ data.tar.gz: c339b1b83dbabe6e3752b302fd8e916f5260b619b1e9861a08271ee63eda825f453d97477187d49349ff83486595f11850c42c6673dde83ef082f58af9bd9140
data/NEWS.md ADDED
@@ -0,0 +1,25 @@
1
+ # Razor Client Release Notes
2
+
3
+ ## 0.15.1 - 2014-06-12
4
+
5
+ Server version compatibility
6
+
7
+ * It is highly recommended that razor-client version 0.15.x be used with
8
+ razor-server version 0.15.x or higher.
9
+
10
+ ## 0.15.0 - 2014-05-22
11
+
12
+ Usability of the client has been greatly enhanced:
13
+
14
+ * Tabular views of most collections: things like 'razor nodes' now display
15
+ a table of results with important details about each node.
16
+ * Get help on commands via `razor help COMMAND`
17
+ * Output now includes hints on how to get more details on the things displayed
18
+ * No need to enter JSON on the command line for most commands (all but
19
+ create-tag)
20
+ + arrays can now be entered by repeating the same option, e.g. `razor
21
+ create-tag --name ... --tag t1 --tag t2`
22
+ + broker configuration is set using `razor create-broker --name
23
+ .. --configuration var1=value1 --configuration var2=value2 ...`
24
+ * Clearer error message when server responds with 'Unauthorized'
25
+ (RAZOR-175)
data/bin/razor CHANGED
@@ -27,7 +27,12 @@ rescue OptionParser::InvalidOption => e
27
27
  die e.message + "\nTry 'razor --help' for more information"
28
28
  end
29
29
 
30
- if parse.show_help?
30
+ if parse.show_version?
31
+ puts parse.version
32
+ exit 0
33
+ end
34
+
35
+ if parse.show_help? and not parse.show_command_help?
31
36
  puts parse.help
32
37
  exit 0
33
38
  end
@@ -35,9 +40,7 @@ end
35
40
  begin
36
41
  document = parse.navigate.get_document
37
42
  url = parse.navigate.last_url
38
- puts "From #{url}:\n\n#{format_document document}\n\n"
39
- rescue Razor::CLI::Error => e
40
- die "#{e}\n#{parse.help}\n\n"
43
+ puts "From #{url}:\n\n#{format_document document, parse}\n\n"
41
44
  rescue SocketError, Errno::ECONNREFUSED => e
42
45
  puts "Error: Could not connect to the server at #{parse.api_url}"
43
46
  puts " #{e}\n"
@@ -58,4 +61,6 @@ rescue RestClient::Exception => e
58
61
  puts r.body
59
62
  end
60
63
  die
64
+ rescue StandardError => e
65
+ die "#{e.message}\nTry 'razor --help' for more information\n\n"
61
66
  end
data/lib/razor/cli.rb CHANGED
@@ -26,9 +26,19 @@ module Razor
26
26
  end
27
27
  end
28
28
 
29
+ class VersionCompatibilityError < Error
30
+ def initialize(reason)
31
+ super "Server version is not compatible with client version: #{reason}"
32
+ end
33
+ end
34
+
29
35
  end
30
36
  end
31
37
 
38
+ require_relative 'cli/version'
32
39
  require_relative 'cli/navigate'
33
40
  require_relative 'cli/parse'
34
41
  require_relative 'cli/format'
42
+ require_relative 'cli/document'
43
+ require_relative 'cli/views'
44
+ require_relative 'cli/transforms'
@@ -0,0 +1,59 @@
1
+ require 'forwardable'
2
+
3
+ module Razor::CLI
4
+ class Document
5
+ extend Forwardable
6
+ attr_reader 'spec', 'items', 'format_view', 'original_items'
7
+ def initialize(doc, format_type)
8
+ if doc['spec'].is_a?(Array)
9
+ @spec, @remaining_navigation = doc['spec']
10
+ else
11
+ @spec = doc['spec']
12
+ end
13
+ @command = doc['command']
14
+ if doc.has_key?('items')
15
+ @type = :list
16
+ else
17
+ @type = :single
18
+ end
19
+ @items = doc['items'] || Array[doc]
20
+ @format_view = Razor::CLI::Views.find_formatting(@spec, format_type, @remaining_navigation)
21
+
22
+ # Untransformed and unordered for displaying nested views.
23
+ @original_items = @items
24
+ @items = hide_or_transform_elements!(items, format_view)
25
+ end
26
+
27
+ def is_list?
28
+ @type == :list
29
+ end
30
+
31
+ private
32
+ # This method:
33
+ # - rearranges columns per Razor::CLI::Views.
34
+ # - hides columns per Razor::CLI::Views.
35
+ # - transforms data using both Razor::CLI::Views and its `TRANSFORMS`.
36
+ def hide_or_transform_elements!(items, format_view)
37
+ if format_view.has_key?('+show')
38
+ items.map do |item|
39
+ Hash[
40
+ format_view['+show'].map do |item_format_spec|
41
+ # Allow both '+column' as overrides.
42
+ item_spec = (item_format_spec[1] or {})
43
+ item_label = item_format_spec[0]
44
+ item_column = (item_spec['+column'] or item_label)
45
+ value = Razor::CLI::Views.transform(item[item_column], item_spec['+format'])
46
+
47
+ [item_label, value]
48
+ end
49
+ ].tap do |hash|
50
+ # Re-add the special 'command' key and value if the key isn't already there.
51
+ hash['command'] = @command if @command and not hash.has_key?('command')
52
+ end
53
+ end
54
+ else
55
+ items
56
+ end
57
+ end
58
+ end
59
+ end
@@ -1,8 +1,10 @@
1
+ require 'forwardable'
1
2
  require 'terminal-table'
2
3
 
3
4
  module Razor::CLI
4
5
  module Format
5
- PriorityKeys = %w[ id name ]
6
+ extend Forwardable
7
+ PriorityKeys = %w[ id name spec ]
6
8
  SpecNames = {
7
9
  "/spec/object/policy" => "Policy",
8
10
  "/spec/object/tag" => "Tag",
@@ -16,12 +18,50 @@ module Razor::CLI
16
18
  spec
17
19
  end
18
20
 
19
- def format_document(doc)
20
- case doc
21
- when Array then format_objects(doc)
22
- when Hash then format_object(doc)
23
- else doc.to_s
24
- end.chomp
21
+ def format_document(doc, parse = nil)
22
+ format = parse && parse.format
23
+ arguments = parse && parse.args
24
+ doc = Razor::CLI::Document.new(doc, format)
25
+
26
+ return "There are no items for this query." if doc.items.empty?
27
+ return format_objects(doc.items).chomp if parse && parse.show_command_help?
28
+
29
+ case (doc.format_view['+layout'] or 'list')
30
+ when 'list'
31
+ format_objects(doc.items) + String(additional_details(doc, parse, arguments)).chomp
32
+ when 'table'
33
+ case doc.items
34
+ when Array then
35
+ get_table(doc.items, doc.format_view) + String(additional_details(doc, parse, arguments))
36
+ else doc.to_s
37
+ end
38
+ else
39
+ raise ArgumentError, "Unrecognized view format #{doc.format_view['+layout']}"
40
+ end
41
+ end
42
+
43
+ private
44
+ def get_table(doc, formatting)
45
+ # Use the formatting if it exists, otherwise build from the data.
46
+ headings = (formatting['+show'] and formatting['+show'].keys or get_headers(doc))
47
+ Terminal::Table.new do |table|
48
+ table.rows = doc.map do |page|
49
+ headings.map do |heading|
50
+ page[heading]
51
+ end
52
+ end
53
+ table.headings = headings
54
+ end.to_s
55
+ end
56
+
57
+ def get_headers(doc)
58
+ [].tap do |headers|
59
+ doc.map do |page|
60
+ page.map do |item|
61
+ headers << item[0] unless headers.include?(item[0])
62
+ end
63
+ end
64
+ end
25
65
  end
26
66
 
27
67
  # We assume that all collections are homogenous
@@ -31,21 +71,16 @@ module Razor::CLI
31
71
  end.join "\n\n"
32
72
  end
33
73
 
34
- def format_reference_object(ref, indent = 0)
35
- output = ' '* indent + "#{ref['name']} => #{ref['id'].to_s.ljust 4}"
36
- end
37
-
38
-
39
74
  def format_object(object, indent = 0)
40
- if object.keys == ["id", "name"]
41
- format_reference_object(object, indent)
75
+ if object.has_key?('help') and object.has_key?('name')
76
+ object['help']['full']
42
77
  else
43
78
  format_default_object(object, indent)
44
79
  end
45
80
  end
46
81
 
47
82
  def format_default_object(object, indent = 0 )
48
- fields = (PriorityKeys & object.keys) + (object.keys - PriorityKeys)
83
+ fields = display_fields(object)
49
84
  key_indent = indent + fields.map {|f| f.length}.max
50
85
  output = ""
51
86
  fields.map do |f|
@@ -59,11 +94,13 @@ module Razor::CLI
59
94
  "\n" + format_object(value, key_indent + 4).rstrip
60
95
  end
61
96
  when Array
62
- if value.all? { |v| v.is_a?(String) }
63
- "[" + value.map(&:inspect).join(",") + "]"
64
- else
65
- "[\n" + format_objects(value, key_indent + 6) + ("\n"+' '*(key_indent+4)+"]")
66
- end
97
+ if value.all? { |v| v.is_a?(String) }
98
+ "[" + value.map(&:to_s).join(",") + "]"
99
+ else
100
+ "[\n" + format_objects(value, key_indent + 6) + ("\n"+' '*(key_indent+4)+"]")
101
+ end
102
+ when String
103
+ value
67
104
  else
68
105
  case f
69
106
  when "spec" then "\"#{Format.spec_name(value)}\""
@@ -72,5 +109,29 @@ module Razor::CLI
72
109
  end
73
110
  end.join "\n"
74
111
  end
112
+
113
+ def display_fields(object)
114
+ keys = object.respond_to?(:keys) ? object.keys : []
115
+ (PriorityKeys & keys) + (keys - PriorityKeys) - ['+spec']
116
+ end
117
+
118
+ def additional_details(doc, parse, arguments)
119
+ objects = doc.original_items
120
+ if objects.empty? or (parse and not parse.query?)
121
+ ""
122
+ elsif doc.is_list? and objects.all? { |it| it.is_a?(Hash) && it.has_key?('name')}
123
+ # If every element has the 'name' key, it has nested elements.
124
+ "\n\nQuery an entry by including its name, e.g. `razor #{arguments.join(' ')} #{objects.first['name']}`"
125
+ elsif objects.any?
126
+ object = objects.first
127
+ fields = display_fields(object) - PriorityKeys
128
+ list = fields.select do |f|
129
+ object[f].is_a?(Hash) or object[f].is_a?(Array)
130
+ end.sort
131
+ if list.any?
132
+ "\n\nQuery additional details via: `razor #{arguments.join(' ')} [#{list.join(', ')}]`"
133
+ end
134
+ end
135
+ end
75
136
  end
76
137
  end
@@ -1,9 +1,11 @@
1
1
  require 'rest-client'
2
2
  require 'multi_json'
3
3
  require 'yaml'
4
+ require 'forwardable'
4
5
 
5
6
  module Razor::CLI
6
7
  class Navigate
8
+ extend Forwardable
7
9
 
8
10
  def initialize(parse, segments)
9
11
  @parse = parse
@@ -28,18 +30,24 @@ module Razor::CLI
28
30
  entrypoint["commands"]
29
31
  end
30
32
 
33
+ def server_version
34
+ entrypoint.has_key?('version') and entrypoint['version']['server'] or 'Unknown'
35
+ end
36
+
31
37
  def query?
32
- collections.any? { |coll| coll["name"] == @segments.first }
38
+ @query ||= collections.any? { |coll| coll["name"] == @segments.first }
33
39
  end
34
40
 
35
41
  def command(name)
36
- commands.find { |coll| coll["name"] == name }
42
+ @command ||= commands.find { |coll| coll["name"] == name }
37
43
  end
38
44
 
39
45
  def command?
40
46
  !! command(@segments.first)
41
47
  end
42
48
 
49
+ def_delegator '@parse', 'show_command_help?'
50
+
43
51
  def get_document
44
52
  if @segments.empty?
45
53
  entrypoint
@@ -48,15 +56,18 @@ module Razor::CLI
48
56
  while @segments.any?
49
57
  move_to @segments.shift
50
58
  end
59
+
60
+ # Get the next level if it's a list of objects.
61
+ if @doc.is_a?(Hash) and @doc['items'].is_a?(Array)
62
+ @doc['items'] = @doc['items'].map do |item|
63
+ item.is_a?(Hash) && item.has_key?('id') ? json_get(item['id']) : item
64
+ end
65
+ end
51
66
  @doc
52
67
  elsif command?
53
68
  # @todo lutter 2013-08-16: None of this has any tests, and error
54
69
  # handling is heinous at best
55
70
  cmd, body = extract_command
56
- if body.empty?
57
- raise Razor::CLI::Error,
58
- "No arguments for command (did you forget --json ?)"
59
- end
60
71
  # Ensure that we copy authentication data from our previous URL.
61
72
  url = cmd["id"]
62
73
  if @doc_url
@@ -64,7 +75,18 @@ module Razor::CLI
64
75
  url.userinfo = @doc_url.userinfo
65
76
  end
66
77
 
67
- json_post(url, body)
78
+ if show_command_help?
79
+ json_get(url)
80
+ else
81
+ if body.empty?
82
+ raise Razor::CLI::Error,
83
+ "No arguments for command (did you forget --json ?)"
84
+ end
85
+ result = json_post(url, body)
86
+ # Get actual object from the id.
87
+ result = result.merge(json_get(result['id'])) if result['id']
88
+ result
89
+ end
68
90
  else
69
91
  raise NavigationError.new(@doc_url, @segments, @doc)
70
92
  end
@@ -72,12 +94,36 @@ module Razor::CLI
72
94
 
73
95
  def extract_command
74
96
  cmd = command(@segments.shift)
97
+ @cmd_url = cmd['id']
75
98
  body = {}
76
99
  until @segments.empty?
77
- if @segments.shift =~ /\A--([a-z-]+)(=(\S+))?\Z/
100
+ argument = @segments.shift
101
+ if argument =~ /\A--([a-z-]+)(=(.+))?\Z/
102
+ # `--arg=value` or `--arg value`
78
103
  arg, value = [$1, $3]
79
104
  value = @segments.shift if value.nil? && @segments[0] !~ /^--/
80
- body[arg] = convert_arg(cmd["name"], arg, value)
105
+ if value =~ /\A(.+?)=(\S+)?\z/
106
+ # `--arg name=value`
107
+ unless body[arg].nil? or body[arg].is_a?(Hash)
108
+ # Error: `--arg value --arg name=value`
109
+ raise ArgumentError, "Cannot handle mixed types for argument #{arg}"
110
+ end
111
+ # Do not convert, assume the above is the conversion.
112
+ body[arg] = (body[arg].nil? ? {} : body[arg]).merge($1 => $2)
113
+ elsif body[arg].is_a?(Hash)
114
+ # Error: `--arg name=value --arg value`
115
+ raise ArgumentError, "Cannot handle mixed types for argument #{arg}"
116
+ else
117
+ value = convert_arg(cmd["name"], arg, value)
118
+ if body[arg].nil?
119
+ body[arg] = value
120
+ else
121
+ # Either/both `body[arg]` or/and `value` might be an array at this point.
122
+ body[arg] = Array(body[arg]) + Array(value)
123
+ end
124
+ end
125
+ else
126
+ raise ArgumentError, "Unexpected argument #{argument}"
81
127
  end
82
128
  end
83
129
 
@@ -89,15 +135,16 @@ module Razor::CLI
89
135
  raise Razor::CLI::Error, "File #{body["json"]} not found"
90
136
  rescue Errno::EACCES
91
137
  raise Razor::CLI::Error,
92
- "Permission to read file #{body["json"]} denied"
138
+ "Permission to read file #{body["json"]} denied"
93
139
  end
94
140
  [cmd, body]
95
141
  end
96
142
 
97
143
  def move_to(key)
98
- key = key.to_i if key.to_i.to_s == key
99
144
  if @doc.is_a? Array
100
145
  obj = @doc.find {|x| x.is_a?(Hash) and x["name"] == key }
146
+ elsif @doc.is_a?(Hash) && @doc['items'].is_a?(Array)
147
+ obj = @doc['items'].find {|x| x.is_a?(Hash) and x["name"] == key }
101
148
  elsif @doc.is_a? Hash
102
149
  obj = @doc[key]
103
150
  end
@@ -112,13 +159,27 @@ module Razor::CLI
112
159
  end
113
160
 
114
161
  @doc = json_get(url)
115
- # strip the wrapper around collections
116
- if @doc.is_a? Hash and @doc["items"].is_a? Array
117
- @doc = @doc["items"]
118
- end
119
162
  @doc_url = url
120
- elsif obj.is_a? Hash
163
+ elsif obj.is_a?(Hash) && obj['spec']
121
164
  @doc = obj
165
+ elsif obj.is_a?(Hash)
166
+ # No spec string; use parent's and remember extra navigation.
167
+ if @doc['+spec'].is_a?(Array)
168
+ # Something's been added.
169
+ @doc['+spec'] << key
170
+ elsif @doc['+spec'].nil? || @doc['+spec'].is_a?(String)
171
+ @doc['+spec'] = [@doc['spec'], key]
172
+ end
173
+ @doc = obj.merge({'+spec' => @doc['+spec']})
174
+ elsif obj.is_a?(Array)
175
+ # No spec string; use parent's and remember extra navigation.
176
+ if @doc['+spec'].is_a?(Array)
177
+ # Something's already been added.
178
+ @doc['+spec'] << key
179
+ elsif @doc['+spec'].nil? || @doc['+spec'].is_a?(String)
180
+ @doc['+spec'] = [@doc['spec'], key]
181
+ end
182
+ @doc = {'+spec' => @doc['+spec'], 'items' => obj}
122
183
  else
123
184
  @doc = nil
124
185
  end
@@ -133,7 +194,7 @@ module Razor::CLI
133
194
  def json_get(url, headers = {})
134
195
  response = get(url,headers.merge(:accept => :json))
135
196
  unless response.headers[:content_type] =~ /application\/json/
136
- raise "Received content type #{response.headers[:content_type]}"
197
+ raise "Received content type #{response.headers[:content_type]}"
137
198
  end
138
199
  MultiJson.load(response.body)
139
200
  end
@@ -152,25 +213,58 @@ module Razor::CLI
152
213
  end
153
214
 
154
215
  private
155
- def self.annotations
156
- @@annotations ||=
157
- YAML::load_file(File::join(File::dirname(__FILE__), "navigate.yaml"))
216
+ def cmd_schema(cmd_name)
217
+ begin
218
+ json_get(@cmd_url)['schema']
219
+ rescue RestClient::ResourceNotFound => _
220
+ raise VersionCompatibilityError, 'Server must supply the expected datatypes for command arguments; use `--json` or upgrade razor-server'
221
+ end
158
222
  end
159
223
 
160
- def self.arg_type(cmd_name, arg_name)
161
- cmd = annotations["commands"][cmd_name]
162
- cmd && cmd["args"][arg_name]
224
+ def arg_type(cmd_name, arg_name)
225
+ # Short-circuit to allow this as a work-around for backwards compatibility.
226
+ return nil if arg_name == 'json'
227
+ cmd = cmd_schema(cmd_name)
228
+ cmd && cmd[arg_name] && cmd[arg_name]['type'] or nil
163
229
  end
164
230
 
165
231
  def convert_arg(cmd_name, arg_name, value)
166
232
  value = nil if value == "null"
167
- case self.class.arg_type(cmd_name, arg_name)
168
- when "json"
169
- MultiJson::load(value)
233
+
234
+ argument_type = arg_type(cmd_name, arg_name)
235
+
236
+ # This might be helpful, since there's no other method for debug-level logging on the client.
237
+ puts "Formatting argument #{arg_name} with value #{value} as #{argument_type}\n" if @parse.dump_response?
238
+
239
+ case argument_type
240
+ when "array"
241
+ # 'array' datatype arguments will never fail. At worst, they'll be wrapped in an array.
242
+ begin
243
+ MultiJson::load(value)
244
+ rescue MultiJson::LoadError => _
245
+ Array(value)
246
+ end
247
+ when "object"
248
+ begin
249
+ MultiJson::load(value)
250
+ rescue MultiJson::LoadError => error
251
+ raise ArgumentError, "Invalid JSON for argument '#{arg_name}': #{error.message}"
252
+ end
170
253
  when "boolean"
171
254
  ["true", nil].include?(value)
172
- else
255
+ when "number"
256
+ begin
257
+ Integer(value)
258
+ rescue ArgumentError
259
+ raise ArgumentError, "Invalid integer for argument '#{arg_name}': #{value}"
260
+ end
261
+ when "null"
262
+ raise ArgumentError, "Expected nothing for argument '#{arg_name}', but was: '#{value}'" unless value.nil?
263
+ nil
264
+ when "string", nil # `nil` for 'might be an alias, send as-is'
173
265
  value
266
+ else
267
+ raise Razor::CLI::Error, "Unexpected datatype '#{argument_type}' for argument #{arg_name}"
174
268
  end
175
269
  end
176
270
  end