haveapi-client 0.3.0 → 0.4.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.
@@ -1,6 +1,7 @@
1
1
  module HaveAPI::Client::Authentication
2
2
  class Token < Base
3
3
  register :token
4
+ attr_reader :token, :valid_to
4
5
 
5
6
  def setup
6
7
  @via = @opts[:via] || :header
@@ -8,11 +8,17 @@ class HaveAPI::Client::Client
8
8
  # The client by default uses the default version of the API.
9
9
  # API is asked for description only when needed or by calling #setup.
10
10
  # +identity+ is sent in each request to the API in User-Agent header.
11
- def initialize(url, v = nil, identity: 'haveapi-client')
11
+ def initialize(url, v = nil, identity: 'haveapi-client', communicator: nil)
12
12
  @setup = false
13
13
  @version = v
14
- @api = HaveAPI::Client::Communicator.new(url, v)
15
- @api.identity = identity
14
+
15
+ if communicator
16
+ @api = communicator
17
+
18
+ else
19
+ @api = HaveAPI::Client::Communicator.new(url, v)
20
+ @api.identity = identity
21
+ end
16
22
  end
17
23
 
18
24
  # Get the description from the API now.
@@ -36,6 +42,18 @@ class HaveAPI::Client::Client
36
42
  @api.authenticate(*args)
37
43
  end
38
44
 
45
+ # Get uthentication provider
46
+ # @return [HaveAPI::Client::Authentication::Base] if authenticated
47
+ # @return [nil] if not authenticated
48
+ def auth
49
+ @api.auth
50
+ end
51
+
52
+ # @see Communicator#compatible?
53
+ def compatible?
54
+ @api.compatible?
55
+ end
56
+
39
57
  # Initialize the client if it is not yet initialized and call the resource
40
58
  # if it exists.
41
59
  def method_missing(symbol, *args)
@@ -4,182 +4,214 @@ require 'active_support/inflections'
4
4
  require 'active_support/inflector'
5
5
  require_rel '../../restclient_ext'
6
6
 
7
- module HaveAPI
8
- module Client
9
- class Communicator
10
- class << self
11
- attr_reader :auth_methods
12
-
13
- def register_auth_method(name, klass)
14
- @auth_methods ||= {}
15
- @auth_methods[name] = klass
16
- end
7
+ module HaveAPI::Client
8
+ class Communicator
9
+ class << self
10
+ attr_reader :auth_methods
11
+
12
+ def register_auth_method(name, klass)
13
+ @auth_methods ||= {}
14
+ @auth_methods[name] = klass
17
15
  end
16
+ end
18
17
 
19
- attr_reader :url
20
- attr_accessor :identity
18
+ attr_reader :url, :auth
19
+ attr_accessor :identity
21
20
 
22
- def initialize(url, v = nil)
23
- @url = url
24
- @auth = Authentication::NoAuth.new(self, {}, {})
25
- @rest = RestClient::Resource.new(@url)
26
- @version = v
27
- @identity = 'haveapi-client-ruby'
28
- @desc = {}
29
- end
21
+ def initialize(url, v = nil)
22
+ @url = url
23
+ @auth = Authentication::NoAuth.new(self, {}, {})
24
+ @rest = RestClient::Resource.new(@url)
25
+ @version = v
26
+ @identity = 'haveapi-client-ruby'
27
+ @desc = {}
28
+ end
30
29
 
31
- # Authenticate user with selected +auth_method+.
32
- # +auth_method+ is a name of registered authentication provider.
33
- # +options+ are specific for each authentication provider.
34
- def authenticate(auth_method, options = {})
35
- desc = describe_api(@version)
30
+ # @return [:compatible] if perfectly compatible
31
+ # @return [:imperfect] if minor version differs
32
+ # @return [false] if not compatible
33
+ def compatible?
34
+ description_for(path_for, {describe: :versions})
35
+ @proto_version == HaveAPI::Client::PROTOCOL_VERSION ? :compatible
36
+ : :imperfect
36
37
 
37
- @auth = self.class.auth_methods[auth_method].new(self, desc[:authentication][auth_method], options)
38
- @rest = @auth.resource || @rest
39
- end
38
+ rescue ProtocolError
39
+ false
40
+ end
40
41
 
41
- def auth_save
42
- @auth.save
43
- end
42
+ # Authenticate user with selected +auth_method+.
43
+ # +auth_method+ is a name of registered authentication provider.
44
+ # +options+ are specific for each authentication provider.
45
+ def authenticate(auth_method, options = {})
46
+ desc = describe_api(@version)
44
47
 
45
- def available_versions
46
- description_for(path_for, {describe: :versions})
47
- end
48
+ @auth = self.class.auth_methods[auth_method].new(self, desc[:authentication][auth_method], options)
49
+ @rest = @auth.resource || @rest
50
+ end
48
51
 
49
- def describe_api(v=nil)
50
- return @desc[v] if @desc.has_key?(v)
52
+ def auth_save
53
+ @auth.save
54
+ end
51
55
 
