haveapi-client 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1f73740e620eeb403f3cdec3491598f9b48bcfc4
4
- data.tar.gz: 3e4d8544316b4a47f948f573de365055021725fc
3
+ metadata.gz: 8c2c9e15337f0d495ac86049fee2e76cf545957c
4
+ data.tar.gz: 9eaf5f13ad4c6d128a55b7efb3795b2840dd64dd
5
5
  SHA512:
6
- metadata.gz: 037eee5721e264c732be091ee8fea13e0c96c63e824c8422b4a31e21b503f7e5be3e27114984e71dca37c7237f6c3a8f9392a7206bdd9e381757c0c42fac8d86
7
- data.tar.gz: c891d03e167e84a0d7103498a65bfcee3146094de96124d998ea5a4d74d182001dc6609c6e0a58d6d39e327a52e04580cc36662287b8633d8e38345e200f88a0
6
+ metadata.gz: 7fa14b35b737f100c15ae7c8091e2abb18f9f8b3286ac006e723484f2c967dbb9f0683606fc7cda477c6cbc7d34ed9aabbe9bba83200a01608c3a9b04c825589
7
+ data.tar.gz: b3ac301b6d97864c2a1ddf909a121efb23aa5ef419be24f9e7ab0a436c16085f156f23ebaff4ed394f67c12665685323c70a7e3e95e147f672a768dc589130c7
data/README.md CHANGED
@@ -17,8 +17,7 @@ Or install it yourself as:
17
17
 
18
18
  $ gem install haveapi-client
19
19
 
20
- ## Usage
21
- ### CLI
20
+ ## CLI
22
21
  $ haveapi-cli -h
23
22
  Usage: haveapi-cli [options] <resource> <action> [objects ids] [-- [parameters]]
24
23
  -u, --api URL API URL
@@ -28,50 +27,72 @@ Or install it yourself as:
28
27
  List available authentication methods
29
28
  --list-resources [VERSION] List all resource in API version
30
29
  --list-actions [VERSION] List all resources and actions in API version
30
+ --version VERSION Use specified API version
31
31
  -r, --raw Print raw response as is
32
32
  -s, --save Save credentials to config file for later use
33
33
  -v, --[no-]verbose Run verbosely
34
+ --client-version Show client version
34
35
  -h, --help Show this message
35
36
 
36
37
  Using the API example from
37
- [HaveAPI README](https://github.com/vpsfreecz/haveapi/blob/master/README.md#example),
38
+ [HaveAPI README](https://github.com/vpsfreecz/haveapi#example),
38
39
  users would be listed with:
39
40
 
40
- $ haveapi-cli --url https://your.api.tld --auth basic --username yourname --password yourpassword user index
41
+ $ haveapi-cli --url https://your.api.tld --auth basic --username yourname --password yourpassword user list
41
42
 
42
43
  Nested resources and object IDs:
43
44
 
44
- $ haveapi-cli --url https://your.api.tld --auth basic --username yourname --password yourpassword user.invoice index 10
45
+ $ haveapi-cli --url https://your.api.tld --auth basic --username yourname --password yourpassword user.invoice list 10
45
46
 
46
47
  where `10` is user ID.
47
48
 
48
49
  User credentials can be saved to a config:
49
50
 
50
- $ haveapi-cli --url https://your.api.tld --auth basic --username yourname --password yourpassword --save user index
51
+ $ haveapi-cli --url https://your.api.tld --auth basic --username yourname --password yourpassword --save user list
51
52
 
52
53
  When saved, they don't have to be specified as command line options:
53
54
 
54
- $ haveapi-cli --url https://your.api.tld user index
55
+ $ haveapi-cli --url https://your.api.tld user list
55
56
 
56
- ### Client library
57
+ List options specific to authentication methods:
58
+
59
+ $ haveapi-cli --url https://your.api.tld --auth basic -h
60
+ $ haveapi-cli --url https://your.api.tld --auth token -h
61
+
62
+ List action parameters with examples:
63
+
64
+ $ haveapi-cli --url https://your.api.tld user new -h
65
+
66
+ Provide action parameters (notice the ``--`` separator):
67
+
68
+ $ haveapi-cli --url https://your.api.tld user new -- --login mylogin --full-name "My Full Name" --role user
69
+
70
+
71
+ ## Client library
57
72
  ```ruby
58
73
  require 'haveapi/client'
59
74
 
60
75
  api = HaveAPI::Client::Client.new('https://your.api.tld')
61
76
  api.authenticate(:basic, user: 'yourname', password: 'yourpassword')
62
77
 
63
- response = api.user.index
64
- p response.ok?
65
- p response.response
78
+ api.user.list.each do |user|
79
+ puts user.login
80
+ end
66
81
 
67
- p api.user(10).invoice
68
- p api.user(10).delete
69
- p api.user(10).delete!
70
- p api.user.delete(10)
82
+ user = api.user.find(10)
83
+ p user.invoice
84
+ user.destroy
71
85
 
72
86
  p api.user.create({
73
87
  login: 'mylogin',
74
88
  full_name: 'Very Full Name',
75
89
  role: 'user'
76
90
  })
91
+
92
+ user = api.user.new
93
+ user.login = 'mylogin'
94
+ user.full_name = 'Very Full Name'
95
+ user.role = 'user'
96
+ user.save
97
+ p user.id
77
98
  ```
@@ -26,5 +26,5 @@ Gem::Specification.new do |spec|
26
26
  spec.add_runtime_dependency 'rest_client', '~> 1.7.3'
27
27
  spec.add_runtime_dependency 'json', '~> 1.8.1'
28
28
  spec.add_runtime_dependency 'highline', '~> 1.6.21'
29
- spec.add_runtime_dependency 'table_print', '~> 1.5.1'
29
+ spec.add_runtime_dependency 'table_print', '~> 1.5.3'
30
30
  end
@@ -15,9 +15,15 @@ module HaveAPI::CLI::Authentication
15
15
  @token = t
16
16
  end
17
17
 
18
- opts.on('--token-validity SECONDS', Integer,
19
- 'How long will token be valid in seconds, 0 for forever') do |s|
20
- @validity = s
18
+ opts.on('--token-lifetime LIFETIME',
19
+ %i(fixed renewable_manual renewable_auto permanent),
20
+ 'Token lifetime, defaults to renewable_auto') do |l|
21
+ @lifetime = l
22
+ end
23
+
24
+ opts.on('--token-interval SECONDS', Integer,
25
+ 'How long will token be valid in seconds') do |s|
26
+ @interval = s
21
27
  end
22
28
 
23
29
  opts.on('--new-token', 'Request new token') do
@@ -49,7 +55,8 @@ module HaveAPI::CLI::Authentication
49
55
  user: @user,
50
56
  password: @password,
51
57
  token: @token,
52
- validity: @validity,
58
+ lifetime: @lifetime || :renewable_auto,
59
+ interval: @interval,
53
60
  valid_to: @valid_to,
54
61
  via: @via
55
62
  })
