haveapi-client 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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