davetron5000-gliffy 0.1.7 → 0.2.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.
@@ -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