@@ -58,7 +65,7 @@ module HaveAPI::CLI::Authentication
58
65
  def save
59
66
  super.update({
60
67
  via: @via,
61
- validity: @validity
68
+ interval: @interval
62
69
  })
63
70
  end
64
71
  end
@@ -24,7 +24,7 @@ module HaveAPI
24
24
  @config = read_config || {}
25
25
  args, @opts = options
26
26
 
27
- @api = HaveAPI::Client::Communicator.new(api_url)
27
+ @api = HaveAPI::Client::Communicator.new(api_url, @opts[:version])
28
28
  @api.identity = $0.split('/').last
29
29
 
30
30
  if @action
@@ -46,10 +46,7 @@ module HaveAPI
46
46
  exit(true)
47
47
  end
48
48
 
49
- action = translate_action(args[1].to_sym)
50
-
51
- action = @api.get_action(resources, action, args[2..-1])
52
-
49
+ action = @api.get_action(resources, args[1].to_sym, args[2..-1])
53
50
  action.update_description(@api.describe_action(action)) if authenticate(action)
54
51
 
55
52
  @input_params = parameters(action)
@@ -63,15 +60,18 @@ module HaveAPI
63
60
 
64
61
  if ret[:status]
65
62
  format_output(action, ret[:response])
63
+
66
64
  else
67
65
  warn "Action failed: #{ret[:message]}"
68
66
 
69
- if ret[:errors].any?
67
+ if ret[:errors] && ret[:errors].any?
70
68
  puts 'Errors:'
71
69
  ret[:errors].each do |param, e|
72
70
  puts "\t#{param}: #{e.join('; ')}"
73
71
  end
74
72
  end
73
+
74
+ print_examples(action)
75
75
  end
76
76
 
77
77
  else
@@ -119,6 +119,10 @@ module HaveAPI
119
119
  @action = [:list_actions, v && v.sub(/^v/, '')]
120
120
  end
121
121
 
122
+ opts.on('--version VERSION', 'Use specified API version') do |v|
123
+ options[:version] = v
124
+ end
125
+
122
126
  opts.on('-r', '--raw', 'Print raw response as is') do
123
127
  options[:raw] = true
124
128
  end
@@ -131,6 +135,10 @@ module HaveAPI
131
135
  options[:verbose] = v
132
136
  end
133
137
 
138
+ opts.on('--client-version', 'Show client version') do
139
+ @action = [:show_version]
140
+ end
141
+
134
142
  opts.on('-h', '--help', 'Show this message') do
135
143
  options[:help] = true
136
144
  end
@@ -178,8 +186,11 @@ module HaveAPI
178
186
  if @opts[:help]
179
187
  puts @global_opt.help
180
188
  puts ''
189
+ puts 'Action description:'
190
+ puts action.description, "\n"
181
191
  print 'Action parameters:'
182
192
  puts @action_opt.help
193
+ print_examples(action)
183
194
  exit
184
195
  end
185
196
 
@@ -204,56 +215,46 @@ module HaveAPI
204
215
  ret
205
216
  end
206
217
 
207
- def translate_action(action)
208
- tr = {
209
- list: :index,
210
- new: :create,
211
- change: :update
212
- }
213
-
214
- if tr.has_key?(action)
215
- return tr[action]
216
- end
217
-
218
- action
219
- end
220
-
221
218
  def list_versions
222
- desc = @api.describe_api
219
+ desc = @api.available_versions
223
220
 
224
221
  desc[:versions].each do |v, _|
225
222
  next if v == :default
226
223
 
227
224
  v_int = v.to_s.to_i
228
225
 
229
- puts "#{v_int == desc[:default_version] ? '*' : ' '} v#{v}"
226
+ puts "#{v_int == desc[:default] ? '*' : ' '} v#{v}"
230
227
  end
