davetron5000-gliffy 0.1.7 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,101 @@
1
+ require 'rubygems'
2
+ require 'httparty'
3
+ require 'logger'
4
+ require 'gliffy/url'
5
+
6
+ module Gliffy
7
+ # Handles making a request of the Gliffy server and all that that entails.
8
+ # This allows you to make requests using the "action" and the URL as
9
+ # described in the Gliffy documentation. For example, if you wish
10
+ # to get a user's folders, you could
11
+ #
12
+ # request = Request.get('https://www.gliffy.com/api/1.0',credentials)
13
+ # results = request.create('accounts/$account_id/users/$username/oauth_token.xml')
14
+ # credentials.update_access_token(
15
+ # results['response']['oauth_token_credentials']['oauth_token_secret'],
16
+ # results['response']['oauth_token_credentials']['oauth_token'])
17
+ # request.get('accounts/$account_id/users/$username/folders.xml')
18
+ #
19
+ # This will return a hash-referencable DOM objects, subbing the account id
20
+ # and username in when making the request (additionally, setting all needed
21
+ # parameters and signing the request).
22
+ class Request
23
+
24
+ attr_accessor :logger
25
+ # Modify the HTTP transport agent used. This should
26
+ # have the same interface as HTTParty.
27
+ attr_accessor :http
28
+
29
+ # Create a new request object.
30
+ #
31
+ # [+api_root+] the root of where all API calls are made
32
+ # [+credentials+] a Credentials object with all the current credentials
33
+ # [+http+] This should implement the HTTParty interface
34
+ def initialize(api_root,credentials,http=HTTParty,logger=nil)
35
+ @api_root = api_root
36
+ @api_root += '/' if !(@api_root =~ /\/$/)
37
+ @credentials = credentials
38
+ @logger = logger || Logger.new(STDOUT)
39
+ @logger.level = Logger::INFO
40
+ @http = http
41
+ end
42
+
43
+ # Implements getting a request and returning a response
44
+ # The implements methods that correspond to Gliffy's "action=" parameter.
45
+ # Based on this, it will know to do a GET or POST. The method signature is
46
+ #
47
+ # request.action_name(url,params)
48
+ # for example
49
+ # request.get('accounts/$account_id.xml',:showUsers => true)
50
+ # note that you can use `$account_id` and `$username` in any URL and it
51
+ # will be replaced accordingly.
52
+ #
53
+ # The return value is the return value from HTTParty, which is basically a hash
54
+ # that allows access to the returned DOM tree
55
+ def method_missing(symbol,*args)
56
+ if args.length >= 1
57
+ link_only = false
58
+ if symbol == :link_for
59
+ symbol = args.shift
60
+ link_only = true
61
+ end
62
+ @logger.debug("Executing a #{symbol} against gliffy for url #{args[0]}")
63
+
64
+ # exposing this for testing
65
+ protocol = determine_protocol(args[1])
66
+ @full_url_no_params = protocol + "://" + @api_root + replace_url(args[0])
67
+ url = SignedURL.new(@credentials,@full_url_no_params,symbol == :GET ? 'GET' : 'POST')
68
+ url.logger = @logger
69
+ url.params = args[1] if !args[1].nil?
70
+ url[:protocol_override] = nil
71
+ url[:action] = symbol
72
+
73
+ # These can be override for testing purposes
74
+ timestamp = args[2] if args[2]
75
+ nonce = args[3] if args[3]
76
+
77
+ full_url = url.full_url(timestamp,nonce)
78
+ if link_only
79
+ return full_url
80
+ else
81
+ response = @http.post(full_url)
82
+ return response
83
+ end
84
+ else
85
+ super(symbol,args)
86
+ end
87
+ end
88
+
89
+ def determine_protocol(params)
90
+ if params && params[:protocol_override]
91
+ params[:protocol_override].to_s
92
+ else
93
+ @credentials.default_protocol.to_s
94
+ end
95
+ end
96
+
97
+ def replace_url(url)
98
+ return url.gsub('$account_id',@credentials.account_id.to_s).gsub('$username',@credentials.username)
99
+ end
100
+ end
101
+ end
@@ -1,127 +1,284 @@
1
- require 'rexml/document'
2
- require 'array_has_response'
3
- require 'gliffy/rest'
4
-
5
- include REXML
1
+ require 'gliffy/request'
6
2
 
7
3
  module Gliffy