52
- @desc[v] = description_for(path_for(v), v.nil? ? {describe: :default} : {})
53
- end
56
+ def available_versions
57
+ description_for(path_for, {describe: :versions})
58
+ end
54
59
 
55
- def describe_resource(path)
56
- tmp = describe_api(@version)
60
+ def describe_api(v=nil)
61
+ return @desc[v] if @desc.has_key?(v)
57
62
 
58
- path.each do |r|
59
- tmp = tmp[:resources][r.to_sym]
63
+ @desc[v] = description_for(path_for(v), v.nil? ? {describe: :default} : {})
64
+ end
60
65
 
61
- return false unless tmp
62
- end
66
+ def describe_resource(path)
67
+ tmp = describe_api(@version)
63
68
 
64
- tmp
65
- end
69
+ path.each do |r|
70
+ tmp = tmp[:resources][r.to_sym]
66
71
 
67
- def describe_action(action)
68
- description_for(action.prepared_help)
72
+ return false unless tmp
69
73
  end
70
74
 
71
- def get_action(resources, action, args)
72
- tmp = describe_api(@version)
75
+ tmp
76
+ end
73
77
 
74
- resources.each do |r|
75
- tmp = tmp[:resources][r.to_sym]
78
+ def describe_action(action)
79
+ description_for(action.prepared_help)
80
+ end
76
81
 
77
- return false unless tmp
78
- end
82
+ def get_action(resources, action, args)
83
+ tmp = describe_api(@version)
79
84
 
80
- a = tmp[:actions][action]
85
+ resources.each do |r|
86
+ tmp = tmp[:resources][r.to_sym]
81
87
 
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
-
91
- if a
92
- obj = Action.new(self, action, a, args)
93
- obj.resource_path = resources
94
- obj
95
- else
96
- false
97
- end
88
+ return false unless tmp
98
89
  end
99
90
 
100
- def call(action, params, raw: false)
101
- args = []
102
- input_namespace = action.namespace(:input)
103
- meta = nil
91
+ a = tmp[:actions][action]
104
92
 
105
- if params.is_a?(Hash) && params[:meta]
106
- meta = params[:meta]
107
- params.delete(:meta)
93
+ unless a # search in aliases
94
+ tmp[:actions].each do |_, v|
95
+ if v[:aliases].include?(action.to_s)
96
+ a = v
97
+ break
98
+ end
108
99
  end
100
+ end
109
101
 
110
- if %w(POST PUT).include?(action.http_method)
111
- ns = {input_namespace => params}
112
- ns[:_meta] = meta if meta
113
- ns.update(@auth.request_payload)
102
+ if a
103
+ obj = Action.new(self, action, a, args)
104
+ obj.resource_path = resources
105
+ obj
106
+ else
107
+ false
108
+ end
109
+ end
114
110
 
115
- args << ns.to_json
116
- args << {content_type: :json, accept: :json, user_agent: @identity}.update(@auth.request_headers)
111
+ def call(action, params, raw: false)
112
+ args = []
113
+ input_namespace = action.namespace(:input)
114
+ meta = nil
117
115
 
118
- elsif %w(GET DELETE).include?(action.http_method)
119
- get_params = {}
116
+ if params.is_a?(Hash) && params[:meta]
117
+ meta = params[:meta]
118
+ params.delete(:meta)
119
+ end
120
120
 
121
- params.each do |k, v|
122
- get_params["#{input_namespace}[#{k}]"] = v
123
- end
121
+ if %w(POST PUT).include?(action.http_method)
122
+ ns = {input_namespace => params}
123
+ ns[:_meta] = meta if meta
124
+ ns.update(@auth.request_payload)
124
125
 
125
- meta.each do |k, v|
126
- get_params["_meta[#{k}]"] = v # FIXME: read _meta namespace from the description
126
+ args << ns.to_json
127
+ args << {content_type: :json, accept: :json, user_agent: @identity}.update(@auth.request_headers)
127
128
 
128
- end if meta
129
+ elsif %w(GET DELETE).include?(action.http_method)
130
+ get_params = {}
129
131
 
130
- args << {params: get_params.update(@auth.request_url_params), accept: :json, user_agent: @identity}.update(@auth.request_headers)
132
+ params.each do |k, v|
133
+ get_params["#{input_namespace}[#{k}]"] = v
131
134
  end
132
135
 
133
- begin
134
- response = parse(@rest[action.prepared_url].method(action.http_method.downcase.to_sym).call(*args))
136
+ meta.each do |k, v|
137
+ get_params["_meta[#{k}]"] = v # FIXME: read _meta namespace from the description
135
138
 
136
- rescue RestClient::Forbidden
137
- return error('Access forbidden. Bad user name or password? Not authorized?')
139
+ end if meta
138
140
 
139
- rescue => e
140
- return error("Fatal API error: #{e.inspect}")
141
- end
141
+ args << {params: get_params.update(@auth.request_url_params), accept: :json, user_agent: @identity}.update(@auth.request_headers)
142
+ end
142
143
 