231
228
  end
232
229
 
233
230
  def list_auth(v=nil)
234
- desc = @api.describe_api
231
+ desc = @api.describe_api(v)
235
232
 
236
- desc[:versions][(v && v.to_sym) || desc[:default_version].to_s.to_sym][:authentication].each_key do |auth|
233
+ desc[:authentication].each_key do |auth|
237
234
  puts auth if Cli.auth_methods.has_key?(auth)
238
235
  end
239
236
  end
240
237
 
241
238
  def list_resources(v=nil)
242
- desc = @api.describe_api
239
+ desc = @api.describe_api(v)
243
240
 
244
- desc[:versions][(v && v.to_sym) || desc[:default_version].to_s.to_sym][:resources].each do |resource, children|
241
+ desc[:resources].each do |resource, children|
245
242
  nested_resource(resource, children, false)
246
243
  end
247
244
  end
248
245
 
249
246
  def list_actions(v=nil)
250
- desc = @api.describe_api
247
+ desc = @api.describe_api(v)
251
248
 
252
- desc[:versions][(v && v.to_sym) || desc[:default_version].to_s.to_sym][:resources].each do |resource, children|
249
+ desc[:resources].each do |resource, children|
253
250
  nested_resource(resource, children, true)
254
251
  end
255
252
  end
256
253
 
254
+ def show_version
255
+ puts HaveAPI::Client::VERSION
256
+ end
257
+
257
258
  def describe_resource(path)
258
259
  desc = @api.describe_resource(path)
259
260
 
@@ -295,7 +296,14 @@ module HaveAPI
295
296
  end
296
297
  end
297
298
 
298
- def format_output(action, response)
299
+ def print_examples(action)
300
+ unless action.examples.empty?
301
+ puts "\nExamples:\n"
302
+ ExampleFormatter.format_examples(self, action)
303
+ end
304
+ end
305
+
306
+ def format_output(action, response, out = $>)
299
307
  if @opts[:raw]
300
308
  puts response
301
309
  return
@@ -303,15 +311,23 @@ module HaveAPI
303
311
 
304
312
  return if response.empty?
305
313
 
314
+ tp.set :io, out
315
+
306
316
  namespace = action.namespace(:output).to_sym
307
317
 
308
- case action.layout.to_sym
309
- when :list
318
+ case action.output_layout.to_sym
319
+ when :object_list, :hash_list
310
320
  cols = []
311
321
 
312
322
  action.params.each do |name, p|
313
323
  if p[:type] == 'Resource'
314
- cols << {name => {display_method: ->(r) { r[name] && r[name][p[:value_label].to_sym] } }}
324
+ cols << {
325
+ name => {
326
+ display_method: ->(r) {
327
+ r[name] && "#{r[name][p[:value_label].to_sym]} (##{r[name][p[:value_id].to_sym]})"
328
+ }
329
+ }
330
+ }
315
331
  else
316
332
  cols << name
317
333
  end
@@ -320,19 +336,19 @@ module HaveAPI
320
336
  tp response[namespace], *cols
321
337
 
322
338
 
323
- when :object
339
+ when :object, :hash
324
340
  response[namespace].each do |k, v|
325
341
 
326
342
  if action.params[k][:type] == 'Resource'
327
- puts "#{k}: #{v[action.params[k][:value_label].to_sym]}"
343
+ out << "#{k}: #{v[action.params[k][:value_label].to_sym]}\n"
328
344
  else
329
- puts "#{k}: #{v}"
345
+ out << "#{k}: #{v}\n"
330
346
  end
331
347
  end
332
348
 
333
349
 
334
350
  when :custom
335
- pp response[namespace]
351
+ PP.pp(response[namespace], out)
336
352
 
337
353
  end
338
354
  end
@@ -0,0 +1,43 @@
1
+ module HaveAPI::CLI
2
+ module ExampleFormatter
3
+ def self.format_examples(cli, action, out = $>)
4
+ action.examples.each do |example|
5
+ out << ' > ' << example[:title] << ":\n" unless example[:title].empty?
6
+
7
+ # request
8
+ out << "$ #{$0} #{action.resource_path.join('.')} #{action.name}"
9
+
10
+ params = example[:request][action.namespace(:input).to_sym]
11
+
12
+ if params
13
+ out << ' --' unless params.empty?
14
+
15
+ params.each do |k, v|
16
+ out << ' ' << example_param(k, v, action.param_description(:input, k))
17
+ end
18
+ end
19
+
20
+ out << "\n"
21
+
22
+ # response
23
+ cli.format_output(action, example[:response], out)
24
+ end
25
+ end
26
+
27
+ def self.example_param(name, value, desc)
28
+ option = name.to_s.dasherize
29
+
30
+ case desc[:type]
31
+ when 'Boolean'
32
+ value ? "--#{option}" : "--no-#{option}"
33
+
34
+ else
35
+ "--#{option} #{example_value(value)}"
36
+ end
37
+ end
38
+
39
+ def self.example_value(v)
40
+ (v.is_a?(String) && (v.empty? || v.index(' '))) ? "\"#{v}\"" : v
41
+ end
42
+ end
43
+ end
@@ -1,2 +1,3 @@
1
1
  require 'require_all'
2
+ require 'date'
2
3
  require_rel 'client'
@@ -1,6 +1,8 @@
1
1
  module HaveAPI
2
2
  module Client
3
3
  class Action