8
-
9
- # Base class for Gliffy response. This class is also the entry point
10
- # for parsing Gliffy XML:
11
- #
12
- # xml = get_xml_from_gliffy
13
- # response = self.from_xml(xml)
14
- # # response is now a Response or subclass of response
15
- # if response.success?
16
- # if response.not_modified?
17
- # # use the item(s) from your local cache
18
- # else
19
- # # it should be what you expect, e.g. Diagram, array of Users, etc.
20
- # end
21
- # else
22
- # puts response.message # in this case it's an Error
23
- # end
24
- #
25
- #
4
+ # Indicates no response at all was received.
5
+ class NoResponseException < Exception
6
+ def initialize(message)
7
+ super(message)
8
+ end
9
+ end
10
+ # Indicates that a response was received by that it wasn't
11
+ # parsable or readable as a Gliffy <response>
12
+ class BadResponseException < Exception
13
+ def initialize(message)
14
+ super(message)
15
+ end
16
+ end
17
+ # Indicates that a valid Gliffy <response> was received and that it
18
+ # indicated failure. The message is the message sent by gliffy (if it was
19
+ # in the response)
20
+ class RequestFailedException < Exception
21
+ def initialize(message)
22
+ super(message)
23
+ end
24
+ end
25
+ # Base class for all response from gliffy
26
26
  class Response
27
-
28
- # Creates a Response based on the XML passed in.
29
- # The xml can be anything passable to REXML::Document.new, such as
30
- # a Document, or a string containing XML
31
- #
32
- # This will return a Response, a subclass of Response, or an array of
33
- # Response/subclass of Response objects. In the case where an array is returned
34
- # both success? and not_modified? may be called, so the idiom in the class Rdoc should
35
- # be usable regardless of return value
36
- def self.from_xml(xml)
37
- raise ArgumentError.new("xml may not be null to #{to_s}.from_xml") if !xml
38
- root = Document.new(xml).root
39
- not_modified = root.attributes['not-modified'] == "true"
40
- success = root.attributes['success'] == "true"
41
-
42
- response = nil
43
- if ! root.elements.empty?
44
- klassname = to_classname(root.elements[1].name)
45
- klass = Gliffy.const_get(klassname)
46
- response = klass.from_xml(root.elements[1])
27
+ @@normal_error_callback = Proc.new do |response,exception|
28
+ if response
29
+ message = response.inspect
30
+ if response['response'] && response['response']['error']
31
+ http_status = response['response']['error']['http_status']
32
+ if http_status = "404"
33
+ message = "Not Found"
34
+ else
35
+ message = "HTTP Status #{http_status}"
36
+ end
37
+ end
38
+ raise exception.class.new(message)
47
39
  else
48
- response = Response.new
40
+ raise exception
49
41
  end
42
+ end
50
43
 
51
- response.success = success
52
- response.not_modified = not_modified
44
+ @@error_callback = @@normal_error_callback
53
45
 
54
- response
46
+ # Factory for creating actual response subclasses.
47
+ # This takes the results of HTTParty's response, which is a hash, essentially.
48
+ # This assumes that any checks for validity have been done.
49
+ # [error_response] Set this to a Proc to handle errors if you don't want the default behavior. The proc will get two arguments:
50
+ # [+response+] the raw response received (may be nil)
51
+ # [+exception+] One of NoResponseException, BadResponseException, or RequestFailedException. The message of that exception is a usable message if you want to ignore the exception
52
+ def self.from_http_response(response,error_callback=nil)
53
+ verify(response,error_callback)
54
+ root = response['response']
55
+ klass = nil
56
+ root.keys.each do |key|
57
+ klassname = to_classname(key)
58
+ begin
59
+ this_klass = Gliffy.const_get(klassname)
60
+ rescue NameError
61
+ this_klass = nil
62
+ end
63
+ klass = this_klass unless this_klass.nil?
64
+ end
65
+ return Response.new(response.body) if !klass
66
+ return klass.from_http_response(root)
55
67
  end
56
68
 
