pe-razor-client 0.14.0 → 0.15.2

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.
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