4
+ attr_accessor :resource_path
5
+
4
6
  def initialize(api, name, spec, args)
5
7
  @api = api
6
8
  @name = name
@@ -24,6 +26,18 @@ module HaveAPI
24
26
  @spec[:auth]
25
27
  end
26
28
 
29
+ def aliases(include_name = false)
30
+ if include_name
31
+ [@name] + @spec[:aliases]
32
+ else
33
+ @spec[:aliases]
34
+ end
35
+ end
36
+
37
+ def description
38
+ @spec[:description]
39
+ end
40
+
27
41
  def input
28
42
  @spec[:input]
29
43
  end
@@ -32,8 +46,12 @@ module HaveAPI
32
46
  @spec[:output]
33
47
  end
34
48
 
35
- def layout
36
- @spec[:output][:layout]
49
+ def input_layout
50
+ @spec[:input][:layout].to_sym
51
+ end
52
+
53
+ def output_layout
54
+ @spec[:output][:layout].to_sym
37
55
  end
38
56
 
39
57
  def structure
@@ -44,10 +62,22 @@ module HaveAPI
44
62
  @spec[src][:namespace]
45
63
  end
46
64
 
65
+ def examples
66
+ @spec[:examples]
67
+ end
68
+
69
+ def input_params
70
+ @spec[:input][:parameters]
71
+ end
72
+
47
73
  def params
48
74
  @spec[:output][:parameters]
49
75
  end
50
76
 
77
+ def param_description(dir, name)
78
+ @spec[dir][:parameters][name]
79
+ end
80
+
51
81
  def url
52
82
  @spec[:url]
53
83
  end
@@ -77,6 +107,11 @@ module HaveAPI
77
107
  apply_args(args)
78
108
  end
79
109
 
110
+ def provide_url(url, help)
111
+ @prepared_url = url
112
+ @prepared_help = help
113
+ end
114
+
80
115
  def update_description(spec)
81
116
  @spec = spec
82
117
  end
@@ -36,7 +36,11 @@ module HaveAPI::Client::Authentication
36
36
  protected
37
37
  def request_token
38
38
  a = HaveAPI::Client::Action.new(@communicator, :request, @desc[:resources][:token][:actions][:request], [])
39
- ret = a.execute({login: @opts[:user], password: @opts[:password], validity: @opts[:validity] || 300})
39
+ ret = a.execute({
40
+ login: @opts[:user],
41
+ password: @opts[:password],
42
+ lifetime: @opts[:lifetime],
43
+ interval: @opts[:interval] || 300})
40
44
 
41
45
  raise AuthenticationFailed.new('bad username or password') unless ret[:status]
42
46
 
@@ -1,14 +1,34 @@
1
1
  require 'pp'
2
2
 
3
+ # HaveAPI client interface.
3
4
  class HaveAPI::Client::Client
4
5
  attr_reader :resources
5
6
 
6
- def initialize(url, v=nil, identity: 'haveapi-client')
7
+ # Create an instance of client.
8
+ # The client by default uses the default version of the API.
9
+ # API is asked for description only when needed or by calling #setup.
10
+ # +identity+ is sent in each request to the API in User-Agent header.
11
+ def initialize(url, v = nil, identity: 'haveapi-client')
12
+ @setup = false
7
13
  @version = v
8
14
  @api = HaveAPI::Client::Communicator.new(url, v)
9
15
  @api.identity = identity
16
+ end
17
+
18
+ # Get the description from the API now.
19
+ def setup(v = :_nil)
20
+ @version = v unless v == :_nil
21
+ setup_api
22
+ end
10
23
 
11
- setup_api(@api.describe_api)
24
+ # Returns a list of API versions.
25
+ # The return value is a hash, e.g.:
26
+ # {
27
+ # versions: [1, 2, 3],
28
+ # default: 3
29
+ # }
30
+ def versions
31
+ @api.available_versions
12
32
  end
13
33
 
14
34
  # See Communicator#authenticate.
@@ -16,14 +36,29 @@ class HaveAPI::Client::Client
16
36
  @api.authenticate(*args)
17
37
  end
18
38
 
19
- private
20
- def setup_api(description)
21
- v = @version || description[:default_version]
39
+ # Initialize the client if it is not yet initialized and call the resource
40
+ # if it exists.
41
+ def method_missing(symbol, *args)
42
+ return super(symbol, *args) if @setup
43
+
44
+ setup_api
45
+
46
+ if @resources.include?(symbol)
47
+ method(symbol).call(*args)
22
48
 
49
+ else
50
+ super(symbol, *args)
51
+ end
52
+ end
53
+
54
+ private
55
+ # Get the description from the API and setup resource methods.
56
+ def setup_api
57
+ @description = @api.describe_api(@version)
23
58
  @resources = {}
24
59
 
25
- description[:versions][v.to_s.to_sym][:resources].each do |name, desc|
26
- r = HaveAPI::Client::Resource.new(@api, name)
60
+ @description[:resources].each do |name, desc|
61
+ r = HaveAPI::Client::Resource.new(self, @api, name)
27
62
  r.setup(desc)
28
63
 
29
64
  define_singleton_method(name) do |*args|
@@ -35,5 +70,7 @@ class HaveAPI::Client::Client
35
70
 
36
71
  @resources[name] = r
37
72
  end
73
+
74
+ @setup = true
38
75
  end
39
76
  end
@@ -25,6 +25,7 @@ module HaveAPI
25
25
  @rest = RestClient::Resource.new(@url)