57
- # Returns true if the response represents a successful response
58
- # false indicates failure and that element is most likely a Error
59
- def success?; @success; end
60
-
61
- # Returns true if this response indicates the requested resource
62
- # was not modified, based on the headers provided with the request.
63
- def not_modified?; @not_modified; end
69
+ # Verifies that the response represents success, calling
70
+ # the error callback if it doesn't
71
+ def self.verify(response,error_callback)
72
+ error_callback = @@error_callback if error_callback.nil?
73
+ return error_callback.call(response,NoResponseException.new('No response received at all')) if response.nil?
74
+ return error_callback.call(response,BadResponseException.new('Not a Gliffy response')) if !response['response']
75
+ return error_callback.call(response,BadResponseException.new('No indication of success from Gliffy')) if !response['response']['success']
76
+ if response['response']['success'] != 'true'
77
+ error = response['response']['error']
78
+ return error_callback.call(response,RequestFailedException.new('Request failed but no error inside response')) if !error
79
+ return error_callback.call(response,RequestFailedException.new(error))
80
+ end
81
+ end
64
82
 
65
- def success=(s); @success = s; end
66
- def not_modified=(s); @not_modified = s; end
67
83
 
68
- # Provides access to the rest implementation
69
- attr_accessor :rest
84
+ attr_reader :body
70
85
 
71
- protected
86
+ def initialize(params)
87
+ @params = params
88
+ end
72
89
 
73
- def initialize
74
- @success = true
75
- @not_modified = false
90
+ # Implements access to the object information.
91
+ # Parameters should be typed appropriately.
92
+ # The names are those as defined by the Gliffy XSD, save for dashes
93
+ # are replaced with underscores.
94
+ def method_missing(symbol,*args)
95
+ if args.length == 0
96
+ @params[symbol]
97
+ else
98
+ super(symbol,args)
99
+ end
76
100
  end
77
101
 
78
- # Converts a dash-delimited string to a camel-cased classname
102
+ private
79
103
  def self.to_classname(name)
80
104
  classname = ""
81
- name.split(/-/).each do |part|
105
+ name.split(/[-_]/).each do |part|
82
106
  classname += part.capitalize
83
107
  end
84
- classname
108
+ classname + "Parser"
85
109
  end
86
110
  end
87
111
 
88
- class ArrayResponseParser
89
- def self.from_xml(element)
90
- single_classname = self.to_s.gsub(/s$/,'').gsub(/Gliffy::/,'')
91
- klass = Gliffy.const_get(single_classname)
92
- list = Array.new
93
- if (element)
94
- element.each_element do |element|
95
- list << klass.from_xml(element)
112
+ class ArrayParser # :nodoc:
113
+ def self.from_http_response(root,single_class,plural_name,single_name)
114
+ root = root[plural_name]
115
+ return nil if root.nil?
116
+ if root[single_name].kind_of?(Array)
117
+ list = Array.new
118
+ root[single_name].each do |item|
119
+ list << single_class.from_http_response(item)
96
120
  end
121
+ list
122
+ else
123
+ [single_class.from_http_response(root[single_name])]
97
124
  end
98
- list
99
125
  end
100
126
  end
101
127
 
102
- # An error from Gliffy
103
- class Error < Response
104
- # The HTTP status code that can help indicate the nature of the problem
105
- attr_reader :http_status
106
- # A description of the error that occured; not necessarily for human
107
- # consumption
108
- attr_reader :message
128
+ # Factory for parsing accounts
129
+ class AccountsParser # :nodoc:
130
+ def self.from_http_response(root)
131
+ return ArrayParser.from_http_response(root,AccountParser,'accounts','account')
132
+ end
133
+ end
109
134
 
110
- def self.from_xml(element)
111
- message = element.text
112
- http_status = element.attributes['http-status'].to_i
113
- Error.new(message,http_status)
135
+ # Factory for parsing folders
136
+ class FoldersParser # :nodoc:
137
+ def self.from_http_response(root)
138
+ return ArrayParser.from_http_response(root,FolderParser,'folders','folder')
114
139
  end
140
+ end
115
141
 
116
- def initialize(message,http_status)
117
- super()
118
- @message = message
119
- @http_status = http_status
142
+ # Factory for parsing versions
143
+ class VersionsParser # :nodoc:
144
+ def self.from_http_response(root)
145
+ return ArrayParser.from_http_response(root,VersionParser,'versions','version')
120
146
  end
147
+ end
121
148
 
122
- def to_s
123
- "#{@http_status}: #{@message}"
149
+ # Factory for parsing users
150
+ class UsersParser # :nodoc:
151
+ def self.from_http_response(root)
152
+ return ArrayParser.from_http_response(root,UserParser,'users','user')
124
153
  end
154
+ end
125
155
 