143
- if response[:status]
144
- if raw
145
- ok(JSON.pretty_generate(response[:response]))
146
- else
147
- ok(response[:response])
148
- end
144
+ begin
145
+ response = parse(@rest[action.prepared_url].method(action.http_method.downcase.to_sym).call(*args))
146
+
147
+ rescue RestClient::Forbidden
148
+ return error('Access forbidden. Bad user name or password? Not authorized?')
149
149
 
150
+ rescue => e
151
+ return error("Fatal API error: #{e.inspect}")
152
+ end
153
+
154
+ if response[:status]
155
+ if raw
156
+ ok(JSON.pretty_generate(response[:response]))
150
157
  else
151
- error(response[:message], response[:errors])
158
+ ok(response[:response])
152
159
  end
160
+
161
+ else
162
+ error(response[:message], response[:errors])
153
163
  end
164
+ end
154
165
 
155
- private
156
- def ok(response)
157
- {status: true, response: response}
158
- end
166
+ private
167
+ def ok(response)
168
+ {status: true, response: response}
169
+ end
159
170
 
160
- def error(msg, errors={})
161
- {status: false, message: msg, errors: errors}
162
- end
171
+ def error(msg, errors={})
172
+ {status: false, message: msg, errors: errors}
173
+ end
163
174
 
164
- def path_for(v=nil, r=nil)
165
- ret = '/'
175
+ def path_for(v=nil, r=nil)
176
+ ret = '/'
166
177
 
167
- ret += "v#{v}/" if v
168
- ret += r if r
178
+ ret += "v#{v}/" if v
179
+ ret += r if r
169
180
 
170
- ret
171
- end
181
+ ret
182
+ end
172
183
 
173
- def description_for(path, query_params={})
174
- parse(@rest[path].get_options({
175
- params: @auth.request_payload.update(@auth.request_url_params).update(query_params),
176
- user_agent: @identity
177
- }.update(@auth.request_headers)))[:response]
178
- end
184
+ def description_for(path, query_params={})
185
+ ret = parse(@rest[path].get_options({
186
+ params: @auth.request_payload.update(@auth.request_url_params).update(query_params),
187
+ user_agent: @identity
188
+ }.update(@auth.request_headers)))
189
+
190
+ @proto_version = ret[:version]
191
+ p_v = HaveAPI::Client::PROTOCOL_VERSION
192
+ return ret[:response] if ret[:version] == p_v
193
+
194
+ unless ret[:version]
195
+ raise ProtocolError,
196
+ "Incompatible protocol version: the client uses v#{p_v} "+
197
+ "while the API server uses an unspecified version (pre 1.0)"
198
+ end
179
199
 
180
- def parse(str)
181
- JSON.parse(str, symbolize_names: true)
182
- end
200
+ major1, minor1 = ret[:version].split('.')
201
+ major2, minor2 = p_v.split('.')
202
+
203
+ if major1 != major2
204
+ raise ProtocolError,
205
+ "Incompatible protocol version: the client uses v#{p_v} "+
206
+ "while the API server uses v#{ret[:version]}"
207
+ end
208
+
209
+ warn "The client uses protocol v#{p_v} while the API server uses v#{ret[:version]}"
210
+ ret[:response]
211
+ end
212
+
213
+ def parse(str)
214
+ JSON.parse(str, symbolize_names: true)
183
215
  end
184
216
  end
185
217
  end
@@ -1,13 +1,26 @@
1
- module HaveAPI
2
- module Client
3
- class ActionFailed < Exception
4
- def initialize(response)
5
- @response = response
6
- end
1
+ module HaveAPI::Client
2
+ class ProtocolError < StandardError ; end
7
3
 
8
- def message
9
- "#{@response.action.name} failed: #{@response.message}"
10
- end
4
+ class ActionFailed < StandardError
5
+ def initialize(response)
6
+ @response = response
7
+ end
8
+
9
+ def message
10
+ "#{@response.action.name} failed: #{@response.message}"
11
+ end
12
+ end
13
+
14
+ class ValidationError < ActionFailed
15
+ attr_reader :errors
16
+
17
+ def initialize(action, errors)
18
+ @action = action
19
+ @errors = errors
20
+ end
21
+
22
+ def message
23
+ "#{@action.name} failed: input parameters not valid"
11
24
  end
12
25
  end
13
- end
26
+ end
@@ -0,0 +1,29 @@
1
+ module HaveAPI::Client
2
+ class Parameters::Resource
3
+ attr_reader :errors
4
+
5
+ def initialize(params, desc, value)
6
+ @errors = []
7
+ @value = coerce(value)
8
+ end
9
+
10
+ def valid?
11
+ @errors.empty?
12
+ end
13
+
14
+ def to_api
15
+ @value
16
+ end
17
+
18
+ protected
19
+ def coerce(v)
20
+ if !v.is_a?(::Integer) && /\A\d+\z/ !~ v
21
+ @errors << 'not a valid resource id'
22
+ nil
23
+
24
+ else
25
+ v.to_i
26
+ end
27
+ end
28
+ end
29
+ end