26
26
  @version = v
27
27
  @identity = 'haveapi-client-ruby'
28
+ @desc = {}
28
29
  end
29
30
 
30
31
  # Authenticate user with selected +auth_method+.
@@ -32,7 +33,6 @@ module HaveAPI
32
33
  # +options+ are specific for each authentication provider.
33
34
  def authenticate(auth_method, options = {})
34
35
  desc = describe_api(@version)
35
- desc = desc[:versions][desc[:default_version].to_s.to_sym] unless @version
36
36
 
37
37
  @auth = self.class.auth_methods[auth_method].new(self, desc[:authentication][auth_method], options)
38
38
  @rest = @auth.resource || @rest
@@ -42,13 +42,18 @@ module HaveAPI
42
42
  @auth.save
43
43
  end
44
44
 
45
+ def available_versions
46
+ description_for(path_for, {describe: :versions})
47
+ end
48
+
45
49
  def describe_api(v=nil)
46
- description_for(path_for(v))
50
+ return @desc[v] if @desc.has_key?(v)
51
+
52
+ @desc[v] = description_for(path_for(v), v.nil? ? {describe: :default} : {})
47
53
  end
48
54
 
49
55
  def describe_resource(path)
50
- api = describe_api
51
- tmp = api[:versions][ api[:default_version].to_s.to_sym ]
56
+ tmp = describe_api(@version)
52
57
 
53
58
  path.each do |r|
54
59
  tmp = tmp[:resources][r.to_sym]
@@ -64,10 +69,7 @@ module HaveAPI
64
69
  end
65
70
 
66
71
  def get_action(resources, action, args)
67
- @spec ||= describe_api(@version)
68
- @spec = @spec[:versions][@spec[:default_version].to_s.to_sym] unless @version
69
-
70
- tmp = @spec
72
+ tmp = describe_api(@version)
71
73
 
72
74
  resources.each do |r|
73
75
  tmp = tmp[:resources][r.to_sym]
@@ -77,8 +79,19 @@ module HaveAPI
77
79
 
78
80
  a = tmp[:actions][action]
79
81
 
82
+ unless a # search in aliases
83
+ tmp[:actions].each do |_, v|
84
+ if v[:aliases].include?(action.to_s)
85
+ a = v
86
+ break
87
+ end
88
+ end
89
+ end
90
+
80
91
  if a
81
- Action.new(self, action, a, args)
92
+ obj = Action.new(self, action, a, args)
93
+ obj.resource_path = resources
94
+ obj
82
95
  else
83
96
  false
84
97
  end
@@ -87,9 +100,19 @@ module HaveAPI
87
100
  def call(action, params, raw: false)
88
101
  args = []
89
102
  input_namespace = action.namespace(:input)
103
+ meta = nil
104
+
105
+ if params.is_a?(Hash) && params[:meta]
106
+ meta = params[:meta]
107
+ params.delete(:meta)
108
+ end
90
109
 
91
110
  if %w(POST PUT).include?(action.http_method)
92
- args << {input_namespace => params}.update(@auth.request_payload).to_json
111
+ ns = {input_namespace => params}
112
+ ns[:_meta] = meta if meta
113
+ ns.update(@auth.request_payload)
114
+
115
+ args << ns.to_json
93
116
  args << {content_type: :json, accept: :json, user_agent: @identity}.update(@auth.request_headers)
94
117
 
95
118
  elsif %w(GET DELETE).include?(action.http_method)
@@ -99,6 +122,11 @@ module HaveAPI
99
122
  get_params["#{input_namespace}[#{k}]"] = v
100
123
  end
101
124
 
125
+ meta.each do |k, v|
126
+ get_params["_meta[#{k}]"] = v # FIXME: read _meta namespace from the description
127
+
128
+ end if meta
129
+
102
130
  args << {params: get_params.update(@auth.request_url_params), accept: :json, user_agent: @identity}.update(@auth.request_headers)
103
131
  end
104
132
 
@@ -142,11 +170,11 @@ module HaveAPI
142
170
  ret
143
171
  end
144
172
 
145
- def description_for(path)
173
+ def description_for(path, query_params={})
146
174
  parse(@rest[path].get_options({
147
- params: @auth.request_payload.update(@auth.request_url_params),
175
+ params: @auth.request_payload.update(@auth.request_url_params).update(query_params),
148
176
  user_agent: @identity
149
- }.update(@auth.request_headers)))
177
+ }.update(@auth.request_headers)))[:response]
150
178
  end
151
179
 
152
180
  def parse(str)
@@ -1,78 +1,136 @@
1
- class HaveAPI::Client::Resource
2
- attr_reader :actions, :resources, :name
3
- attr_accessor :prepared_args
4
-
5
- def initialize(api, name)
6
- @api = api
7
- @name = name
8
- @prepared_args = []
9
- end
1
+ module HaveAPI::Client
2
+ # An API resource.
3
+ class Resource
4
+ attr_reader :actions, :resources
5
+ attr_accessor :prepared_args
6
+
7
+ def initialize(client, api, name)
8
+ @client = client
9
+ @api = api
10
+ @name = name
11
+ @prepared_args = []
12
+ end
13
+
14
+ def setup(description)
15
+ @actions = {}
16
+ @resources = {}
10
17
 
