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.
- checksums.yaml +4 -4
- data/CHANGELOG +17 -0
- data/README.md +14 -2
- data/haveapi-client.gemspec +5 -5
- data/lib/haveapi/cli.rb +5 -0
- data/lib/haveapi/cli/cli.rb +528 -277
- data/lib/haveapi/cli/command.rb +52 -0
- data/lib/haveapi/cli/output_formatter.rb +221 -0
- data/lib/haveapi/client.rb +5 -0
- data/lib/haveapi/client/action.rb +102 -98
- data/lib/haveapi/client/authentication/token.rb +1 -0
- data/lib/haveapi/client/client.rb +21 -3
- data/lib/haveapi/client/communicator.rb +161 -129
- data/lib/haveapi/client/exceptions.rb +23 -10
- data/lib/haveapi/client/parameters/resource.rb +29 -0
- data/lib/haveapi/client/parameters/typed.rb +72 -0
- data/lib/haveapi/client/params.rb +72 -0
- data/lib/haveapi/client/resource_instance.rb +2 -0
- data/lib/haveapi/client/validator.rb +55 -0
- data/lib/haveapi/client/validators/acceptance.rb +9 -0
- data/lib/haveapi/client/validators/confirmation.rb +18 -0
- data/lib/haveapi/client/validators/custom.rb +9 -0
- data/lib/haveapi/client/validators/exclusion.rb +9 -0
- data/lib/haveapi/client/validators/format.rb +16 -0
- data/lib/haveapi/client/validators/inclusion.rb +14 -0
- data/lib/haveapi/client/validators/length.rb +14 -0
- data/lib/haveapi/client/validators/numericality.rb +24 -0
- data/lib/haveapi/client/validators/presence.rb +11 -0
- data/lib/haveapi/client/version.rb +3 -2
- metadata +28 -25
@@ -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
|
-
|
15
|
-
|
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
|
-
|
9
|
-
class
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
20
|
-
|
18
|
+
attr_reader :url, :auth
|
19
|
+
attr_accessor :identity
|
21
20
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
38
|
+
rescue ProtocolError
|
39
|
+
false
|
40
|
+
end
|
40
41
|
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
50
|
-
|
52
|
+
def auth_save
|
53
|
+
@auth.save
|
54
|
+
end
|
51
55
|
|
52
|
-
|
53
|
-
|
56
|
+
def available_versions
|
57
|
+
description_for(path_for, {describe: :versions})
|
58
|
+
end
|
54
59
|
|
55
|
-
|
56
|
-
|
60
|
+
def describe_api(v=nil)
|
61
|
+
return @desc[v] if @desc.has_key?(v)
|
57
62
|
|
58
|
-
|
59
|
-
|
63
|
+
@desc[v] = description_for(path_for(v), v.nil? ? {describe: :default} : {})
|
64
|
+
end
|
60
65
|
|
61
|
-
|
62
|
-
|
66
|
+
def describe_resource(path)
|
67
|
+
tmp = describe_api(@version)
|
63
68
|
|
64
|
-
|
65
|
-
|
69
|
+
path.each do |r|
|
70
|
+
tmp = tmp[:resources][r.to_sym]
|
66
71
|
|
67
|
-
|
68
|
-
description_for(action.prepared_help)
|
72
|
+
return false unless tmp
|
69
73
|
end
|
70
74
|
|
71
|
-
|
72
|
-
|
75
|
+
tmp
|
76
|
+
end
|
73
77
|
|
74
|
-
|
75
|
-
|
78
|
+
def describe_action(action)
|
79
|
+
description_for(action.prepared_help)
|
80
|
+
end
|
76
81
|
|
77
|
-
|
78
|
-
|
82
|
+
def get_action(resources, action, args)
|
83
|
+
tmp = describe_api(@version)
|
79
84
|
|
80
|
-
|
85
|
+
resources.each do |r|
|
86
|
+
tmp = tmp[:resources][r.to_sym]
|
81
87
|
|
82
|
-
|
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
|
-
|
101
|
-
args = []
|
102
|
-
input_namespace = action.namespace(:input)
|
103
|
-
meta = nil
|
91
|
+
a = tmp[:actions][action]
|
104
92
|
|
105
|
-
|
106
|
-
|
107
|
-
|
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
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
-
|
116
|
-
|
111
|
+
def call(action, params, raw: false)
|
112
|
+
args = []
|
113
|
+
input_namespace = action.namespace(:input)
|
114
|
+
meta = nil
|
117
115
|
|
118
|
-
|
119
|
-
|
116
|
+
if params.is_a?(Hash) && params[:meta]
|
117
|
+
meta = params[:meta]
|
118
|
+
params.delete(:meta)
|
119
|
+
end
|
120
120
|
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
-
|
126
|
-
|
126
|
+
args << ns.to_json
|
127
|
+
args << {content_type: :json, accept: :json, user_agent: @identity}.update(@auth.request_headers)
|
127
128
|
|
128
|
-
|
129
|
+
elsif %w(GET DELETE).include?(action.http_method)
|
130
|
+
get_params = {}
|
129
131
|
|
130
|
-
|
132
|
+
params.each do |k, v|
|
133
|
+
get_params["#{input_namespace}[#{k}]"] = v
|
131
134
|
end
|
132
135
|
|
133
|
-
|
134
|
-
|
136
|
+
meta.each do |k, v|
|
137
|
+
get_params["_meta[#{k}]"] = v # FIXME: read _meta namespace from the description
|
135
138
|
|
136
|
-
|
137
|
-
return error('Access forbidden. Bad user name or password? Not authorized?')
|
139
|
+
end if meta
|
138
140
|
|
139
|
-
|
140
|
-
|
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
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
-
|
158
|
+
ok(response[:response])
|
152
159
|
end
|
160
|
+
|
161
|
+
else
|
162
|
+
error(response[:message], response[:errors])
|
153
163
|
end
|
164
|
+
end
|
154
165
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
166
|
+
private
|
167
|
+
def ok(response)
|
168
|
+
{status: true, response: response}
|
169
|
+
end
|
159
170
|
|
160
|
-
|
161
|
-
|
162
|
-
|
171
|
+
def error(msg, errors={})
|
172
|
+
{status: false, message: msg, errors: errors}
|
173
|
+
end
|
163
174
|
|
164
|
-
|
165
|
-
|
175
|
+
def path_for(v=nil, r=nil)
|
176
|
+
ret = '/'
|
166
177
|
|
167
|
-
|
168
|
-
|
178
|
+
ret += "v#{v}/" if v
|
179
|
+
ret += r if r
|
169
180
|
|
170
|
-
|
171
|
-
|
181
|
+
ret
|
182
|
+
end
|
172
183
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
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
|
-
|
181
|
-
|
182
|
-
|
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
|
-
|
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
|
-
|
9
|
-
|
10
|
-
|
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
|