156
+ # Factory for parsing documents
157
+ class DocumentsParser # :nodoc:
158
+ def self.from_http_response(root)
159
+ return ArrayParser.from_http_response(root,DocumentParser,'documents','document')
160
+ end
126
161
  end
162
+
163
+ class BaseParser # :nodoc:
164
+ def self.from_http_response(root)
165
+ params = Hash.new
166
+ root.each do |key,value|
167
+ params[key.to_sym] = value
168
+ end
169
+ Response.new(params)
170
+ end
171
+
172
+ # Returns the item as an array, or nil if it was nil
173
+ def self.as_array(item)
174
+ if item.nil?
175
+ nil
176
+ else
177
+ [item].flatten
178
+ end
179
+ end
180
+
181
+ def self.add_int(root,name,new_name=nil)
182
+ if root[name]
183
+ root[new_name.nil? ? name : new_name] = root[name].to_i
184
+ end
185
+ end
186
+
187
+ def self.add_boolean(root,name)
188
+ root[name + "?"] = root[name] == 'true'
189
+ end
190
+
191
+ def self.add_date(root,name)
192
+ if root[name]
193
+ root[name] = Time.at(root[name].to_i / 1000) unless root[name].kind_of? Time
194
+ end
195
+ end
196
+ end
197
+
198
+ class FolderParser < BaseParser # :nodoc:
199
+ def self.from_http_response(root)
200
+ add_int(root,'id','folder_id')
201
+ add_boolean(root,'is_default')
202
+ if root['folder']
203
+ if root['folder'].kind_of? Array
204
+ root['child_folders'] = Array.new
205
+ root['folder'].each do |one|
206
+ root['child_folders'] << from_http_response(one)
207
+ end
208
+ else
209
+ root['child_folders'] = [from_http_response(root['folder'])]
210
+ end
211
+ else
212
+ root['child_folders'] = Array.new
213
+ end
214
+ super(root)
215
+ end
216
+ end
217
+
218
+ class UserParser < BaseParser # :nodoc:
219
+ def self.from_http_response(root)
220
+ add_int(root,'id','user_id')
221
+ add_boolean(root,'is_admin')
222
+ super(root)
223
+ end
224
+ end
225
+
226
+ # Parses the results from the test account request
227
+ class TestaccountParser < BaseParser # :nodoc:
228
+ def self.from_http_response(root)
229
+ root = root['testAccount']
230
+ add_int(root,'id','account_id')
231
+ add_int(root,'max_users')
232
+ add_boolean(root,'terms')
233
+ add_date(root,'expiration_date')
234
+ super(root)
235
+ end
236
+ end
237
+
238
+ # Factory for parsing an Account
239
+ class AccountParser < BaseParser # :nodoc:
240
+ def self.from_http_response(root)
241
+ add_int(root,'id','account_id')
242
+ add_int(root,'max_users')
243
+ add_boolean(root,'terms')
244
+ add_date(root,'expiration_date')
245
+ root['users'] = as_array(UsersParser.from_http_response(root))
246
+ super(root)
247
+ end
248
+ end
249
+
250
+ # Factory for parsing an Account
251
+ class DocumentParser < BaseParser # :nodoc:
252
+ def self.from_http_response(root)
253
+ add_int(root,'id','document_id')
254
+ add_int(root,'num_versions')
255
+ add_boolean(root,'is_private')
256
+ add_boolean(root,'is_public')
257
+ add_date(root,'create_date')
258
+ add_date(root,'mod_date')
259
+ add_date(root,'published_date')
260
+ root['owner'] = UserParser.from_http_response(root['owner'])
261
+ root['versions'] = as_array(VersionsParser.from_http_response(root))
262
+ super(root)
263
+ end
264
+ end
265
+
266
+ class VersionParser < BaseParser # :nodoc:
267
+ def self.from_http_response(root)
268
+ add_int(root,'id','version_id')
269
+ add_int(root,'num')
270
+ add_date(root,'create_date')
271
+ root['owner'] = UserParser.from_http_response(root['owner'])
272
+ super(root)
273
+ end
274
+ end
275
+
276
+ class OauthTokenCredentialsParser # :nodoc:
277
+ def self.from_http_response(root)
278
+ token = root['oauth_token_credentials']['oauth_token']
279
+ secret = root['oauth_token_credentials']['oauth_token_secret']
280
+ return AccessToken.new(token,secret)
281
+ end
282
+ end
283
+
127
284
  end