11
- def setup(description)
12
- @actions = {}
13
- @resources = {}
18
+ description[:actions].each do |name, desc|
19
+ action = HaveAPI::Client::Action.new(@api, name, desc, [])
20
+ define_action(action)
21
+ @actions[name] = action
22
+ end
14
23
 
15
- description[:actions].each do |name, desc|
16
- action = HaveAPI::Client::Action.new(@api, name, desc, [])
17
- define_action(action)
18
- @actions[name] = action
24
+ description[:resources].each do |name, desc|
25
+ r = HaveAPI::Client::Resource.new(@client, @api, name)
26
+ r.setup(desc)
27
+ define_resource(r)
28
+ @resources[name] = r
29
+ end
19
30
  end
20
31
 
21
- description[:resources].each do |name, desc|
22
- r = HaveAPI::Client::Resource.new(@api, name)
23
- r.setup(desc)
24
- define_resource(r)
25
- @resources[name] = r
32
+ # Copy actions and resources from the +original+ resource
33
+ # and create methods for this instance.
34
+ def setup_from_clone(original)
35
+ original.actions.each_value do |action|
36
+ define_action(action)
37
+ end
38
+
39
+ original.resources.each_value do |resource|
40
+ define_resource(resource)
41
+ end
26
42
  end
27
- end
28
43
 
29
- def setup_from_clone(original)
30
- original.actions.each_value do |action|
31
- define_action(action)
44
+ def inspect
45
+ super
32
46
  end
33
47
 
34
- original.resources.each_value do |resource|
35
- define_resource(resource)
48
+ # Create a new instance of a resource. The created instance
49
+ # is not persistent until ResourceInstance#save is called.
50
+ def new
51
+ ResourceInstance.new(@client, @api, self, action: @actions[:create], persistent: false)
36
52
  end
37
- end
38
53
 
39
- private
40
- def define_action(action)
41
- define_singleton_method(action.name) do |*args|
42
- all_args = @prepared_args + args
54
+ # Return resource name.
55
+ # Method is prefixed with an underscore to prevent name collision
56
+ # with ResourceInstance attributes.
57
+ def _name
58
+ @name
59
+ end
43
60
 
44
- if action.unresolved_args?
45
- all_args.delete_if do |arg|
46
- break unless action.unresolved_args?
61
+ protected
62
+ # Define access/write methods for action +action+.
63
+ def define_action(action)
64
+ action.aliases(true).each do |name|
65
+ next unless define_method?(action, name)
47
66
 
48
- action.provide_args(arg)
49
- true
50
- end
67
+ define_singleton_method(name) do |*args|
68
+ all_args = @prepared_args + args
51
69
 
52
- if action.unresolved_args?
53
- raise ArgumentError.new('One or more object ids missing')
54
- end
55
- end
70
+ if action.unresolved_args?
71
+ all_args.delete_if do |arg|
72
+ break unless action.unresolved_args?
56
73
 
57
- all_args << {} if all_args.empty?
74
+ action.provide_args(arg)
75
+ true
76
+ end
58
77
 
59
- HaveAPI::Client::Response.new(action, action.execute(*all_args))
78
+ if action.unresolved_args?
79
+ raise ArgumentError.new('One or more object ids missing')
80
+ end
81
+ end
82
+
83
+ if all_args.empty?
84
+ all_args << default_action_input_params(action)
85
+
86
+ elsif all_args.last.is_a?(Hash)
87
+ last = all_args.pop
88
+
89
+ all_args << default_action_input_params(action).update(last)
90
+ end
91
+
92
+ ret = Response.new(action, action.execute(*all_args))
93
+
94
+ raise ActionFailed.new(ret) unless ret.ok?
95
+
96
+ case action.output_layout
97
+ when :object
98
+ ResourceInstance.new(@client, @api, self, action: action, response: ret)
99
+
100
+ when :object_list
101
+ ResourceInstanceList.new(@client, @api, self, action, ret)
102
+
103
+ when :hash, :hash_list
104
+ ret
105
+
106
+ else
107
+ ret
108
+ end
109
+ end
110
+ end
60
111
  end
61
112
 
62
- define_singleton_method("#{action.name}!".to_sym) do |*args|
63
- ret = method(action.name).call(*args)
64
- raise HaveAPI::Client::ActionFailed.new(ret) unless ret.ok?
113
+ # Called before defining a method named +name+ that will
114
+ # invoke +action+.
115
+ def define_method?(action, name)
116
+ return false if %i(new).include?(name.to_sym)
117
+ true
118
+ end
65
119
 
66
- ret
120
+ # This method is called when an action is invoked.
121
+ # Override it to return a default hash of parameters to be sent to API.
122
+ # Used for example in ResourceInstance, which returns its instance attributes.
123
+ def default_action_input_params(action)
124
+ {}
67
125
  end
68
- end
69
126
 
70
- def define_resource(resource)
71
- define_singleton_method(resource.name) do |*args|
72
- tmp = resource.dup
73
- tmp.prepared_args = @prepared_args + args
74
- tmp.setup_from_clone(resource)
75
- tmp
127
+ def define_resource(resource)
128
+ define_singleton_method(resource._name) do |*args|
129
+ tmp = resource.dup
130
+ tmp.prepared_args = @prepared_args + args
131
+ tmp.setup_from_clone(resource)
132
+ tmp
133
+ end
76
134
  end
77
135
  end
78
136
  end
