haveapi-client 0.2.0 → 0.3.0

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