@@ -0,0 +1,201 @@
1
+ module HaveAPI::Client
2
+ # Instance of an object from the API.
3
+ # An instance of this class may be in three states:
4
+ # - resolved/persistent - the instance was created by an action that retrieved
5
+ # it from the API.
6
+ # - unresolved - this instance is an attribute of another instance that was resolved
7
+ # and will be resolved when first accessed.
8
+ # - not persistent - created by Resource.new, the object was not yet sent to the API.
9
+ class ResourceInstance < Resource
10
+ def initialize(client, api, resource, action: nil, response: nil,
11
+ resolved: false, meta: nil, persistent: true)
12
+ super(client, api, resource._name)
13
+
14
+ @action = action
15
+ @resource = resource
16
+ @resolved = resolved
17
+ @meta = meta
18
+ @persistent = persistent
19
+ @resource_instances = {}
20
+
21
+ if response
22
+ if response.is_a?(Hash)
23
+ @params = response
24
+
25
+ else
26
+ @response = response
27
+ @params = response.response
28
+ end
29
+
30
+ setup_from_clone(resource)
31
+ define_attributes
32
+ end
33
+
34
+ unless @persistent
35
+ setup_from_clone(resource)
36
+ define_implicit_attributes
37
+ define_attributes(@action.input_params)
38
+ end
39
+ end
40
+
41
+ def new
42
+ raise NoMethodError.new
43
+ end
44
+
45
+ # Invoke +create+ action if the object is not persistent,
46
+ # +update+ action if it is.
47
+ def save
48
+ if @persistent
49
+ method(:update).call
50
+
51
+ else
52
+ @action.provide_args
53
+ @response = Response.new(@action, @action.execute(attributes_for_api(@action)))
54
+
55
+ if @response.ok?
56
+ @params = @response.response
57
+ define_attributes
58
+
59
+ else
60
+ return nil
61
+ end
62
+
63
+ @persistent = true
64
+ self
65
+ end
66
+ end
67
+
68
+ # Call #save and raise ActionFailed if it fails.
69
+ def save!
70
+ raise ActionFailed.new(@response) if save.nil?
71
+ self
72
+ end
73
+
74
+ # Resolve the object (fetch it from the API) if it is not resolved yet.
75
+ def resolve
76
+ return self if @resolved
77
+
78
+ @action.provide_args(*@meta[:url_params])
79
+ @response = Response.new(@action, @action.execute({}))
80
+ @params = @response.response
81
+
82
+ setup_from_clone(@resource)
83
+ define_attributes
84
+
85
+ @resolved = true
86
+ self
87
+ end
88
+
89
+ # Return Response object which created this instance.
90
+ def api_response
91
+ @response
92
+ end
93
+
94
+ # Return a hash of all object attributes retrieved from the API.
95
+ def attributes
96
+ @params
97
+ end
98
+
99
+ def to_s
100
+ "<#{self.class.to_s}:#{object_id}:#{@resource._name}>"
101
+ end
102
+
103
+ protected
104
+ # Define access/writer methods for object attributes.
105
+ def define_attributes(params = nil)
106
+ (params || @action.params).each do |name, param|
107
+ case param[:type]
108
+ when 'Resource'
109
+ @resource_instances[name] = find_association(param, @params[name])
110
+
111
+ # id reader
112
+ ensure_method(:"#{name}_id") { @params[name][ param[:value_id].to_sym ] }
113
+
114
+ # id writer
115
+ ensure_method(:"#{name}_id=") { |id| @params[name][ param[:value_id].to_sym ] = id }
116
+
117
+ # value reader
118
+ ensure_method(name) do
119
+ @resource_instances[name] && @resource_instances[name].resolve
120
+ end
121
+
122
+ # value writer
123
+ ensure_method(:"#{name}=") do |obj|
124
+ @params[name][ param[:value_id].to_sym ] = obj.method(param[:value_id]).call
125
+ @params[name][ param[:value_label].to_sym ] = obj.method(param[:value_label]).call
126
+
127
+ @resource_instances[name] = obj
128
+ end
129
+
130
+ else
131
+ # reader
132
+ ensure_method(name) { @params[name] }
133
+
134
+ # writer
135
+ ensure_method(:"#{name}=") { |new_val| @params[name] = new_val }
136
+ end
137
+ end
138
+ end
139
+
140
+ # Define method +name+ with +block+ if it isn't defined yet.
141
+ def ensure_method(name, &block)
142
+ define_singleton_method(name, &block) unless respond_to?(name)
143
+ end
144
+
145
+ # Define nil references to resource attributes.
146
+ # Used only for not-persistent objects.
147
+ def define_implicit_attributes
148
+ @params = {}
149
+
150
+ @action.input_params.each do |name, param|
151
+ @params[name] = {} if param[:type] == 'Resource'
152
+ end
153
+ end
154
+
155
+ # Return a hash of all attributes suitable to be sent to the API +action+.
156
+ def attributes_for_api(action)
157
+ ret = {}
158
+
159
+ return ret if action.input_layout != :object
160
+
161
+ action.input_params.each do |name, param|
162
+ case param[:type]
163
+ when 'Resource'
164
+ ret[name] = @params[name][ param[:value_id].to_sym ]
165
+
166
+ else
167
+ ret[name] = @params[name]
168
+ end
169
+ end
170
+
171
+ ret
172
+ end
173
+
174
+ # Find associated resource and create its unresolved instance.
175
+ def find_association(res_desc, res_val)
176
+ return nil unless res_val
177
+
178
+ tmp = @client
179
+
180
+ res_desc[:resource].each do |r|
181
+ tmp = tmp.method(r).call
182
+ end
183
+
184
+ # FIXME: read _meta namespace from description
185
+ ResourceInstance.new(
186
+ @client,
187
+ @api,
188
+ tmp,
189
+ action: tmp.actions[:show],
190
+ resolved: res_val[:_meta][:resolved],
191
+ response: res_val[:_meta][:resolved] ? res_val : nil,
192
+ meta: res_val[:_meta]
193
+ )
194
+ end
195
+
196
+ # Override Resource.default_action_input_params.
197
+ def default_action_input_params(action)
198
+ attributes_for_api(action)
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,29 @@
1
+ module HaveAPI::Client
2
+ # A list of ResourceInstance objects.
3
+ class ResourceInstanceList < Array
4
+ def initialize(client, api, resource, action, response)
5
+ @response = response
6
+
7
+ response.response.each do |hash|
8
+ self << ResourceInstance.new(client, api, resource, action: action, response: hash)
9
+ end
10
+ end
11
+
12
+ # Return the API response that created this object.
13
+ def api_response
14
+ @response
15
+ end
16
+
17
+ def meta
18
+ @response.meta
19
+ end
20
+
21
+ # Return the total count of items.
22
+ # Note that for this method to work, the action that returns this
23
+ # object list must be invoked with +meta: {count: true}+, otherwise
24
+ # the object count is not sent.
25
+ def total_count
26
+ meta[:total_count]
27
+ end
28
+ end
29
+ end
@@ -1,6 +1,9 @@
1
+ # Represents a response from the API.
1
2
  class HaveAPI::Client::Response
2
3
  attr_reader :action
3
4
 
5
+ # Create instance.
6
+ # +action+ being the called action and +response+ a received hash.
4
7
  def initialize(action, response)
5
8
  @action = action
6
9
  @response = response
@@ -15,14 +18,22 @@ class HaveAPI::Client::Response
15
18
  end
16
19
 
17
20
  def response
18
- case @action.layout.to_sym
19
- when :object, :list
21
+ case @action.output_layout
22
+ when :object, :object_list, :hash, :hash_list
20
23
  @response[:response][@action.namespace(:output).to_sym]
21
24
  else
22
25
  @response[:response]
23
26
  end
24
27
  end
25
28
 
29
+ def meta
30
+ @response[:response][:_meta] # FIXME: read _meta from API description
31
+ end
32
+
33
+ def to_hash
34
+ response
35
+ end
36
+
26
37
  def message
27
38
  @response[:message]
28
39
  end
@@ -30,4 +41,19 @@ class HaveAPI::Client::Response
30
41
  def errors
31
42
  @response[:errors]
32
43
  end
44
+
45
+ # Access namespaced params directly.
46
+ def [](key)
47
+ return unless %i(object hash).include?(@action.layout.to_sym)
48
+
49
+ @response[:response][@action.namespace(:output).to_sym][key]
50
+ end
51
+
52
+ # Iterate over namespaced items directly. Works for only for
53
+ # object_list or hash_list.
54
+ def each
55
+ return unless %i(list).include?(@action.layout.to_sym)
56
+
57
+ @response[:response][@action.namespace(:output).to_sym].each
58
+ end
33
59
  end
@@ -1,5 +1,5 @@
1
1
  module HaveAPI
2
2
  module Client
3
- VERSION = '0.2.0'
3
+ VERSION = '0.3.0'
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: haveapi-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jakub Skokan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-08-18 00:00:00.000000000 Z
11
+ date: 2015-07-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -114,14 +114,14 @@ dependencies:
114
114
  requirements:
115
115
  - - ~>
116
116
  - !ruby/object:Gem::Version
117
- version: 1.5.1
117
+ version: 1.5.3
118
118
  type: :runtime
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - ~>
123
123
  - !ruby/object:Gem::Version
124
- version: 1.5.1
124
+ version: 1.5.3
125
125
  description: Ruby API and CLI for HaveAPI
126
126
  email:
127
127
  - jakub.skokan@vpsfree.cz
@@ -141,6 +141,7 @@ files:
141
141
  - lib/haveapi/cli/authentication/basic.rb
142
142
  - lib/haveapi/cli/authentication/token.rb
143
143
  - lib/haveapi/cli/cli.rb
144
+ - lib/haveapi/cli/example_formatter.rb
144
145
  - lib/haveapi/client.rb
145
146
  - lib/haveapi/client/action.rb
146
147
  - lib/haveapi/client/authentication/base.rb
@@ -152,6 +153,8 @@ files:
152
153
  - lib/haveapi/client/exceptions.rb
153
154
  - lib/haveapi/client/inflections.rb
154
155
  - lib/haveapi/client/resource.rb
156
+ - lib/haveapi/client/resource_instance.rb
157
+ - lib/haveapi/client/resource_instance_list.rb
155
158
  - lib/haveapi/client/response.rb
156
159
  - lib/haveapi/client/version.rb
157
160
  - lib/restclient_ext/request.rb
@@ -176,7 +179,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
176
179
  version: '0'
177
180
  requirements: []
178
181
  rubyforge_project:
179
- rubygems_version: 2.1.11
182
+ rubygems_version: 2.2.2
180
183
  signing_key:
181
184
  specification_version: 4
182
185
  summary: Ruby API and CLI for HaveAPI